1 # keyword.py - keyword expansion for mercurial |
1 # keyword.py - keyword expansion for Mercurial |
|
2 # |
|
3 # Copyright 2007 Christian Ebert <blacktrash@gmx.net> |
|
4 # |
|
5 # This software may be used and distributed according to the terms |
|
6 # of the GNU General Public License, incorporated herein by reference. |
|
7 # |
2 # $Id$ |
8 # $Id$ |
3 |
9 # |
4 '''keyword expansion hack against the grain of a DSCM |
10 # Keyword expansion hack against the grain of a DSCM |
5 |
11 # |
6 This extension lets you expand RCS/CVS-like keywords in a Mercurial |
12 # There are many good reasons why this is not needed in a distributed |
7 repository. |
13 # SCM, still it may be useful in very small projects based on single |
8 |
14 # files (like LaTeX packages), that are mostly addressed to an audience |
9 There are many good reasons why this is not needed in a distributed |
15 # not running a version control system. |
10 SCM, still it may be useful in very small projects based on single |
16 # |
11 files (like LaTeX packages), that are mostly addressed to an audience |
17 # For in-depth discussion refer to |
12 not running a version control system. |
18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>. |
13 |
19 # |
14 Supported $keywords$ and their $keyword: substition $ are: |
20 # Keyword expansion is based on Mercurial's changeset template mappings. |
15 Revision: changeset id |
21 # The extension provides an additional UTC-date filter ({date|utcdate}). |
16 Author: short username |
22 # |
17 Date: %Y/%m/%d %H:%M:%S [UTC] |
23 # The user has the choice either to create his own keywords and their |
18 RCSFile: basename,v |
24 # expansions or to use the CVS-like default ones. |
19 Source: /path/to/basename,v |
25 # |
20 Id: basename,v csetid %Y/%m/%d %H:%M:%S shortname |
26 # Expansions spanning more than one line are truncated to their first line. |
21 Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S shortname |
27 # Incremental expansion (like CVS' $Log$) is not supported. |
22 |
28 # |
23 Simple setup in hgrc: |
29 # Binary files are not touched. |
24 |
30 # |
25 # enable extension |
31 # Setup in hgrc: |
26 hgext.keyword = |
32 # |
27 # or, if script not in hgext folder: |
33 # # enable extension |
28 # hgext.keyword = /full/path/to/script |
34 # keyword = /full/path/to/keyword.py |
29 |
35 # # or, if script in hgext folder: |
30 # filename patterns for expansion are configured in this section |
36 # # hgext.keyword = |
31 [keyword] |
37 |
32 **.py = expand |
38 '''keyword expansion in local repositories |
33 ... |
39 |
|
40 This extension expands RCS/CVS-like or self-customized keywords in |
|
41 the text files selected by your configuration. |
|
42 |
|
43 Keywords are only expanded in local repositories and not logged by |
|
44 Mercurial internally. The mechanism can be regarded as a convenience |
|
45 for the current user and may be turned off anytime. |
|
46 |
|
47 The exansion works in 2 modes: |
|
48 1) working mode: substitution takes place on every commit and |
|
49 update of the working repository. |
|
50 2) archive mode: substitution is only triggered by "hg archive". |
|
51 |
|
52 Caveat: "hg import" might fail if the patches were exported from a |
|
53 repo with a different/no keyword setup, whereas "hg unbundle" is |
|
54 safe. |
|
55 |
|
56 Configuration is done in the [keyword] and [keywordmaps] sections of |
|
57 hgrc files. |
|
58 |
|
59 Example: |
|
60 [keyword] |
|
61 # filename patterns for expansion are configured in this section |
|
62 **.py = ## expand keywords in all python files |
|
63 x* = ignore ## but ignore files matching "x*" |
|
64 ** = archive ## keywords in all textfiles are expanded |
|
65 ## when creating a distribution |
|
66 y* = noarchive ## keywords in files matching "y*" are not expanded |
|
67 ## on archive creation |
|
68 ... |
|
69 [keywordmaps] |
|
70 # custom hg template maps _replace_ the CVS-like default ones |
|
71 HGdate = {date|rfc822date} |
|
72 lastlog = {desc} ## same as {desc|firstline} in this context |
|
73 checked in by = {author} |
|
74 ... |
|
75 |
|
76 If no [keywordmaps] are configured the extension falls back on the |
|
77 following defaults: |
|
78 |
|
79 Revision: changeset id |
|
80 Author: username |
|
81 Date: %Y/%m/%d %H:%M:%S ## [UTC] |
|
82 RCSFile: basename,v |
|
83 Source: /path/to/basename,v |
|
84 Id: basename,v csetid %Y/%m/%d %H:%M:%S username |
|
85 Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S username |
34 ''' |
86 ''' |
35 |
87 |
36 from mercurial import context, util |
88 from mercurial.node import * |
37 import os.path, re, sys, time |
89 try: |
38 |
90 from mercurial.demandload import * # stable |
39 re_kw = re.compile( |
91 from mercurial.i18n import gettext as _ |
40 r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$') |
92 demandload(globals(), 'mercurial:commands,fancyopts,templater,util') |
41 |
93 demandload(globals(), 'mercurial:cmdutil,context,filelog') |
42 def kwfmatches(ui, repo, files): |
94 demandload(globals(), 'os re sys time') |
43 '''Selects candidates for keyword substitution |
95 except ImportError: # demandimport |
44 configured in keyword section in hgrc.''' |
96 from mercurial.i18n import _ |
45 files = [f for f in files if not f.startswith('.hg')] |
97 from mercurial import commands, fancyopts, templater, util |
46 if not files: |
98 from mercurial import cmdutil, context, filelog |
47 return [] |
99 import os, re, sys, time |
48 candidates = [] |
100 |
49 kwfmatchers = [util.matcher(repo.root, '', [pat], [], [])[1] |
101 deftemplates = { |
50 for pat, opt in ui.configitems('keyword') if opt == 'expand'] |
102 'Revision': '{node|short}', |
51 for f in files: |
103 'Author': '{author|user}', |
52 for mf in kwfmatchers: |
104 'Date': '{date|utcdate}', |
53 if mf(f): |
105 'RCSFile': '{file|basename},v', |
54 candidates.append(f) |
106 'Source': '{root}/{file},v', |
55 break |
107 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', |
56 return candidates |
108 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', |
|
109 } |
57 |
110 |
58 def utcdate(date): |
111 def utcdate(date): |
59 '''Returns hgdate in cvs-like UTC format.''' |
112 '''Returns hgdate in cvs-like UTC format.''' |
60 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
113 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
61 |
114 |
62 |
115 def getcmd(ui): |
63 class kwfilectx(context.filectx): |
116 '''Returns current hg command.''' |
64 ''' |
117 # commands.parse(ui, sys.argv[1:])[0] breaks "hg diff -r" |
65 Provides keyword expansion functions based on file context. |
118 try: |
66 ''' |
119 args = fancyopts.fancyopts(sys.argv[1:], commands.globalopts, {}) |
67 def __init__(self, repo, path, changeid=None, fileid=None, filelog=None): |
120 except fancyopts.getopt.GetoptError, inst: |
68 context.filectx.__init__(self, repo, path, changeid, fileid, filelog) |
121 raise commands.ParseError(None, inst) |
69 def Revision(self): |
122 if args: |
70 return str(self.changectx()) |
123 cmd = args[0] |
71 def Author(self): |
124 aliases, i = commands.findcmd(ui, cmd) |
72 return util.shortuser(self.user()) |
125 return aliases[0] |
73 def Date(self): |
126 |
74 return utcdate(self.date()) |
127 class kwtemplater(object): |
75 def RCSFile(self): |
128 ''' |
76 return os.path.basename(self._path)+',v' |
129 Sets up keyword templates, corresponding keyword regex, and |
77 def Source(self): |
130 provides keyword substitution functions. |
78 return self._repo.wjoin(self._path)+',v' |
131 ''' |
79 def Header(self): |
132 def __init__(self, ui, repo): |
80 return ' '.join( |
133 self.ui = ui |
81 [self.Source(), self.Revision(), self.Date(), self.Author()]) |
134 self.repo = repo |
82 def Id(self): |
135 templates = dict(ui.configitems('keywordmaps')) |
83 return ' '.join( |
136 if templates: |
84 [self.RCSFile(), self.Revision(), self.Date(), self.Author()]) |
137 # parse templates here for less overhead in kwsub matchfunc |
85 def expand(self, mobj): |
138 for k in templates.keys(): |
86 '''Called from kwexpand, evaluates keyword.''' |
139 templates[k] = templater.parsestring(templates[k], |
|
140 quoted=False) |
|
141 self.templates = templates or deftemplates |
|
142 self.re_kw = re.compile(r'\$(%s)[^$]*?\$' % |
|
143 '|'.join([re.escape(k) for k in self.templates.keys()])) |
|
144 templater.common_filters['utcdate'] = utcdate |
|
145 try: |
|
146 self.t = cmdutil.changeset_templater(ui, repo, |
|
147 False, '', False) |
|
148 except TypeError: |
|
149 # depending on hg rev changeset_templater has extra "brinfo" arg |
|
150 self.t = cmdutil.changeset_templater(ui, repo, |
|
151 False, None, '', False) |
|
152 |
|
153 def kwsub(self, mobj, path, node): |
|
154 '''Substitutes keyword using corresponding template.''' |
87 kw = mobj.group(1) |
155 kw = mobj.group(1) |
88 return '$%s: %s $' % (kw, eval('self.%s()' % kw)) |
156 self.t.use_template(self.templates[kw]) |
|
157 self.ui.pushbuffer() |
|
158 self.t.show(changenode=node, root=self.repo.root, file=path) |
|
159 keywordsub = templater.firstline(self.ui.popbuffer()) |
|
160 return '$%s: %s $' % (kw, keywordsub) |
|
161 |
|
162 def expand(self, path, node, filelog, data): |
|
163 '''Returns data with expanded keywords.''' |
|
164 if util.binary(data): |
|
165 return data |
|
166 c = context.filectx(self.repo, path, fileid=node, filelog=filelog) |
|
167 cnode = c.node() |
|
168 return self.re_kw.sub(lambda m: self.kwsub(m, path, cnode), data) |
|
169 |
|
170 def shrink(self, text): |
|
171 '''Returns text with all keyword substitutions removed.''' |
|
172 if util.binary(text): |
|
173 return text |
|
174 return self.re_kw.sub(r'$\1$', text) |
|
175 |
|
176 def overwrite(self, candidates, node): |
|
177 '''Overwrites candidates in working dir expanding keywords.''' |
|
178 for f in candidates: |
|
179 data = self.repo.wfile(f).read() |
|
180 if not util.binary(data): |
|
181 data, kwct = self.re_kw.subn(lambda m: |
|
182 self.kwsub(m, f, node), data) |
|
183 if kwct: |
|
184 self.ui.debug(_('overwriting %s expanding keywords\n') % f) |
|
185 self.repo.wfile(f, 'w').write(data) |
|
186 |
|
187 class kwfilelog(filelog.filelog): |
|
188 ''' |
|
189 Superclass over filelog to customize its read, add, cmp methods. |
|
190 Keywords are "stored" unexpanded, and expanded on reading. |
|
191 ''' |
|
192 def __init__(self, opener, path, kwtemplater): |
|
193 super(kwfilelog, self).__init__(opener, path) |
|
194 self.path = path |
|
195 self.kwtemplater = kwtemplater |
|
196 |
|
197 def read(self, node): |
|
198 '''Substitutes keywords when reading filelog.''' |
|
199 data = super(kwfilelog, self).read(node) |
|
200 return self.kwtemplater.expand(self.path, node, self, data) |
|
201 |
|
202 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
203 '''Removes keyword substitutions when adding to filelog.''' |
|
204 text = self.kwtemplater.shrink(text) |
|
205 return super(kwfilelog, self).add(text, |
|
206 meta, tr, link, p1=p1, p2=p2) |
|
207 |
|
208 def cmp(self, node, text): |
|
209 '''Removes keyword substitutions for comparison.''' |
|
210 text = self.kwtemplater.shrink(text) |
|
211 return super(kwfilelog, self).cmp(node, text) |
89 |
212 |
90 |
213 |
91 def reposetup(ui, repo): |
214 def reposetup(ui, repo): |
92 from mercurial import filelog, revlog |
215 '''Sets up repo as kwrepo for keyword substitution. |
|
216 Overrides file method to return kwfilelog instead of filelog |
|
217 if file matches user configuration. |
|
218 Uses self-initializing pretxncommit-hook to overwrite configured files with |
|
219 updated keyword substitutions. |
|
220 This is done for local repos only, and only if there are |
|
221 files configured at all for keyword substitution.''' |
93 |
222 |
94 if not repo.local(): |
223 if not repo.local(): |
95 return |
224 return |
|
225 |
|
226 archivemode = (getcmd(repo.ui) == 'archive') |
|
227 |
|
228 inc, exc, archive, noarchive = [], ['.hg*'], [], ['.hg*'] |
|
229 for pat, opt in repo.ui.configitems('keyword'): |
|
230 if opt == 'archive': |
|
231 archive.append(pat) |
|
232 elif opt == 'noarchive': |
|
233 noarchive.append(pat) |
|
234 elif opt == 'ignore': |
|
235 exc.append(pat) |
|
236 else: |
|
237 inc.append(pat) |
|
238 if archivemode: |
|
239 inc, exc = archive, noarchive |
|
240 if not inc: |
|
241 return |
|
242 |
|
243 repo.kwfmatcher = util.matcher(repo.root, inc=inc, exc=exc)[1] |
96 |
244 |
97 class kwrepo(repo.__class__): |
245 class kwrepo(repo.__class__): |
98 def file(self, f): |
246 def file(self, f): |
99 if f[0] == '/': |
247 if f[0] == '/': |
100 f = f[1:] |
248 f = f[1:] |
101 return filelog.filelog(self.sopener, f, self, self.revlogversion) |
249 # only use kwfilelog when needed |
102 |
250 if self.kwfmatcher(f): |
103 class kwfilelog(filelog.filelog): |
251 kwt = kwtemplater(self.ui, self) |
104 ''' |
252 return kwfilelog(self.sopener, f, kwt) |
105 Superclass over filelog to customize it's read, add, cmp methods. |
253 else: |
106 Keywords are "stored" unexpanded, and expanded on reading. |
254 return filelog.filelog(self.sopener, f) |
107 ''' |
255 |
108 def __init__(self, opener, path, repo, |
|
109 defversion=revlog.REVLOG_DEFAULT_VERSION): |
|
110 super(kwfilelog, self).__init__(opener, path, defversion) |
|
111 self._repo = repo |
|
112 self._path = path |
|
113 |
|
114 def iskwcandidate(self, data): |
|
115 '''Decides whether to act on keywords.''' |
|
116 return (kwfmatches(ui, self._repo, [self._path]) |
|
117 and not util.binary(data)) |
|
118 |
|
119 def read(self, node): |
|
120 '''Substitutes keywords when reading filelog.''' |
|
121 data = super(kwfilelog, self).read(node) |
|
122 if self.iskwcandidate(data): |
|
123 kwfctx = kwfilectx(self._repo, self._path, |
|
124 fileid=node, filelog=self) |
|
125 return re_kw.sub(kwfctx.expand, data) |
|
126 return data |
|
127 |
|
128 def add(self, text, meta, tr, link, p1=None, p2=None): |
|
129 '''Removes keyword substitutions when adding to filelog.''' |
|
130 if self.iskwcandidate(text): |
|
131 text = re_kw.sub(r'$\1$', text) |
|
132 return super(kwfilelog, self).add(text, |
|
133 meta, tr, link, p1=p1, p2=p2) |
|
134 |
|
135 def cmp(self, node, text): |
|
136 '''Removes keyword substitutions for comparison.''' |
|
137 if self.iskwcandidate(text): |
|
138 text = re_kw.sub(r'$\1$', text) |
|
139 return super(kwfilelog, self).cmp(node, text) |
|
140 |
|
141 filelog.filelog = kwfilelog |
|
142 repo.__class__ = kwrepo |
256 repo.__class__ = kwrepo |
|
257 |
143 # make pretxncommit hook import kwmodule regardless of where it's located |
258 # make pretxncommit hook import kwmodule regardless of where it's located |
144 for k, v in sys.modules.iteritems(): |
259 for k, v in sys.modules.iteritems(): |
145 if v is None: |
260 if v is None: |
146 continue |
261 continue |
147 if not hasattr(v, '__file__'): |
262 if not hasattr(v, '__file__'): |