##// END OF EJS Templates
revpair: handle odd ranges (issue3474)
Matt Mackall -
r16790:2a0efa11 stable
parent child Browse files
Show More
@@ -1,884 +1,886 b''
1 1 # scmutil.py - Mercurial core utility functions
2 2 #
3 3 # Copyright Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import util, error, osutil, revset, similar, encoding
10 10 import match as matchmod
11 11 import os, errno, re, stat, sys, glob
12 12
13 13 def nochangesfound(ui, secretlist=None):
14 14 '''report no changes for push/pull'''
15 15 if secretlist:
16 16 ui.status(_("no changes found (ignored %d secret changesets)\n")
17 17 % len(secretlist))
18 18 else:
19 19 ui.status(_("no changes found\n"))
20 20
21 21 def checkfilename(f):
22 22 '''Check that the filename f is an acceptable filename for a tracked file'''
23 23 if '\r' in f or '\n' in f:
24 24 raise util.Abort(_("'\\n' and '\\r' disallowed in filenames: %r") % f)
25 25
26 26 def checkportable(ui, f):
27 27 '''Check if filename f is portable and warn or abort depending on config'''
28 28 checkfilename(f)
29 29 abort, warn = checkportabilityalert(ui)
30 30 if abort or warn:
31 31 msg = util.checkwinfilename(f)
32 32 if msg:
33 33 msg = "%s: %r" % (msg, f)
34 34 if abort:
35 35 raise util.Abort(msg)
36 36 ui.warn(_("warning: %s\n") % msg)
37 37
38 38 def checkportabilityalert(ui):
39 39 '''check if the user's config requests nothing, a warning, or abort for
40 40 non-portable filenames'''
41 41 val = ui.config('ui', 'portablefilenames', 'warn')
42 42 lval = val.lower()
43 43 bval = util.parsebool(val)
44 44 abort = os.name == 'nt' or lval == 'abort'
45 45 warn = bval or lval == 'warn'
46 46 if bval is None and not (warn or abort or lval == 'ignore'):
47 47 raise error.ConfigError(
48 48 _("ui.portablefilenames value is invalid ('%s')") % val)
49 49 return abort, warn
50 50
51 51 class casecollisionauditor(object):
52 52 def __init__(self, ui, abort, existingiter):
53 53 self._ui = ui
54 54 self._abort = abort
55 55 self._map = {}
56 56 for f in existingiter:
57 57 self._map[encoding.lower(f)] = f
58 58
59 59 def __call__(self, f):
60 60 fl = encoding.lower(f)
61 61 map = self._map
62 62 if fl in map and map[fl] != f:
63 63 msg = _('possible case-folding collision for %s') % f
64 64 if self._abort:
65 65 raise util.Abort(msg)
66 66 self._ui.warn(_("warning: %s\n") % msg)
67 67 map[fl] = f
68 68
69 69 class pathauditor(object):
70 70 '''ensure that a filesystem path contains no banned components.
71 71 the following properties of a path are checked:
72 72
73 73 - ends with a directory separator
74 74 - under top-level .hg
75 75 - starts at the root of a windows drive
76 76 - contains ".."
77 77 - traverses a symlink (e.g. a/symlink_here/b)
78 78 - inside a nested repository (a callback can be used to approve
79 79 some nested repositories, e.g., subrepositories)
80 80 '''
81 81
82 82 def __init__(self, root, callback=None):
83 83 self.audited = set()
84 84 self.auditeddir = set()
85 85 self.root = root
86 86 self.callback = callback
87 87 if os.path.lexists(root) and not util.checkcase(root):
88 88 self.normcase = util.normcase
89 89 else:
90 90 self.normcase = lambda x: x
91 91
92 92 def __call__(self, path):
93 93 '''Check the relative path.
94 94 path may contain a pattern (e.g. foodir/**.txt)'''
95 95
96 96 path = util.localpath(path)
97 97 normpath = self.normcase(path)
98 98 if normpath in self.audited:
99 99 return
100 100 # AIX ignores "/" at end of path, others raise EISDIR.
101 101 if util.endswithsep(path):
102 102 raise util.Abort(_("path ends in directory separator: %s") % path)
103 103 parts = util.splitpath(path)
104 104 if (os.path.splitdrive(path)[0]
105 105 or parts[0].lower() in ('.hg', '.hg.', '')
106 106 or os.pardir in parts):
107 107 raise util.Abort(_("path contains illegal component: %s") % path)
108 108 if '.hg' in path.lower():
109 109 lparts = [p.lower() for p in parts]
110 110 for p in '.hg', '.hg.':
111 111 if p in lparts[1:]:
112 112 pos = lparts.index(p)
113 113 base = os.path.join(*parts[:pos])
114 114 raise util.Abort(_("path '%s' is inside nested repo %r")
115 115 % (path, base))
116 116
117 117 normparts = util.splitpath(normpath)
118 118 assert len(parts) == len(normparts)
119 119
120 120 parts.pop()
121 121 normparts.pop()
122 122 prefixes = []
123 123 while parts:
124 124 prefix = os.sep.join(parts)
125 125 normprefix = os.sep.join(normparts)
126 126 if normprefix in self.auditeddir:
127 127 break
128 128 curpath = os.path.join(self.root, prefix)
129 129 try:
130 130 st = os.lstat(curpath)
131 131 except OSError, err:
132 132 # EINVAL can be raised as invalid path syntax under win32.
133 133 # They must be ignored for patterns can be checked too.
134 134 if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL):
135 135 raise
136 136 else:
137 137 if stat.S_ISLNK(st.st_mode):
138 138 raise util.Abort(
139 139 _('path %r traverses symbolic link %r')
140 140 % (path, prefix))
141 141 elif (stat.S_ISDIR(st.st_mode) and
142 142 os.path.isdir(os.path.join(curpath, '.hg'))):
143 143 if not self.callback or not self.callback(curpath):
144 144 raise util.Abort(_("path '%s' is inside nested repo %r") %
145 145 (path, prefix))
146 146 prefixes.append(normprefix)
147 147 parts.pop()
148 148 normparts.pop()
149 149
150 150 self.audited.add(normpath)
151 151 # only add prefixes to the cache after checking everything: we don't
152 152 # want to add "foo/bar/baz" before checking if there's a "foo/.hg"
153 153 self.auditeddir.update(prefixes)
154 154
155 155 class abstractopener(object):
156 156 """Abstract base class; cannot be instantiated"""
157 157
158 158 def __init__(self, *args, **kwargs):
159 159 '''Prevent instantiation; don't call this from subclasses.'''
160 160 raise NotImplementedError('attempted instantiating ' + str(type(self)))
161 161
162 162 def tryread(self, path):
163 163 '''gracefully return an empty string for missing files'''
164 164 try:
165 165 return self.read(path)
166 166 except IOError, inst:
167 167 if inst.errno != errno.ENOENT:
168 168 raise
169 169 return ""
170 170
171 171 def read(self, path):
172 172 fp = self(path, 'rb')
173 173 try:
174 174 return fp.read()
175 175 finally:
176 176 fp.close()
177 177
178 178 def write(self, path, data):
179 179 fp = self(path, 'wb')
180 180 try:
181 181 return fp.write(data)
182 182 finally:
183 183 fp.close()
184 184
185 185 def append(self, path, data):
186 186 fp = self(path, 'ab')
187 187 try:
188 188 return fp.write(data)
189 189 finally:
190 190 fp.close()
191 191
192 192 class opener(abstractopener):
193 193 '''Open files relative to a base directory
194 194
195 195 This class is used to hide the details of COW semantics and
196 196 remote file access from higher level code.
197 197 '''
198 198 def __init__(self, base, audit=True):
199 199 self.base = base
200 200 self._audit = audit
201 201 if audit:
202 202 self.auditor = pathauditor(base)
203 203 else:
204 204 self.auditor = util.always
205 205 self.createmode = None
206 206 self._trustnlink = None
207 207
208 208 @util.propertycache
209 209 def _cansymlink(self):
210 210 return util.checklink(self.base)
211 211
212 212 def _fixfilemode(self, name):
213 213 if self.createmode is None:
214 214 return
215 215 os.chmod(name, self.createmode & 0666)
216 216
217 217 def __call__(self, path, mode="r", text=False, atomictemp=False):
218 218 if self._audit:
219 219 r = util.checkosfilename(path)
220 220 if r:
221 221 raise util.Abort("%s: %r" % (r, path))
222 222 self.auditor(path)
223 223 f = self.join(path)
224 224
225 225 if not text and "b" not in mode:
226 226 mode += "b" # for that other OS
227 227
228 228 nlink = -1
229 229 dirname, basename = os.path.split(f)
230 230 # If basename is empty, then the path is malformed because it points
231 231 # to a directory. Let the posixfile() call below raise IOError.
232 232 if basename and mode not in ('r', 'rb'):
233 233 if atomictemp:
234 234 if not os.path.isdir(dirname):
235 235 util.makedirs(dirname, self.createmode)
236 236 return util.atomictempfile(f, mode, self.createmode)
237 237 try:
238 238 if 'w' in mode:
239 239 util.unlink(f)
240 240 nlink = 0
241 241 else:
242 242 # nlinks() may behave differently for files on Windows
243 243 # shares if the file is open.
244 244 fd = util.posixfile(f)
245 245 nlink = util.nlinks(f)
246 246 if nlink < 1:
247 247 nlink = 2 # force mktempcopy (issue1922)
248 248 fd.close()
249 249 except (OSError, IOError), e:
250 250 if e.errno != errno.ENOENT:
251 251 raise
252 252 nlink = 0
253 253 if not os.path.isdir(dirname):
254 254 util.makedirs(dirname, self.createmode)
255 255 if nlink > 0:
256 256 if self._trustnlink is None:
257 257 self._trustnlink = nlink > 1 or util.checknlink(f)
258 258 if nlink > 1 or not self._trustnlink:
259 259 util.rename(util.mktempcopy(f), f)
260 260 fp = util.posixfile(f, mode)
261 261 if nlink == 0:
262 262 self._fixfilemode(f)
263 263 return fp
264 264
265 265 def symlink(self, src, dst):
266 266 self.auditor(dst)
267 267 linkname = self.join(dst)
268 268 try:
269 269 os.unlink(linkname)
270 270 except OSError:
271 271 pass
272 272
273 273 dirname = os.path.dirname(linkname)
274 274 if not os.path.exists(dirname):
275 275 util.makedirs(dirname, self.createmode)
276 276
277 277 if self._cansymlink:
278 278 try:
279 279 os.symlink(src, linkname)
280 280 except OSError, err:
281 281 raise OSError(err.errno, _('could not symlink to %r: %s') %
282 282 (src, err.strerror), linkname)
283 283 else:
284 284 f = self(dst, "w")
285 285 f.write(src)
286 286 f.close()
287 287 self._fixfilemode(dst)
288 288
289 289 def audit(self, path):
290 290 self.auditor(path)
291 291
292 292 def join(self, path):
293 293 return os.path.join(self.base, path)
294 294
295 295 class filteropener(abstractopener):
296 296 '''Wrapper opener for filtering filenames with a function.'''
297 297
298 298 def __init__(self, opener, filter):
299 299 self._filter = filter
300 300 self._orig = opener
301 301
302 302 def __call__(self, path, *args, **kwargs):
303 303 return self._orig(self._filter(path), *args, **kwargs)
304 304
305 305 def canonpath(root, cwd, myname, auditor=None):
306 306 '''return the canonical path of myname, given cwd and root'''
307 307 if util.endswithsep(root):
308 308 rootsep = root
309 309 else:
310 310 rootsep = root + os.sep
311 311 name = myname
312 312 if not os.path.isabs(name):
313 313 name = os.path.join(root, cwd, name)
314 314 name = os.path.normpath(name)
315 315 if auditor is None:
316 316 auditor = pathauditor(root)
317 317 if name != rootsep and name.startswith(rootsep):
318 318 name = name[len(rootsep):]
319 319 auditor(name)
320 320 return util.pconvert(name)
321 321 elif name == root:
322 322 return ''
323 323 else:
324 324 # Determine whether `name' is in the hierarchy at or beneath `root',
325 325 # by iterating name=dirname(name) until that causes no change (can't
326 326 # check name == '/', because that doesn't work on windows). For each
327 327 # `name', compare dev/inode numbers. If they match, the list `rel'
328 328 # holds the reversed list of components making up the relative file
329 329 # name we want.
330 330 root_st = os.stat(root)
331 331 rel = []
332 332 while True:
333 333 try:
334 334 name_st = os.stat(name)
335 335 except OSError:
336 336 name_st = None
337 337 if name_st and util.samestat(name_st, root_st):
338 338 if not rel:
339 339 # name was actually the same as root (maybe a symlink)
340 340 return ''
341 341 rel.reverse()
342 342 name = os.path.join(*rel)
343 343 auditor(name)
344 344 return util.pconvert(name)
345 345 dirname, basename = os.path.split(name)
346 346 rel.append(basename)
347 347 if dirname == name:
348 348 break
349 349 name = dirname
350 350
351 351 raise util.Abort('%s not under root' % myname)
352 352
353 353 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
354 354 '''yield every hg repository under path, recursively.'''
355 355 def errhandler(err):
356 356 if err.filename == path:
357 357 raise err
358 358 samestat = getattr(os.path, 'samestat', None)
359 359 if followsym and samestat is not None:
360 360 def adddir(dirlst, dirname):
361 361 match = False
362 362 dirstat = os.stat(dirname)
363 363 for lstdirstat in dirlst:
364 364 if samestat(dirstat, lstdirstat):
365 365 match = True
366 366 break
367 367 if not match:
368 368 dirlst.append(dirstat)
369 369 return not match
370 370 else:
371 371 followsym = False
372 372
373 373 if (seen_dirs is None) and followsym:
374 374 seen_dirs = []
375 375 adddir(seen_dirs, path)
376 376 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
377 377 dirs.sort()
378 378 if '.hg' in dirs:
379 379 yield root # found a repository
380 380 qroot = os.path.join(root, '.hg', 'patches')
381 381 if os.path.isdir(os.path.join(qroot, '.hg')):
382 382 yield qroot # we have a patch queue repo here
383 383 if recurse:
384 384 # avoid recursing inside the .hg directory
385 385 dirs.remove('.hg')
386 386 else:
387 387 dirs[:] = [] # don't descend further
388 388 elif followsym:
389 389 newdirs = []
390 390 for d in dirs:
391 391 fname = os.path.join(root, d)
392 392 if adddir(seen_dirs, fname):
393 393 if os.path.islink(fname):
394 394 for hgname in walkrepos(fname, True, seen_dirs):
395 395 yield hgname
396 396 else:
397 397 newdirs.append(d)
398 398 dirs[:] = newdirs
399 399
400 400 def osrcpath():
401 401 '''return default os-specific hgrc search path'''
402 402 path = systemrcpath()
403 403 path.extend(userrcpath())
404 404 path = [os.path.normpath(f) for f in path]
405 405 return path
406 406
407 407 _rcpath = None
408 408
409 409 def rcpath():
410 410 '''return hgrc search path. if env var HGRCPATH is set, use it.
411 411 for each item in path, if directory, use files ending in .rc,
412 412 else use item.
413 413 make HGRCPATH empty to only look in .hg/hgrc of current repo.
414 414 if no HGRCPATH, use default os-specific path.'''
415 415 global _rcpath
416 416 if _rcpath is None:
417 417 if 'HGRCPATH' in os.environ:
418 418 _rcpath = []
419 419 for p in os.environ['HGRCPATH'].split(os.pathsep):
420 420 if not p:
421 421 continue
422 422 p = util.expandpath(p)
423 423 if os.path.isdir(p):
424 424 for f, kind in osutil.listdir(p):
425 425 if f.endswith('.rc'):
426 426 _rcpath.append(os.path.join(p, f))
427 427 else:
428 428 _rcpath.append(p)
429 429 else:
430 430 _rcpath = osrcpath()
431 431 return _rcpath
432 432
433 433 if os.name != 'nt':
434 434
435 435 def rcfiles(path):
436 436 rcs = [os.path.join(path, 'hgrc')]
437 437 rcdir = os.path.join(path, 'hgrc.d')
438 438 try:
439 439 rcs.extend([os.path.join(rcdir, f)
440 440 for f, kind in osutil.listdir(rcdir)
441 441 if f.endswith(".rc")])
442 442 except OSError:
443 443 pass
444 444 return rcs
445 445
446 446 def systemrcpath():
447 447 path = []
448 448 if sys.platform == 'plan9':
449 449 root = 'lib/mercurial'
450 450 else:
451 451 root = 'etc/mercurial'
452 452 # old mod_python does not set sys.argv
453 453 if len(getattr(sys, 'argv', [])) > 0:
454 454 p = os.path.dirname(os.path.dirname(sys.argv[0]))
455 455 path.extend(rcfiles(os.path.join(p, root)))
456 456 path.extend(rcfiles('/' + root))
457 457 return path
458 458
459 459 def userrcpath():
460 460 if sys.platform == 'plan9':
461 461 return [os.environ['home'] + '/lib/hgrc']
462 462 else:
463 463 return [os.path.expanduser('~/.hgrc')]
464 464
465 465 else:
466 466
467 467 _HKEY_LOCAL_MACHINE = 0x80000002L
468 468
469 469 def systemrcpath():
470 470 '''return default os-specific hgrc search path'''
471 471 rcpath = []
472 472 filename = util.executablepath()
473 473 # Use mercurial.ini found in directory with hg.exe
474 474 progrc = os.path.join(os.path.dirname(filename), 'mercurial.ini')
475 475 if os.path.isfile(progrc):
476 476 rcpath.append(progrc)
477 477 return rcpath
478 478 # Use hgrc.d found in directory with hg.exe
479 479 progrcd = os.path.join(os.path.dirname(filename), 'hgrc.d')
480 480 if os.path.isdir(progrcd):
481 481 for f, kind in osutil.listdir(progrcd):
482 482 if f.endswith('.rc'):
483 483 rcpath.append(os.path.join(progrcd, f))
484 484 return rcpath
485 485 # else look for a system rcpath in the registry
486 486 value = util.lookupreg('SOFTWARE\\Mercurial', None,
487 487 _HKEY_LOCAL_MACHINE)
488 488 if not isinstance(value, str) or not value:
489 489 return rcpath
490 490 value = util.localpath(value)
491 491 for p in value.split(os.pathsep):
492 492 if p.lower().endswith('mercurial.ini'):
493 493 rcpath.append(p)
494 494 elif os.path.isdir(p):
495 495 for f, kind in osutil.listdir(p):
496 496 if f.endswith('.rc'):
497 497 rcpath.append(os.path.join(p, f))
498 498 return rcpath
499 499
500 500 def userrcpath():
501 501 '''return os-specific hgrc search path to the user dir'''
502 502 home = os.path.expanduser('~')
503 503 path = [os.path.join(home, 'mercurial.ini'),
504 504 os.path.join(home, '.hgrc')]
505 505 userprofile = os.environ.get('USERPROFILE')
506 506 if userprofile:
507 507 path.append(os.path.join(userprofile, 'mercurial.ini'))
508 508 path.append(os.path.join(userprofile, '.hgrc'))
509 509 return path
510 510
511 511 def revsingle(repo, revspec, default='.'):
512 512 if not revspec:
513 513 return repo[default]
514 514
515 515 l = revrange(repo, [revspec])
516 516 if len(l) < 1:
517 517 raise util.Abort(_('empty revision set'))
518 518 return repo[l[-1]]
519 519
520 520 def revpair(repo, revs):
521 521 if not revs:
522 522 return repo.dirstate.p1(), None
523 523
524 524 l = revrange(repo, revs)
525 525
526 526 if len(l) == 0:
527 if revs:
528 raise util.Abort(_('empty revision range'))
527 529 return repo.dirstate.p1(), None
528 530
529 if len(l) == 1:
531 if len(l) == 1 and len(revs) == 1 and _revrangesep not in revs[0]:
530 532 return repo.lookup(l[0]), None
531 533
532 534 return repo.lookup(l[0]), repo.lookup(l[-1])
533 535
534 536 _revrangesep = ':'
535 537
536 538 def revrange(repo, revs):
537 539 """Yield revision as strings from a list of revision specifications."""
538 540
539 541 def revfix(repo, val, defval):
540 542 if not val and val != 0 and defval is not None:
541 543 return defval
542 544 return repo[val].rev()
543 545
544 546 seen, l = set(), []
545 547 for spec in revs:
546 548 if l and not seen:
547 549 seen = set(l)
548 550 # attempt to parse old-style ranges first to deal with
549 551 # things like old-tag which contain query metacharacters
550 552 try:
551 553 if isinstance(spec, int):
552 554 seen.add(spec)
553 555 l.append(spec)
554 556 continue
555 557
556 558 if _revrangesep in spec:
557 559 start, end = spec.split(_revrangesep, 1)
558 560 start = revfix(repo, start, 0)
559 561 end = revfix(repo, end, len(repo) - 1)
560 562 step = start > end and -1 or 1
561 563 if not seen and not l:
562 564 # by far the most common case: revs = ["-1:0"]
563 565 l = range(start, end + step, step)
564 566 # defer syncing seen until next iteration
565 567 continue
566 568 newrevs = set(xrange(start, end + step, step))
567 569 if seen:
568 570 newrevs.difference_update(seen)
569 571 seen.union(newrevs)
570 572 else:
571 573 seen = newrevs
572 574 l.extend(sorted(newrevs, reverse=start > end))
573 575 continue
574 576 elif spec and spec in repo: # single unquoted rev
575 577 rev = revfix(repo, spec, None)
576 578 if rev in seen:
577 579 continue
578 580 seen.add(rev)
579 581 l.append(rev)
580 582 continue
581 583 except error.RepoLookupError:
582 584 pass
583 585
584 586 # fall through to new-style queries if old-style fails
585 587 m = revset.match(repo.ui, spec)
586 588 for r in m(repo, range(len(repo))):
587 589 if r not in seen:
588 590 l.append(r)
589 591 seen.update(l)
590 592
591 593 return l
592 594
593 595 def expandpats(pats):
594 596 if not util.expandglobs:
595 597 return list(pats)
596 598 ret = []
597 599 for p in pats:
598 600 kind, name = matchmod._patsplit(p, None)
599 601 if kind is None:
600 602 try:
601 603 globbed = glob.glob(name)
602 604 except re.error:
603 605 globbed = [name]
604 606 if globbed:
605 607 ret.extend(globbed)
606 608 continue
607 609 ret.append(p)
608 610 return ret
609 611
610 612 def matchandpats(ctx, pats=[], opts={}, globbed=False, default='relpath'):
611 613 if pats == ("",):
612 614 pats = []
613 615 if not globbed and default == 'relpath':
614 616 pats = expandpats(pats or [])
615 617
616 618 m = ctx.match(pats, opts.get('include'), opts.get('exclude'),
617 619 default)
618 620 def badfn(f, msg):
619 621 ctx._repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
620 622 m.bad = badfn
621 623 return m, pats
622 624
623 625 def match(ctx, pats=[], opts={}, globbed=False, default='relpath'):
624 626 return matchandpats(ctx, pats, opts, globbed, default)[0]
625 627
626 628 def matchall(repo):
627 629 return matchmod.always(repo.root, repo.getcwd())
628 630
629 631 def matchfiles(repo, files):
630 632 return matchmod.exact(repo.root, repo.getcwd(), files)
631 633
632 634 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
633 635 if dry_run is None:
634 636 dry_run = opts.get('dry_run')
635 637 if similarity is None:
636 638 similarity = float(opts.get('similarity') or 0)
637 639 # we'd use status here, except handling of symlinks and ignore is tricky
638 640 added, unknown, deleted, removed = [], [], [], []
639 641 audit_path = pathauditor(repo.root)
640 642 m = match(repo[None], pats, opts)
641 643 rejected = []
642 644 m.bad = lambda x, y: rejected.append(x)
643 645
644 646 for abs in repo.walk(m):
645 647 target = repo.wjoin(abs)
646 648 good = True
647 649 try:
648 650 audit_path(abs)
649 651 except (OSError, util.Abort):
650 652 good = False
651 653 rel = m.rel(abs)
652 654 exact = m.exact(abs)
653 655 if good and abs not in repo.dirstate:
654 656 unknown.append(abs)
655 657 if repo.ui.verbose or not exact:
656 658 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
657 659 elif repo.dirstate[abs] != 'r' and (not good or not os.path.lexists(target)
658 660 or (os.path.isdir(target) and not os.path.islink(target))):
659 661 deleted.append(abs)
660 662 if repo.ui.verbose or not exact:
661 663 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
662 664 # for finding renames
663 665 elif repo.dirstate[abs] == 'r':
664 666 removed.append(abs)
665 667 elif repo.dirstate[abs] == 'a':
666 668 added.append(abs)
667 669 copies = {}
668 670 if similarity > 0:
669 671 for old, new, score in similar.findrenames(repo,
670 672 added + unknown, removed + deleted, similarity):
671 673 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
672 674 repo.ui.status(_('recording removal of %s as rename to %s '
673 675 '(%d%% similar)\n') %
674 676 (m.rel(old), m.rel(new), score * 100))
675 677 copies[new] = old
676 678
677 679 if not dry_run:
678 680 wctx = repo[None]
679 681 wlock = repo.wlock()
680 682 try:
681 683 wctx.forget(deleted)
682 684 wctx.add(unknown)
683 685 for new, old in copies.iteritems():
684 686 wctx.copy(old, new)
685 687 finally:
686 688 wlock.release()
687 689
688 690 for f in rejected:
689 691 if f in m.files():
690 692 return 1
691 693 return 0
692 694
693 695 def updatedir(ui, repo, patches, similarity=0):
694 696 '''Update dirstate after patch application according to metadata'''
695 697 if not patches:
696 698 return []
697 699 copies = []
698 700 removes = set()
699 701 cfiles = patches.keys()
700 702 cwd = repo.getcwd()
701 703 if cwd:
702 704 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
703 705 for f in patches:
704 706 gp = patches[f]
705 707 if not gp:
706 708 continue
707 709 if gp.op == 'RENAME':
708 710 copies.append((gp.oldpath, gp.path))
709 711 removes.add(gp.oldpath)
710 712 elif gp.op == 'COPY':
711 713 copies.append((gp.oldpath, gp.path))
712 714 elif gp.op == 'DELETE':
713 715 removes.add(gp.path)
714 716
715 717 wctx = repo[None]
716 718 for src, dst in copies:
717 719 dirstatecopy(ui, repo, wctx, src, dst, cwd=cwd)
718 720 if (not similarity) and removes:
719 721 wctx.remove(sorted(removes), True)
720 722
721 723 for f in patches:
722 724 gp = patches[f]
723 725 if gp and gp.mode:
724 726 islink, isexec = gp.mode
725 727 dst = repo.wjoin(gp.path)
726 728 # patch won't create empty files
727 729 if gp.op == 'ADD' and not os.path.lexists(dst):
728 730 flags = (isexec and 'x' or '') + (islink and 'l' or '')
729 731 repo.wwrite(gp.path, '', flags)
730 732 util.setflags(dst, islink, isexec)
731 733 addremove(repo, cfiles, similarity=similarity)
732 734 files = patches.keys()
733 735 files.extend([r for r in removes if r not in files])
734 736 return sorted(files)
735 737
736 738 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
737 739 """Update the dirstate to reflect the intent of copying src to dst. For
738 740 different reasons it might not end with dst being marked as copied from src.
739 741 """
740 742 origsrc = repo.dirstate.copied(src) or src
741 743 if dst == origsrc: # copying back a copy?
742 744 if repo.dirstate[dst] not in 'mn' and not dryrun:
743 745 repo.dirstate.normallookup(dst)
744 746 else:
745 747 if repo.dirstate[origsrc] == 'a' and origsrc == src:
746 748 if not ui.quiet:
747 749 ui.warn(_("%s has not been committed yet, so no copy "
748 750 "data will be stored for %s.\n")
749 751 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd)))
750 752 if repo.dirstate[dst] in '?r' and not dryrun:
751 753 wctx.add([dst])
752 754 elif not dryrun:
753 755 wctx.copy(origsrc, dst)
754 756
755 757 def readrequires(opener, supported):
756 758 '''Reads and parses .hg/requires and checks if all entries found
757 759 are in the list of supported features.'''
758 760 requirements = set(opener.read("requires").splitlines())
759 761 missings = []
760 762 for r in requirements:
761 763 if r not in supported:
762 764 if not r or not r[0].isalnum():
763 765 raise error.RequirementError(_(".hg/requires file is corrupt"))
764 766 missings.append(r)
765 767 missings.sort()
766 768 if missings:
767 769 raise error.RequirementError(_("unknown repository format: "
768 770 "requires features '%s' (upgrade Mercurial)") % "', '".join(missings))
769 771 return requirements
770 772
771 773 class filecacheentry(object):
772 774 def __init__(self, path):
773 775 self.path = path
774 776 self.cachestat = filecacheentry.stat(self.path)
775 777
776 778 if self.cachestat:
777 779 self._cacheable = self.cachestat.cacheable()
778 780 else:
779 781 # None means we don't know yet
780 782 self._cacheable = None
781 783
782 784 def refresh(self):
783 785 if self.cacheable():
784 786 self.cachestat = filecacheentry.stat(self.path)
785 787
786 788 def cacheable(self):
787 789 if self._cacheable is not None:
788 790 return self._cacheable
789 791
790 792 # we don't know yet, assume it is for now
791 793 return True
792 794
793 795 def changed(self):
794 796 # no point in going further if we can't cache it
795 797 if not self.cacheable():
796 798 return True
797 799
798 800 newstat = filecacheentry.stat(self.path)
799 801
800 802 # we may not know if it's cacheable yet, check again now
801 803 if newstat and self._cacheable is None:
802 804 self._cacheable = newstat.cacheable()
803 805
804 806 # check again
805 807 if not self._cacheable:
806 808 return True
807 809
808 810 if self.cachestat != newstat:
809 811 self.cachestat = newstat
810 812 return True
811 813 else:
812 814 return False
813 815
814 816 @staticmethod
815 817 def stat(path):
816 818 try:
817 819 return util.cachestat(path)
818 820 except OSError, e:
819 821 if e.errno != errno.ENOENT:
820 822 raise
821 823
822 824 class filecache(object):
823 825 '''A property like decorator that tracks a file under .hg/ for updates.
824 826
825 827 Records stat info when called in _filecache.
826 828
827 829 On subsequent calls, compares old stat info with new info, and recreates
828 830 the object when needed, updating the new stat info in _filecache.
829 831
830 832 Mercurial either atomic renames or appends for files under .hg,
831 833 so to ensure the cache is reliable we need the filesystem to be able
832 834 to tell us if a file has been replaced. If it can't, we fallback to
833 835 recreating the object on every call (essentially the same behaviour as
834 836 propertycache).'''
835 837 def __init__(self, path):
836 838 self.path = path
837 839
838 840 def join(self, obj, fname):
839 841 """Used to compute the runtime path of the cached file.
840 842
841 843 Users should subclass filecache and provide their own version of this
842 844 function to call the appropriate join function on 'obj' (an instance
843 845 of the class that its member function was decorated).
844 846 """
845 847 return obj.join(fname)
846 848
847 849 def __call__(self, func):
848 850 self.func = func
849 851 self.name = func.__name__
850 852 return self
851 853
852 854 def __get__(self, obj, type=None):
853 855 # do we need to check if the file changed?
854 856 if self.name in obj.__dict__:
855 857 return obj.__dict__[self.name]
856 858
857 859 entry = obj._filecache.get(self.name)
858 860
859 861 if entry:
860 862 if entry.changed():
861 863 entry.obj = self.func(obj)
862 864 else:
863 865 path = self.join(obj, self.path)
864 866
865 867 # We stat -before- creating the object so our cache doesn't lie if
866 868 # a writer modified between the time we read and stat
867 869 entry = filecacheentry(path)
868 870 entry.obj = self.func(obj)
869 871
870 872 obj._filecache[self.name] = entry
871 873
872 874 obj.__dict__[self.name] = entry.obj
873 875 return entry.obj
874 876
875 877 def __set__(self, obj, value):
876 878 if self.name in obj._filecache:
877 879 obj._filecache[self.name].obj = value # update cached copy
878 880 obj.__dict__[self.name] = value # update copy returned by obj.x
879 881
880 882 def __delete__(self, obj):
881 883 try:
882 884 del obj.__dict__[self.name]
883 885 except KeyError:
884 886 raise AttributeError, self.name
@@ -1,86 +1,92 b''
1 1 Testing diff --change
2 2
3 3 $ hg init a
4 4 $ cd a
5 5
6 6 $ echo "first" > file.txt
7 7 $ hg add file.txt
8 8 $ hg commit -m 'first commit' # 0
9 9
10 10 $ echo "second" > file.txt
11 11 $ hg commit -m 'second commit' # 1
12 12
13 13 $ echo "third" > file.txt
14 14 $ hg commit -m 'third commit' # 2
15 15
16 16 $ hg diff --nodates --change 1
17 17 diff -r 4bb65dda5db4 -r e9b286083166 file.txt
18 18 --- a/file.txt
19 19 +++ b/file.txt
20 20 @@ -1,1 +1,1 @@
21 21 -first
22 22 +second
23 23
24 24 $ hg diff --change e9b286083166
25 25 diff -r 4bb65dda5db4 -r e9b286083166 file.txt
26 26 --- a/file.txt Thu Jan 01 00:00:00 1970 +0000
27 27 +++ b/file.txt Thu Jan 01 00:00:00 1970 +0000
28 28 @@ -1,1 +1,1 @@
29 29 -first
30 30 +second
31 31
32 Test dumb revspecs (issue3474)
33
34 $ hg diff -r 2:2
35 $ hg diff -r "2 and 1"
36 abort: empty revision range
37 [255]
32 38
33 39 Testing diff --change when merge:
34 40
35 41 $ for i in 1 2 3 4 5 6 7 8 9 10; do
36 42 > echo $i >> file.txt
37 43 > done
38 44 $ hg commit -m "lots of text" # 3
39 45
40 46 $ sed -e 's,^2$,x,' file.txt > file.txt.tmp
41 47 $ mv file.txt.tmp file.txt
42 48 $ hg commit -m "change 2 to x" # 4
43 49
44 50 $ hg up -r 3
45 51 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
46 52 $ sed -e 's,^8$,y,' file.txt > file.txt.tmp
47 53 $ mv file.txt.tmp file.txt
48 54 $ hg commit -m "change 8 to y"
49 55 created new head
50 56
51 57 $ hg up -C -r 4
52 58 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 59 $ hg merge -r 5
54 60 merging file.txt
55 61 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
56 62 (branch merge, don't forget to commit)
57 63 $ hg commit -m "merge 8 to y" # 6
58 64
59 65 $ hg diff --change 5
60 66 diff -r ae119d680c82 -r 9085c5c02e52 file.txt
61 67 --- a/file.txt Thu Jan 01 00:00:00 1970 +0000
62 68 +++ b/file.txt Thu Jan 01 00:00:00 1970 +0000
63 69 @@ -6,6 +6,6 @@
64 70 5
65 71 6
66 72 7
67 73 -8
68 74 +y
69 75 9
70 76 10
71 77
72 78 must be similar to 'hg diff --change 5':
73 79
74 80 $ hg diff -c 6
75 81 diff -r 273b50f17c6d -r 979ca961fd2e file.txt
76 82 --- a/file.txt Thu Jan 01 00:00:00 1970 +0000
77 83 +++ b/file.txt Thu Jan 01 00:00:00 1970 +0000
78 84 @@ -6,6 +6,6 @@
79 85 5
80 86 6
81 87 7
82 88 -8
83 89 +y
84 90 9
85 91 10
86 92
General Comments 0
You need to be logged in to leave comments. Login now