9 There are many good reasons why this is not needed in a distributed |
9 There are many good reasons why this is not needed in a distributed |
10 SCM, still it may be useful in very small projects based on single |
10 SCM, still it may be useful in very small projects based on single |
11 files (like LaTeX packages), that are mostly addressed to an audience |
11 files (like LaTeX packages), that are mostly addressed to an audience |
12 not running a version control system. |
12 not running a version control system. |
13 |
13 |
14 Supported $keywords$ and their $keyword: substition $ are: |
14 You can either use the default cvs-like keywords or provide your |
|
15 own in hgrc. |
|
16 |
|
17 Default $keywords$ and their $keyword: substition $ are: |
15 Revision: changeset id |
18 Revision: changeset id |
16 Author: short username |
19 Author: username |
17 Date: %Y/%m/%d %H:%M:%S [UTC] |
20 Date: %Y/%m/%d %H:%M:%S [UTC] |
18 RCSFile: basename,v |
21 RCSFile: basename,v |
19 Source: /path/to/basename,v |
22 Source: /path/to/basename,v |
20 Id: basename,v csetid %Y/%m/%d %H:%M:%S shortname |
23 Id: basename,v csetid %Y/%m/%d %H:%M:%S username |
21 Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S shortname |
24 Header: /path/to/basename,v csetid %Y/%m/%d %H:%M:%S username |
22 |
25 |
23 Simple setup in hgrc: |
26 Simple setup in hgrc: |
24 |
27 |
25 # enable extension |
28 # enable extension |
26 hgext.keyword = |
29 hgext.keyword = /full/path/to/script |
27 # or, if script not in hgext folder: |
30 # or, if script in hgext folder: |
28 # hgext.keyword = /full/path/to/script |
31 # hgext.keyword = |
29 |
32 |
30 # filename patterns for expansion are configured in this section |
33 # filename patterns for expansion are configured in this section |
31 [keyword] |
34 [keyword] |
32 **.py = expand |
35 **.py = expand |
33 ... |
36 ... |
|
37 # in case you prefer you own keyword maps over the cvs-like defaults: |
|
38 [keywordmaps] |
|
39 hgdate = {date|rfc822date} |
|
40 lastlog = {desc} |
|
41 checked in by = {author} |
|
42 ... |
34 ''' |
43 ''' |
35 |
44 |
36 from mercurial import context, util |
45 from mercurial import cmdutil, templater, util |
37 import os.path, re, sys, time |
46 import os.path, re, sys, time |
38 |
47 |
39 re_kw = re.compile( |
48 deftemplates = { |
40 r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$') |
49 'Revision': '{node|short}', |
41 |
50 'Author': '{author|user}', |
42 def kwexpand(mobj, kwfctx): |
51 'Date': '{date|utcdate}', |
43 '''Called by kwfilelog.read and pretxnkw. |
52 'RCSFile': '{file|basename},v', |
44 Expands keywords according to file context.''' |
53 'Source': '{root}/{file},v', |
45 return '$%s: %s $' % (mobj.group(1), kwfctx.expand(mobj.group(1))) |
54 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}', |
|
55 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}', |
|
56 } |
|
57 |
|
58 def utcdate(date): |
|
59 '''Returns hgdate in cvs-like UTC format.''' |
|
60 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
|
61 |
|
62 templater.common_filters['utcdate'] = utcdate |
46 |
63 |
47 def kwfmatches(ui, repo, files): |
64 def kwfmatches(ui, repo, files): |
48 '''Selects candidates for keyword substitution |
65 '''Selects candidates for keyword substitution |
49 configured in keyword section in hgrc.''' |
66 configured in keyword section in hgrc.''' |
50 files = [f for f in files if not f.startswith('.hg')] |
67 files = [f for f in files if not f.startswith('.hg')] |
58 if mf(f): |
75 if mf(f): |
59 candidates.append(f) |
76 candidates.append(f) |
60 break |
77 break |
61 return candidates |
78 return candidates |
62 |
79 |
63 def utcdate(date): |
80 |
64 '''Returns hgdate in cvs-like UTC format.''' |
81 class kwtemplater(object): |
65 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0])) |
|
66 |
|
67 |
|
68 class kwfilectx(context.filectx): |
|
69 ''' |
82 ''' |
70 Provides keyword expansion functions based on file context. |
83 Sets up keyword templates, corresponding keyword regex, and |
|
84 provides keyword expansion function. |
71 ''' |
85 ''' |
72 def __init__(self, repo, path, changeid=None, fileid=None, filelog=None): |
86 def __init__(self, ui, repo, node=None): |
73 context.filectx.__init__(self, repo, path, changeid, fileid, filelog) |
87 self.ui = ui |
74 def Revision(self): |
88 self.repo = repo |
75 return str(self.changectx()) |
89 self.node = node |
76 def Author(self): |
90 templates = {} |
77 return util.shortuser(self.user()) |
91 for k, v in self.ui.configitems('keywordmaps'): |
78 def Date(self): |
92 templates[k] = v |
79 return utcdate(self.date()) |
93 self.templates = templates or deftemplates |
80 def RCSFile(self): |
94 self.re_kw = re.compile(r'\$(%s)[^$]*?\$' % |
81 return os.path.basename(self._path)+',v' |
95 '|'.join(re.escape(k) for k in self.templates.keys())) |
82 def Source(self): |
96 self.t = cmdutil.changeset_templater(self.ui, self.repo, |
83 return self._repo.wjoin(self._path)+',v' |
97 False, '', False) |
84 def Header(self): |
98 |
85 return ' '.join( |
99 def expand(self, mobj, path): |
86 [self.Source(), self.Revision(), self.Date(), self.Author()]) |
100 kw = mobj.group(1) |
87 def Id(self): |
101 template = templater.parsestring(self.templates[kw], quoted=False) |
88 return ' '.join( |
102 self.t.use_template(template) |
89 [self.RCSFile(), self.Revision(), self.Date(), self.Author()]) |
103 self.ui.pushbuffer() |
90 def expand(self, kw): |
104 self.t.show(changenode=self.node, |
91 '''Called from kwexpand, evaluates keyword.''' |
105 changes=self.repo.changelog.read(self.node), |
92 return eval('self.%s()' % kw) |
106 root=self.repo.root, file=path) |
|
107 return '$%s: %s $' % (kw, self.ui.popbuffer()) |
93 |
108 |
94 |
109 |
95 def reposetup(ui, repo): |
110 def reposetup(ui, repo): |
96 from mercurial import filelog, revlog |
111 from mercurial import context, filelog, revlog |
97 |
|
98 if not repo.local(): |
112 if not repo.local(): |
99 return |
113 return |
100 |
114 |
101 class kwrepo(repo.__class__): |
115 class kwrepo(repo.__class__): |
102 def file(self, f): |
116 def file(self, f): |
112 def __init__(self, opener, path, repo, |
126 def __init__(self, opener, path, repo, |
113 defversion=revlog.REVLOG_DEFAULT_VERSION): |
127 defversion=revlog.REVLOG_DEFAULT_VERSION): |
114 super(kwfilelog, self).__init__(opener, path, defversion) |
128 super(kwfilelog, self).__init__(opener, path, defversion) |
115 self._repo = repo |
129 self._repo = repo |
116 self._path = path |
130 self._path = path |
|
131 self.kwt = kwtemplater(ui, self._repo) |
117 |
132 |
118 def iskwcandidate(self, data): |
133 def iskwcandidate(self, data): |
119 '''Decides whether to act on keywords.''' |
134 '''Decides whether to act on keywords.''' |
120 return (kwfmatches(ui, self._repo, [self._path]) |
135 return (kwfmatches(ui, self._repo, [self._path]) |
121 and not util.binary(data)) |
136 and not util.binary(data)) |
122 |
137 |
123 def read(self, node): |
138 def read(self, node): |
124 '''Substitutes keywords when reading filelog.''' |
139 '''Substitutes keywords when reading filelog.''' |
125 data = super(kwfilelog, self).read(node) |
140 data = super(kwfilelog, self).read(node) |
126 if self.iskwcandidate(data): |
141 if self.iskwcandidate(data): |
127 kwfctx = kwfilectx(self._repo, self._path, |
142 c = context.filectx(self._repo, self._path, |
128 fileid=node, filelog=self) |
143 fileid=node, filelog=self) |
129 return re_kw.sub(lambda m: kwexpand(m, kwfctx), data) |
144 self.kwt.node = c.node() |
|
145 return self.kwt.re_kw.sub(lambda m: |
|
146 self.kwt.expand(m, self._path), data) |
130 return data |
147 return data |
131 |
148 |
132 def add(self, text, meta, tr, link, p1=None, p2=None): |
149 def add(self, text, meta, tr, link, p1=None, p2=None): |
133 '''Removes keyword substitutions when adding to filelog.''' |
150 '''Removes keyword substitutions when adding to filelog.''' |
134 if self.iskwcandidate(text): |
151 if self.iskwcandidate(text): |
135 text = re_kw.sub(r'$\1$', text) |
152 text = self.kwt.re_kw.sub(r'$\1$', text) |
136 return super(kwfilelog, self).add(text, |
153 return super(kwfilelog, self).add(text, |
137 meta, tr, link, p1=p1, p2=p2) |
154 meta, tr, link, p1=p1, p2=p2) |
138 |
155 |
139 def cmp(self, node, text): |
156 def cmp(self, node, text): |
140 '''Removes keyword substitutions for comparison.''' |
157 '''Removes keyword substitutions for comparison.''' |
141 if self.iskwcandidate(text): |
158 if self.iskwcandidate(text): |
142 text = re_kw.sub(r'$\1$', text) |
159 text = self.kwt.re_kw.sub(r'$\1$', text) |
143 return super(kwfilelog, self).cmp(node, text) |
160 return super(kwfilelog, self).cmp(node, text) |
144 |
161 |
145 filelog.filelog = kwfilelog |
162 filelog.filelog = kwfilelog |
146 repo.__class__ = kwrepo |
163 repo.__class__ = kwrepo |
147 # make pretxncommit hook import kwmodule regardless of where it's located |
164 # make pretxncommit hook import kwmodule regardless of where it's located |
165 on commit and expands keywords in working dir.''' |
182 on commit and expands keywords in working dir.''' |
166 from mercurial.i18n import gettext as _ |
183 from mercurial.i18n import gettext as _ |
167 # above line for backwards compatibility; can be changed to |
184 # above line for backwards compatibility; can be changed to |
168 # from mercurial.i18n import _ |
185 # from mercurial.i18n import _ |
169 # some day |
186 # some day |
170 from mercurial import cmdutil, commands |
187 from mercurial import commands |
171 |
188 |
172 if hooktype != 'pretxncommit': |
189 if hooktype != 'pretxncommit': |
173 return True |
190 return True |
174 |
191 |
175 cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:] |
192 cmd, sysargs, globalopts, cmdopts = commands.parse(ui, sys.argv[1:])[1:] |
176 if repr(cmd).split()[1] in ('tag', 'import_'): |
193 if repr(cmd).split()[1] in ('tag', 'import_'): |
177 return False |
194 return False |
178 |
195 |
179 files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts) |
196 files, match, anypats = cmdutil.matchpats(repo, sysargs, cmdopts) |
180 modified, added = repo.status(files=files, match=match)[:2] |
197 modified, added = repo.status(files=files, match=match)[:2] |
181 |
198 candidates = kwfmatches(ui, repo, modified+added) |
182 for f in kwfmatches(ui, repo, modified+added): |
199 if not candidates: |
|
200 return False |
|
201 |
|
202 kwt = kwtemplater(ui, repo, node=repo.changelog.tip()) |
|
203 for f in candidates: |
183 data = repo.wfile(f).read() |
204 data = repo.wfile(f).read() |
184 if not util.binary(data): |
205 if not util.binary(data): |
185 kwfctx = kwfilectx(repo, f, changeid=args['node']) |
206 data, kwct = kwt.re_kw.subn(lambda m: kwt.expand(m, f), data) |
186 data, kwct = re_kw.subn(lambda m: kwexpand(m, kwfctx), data) |
|
187 if kwct: |
207 if kwct: |
188 ui.debug(_('overwriting %s expanding keywords\n' % f)) |
208 ui.debug(_('overwriting %s expanding keywords\n' % f)) |
189 repo.wfile(f, 'w').write(data) |
209 repo.wfile(f, 'w').write(data) |