##// END OF EJS Templates
Fix file-changed-to-dir and dir-to-file commits (issue660)....
Maxim Dounin -
r5487:7a64931e default
parent child Browse files
Show More
@@ -0,0 +1,89 b''
1 #!/bin/sh
2 # http://www.selenic.com/mercurial/bts/issue660
3
4
5 hg init a
6 cd a
7 echo a > a
8 mkdir b
9 echo b > b/b
10 hg commit -A -m "a is file, b is dir"
11
12 echo % file replaced with directory
13
14 rm a
15 mkdir a
16 echo a > a/a
17
18 echo % should fail - would corrupt dirstate
19 hg add a/a
20
21 echo % removing shadow
22 hg rm --after a
23
24 echo % should succeed - shadow removed
25 hg add a/a
26
27 echo % directory replaced with file
28
29 rm -r b
30 echo b > b
31
32 echo % should fail - would corrupt dirstate
33 hg add b
34
35 echo % removing shadow
36 hg rm --after b/b
37
38 echo % should succeed - shadow removed
39 hg add b
40
41 echo % look what we got
42 hg st
43
44 echo % revert reintroducing shadow - should fail
45 rm -r a b
46 hg revert b/b
47
48 echo % revert all - should succeed
49 hg revert --all
50 hg st
51
52 echo % addremove
53
54 rm -r a b
55 mkdir a
56 echo a > a/a
57 echo b > b
58
59 hg addremove
60 hg st
61
62 echo % commit
63 hg ci -A -m "a is dir, b is file"
64 hg st --all
65
66 echo % long directory replaced with file
67
68 mkdir d
69 mkdir d/d
70 echo d > d/d/d
71 hg commit -A -m "d is long directory"
72 rm -r d
73 echo d > d
74
75 echo % should fail - would corrupt dirstate
76 hg add d
77
78 echo % removing shadow
79 hg rm --after d/d/d
80
81 echo % should succeed - shadow removed
82 hg add d
83
84 #echo % update should work
85 #
86 #hg up -r 0
87 #hg up -r 1
88
89 exit 0
@@ -0,0 +1,42 b''
1 adding a
2 adding b/b
3 % file replaced with directory
4 % should fail - would corrupt dirstate
5 abort: file 'a' in dirstate clashes with 'a/a'
6 % removing shadow
7 % should succeed - shadow removed
8 % directory replaced with file
9 % should fail - would corrupt dirstate
10 abort: directory 'b' already in dirstate
11 % removing shadow
12 % should succeed - shadow removed
13 % look what we got
14 A a/a
15 A b
16 R a
17 R b/b
18 % revert reintroducing shadow - should fail
19 abort: file 'b' in dirstate clashes with 'b/b'
20 % revert all - should succeed
21 undeleting a
22 forgetting a/a
23 forgetting b
24 undeleting b/b
25 % addremove
26 removing a
27 adding a/a
28 adding b
29 removing b/b
30 A a/a
31 A b
32 R a
33 R b/b
34 % commit
35 C a/a
36 C b
37 % long directory replaced with file
38 adding d/d/d
39 % should fail - would corrupt dirstate
40 abort: directory 'd' already in dirstate
41 % removing shadow
42 % should succeed - shadow removed
@@ -1,943 +1,944 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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import *
9 9 from i18n import _
10 10 import os, sys, bisect, stat
11 11 import mdiff, bdiff, util, templater, patch
12 12
13 13 revrangesep = ':'
14 14
15 15 class UnknownCommand(Exception):
16 16 """Exception raised if command is not in the command table."""
17 17 class AmbiguousCommand(Exception):
18 18 """Exception raised if command shortcut matches more than one command."""
19 19
20 20 def findpossible(ui, cmd, table):
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 = e.lstrip("^").split("|")
30 30 found = None
31 31 if cmd in aliases:
32 32 found = cmd
33 33 elif not ui.config("ui", "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(ui, cmd, table):
50 50 """Return (aliases, command table entry) for command string."""
51 51 choice = findpossible(ui, cmd, table)
52 52
53 53 if choice.has_key(cmd):
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 AmbiguousCommand(cmd, clist)
60 60
61 61 if choice:
62 62 return choice.values()[0]
63 63
64 64 raise UnknownCommand(cmd)
65 65
66 66 def bail_if_changed(repo):
67 67 modified, added, removed, deleted = repo.status()[:4]
68 68 if modified or added or removed or deleted:
69 69 raise util.Abort(_("outstanding uncommitted changes"))
70 70
71 71 def logmessage(opts):
72 72 """ get the log message according to -m and -l option """
73 73 message = opts['message']
74 74 logfile = opts['logfile']
75 75
76 76 if message and logfile:
77 77 raise util.Abort(_('options --message and --logfile are mutually '
78 78 'exclusive'))
79 79 if not message and logfile:
80 80 try:
81 81 if logfile == '-':
82 82 message = sys.stdin.read()
83 83 else:
84 84 message = open(logfile).read()
85 85 except IOError, inst:
86 86 raise util.Abort(_("can't read commit message '%s': %s") %
87 87 (logfile, inst.strerror))
88 88 return message
89 89
90 90 def setremoteconfig(ui, opts):
91 91 "copy remote options to ui tree"
92 92 if opts.get('ssh'):
93 93 ui.setconfig("ui", "ssh", opts['ssh'])
94 94 if opts.get('remotecmd'):
95 95 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
96 96
97 97 def revpair(repo, revs):
98 98 '''return pair of nodes, given list of revisions. second item can
99 99 be None, meaning use working dir.'''
100 100
101 101 def revfix(repo, val, defval):
102 102 if not val and val != 0 and defval is not None:
103 103 val = defval
104 104 return repo.lookup(val)
105 105
106 106 if not revs:
107 107 return repo.dirstate.parents()[0], None
108 108 end = None
109 109 if len(revs) == 1:
110 110 if revrangesep in revs[0]:
111 111 start, end = revs[0].split(revrangesep, 1)
112 112 start = revfix(repo, start, 0)
113 113 end = revfix(repo, end, repo.changelog.count() - 1)
114 114 else:
115 115 start = revfix(repo, revs[0], None)
116 116 elif len(revs) == 2:
117 117 if revrangesep in revs[0] or revrangesep in revs[1]:
118 118 raise util.Abort(_('too many revisions specified'))
119 119 start = revfix(repo, revs[0], None)
120 120 end = revfix(repo, revs[1], None)
121 121 else:
122 122 raise util.Abort(_('too many revisions specified'))
123 123 return start, end
124 124
125 125 def revrange(repo, revs):
126 126 """Yield revision as strings from a list of revision specifications."""
127 127
128 128 def revfix(repo, val, defval):
129 129 if not val and val != 0 and defval is not None:
130 130 return defval
131 131 return repo.changelog.rev(repo.lookup(val))
132 132
133 133 seen, l = {}, []
134 134 for spec in revs:
135 135 if revrangesep in spec:
136 136 start, end = spec.split(revrangesep, 1)
137 137 start = revfix(repo, start, 0)
138 138 end = revfix(repo, end, repo.changelog.count() - 1)
139 139 step = start > end and -1 or 1
140 140 for rev in xrange(start, end+step, step):
141 141 if rev in seen:
142 142 continue
143 143 seen[rev] = 1
144 144 l.append(rev)
145 145 else:
146 146 rev = revfix(repo, spec, None)
147 147 if rev in seen:
148 148 continue
149 149 seen[rev] = 1
150 150 l.append(rev)
151 151
152 152 return l
153 153
154 154 def make_filename(repo, pat, node,
155 155 total=None, seqno=None, revwidth=None, pathname=None):
156 156 node_expander = {
157 157 'H': lambda: hex(node),
158 158 'R': lambda: str(repo.changelog.rev(node)),
159 159 'h': lambda: short(node),
160 160 }
161 161 expander = {
162 162 '%': lambda: '%',
163 163 'b': lambda: os.path.basename(repo.root),
164 164 }
165 165
166 166 try:
167 167 if node:
168 168 expander.update(node_expander)
169 169 if node:
170 170 expander['r'] = (lambda:
171 171 str(repo.changelog.rev(node)).zfill(revwidth or 0))
172 172 if total is not None:
173 173 expander['N'] = lambda: str(total)
174 174 if seqno is not None:
175 175 expander['n'] = lambda: str(seqno)
176 176 if total is not None and seqno is not None:
177 177 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
178 178 if pathname is not None:
179 179 expander['s'] = lambda: os.path.basename(pathname)
180 180 expander['d'] = lambda: os.path.dirname(pathname) or '.'
181 181 expander['p'] = lambda: pathname
182 182
183 183 newname = []
184 184 patlen = len(pat)
185 185 i = 0
186 186 while i < patlen:
187 187 c = pat[i]
188 188 if c == '%':
189 189 i += 1
190 190 c = pat[i]
191 191 c = expander[c]()
192 192 newname.append(c)
193 193 i += 1
194 194 return ''.join(newname)
195 195 except KeyError, inst:
196 196 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
197 197 inst.args[0])
198 198
199 199 def make_file(repo, pat, node=None,
200 200 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
201 201 if not pat or pat == '-':
202 202 return 'w' in mode and sys.stdout or sys.stdin
203 203 if hasattr(pat, 'write') and 'w' in mode:
204 204 return pat
205 205 if hasattr(pat, 'read') and 'r' in mode:
206 206 return pat
207 207 return open(make_filename(repo, pat, node, total, seqno, revwidth,
208 208 pathname),
209 209 mode)
210 210
211 211 def matchpats(repo, pats=[], opts={}, globbed=False, default=None):
212 212 cwd = repo.getcwd()
213 213 return util.cmdmatcher(repo.root, cwd, pats or [], opts.get('include'),
214 214 opts.get('exclude'), globbed=globbed,
215 215 default=default)
216 216
217 217 def walk(repo, pats=[], opts={}, node=None, badmatch=None, globbed=False,
218 218 default=None):
219 219 files, matchfn, anypats = matchpats(repo, pats, opts, globbed=globbed,
220 220 default=default)
221 221 exact = dict.fromkeys(files)
222 222 cwd = repo.getcwd()
223 223 for src, fn in repo.walk(node=node, files=files, match=matchfn,
224 224 badmatch=badmatch):
225 225 yield src, fn, repo.pathto(fn, cwd), fn in exact
226 226
227 227 def findrenames(repo, added=None, removed=None, threshold=0.5):
228 228 '''find renamed files -- yields (before, after, score) tuples'''
229 229 if added is None or removed is None:
230 230 added, removed = repo.status()[1:3]
231 231 ctx = repo.changectx()
232 232 for a in added:
233 233 aa = repo.wread(a)
234 234 bestname, bestscore = None, threshold
235 235 for r in removed:
236 236 rr = ctx.filectx(r).data()
237 237
238 238 # bdiff.blocks() returns blocks of matching lines
239 239 # count the number of bytes in each
240 240 equal = 0
241 241 alines = mdiff.splitnewlines(aa)
242 242 matches = bdiff.blocks(aa, rr)
243 243 for x1,x2,y1,y2 in matches:
244 244 for line in alines[x1:x2]:
245 245 equal += len(line)
246 246
247 247 lengths = len(aa) + len(rr)
248 248 if lengths:
249 249 myscore = equal*2.0 / lengths
250 250 if myscore >= bestscore:
251 251 bestname, bestscore = r, myscore
252 252 if bestname:
253 253 yield bestname, a, bestscore
254 254
255 255 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
256 256 if dry_run is None:
257 257 dry_run = opts.get('dry_run')
258 258 if similarity is None:
259 259 similarity = float(opts.get('similarity') or 0)
260 260 add, remove = [], []
261 261 mapping = {}
262 262 for src, abs, rel, exact in walk(repo, pats, opts):
263 263 target = repo.wjoin(abs)
264 264 if src == 'f' and abs not in repo.dirstate:
265 265 add.append(abs)
266 266 mapping[abs] = rel, exact
267 267 if repo.ui.verbose or not exact:
268 268 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
269 if repo.dirstate[abs] != 'r' and not util.lexists(target):
269 if repo.dirstate[abs] != 'r' and (not util.lexists(target)
270 or (os.path.isdir(target) and not os.path.islink(target))):
270 271 remove.append(abs)
271 272 mapping[abs] = rel, exact
272 273 if repo.ui.verbose or not exact:
273 274 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
274 275 if not dry_run:
276 repo.remove(remove)
275 277 repo.add(add)
276 repo.remove(remove)
277 278 if similarity > 0:
278 279 for old, new, score in findrenames(repo, add, remove, similarity):
279 280 oldrel, oldexact = mapping[old]
280 281 newrel, newexact = mapping[new]
281 282 if repo.ui.verbose or not oldexact or not newexact:
282 283 repo.ui.status(_('recording removal of %s as rename to %s '
283 284 '(%d%% similar)\n') %
284 285 (oldrel, newrel, score * 100))
285 286 if not dry_run:
286 287 repo.copy(old, new)
287 288
288 289 def service(opts, parentfn=None, initfn=None, runfn=None):
289 290 '''Run a command as a service.'''
290 291
291 292 if opts['daemon'] and not opts['daemon_pipefds']:
292 293 rfd, wfd = os.pipe()
293 294 args = sys.argv[:]
294 295 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
295 296 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
296 297 args[0], args)
297 298 os.close(wfd)
298 299 os.read(rfd, 1)
299 300 if parentfn:
300 301 return parentfn(pid)
301 302 else:
302 303 os._exit(0)
303 304
304 305 if initfn:
305 306 initfn()
306 307
307 308 if opts['pid_file']:
308 309 fp = open(opts['pid_file'], 'w')
309 310 fp.write(str(os.getpid()) + '\n')
310 311 fp.close()
311 312
312 313 if opts['daemon_pipefds']:
313 314 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
314 315 os.close(rfd)
315 316 try:
316 317 os.setsid()
317 318 except AttributeError:
318 319 pass
319 320 os.write(wfd, 'y')
320 321 os.close(wfd)
321 322 sys.stdout.flush()
322 323 sys.stderr.flush()
323 324 fd = os.open(util.nulldev, os.O_RDWR)
324 325 if fd != 0: os.dup2(fd, 0)
325 326 if fd != 1: os.dup2(fd, 1)
326 327 if fd != 2: os.dup2(fd, 2)
327 328 if fd not in (0, 1, 2): os.close(fd)
328 329
329 330 if runfn:
330 331 return runfn()
331 332
332 333 class changeset_printer(object):
333 334 '''show changeset information when templating not requested.'''
334 335
335 336 def __init__(self, ui, repo, patch, buffered):
336 337 self.ui = ui
337 338 self.repo = repo
338 339 self.buffered = buffered
339 340 self.patch = patch
340 341 self.header = {}
341 342 self.hunk = {}
342 343 self.lastheader = None
343 344
344 345 def flush(self, rev):
345 346 if rev in self.header:
346 347 h = self.header[rev]
347 348 if h != self.lastheader:
348 349 self.lastheader = h
349 350 self.ui.write(h)
350 351 del self.header[rev]
351 352 if rev in self.hunk:
352 353 self.ui.write(self.hunk[rev])
353 354 del self.hunk[rev]
354 355 return 1
355 356 return 0
356 357
357 358 def show(self, rev=0, changenode=None, copies=(), **props):
358 359 if self.buffered:
359 360 self.ui.pushbuffer()
360 361 self._show(rev, changenode, copies, props)
361 362 self.hunk[rev] = self.ui.popbuffer()
362 363 else:
363 364 self._show(rev, changenode, copies, props)
364 365
365 366 def _show(self, rev, changenode, copies, props):
366 367 '''show a single changeset or file revision'''
367 368 log = self.repo.changelog
368 369 if changenode is None:
369 370 changenode = log.node(rev)
370 371 elif not rev:
371 372 rev = log.rev(changenode)
372 373
373 374 if self.ui.quiet:
374 375 self.ui.write("%d:%s\n" % (rev, short(changenode)))
375 376 return
376 377
377 378 changes = log.read(changenode)
378 379 date = util.datestr(changes[2])
379 380 extra = changes[5]
380 381 branch = extra.get("branch")
381 382
382 383 hexfunc = self.ui.debugflag and hex or short
383 384
384 385 parents = [(p, hexfunc(log.node(p)))
385 386 for p in self._meaningful_parentrevs(log, rev)]
386 387
387 388 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
388 389
389 390 # don't show the default branch name
390 391 if branch != 'default':
391 392 branch = util.tolocal(branch)
392 393 self.ui.write(_("branch: %s\n") % branch)
393 394 for tag in self.repo.nodetags(changenode):
394 395 self.ui.write(_("tag: %s\n") % tag)
395 396 for parent in parents:
396 397 self.ui.write(_("parent: %d:%s\n") % parent)
397 398
398 399 if self.ui.debugflag:
399 400 self.ui.write(_("manifest: %d:%s\n") %
400 401 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
401 402 self.ui.write(_("user: %s\n") % changes[1])
402 403 self.ui.write(_("date: %s\n") % date)
403 404
404 405 if self.ui.debugflag:
405 406 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
406 407 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
407 408 files):
408 409 if value:
409 410 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
410 411 elif changes[3] and self.ui.verbose:
411 412 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
412 413 if copies and self.ui.verbose:
413 414 copies = ['%s (%s)' % c for c in copies]
414 415 self.ui.write(_("copies: %s\n") % ' '.join(copies))
415 416
416 417 if extra and self.ui.debugflag:
417 418 extraitems = extra.items()
418 419 extraitems.sort()
419 420 for key, value in extraitems:
420 421 self.ui.write(_("extra: %s=%s\n")
421 422 % (key, value.encode('string_escape')))
422 423
423 424 description = changes[4].strip()
424 425 if description:
425 426 if self.ui.verbose:
426 427 self.ui.write(_("description:\n"))
427 428 self.ui.write(description)
428 429 self.ui.write("\n\n")
429 430 else:
430 431 self.ui.write(_("summary: %s\n") %
431 432 description.splitlines()[0])
432 433 self.ui.write("\n")
433 434
434 435 self.showpatch(changenode)
435 436
436 437 def showpatch(self, node):
437 438 if self.patch:
438 439 prev = self.repo.changelog.parents(node)[0]
439 440 patch.diff(self.repo, prev, node, match=self.patch, fp=self.ui,
440 441 opts=patch.diffopts(self.ui))
441 442 self.ui.write("\n")
442 443
443 444 def _meaningful_parentrevs(self, log, rev):
444 445 """Return list of meaningful (or all if debug) parentrevs for rev.
445 446
446 447 For merges (two non-nullrev revisions) both parents are meaningful.
447 448 Otherwise the first parent revision is considered meaningful if it
448 449 is not the preceding revision.
449 450 """
450 451 parents = log.parentrevs(rev)
451 452 if not self.ui.debugflag and parents[1] == nullrev:
452 453 if parents[0] >= rev - 1:
453 454 parents = []
454 455 else:
455 456 parents = [parents[0]]
456 457 return parents
457 458
458 459
459 460 class changeset_templater(changeset_printer):
460 461 '''format changeset information.'''
461 462
462 463 def __init__(self, ui, repo, patch, mapfile, buffered):
463 464 changeset_printer.__init__(self, ui, repo, patch, buffered)
464 465 filters = templater.common_filters.copy()
465 466 filters['formatnode'] = (ui.debugflag and (lambda x: x)
466 467 or (lambda x: x[:12]))
467 468 self.t = templater.templater(mapfile, filters,
468 469 cache={
469 470 'parent': '{rev}:{node|formatnode} ',
470 471 'manifest': '{rev}:{node|formatnode}',
471 472 'filecopy': '{name} ({source})'})
472 473
473 474 def use_template(self, t):
474 475 '''set template string to use'''
475 476 self.t.cache['changeset'] = t
476 477
477 478 def _show(self, rev, changenode, copies, props):
478 479 '''show a single changeset or file revision'''
479 480 log = self.repo.changelog
480 481 if changenode is None:
481 482 changenode = log.node(rev)
482 483 elif not rev:
483 484 rev = log.rev(changenode)
484 485
485 486 changes = log.read(changenode)
486 487
487 488 def showlist(name, values, plural=None, **args):
488 489 '''expand set of values.
489 490 name is name of key in template map.
490 491 values is list of strings or dicts.
491 492 plural is plural of name, if not simply name + 's'.
492 493
493 494 expansion works like this, given name 'foo'.
494 495
495 496 if values is empty, expand 'no_foos'.
496 497
497 498 if 'foo' not in template map, return values as a string,
498 499 joined by space.
499 500
500 501 expand 'start_foos'.
501 502
502 503 for each value, expand 'foo'. if 'last_foo' in template
503 504 map, expand it instead of 'foo' for last key.
504 505
505 506 expand 'end_foos'.
506 507 '''
507 508 if plural: names = plural
508 509 else: names = name + 's'
509 510 if not values:
510 511 noname = 'no_' + names
511 512 if noname in self.t:
512 513 yield self.t(noname, **args)
513 514 return
514 515 if name not in self.t:
515 516 if isinstance(values[0], str):
516 517 yield ' '.join(values)
517 518 else:
518 519 for v in values:
519 520 yield dict(v, **args)
520 521 return
521 522 startname = 'start_' + names
522 523 if startname in self.t:
523 524 yield self.t(startname, **args)
524 525 vargs = args.copy()
525 526 def one(v, tag=name):
526 527 try:
527 528 vargs.update(v)
528 529 except (AttributeError, ValueError):
529 530 try:
530 531 for a, b in v:
531 532 vargs[a] = b
532 533 except ValueError:
533 534 vargs[name] = v
534 535 return self.t(tag, **vargs)
535 536 lastname = 'last_' + name
536 537 if lastname in self.t:
537 538 last = values.pop()
538 539 else:
539 540 last = None
540 541 for v in values:
541 542 yield one(v)
542 543 if last is not None:
543 544 yield one(last, tag=lastname)
544 545 endname = 'end_' + names
545 546 if endname in self.t:
546 547 yield self.t(endname, **args)
547 548
548 549 def showbranches(**args):
549 550 branch = changes[5].get("branch")
550 551 if branch != 'default':
551 552 branch = util.tolocal(branch)
552 553 return showlist('branch', [branch], plural='branches', **args)
553 554
554 555 def showparents(**args):
555 556 parents = [[('rev', p), ('node', hex(log.node(p)))]
556 557 for p in self._meaningful_parentrevs(log, rev)]
557 558 return showlist('parent', parents, **args)
558 559
559 560 def showtags(**args):
560 561 return showlist('tag', self.repo.nodetags(changenode), **args)
561 562
562 563 def showextras(**args):
563 564 extras = changes[5].items()
564 565 extras.sort()
565 566 for key, value in extras:
566 567 args = args.copy()
567 568 args.update(dict(key=key, value=value))
568 569 yield self.t('extra', **args)
569 570
570 571 def showcopies(**args):
571 572 c = [{'name': x[0], 'source': x[1]} for x in copies]
572 573 return showlist('file_copy', c, plural='file_copies', **args)
573 574
574 575 if self.ui.debugflag:
575 576 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
576 577 def showfiles(**args):
577 578 return showlist('file', files[0], **args)
578 579 def showadds(**args):
579 580 return showlist('file_add', files[1], **args)
580 581 def showdels(**args):
581 582 return showlist('file_del', files[2], **args)
582 583 def showmanifest(**args):
583 584 args = args.copy()
584 585 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
585 586 node=hex(changes[0])))
586 587 return self.t('manifest', **args)
587 588 else:
588 589 def showfiles(**args):
589 590 return showlist('file', changes[3], **args)
590 591 showadds = ''
591 592 showdels = ''
592 593 showmanifest = ''
593 594
594 595 defprops = {
595 596 'author': changes[1],
596 597 'branches': showbranches,
597 598 'date': changes[2],
598 599 'desc': changes[4].strip(),
599 600 'file_adds': showadds,
600 601 'file_dels': showdels,
601 602 'files': showfiles,
602 603 'file_copies': showcopies,
603 604 'manifest': showmanifest,
604 605 'node': hex(changenode),
605 606 'parents': showparents,
606 607 'rev': rev,
607 608 'tags': showtags,
608 609 'extras': showextras,
609 610 }
610 611 props = props.copy()
611 612 props.update(defprops)
612 613
613 614 try:
614 615 if self.ui.debugflag and 'header_debug' in self.t:
615 616 key = 'header_debug'
616 617 elif self.ui.quiet and 'header_quiet' in self.t:
617 618 key = 'header_quiet'
618 619 elif self.ui.verbose and 'header_verbose' in self.t:
619 620 key = 'header_verbose'
620 621 elif 'header' in self.t:
621 622 key = 'header'
622 623 else:
623 624 key = ''
624 625 if key:
625 626 h = templater.stringify(self.t(key, **props))
626 627 if self.buffered:
627 628 self.header[rev] = h
628 629 else:
629 630 self.ui.write(h)
630 631 if self.ui.debugflag and 'changeset_debug' in self.t:
631 632 key = 'changeset_debug'
632 633 elif self.ui.quiet and 'changeset_quiet' in self.t:
633 634 key = 'changeset_quiet'
634 635 elif self.ui.verbose and 'changeset_verbose' in self.t:
635 636 key = 'changeset_verbose'
636 637 else:
637 638 key = 'changeset'
638 639 self.ui.write(templater.stringify(self.t(key, **props)))
639 640 self.showpatch(changenode)
640 641 except KeyError, inst:
641 642 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
642 643 inst.args[0]))
643 644 except SyntaxError, inst:
644 645 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
645 646
646 647 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
647 648 """show one changeset using template or regular display.
648 649
649 650 Display format will be the first non-empty hit of:
650 651 1. option 'template'
651 652 2. option 'style'
652 653 3. [ui] setting 'logtemplate'
653 654 4. [ui] setting 'style'
654 655 If all of these values are either the unset or the empty string,
655 656 regular display via changeset_printer() is done.
656 657 """
657 658 # options
658 659 patch = False
659 660 if opts.get('patch'):
660 661 patch = matchfn or util.always
661 662
662 663 tmpl = opts.get('template')
663 664 mapfile = None
664 665 if tmpl:
665 666 tmpl = templater.parsestring(tmpl, quoted=False)
666 667 else:
667 668 mapfile = opts.get('style')
668 669 # ui settings
669 670 if not mapfile:
670 671 tmpl = ui.config('ui', 'logtemplate')
671 672 if tmpl:
672 673 tmpl = templater.parsestring(tmpl)
673 674 else:
674 675 mapfile = ui.config('ui', 'style')
675 676
676 677 if tmpl or mapfile:
677 678 if mapfile:
678 679 if not os.path.split(mapfile)[0]:
679 680 mapname = (templater.templatepath('map-cmdline.' + mapfile)
680 681 or templater.templatepath(mapfile))
681 682 if mapname: mapfile = mapname
682 683 try:
683 684 t = changeset_templater(ui, repo, patch, mapfile, buffered)
684 685 except SyntaxError, inst:
685 686 raise util.Abort(inst.args[0])
686 687 if tmpl: t.use_template(tmpl)
687 688 return t
688 689 return changeset_printer(ui, repo, patch, buffered)
689 690
690 691 def finddate(ui, repo, date):
691 692 """Find the tipmost changeset that matches the given date spec"""
692 693 df = util.matchdate(date + " to " + date)
693 694 get = util.cachefunc(lambda r: repo.changectx(r).changeset())
694 695 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
695 696 results = {}
696 697 for st, rev, fns in changeiter:
697 698 if st == 'add':
698 699 d = get(rev)[2]
699 700 if df(d[0]):
700 701 results[rev] = d
701 702 elif st == 'iter':
702 703 if rev in results:
703 704 ui.status("Found revision %s from %s\n" %
704 705 (rev, util.datestr(results[rev])))
705 706 return str(rev)
706 707
707 708 raise util.Abort(_("revision matching date not found"))
708 709
709 710 def walkchangerevs(ui, repo, pats, change, opts):
710 711 '''Iterate over files and the revs they changed in.
711 712
712 713 Callers most commonly need to iterate backwards over the history
713 714 it is interested in. Doing so has awful (quadratic-looking)
714 715 performance, so we use iterators in a "windowed" way.
715 716
716 717 We walk a window of revisions in the desired order. Within the
717 718 window, we first walk forwards to gather data, then in the desired
718 719 order (usually backwards) to display it.
719 720
720 721 This function returns an (iterator, matchfn) tuple. The iterator
721 722 yields 3-tuples. They will be of one of the following forms:
722 723
723 724 "window", incrementing, lastrev: stepping through a window,
724 725 positive if walking forwards through revs, last rev in the
725 726 sequence iterated over - use to reset state for the current window
726 727
727 728 "add", rev, fns: out-of-order traversal of the given file names
728 729 fns, which changed during revision rev - use to gather data for
729 730 possible display
730 731
731 732 "iter", rev, None: in-order traversal of the revs earlier iterated
732 733 over with "add" - use to display data'''
733 734
734 735 def increasing_windows(start, end, windowsize=8, sizelimit=512):
735 736 if start < end:
736 737 while start < end:
737 738 yield start, min(windowsize, end-start)
738 739 start += windowsize
739 740 if windowsize < sizelimit:
740 741 windowsize *= 2
741 742 else:
742 743 while start > end:
743 744 yield start, min(windowsize, start-end-1)
744 745 start -= windowsize
745 746 if windowsize < sizelimit:
746 747 windowsize *= 2
747 748
748 749 files, matchfn, anypats = matchpats(repo, pats, opts)
749 750 follow = opts.get('follow') or opts.get('follow_first')
750 751
751 752 if repo.changelog.count() == 0:
752 753 return [], matchfn
753 754
754 755 if follow:
755 756 defrange = '%s:0' % repo.changectx().rev()
756 757 else:
757 758 defrange = 'tip:0'
758 759 revs = revrange(repo, opts['rev'] or [defrange])
759 760 wanted = {}
760 761 slowpath = anypats or opts.get('removed')
761 762 fncache = {}
762 763
763 764 if not slowpath and not files:
764 765 # No files, no patterns. Display all revs.
765 766 wanted = dict.fromkeys(revs)
766 767 copies = []
767 768 if not slowpath:
768 769 # Only files, no patterns. Check the history of each file.
769 770 def filerevgen(filelog, node):
770 771 cl_count = repo.changelog.count()
771 772 if node is None:
772 773 last = filelog.count() - 1
773 774 else:
774 775 last = filelog.rev(node)
775 776 for i, window in increasing_windows(last, nullrev):
776 777 revs = []
777 778 for j in xrange(i - window, i + 1):
778 779 n = filelog.node(j)
779 780 revs.append((filelog.linkrev(n),
780 781 follow and filelog.renamed(n)))
781 782 revs.reverse()
782 783 for rev in revs:
783 784 # only yield rev for which we have the changelog, it can
784 785 # happen while doing "hg log" during a pull or commit
785 786 if rev[0] < cl_count:
786 787 yield rev
787 788 def iterfiles():
788 789 for filename in files:
789 790 yield filename, None
790 791 for filename_node in copies:
791 792 yield filename_node
792 793 minrev, maxrev = min(revs), max(revs)
793 794 for file_, node in iterfiles():
794 795 filelog = repo.file(file_)
795 796 # A zero count may be a directory or deleted file, so
796 797 # try to find matching entries on the slow path.
797 798 if filelog.count() == 0:
798 799 slowpath = True
799 800 break
800 801 for rev, copied in filerevgen(filelog, node):
801 802 if rev <= maxrev:
802 803 if rev < minrev:
803 804 break
804 805 fncache.setdefault(rev, [])
805 806 fncache[rev].append(file_)
806 807 wanted[rev] = 1
807 808 if follow and copied:
808 809 copies.append(copied)
809 810 if slowpath:
810 811 if follow:
811 812 raise util.Abort(_('can only follow copies/renames for explicit '
812 813 'file names'))
813 814
814 815 # The slow path checks files modified in every changeset.
815 816 def changerevgen():
816 817 for i, window in increasing_windows(repo.changelog.count()-1,
817 818 nullrev):
818 819 for j in xrange(i - window, i + 1):
819 820 yield j, change(j)[3]
820 821
821 822 for rev, changefiles in changerevgen():
822 823 matches = filter(matchfn, changefiles)
823 824 if matches:
824 825 fncache[rev] = matches
825 826 wanted[rev] = 1
826 827
827 828 class followfilter:
828 829 def __init__(self, onlyfirst=False):
829 830 self.startrev = nullrev
830 831 self.roots = []
831 832 self.onlyfirst = onlyfirst
832 833
833 834 def match(self, rev):
834 835 def realparents(rev):
835 836 if self.onlyfirst:
836 837 return repo.changelog.parentrevs(rev)[0:1]
837 838 else:
838 839 return filter(lambda x: x != nullrev,
839 840 repo.changelog.parentrevs(rev))
840 841
841 842 if self.startrev == nullrev:
842 843 self.startrev = rev
843 844 return True
844 845
845 846 if rev > self.startrev:
846 847 # forward: all descendants
847 848 if not self.roots:
848 849 self.roots.append(self.startrev)
849 850 for parent in realparents(rev):
850 851 if parent in self.roots:
851 852 self.roots.append(rev)
852 853 return True
853 854 else:
854 855 # backwards: all parents
855 856 if not self.roots:
856 857 self.roots.extend(realparents(self.startrev))
857 858 if rev in self.roots:
858 859 self.roots.remove(rev)
859 860 self.roots.extend(realparents(rev))
860 861 return True
861 862
862 863 return False
863 864
864 865 # it might be worthwhile to do this in the iterator if the rev range
865 866 # is descending and the prune args are all within that range
866 867 for rev in opts.get('prune', ()):
867 868 rev = repo.changelog.rev(repo.lookup(rev))
868 869 ff = followfilter()
869 870 stop = min(revs[0], revs[-1])
870 871 for x in xrange(rev, stop-1, -1):
871 872 if ff.match(x) and x in wanted:
872 873 del wanted[x]
873 874
874 875 def iterate():
875 876 if follow and not files:
876 877 ff = followfilter(onlyfirst=opts.get('follow_first'))
877 878 def want(rev):
878 879 if ff.match(rev) and rev in wanted:
879 880 return True
880 881 return False
881 882 else:
882 883 def want(rev):
883 884 return rev in wanted
884 885
885 886 for i, window in increasing_windows(0, len(revs)):
886 887 yield 'window', revs[0] < revs[-1], revs[-1]
887 888 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
888 889 srevs = list(nrevs)
889 890 srevs.sort()
890 891 for rev in srevs:
891 892 fns = fncache.get(rev)
892 893 if not fns:
893 894 def fns_generator():
894 895 for f in change(rev)[3]:
895 896 if matchfn(f):
896 897 yield f
897 898 fns = fns_generator()
898 899 yield 'add', rev, fns
899 900 for rev in nrevs:
900 901 yield 'iter', rev, None
901 902 return iterate(), matchfn
902 903
903 904 def commit(ui, repo, commitfunc, pats, opts):
904 905 '''commit the specified files or all outstanding changes'''
905 906 message = logmessage(opts)
906 907
907 908 if opts['addremove']:
908 909 addremove(repo, pats, opts)
909 910 fns, match, anypats = matchpats(repo, pats, opts)
910 911 if pats:
911 912 status = repo.status(files=fns, match=match)
912 913 modified, added, removed, deleted, unknown = status[:5]
913 914 files = modified + added + removed
914 915 slist = None
915 916 for f in fns:
916 917 if f == '.':
917 918 continue
918 919 if f not in files:
919 920 rf = repo.wjoin(f)
920 921 try:
921 922 mode = os.lstat(rf)[stat.ST_MODE]
922 923 except OSError:
923 924 raise util.Abort(_("file %s not found!") % rf)
924 925 if stat.S_ISDIR(mode):
925 926 name = f + '/'
926 927 if slist is None:
927 928 slist = list(files)
928 929 slist.sort()
929 930 i = bisect.bisect(slist, name)
930 931 if i >= len(slist) or not slist[i].startswith(name):
931 932 raise util.Abort(_("no match under directory %s!")
932 933 % rf)
933 934 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
934 935 raise util.Abort(_("can't commit %s: "
935 936 "unsupported file type!") % rf)
936 937 elif f not in repo.dirstate:
937 938 raise util.Abort(_("file %s not tracked!") % rf)
938 939 else:
939 940 files = []
940 941 try:
941 942 return commitfunc(ui, repo, files, message, match, opts)
942 943 except ValueError, inst:
943 944 raise util.Abort(str(inst))
@@ -1,557 +1,574 b''
1 1 """
2 2 dirstate.py - working directory tracking for mercurial
3 3
4 4 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 5
6 6 This software may be used and distributed according to the terms
7 7 of the GNU General Public License, incorporated herein by reference.
8 8 """
9 9
10 10 from node import *
11 11 from i18n import _
12 12 import struct, os, time, bisect, stat, strutil, util, re, errno, ignore
13 13 import cStringIO, osutil
14 14
15 15 _unknown = ('?', 0, 0, 0)
16 16 _format = ">cllll"
17 17
18 18 class dirstate(object):
19 19
20 20 def __init__(self, opener, ui, root):
21 21 self._opener = opener
22 22 self._root = root
23 23 self._dirty = False
24 24 self._dirtypl = False
25 25 self._ui = ui
26 26
27 27 def __getattr__(self, name):
28 28 if name == '_map':
29 29 self._read()
30 30 return self._map
31 31 elif name == '_copymap':
32 32 self._read()
33 33 return self._copymap
34 34 elif name == '_branch':
35 35 try:
36 36 self._branch = (self._opener("branch").read().strip()
37 37 or "default")
38 38 except IOError:
39 39 self._branch = "default"
40 40 return self._branch
41 41 elif name == '_pl':
42 42 self._pl = [nullid, nullid]
43 43 try:
44 44 st = self._opener("dirstate").read(40)
45 45 if len(st) == 40:
46 46 self._pl = st[:20], st[20:40]
47 47 except IOError, err:
48 48 if err.errno != errno.ENOENT: raise
49 49 return self._pl
50 50 elif name == '_dirs':
51 51 self._dirs = {}
52 52 for f in self._map:
53 self._incpath(f)
53 if self[f] != 'r':
54 self._incpath(f)
54 55 return self._dirs
55 56 elif name == '_ignore':
56 57 files = [self._join('.hgignore')]
57 58 for name, path in self._ui.configitems("ui"):
58 59 if name == 'ignore' or name.startswith('ignore.'):
59 60 files.append(os.path.expanduser(path))
60 61 self._ignore = ignore.ignore(self._root, files, self._ui.warn)
61 62 return self._ignore
62 63 elif name == '_slash':
63 64 self._slash = self._ui.configbool('ui', 'slash') and os.sep != '/'
64 65 return self._slash
65 66 else:
66 67 raise AttributeError, name
67 68
68 69 def _join(self, f):
69 70 return os.path.join(self._root, f)
70 71
71 72 def getcwd(self):
72 73 cwd = os.getcwd()
73 74 if cwd == self._root: return ''
74 75 # self._root ends with a path separator if self._root is '/' or 'C:\'
75 76 rootsep = self._root
76 77 if not rootsep.endswith(os.sep):
77 78 rootsep += os.sep
78 79 if cwd.startswith(rootsep):
79 80 return cwd[len(rootsep):]
80 81 else:
81 82 # we're outside the repo. return an absolute path.
82 83 return cwd
83 84
84 85 def pathto(self, f, cwd=None):
85 86 if cwd is None:
86 87 cwd = self.getcwd()
87 88 path = util.pathto(self._root, cwd, f)
88 89 if self._slash:
89 90 return path.replace(os.sep, '/')
90 91 return path
91 92
92 93 def __getitem__(self, key):
93 94 ''' current states:
94 95 n normal
95 96 m needs merging
96 97 r marked for removal
97 98 a marked for addition
98 99 ? not tracked'''
99 100 return self._map.get(key, ("?",))[0]
100 101
101 102 def __contains__(self, key):
102 103 return key in self._map
103 104
104 105 def __iter__(self):
105 106 a = self._map.keys()
106 107 a.sort()
107 108 for x in a:
108 109 yield x
109 110
110 111 def parents(self):
111 112 return self._pl
112 113
113 114 def branch(self):
114 115 return self._branch
115 116
116 117 def setparents(self, p1, p2=nullid):
117 118 self._dirty = self._dirtypl = True
118 119 self._pl = p1, p2
119 120
120 121 def setbranch(self, branch):
121 122 self._branch = branch
122 123 self._opener("branch", "w").write(branch + '\n')
123 124
124 125 def _read(self):
125 126 self._map = {}
126 127 self._copymap = {}
127 128 if not self._dirtypl:
128 129 self._pl = [nullid, nullid]
129 130 try:
130 131 st = self._opener("dirstate").read()
131 132 except IOError, err:
132 133 if err.errno != errno.ENOENT: raise
133 134 return
134 135 if not st:
135 136 return
136 137
137 138 if not self._dirtypl:
138 139 self._pl = [st[:20], st[20: 40]]
139 140
140 141 # deref fields so they will be local in loop
141 142 dmap = self._map
142 143 copymap = self._copymap
143 144 unpack = struct.unpack
144 145 e_size = struct.calcsize(_format)
145 146 pos1 = 40
146 147 l = len(st)
147 148
148 149 # the inner loop
149 150 while pos1 < l:
150 151 pos2 = pos1 + e_size
151 152 e = unpack(">cllll", st[pos1:pos2]) # a literal here is faster
152 153 pos1 = pos2 + e[4]
153 154 f = st[pos2:pos1]
154 155 if '\0' in f:
155 156 f, c = f.split('\0')
156 157 copymap[f] = c
157 158 dmap[f] = e # we hold onto e[4] because making a subtuple is slow
158 159
159 160 def invalidate(self):
160 161 for a in "_map _copymap _branch _pl _dirs _ignore".split():
161 162 if a in self.__dict__:
162 163 delattr(self, a)
163 164 self._dirty = False
164 165
165 166 def copy(self, source, dest):
166 167 self._dirty = True
167 168 self._copymap[dest] = source
168 169
169 170 def copied(self, file):
170 171 return self._copymap.get(file, None)
171 172
172 173 def copies(self):
173 174 return self._copymap
174 175
175 176 def _incpath(self, path):
176 177 c = path.rfind('/')
177 178 if c >= 0:
178 179 dirs = self._dirs
179 180 base = path[:c]
180 181 if base not in dirs:
181 182 self._incpath(base)
182 183 dirs[base] = 1
183 184 else:
184 185 dirs[base] += 1
185 186
186 187 def _decpath(self, path):
187 188 if "_dirs" in self.__dict__:
188 189 c = path.rfind('/')
189 190 if c >= 0:
190 191 base = path[:c]
191 192 dirs = self._dirs
192 193 if dirs[base] == 1:
193 194 del dirs[base]
194 195 self._decpath(base)
195 196 else:
196 197 dirs[base] -= 1
197 198
198 199 def _incpathcheck(self, f):
199 200 if '\r' in f or '\n' in f:
200 201 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames"))
201 202 # shadows
202 203 if f in self._dirs:
203 204 raise util.Abort(_('directory %r already in dirstate') % f)
204 205 for c in strutil.rfindall(f, '/'):
205 206 d = f[:c]
206 207 if d in self._dirs:
207 208 break
208 if d in self._map:
209 if d in self._map and self[d] != 'r':
209 210 raise util.Abort(_('file %r in dirstate clashes with %r') %
210 211 (d, f))
211 212 self._incpath(f)
212 213
214 def _changepath(self, f, newstate):
215 # handle upcoming path changes
216 oldstate = self[f]
217 if oldstate not in "?r" and newstate in "?r":
218 self._decpath(f)
219 return
220 if oldstate in "?r" and newstate not in "?r":
221 self._incpathcheck(f)
222 return
223
213 224 def normal(self, f):
214 225 'mark a file normal and clean'
215 226 self._dirty = True
227 self._changepath(f, 'n')
216 228 s = os.lstat(self._join(f))
217 229 self._map[f] = ('n', s.st_mode, s.st_size, s.st_mtime, 0)
218 230 if self._copymap.has_key(f):
219 231 del self._copymap[f]
220 232
221 233 def normallookup(self, f):
222 234 'mark a file normal, but possibly dirty'
223 235 self._dirty = True
236 self._changepath(f, 'n')
224 237 self._map[f] = ('n', 0, -1, -1, 0)
225 238 if f in self._copymap:
226 239 del self._copymap[f]
227 240
228 241 def normaldirty(self, f):
229 242 'mark a file normal, but dirty'
230 243 self._dirty = True
244 self._changepath(f, 'n')
231 245 self._map[f] = ('n', 0, -2, -1, 0)
232 246 if f in self._copymap:
233 247 del self._copymap[f]
234 248
235 249 def add(self, f):
236 250 'mark a file added'
237 251 self._dirty = True
238 self._incpathcheck(f)
252 self._changepath(f, 'a')
239 253 self._map[f] = ('a', 0, -1, -1, 0)
240 254 if f in self._copymap:
241 255 del self._copymap[f]
242 256
243 257 def remove(self, f):
244 258 'mark a file removed'
245 259 self._dirty = True
260 self._changepath(f, 'r')
246 261 self._map[f] = ('r', 0, 0, 0, 0)
247 self._decpath(f)
248 262 if f in self._copymap:
249 263 del self._copymap[f]
250 264
251 265 def merge(self, f):
252 266 'mark a file merged'
253 267 self._dirty = True
254 268 s = os.lstat(self._join(f))
269 self._changepath(f, 'm')
255 270 self._map[f] = ('m', s.st_mode, s.st_size, s.st_mtime, 0)
256 271 if f in self._copymap:
257 272 del self._copymap[f]
258 273
259 274 def forget(self, f):
260 275 'forget a file'
261 276 self._dirty = True
262 277 try:
278 self._changepath(f, '?')
263 279 del self._map[f]
264 self._decpath(f)
265 280 except KeyError:
266 281 self._ui.warn(_("not in dirstate: %s!\n") % f)
267 282
268 283 def clear(self):
269 284 self._map = {}
285 if "_dirs" in self.__dict__:
286 delattr(self, "_dirs");
270 287 self._copymap = {}
271 288 self._pl = [nullid, nullid]
272 289 self._dirty = True
273 290
274 291 def rebuild(self, parent, files):
275 292 self.clear()
276 293 for f in files:
277 294 if files.execf(f):
278 295 self._map[f] = ('n', 0777, -1, 0, 0)
279 296 else:
280 297 self._map[f] = ('n', 0666, -1, 0, 0)
281 298 self._pl = (parent, nullid)
282 299 self._dirty = True
283 300
284 301 def write(self):
285 302 if not self._dirty:
286 303 return
287 304 cs = cStringIO.StringIO()
288 305 copymap = self._copymap
289 306 pack = struct.pack
290 307 write = cs.write
291 308 write("".join(self._pl))
292 309 for f, e in self._map.iteritems():
293 310 if f in copymap:
294 311 f = "%s\0%s" % (f, copymap[f])
295 312 e = pack(_format, e[0], e[1], e[2], e[3], len(f))
296 313 write(e)
297 314 write(f)
298 315 st = self._opener("dirstate", "w", atomictemp=True)
299 316 st.write(cs.getvalue())
300 317 st.rename()
301 318 self._dirty = self._dirtypl = False
302 319
303 320 def _filter(self, files):
304 321 ret = {}
305 322 unknown = []
306 323
307 324 for x in files:
308 325 if x == '.':
309 326 return self._map.copy()
310 327 if x not in self._map:
311 328 unknown.append(x)
312 329 else:
313 330 ret[x] = self._map[x]
314 331
315 332 if not unknown:
316 333 return ret
317 334
318 335 b = self._map.keys()
319 336 b.sort()
320 337 blen = len(b)
321 338
322 339 for x in unknown:
323 340 bs = bisect.bisect(b, "%s%s" % (x, '/'))
324 341 while bs < blen:
325 342 s = b[bs]
326 343 if len(s) > len(x) and s.startswith(x):
327 344 ret[s] = self._map[s]
328 345 else:
329 346 break
330 347 bs += 1
331 348 return ret
332 349
333 350 def _supported(self, f, mode, verbose=False):
334 351 if stat.S_ISREG(mode) or stat.S_ISLNK(mode):
335 352 return True
336 353 if verbose:
337 354 kind = 'unknown'
338 355 if stat.S_ISCHR(mode): kind = _('character device')
339 356 elif stat.S_ISBLK(mode): kind = _('block device')
340 357 elif stat.S_ISFIFO(mode): kind = _('fifo')
341 358 elif stat.S_ISSOCK(mode): kind = _('socket')
342 359 elif stat.S_ISDIR(mode): kind = _('directory')
343 360 self._ui.warn(_('%s: unsupported file type (type is %s)\n')
344 361 % (self.pathto(f), kind))
345 362 return False
346 363
347 364 def walk(self, files=None, match=util.always, badmatch=None):
348 365 # filter out the stat
349 366 for src, f, st in self.statwalk(files, match, badmatch=badmatch):
350 367 yield src, f
351 368
352 369 def statwalk(self, files=None, match=util.always, ignored=False,
353 370 badmatch=None, directories=False):
354 371 '''
355 372 walk recursively through the directory tree, finding all files
356 373 matched by the match function
357 374
358 375 results are yielded in a tuple (src, filename, st), where src
359 376 is one of:
360 377 'f' the file was found in the directory tree
361 378 'd' the file is a directory of the tree
362 379 'm' the file was only in the dirstate and not in the tree
363 380 'b' file was not found and matched badmatch
364 381
365 382 and st is the stat result if the file was found in the directory.
366 383 '''
367 384
368 385 # walk all files by default
369 386 if not files:
370 387 files = ['.']
371 388 dc = self._map.copy()
372 389 else:
373 390 files = util.unique(files)
374 391 dc = self._filter(files)
375 392
376 393 def imatch(file_):
377 394 if file_ not in dc and self._ignore(file_):
378 395 return False
379 396 return match(file_)
380 397
381 398 ignore = self._ignore
382 399 if ignored:
383 400 imatch = match
384 401 ignore = util.never
385 402
386 403 # self._root may end with a path separator when self._root == '/'
387 404 common_prefix_len = len(self._root)
388 405 if not self._root.endswith(os.sep):
389 406 common_prefix_len += 1
390 407
391 408 normpath = util.normpath
392 409 listdir = osutil.listdir
393 410 lstat = os.lstat
394 411 bisect_left = bisect.bisect_left
395 412 isdir = os.path.isdir
396 413 pconvert = util.pconvert
397 414 join = os.path.join
398 415 s_isdir = stat.S_ISDIR
399 416 supported = self._supported
400 417 _join = self._join
401 418 known = {'.hg': 1}
402 419
403 420 # recursion free walker, faster than os.walk.
404 421 def findfiles(s):
405 422 work = [s]
406 423 wadd = work.append
407 424 found = []
408 425 add = found.append
409 426 if directories:
410 427 add((normpath(s[common_prefix_len:]), 'd', lstat(s)))
411 428 while work:
412 429 top = work.pop()
413 430 entries = listdir(top, stat=True)
414 431 # nd is the top of the repository dir tree
415 432 nd = normpath(top[common_prefix_len:])
416 433 if nd == '.':
417 434 nd = ''
418 435 else:
419 436 # do not recurse into a repo contained in this
420 437 # one. use bisect to find .hg directory so speed
421 438 # is good on big directory.
422 439 names = [e[0] for e in entries]
423 440 hg = bisect_left(names, '.hg')
424 441 if hg < len(names) and names[hg] == '.hg':
425 442 if isdir(join(top, '.hg')):
426 443 continue
427 444 for f, kind, st in entries:
428 445 np = pconvert(join(nd, f))
429 446 if np in known:
430 447 continue
431 448 known[np] = 1
432 449 p = join(top, f)
433 450 # don't trip over symlinks
434 451 if kind == stat.S_IFDIR:
435 452 if not ignore(np):
436 453 wadd(p)
437 454 if directories:
438 455 add((np, 'd', st))
439 456 if np in dc and match(np):
440 457 add((np, 'm', st))
441 458 elif imatch(np):
442 459 if supported(np, st.st_mode):
443 460 add((np, 'f', st))
444 461 elif np in dc:
445 462 add((np, 'm', st))
446 463 found.sort()
447 464 return found
448 465
449 466 # step one, find all files that match our criteria
450 467 files.sort()
451 468 for ff in files:
452 469 nf = normpath(ff)
453 470 f = _join(ff)
454 471 try:
455 472 st = lstat(f)
456 473 except OSError, inst:
457 474 found = False
458 475 for fn in dc:
459 476 if nf == fn or (fn.startswith(nf) and fn[len(nf)] == '/'):
460 477 found = True
461 478 break
462 479 if not found:
463 480 if inst.errno != errno.ENOENT or not badmatch:
464 481 self._ui.warn('%s: %s\n' %
465 482 (self.pathto(ff), inst.strerror))
466 483 elif badmatch and badmatch(ff) and imatch(nf):
467 484 yield 'b', ff, None
468 485 continue
469 486 if s_isdir(st.st_mode):
470 487 for f, src, st in findfiles(f):
471 488 yield src, f, st
472 489 else:
473 490 if nf in known:
474 491 continue
475 492 known[nf] = 1
476 493 if match(nf):
477 494 if supported(ff, st.st_mode, verbose=True):
478 495 yield 'f', nf, st
479 496 elif ff in dc:
480 497 yield 'm', nf, st
481 498
482 499 # step two run through anything left in the dc hash and yield
483 500 # if we haven't already seen it
484 501 ks = dc.keys()
485 502 ks.sort()
486 503 for k in ks:
487 504 if k in known:
488 505 continue
489 506 known[k] = 1
490 507 if imatch(k):
491 508 yield 'm', k, None
492 509
493 510 def status(self, files, match, list_ignored, list_clean):
494 511 lookup, modified, added, unknown, ignored = [], [], [], [], []
495 512 removed, deleted, clean = [], [], []
496 513
497 514 _join = self._join
498 515 lstat = os.lstat
499 516 cmap = self._copymap
500 517 dmap = self._map
501 518 ladd = lookup.append
502 519 madd = modified.append
503 520 aadd = added.append
504 521 uadd = unknown.append
505 522 iadd = ignored.append
506 523 radd = removed.append
507 524 dadd = deleted.append
508 525 cadd = clean.append
509 526
510 527 for src, fn, st in self.statwalk(files, match, ignored=list_ignored):
511 528 if fn in dmap:
512 529 type_, mode, size, time, foo = dmap[fn]
513 530 else:
514 531 if list_ignored and self._ignore(fn):
515 532 iadd(fn)
516 533 else:
517 534 uadd(fn)
518 535 continue
519 536 if src == 'm':
520 537 nonexistent = True
521 538 if not st:
522 539 try:
523 540 st = lstat(_join(fn))
524 541 except OSError, inst:
525 if inst.errno != errno.ENOENT:
542 if inst.errno not in (errno.ENOENT, errno.ENOTDIR):
526 543 raise
527 544 st = None
528 545 # We need to re-check that it is a valid file
529 546 if st and self._supported(fn, st.st_mode):
530 547 nonexistent = False
531 548 # XXX: what to do with file no longer present in the fs
532 549 # who are not removed in the dirstate ?
533 550 if nonexistent and type_ in "nm":
534 551 dadd(fn)
535 552 continue
536 553 # check the common case first
537 554 if type_ == 'n':
538 555 if not st:
539 556 st = lstat(_join(fn))
540 557 if (size >= 0 and (size != st.st_size
541 558 or (mode ^ st.st_mode) & 0100)
542 559 or size == -2
543 560 or fn in self._copymap):
544 561 madd(fn)
545 562 elif time != int(st.st_mtime):
546 563 ladd(fn)
547 564 elif list_clean:
548 565 cadd(fn)
549 566 elif type_ == 'm':
550 567 madd(fn)
551 568 elif type_ == 'a':
552 569 aadd(fn)
553 570 elif type_ == 'r':
554 571 radd(fn)
555 572
556 573 return (lookup, modified, added, removed, deleted, unknown, ignored,
557 574 clean)
@@ -1,1700 +1,1700 b''
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 5 Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import _
16 16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile, strutil
17 17 import os, stat, threading, time, calendar, ConfigParser, locale, glob, osutil
18 18
19 19 try:
20 20 set = set
21 21 frozenset = frozenset
22 22 except NameError:
23 23 from sets import Set as set, ImmutableSet as frozenset
24 24
25 25 try:
26 26 _encoding = os.environ.get("HGENCODING")
27 27 if sys.platform == 'darwin' and not _encoding:
28 28 # On darwin, getpreferredencoding ignores the locale environment and
29 29 # always returns mac-roman. We override this if the environment is
30 30 # not C (has been customized by the user).
31 31 locale.setlocale(locale.LC_CTYPE, '')
32 32 _encoding = locale.getlocale()[1]
33 33 if not _encoding:
34 34 _encoding = locale.getpreferredencoding() or 'ascii'
35 35 except locale.Error:
36 36 _encoding = 'ascii'
37 37 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
38 38 _fallbackencoding = 'ISO-8859-1'
39 39
40 40 def tolocal(s):
41 41 """
42 42 Convert a string from internal UTF-8 to local encoding
43 43
44 44 All internal strings should be UTF-8 but some repos before the
45 45 implementation of locale support may contain latin1 or possibly
46 46 other character sets. We attempt to decode everything strictly
47 47 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
48 48 replace unknown characters.
49 49 """
50 50 for e in ('UTF-8', _fallbackencoding):
51 51 try:
52 52 u = s.decode(e) # attempt strict decoding
53 53 return u.encode(_encoding, "replace")
54 54 except LookupError, k:
55 55 raise Abort(_("%s, please check your locale settings") % k)
56 56 except UnicodeDecodeError:
57 57 pass
58 58 u = s.decode("utf-8", "replace") # last ditch
59 59 return u.encode(_encoding, "replace")
60 60
61 61 def fromlocal(s):
62 62 """
63 63 Convert a string from the local character encoding to UTF-8
64 64
65 65 We attempt to decode strings using the encoding mode set by
66 66 HGENCODINGMODE, which defaults to 'strict'. In this mode, unknown
67 67 characters will cause an error message. Other modes include
68 68 'replace', which replaces unknown characters with a special
69 69 Unicode character, and 'ignore', which drops the character.
70 70 """
71 71 try:
72 72 return s.decode(_encoding, _encodingmode).encode("utf-8")
73 73 except UnicodeDecodeError, inst:
74 74 sub = s[max(0, inst.start-10):inst.start+10]
75 75 raise Abort("decoding near '%s': %s!" % (sub, inst))
76 76 except LookupError, k:
77 77 raise Abort(_("%s, please check your locale settings") % k)
78 78
79 79 def locallen(s):
80 80 """Find the length in characters of a local string"""
81 81 return len(s.decode(_encoding, "replace"))
82 82
83 83 def localsub(s, a, b=None):
84 84 try:
85 85 u = s.decode(_encoding, _encodingmode)
86 86 if b is not None:
87 87 u = u[a:b]
88 88 else:
89 89 u = u[:a]
90 90 return u.encode(_encoding, _encodingmode)
91 91 except UnicodeDecodeError, inst:
92 92 sub = s[max(0, inst.start-10), inst.start+10]
93 93 raise Abort(_("decoding near '%s': %s!") % (sub, inst))
94 94
95 95 # used by parsedate
96 96 defaultdateformats = (
97 97 '%Y-%m-%d %H:%M:%S',
98 98 '%Y-%m-%d %I:%M:%S%p',
99 99 '%Y-%m-%d %H:%M',
100 100 '%Y-%m-%d %I:%M%p',
101 101 '%Y-%m-%d',
102 102 '%m-%d',
103 103 '%m/%d',
104 104 '%m/%d/%y',
105 105 '%m/%d/%Y',
106 106 '%a %b %d %H:%M:%S %Y',
107 107 '%a %b %d %I:%M:%S%p %Y',
108 108 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
109 109 '%b %d %H:%M:%S %Y',
110 110 '%b %d %I:%M:%S%p %Y',
111 111 '%b %d %H:%M:%S',
112 112 '%b %d %I:%M:%S%p',
113 113 '%b %d %H:%M',
114 114 '%b %d %I:%M%p',
115 115 '%b %d %Y',
116 116 '%b %d',
117 117 '%H:%M:%S',
118 118 '%I:%M:%SP',
119 119 '%H:%M',
120 120 '%I:%M%p',
121 121 )
122 122
123 123 extendeddateformats = defaultdateformats + (
124 124 "%Y",
125 125 "%Y-%m",
126 126 "%b",
127 127 "%b %Y",
128 128 )
129 129
130 130 class SignalInterrupt(Exception):
131 131 """Exception raised on SIGTERM and SIGHUP."""
132 132
133 133 # differences from SafeConfigParser:
134 134 # - case-sensitive keys
135 135 # - allows values that are not strings (this means that you may not
136 136 # be able to save the configuration to a file)
137 137 class configparser(ConfigParser.SafeConfigParser):
138 138 def optionxform(self, optionstr):
139 139 return optionstr
140 140
141 141 def set(self, section, option, value):
142 142 return ConfigParser.ConfigParser.set(self, section, option, value)
143 143
144 144 def _interpolate(self, section, option, rawval, vars):
145 145 if not isinstance(rawval, basestring):
146 146 return rawval
147 147 return ConfigParser.SafeConfigParser._interpolate(self, section,
148 148 option, rawval, vars)
149 149
150 150 def cachefunc(func):
151 151 '''cache the result of function calls'''
152 152 # XXX doesn't handle keywords args
153 153 cache = {}
154 154 if func.func_code.co_argcount == 1:
155 155 # we gain a small amount of time because
156 156 # we don't need to pack/unpack the list
157 157 def f(arg):
158 158 if arg not in cache:
159 159 cache[arg] = func(arg)
160 160 return cache[arg]
161 161 else:
162 162 def f(*args):
163 163 if args not in cache:
164 164 cache[args] = func(*args)
165 165 return cache[args]
166 166
167 167 return f
168 168
169 169 def pipefilter(s, cmd):
170 170 '''filter string S through command CMD, returning its output'''
171 171 (pin, pout) = os.popen2(cmd, 'b')
172 172 def writer():
173 173 try:
174 174 pin.write(s)
175 175 pin.close()
176 176 except IOError, inst:
177 177 if inst.errno != errno.EPIPE:
178 178 raise
179 179
180 180 # we should use select instead on UNIX, but this will work on most
181 181 # systems, including Windows
182 182 w = threading.Thread(target=writer)
183 183 w.start()
184 184 f = pout.read()
185 185 pout.close()
186 186 w.join()
187 187 return f
188 188
189 189 def tempfilter(s, cmd):
190 190 '''filter string S through a pair of temporary files with CMD.
191 191 CMD is used as a template to create the real command to be run,
192 192 with the strings INFILE and OUTFILE replaced by the real names of
193 193 the temporary files generated.'''
194 194 inname, outname = None, None
195 195 try:
196 196 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
197 197 fp = os.fdopen(infd, 'wb')
198 198 fp.write(s)
199 199 fp.close()
200 200 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
201 201 os.close(outfd)
202 202 cmd = cmd.replace('INFILE', inname)
203 203 cmd = cmd.replace('OUTFILE', outname)
204 204 code = os.system(cmd)
205 205 if sys.platform == 'OpenVMS' and code & 1:
206 206 code = 0
207 207 if code: raise Abort(_("command '%s' failed: %s") %
208 208 (cmd, explain_exit(code)))
209 209 return open(outname, 'rb').read()
210 210 finally:
211 211 try:
212 212 if inname: os.unlink(inname)
213 213 except: pass
214 214 try:
215 215 if outname: os.unlink(outname)
216 216 except: pass
217 217
218 218 filtertable = {
219 219 'tempfile:': tempfilter,
220 220 'pipe:': pipefilter,
221 221 }
222 222
223 223 def filter(s, cmd):
224 224 "filter a string through a command that transforms its input to its output"
225 225 for name, fn in filtertable.iteritems():
226 226 if cmd.startswith(name):
227 227 return fn(s, cmd[len(name):].lstrip())
228 228 return pipefilter(s, cmd)
229 229
230 230 def binary(s):
231 231 """return true if a string is binary data using diff's heuristic"""
232 232 if s and '\0' in s[:4096]:
233 233 return True
234 234 return False
235 235
236 236 def unique(g):
237 237 """return the uniq elements of iterable g"""
238 238 seen = {}
239 239 l = []
240 240 for f in g:
241 241 if f not in seen:
242 242 seen[f] = 1
243 243 l.append(f)
244 244 return l
245 245
246 246 class Abort(Exception):
247 247 """Raised if a command needs to print an error and exit."""
248 248
249 249 class UnexpectedOutput(Abort):
250 250 """Raised to print an error with part of output and exit."""
251 251
252 252 def always(fn): return True
253 253 def never(fn): return False
254 254
255 255 def expand_glob(pats):
256 256 '''On Windows, expand the implicit globs in a list of patterns'''
257 257 if os.name != 'nt':
258 258 return list(pats)
259 259 ret = []
260 260 for p in pats:
261 261 kind, name = patkind(p, None)
262 262 if kind is None:
263 263 globbed = glob.glob(name)
264 264 if globbed:
265 265 ret.extend(globbed)
266 266 continue
267 267 # if we couldn't expand the glob, just keep it around
268 268 ret.append(p)
269 269 return ret
270 270
271 271 def patkind(name, dflt_pat='glob'):
272 272 """Split a string into an optional pattern kind prefix and the
273 273 actual pattern."""
274 274 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
275 275 if name.startswith(prefix + ':'): return name.split(':', 1)
276 276 return dflt_pat, name
277 277
278 278 def globre(pat, head='^', tail='$'):
279 279 "convert a glob pattern into a regexp"
280 280 i, n = 0, len(pat)
281 281 res = ''
282 282 group = False
283 283 def peek(): return i < n and pat[i]
284 284 while i < n:
285 285 c = pat[i]
286 286 i = i+1
287 287 if c == '*':
288 288 if peek() == '*':
289 289 i += 1
290 290 res += '.*'
291 291 else:
292 292 res += '[^/]*'
293 293 elif c == '?':
294 294 res += '.'
295 295 elif c == '[':
296 296 j = i
297 297 if j < n and pat[j] in '!]':
298 298 j += 1
299 299 while j < n and pat[j] != ']':
300 300 j += 1
301 301 if j >= n:
302 302 res += '\\['
303 303 else:
304 304 stuff = pat[i:j].replace('\\','\\\\')
305 305 i = j + 1
306 306 if stuff[0] == '!':
307 307 stuff = '^' + stuff[1:]
308 308 elif stuff[0] == '^':
309 309 stuff = '\\' + stuff
310 310 res = '%s[%s]' % (res, stuff)
311 311 elif c == '{':
312 312 group = True
313 313 res += '(?:'
314 314 elif c == '}' and group:
315 315 res += ')'
316 316 group = False
317 317 elif c == ',' and group:
318 318 res += '|'
319 319 elif c == '\\':
320 320 p = peek()
321 321 if p:
322 322 i += 1
323 323 res += re.escape(p)
324 324 else:
325 325 res += re.escape(c)
326 326 else:
327 327 res += re.escape(c)
328 328 return head + res + tail
329 329
330 330 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
331 331
332 332 def pathto(root, n1, n2):
333 333 '''return the relative path from one place to another.
334 334 root should use os.sep to separate directories
335 335 n1 should use os.sep to separate directories
336 336 n2 should use "/" to separate directories
337 337 returns an os.sep-separated path.
338 338
339 339 If n1 is a relative path, it's assumed it's
340 340 relative to root.
341 341 n2 should always be relative to root.
342 342 '''
343 343 if not n1: return localpath(n2)
344 344 if os.path.isabs(n1):
345 345 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
346 346 return os.path.join(root, localpath(n2))
347 347 n2 = '/'.join((pconvert(root), n2))
348 348 a, b = n1.split(os.sep), n2.split('/')
349 349 a.reverse()
350 350 b.reverse()
351 351 while a and b and a[-1] == b[-1]:
352 352 a.pop()
353 353 b.pop()
354 354 b.reverse()
355 355 return os.sep.join((['..'] * len(a)) + b)
356 356
357 357 def canonpath(root, cwd, myname):
358 358 """return the canonical path of myname, given cwd and root"""
359 359 if root == os.sep:
360 360 rootsep = os.sep
361 361 elif root.endswith(os.sep):
362 362 rootsep = root
363 363 else:
364 364 rootsep = root + os.sep
365 365 name = myname
366 366 if not os.path.isabs(name):
367 367 name = os.path.join(root, cwd, name)
368 368 name = os.path.normpath(name)
369 369 audit_path = path_auditor(root)
370 370 if name != rootsep and name.startswith(rootsep):
371 371 name = name[len(rootsep):]
372 372 audit_path(name)
373 373 return pconvert(name)
374 374 elif name == root:
375 375 return ''
376 376 else:
377 377 # Determine whether `name' is in the hierarchy at or beneath `root',
378 378 # by iterating name=dirname(name) until that causes no change (can't
379 379 # check name == '/', because that doesn't work on windows). For each
380 380 # `name', compare dev/inode numbers. If they match, the list `rel'
381 381 # holds the reversed list of components making up the relative file
382 382 # name we want.
383 383 root_st = os.stat(root)
384 384 rel = []
385 385 while True:
386 386 try:
387 387 name_st = os.stat(name)
388 388 except OSError:
389 389 break
390 390 if samestat(name_st, root_st):
391 391 if not rel:
392 392 # name was actually the same as root (maybe a symlink)
393 393 return ''
394 394 rel.reverse()
395 395 name = os.path.join(*rel)
396 396 audit_path(name)
397 397 return pconvert(name)
398 398 dirname, basename = os.path.split(name)
399 399 rel.append(basename)
400 400 if dirname == name:
401 401 break
402 402 name = dirname
403 403
404 404 raise Abort('%s not under root' % myname)
405 405
406 406 def matcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None):
407 407 return _matcher(canonroot, cwd, names, inc, exc, 'glob', src)
408 408
409 409 def cmdmatcher(canonroot, cwd='', names=[], inc=[], exc=[], src=None,
410 410 globbed=False, default=None):
411 411 default = default or 'relpath'
412 412 if default == 'relpath' and not globbed:
413 413 names = expand_glob(names)
414 414 return _matcher(canonroot, cwd, names, inc, exc, default, src)
415 415
416 416 def _matcher(canonroot, cwd, names, inc, exc, dflt_pat, src):
417 417 """build a function to match a set of file patterns
418 418
419 419 arguments:
420 420 canonroot - the canonical root of the tree you're matching against
421 421 cwd - the current working directory, if relevant
422 422 names - patterns to find
423 423 inc - patterns to include
424 424 exc - patterns to exclude
425 425 dflt_pat - if a pattern in names has no explicit type, assume this one
426 426 src - where these patterns came from (e.g. .hgignore)
427 427
428 428 a pattern is one of:
429 429 'glob:<glob>' - a glob relative to cwd
430 430 're:<regexp>' - a regular expression
431 431 'path:<path>' - a path relative to canonroot
432 432 'relglob:<glob>' - an unrooted glob (*.c matches C files in all dirs)
433 433 'relpath:<path>' - a path relative to cwd
434 434 'relre:<regexp>' - a regexp that doesn't have to match the start of a name
435 435 '<something>' - one of the cases above, selected by the dflt_pat argument
436 436
437 437 returns:
438 438 a 3-tuple containing
439 439 - list of roots (places where one should start a recursive walk of the fs);
440 440 this often matches the explicit non-pattern names passed in, but also
441 441 includes the initial part of glob: patterns that has no glob characters
442 442 - a bool match(filename) function
443 443 - a bool indicating if any patterns were passed in
444 444 """
445 445
446 446 # a common case: no patterns at all
447 447 if not names and not inc and not exc:
448 448 return [], always, False
449 449
450 450 def contains_glob(name):
451 451 for c in name:
452 452 if c in _globchars: return True
453 453 return False
454 454
455 455 def regex(kind, name, tail):
456 456 '''convert a pattern into a regular expression'''
457 457 if not name:
458 458 return ''
459 459 if kind == 're':
460 460 return name
461 461 elif kind == 'path':
462 462 return '^' + re.escape(name) + '(?:/|$)'
463 463 elif kind == 'relglob':
464 464 return globre(name, '(?:|.*/)', tail)
465 465 elif kind == 'relpath':
466 466 return re.escape(name) + '(?:/|$)'
467 467 elif kind == 'relre':
468 468 if name.startswith('^'):
469 469 return name
470 470 return '.*' + name
471 471 return globre(name, '', tail)
472 472
473 473 def matchfn(pats, tail):
474 474 """build a matching function from a set of patterns"""
475 475 if not pats:
476 476 return
477 477 try:
478 478 pat = '(?:%s)' % '|'.join([regex(k, p, tail) for (k, p) in pats])
479 479 return re.compile(pat).match
480 480 except OverflowError:
481 481 # We're using a Python with a tiny regex engine and we
482 482 # made it explode, so we'll divide the pattern list in two
483 483 # until it works
484 484 l = len(pats)
485 485 if l < 2:
486 486 raise
487 487 a, b = matchfn(pats[:l//2], tail), matchfn(pats[l//2:], tail)
488 488 return lambda s: a(s) or b(s)
489 489 except re.error:
490 490 for k, p in pats:
491 491 try:
492 492 re.compile('(?:%s)' % regex(k, p, tail))
493 493 except re.error:
494 494 if src:
495 495 raise Abort("%s: invalid pattern (%s): %s" %
496 496 (src, k, p))
497 497 else:
498 498 raise Abort("invalid pattern (%s): %s" % (k, p))
499 499 raise Abort("invalid pattern")
500 500
501 501 def globprefix(pat):
502 502 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
503 503 root = []
504 504 for p in pat.split('/'):
505 505 if contains_glob(p): break
506 506 root.append(p)
507 507 return '/'.join(root) or '.'
508 508
509 509 def normalizepats(names, default):
510 510 pats = []
511 511 roots = []
512 512 anypats = False
513 513 for kind, name in [patkind(p, default) for p in names]:
514 514 if kind in ('glob', 'relpath'):
515 515 name = canonpath(canonroot, cwd, name)
516 516 elif kind in ('relglob', 'path'):
517 517 name = normpath(name)
518 518
519 519 pats.append((kind, name))
520 520
521 521 if kind in ('glob', 're', 'relglob', 'relre'):
522 522 anypats = True
523 523
524 524 if kind == 'glob':
525 525 root = globprefix(name)
526 526 roots.append(root)
527 527 elif kind in ('relpath', 'path'):
528 528 roots.append(name or '.')
529 529 elif kind == 'relglob':
530 530 roots.append('.')
531 531 return roots, pats, anypats
532 532
533 533 roots, pats, anypats = normalizepats(names, dflt_pat)
534 534
535 535 patmatch = matchfn(pats, '$') or always
536 536 incmatch = always
537 537 if inc:
538 538 dummy, inckinds, dummy = normalizepats(inc, 'glob')
539 539 incmatch = matchfn(inckinds, '(?:/|$)')
540 540 excmatch = lambda fn: False
541 541 if exc:
542 542 dummy, exckinds, dummy = normalizepats(exc, 'glob')
543 543 excmatch = matchfn(exckinds, '(?:/|$)')
544 544
545 545 if not names and inc and not exc:
546 546 # common case: hgignore patterns
547 547 match = incmatch
548 548 else:
549 549 match = lambda fn: incmatch(fn) and not excmatch(fn) and patmatch(fn)
550 550
551 551 return (roots, match, (inc or exc or anypats) and True)
552 552
553 553 _hgexecutable = None
554 554
555 555 def hgexecutable():
556 556 """return location of the 'hg' executable.
557 557
558 558 Defaults to $HG or 'hg' in the search path.
559 559 """
560 560 if _hgexecutable is None:
561 561 set_hgexecutable(os.environ.get('HG') or find_exe('hg', 'hg'))
562 562 return _hgexecutable
563 563
564 564 def set_hgexecutable(path):
565 565 """set location of the 'hg' executable"""
566 566 global _hgexecutable
567 567 _hgexecutable = path
568 568
569 569 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
570 570 '''enhanced shell command execution.
571 571 run with environment maybe modified, maybe in different dir.
572 572
573 573 if command fails and onerr is None, return status. if ui object,
574 574 print error message and return status, else raise onerr object as
575 575 exception.'''
576 576 def py2shell(val):
577 577 'convert python object into string that is useful to shell'
578 578 if val in (None, False):
579 579 return '0'
580 580 if val == True:
581 581 return '1'
582 582 return str(val)
583 583 oldenv = {}
584 584 for k in environ:
585 585 oldenv[k] = os.environ.get(k)
586 586 if cwd is not None:
587 587 oldcwd = os.getcwd()
588 588 origcmd = cmd
589 589 if os.name == 'nt':
590 590 cmd = '"%s"' % cmd
591 591 try:
592 592 for k, v in environ.iteritems():
593 593 os.environ[k] = py2shell(v)
594 594 os.environ['HG'] = hgexecutable()
595 595 if cwd is not None and oldcwd != cwd:
596 596 os.chdir(cwd)
597 597 rc = os.system(cmd)
598 598 if sys.platform == 'OpenVMS' and rc & 1:
599 599 rc = 0
600 600 if rc and onerr:
601 601 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
602 602 explain_exit(rc)[0])
603 603 if errprefix:
604 604 errmsg = '%s: %s' % (errprefix, errmsg)
605 605 try:
606 606 onerr.warn(errmsg + '\n')
607 607 except AttributeError:
608 608 raise onerr(errmsg)
609 609 return rc
610 610 finally:
611 611 for k, v in oldenv.iteritems():
612 612 if v is None:
613 613 del os.environ[k]
614 614 else:
615 615 os.environ[k] = v
616 616 if cwd is not None and oldcwd != cwd:
617 617 os.chdir(oldcwd)
618 618
619 619 # os.path.lexists is not available on python2.3
620 620 def lexists(filename):
621 621 "test whether a file with this name exists. does not follow symlinks"
622 622 try:
623 623 os.lstat(filename)
624 624 except:
625 625 return False
626 626 return True
627 627
628 628 def rename(src, dst):
629 629 """forcibly rename a file"""
630 630 try:
631 631 os.rename(src, dst)
632 632 except OSError, err: # FIXME: check err (EEXIST ?)
633 633 # on windows, rename to existing file is not allowed, so we
634 634 # must delete destination first. but if file is open, unlink
635 635 # schedules it for delete but does not delete it. rename
636 636 # happens immediately even for open files, so we create
637 637 # temporary file, delete it, rename destination to that name,
638 638 # then delete that. then rename is safe to do.
639 639 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
640 640 os.close(fd)
641 641 os.unlink(temp)
642 642 os.rename(dst, temp)
643 643 os.unlink(temp)
644 644 os.rename(src, dst)
645 645
646 646 def unlink(f):
647 647 """unlink and remove the directory if it is empty"""
648 648 os.unlink(f)
649 649 # try removing directories that might now be empty
650 650 try:
651 651 os.removedirs(os.path.dirname(f))
652 652 except OSError:
653 653 pass
654 654
655 655 def copyfile(src, dest):
656 656 "copy a file, preserving mode"
657 657 if os.path.islink(src):
658 658 try:
659 659 os.unlink(dest)
660 660 except:
661 661 pass
662 662 os.symlink(os.readlink(src), dest)
663 663 else:
664 664 try:
665 665 shutil.copyfile(src, dest)
666 666 shutil.copymode(src, dest)
667 667 except shutil.Error, inst:
668 668 raise Abort(str(inst))
669 669
670 670 def copyfiles(src, dst, hardlink=None):
671 671 """Copy a directory tree using hardlinks if possible"""
672 672
673 673 if hardlink is None:
674 674 hardlink = (os.stat(src).st_dev ==
675 675 os.stat(os.path.dirname(dst)).st_dev)
676 676
677 677 if os.path.isdir(src):
678 678 os.mkdir(dst)
679 679 for name, kind in osutil.listdir(src):
680 680 srcname = os.path.join(src, name)
681 681 dstname = os.path.join(dst, name)
682 682 copyfiles(srcname, dstname, hardlink)
683 683 else:
684 684 if hardlink:
685 685 try:
686 686 os_link(src, dst)
687 687 except (IOError, OSError):
688 688 hardlink = False
689 689 shutil.copy(src, dst)
690 690 else:
691 691 shutil.copy(src, dst)
692 692
693 693 class path_auditor(object):
694 694 '''ensure that a filesystem path contains no banned components.
695 695 the following properties of a path are checked:
696 696
697 697 - under top-level .hg
698 698 - starts at the root of a windows drive
699 699 - contains ".."
700 700 - traverses a symlink (e.g. a/symlink_here/b)
701 701 - inside a nested repository'''
702 702
703 703 def __init__(self, root):
704 704 self.audited = set()
705 705 self.auditeddir = set()
706 706 self.root = root
707 707
708 708 def __call__(self, path):
709 709 if path in self.audited:
710 710 return
711 711 normpath = os.path.normcase(path)
712 712 parts = normpath.split(os.sep)
713 713 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
714 714 or os.pardir in parts):
715 715 raise Abort(_("path contains illegal component: %s") % path)
716 716 def check(prefix):
717 717 curpath = os.path.join(self.root, prefix)
718 718 try:
719 719 st = os.lstat(curpath)
720 720 except OSError, err:
721 721 # EINVAL can be raised as invalid path syntax under win32.
722 722 # They must be ignored for patterns can be checked too.
723 if err.errno not in (errno.ENOENT, errno.EINVAL):
723 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
724 724 raise
725 725 else:
726 726 if stat.S_ISLNK(st.st_mode):
727 727 raise Abort(_('path %r traverses symbolic link %r') %
728 728 (path, prefix))
729 729 elif (stat.S_ISDIR(st.st_mode) and
730 730 os.path.isdir(os.path.join(curpath, '.hg'))):
731 731 raise Abort(_('path %r is inside repo %r') %
732 732 (path, prefix))
733 733
734 734 prefixes = []
735 735 for c in strutil.rfindall(normpath, os.sep):
736 736 prefix = normpath[:c]
737 737 if prefix in self.auditeddir:
738 738 break
739 739 check(prefix)
740 740 prefixes.append(prefix)
741 741
742 742 self.audited.add(path)
743 743 # only add prefixes to the cache after checking everything: we don't
744 744 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
745 745 self.auditeddir.update(prefixes)
746 746
747 747 def _makelock_file(info, pathname):
748 748 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
749 749 os.write(ld, info)
750 750 os.close(ld)
751 751
752 752 def _readlock_file(pathname):
753 753 return posixfile(pathname).read()
754 754
755 755 def nlinks(pathname):
756 756 """Return number of hardlinks for the given file."""
757 757 return os.lstat(pathname).st_nlink
758 758
759 759 if hasattr(os, 'link'):
760 760 os_link = os.link
761 761 else:
762 762 def os_link(src, dst):
763 763 raise OSError(0, _("Hardlinks not supported"))
764 764
765 765 def fstat(fp):
766 766 '''stat file object that may not have fileno method.'''
767 767 try:
768 768 return os.fstat(fp.fileno())
769 769 except AttributeError:
770 770 return os.stat(fp.name)
771 771
772 772 posixfile = file
773 773
774 774 def is_win_9x():
775 775 '''return true if run on windows 95, 98 or me.'''
776 776 try:
777 777 return sys.getwindowsversion()[3] == 1
778 778 except AttributeError:
779 779 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
780 780
781 781 getuser_fallback = None
782 782
783 783 def getuser():
784 784 '''return name of current user'''
785 785 try:
786 786 return getpass.getuser()
787 787 except ImportError:
788 788 # import of pwd will fail on windows - try fallback
789 789 if getuser_fallback:
790 790 return getuser_fallback()
791 791 # raised if win32api not available
792 792 raise Abort(_('user name not available - set USERNAME '
793 793 'environment variable'))
794 794
795 795 def username(uid=None):
796 796 """Return the name of the user with the given uid.
797 797
798 798 If uid is None, return the name of the current user."""
799 799 try:
800 800 import pwd
801 801 if uid is None:
802 802 uid = os.getuid()
803 803 try:
804 804 return pwd.getpwuid(uid)[0]
805 805 except KeyError:
806 806 return str(uid)
807 807 except ImportError:
808 808 return None
809 809
810 810 def groupname(gid=None):
811 811 """Return the name of the group with the given gid.
812 812
813 813 If gid is None, return the name of the current group."""
814 814 try:
815 815 import grp
816 816 if gid is None:
817 817 gid = os.getgid()
818 818 try:
819 819 return grp.getgrgid(gid)[0]
820 820 except KeyError:
821 821 return str(gid)
822 822 except ImportError:
823 823 return None
824 824
825 825 # File system features
826 826
827 827 def checkfolding(path):
828 828 """
829 829 Check whether the given path is on a case-sensitive filesystem
830 830
831 831 Requires a path (like /foo/.hg) ending with a foldable final
832 832 directory component.
833 833 """
834 834 s1 = os.stat(path)
835 835 d, b = os.path.split(path)
836 836 p2 = os.path.join(d, b.upper())
837 837 if path == p2:
838 838 p2 = os.path.join(d, b.lower())
839 839 try:
840 840 s2 = os.stat(p2)
841 841 if s2 == s1:
842 842 return False
843 843 return True
844 844 except:
845 845 return True
846 846
847 847 def checkexec(path):
848 848 """
849 849 Check whether the given path is on a filesystem with UNIX-like exec flags
850 850
851 851 Requires a directory (like /foo/.hg)
852 852 """
853 853 try:
854 854 EXECFLAGS = stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
855 855 fh, fn = tempfile.mkstemp("", "", path)
856 856 os.close(fh)
857 857 m = os.stat(fn).st_mode
858 858 # VFAT on Linux can flip mode but it doesn't persist a FS remount.
859 859 # frequently we can detect it if files are created with exec bit on.
860 860 new_file_has_exec = m & EXECFLAGS
861 861 os.chmod(fn, m ^ EXECFLAGS)
862 862 exec_flags_cannot_flip = (os.stat(fn).st_mode == m)
863 863 os.unlink(fn)
864 864 except (IOError,OSError):
865 865 # we don't care, the user probably won't be able to commit anyway
866 866 return False
867 867 return not (new_file_has_exec or exec_flags_cannot_flip)
868 868
869 869 def execfunc(path, fallback):
870 870 '''return an is_exec() function with default to fallback'''
871 871 if checkexec(path):
872 872 return lambda x: is_exec(os.path.join(path, x))
873 873 return fallback
874 874
875 875 def checklink(path):
876 876 """check whether the given path is on a symlink-capable filesystem"""
877 877 # mktemp is not racy because symlink creation will fail if the
878 878 # file already exists
879 879 name = tempfile.mktemp(dir=path)
880 880 try:
881 881 os.symlink(".", name)
882 882 os.unlink(name)
883 883 return True
884 884 except (OSError, AttributeError):
885 885 return False
886 886
887 887 def linkfunc(path, fallback):
888 888 '''return an is_link() function with default to fallback'''
889 889 if checklink(path):
890 890 return lambda x: os.path.islink(os.path.join(path, x))
891 891 return fallback
892 892
893 893 _umask = os.umask(0)
894 894 os.umask(_umask)
895 895
896 896 def needbinarypatch():
897 897 """return True if patches should be applied in binary mode by default."""
898 898 return os.name == 'nt'
899 899
900 900 # Platform specific variants
901 901 if os.name == 'nt':
902 902 import msvcrt
903 903 nulldev = 'NUL:'
904 904
905 905 class winstdout:
906 906 '''stdout on windows misbehaves if sent through a pipe'''
907 907
908 908 def __init__(self, fp):
909 909 self.fp = fp
910 910
911 911 def __getattr__(self, key):
912 912 return getattr(self.fp, key)
913 913
914 914 def close(self):
915 915 try:
916 916 self.fp.close()
917 917 except: pass
918 918
919 919 def write(self, s):
920 920 try:
921 921 return self.fp.write(s)
922 922 except IOError, inst:
923 923 if inst.errno != 0: raise
924 924 self.close()
925 925 raise IOError(errno.EPIPE, 'Broken pipe')
926 926
927 927 def flush(self):
928 928 try:
929 929 return self.fp.flush()
930 930 except IOError, inst:
931 931 if inst.errno != errno.EINVAL: raise
932 932 self.close()
933 933 raise IOError(errno.EPIPE, 'Broken pipe')
934 934
935 935 sys.stdout = winstdout(sys.stdout)
936 936
937 937 def system_rcpath():
938 938 try:
939 939 return system_rcpath_win32()
940 940 except:
941 941 return [r'c:\mercurial\mercurial.ini']
942 942
943 943 def user_rcpath():
944 944 '''return os-specific hgrc search path to the user dir'''
945 945 try:
946 946 userrc = user_rcpath_win32()
947 947 except:
948 948 userrc = os.path.join(os.path.expanduser('~'), 'mercurial.ini')
949 949 path = [userrc]
950 950 userprofile = os.environ.get('USERPROFILE')
951 951 if userprofile:
952 952 path.append(os.path.join(userprofile, 'mercurial.ini'))
953 953 return path
954 954
955 955 def parse_patch_output(output_line):
956 956 """parses the output produced by patch and returns the file name"""
957 957 pf = output_line[14:]
958 958 if pf[0] == '`':
959 959 pf = pf[1:-1] # Remove the quotes
960 960 return pf
961 961
962 962 def testpid(pid):
963 963 '''return False if pid dead, True if running or not known'''
964 964 return True
965 965
966 966 def set_exec(f, mode):
967 967 pass
968 968
969 969 def set_link(f, mode):
970 970 pass
971 971
972 972 def set_binary(fd):
973 973 msvcrt.setmode(fd.fileno(), os.O_BINARY)
974 974
975 975 def pconvert(path):
976 976 return path.replace("\\", "/")
977 977
978 978 def localpath(path):
979 979 return path.replace('/', '\\')
980 980
981 981 def normpath(path):
982 982 return pconvert(os.path.normpath(path))
983 983
984 984 makelock = _makelock_file
985 985 readlock = _readlock_file
986 986
987 987 def samestat(s1, s2):
988 988 return False
989 989
990 990 # A sequence of backslashes is special iff it precedes a double quote:
991 991 # - if there's an even number of backslashes, the double quote is not
992 992 # quoted (i.e. it ends the quoted region)
993 993 # - if there's an odd number of backslashes, the double quote is quoted
994 994 # - in both cases, every pair of backslashes is unquoted into a single
995 995 # backslash
996 996 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
997 997 # So, to quote a string, we must surround it in double quotes, double
998 998 # the number of backslashes that preceed double quotes and add another
999 999 # backslash before every double quote (being careful with the double
1000 1000 # quote we've appended to the end)
1001 1001 _quotere = None
1002 1002 def shellquote(s):
1003 1003 global _quotere
1004 1004 if _quotere is None:
1005 1005 _quotere = re.compile(r'(\\*)("|\\$)')
1006 1006 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
1007 1007
1008 1008 def quotecommand(cmd):
1009 1009 """Build a command string suitable for os.popen* calls."""
1010 1010 # The extra quotes are needed because popen* runs the command
1011 1011 # through the current COMSPEC. cmd.exe suppress enclosing quotes.
1012 1012 return '"' + cmd + '"'
1013 1013
1014 1014 def popen(command):
1015 1015 # Work around "popen spawned process may not write to stdout
1016 1016 # under windows"
1017 1017 # http://bugs.python.org/issue1366
1018 1018 command += " 2> %s" % nulldev
1019 1019 return os.popen(quotecommand(command))
1020 1020
1021 1021 def explain_exit(code):
1022 1022 return _("exited with status %d") % code, code
1023 1023
1024 1024 # if you change this stub into a real check, please try to implement the
1025 1025 # username and groupname functions above, too.
1026 1026 def isowner(fp, st=None):
1027 1027 return True
1028 1028
1029 1029 def find_in_path(name, path, default=None):
1030 1030 '''find name in search path. path can be string (will be split
1031 1031 with os.pathsep), or iterable thing that returns strings. if name
1032 1032 found, return path to name. else return default. name is looked up
1033 1033 using cmd.exe rules, using PATHEXT.'''
1034 1034 if isinstance(path, str):
1035 1035 path = path.split(os.pathsep)
1036 1036
1037 1037 pathext = os.environ.get('PATHEXT', '.COM;.EXE;.BAT;.CMD')
1038 1038 pathext = pathext.lower().split(os.pathsep)
1039 1039 isexec = os.path.splitext(name)[1].lower() in pathext
1040 1040
1041 1041 for p in path:
1042 1042 p_name = os.path.join(p, name)
1043 1043
1044 1044 if isexec and os.path.exists(p_name):
1045 1045 return p_name
1046 1046
1047 1047 for ext in pathext:
1048 1048 p_name_ext = p_name + ext
1049 1049 if os.path.exists(p_name_ext):
1050 1050 return p_name_ext
1051 1051 return default
1052 1052
1053 1053 def set_signal_handler():
1054 1054 try:
1055 1055 set_signal_handler_win32()
1056 1056 except NameError:
1057 1057 pass
1058 1058
1059 1059 try:
1060 1060 # override functions with win32 versions if possible
1061 1061 from util_win32 import *
1062 1062 if not is_win_9x():
1063 1063 posixfile = posixfile_nt
1064 1064 except ImportError:
1065 1065 pass
1066 1066
1067 1067 else:
1068 1068 nulldev = '/dev/null'
1069 1069
1070 1070 def rcfiles(path):
1071 1071 rcs = [os.path.join(path, 'hgrc')]
1072 1072 rcdir = os.path.join(path, 'hgrc.d')
1073 1073 try:
1074 1074 rcs.extend([os.path.join(rcdir, f)
1075 1075 for f, kind in osutil.listdir(rcdir)
1076 1076 if f.endswith(".rc")])
1077 1077 except OSError:
1078 1078 pass
1079 1079 return rcs
1080 1080
1081 1081 def system_rcpath():
1082 1082 path = []
1083 1083 # old mod_python does not set sys.argv
1084 1084 if len(getattr(sys, 'argv', [])) > 0:
1085 1085 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
1086 1086 '/../etc/mercurial'))
1087 1087 path.extend(rcfiles('/etc/mercurial'))
1088 1088 return path
1089 1089
1090 1090 def user_rcpath():
1091 1091 return [os.path.expanduser('~/.hgrc')]
1092 1092
1093 1093 def parse_patch_output(output_line):
1094 1094 """parses the output produced by patch and returns the file name"""
1095 1095 pf = output_line[14:]
1096 1096 if os.sys.platform == 'OpenVMS':
1097 1097 if pf[0] == '`':
1098 1098 pf = pf[1:-1] # Remove the quotes
1099 1099 else:
1100 1100 if pf.startswith("'") and pf.endswith("'") and " " in pf:
1101 1101 pf = pf[1:-1] # Remove the quotes
1102 1102 return pf
1103 1103
1104 1104 def is_exec(f):
1105 1105 """check whether a file is executable"""
1106 1106 return (os.lstat(f).st_mode & 0100 != 0)
1107 1107
1108 1108 def set_exec(f, mode):
1109 1109 s = os.lstat(f).st_mode
1110 1110 if stat.S_ISLNK(s) or (s & 0100 != 0) == mode:
1111 1111 return
1112 1112 if mode:
1113 1113 # Turn on +x for every +r bit when making a file executable
1114 1114 # and obey umask.
1115 1115 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
1116 1116 else:
1117 1117 os.chmod(f, s & 0666)
1118 1118
1119 1119 def set_link(f, mode):
1120 1120 """make a file a symbolic link/regular file
1121 1121
1122 1122 if a file is changed to a link, its contents become the link data
1123 1123 if a link is changed to a file, its link data become its contents
1124 1124 """
1125 1125
1126 1126 m = os.path.islink(f)
1127 1127 if m == bool(mode):
1128 1128 return
1129 1129
1130 1130 if mode: # switch file to link
1131 1131 data = file(f).read()
1132 1132 os.unlink(f)
1133 1133 os.symlink(data, f)
1134 1134 else:
1135 1135 data = os.readlink(f)
1136 1136 os.unlink(f)
1137 1137 file(f, "w").write(data)
1138 1138
1139 1139 def set_binary(fd):
1140 1140 pass
1141 1141
1142 1142 def pconvert(path):
1143 1143 return path
1144 1144
1145 1145 def localpath(path):
1146 1146 return path
1147 1147
1148 1148 normpath = os.path.normpath
1149 1149 samestat = os.path.samestat
1150 1150
1151 1151 def makelock(info, pathname):
1152 1152 try:
1153 1153 os.symlink(info, pathname)
1154 1154 except OSError, why:
1155 1155 if why.errno == errno.EEXIST:
1156 1156 raise
1157 1157 else:
1158 1158 _makelock_file(info, pathname)
1159 1159
1160 1160 def readlock(pathname):
1161 1161 try:
1162 1162 return os.readlink(pathname)
1163 1163 except OSError, why:
1164 1164 if why.errno in (errno.EINVAL, errno.ENOSYS):
1165 1165 return _readlock_file(pathname)
1166 1166 else:
1167 1167 raise
1168 1168
1169 1169 def shellquote(s):
1170 1170 if os.sys.platform == 'OpenVMS':
1171 1171 return '"%s"' % s
1172 1172 else:
1173 1173 return "'%s'" % s.replace("'", "'\\''")
1174 1174
1175 1175 def quotecommand(cmd):
1176 1176 return cmd
1177 1177
1178 1178 def popen(command):
1179 1179 return os.popen(command)
1180 1180
1181 1181 def testpid(pid):
1182 1182 '''return False if pid dead, True if running or not sure'''
1183 1183 if os.sys.platform == 'OpenVMS':
1184 1184 return True
1185 1185 try:
1186 1186 os.kill(pid, 0)
1187 1187 return True
1188 1188 except OSError, inst:
1189 1189 return inst.errno != errno.ESRCH
1190 1190
1191 1191 def explain_exit(code):
1192 1192 """return a 2-tuple (desc, code) describing a process's status"""
1193 1193 if os.WIFEXITED(code):
1194 1194 val = os.WEXITSTATUS(code)
1195 1195 return _("exited with status %d") % val, val
1196 1196 elif os.WIFSIGNALED(code):
1197 1197 val = os.WTERMSIG(code)
1198 1198 return _("killed by signal %d") % val, val
1199 1199 elif os.WIFSTOPPED(code):
1200 1200 val = os.WSTOPSIG(code)
1201 1201 return _("stopped by signal %d") % val, val
1202 1202 raise ValueError(_("invalid exit code"))
1203 1203
1204 1204 def isowner(fp, st=None):
1205 1205 """Return True if the file object f belongs to the current user.
1206 1206
1207 1207 The return value of a util.fstat(f) may be passed as the st argument.
1208 1208 """
1209 1209 if st is None:
1210 1210 st = fstat(fp)
1211 1211 return st.st_uid == os.getuid()
1212 1212
1213 1213 def find_in_path(name, path, default=None):
1214 1214 '''find name in search path. path can be string (will be split
1215 1215 with os.pathsep), or iterable thing that returns strings. if name
1216 1216 found, return path to name. else return default.'''
1217 1217 if isinstance(path, str):
1218 1218 path = path.split(os.pathsep)
1219 1219 for p in path:
1220 1220 p_name = os.path.join(p, name)
1221 1221 if os.path.exists(p_name):
1222 1222 return p_name
1223 1223 return default
1224 1224
1225 1225 def set_signal_handler():
1226 1226 pass
1227 1227
1228 1228 def find_exe(name, default=None):
1229 1229 '''find path of an executable.
1230 1230 if name contains a path component, return it as is. otherwise,
1231 1231 use normal executable search path.'''
1232 1232
1233 1233 if os.sep in name or sys.platform == 'OpenVMS':
1234 1234 # don't check the executable bit. if the file isn't
1235 1235 # executable, whoever tries to actually run it will give a
1236 1236 # much more useful error message.
1237 1237 return name
1238 1238 return find_in_path(name, os.environ.get('PATH', ''), default=default)
1239 1239
1240 1240 def _buildencodefun():
1241 1241 e = '_'
1242 1242 win_reserved = [ord(x) for x in '\\:*?"<>|']
1243 1243 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1244 1244 for x in (range(32) + range(126, 256) + win_reserved):
1245 1245 cmap[chr(x)] = "~%02x" % x
1246 1246 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1247 1247 cmap[chr(x)] = e + chr(x).lower()
1248 1248 dmap = {}
1249 1249 for k, v in cmap.iteritems():
1250 1250 dmap[v] = k
1251 1251 def decode(s):
1252 1252 i = 0
1253 1253 while i < len(s):
1254 1254 for l in xrange(1, 4):
1255 1255 try:
1256 1256 yield dmap[s[i:i+l]]
1257 1257 i += l
1258 1258 break
1259 1259 except KeyError:
1260 1260 pass
1261 1261 else:
1262 1262 raise KeyError
1263 1263 return (lambda s: "".join([cmap[c] for c in s]),
1264 1264 lambda s: "".join(list(decode(s))))
1265 1265
1266 1266 encodefilename, decodefilename = _buildencodefun()
1267 1267
1268 1268 def encodedopener(openerfn, fn):
1269 1269 def o(path, *args, **kw):
1270 1270 return openerfn(fn(path), *args, **kw)
1271 1271 return o
1272 1272
1273 1273 def mktempcopy(name, emptyok=False):
1274 1274 """Create a temporary file with the same contents from name
1275 1275
1276 1276 The permission bits are copied from the original file.
1277 1277
1278 1278 If the temporary file is going to be truncated immediately, you
1279 1279 can use emptyok=True as an optimization.
1280 1280
1281 1281 Returns the name of the temporary file.
1282 1282 """
1283 1283 d, fn = os.path.split(name)
1284 1284 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1285 1285 os.close(fd)
1286 1286 # Temporary files are created with mode 0600, which is usually not
1287 1287 # what we want. If the original file already exists, just copy
1288 1288 # its mode. Otherwise, manually obey umask.
1289 1289 try:
1290 1290 st_mode = os.lstat(name).st_mode
1291 1291 except OSError, inst:
1292 1292 if inst.errno != errno.ENOENT:
1293 1293 raise
1294 1294 st_mode = 0666 & ~_umask
1295 1295 os.chmod(temp, st_mode)
1296 1296 if emptyok:
1297 1297 return temp
1298 1298 try:
1299 1299 try:
1300 1300 ifp = posixfile(name, "rb")
1301 1301 except IOError, inst:
1302 1302 if inst.errno == errno.ENOENT:
1303 1303 return temp
1304 1304 if not getattr(inst, 'filename', None):
1305 1305 inst.filename = name
1306 1306 raise
1307 1307 ofp = posixfile(temp, "wb")
1308 1308 for chunk in filechunkiter(ifp):
1309 1309 ofp.write(chunk)
1310 1310 ifp.close()
1311 1311 ofp.close()
1312 1312 except:
1313 1313 try: os.unlink(temp)
1314 1314 except: pass
1315 1315 raise
1316 1316 return temp
1317 1317
1318 1318 class atomictempfile(posixfile):
1319 1319 """file-like object that atomically updates a file
1320 1320
1321 1321 All writes will be redirected to a temporary copy of the original
1322 1322 file. When rename is called, the copy is renamed to the original
1323 1323 name, making the changes visible.
1324 1324 """
1325 1325 def __init__(self, name, mode):
1326 1326 self.__name = name
1327 1327 self.temp = mktempcopy(name, emptyok=('w' in mode))
1328 1328 posixfile.__init__(self, self.temp, mode)
1329 1329
1330 1330 def rename(self):
1331 1331 if not self.closed:
1332 1332 posixfile.close(self)
1333 1333 rename(self.temp, localpath(self.__name))
1334 1334
1335 1335 def __del__(self):
1336 1336 if not self.closed:
1337 1337 try:
1338 1338 os.unlink(self.temp)
1339 1339 except: pass
1340 1340 posixfile.close(self)
1341 1341
1342 1342 class opener(object):
1343 1343 """Open files relative to a base directory
1344 1344
1345 1345 This class is used to hide the details of COW semantics and
1346 1346 remote file access from higher level code.
1347 1347 """
1348 1348 def __init__(self, base, audit=True):
1349 1349 self.base = base
1350 1350 if audit:
1351 1351 self.audit_path = path_auditor(base)
1352 1352 else:
1353 1353 self.audit_path = always
1354 1354
1355 1355 def __getattr__(self, name):
1356 1356 if name == '_can_symlink':
1357 1357 self._can_symlink = checklink(self.base)
1358 1358 return self._can_symlink
1359 1359 raise AttributeError(name)
1360 1360
1361 1361 def __call__(self, path, mode="r", text=False, atomictemp=False):
1362 1362 self.audit_path(path)
1363 1363 f = os.path.join(self.base, path)
1364 1364
1365 1365 if not text and "b" not in mode:
1366 1366 mode += "b" # for that other OS
1367 1367
1368 1368 if mode[0] != "r":
1369 1369 try:
1370 1370 nlink = nlinks(f)
1371 1371 except OSError:
1372 1372 nlink = 0
1373 1373 d = os.path.dirname(f)
1374 1374 if not os.path.isdir(d):
1375 1375 os.makedirs(d)
1376 1376 if atomictemp:
1377 1377 return atomictempfile(f, mode)
1378 1378 if nlink > 1:
1379 1379 rename(mktempcopy(f), f)
1380 1380 return posixfile(f, mode)
1381 1381
1382 1382 def symlink(self, src, dst):
1383 1383 self.audit_path(dst)
1384 1384 linkname = os.path.join(self.base, dst)
1385 1385 try:
1386 1386 os.unlink(linkname)
1387 1387 except OSError:
1388 1388 pass
1389 1389
1390 1390 dirname = os.path.dirname(linkname)
1391 1391 if not os.path.exists(dirname):
1392 1392 os.makedirs(dirname)
1393 1393
1394 1394 if self._can_symlink:
1395 1395 try:
1396 1396 os.symlink(src, linkname)
1397 1397 except OSError, err:
1398 1398 raise OSError(err.errno, _('could not symlink to %r: %s') %
1399 1399 (src, err.strerror), linkname)
1400 1400 else:
1401 1401 f = self(dst, "w")
1402 1402 f.write(src)
1403 1403 f.close()
1404 1404
1405 1405 class chunkbuffer(object):
1406 1406 """Allow arbitrary sized chunks of data to be efficiently read from an
1407 1407 iterator over chunks of arbitrary size."""
1408 1408
1409 1409 def __init__(self, in_iter):
1410 1410 """in_iter is the iterator that's iterating over the input chunks.
1411 1411 targetsize is how big a buffer to try to maintain."""
1412 1412 self.iter = iter(in_iter)
1413 1413 self.buf = ''
1414 1414 self.targetsize = 2**16
1415 1415
1416 1416 def read(self, l):
1417 1417 """Read L bytes of data from the iterator of chunks of data.
1418 1418 Returns less than L bytes if the iterator runs dry."""
1419 1419 if l > len(self.buf) and self.iter:
1420 1420 # Clamp to a multiple of self.targetsize
1421 1421 targetsize = max(l, self.targetsize)
1422 1422 collector = cStringIO.StringIO()
1423 1423 collector.write(self.buf)
1424 1424 collected = len(self.buf)
1425 1425 for chunk in self.iter:
1426 1426 collector.write(chunk)
1427 1427 collected += len(chunk)
1428 1428 if collected >= targetsize:
1429 1429 break
1430 1430 if collected < targetsize:
1431 1431 self.iter = False
1432 1432 self.buf = collector.getvalue()
1433 1433 if len(self.buf) == l:
1434 1434 s, self.buf = str(self.buf), ''
1435 1435 else:
1436 1436 s, self.buf = self.buf[:l], buffer(self.buf, l)
1437 1437 return s
1438 1438
1439 1439 def filechunkiter(f, size=65536, limit=None):
1440 1440 """Create a generator that produces the data in the file size
1441 1441 (default 65536) bytes at a time, up to optional limit (default is
1442 1442 to read all data). Chunks may be less than size bytes if the
1443 1443 chunk is the last chunk in the file, or the file is a socket or
1444 1444 some other type of file that sometimes reads less data than is
1445 1445 requested."""
1446 1446 assert size >= 0
1447 1447 assert limit is None or limit >= 0
1448 1448 while True:
1449 1449 if limit is None: nbytes = size
1450 1450 else: nbytes = min(limit, size)
1451 1451 s = nbytes and f.read(nbytes)
1452 1452 if not s: break
1453 1453 if limit: limit -= len(s)
1454 1454 yield s
1455 1455
1456 1456 def makedate():
1457 1457 lt = time.localtime()
1458 1458 if lt[8] == 1 and time.daylight:
1459 1459 tz = time.altzone
1460 1460 else:
1461 1461 tz = time.timezone
1462 1462 return time.mktime(lt), tz
1463 1463
1464 1464 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True, timezone_format=" %+03d%02d"):
1465 1465 """represent a (unixtime, offset) tuple as a localized time.
1466 1466 unixtime is seconds since the epoch, and offset is the time zone's
1467 1467 number of seconds away from UTC. if timezone is false, do not
1468 1468 append time zone to string."""
1469 1469 t, tz = date or makedate()
1470 1470 s = time.strftime(format, time.gmtime(float(t) - tz))
1471 1471 if timezone:
1472 1472 s += timezone_format % (-tz / 3600, ((-tz % 3600) / 60))
1473 1473 return s
1474 1474
1475 1475 def strdate(string, format, defaults=[]):
1476 1476 """parse a localized time string and return a (unixtime, offset) tuple.
1477 1477 if the string cannot be parsed, ValueError is raised."""
1478 1478 def timezone(string):
1479 1479 tz = string.split()[-1]
1480 1480 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1481 1481 tz = int(tz)
1482 1482 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1483 1483 return offset
1484 1484 if tz == "GMT" or tz == "UTC":
1485 1485 return 0
1486 1486 return None
1487 1487
1488 1488 # NOTE: unixtime = localunixtime + offset
1489 1489 offset, date = timezone(string), string
1490 1490 if offset != None:
1491 1491 date = " ".join(string.split()[:-1])
1492 1492
1493 1493 # add missing elements from defaults
1494 1494 for part in defaults:
1495 1495 found = [True for p in part if ("%"+p) in format]
1496 1496 if not found:
1497 1497 date += "@" + defaults[part]
1498 1498 format += "@%" + part[0]
1499 1499
1500 1500 timetuple = time.strptime(date, format)
1501 1501 localunixtime = int(calendar.timegm(timetuple))
1502 1502 if offset is None:
1503 1503 # local timezone
1504 1504 unixtime = int(time.mktime(timetuple))
1505 1505 offset = unixtime - localunixtime
1506 1506 else:
1507 1507 unixtime = localunixtime + offset
1508 1508 return unixtime, offset
1509 1509
1510 1510 def parsedate(string, formats=None, defaults=None):
1511 1511 """parse a localized time string and return a (unixtime, offset) tuple.
1512 1512 The date may be a "unixtime offset" string or in one of the specified
1513 1513 formats."""
1514 1514 if not string:
1515 1515 return 0, 0
1516 1516 if not formats:
1517 1517 formats = defaultdateformats
1518 1518 string = string.strip()
1519 1519 try:
1520 1520 when, offset = map(int, string.split(' '))
1521 1521 except ValueError:
1522 1522 # fill out defaults
1523 1523 if not defaults:
1524 1524 defaults = {}
1525 1525 now = makedate()
1526 1526 for part in "d mb yY HI M S".split():
1527 1527 if part not in defaults:
1528 1528 if part[0] in "HMS":
1529 1529 defaults[part] = "00"
1530 1530 elif part[0] in "dm":
1531 1531 defaults[part] = "1"
1532 1532 else:
1533 1533 defaults[part] = datestr(now, "%" + part[0], False)
1534 1534
1535 1535 for format in formats:
1536 1536 try:
1537 1537 when, offset = strdate(string, format, defaults)
1538 1538 except ValueError:
1539 1539 pass
1540 1540 else:
1541 1541 break
1542 1542 else:
1543 1543 raise Abort(_('invalid date: %r ') % string)
1544 1544 # validate explicit (probably user-specified) date and
1545 1545 # time zone offset. values must fit in signed 32 bits for
1546 1546 # current 32-bit linux runtimes. timezones go from UTC-12
1547 1547 # to UTC+14
1548 1548 if abs(when) > 0x7fffffff:
1549 1549 raise Abort(_('date exceeds 32 bits: %d') % when)
1550 1550 if offset < -50400 or offset > 43200:
1551 1551 raise Abort(_('impossible time zone offset: %d') % offset)
1552 1552 return when, offset
1553 1553
1554 1554 def matchdate(date):
1555 1555 """Return a function that matches a given date match specifier
1556 1556
1557 1557 Formats include:
1558 1558
1559 1559 '{date}' match a given date to the accuracy provided
1560 1560
1561 1561 '<{date}' on or before a given date
1562 1562
1563 1563 '>{date}' on or after a given date
1564 1564
1565 1565 """
1566 1566
1567 1567 def lower(date):
1568 1568 return parsedate(date, extendeddateformats)[0]
1569 1569
1570 1570 def upper(date):
1571 1571 d = dict(mb="12", HI="23", M="59", S="59")
1572 1572 for days in "31 30 29".split():
1573 1573 try:
1574 1574 d["d"] = days
1575 1575 return parsedate(date, extendeddateformats, d)[0]
1576 1576 except:
1577 1577 pass
1578 1578 d["d"] = "28"
1579 1579 return parsedate(date, extendeddateformats, d)[0]
1580 1580
1581 1581 if date[0] == "<":
1582 1582 when = upper(date[1:])
1583 1583 return lambda x: x <= when
1584 1584 elif date[0] == ">":
1585 1585 when = lower(date[1:])
1586 1586 return lambda x: x >= when
1587 1587 elif date[0] == "-":
1588 1588 try:
1589 1589 days = int(date[1:])
1590 1590 except ValueError:
1591 1591 raise Abort(_("invalid day spec: %s") % date[1:])
1592 1592 when = makedate()[0] - days * 3600 * 24
1593 1593 return lambda x: x >= when
1594 1594 elif " to " in date:
1595 1595 a, b = date.split(" to ")
1596 1596 start, stop = lower(a), upper(b)
1597 1597 return lambda x: x >= start and x <= stop
1598 1598 else:
1599 1599 start, stop = lower(date), upper(date)
1600 1600 return lambda x: x >= start and x <= stop
1601 1601
1602 1602 def shortuser(user):
1603 1603 """Return a short representation of a user name or email address."""
1604 1604 f = user.find('@')
1605 1605 if f >= 0:
1606 1606 user = user[:f]
1607 1607 f = user.find('<')
1608 1608 if f >= 0:
1609 1609 user = user[f+1:]
1610 1610 f = user.find(' ')
1611 1611 if f >= 0:
1612 1612 user = user[:f]
1613 1613 f = user.find('.')
1614 1614 if f >= 0:
1615 1615 user = user[:f]
1616 1616 return user
1617 1617
1618 1618 def ellipsis(text, maxlength=400):
1619 1619 """Trim string to at most maxlength (default: 400) characters."""
1620 1620 if len(text) <= maxlength:
1621 1621 return text
1622 1622 else:
1623 1623 return "%s..." % (text[:maxlength-3])
1624 1624
1625 1625 def walkrepos(path):
1626 1626 '''yield every hg repository under path, recursively.'''
1627 1627 def errhandler(err):
1628 1628 if err.filename == path:
1629 1629 raise err
1630 1630
1631 1631 for root, dirs, files in os.walk(path, onerror=errhandler):
1632 1632 for d in dirs:
1633 1633 if d == '.hg':
1634 1634 yield root
1635 1635 dirs[:] = []
1636 1636 break
1637 1637
1638 1638 _rcpath = None
1639 1639
1640 1640 def os_rcpath():
1641 1641 '''return default os-specific hgrc search path'''
1642 1642 path = system_rcpath()
1643 1643 path.extend(user_rcpath())
1644 1644 path = [os.path.normpath(f) for f in path]
1645 1645 return path
1646 1646
1647 1647 def rcpath():
1648 1648 '''return hgrc search path. if env var HGRCPATH is set, use it.
1649 1649 for each item in path, if directory, use files ending in .rc,
1650 1650 else use item.
1651 1651 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1652 1652 if no HGRCPATH, use default os-specific path.'''
1653 1653 global _rcpath
1654 1654 if _rcpath is None:
1655 1655 if 'HGRCPATH' in os.environ:
1656 1656 _rcpath = []
1657 1657 for p in os.environ['HGRCPATH'].split(os.pathsep):
1658 1658 if not p: continue
1659 1659 if os.path.isdir(p):
1660 1660 for f, kind in osutil.listdir(p):
1661 1661 if f.endswith('.rc'):
1662 1662 _rcpath.append(os.path.join(p, f))
1663 1663 else:
1664 1664 _rcpath.append(p)
1665 1665 else:
1666 1666 _rcpath = os_rcpath()
1667 1667 return _rcpath
1668 1668
1669 1669 def bytecount(nbytes):
1670 1670 '''return byte count formatted as readable string, with units'''
1671 1671
1672 1672 units = (
1673 1673 (100, 1<<30, _('%.0f GB')),
1674 1674 (10, 1<<30, _('%.1f GB')),
1675 1675 (1, 1<<30, _('%.2f GB')),
1676 1676 (100, 1<<20, _('%.0f MB')),
1677 1677 (10, 1<<20, _('%.1f MB')),
1678 1678 (1, 1<<20, _('%.2f MB')),
1679 1679 (100, 1<<10, _('%.0f KB')),
1680 1680 (10, 1<<10, _('%.1f KB')),
1681 1681 (1, 1<<10, _('%.2f KB')),
1682 1682 (1, 1, _('%.0f bytes')),
1683 1683 )
1684 1684
1685 1685 for multiplier, divisor, format in units:
1686 1686 if nbytes >= divisor * multiplier:
1687 1687 return format % (nbytes / float(divisor))
1688 1688 return units[-1][2] % nbytes
1689 1689
1690 1690 def drop_scheme(scheme, path):
1691 1691 sc = scheme + ':'
1692 1692 if path.startswith(sc):
1693 1693 path = path[len(sc):]
1694 1694 if path.startswith('//'):
1695 1695 path = path[2:]
1696 1696 return path
1697 1697
1698 1698 def uirepr(s):
1699 1699 # Avoid double backslash in Windows path repr()
1700 1700 return repr(s).replace('\\\\', '\\')
General Comments 0
You need to be logged in to leave comments. Login now