82 Caveat: "hg import" fails if the patch context contains an active |
82 Caveat: "hg import" fails if the patch context contains an active |
83 keyword. In that case run "hg kwshrink", and then reimport. |
83 keyword. In that case run "hg kwshrink", and then reimport. |
84 Or, better, use bundle/unbundle to share changes. |
84 Or, better, use bundle/unbundle to share changes. |
85 ''' |
85 ''' |
86 |
86 |
87 from mercurial import commands, cmdutil, context, fancyopts |
87 from mercurial import commands, cmdutil, context, filelog, localrepo |
88 from mercurial import filelog, localrepo, revlog, templater, util |
88 from mercurial import patch, revlog, templater, util |
89 from mercurial.node import * |
89 from mercurial.node import * |
90 from mercurial.i18n import gettext as _ |
90 from mercurial.i18n import gettext as _ |
91 import getopt, os.path, re, shutil, sys, tempfile, time |
91 import os.path, re, shutil, tempfile, time |
92 |
92 |
93 # backwards compatibility hacks |
93 # backwards compatibility hacks |
94 |
|
95 try: |
|
96 # cmdutil.parse moves to dispatch._parse in 18a9fbb5cd78 |
|
97 # also avoid name conflict with other dispatch package(s) |
|
98 from mercurial.dispatch import _parse |
|
99 except ImportError: |
|
100 try: |
|
101 # commands.parse moves to cmdutil.parse in 0c61124ad877 |
|
102 _parse = cmdutil.parse |
|
103 except AttributeError: |
|
104 _parse = commands.parse |
|
105 |
|
106 def _wwrite(repo, f, data, mf): |
|
107 '''Makes repo.wwrite backwards compatible.''' |
|
108 # 656e06eebda7 removed file descriptor argument |
|
109 # 67982d3ee76c added flags argument |
|
110 try: |
|
111 repo.wwrite(f, data, mf.flags(f)) |
|
112 except (AttributeError, TypeError): |
|
113 repo.wwrite(f, data) |
|
114 |
94 |
115 def _normal(repo, files): |
95 def _normal(repo, files): |
116 '''Backwards compatible repo.dirstate.normal/update.''' |
96 '''Backwards compatible repo.dirstate.normal/update.''' |
117 # 6fd953d5faea introduced dirstate.normal() |
97 # 6fd953d5faea introduced dirstate.normal() |
118 try: |
98 try: |
119 for f in files: |
99 for f in files: |
120 repo.dirstate.normal(f) |
100 repo.dirstate.normal(f) |
121 except AttributeError: |
101 except AttributeError: |
122 repo.dirstate.update(files, 'n') |
102 repo.dirstate.update(files, 'n') |
123 |
103 |
|
104 def _link(repo, f): |
|
105 try: |
|
106 return repo._link(f) |
|
107 except AttributeError: |
|
108 return os.path.islink(repo.wjoin(f)) |
|
109 |
124 def _pathto(repo, f, cwd=None): |
110 def _pathto(repo, f, cwd=None): |
125 '''kwfiles behaves similar to status, using pathto since 78b6add1f966.''' |
111 '''kwfiles behaves similar to status, using pathto since 78b6add1f966.''' |
126 try: |
112 try: |
127 return repo.pathto(f, cwd) |
113 return repo.pathto(f, cwd) |
128 except AttributeError: |
114 except AttributeError: |
129 return f |
115 return f |
130 |
116 |
131 # commands.parse/cmdutil.parse returned nothing for |
|
132 # "hg diff --rev" before 88803a69b24a due to bug in fancyopts |
|
133 def _fancyopts(args, options, state): |
|
134 '''Fixed fancyopts from a9b7e425674f.''' |
|
135 namelist = [] |
|
136 shortlist = '' |
|
137 argmap = {} |
|
138 defmap = {} |
|
139 |
|
140 for short, name, default, comment in options: |
|
141 # convert opts to getopt format |
|
142 oname = name |
|
143 name = name.replace('-', '_') |
|
144 |
|
145 argmap['-' + short] = argmap['--' + oname] = name |
|
146 defmap[name] = default |
|
147 |
|
148 # copy defaults to state |
|
149 if isinstance(default, list): |
|
150 state[name] = default[:] |
|
151 elif callable(default): |
|
152 print "whoa", name, default |
|
153 state[name] = None |
|
154 else: |
|
155 state[name] = default |
|
156 |
|
157 # does it take a parameter? |
|
158 if not (default is None or default is True or default is False): |
|
159 if short: short += ':' |
|
160 if oname: oname += '=' |
|
161 if short: |
|
162 shortlist += short |
|
163 if name: |
|
164 namelist.append(oname) |
|
165 |
|
166 # parse arguments |
|
167 opts, args = getopt.getopt(args, shortlist, namelist) |
|
168 |
|
169 # transfer result to state |
|
170 for opt, val in opts: |
|
171 name = argmap[opt] |
|
172 t = type(defmap[name]) |
|
173 if t is type(fancyopts): |
|
174 state[name] = defmap[name](val) |
|
175 elif t is type(1): |
|
176 state[name] = int(val) |
|
177 elif t is type(''): |
|
178 state[name] = val |
|
179 elif t is type([]): |
|
180 state[name].append(val) |
|
181 elif t is type(None) or t is type(False): |
|
182 state[name] = True |
|
183 |
|
184 # return unparsed args |
|
185 return args |
|
186 |
|
187 fancyopts.fancyopts = _fancyopts |
|
188 |
|
189 |
117 |
190 commands.optionalrepo += ' kwdemo' |
118 commands.optionalrepo += ' kwdemo' |
191 |
|
192 # handle for external callers |
|
193 externalcall = None, None, {} |
|
194 |
|
195 def externalcmdhook(hgcmd, *args, **opts): |
|
196 '''Hook for external callers to pass hg commands to keyword. |
|
197 |
|
198 Caveat: hgcmd, args, opts are not checked for validity. |
|
199 This is the responsibility of the caller. |
|
200 |
|
201 hgmcd can be either the hg function object, eg diff or patch, |
|
202 or its string represenation, eg 'diff' or 'patch'.''' |
|
203 global externalcall |
|
204 if not isinstance(hgcmd, str): |
|
205 hgcmd = hgcmd.__name__.split('.')[-1] |
|
206 externalcall = hgcmd, args, opts |
|
207 |
|
208 # hg commands that trigger expansion only when writing to working dir, |
|
209 # not when reading filelog, and unexpand when reading from working dir |
|
210 restricted = ('diff1', 'record', |
|
211 'qfold', 'qimport', 'qnew', 'qpush', 'qrefresh', 'qrecord') |
|
212 |
119 |
213 def utcdate(date): |
120 def utcdate(date): |
214 '''Returns hgdate in cvs-like UTC format.''' |
121 '''Returns hgdate in cvs-like UTC format.''' |
215 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
122 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
216 |
|
217 |
|
218 _kwtemplater = None |
|
219 |
123 |
220 class kwtemplater(object): |
124 class kwtemplater(object): |
221 ''' |
125 ''' |
222 Sets up keyword templates, corresponding keyword regex, and |
126 Sets up keyword templates, corresponding keyword regex, and |
223 provides keyword substitution functions. |
127 provides keyword substitution functions. |
260 False, '', False) |
162 False, '', False) |
261 except TypeError: |
163 except TypeError: |
262 return cmdutil.changeset_templater(self.ui, self.repo, |
164 return cmdutil.changeset_templater(self.ui, self.repo, |
263 False, None, '', False) |
165 False, None, '', False) |
264 |
166 |
265 def substitute(self, node, data, subfunc): |
167 def substitute(self, path, data, node, subfunc): |
266 '''Obtains file's changenode if commit node not given, |
168 '''Obtains file's changenode if node not given, |
267 and calls given substitution function.''' |
169 and calls given substitution function.''' |
268 if self.commitnode: |
170 if node is None: |
269 fnode = self.commitnode |
171 # kwrepo.wwrite except when overwriting on commit |
270 else: |
172 if self.ctx is None: |
271 c = context.filectx(self.repo, self.path, fileid=node) |
173 self.ctx = self.repo.changectx() |
272 fnode = c.node() |
174 try: |
|
175 fnode = self.ctx.filenode(path) |
|
176 fl = self.repo.file(path) |
|
177 c = context.filectx(self.repo, path, fileid=fnode, filelog=fl) |
|
178 node = c.node() |
|
179 except revlog.LookupError: |
|
180 # eg: convert |
|
181 return subfunc == self.re_kw.sub and data or (data, None) |
|
182 elif subfunc == self.re_kw.sub: |
|
183 # hg kwcat using kwfilelog.read |
|
184 c = context.filectx(self.repo, path, fileid=node) |
|
185 node = c.node() |
273 |
186 |
274 def kwsub(mobj): |
187 def kwsub(mobj): |
275 '''Substitutes keyword using corresponding template.''' |
188 '''Substitutes keyword using corresponding template.''' |
276 kw = mobj.group(1) |
189 kw = mobj.group(1) |
277 self.ct.use_template(self.templates[kw]) |
190 self.ct.use_template(self.templates[kw]) |
278 self.ui.pushbuffer() |
191 self.ui.pushbuffer() |
279 self.ct.show(changenode=fnode, root=self.repo.root, file=self.path) |
192 self.ct.show(changenode=node, root=self.repo.root, file=path) |
280 return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer())) |
193 return '$%s: %s $' % (kw, templater.firstline(self.ui.popbuffer())) |
281 |
194 |
282 return subfunc(kwsub, data) |
195 return subfunc(kwsub, data) |
283 |
196 |
284 def expand(self, node, data): |
197 def expand(self, path, data, node): |
285 '''Returns data with keywords expanded.''' |
198 '''Returns data with keywords expanded.''' |
286 if util.binary(data) or self.hgcmd in restricted: |
199 if util.binary(data): |
287 return data |
200 return data |
288 return self.substitute(node, data, self.re_kw.sub) |
201 return self.substitute(path, data, node, self.re_kw.sub) |
289 |
202 |
290 def process(self, node, data, expand): |
203 def process(self, path, data, expand, ctx, node): |
291 '''Returns a tuple: data, count. |
204 '''Returns a tuple: data, count. |
292 Count is number of keywords/keyword substitutions, |
205 Count is number of keywords/keyword substitutions, |
293 telling caller whether to act on file containing data.''' |
206 telling caller whether to act on file containing data.''' |
294 if util.binary(data): |
207 if util.binary(data): |
295 return data, None |
208 return data, None |
296 if expand: |
209 if expand: |
297 return self.substitute(node, data, self.re_kw.subn) |
210 self.ctx = ctx |
298 return data, self.re_kw.search(data) |
211 return self.substitute(path, data, node, self.re_kw.subn) |
299 |
212 return self.re_kw.subn(r'$\1$', data) |
300 def shrink(self, text): |
213 |
|
214 def shrink(self, data): |
301 '''Returns text with all keyword substitutions removed.''' |
215 '''Returns text with all keyword substitutions removed.''' |
302 if util.binary(text): |
216 if util.binary(data): |
303 return text |
217 return data |
304 return self.re_kw.sub(r'$\1$', text) |
218 return self.re_kw.sub(r'$\1$', data) |
305 |
219 |
306 class kwfilelog(filelog.filelog): |
220 class kwfilelog(filelog.filelog): |
307 ''' |
221 ''' |
308 Subclass of filelog to hook into its read, add, cmp methods. |
222 Subclass of filelog to hook into its read method for kwcat. |
309 Keywords are "stored" unexpanded, and processed on reading. |
223 ''' |
310 ''' |
224 def __init__(self, opener, path, kwt): |
311 def __init__(self, opener, path): |
|
312 super(kwfilelog, self).__init__(opener, path) |
225 super(kwfilelog, self).__init__(opener, path) |
313 _kwtemplater.path = path |
226 self._kwt = kwt |
314 |
227 self._path = path |
315 def kwctread(self, node, expand): |
|
316 '''Reads expanding and counting keywords, called from _overwrite.''' |
|
317 data = super(kwfilelog, self).read(node) |
|
318 return _kwtemplater.process(node, data, expand) |
|
319 |
228 |
320 def read(self, node): |
229 def read(self, node): |
321 '''Expands keywords when reading filelog.''' |
230 '''Expands keywords when reading filelog.''' |
322 data = super(kwfilelog, self).read(node) |
231 data = super(kwfilelog, self).read(node) |
323 return _kwtemplater.expand(node, data) |
232 return self._kwt.expand(self._path, data, node) |
324 |
233 |
325 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
326 '''Removes keyword substitutions when adding to filelog.''' |
|
327 text = _kwtemplater.shrink(text) |
|
328 return super(kwfilelog, self).add(text, meta, tr, link, p1=p1, p2=p2) |
|
329 |
|
330 def cmp(self, node, text): |
|
331 '''Removes keyword substitutions for comparison.''' |
|
332 text = _kwtemplater.shrink(text) |
|
333 if self.renamed(node): |
|
334 t2 = super(kwfilelog, self).read(node) |
|
335 return t2 != text |
|
336 return revlog.revlog.cmp(self, node, text) |
|
337 |
|
338 def _iskwfile(f, link): |
|
339 return not link(f) and _kwtemplater.matcher(f) |
|
340 |
234 |
341 def _status(ui, repo, *pats, **opts): |
235 def _status(ui, repo, *pats, **opts): |
342 '''Bails out if [keyword] configuration is not active. |
236 '''Bails out if [keyword] configuration is not active. |
343 Returns status of working directory.''' |
237 Returns status of working directory.''' |
344 if _kwtemplater: |
238 if hasattr(repo, '_kwt'): |
345 files, match, anypats = cmdutil.matchpats(repo, pats, opts) |
239 files, match, anypats = cmdutil.matchpats(repo, pats, opts) |
346 return repo.status(files=files, match=match, list_clean=True) |
240 return repo.status(files=files, match=match, list_clean=True) |
347 if ui.configitems('keyword'): |
241 if ui.configitems('keyword'): |
348 raise util.Abort(_('[keyword] patterns cannot match')) |
242 raise util.Abort(_('[keyword] patterns cannot match')) |
349 raise util.Abort(_('no [keyword] patterns configured')) |
243 raise util.Abort(_('no [keyword] patterns configured')) |
350 |
244 |
351 def _overwrite(ui, repo, node=None, expand=True, files=None): |
245 def _overwrite(ui, repo, node=None, expand=True, files=None): |
352 '''Overwrites selected files expanding/shrinking keywords.''' |
246 '''Overwrites selected files expanding/shrinking keywords.''' |
353 ctx = repo.changectx(node) |
247 ctx = repo.changectx(node) |
354 mf = ctx.manifest() |
248 mf = ctx.manifest() |
355 if node is not None: # commit |
249 if node is not None: |
356 _kwtemplater.commitnode = node |
250 # commit |
357 files = [f for f in ctx.files() if f in mf] |
251 files = [f for f in ctx.files() if f in mf] |
358 notify = ui.debug |
252 notify = ui.debug |
359 else: # kwexpand/kwshrink |
253 else: |
|
254 # kwexpand/kwshrink |
360 notify = ui.note |
255 notify = ui.note |
361 candidates = [f for f in files if _iskwfile(f, mf.linkf)] |
256 candidates = [f for f in files if not mf.linkf(f) and repo._kwt.matcher(f)] |
362 if candidates: |
257 if candidates: |
363 overwritten = [] |
258 overwritten = [] |
364 candidates.sort() |
259 candidates.sort() |
365 action = expand and 'expanding' or 'shrinking' |
260 action = expand and 'expanding' or 'shrinking' |
366 for f in candidates: |
261 for f in candidates: |
367 fp = repo.file(f, kwmatch=True) |
262 data, kwfound = repo._wreadkwct(f, expand, ctx, node) |
368 data, kwfound = fp.kwctread(mf[f], expand) |
|
369 if kwfound: |
263 if kwfound: |
370 notify(_('overwriting %s %s keywords\n') % (f, action)) |
264 notify(_('overwriting %s %s keywords\n') % (f, action)) |
371 _wwrite(repo, f, data, mf) |
265 repo.wwrite(f, data, mf.flags(f), overwrite=True) |
372 overwritten.append(f) |
266 overwritten.append(f) |
373 _normal(repo, overwritten) |
267 _normal(repo, overwritten) |
374 |
268 |
375 def _kwfwrite(ui, repo, expand, *pats, **opts): |
269 def _kwfwrite(ui, repo, expand, *pats, **opts): |
376 '''Selects files and passes them to _overwrite.''' |
270 '''Selects files and passes them to _overwrite.''' |
513 # 3rd argument sets expansion to False |
425 # 3rd argument sets expansion to False |
514 _kwfwrite(ui, repo, False, *pats, **opts) |
426 _kwfwrite(ui, repo, False, *pats, **opts) |
515 |
427 |
516 |
428 |
517 def reposetup(ui, repo): |
429 def reposetup(ui, repo): |
518 '''Sets up repo as kwrepo for keyword substitution. |
|
519 Overrides file method to return kwfilelog instead of filelog |
|
520 if file matches user configuration. |
|
521 Wraps commit to overwrite configured files with updated |
|
522 keyword substitutions. |
|
523 This is done for local repos only, and only if there are |
|
524 files configured at all for keyword substitution.''' |
|
525 |
|
526 if not repo.local(): |
430 if not repo.local(): |
527 return |
431 return |
528 |
432 |
529 nokwcommands = ('add', 'addremove', 'bundle', 'clone', 'copy', |
433 inc, exc = [], ['.hgtags', '.hg_archival.txt'] |
530 'export', 'grep', 'identify', 'incoming', 'init', |
|
531 'log', 'outgoing', 'push', 'remove', 'rename', |
|
532 'rollback', 'tip', |
|
533 'convert') |
|
534 try: |
|
535 hgcmd, func, args, opts, cmdopts = dispatch._parse(ui, sys.argv[1:]) |
|
536 except (cmdutil.UnknownCommand, dispatch.ParseError): |
|
537 # must be an external caller, otherwise Exception would have been |
|
538 # raised at core command line parsing |
|
539 hgcmd, args, cmdopts = externalcall |
|
540 if hgcmd is None: |
|
541 # not an "official" hg command as from command line |
|
542 return |
|
543 if hgcmd in nokwcommands: |
|
544 return |
|
545 |
|
546 if hgcmd == 'diff': |
|
547 # only expand if comparing against working dir |
|
548 node1, node2 = cmdutil.revpair(repo, cmdopts.get('rev')) |
|
549 if node2 is not None: |
|
550 return |
|
551 # shrink if rev is not current node |
|
552 if node1 is not None and node1 != repo.changectx().node(): |
|
553 hgcmd = 'diff1' |
|
554 |
|
555 inc, exc = [], ['.hgtags'] |
|
556 for pat, opt in ui.configitems('keyword'): |
434 for pat, opt in ui.configitems('keyword'): |
557 if opt != 'ignore': |
435 if opt != 'ignore': |
558 inc.append(pat) |
436 inc.append(pat) |
559 else: |
437 else: |
560 exc.append(pat) |
438 exc.append(pat) |
561 if not inc: |
439 if not inc: |
562 return |
440 return |
563 |
441 |
564 global _kwtemplater |
|
565 _kwtemplater = kwtemplater(ui, repo, inc, exc, hgcmd) |
|
566 |
|
567 class kwrepo(repo.__class__): |
442 class kwrepo(repo.__class__): |
568 def file(self, f, kwmatch=False): |
443 def _kwfile(self, f): |
|
444 '''Returns filelog expanding keywords on read (for kwcat).''' |
569 if f[0] == '/': |
445 if f[0] == '/': |
570 f = f[1:] |
446 f = f[1:] |
571 if kwmatch or _kwtemplater.matcher(f): |
447 if self._kwt.matcher(f): |
572 return kwfilelog(self.sopener, f) |
448 return kwfilelog(self.sopener, f, self._kwt) |
573 return filelog.filelog(self.sopener, f) |
449 return filelog.filelog(self.sopener, f) |
|
450 |
|
451 def _wreadkwct(self, filename, expand, ctx, node): |
|
452 '''Reads filename and returns tuple of data with keywords |
|
453 expanded/shrunk and count of keywords (for _overwrite).''' |
|
454 data = super(kwrepo, self).wread(filename) |
|
455 return self._kwt.process(filename, data, expand, ctx, node) |
574 |
456 |
575 def wread(self, filename): |
457 def wread(self, filename): |
576 data = super(kwrepo, self).wread(filename) |
458 data = super(kwrepo, self).wread(filename) |
577 if hgcmd in restricted and _kwtemplater.matcher(filename): |
459 if self._kwt.matcher(filename): |
578 return _kwtemplater.shrink(data) |
460 return self._kwt.shrink(data) |
579 return data |
461 return data |
580 |
462 |
|
463 def wwrite(self, filename, data, flags=None, overwrite=False): |
|
464 if not overwrite and self._kwt.matcher(filename): |
|
465 data = self._kwt.expand(filename, data, None) |
|
466 try: |
|
467 super(kwrepo, self).wwrite(filename, data, flags) |
|
468 except (AttributeError, TypeError): |
|
469 # 656e06eebda7 removed file descriptor argument |
|
470 # 67982d3ee76c added flags argument |
|
471 super(kwrepo, self).wwrite(filename, data) |
|
472 |
|
473 def wwritedata(self, filename, data): |
|
474 if self._kwt.matcher(filename): |
|
475 data = self._kwt.expand(filename, data, None) |
|
476 return super(kwrepo, self).wwritedata(filename, data) |
|
477 |
581 def _commit(self, files, text, user, date, match, force, lock, wlock, |
478 def _commit(self, files, text, user, date, match, force, lock, wlock, |
582 force_editor, p1, p2, extra): |
479 force_editor, p1, p2, extra, empty_ok): |
583 '''Private commit wrapper for backwards compatibility.''' |
480 '''Private commit wrapper for backwards compatibility.''' |
584 try: |
481 try: |
585 return super(kwrepo, self).commit(files=files, text=text, |
482 return super(kwrepo, self).commit(files=files, text=text, |
586 user=user, date=date, |
483 user=user, date=date, |
587 match=match, force=force, |
484 match=match, force=force, |
588 lock=lock, wlock=wlock, |
485 lock=lock, wlock=wlock, |
589 force_editor=force_editor, |
486 force_editor=force_editor, |
590 p1=p1, p2=p2, extra=extra) |
487 p1=p1, p2=p2, extra=extra) |
591 except TypeError: |
488 except TypeError: |
592 return super(kwrepo, self).commit(files=files, text=text, |
489 try: |
593 user=user, date=date, |
490 return super(kwrepo, self).commit(files=files, text=text, |
594 match=match, force=force, |
491 user=user, date=date, |
595 force_editor=force_editor, |
492 match=match, force=force, |
596 p1=p1, p2=p2, extra=extra) |
493 force_editor=force_editor, |
|
494 p1=p1, p2=p2, |
|
495 extra=extra, |
|
496 empty_ok=empty_ok) |
|
497 except TypeError: |
|
498 return super(kwrepo, self).commit(files=files, text=text, |
|
499 user=user, date=date, |
|
500 match=match, force=force, |
|
501 force_editor=force_editor, |
|
502 p1=p1, p2=p2, extra=extra) |
597 |
503 |
598 def commit(self, files=None, text='', user=None, date=None, |
504 def commit(self, files=None, text='', user=None, date=None, |
599 match=util.always, force=False, lock=None, wlock=None, |
505 match=util.always, force=False, lock=None, wlock=None, |
600 force_editor=False, p1=None, p2=None, extra={}): |
506 force_editor=False, p1=None, p2=None, extra={}, |
|
507 empty_ok=False): |
601 # (w)lock arguments removed in 126f527b3ba3 |
508 # (w)lock arguments removed in 126f527b3ba3 |
602 # so they are None or what was passed to commit |
509 # so they are None or what was passed to commit |
603 # use private _(w)lock for deletion |
510 # use private _(w)lock for deletion |
604 _lock = lock |
511 _lock = lock |
605 _wlock = wlock |
512 _wlock = wlock |