##// END OF EJS Templates
filectx: use ctx.size comparisons to speed up ctx.cmp...
Nicolas Dumazet -
r12709:4147a292 default
parent child Browse files
Show More
@@ -1,619 +1,625 b''
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2010 Christian Ebert <blacktrash@gmx.net>
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 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56 The more specific you are in your filename patterns the less you
57 57 lose speed in huge repositories.
58 58
59 59 For [keywordmaps] template mapping and expansion demonstration and
60 60 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
61 61 available templates and filters.
62 62
63 63 Three additional date template filters are provided::
64 64
65 65 utcdate "2006/09/18 15:13:13"
66 66 svnutcdate "2006-09-18 15:13:13Z"
67 67 svnisodate "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
68 68
69 69 The default template mappings (view with :hg:`kwdemo -d`) can be
70 70 replaced with customized keywords and templates. Again, run
71 71 :hg:`kwdemo` to control the results of your config changes.
72 72
73 73 Before changing/disabling active keywords, run :hg:`kwshrink` to avoid
74 74 the risk of inadvertently storing expanded keywords in the change
75 75 history.
76 76
77 77 To force expansion after enabling it, or a configuration change, run
78 78 :hg:`kwexpand`.
79 79
80 80 Expansions spanning more than one line and incremental expansions,
81 81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 82 {desc}" expands to the first line of the changeset description.
83 83 '''
84 84
85 from mercurial import commands, cmdutil, dispatch, filelog, extensions
85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 87 from mercurial.hgweb import webcommands
88 88 from mercurial.i18n import _
89 89 import re, shutil, tempfile
90 90
91 91 commands.optionalrepo += ' kwdemo'
92 92
93 93 # hg commands that do not act on keywords
94 94 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
95 95 ' outgoing push tip verify convert email glog')
96 96
97 97 # hg commands that trigger expansion only when writing to working dir,
98 98 # not when reading filelog, and unexpand when reading from working dir
99 99 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
100 100
101 101 # names of extensions using dorecord
102 102 recordextensions = 'record'
103 103
104 104 # date like in cvs' $Date
105 105 utcdate = lambda x: util.datestr((x[0], 0), '%Y/%m/%d %H:%M:%S')
106 106 # date like in svn's $Date
107 107 svnisodate = lambda x: util.datestr(x, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
108 108 # date like in svn's $Id
109 109 svnutcdate = lambda x: util.datestr((x[0], 0), '%Y-%m-%d %H:%M:%SZ')
110 110
111 111 # make keyword tools accessible
112 112 kwtools = {'templater': None, 'hgcmd': ''}
113 113
114 114
115 115 def _defaultkwmaps(ui):
116 116 '''Returns default keywordmaps according to keywordset configuration.'''
117 117 templates = {
118 118 'Revision': '{node|short}',
119 119 'Author': '{author|user}',
120 120 }
121 121 kwsets = ({
122 122 'Date': '{date|utcdate}',
123 123 'RCSfile': '{file|basename},v',
124 124 'RCSFile': '{file|basename},v', # kept for backwards compatibility
125 125 # with hg-keyword
126 126 'Source': '{root}/{file},v',
127 127 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
128 128 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
129 129 }, {
130 130 'Date': '{date|svnisodate}',
131 131 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
132 132 'LastChangedRevision': '{node|short}',
133 133 'LastChangedBy': '{author|user}',
134 134 'LastChangedDate': '{date|svnisodate}',
135 135 })
136 136 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
137 137 return templates
138 138
139 139 def _shrinktext(text, subfunc):
140 140 '''Helper for keyword expansion removal in text.
141 141 Depending on subfunc also returns number of substitutions.'''
142 142 return subfunc(r'$\1$', text)
143 143
144 144
145 145 class kwtemplater(object):
146 146 '''
147 147 Sets up keyword templates, corresponding keyword regex, and
148 148 provides keyword substitution functions.
149 149 '''
150 150
151 151 def __init__(self, ui, repo, inc, exc):
152 152 self.ui = ui
153 153 self.repo = repo
154 154 self.match = match.match(repo.root, '', [], inc, exc)
155 155 self.restrict = kwtools['hgcmd'] in restricted.split()
156 156 self.record = False
157 157
158 158 kwmaps = self.ui.configitems('keywordmaps')
159 159 if kwmaps: # override default templates
160 160 self.templates = dict((k, templater.parsestring(v, False))
161 161 for k, v in kwmaps)
162 162 else:
163 163 self.templates = _defaultkwmaps(self.ui)
164 164 escaped = '|'.join(map(re.escape, self.templates.keys()))
165 165 self.re_kw = re.compile(r'\$(%s)\$' % escaped)
166 166 self.re_kwexp = re.compile(r'\$(%s): [^$\n\r]*? \$' % escaped)
167 167
168 168 templatefilters.filters.update({'utcdate': utcdate,
169 169 'svnisodate': svnisodate,
170 170 'svnutcdate': svnutcdate})
171 171
172 172 def substitute(self, data, path, ctx, subfunc):
173 173 '''Replaces keywords in data with expanded template.'''
174 174 def kwsub(mobj):
175 175 kw = mobj.group(1)
176 176 ct = cmdutil.changeset_templater(self.ui, self.repo,
177 177 False, None, '', False)
178 178 ct.use_template(self.templates[kw])
179 179 self.ui.pushbuffer()
180 180 ct.show(ctx, root=self.repo.root, file=path)
181 181 ekw = templatefilters.firstline(self.ui.popbuffer())
182 182 return '$%s: %s $' % (kw, ekw)
183 183 return subfunc(kwsub, data)
184 184
185 185 def expand(self, path, node, data):
186 186 '''Returns data with keywords expanded.'''
187 187 if not self.restrict and self.match(path) and not util.binary(data):
188 188 ctx = self.repo.filectx(path, fileid=node).changectx()
189 189 return self.substitute(data, path, ctx, self.re_kw.sub)
190 190 return data
191 191
192 192 def iskwfile(self, cand, ctx):
193 193 '''Returns subset of candidates which are configured for keyword
194 194 expansion are not symbolic links.'''
195 195 return [f for f in cand if self.match(f) and not 'l' in ctx.flags(f)]
196 196
197 197 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
198 198 '''Overwrites selected files expanding/shrinking keywords.'''
199 199 if self.restrict or lookup: # exclude kw_copy
200 200 candidates = self.iskwfile(candidates, ctx)
201 201 if not candidates:
202 202 return
203 203 commit = self.restrict and not lookup
204 204 if self.restrict or expand and lookup:
205 205 mf = ctx.manifest()
206 206 fctx = ctx
207 207 subn = (self.restrict or rekw) and self.re_kw.subn or self.re_kwexp.subn
208 208 msg = (expand and _('overwriting %s expanding keywords\n')
209 209 or _('overwriting %s shrinking keywords\n'))
210 210 for f in candidates:
211 211 if self.restrict:
212 212 data = self.repo.file(f).read(mf[f])
213 213 else:
214 214 data = self.repo.wread(f)
215 215 if util.binary(data):
216 216 continue
217 217 if expand:
218 218 if lookup:
219 219 fctx = self.repo.filectx(f, fileid=mf[f]).changectx()
220 220 data, found = self.substitute(data, f, fctx, subn)
221 221 elif self.restrict:
222 222 found = self.re_kw.search(data)
223 223 else:
224 224 data, found = _shrinktext(data, subn)
225 225 if found:
226 226 self.ui.note(msg % f)
227 227 self.repo.wwrite(f, data, ctx.flags(f))
228 228 if commit:
229 229 self.repo.dirstate.normal(f)
230 230 elif self.record:
231 231 self.repo.dirstate.normallookup(f)
232 232
233 233 def shrink(self, fname, text):
234 234 '''Returns text with all keyword substitutions removed.'''
235 235 if self.match(fname) and not util.binary(text):
236 236 return _shrinktext(text, self.re_kwexp.sub)
237 237 return text
238 238
239 239 def shrinklines(self, fname, lines):
240 240 '''Returns lines with keyword substitutions removed.'''
241 241 if self.match(fname):
242 242 text = ''.join(lines)
243 243 if not util.binary(text):
244 244 return _shrinktext(text, self.re_kwexp.sub).splitlines(True)
245 245 return lines
246 246
247 247 def wread(self, fname, data):
248 248 '''If in restricted mode returns data read from wdir with
249 249 keyword substitutions removed.'''
250 250 return self.restrict and self.shrink(fname, data) or data
251 251
252 252 class kwfilelog(filelog.filelog):
253 253 '''
254 254 Subclass of filelog to hook into its read, add, cmp methods.
255 255 Keywords are "stored" unexpanded, and processed on reading.
256 256 '''
257 257 def __init__(self, opener, kwt, path):
258 258 super(kwfilelog, self).__init__(opener, path)
259 259 self.kwt = kwt
260 260 self.path = path
261 261
262 262 def read(self, node):
263 263 '''Expands keywords when reading filelog.'''
264 264 data = super(kwfilelog, self).read(node)
265 265 if self.renamed(node):
266 266 return data
267 267 return self.kwt.expand(self.path, node, data)
268 268
269 269 def add(self, text, meta, tr, link, p1=None, p2=None):
270 270 '''Removes keyword substitutions when adding to filelog.'''
271 271 text = self.kwt.shrink(self.path, text)
272 272 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
273 273
274 274 def cmp(self, node, text):
275 275 '''Removes keyword substitutions for comparison.'''
276 276 text = self.kwt.shrink(self.path, text)
277 277 return super(kwfilelog, self).cmp(node, text)
278 278
279 279 def _status(ui, repo, kwt, *pats, **opts):
280 280 '''Bails out if [keyword] configuration is not active.
281 281 Returns status of working directory.'''
282 282 if kwt:
283 283 return repo.status(match=cmdutil.match(repo, pats, opts), clean=True,
284 284 unknown=opts.get('unknown') or opts.get('all'))
285 285 if ui.configitems('keyword'):
286 286 raise util.Abort(_('[keyword] patterns cannot match'))
287 287 raise util.Abort(_('no [keyword] patterns configured'))
288 288
289 289 def _kwfwrite(ui, repo, expand, *pats, **opts):
290 290 '''Selects files and passes them to kwtemplater.overwrite.'''
291 291 wctx = repo[None]
292 292 if len(wctx.parents()) > 1:
293 293 raise util.Abort(_('outstanding uncommitted merge'))
294 294 kwt = kwtools['templater']
295 295 wlock = repo.wlock()
296 296 try:
297 297 status = _status(ui, repo, kwt, *pats, **opts)
298 298 modified, added, removed, deleted, unknown, ignored, clean = status
299 299 if modified or added or removed or deleted:
300 300 raise util.Abort(_('outstanding uncommitted changes'))
301 301 kwt.overwrite(wctx, clean, True, expand)
302 302 finally:
303 303 wlock.release()
304 304
305 305 def demo(ui, repo, *args, **opts):
306 306 '''print [keywordmaps] configuration and an expansion example
307 307
308 308 Show current, custom, or default keyword template maps and their
309 309 expansions.
310 310
311 311 Extend the current configuration by specifying maps as arguments
312 312 and using -f/--rcfile to source an external hgrc file.
313 313
314 314 Use -d/--default to disable current configuration.
315 315
316 316 See :hg:`help templates` for information on templates and filters.
317 317 '''
318 318 def demoitems(section, items):
319 319 ui.write('[%s]\n' % section)
320 320 for k, v in sorted(items):
321 321 ui.write('%s = %s\n' % (k, v))
322 322
323 323 fn = 'demo.txt'
324 324 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
325 325 ui.note(_('creating temporary repository at %s\n') % tmpdir)
326 326 repo = localrepo.localrepository(ui, tmpdir, True)
327 327 ui.setconfig('keyword', fn, '')
328 328
329 329 uikwmaps = ui.configitems('keywordmaps')
330 330 if args or opts.get('rcfile'):
331 331 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
332 332 if uikwmaps:
333 333 ui.status(_('\textending current template maps\n'))
334 334 if opts.get('default') or not uikwmaps:
335 335 ui.status(_('\toverriding default template maps\n'))
336 336 if opts.get('rcfile'):
337 337 ui.readconfig(opts.get('rcfile'))
338 338 if args:
339 339 # simulate hgrc parsing
340 340 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
341 341 fp = repo.opener('hgrc', 'w')
342 342 fp.writelines(rcmaps)
343 343 fp.close()
344 344 ui.readconfig(repo.join('hgrc'))
345 345 kwmaps = dict(ui.configitems('keywordmaps'))
346 346 elif opts.get('default'):
347 347 ui.status(_('\n\tconfiguration using default keyword template maps\n'))
348 348 kwmaps = _defaultkwmaps(ui)
349 349 if uikwmaps:
350 350 ui.status(_('\tdisabling current template maps\n'))
351 351 for k, v in kwmaps.iteritems():
352 352 ui.setconfig('keywordmaps', k, v)
353 353 else:
354 354 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
355 355 kwmaps = dict(uikwmaps) or _defaultkwmaps(ui)
356 356
357 357 uisetup(ui)
358 358 reposetup(ui, repo)
359 359 ui.write('[extensions]\nkeyword =\n')
360 360 demoitems('keyword', ui.configitems('keyword'))
361 361 demoitems('keywordmaps', kwmaps.iteritems())
362 362 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
363 363 repo.wopener(fn, 'w').write(keywords)
364 364 repo[None].add([fn])
365 365 ui.note(_('\nkeywords written to %s:\n') % fn)
366 366 ui.note(keywords)
367 367 repo.dirstate.setbranch('demobranch')
368 368 for name, cmd in ui.configitems('hooks'):
369 369 if name.split('.', 1)[0].find('commit') > -1:
370 370 repo.ui.setconfig('hooks', name, '')
371 371 msg = _('hg keyword configuration and expansion example')
372 372 ui.note("hg ci -m '%s'\n" % msg)
373 373 repo.commit(text=msg)
374 374 ui.status(_('\n\tkeywords expanded\n'))
375 375 ui.write(repo.wread(fn))
376 376 shutil.rmtree(tmpdir, ignore_errors=True)
377 377
378 378 def expand(ui, repo, *pats, **opts):
379 379 '''expand keywords in the working directory
380 380
381 381 Run after (re)enabling keyword expansion.
382 382
383 383 kwexpand refuses to run if given files contain local changes.
384 384 '''
385 385 # 3rd argument sets expansion to True
386 386 _kwfwrite(ui, repo, True, *pats, **opts)
387 387
388 388 def files(ui, repo, *pats, **opts):
389 389 '''show files configured for keyword expansion
390 390
391 391 List which files in the working directory are matched by the
392 392 [keyword] configuration patterns.
393 393
394 394 Useful to prevent inadvertent keyword expansion and to speed up
395 395 execution by including only files that are actual candidates for
396 396 expansion.
397 397
398 398 See :hg:`help keyword` on how to construct patterns both for
399 399 inclusion and exclusion of files.
400 400
401 401 With -A/--all and -v/--verbose the codes used to show the status
402 402 of files are::
403 403
404 404 K = keyword expansion candidate
405 405 k = keyword expansion candidate (not tracked)
406 406 I = ignored
407 407 i = ignored (not tracked)
408 408 '''
409 409 kwt = kwtools['templater']
410 410 status = _status(ui, repo, kwt, *pats, **opts)
411 411 cwd = pats and repo.getcwd() or ''
412 412 modified, added, removed, deleted, unknown, ignored, clean = status
413 413 files = []
414 414 if not opts.get('unknown') or opts.get('all'):
415 415 files = sorted(modified + added + clean)
416 416 wctx = repo[None]
417 417 kwfiles = kwt.iskwfile(files, wctx)
418 418 kwunknown = kwt.iskwfile(unknown, wctx)
419 419 if not opts.get('ignore') or opts.get('all'):
420 420 showfiles = kwfiles, kwunknown
421 421 else:
422 422 showfiles = [], []
423 423 if opts.get('all') or opts.get('ignore'):
424 424 showfiles += ([f for f in files if f not in kwfiles],
425 425 [f for f in unknown if f not in kwunknown])
426 426 for char, filenames in zip('KkIi', showfiles):
427 427 fmt = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
428 428 for f in filenames:
429 429 ui.write(fmt % repo.pathto(f, cwd))
430 430
431 431 def shrink(ui, repo, *pats, **opts):
432 432 '''revert expanded keywords in the working directory
433 433
434 434 Run before changing/disabling active keywords or if you experience
435 435 problems with :hg:`import` or :hg:`merge`.
436 436
437 437 kwshrink refuses to run if given files contain local changes.
438 438 '''
439 439 # 3rd argument sets expansion to False
440 440 _kwfwrite(ui, repo, False, *pats, **opts)
441 441
442 442
443 443 def uisetup(ui):
444 444 ''' Monkeypatches dispatch._parse to retrieve user command.'''
445 445
446 446 def kwdispatch_parse(orig, ui, args):
447 447 '''Monkeypatch dispatch._parse to obtain running hg command.'''
448 448 cmd, func, args, options, cmdoptions = orig(ui, args)
449 449 kwtools['hgcmd'] = cmd
450 450 return cmd, func, args, options, cmdoptions
451 451
452 452 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
453 453
454 454 def reposetup(ui, repo):
455 455 '''Sets up repo as kwrepo for keyword substitution.
456 456 Overrides file method to return kwfilelog instead of filelog
457 457 if file matches user configuration.
458 458 Wraps commit to overwrite configured files with updated
459 459 keyword substitutions.
460 460 Monkeypatches patch and webcommands.'''
461 461
462 462 try:
463 463 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
464 464 or '.hg' in util.splitpath(repo.root)
465 465 or repo._url.startswith('bundle:')):
466 466 return
467 467 except AttributeError:
468 468 pass
469 469
470 470 inc, exc = [], ['.hg*']
471 471 for pat, opt in ui.configitems('keyword'):
472 472 if opt != 'ignore':
473 473 inc.append(pat)
474 474 else:
475 475 exc.append(pat)
476 476 if not inc:
477 477 return
478 478
479 479 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
480 480
481 481 class kwrepo(repo.__class__):
482 482 def file(self, f):
483 483 if f[0] == '/':
484 484 f = f[1:]
485 485 return kwfilelog(self.sopener, kwt, f)
486 486
487 487 def wread(self, filename):
488 488 data = super(kwrepo, self).wread(filename)
489 489 return kwt.wread(filename, data)
490 490
491 491 def commit(self, *args, **opts):
492 492 # use custom commitctx for user commands
493 493 # other extensions can still wrap repo.commitctx directly
494 494 self.commitctx = self.kwcommitctx
495 495 try:
496 496 return super(kwrepo, self).commit(*args, **opts)
497 497 finally:
498 498 del self.commitctx
499 499
500 500 def kwcommitctx(self, ctx, error=False):
501 501 n = super(kwrepo, self).commitctx(ctx, error)
502 502 # no lock needed, only called from repo.commit() which already locks
503 503 if not kwt.record:
504 504 restrict = kwt.restrict
505 505 kwt.restrict = True
506 506 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
507 507 False, True)
508 508 kwt.restrict = restrict
509 509 return n
510 510
511 511 def rollback(self, dryrun=False):
512 512 wlock = repo.wlock()
513 513 try:
514 514 if not dryrun:
515 515 changed = self['.'].files()
516 516 ret = super(kwrepo, self).rollback(dryrun)
517 517 if not dryrun:
518 518 ctx = self['.']
519 519 modified, added = self[None].status()[:2]
520 520 modified = [f for f in modified if f in changed]
521 521 added = [f for f in added if f in changed]
522 522 kwt.overwrite(ctx, added, True, False)
523 523 kwt.overwrite(ctx, modified, True, True)
524 524 return ret
525 525 finally:
526 526 wlock.release()
527 527
528 528 # monkeypatches
529 529 def kwpatchfile_init(orig, self, ui, fname, opener,
530 530 missing=False, eolmode=None):
531 531 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
532 532 rejects or conflicts due to expanded keywords in working dir.'''
533 533 orig(self, ui, fname, opener, missing, eolmode)
534 534 # shrink keywords read from working dir
535 535 self.lines = kwt.shrinklines(self.fname, self.lines)
536 536
537 537 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
538 538 opts=None, prefix=''):
539 539 '''Monkeypatch patch.diff to avoid expansion.'''
540 540 kwt.restrict = True
541 541 return orig(repo, node1, node2, match, changes, opts, prefix)
542 542
543 543 def kwweb_skip(orig, web, req, tmpl):
544 544 '''Wraps webcommands.x turning off keyword expansion.'''
545 545 kwt.match = util.never
546 546 return orig(web, req, tmpl)
547 547
548 548 def kw_copy(orig, ui, repo, pats, opts, rename=False):
549 549 '''Wraps cmdutil.copy so that copy/rename destinations do not
550 550 contain expanded keywords.
551 551 Note that the source may also be a symlink as:
552 552 hg cp sym x -> x is symlink
553 553 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
554 554 '''
555 555 orig(ui, repo, pats, opts, rename)
556 556 if opts.get('dry_run'):
557 557 return
558 558 wctx = repo[None]
559 559 candidates = [f for f in repo.dirstate.copies() if
560 560 kwt.match(repo.dirstate.copied(f)) and
561 561 not 'l' in wctx.flags(f)]
562 562 kwt.overwrite(wctx, candidates, False, False)
563 563
564 564 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
565 565 '''Wraps record.dorecord expanding keywords after recording.'''
566 566 wlock = repo.wlock()
567 567 try:
568 568 # record returns 0 even when nothing has changed
569 569 # therefore compare nodes before and after
570 570 kwt.record = True
571 571 ctx = repo['.']
572 572 modified, added = repo[None].status()[:2]
573 573 ret = orig(ui, repo, commitfunc, *pats, **opts)
574 574 recctx = repo['.']
575 575 if ctx != recctx:
576 576 changed = recctx.files()
577 577 modified = [f for f in modified if f in changed]
578 578 added = [f for f in added if f in changed]
579 579 kwt.restrict = False
580 580 kwt.overwrite(recctx, modified, False, True)
581 581 kwt.overwrite(recctx, added, False, True, True)
582 582 kwt.restrict = True
583 583 return ret
584 584 finally:
585 585 wlock.release()
586 586
587 587 repo.__class__ = kwrepo
588 588
589 def kwfilectx_cmp(orig, self, fctx):
590 # keyword affects data size, comparing wdir and filelog size does
591 # not make sense
592 return self._filelog.cmp(self._filenode, fctx.data())
593 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
594
589 595 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
590 596 extensions.wrapfunction(patch, 'diff', kw_diff)
591 597 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
592 598 for c in 'annotate changeset rev filediff diff'.split():
593 599 extensions.wrapfunction(webcommands, c, kwweb_skip)
594 600 for name in recordextensions.split():
595 601 try:
596 602 record = extensions.find(name)
597 603 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
598 604 except KeyError:
599 605 pass
600 606
601 607 cmdtable = {
602 608 'kwdemo':
603 609 (demo,
604 610 [('d', 'default', None, _('show default keyword template maps')),
605 611 ('f', 'rcfile', '',
606 612 _('read maps from rcfile'), _('FILE'))],
607 613 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
608 614 'kwexpand': (expand, commands.walkopts,
609 615 _('hg kwexpand [OPTION]... [FILE]...')),
610 616 'kwfiles':
611 617 (files,
612 618 [('A', 'all', None, _('show keyword status flags of all files')),
613 619 ('i', 'ignore', None, _('show files excluded from expansion')),
614 620 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
615 621 ] + commands.walkopts,
616 622 _('hg kwfiles [OPTION]... [FILE]...')),
617 623 'kwshrink': (shrink, commands.walkopts,
618 624 _('hg kwshrink [OPTION]... [FILE]...')),
619 625 }
@@ -1,1089 +1,1092 b''
1 1 # context.py - changeset and file context objects for mercurial
2 2 #
3 3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import nullid, nullrev, short, hex
9 9 from i18n import _
10 10 import ancestor, bdiff, error, util, subrepo, patch
11 11 import os, errno, stat
12 12
13 13 propertycache = util.propertycache
14 14
15 15 class changectx(object):
16 16 """A changecontext object makes access to data related to a particular
17 17 changeset convenient."""
18 18 def __init__(self, repo, changeid=''):
19 19 """changeid is a revision number, node, or tag"""
20 20 if changeid == '':
21 21 changeid = '.'
22 22 self._repo = repo
23 23 if isinstance(changeid, (long, int)):
24 24 self._rev = changeid
25 25 self._node = self._repo.changelog.node(changeid)
26 26 else:
27 27 self._node = self._repo.lookup(changeid)
28 28 self._rev = self._repo.changelog.rev(self._node)
29 29
30 30 def __str__(self):
31 31 return short(self.node())
32 32
33 33 def __int__(self):
34 34 return self.rev()
35 35
36 36 def __repr__(self):
37 37 return "<changectx %s>" % str(self)
38 38
39 39 def __hash__(self):
40 40 try:
41 41 return hash(self._rev)
42 42 except AttributeError:
43 43 return id(self)
44 44
45 45 def __eq__(self, other):
46 46 try:
47 47 return self._rev == other._rev
48 48 except AttributeError:
49 49 return False
50 50
51 51 def __ne__(self, other):
52 52 return not (self == other)
53 53
54 54 def __nonzero__(self):
55 55 return self._rev != nullrev
56 56
57 57 @propertycache
58 58 def _changeset(self):
59 59 return self._repo.changelog.read(self.node())
60 60
61 61 @propertycache
62 62 def _manifest(self):
63 63 return self._repo.manifest.read(self._changeset[0])
64 64
65 65 @propertycache
66 66 def _manifestdelta(self):
67 67 return self._repo.manifest.readdelta(self._changeset[0])
68 68
69 69 @propertycache
70 70 def _parents(self):
71 71 p = self._repo.changelog.parentrevs(self._rev)
72 72 if p[1] == nullrev:
73 73 p = p[:-1]
74 74 return [changectx(self._repo, x) for x in p]
75 75
76 76 @propertycache
77 77 def substate(self):
78 78 return subrepo.state(self, self._repo.ui)
79 79
80 80 def __contains__(self, key):
81 81 return key in self._manifest
82 82
83 83 def __getitem__(self, key):
84 84 return self.filectx(key)
85 85
86 86 def __iter__(self):
87 87 for f in sorted(self._manifest):
88 88 yield f
89 89
90 90 def changeset(self):
91 91 return self._changeset
92 92 def manifest(self):
93 93 return self._manifest
94 94 def manifestnode(self):
95 95 return self._changeset[0]
96 96
97 97 def rev(self):
98 98 return self._rev
99 99 def node(self):
100 100 return self._node
101 101 def hex(self):
102 102 return hex(self._node)
103 103 def user(self):
104 104 return self._changeset[1]
105 105 def date(self):
106 106 return self._changeset[2]
107 107 def files(self):
108 108 return self._changeset[3]
109 109 def description(self):
110 110 return self._changeset[4]
111 111 def branch(self):
112 112 return self._changeset[5].get("branch")
113 113 def extra(self):
114 114 return self._changeset[5]
115 115 def tags(self):
116 116 return self._repo.nodetags(self._node)
117 117
118 118 def parents(self):
119 119 """return contexts for each parent changeset"""
120 120 return self._parents
121 121
122 122 def p1(self):
123 123 return self._parents[0]
124 124
125 125 def p2(self):
126 126 if len(self._parents) == 2:
127 127 return self._parents[1]
128 128 return changectx(self._repo, -1)
129 129
130 130 def children(self):
131 131 """return contexts for each child changeset"""
132 132 c = self._repo.changelog.children(self._node)
133 133 return [changectx(self._repo, x) for x in c]
134 134
135 135 def ancestors(self):
136 136 for a in self._repo.changelog.ancestors(self._rev):
137 137 yield changectx(self._repo, a)
138 138
139 139 def descendants(self):
140 140 for d in self._repo.changelog.descendants(self._rev):
141 141 yield changectx(self._repo, d)
142 142
143 143 def _fileinfo(self, path):
144 144 if '_manifest' in self.__dict__:
145 145 try:
146 146 return self._manifest[path], self._manifest.flags(path)
147 147 except KeyError:
148 148 raise error.LookupError(self._node, path,
149 149 _('not found in manifest'))
150 150 if '_manifestdelta' in self.__dict__ or path in self.files():
151 151 if path in self._manifestdelta:
152 152 return self._manifestdelta[path], self._manifestdelta.flags(path)
153 153 node, flag = self._repo.manifest.find(self._changeset[0], path)
154 154 if not node:
155 155 raise error.LookupError(self._node, path,
156 156 _('not found in manifest'))
157 157
158 158 return node, flag
159 159
160 160 def filenode(self, path):
161 161 return self._fileinfo(path)[0]
162 162
163 163 def flags(self, path):
164 164 try:
165 165 return self._fileinfo(path)[1]
166 166 except error.LookupError:
167 167 return ''
168 168
169 169 def filectx(self, path, fileid=None, filelog=None):
170 170 """get a file context from this changeset"""
171 171 if fileid is None:
172 172 fileid = self.filenode(path)
173 173 return filectx(self._repo, path, fileid=fileid,
174 174 changectx=self, filelog=filelog)
175 175
176 176 def ancestor(self, c2):
177 177 """
178 178 return the ancestor context of self and c2
179 179 """
180 180 # deal with workingctxs
181 181 n2 = c2._node
182 182 if n2 == None:
183 183 n2 = c2._parents[0]._node
184 184 n = self._repo.changelog.ancestor(self._node, n2)
185 185 return changectx(self._repo, n)
186 186
187 187 def walk(self, match):
188 188 fset = set(match.files())
189 189 # for dirstate.walk, files=['.'] means "walk the whole tree".
190 190 # follow that here, too
191 191 fset.discard('.')
192 192 for fn in self:
193 193 for ffn in fset:
194 194 # match if the file is the exact name or a directory
195 195 if ffn == fn or fn.startswith("%s/" % ffn):
196 196 fset.remove(ffn)
197 197 break
198 198 if match(fn):
199 199 yield fn
200 200 for fn in sorted(fset):
201 201 if match.bad(fn, _('no such file in rev %s') % self) and match(fn):
202 202 yield fn
203 203
204 204 def sub(self, path):
205 205 return subrepo.subrepo(self, path)
206 206
207 207 def diff(self, ctx2=None, match=None, **opts):
208 208 """Returns a diff generator for the given contexts and matcher"""
209 209 if ctx2 is None:
210 210 ctx2 = self.p1()
211 211 if ctx2 is not None and not isinstance(ctx2, changectx):
212 212 ctx2 = self._repo[ctx2]
213 213 diffopts = patch.diffopts(self._repo.ui, opts)
214 214 return patch.diff(self._repo, ctx2.node(), self.node(),
215 215 match=match, opts=diffopts)
216 216
217 217 class filectx(object):
218 218 """A filecontext object makes access to data related to a particular
219 219 filerevision convenient."""
220 220 def __init__(self, repo, path, changeid=None, fileid=None,
221 221 filelog=None, changectx=None):
222 222 """changeid can be a changeset revision, node, or tag.
223 223 fileid can be a file revision or node."""
224 224 self._repo = repo
225 225 self._path = path
226 226
227 227 assert (changeid is not None
228 228 or fileid is not None
229 229 or changectx is not None), \
230 230 ("bad args: changeid=%r, fileid=%r, changectx=%r"
231 231 % (changeid, fileid, changectx))
232 232
233 233 if filelog:
234 234 self._filelog = filelog
235 235
236 236 if changeid is not None:
237 237 self._changeid = changeid
238 238 if changectx is not None:
239 239 self._changectx = changectx
240 240 if fileid is not None:
241 241 self._fileid = fileid
242 242
243 243 @propertycache
244 244 def _changectx(self):
245 245 return changectx(self._repo, self._changeid)
246 246
247 247 @propertycache
248 248 def _filelog(self):
249 249 return self._repo.file(self._path)
250 250
251 251 @propertycache
252 252 def _changeid(self):
253 253 if '_changectx' in self.__dict__:
254 254 return self._changectx.rev()
255 255 else:
256 256 return self._filelog.linkrev(self._filerev)
257 257
258 258 @propertycache
259 259 def _filenode(self):
260 260 if '_fileid' in self.__dict__:
261 261 return self._filelog.lookup(self._fileid)
262 262 else:
263 263 return self._changectx.filenode(self._path)
264 264
265 265 @propertycache
266 266 def _filerev(self):
267 267 return self._filelog.rev(self._filenode)
268 268
269 269 @propertycache
270 270 def _repopath(self):
271 271 return self._path
272 272
273 273 def __nonzero__(self):
274 274 try:
275 275 self._filenode
276 276 return True
277 277 except error.LookupError:
278 278 # file is missing
279 279 return False
280 280
281 281 def __str__(self):
282 282 return "%s@%s" % (self.path(), short(self.node()))
283 283
284 284 def __repr__(self):
285 285 return "<filectx %s>" % str(self)
286 286
287 287 def __hash__(self):
288 288 try:
289 289 return hash((self._path, self._filenode))
290 290 except AttributeError:
291 291 return id(self)
292 292
293 293 def __eq__(self, other):
294 294 try:
295 295 return (self._path == other._path
296 296 and self._filenode == other._filenode)
297 297 except AttributeError:
298 298 return False
299 299
300 300 def __ne__(self, other):
301 301 return not (self == other)
302 302
303 303 def filectx(self, fileid):
304 304 '''opens an arbitrary revision of the file without
305 305 opening a new filelog'''
306 306 return filectx(self._repo, self._path, fileid=fileid,
307 307 filelog=self._filelog)
308 308
309 309 def filerev(self):
310 310 return self._filerev
311 311 def filenode(self):
312 312 return self._filenode
313 313 def flags(self):
314 314 return self._changectx.flags(self._path)
315 315 def filelog(self):
316 316 return self._filelog
317 317
318 318 def rev(self):
319 319 if '_changectx' in self.__dict__:
320 320 return self._changectx.rev()
321 321 if '_changeid' in self.__dict__:
322 322 return self._changectx.rev()
323 323 return self._filelog.linkrev(self._filerev)
324 324
325 325 def linkrev(self):
326 326 return self._filelog.linkrev(self._filerev)
327 327 def node(self):
328 328 return self._changectx.node()
329 329 def hex(self):
330 330 return hex(self.node())
331 331 def user(self):
332 332 return self._changectx.user()
333 333 def date(self):
334 334 return self._changectx.date()
335 335 def files(self):
336 336 return self._changectx.files()
337 337 def description(self):
338 338 return self._changectx.description()
339 339 def branch(self):
340 340 return self._changectx.branch()
341 341 def extra(self):
342 342 return self._changectx.extra()
343 343 def manifest(self):
344 344 return self._changectx.manifest()
345 345 def changectx(self):
346 346 return self._changectx
347 347
348 348 def data(self):
349 349 return self._filelog.read(self._filenode)
350 350 def path(self):
351 351 return self._path
352 352 def size(self):
353 353 return self._filelog.size(self._filerev)
354 354
355 355 def cmp(self, fctx):
356 356 """compare with other file context
357 357
358 358 returns True if different than fctx.
359 359 """
360 if not self._repo._encodefilterpats and self.size() != fctx.size():
361 return True
362
360 363 return self._filelog.cmp(self._filenode, fctx.data())
361 364
362 365 def renamed(self):
363 366 """check if file was actually renamed in this changeset revision
364 367
365 368 If rename logged in file revision, we report copy for changeset only
366 369 if file revisions linkrev points back to the changeset in question
367 370 or both changeset parents contain different file revisions.
368 371 """
369 372
370 373 renamed = self._filelog.renamed(self._filenode)
371 374 if not renamed:
372 375 return renamed
373 376
374 377 if self.rev() == self.linkrev():
375 378 return renamed
376 379
377 380 name = self.path()
378 381 fnode = self._filenode
379 382 for p in self._changectx.parents():
380 383 try:
381 384 if fnode == p.filenode(name):
382 385 return None
383 386 except error.LookupError:
384 387 pass
385 388 return renamed
386 389
387 390 def parents(self):
388 391 p = self._path
389 392 fl = self._filelog
390 393 pl = [(p, n, fl) for n in self._filelog.parents(self._filenode)]
391 394
392 395 r = self._filelog.renamed(self._filenode)
393 396 if r:
394 397 pl[0] = (r[0], r[1], None)
395 398
396 399 return [filectx(self._repo, p, fileid=n, filelog=l)
397 400 for p, n, l in pl if n != nullid]
398 401
399 402 def children(self):
400 403 # hard for renames
401 404 c = self._filelog.children(self._filenode)
402 405 return [filectx(self._repo, self._path, fileid=x,
403 406 filelog=self._filelog) for x in c]
404 407
405 408 def annotate(self, follow=False, linenumber=None):
406 409 '''returns a list of tuples of (ctx, line) for each line
407 410 in the file, where ctx is the filectx of the node where
408 411 that line was last changed.
409 412 This returns tuples of ((ctx, linenumber), line) for each line,
410 413 if "linenumber" parameter is NOT "None".
411 414 In such tuples, linenumber means one at the first appearance
412 415 in the managed file.
413 416 To reduce annotation cost,
414 417 this returns fixed value(False is used) as linenumber,
415 418 if "linenumber" parameter is "False".'''
416 419
417 420 def decorate_compat(text, rev):
418 421 return ([rev] * len(text.splitlines()), text)
419 422
420 423 def without_linenumber(text, rev):
421 424 return ([(rev, False)] * len(text.splitlines()), text)
422 425
423 426 def with_linenumber(text, rev):
424 427 size = len(text.splitlines())
425 428 return ([(rev, i) for i in xrange(1, size + 1)], text)
426 429
427 430 decorate = (((linenumber is None) and decorate_compat) or
428 431 (linenumber and with_linenumber) or
429 432 without_linenumber)
430 433
431 434 def pair(parent, child):
432 435 for a1, a2, b1, b2 in bdiff.blocks(parent[1], child[1]):
433 436 child[0][b1:b2] = parent[0][a1:a2]
434 437 return child
435 438
436 439 getlog = util.lrucachefunc(lambda x: self._repo.file(x))
437 440 def getctx(path, fileid):
438 441 log = path == self._path and self._filelog or getlog(path)
439 442 return filectx(self._repo, path, fileid=fileid, filelog=log)
440 443 getctx = util.lrucachefunc(getctx)
441 444
442 445 def parents(f):
443 446 # we want to reuse filectx objects as much as possible
444 447 p = f._path
445 448 if f._filerev is None: # working dir
446 449 pl = [(n.path(), n.filerev()) for n in f.parents()]
447 450 else:
448 451 pl = [(p, n) for n in f._filelog.parentrevs(f._filerev)]
449 452
450 453 if follow:
451 454 r = f.renamed()
452 455 if r:
453 456 pl[0] = (r[0], getlog(r[0]).rev(r[1]))
454 457
455 458 return [getctx(p, n) for p, n in pl if n != nullrev]
456 459
457 460 # use linkrev to find the first changeset where self appeared
458 461 if self.rev() != self.linkrev():
459 462 base = self.filectx(self.filerev())
460 463 else:
461 464 base = self
462 465
463 466 # find all ancestors
464 467 needed = {base: 1}
465 468 visit = [base]
466 469 files = [base._path]
467 470 while visit:
468 471 f = visit.pop(0)
469 472 for p in parents(f):
470 473 if p not in needed:
471 474 needed[p] = 1
472 475 visit.append(p)
473 476 if p._path not in files:
474 477 files.append(p._path)
475 478 else:
476 479 # count how many times we'll use this
477 480 needed[p] += 1
478 481
479 482 # sort by revision (per file) which is a topological order
480 483 visit = []
481 484 for f in files:
482 485 visit.extend(n for n in needed if n._path == f)
483 486
484 487 hist = {}
485 488 for f in sorted(visit, key=lambda x: x.rev()):
486 489 curr = decorate(f.data(), f)
487 490 for p in parents(f):
488 491 curr = pair(hist[p], curr)
489 492 # trim the history of unneeded revs
490 493 needed[p] -= 1
491 494 if not needed[p]:
492 495 del hist[p]
493 496 hist[f] = curr
494 497
495 498 return zip(hist[f][0], hist[f][1].splitlines(True))
496 499
497 500 def ancestor(self, fc2, actx=None):
498 501 """
499 502 find the common ancestor file context, if any, of self, and fc2
500 503
501 504 If actx is given, it must be the changectx of the common ancestor
502 505 of self's and fc2's respective changesets.
503 506 """
504 507
505 508 if actx is None:
506 509 actx = self.changectx().ancestor(fc2.changectx())
507 510
508 511 # the trivial case: changesets are unrelated, files must be too
509 512 if not actx:
510 513 return None
511 514
512 515 # the easy case: no (relevant) renames
513 516 if fc2.path() == self.path() and self.path() in actx:
514 517 return actx[self.path()]
515 518 acache = {}
516 519
517 520 # prime the ancestor cache for the working directory
518 521 for c in (self, fc2):
519 522 if c._filerev is None:
520 523 pl = [(n.path(), n.filenode()) for n in c.parents()]
521 524 acache[(c._path, None)] = pl
522 525
523 526 flcache = {self._repopath:self._filelog, fc2._repopath:fc2._filelog}
524 527 def parents(vertex):
525 528 if vertex in acache:
526 529 return acache[vertex]
527 530 f, n = vertex
528 531 if f not in flcache:
529 532 flcache[f] = self._repo.file(f)
530 533 fl = flcache[f]
531 534 pl = [(f, p) for p in fl.parents(n) if p != nullid]
532 535 re = fl.renamed(n)
533 536 if re:
534 537 pl.append(re)
535 538 acache[vertex] = pl
536 539 return pl
537 540
538 541 a, b = (self._path, self._filenode), (fc2._path, fc2._filenode)
539 542 v = ancestor.ancestor(a, b, parents)
540 543 if v:
541 544 f, n = v
542 545 return filectx(self._repo, f, fileid=n, filelog=flcache[f])
543 546
544 547 return None
545 548
546 549 def ancestors(self):
547 550 seen = set(str(self))
548 551 visit = [self]
549 552 while visit:
550 553 for parent in visit.pop(0).parents():
551 554 s = str(parent)
552 555 if s not in seen:
553 556 visit.append(parent)
554 557 seen.add(s)
555 558 yield parent
556 559
557 560 class workingctx(changectx):
558 561 """A workingctx object makes access to data related to
559 562 the current working directory convenient.
560 563 date - any valid date string or (unixtime, offset), or None.
561 564 user - username string, or None.
562 565 extra - a dictionary of extra values, or None.
563 566 changes - a list of file lists as returned by localrepo.status()
564 567 or None to use the repository status.
565 568 """
566 569 def __init__(self, repo, text="", user=None, date=None, extra=None,
567 570 changes=None):
568 571 self._repo = repo
569 572 self._rev = None
570 573 self._node = None
571 574 self._text = text
572 575 if date:
573 576 self._date = util.parsedate(date)
574 577 if user:
575 578 self._user = user
576 579 if changes:
577 580 self._status = list(changes[:4])
578 581 self._unknown = changes[4]
579 582 self._ignored = changes[5]
580 583 self._clean = changes[6]
581 584 else:
582 585 self._unknown = None
583 586 self._ignored = None
584 587 self._clean = None
585 588
586 589 self._extra = {}
587 590 if extra:
588 591 self._extra = extra.copy()
589 592 if 'branch' not in self._extra:
590 593 branch = self._repo.dirstate.branch()
591 594 try:
592 595 branch = branch.decode('UTF-8').encode('UTF-8')
593 596 except UnicodeDecodeError:
594 597 raise util.Abort(_('branch name not in UTF-8!'))
595 598 self._extra['branch'] = branch
596 599 if self._extra['branch'] == '':
597 600 self._extra['branch'] = 'default'
598 601
599 602 def __str__(self):
600 603 return str(self._parents[0]) + "+"
601 604
602 605 def __nonzero__(self):
603 606 return True
604 607
605 608 def __contains__(self, key):
606 609 return self._repo.dirstate[key] not in "?r"
607 610
608 611 @propertycache
609 612 def _manifest(self):
610 613 """generate a manifest corresponding to the working directory"""
611 614
612 615 if self._unknown is None:
613 616 self.status(unknown=True)
614 617
615 618 man = self._parents[0].manifest().copy()
616 619 copied = self._repo.dirstate.copies()
617 620 if len(self._parents) > 1:
618 621 man2 = self.p2().manifest()
619 622 def getman(f):
620 623 if f in man:
621 624 return man
622 625 return man2
623 626 else:
624 627 getman = lambda f: man
625 628 def cf(f):
626 629 f = copied.get(f, f)
627 630 return getman(f).flags(f)
628 631 ff = self._repo.dirstate.flagfunc(cf)
629 632 modified, added, removed, deleted = self._status
630 633 unknown = self._unknown
631 634 for i, l in (("a", added), ("m", modified), ("u", unknown)):
632 635 for f in l:
633 636 orig = copied.get(f, f)
634 637 man[f] = getman(orig).get(orig, nullid) + i
635 638 try:
636 639 man.set(f, ff(f))
637 640 except OSError:
638 641 pass
639 642
640 643 for f in deleted + removed:
641 644 if f in man:
642 645 del man[f]
643 646
644 647 return man
645 648
646 649 @propertycache
647 650 def _status(self):
648 651 return self._repo.status()[:4]
649 652
650 653 @propertycache
651 654 def _user(self):
652 655 return self._repo.ui.username()
653 656
654 657 @propertycache
655 658 def _date(self):
656 659 return util.makedate()
657 660
658 661 @propertycache
659 662 def _parents(self):
660 663 p = self._repo.dirstate.parents()
661 664 if p[1] == nullid:
662 665 p = p[:-1]
663 666 self._parents = [changectx(self._repo, x) for x in p]
664 667 return self._parents
665 668
666 669 def status(self, ignored=False, clean=False, unknown=False):
667 670 """Explicit status query
668 671 Unless this method is used to query the working copy status, the
669 672 _status property will implicitly read the status using its default
670 673 arguments."""
671 674 stat = self._repo.status(ignored=ignored, clean=clean, unknown=unknown)
672 675 self._unknown = self._ignored = self._clean = None
673 676 if unknown:
674 677 self._unknown = stat[4]
675 678 if ignored:
676 679 self._ignored = stat[5]
677 680 if clean:
678 681 self._clean = stat[6]
679 682 self._status = stat[:4]
680 683 return stat
681 684
682 685 def manifest(self):
683 686 return self._manifest
684 687 def user(self):
685 688 return self._user or self._repo.ui.username()
686 689 def date(self):
687 690 return self._date
688 691 def description(self):
689 692 return self._text
690 693 def files(self):
691 694 return sorted(self._status[0] + self._status[1] + self._status[2])
692 695
693 696 def modified(self):
694 697 return self._status[0]
695 698 def added(self):
696 699 return self._status[1]
697 700 def removed(self):
698 701 return self._status[2]
699 702 def deleted(self):
700 703 return self._status[3]
701 704 def unknown(self):
702 705 assert self._unknown is not None # must call status first
703 706 return self._unknown
704 707 def ignored(self):
705 708 assert self._ignored is not None # must call status first
706 709 return self._ignored
707 710 def clean(self):
708 711 assert self._clean is not None # must call status first
709 712 return self._clean
710 713 def branch(self):
711 714 return self._extra['branch']
712 715 def extra(self):
713 716 return self._extra
714 717
715 718 def tags(self):
716 719 t = []
717 720 [t.extend(p.tags()) for p in self.parents()]
718 721 return t
719 722
720 723 def children(self):
721 724 return []
722 725
723 726 def flags(self, path):
724 727 if '_manifest' in self.__dict__:
725 728 try:
726 729 return self._manifest.flags(path)
727 730 except KeyError:
728 731 return ''
729 732
730 733 orig = self._repo.dirstate.copies().get(path, path)
731 734
732 735 def findflag(ctx):
733 736 mnode = ctx.changeset()[0]
734 737 node, flag = self._repo.manifest.find(mnode, orig)
735 738 ff = self._repo.dirstate.flagfunc(lambda x: flag or '')
736 739 try:
737 740 return ff(path)
738 741 except OSError:
739 742 pass
740 743
741 744 flag = findflag(self._parents[0])
742 745 if flag is None and len(self.parents()) > 1:
743 746 flag = findflag(self._parents[1])
744 747 if flag is None or self._repo.dirstate[path] == 'r':
745 748 return ''
746 749 return flag
747 750
748 751 def filectx(self, path, filelog=None):
749 752 """get a file context from the working directory"""
750 753 return workingfilectx(self._repo, path, workingctx=self,
751 754 filelog=filelog)
752 755
753 756 def ancestor(self, c2):
754 757 """return the ancestor context of self and c2"""
755 758 return self._parents[0].ancestor(c2) # punt on two parents for now
756 759
757 760 def walk(self, match):
758 761 return sorted(self._repo.dirstate.walk(match, self.substate.keys(),
759 762 True, False))
760 763
761 764 def dirty(self, missing=False):
762 765 "check whether a working directory is modified"
763 766 # check subrepos first
764 767 for s in self.substate:
765 768 if self.sub(s).dirty():
766 769 return True
767 770 # check current working dir
768 771 return (self.p2() or self.branch() != self.p1().branch() or
769 772 self.modified() or self.added() or self.removed() or
770 773 (missing and self.deleted()))
771 774
772 775 def add(self, list, prefix=""):
773 776 join = lambda f: os.path.join(prefix, f)
774 777 wlock = self._repo.wlock()
775 778 ui, ds = self._repo.ui, self._repo.dirstate
776 779 try:
777 780 rejected = []
778 781 for f in list:
779 782 p = self._repo.wjoin(f)
780 783 try:
781 784 st = os.lstat(p)
782 785 except:
783 786 ui.warn(_("%s does not exist!\n") % join(f))
784 787 rejected.append(f)
785 788 continue
786 789 if st.st_size > 10000000:
787 790 ui.warn(_("%s: up to %d MB of RAM may be required "
788 791 "to manage this file\n"
789 792 "(use 'hg revert %s' to cancel the "
790 793 "pending addition)\n")
791 794 % (f, 3 * st.st_size // 1000000, join(f)))
792 795 if not (stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode)):
793 796 ui.warn(_("%s not added: only files and symlinks "
794 797 "supported currently\n") % join(f))
795 798 rejected.append(p)
796 799 elif ds[f] in 'amn':
797 800 ui.warn(_("%s already tracked!\n") % join(f))
798 801 elif ds[f] == 'r':
799 802 ds.normallookup(f)
800 803 else:
801 804 ds.add(f)
802 805 return rejected
803 806 finally:
804 807 wlock.release()
805 808
806 809 def forget(self, list):
807 810 wlock = self._repo.wlock()
808 811 try:
809 812 for f in list:
810 813 if self._repo.dirstate[f] != 'a':
811 814 self._repo.ui.warn(_("%s not added!\n") % f)
812 815 else:
813 816 self._repo.dirstate.forget(f)
814 817 finally:
815 818 wlock.release()
816 819
817 820 def remove(self, list, unlink=False):
818 821 if unlink:
819 822 for f in list:
820 823 try:
821 824 util.unlink(self._repo.wjoin(f))
822 825 except OSError, inst:
823 826 if inst.errno != errno.ENOENT:
824 827 raise
825 828 wlock = self._repo.wlock()
826 829 try:
827 830 for f in list:
828 831 if unlink and os.path.lexists(self._repo.wjoin(f)):
829 832 self._repo.ui.warn(_("%s still exists!\n") % f)
830 833 elif self._repo.dirstate[f] == 'a':
831 834 self._repo.dirstate.forget(f)
832 835 elif f not in self._repo.dirstate:
833 836 self._repo.ui.warn(_("%s not tracked!\n") % f)
834 837 else:
835 838 self._repo.dirstate.remove(f)
836 839 finally:
837 840 wlock.release()
838 841
839 842 def undelete(self, list):
840 843 pctxs = self.parents()
841 844 wlock = self._repo.wlock()
842 845 try:
843 846 for f in list:
844 847 if self._repo.dirstate[f] != 'r':
845 848 self._repo.ui.warn(_("%s not removed!\n") % f)
846 849 else:
847 850 fctx = f in pctxs[0] and pctxs[0][f] or pctxs[1][f]
848 851 t = fctx.data()
849 852 self._repo.wwrite(f, t, fctx.flags())
850 853 self._repo.dirstate.normal(f)
851 854 finally:
852 855 wlock.release()
853 856
854 857 def copy(self, source, dest):
855 858 p = self._repo.wjoin(dest)
856 859 if not os.path.lexists(p):
857 860 self._repo.ui.warn(_("%s does not exist!\n") % dest)
858 861 elif not (os.path.isfile(p) or os.path.islink(p)):
859 862 self._repo.ui.warn(_("copy failed: %s is not a file or a "
860 863 "symbolic link\n") % dest)
861 864 else:
862 865 wlock = self._repo.wlock()
863 866 try:
864 867 if self._repo.dirstate[dest] in '?r':
865 868 self._repo.dirstate.add(dest)
866 869 self._repo.dirstate.copy(source, dest)
867 870 finally:
868 871 wlock.release()
869 872
870 873 class workingfilectx(filectx):
871 874 """A workingfilectx object makes access to data related to a particular
872 875 file in the working directory convenient."""
873 876 def __init__(self, repo, path, filelog=None, workingctx=None):
874 877 """changeid can be a changeset revision, node, or tag.
875 878 fileid can be a file revision or node."""
876 879 self._repo = repo
877 880 self._path = path
878 881 self._changeid = None
879 882 self._filerev = self._filenode = None
880 883
881 884 if filelog:
882 885 self._filelog = filelog
883 886 if workingctx:
884 887 self._changectx = workingctx
885 888
886 889 @propertycache
887 890 def _changectx(self):
888 891 return workingctx(self._repo)
889 892
890 893 def __nonzero__(self):
891 894 return True
892 895
893 896 def __str__(self):
894 897 return "%s@%s" % (self.path(), self._changectx)
895 898
896 899 def data(self):
897 900 return self._repo.wread(self._path)
898 901 def renamed(self):
899 902 rp = self._repo.dirstate.copied(self._path)
900 903 if not rp:
901 904 return None
902 905 return rp, self._changectx._parents[0]._manifest.get(rp, nullid)
903 906
904 907 def parents(self):
905 908 '''return parent filectxs, following copies if necessary'''
906 909 def filenode(ctx, path):
907 910 return ctx._manifest.get(path, nullid)
908 911
909 912 path = self._path
910 913 fl = self._filelog
911 914 pcl = self._changectx._parents
912 915 renamed = self.renamed()
913 916
914 917 if renamed:
915 918 pl = [renamed + (None,)]
916 919 else:
917 920 pl = [(path, filenode(pcl[0], path), fl)]
918 921
919 922 for pc in pcl[1:]:
920 923 pl.append((path, filenode(pc, path), fl))
921 924
922 925 return [filectx(self._repo, p, fileid=n, filelog=l)
923 926 for p, n, l in pl if n != nullid]
924 927
925 928 def children(self):
926 929 return []
927 930
928 931 def size(self):
929 932 return os.lstat(self._repo.wjoin(self._path)).st_size
930 933 def date(self):
931 934 t, tz = self._changectx.date()
932 935 try:
933 936 return (int(os.lstat(self._repo.wjoin(self._path)).st_mtime), tz)
934 937 except OSError, err:
935 938 if err.errno != errno.ENOENT:
936 939 raise
937 940 return (t, tz)
938 941
939 942 def cmp(self, fctx):
940 943 """compare with other file context
941 944
942 945 returns True if different than fctx.
943 946 """
944 947 # fctx should be a filectx (not a wfctx)
945 948 # invert comparison to reuse the same code path
946 949 return fctx.cmp(self)
947 950
948 951 class memctx(object):
949 952 """Use memctx to perform in-memory commits via localrepo.commitctx().
950 953
951 954 Revision information is supplied at initialization time while
952 955 related files data and is made available through a callback
953 956 mechanism. 'repo' is the current localrepo, 'parents' is a
954 957 sequence of two parent revisions identifiers (pass None for every
955 958 missing parent), 'text' is the commit message and 'files' lists
956 959 names of files touched by the revision (normalized and relative to
957 960 repository root).
958 961
959 962 filectxfn(repo, memctx, path) is a callable receiving the
960 963 repository, the current memctx object and the normalized path of
961 964 requested file, relative to repository root. It is fired by the
962 965 commit function for every file in 'files', but calls order is
963 966 undefined. If the file is available in the revision being
964 967 committed (updated or added), filectxfn returns a memfilectx
965 968 object. If the file was removed, filectxfn raises an
966 969 IOError. Moved files are represented by marking the source file
967 970 removed and the new file added with copy information (see
968 971 memfilectx).
969 972
970 973 user receives the committer name and defaults to current
971 974 repository username, date is the commit date in any format
972 975 supported by util.parsedate() and defaults to current date, extra
973 976 is a dictionary of metadata or is left empty.
974 977 """
975 978 def __init__(self, repo, parents, text, files, filectxfn, user=None,
976 979 date=None, extra=None):
977 980 self._repo = repo
978 981 self._rev = None
979 982 self._node = None
980 983 self._text = text
981 984 self._date = date and util.parsedate(date) or util.makedate()
982 985 self._user = user
983 986 parents = [(p or nullid) for p in parents]
984 987 p1, p2 = parents
985 988 self._parents = [changectx(self._repo, p) for p in (p1, p2)]
986 989 files = sorted(set(files))
987 990 self._status = [files, [], [], [], []]
988 991 self._filectxfn = filectxfn
989 992
990 993 self._extra = extra and extra.copy() or {}
991 994 if 'branch' not in self._extra:
992 995 self._extra['branch'] = 'default'
993 996 elif self._extra.get('branch') == '':
994 997 self._extra['branch'] = 'default'
995 998
996 999 def __str__(self):
997 1000 return str(self._parents[0]) + "+"
998 1001
999 1002 def __int__(self):
1000 1003 return self._rev
1001 1004
1002 1005 def __nonzero__(self):
1003 1006 return True
1004 1007
1005 1008 def __getitem__(self, key):
1006 1009 return self.filectx(key)
1007 1010
1008 1011 def p1(self):
1009 1012 return self._parents[0]
1010 1013 def p2(self):
1011 1014 return self._parents[1]
1012 1015
1013 1016 def user(self):
1014 1017 return self._user or self._repo.ui.username()
1015 1018 def date(self):
1016 1019 return self._date
1017 1020 def description(self):
1018 1021 return self._text
1019 1022 def files(self):
1020 1023 return self.modified()
1021 1024 def modified(self):
1022 1025 return self._status[0]
1023 1026 def added(self):
1024 1027 return self._status[1]
1025 1028 def removed(self):
1026 1029 return self._status[2]
1027 1030 def deleted(self):
1028 1031 return self._status[3]
1029 1032 def unknown(self):
1030 1033 return self._status[4]
1031 1034 def ignored(self):
1032 1035 return self._status[5]
1033 1036 def clean(self):
1034 1037 return self._status[6]
1035 1038 def branch(self):
1036 1039 return self._extra['branch']
1037 1040 def extra(self):
1038 1041 return self._extra
1039 1042 def flags(self, f):
1040 1043 return self[f].flags()
1041 1044
1042 1045 def parents(self):
1043 1046 """return contexts for each parent changeset"""
1044 1047 return self._parents
1045 1048
1046 1049 def filectx(self, path, filelog=None):
1047 1050 """get a file context from the working directory"""
1048 1051 return self._filectxfn(self._repo, self, path)
1049 1052
1050 1053 def commit(self):
1051 1054 """commit context to the repo"""
1052 1055 return self._repo.commitctx(self)
1053 1056
1054 1057 class memfilectx(object):
1055 1058 """memfilectx represents an in-memory file to commit.
1056 1059
1057 1060 See memctx for more details.
1058 1061 """
1059 1062 def __init__(self, path, data, islink=False, isexec=False, copied=None):
1060 1063 """
1061 1064 path is the normalized file path relative to repository root.
1062 1065 data is the file content as a string.
1063 1066 islink is True if the file is a symbolic link.
1064 1067 isexec is True if the file is executable.
1065 1068 copied is the source file path if current file was copied in the
1066 1069 revision being committed, or None."""
1067 1070 self._path = path
1068 1071 self._data = data
1069 1072 self._flags = (islink and 'l' or '') + (isexec and 'x' or '')
1070 1073 self._copied = None
1071 1074 if copied:
1072 1075 self._copied = (copied, nullid)
1073 1076
1074 1077 def __nonzero__(self):
1075 1078 return True
1076 1079 def __str__(self):
1077 1080 return "%s@%s" % (self.path(), self._changectx)
1078 1081 def path(self):
1079 1082 return self._path
1080 1083 def data(self):
1081 1084 return self._data
1082 1085 def flags(self):
1083 1086 return self._flags
1084 1087 def isexec(self):
1085 1088 return 'x' in self._flags
1086 1089 def islink(self):
1087 1090 return 'l' in self._flags
1088 1091 def renamed(self):
1089 1092 return self._copied
General Comments 0
You need to be logged in to leave comments. Login now