8 There are many good reasons why this is not needed in a distributed |
8 There are many good reasons why this is not needed in a distributed |
9 SCM, still it may be useful in very small projects based on single |
9 SCM, still it may be useful in very small projects based on single |
10 files (like LaTeX packages), that are mostly addressed to an audience |
10 files (like LaTeX packages), that are mostly addressed to an audience |
11 not running a version control system. |
11 not running a version control system. |
12 |
12 |
13 Supported keywords are (changeset 000000000000): |
13 Supported $keywords$ are: |
14 $Revision: 000000000000 $ |
14 Revision: changeset id |
15 $Author: Your Name <address@example.com> $ |
15 Author: full username |
16 $Date: %a %b %d %H:%M:%S %Y %z $ |
16 Date: %a %b %d %H:%M:%S %Y %z $ |
17 $RCSFile: basename,v $ |
17 RCSFile: basename,v |
18 $Source: /path/to/basename,v $ |
18 Source: /path/to/basename,v |
19 $Id: basename,v 000000000000 %Y-%m-%d %H:%M:%S %z shortname $ |
19 Id: basename,v csetid %Y-%m-%d %H:%M:%S %s shortname |
20 $Header: /path/to/basename,v 000000000000 %Y-%m-%d %H:%M:%S %z shortname $ |
20 Header: /path/to/basename,v csetid %Y-%m-%d %H:%M:%S %s shortname |
21 |
|
22 The extension, according to its hackish nature, is a hybrid and consists |
|
23 actually in 2 parts: |
|
24 |
|
25 1. pure extension code (reposetup) that is triggered on checkout and |
|
26 logging of changes. |
|
27 2. a pretxncommit hook (hgrc (5)) that expands keywords immediately |
|
28 at commit time in the working directory. |
|
29 |
21 |
30 Simple setup in hgrc: |
22 Simple setup in hgrc: |
31 |
23 |
32 # enable extension |
24 # enable extension |
|
25 # keyword.py in hgext folder, specify full path otherwise |
33 hgext.keyword = |
26 hgext.keyword = |
34 |
27 |
35 # filename patterns for expansion are configured in this section |
28 # filename patterns for expansion are configured in this section |
36 [keyword] |
29 [keyword] |
37 *.sty = expand |
30 **.py = expand |
38 ... |
31 ... |
39 |
|
40 # set up pretxncommit hook |
|
41 [hooks] |
|
42 pretxncommit = |
|
43 pretxncommit.keyword = python:hgext.keyword.pretxnkw |
|
44 ''' |
32 ''' |
45 |
33 |
|
34 from mercurial.node import * |
46 from mercurial.i18n import _ |
35 from mercurial.i18n import _ |
47 from mercurial import context, util |
36 from mercurial import context, filelog, revlog, util |
48 import os.path, re |
37 import os.path, re |
49 |
38 |
50 |
39 |
51 re_kw = re.compile( |
40 re_kw = re.compile( |
52 r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$') |
41 r'\$(Id|Header|Author|Date|Revision|RCSFile|Source)[^$]*?\$') |
53 |
42 |
54 |
43 |
55 def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None): |
44 def kwexpand(matchobj, repo, path, changeid=None, fileid=None, filelog=None): |
56 '''Called by kwfilelog.read and pretxnkw. |
45 '''Called by kwrepo.commit and kwfilelog.read. |
57 Sets supported keywords as local variables and evaluates them to |
46 Sets supported keywords as local variables and evaluates them to |
58 their expansion if matchobj is equal to string representation.''' |
47 their expansion if matchobj is equal to string representation.''' |
59 |
|
60 c = context.filectx(repo, path, |
48 c = context.filectx(repo, path, |
61 changeid=changeid, fileid=fileid, filelog=filelog) |
49 changeid=changeid, fileid=fileid, filelog=filelog) |
62 date = c.date() |
50 date = c.date() |
63 |
|
64 Revision = c.changectx() |
51 Revision = c.changectx() |
65 Author = c.user() |
52 Author = c.user() |
66 RCSFile = os.path.basename(path)+',v' |
53 RCSFile = os.path.basename(path)+',v' |
67 Source = repo.wjoin(path)+',v' |
54 Source = repo.wjoin(path)+',v' |
68 Date = util.datestr(date=date) |
55 Date = util.datestr(date=date) |
69 revdateauth = '%s %s %s' % (Revision, |
56 revdateauth = '%s %s %s' % (Revision, |
70 util.datestr(date=date, format=util.defaultdateformats[0]), |
57 util.datestr(date=date, format=util.defaultdateformats[0]), |
71 util.shortuser(Author)) |
58 util.shortuser(Author)) |
72 Header = '%s %s' % (Source, revdateauth) |
59 Header = '%s %s' % (Source, revdateauth) |
73 Id = '%s %s' % (RCSFile, revdateauth) |
60 Id = '%s %s' % (RCSFile, revdateauth) |
74 |
|
75 return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1))) |
61 return '$%s: %s $' % (matchobj.group(1), eval(matchobj.group(1))) |
76 |
62 |
77 def kwfmatchers(ui, repo): |
63 def kwfmatches(ui, repo, files): |
78 '''Returns filename matchers from ui keyword section.''' |
64 '''Selects candidates for keyword substitution |
79 return [util.matcher(repo.root, '', [pat], [], [])[1] |
65 configured in keyword section in hgrc.''' |
|
66 files = [f for f in files if not f.startswith('.hg')] |
|
67 if not files: |
|
68 return [] |
|
69 candidates = [] |
|
70 fmatchers = [util.matcher(repo.root, '', [pat], [], [])[1] |
80 for pat, opt in ui.configitems('keyword') |
71 for pat, opt in ui.configitems('keyword') |
81 if opt == 'expand'] |
72 if opt == 'expand'] |
|
73 for f in files: |
|
74 for mf in fmatchers: |
|
75 if mf(f): |
|
76 candidates.append(f) |
|
77 break |
|
78 return candidates |
82 |
79 |
83 |
80 |
84 def reposetup(ui, repo): |
81 def reposetup(ui, repo): |
85 from mercurial import filelog, revlog |
|
86 |
82 |
87 if not repo.local(): |
83 if not repo.local(): |
88 return |
84 return |
89 |
85 |
90 class kwrepo(repo.__class__): |
86 class kwrepo(repo.__class__): |
91 def file(self, f): |
87 def file(self, f): |
92 if f[0] == '/': |
88 if f[0] == '/': |
93 f = f[1:] |
89 f = f[1:] |
94 return filelog.filelog(self.sopener, f, self, self.revlogversion) |
90 return filelog.filelog(self.sopener, f, self, self.revlogversion) |
95 |
91 |
|
92 def commit(self, files=None, text="", user=None, date=None, |
|
93 match=util.always, force=False, lock=None, wlock=None, |
|
94 force_editor=False, p1=None, p2=None, extra={}): |
|
95 |
|
96 commit = [] |
|
97 remove = [] |
|
98 changed = [] |
|
99 use_dirstate = (p1 is None) # not rawcommit |
|
100 extra = extra.copy() |
|
101 |
|
102 if use_dirstate: |
|
103 if files: |
|
104 for f in files: |
|
105 s = self.dirstate.state(f) |
|
106 if s in 'nmai': |
|
107 commit.append(f) |
|
108 elif s == 'r': |
|
109 remove.append(f) |
|
110 else: |
|
111 ui.warn(_("%s not tracked!\n") % f) |
|
112 else: |
|
113 changes = self.status(match=match)[:5] |
|
114 modified, added, removed, deleted, unknown = changes |
|
115 commit = modified + added |
|
116 remove = removed |
|
117 else: |
|
118 commit = files |
|
119 |
|
120 if use_dirstate: |
|
121 p1, p2 = self.dirstate.parents() |
|
122 update_dirstate = True |
|
123 else: |
|
124 p1, p2 = p1, p2 or nullid |
|
125 update_dirstate = (self.dirstate.parents()[0] == p1) |
|
126 |
|
127 c1 = self.changelog.read(p1) |
|
128 c2 = self.changelog.read(p2) |
|
129 m1 = self.manifest.read(c1[0]).copy() |
|
130 m2 = self.manifest.read(c2[0]) |
|
131 |
|
132 if use_dirstate: |
|
133 branchname = self.workingctx().branch() |
|
134 try: |
|
135 branchname = branchname.decode('UTF-8').encode('UTF-8') |
|
136 except UnicodeDecodeError: |
|
137 raise util.Abort(_('branch name not in UTF-8!')) |
|
138 else: |
|
139 branchname = "" |
|
140 |
|
141 if use_dirstate: |
|
142 oldname = c1[5].get("branch", "") # stored in UTF-8 |
|
143 if not commit and not remove and not force and p2 == nullid and \ |
|
144 branchname == oldname: |
|
145 ui.status(_("nothing changed\n")) |
|
146 return None |
|
147 |
|
148 xp1 = hex(p1) |
|
149 if p2 == nullid: xp2 = '' |
|
150 else: xp2 = hex(p2) |
|
151 |
|
152 self.hook("precommit", throw=True, parent1=xp1, parent2=xp2) |
|
153 |
|
154 if not wlock: |
|
155 wlock = self.wlock() |
|
156 if not lock: |
|
157 lock = self.lock() |
|
158 tr = self.transaction() |
|
159 |
|
160 # check in files |
|
161 new = {} |
|
162 linkrev = self.changelog.count() |
|
163 commit.sort() |
|
164 is_exec = util.execfunc(self.root, m1.execf) |
|
165 is_link = util.linkfunc(self.root, m1.linkf) |
|
166 for f in commit: |
|
167 ui.note(f + "\n") |
|
168 try: |
|
169 new[f] = self.filecommit(f, m1, m2, linkrev, tr, changed) |
|
170 m1.set(f, is_exec(f), is_link(f)) |
|
171 except OSError: |
|
172 if use_dirstate: |
|
173 ui.warn(_("trouble committing %s!\n") % f) |
|
174 raise |
|
175 else: |
|
176 remove.append(f) |
|
177 |
|
178 # update manifest |
|
179 m1.update(new) |
|
180 remove.sort() |
|
181 removed = [] |
|
182 |
|
183 for f in remove: |
|
184 if f in m1: |
|
185 del m1[f] |
|
186 removed.append(f) |
|
187 mn = self.manifest.add(m1, tr, linkrev, c1[0], c2[0], (new, removed)) |
|
188 |
|
189 # add changeset |
|
190 new = new.keys() |
|
191 new.sort() |
|
192 |
|
193 user = user or ui.username() |
|
194 if not text or force_editor: |
|
195 edittext = [] |
|
196 if text: |
|
197 edittext.append(text) |
|
198 edittext.append("") |
|
199 edittext.append("HG: user: %s" % user) |
|
200 if p2 != nullid: |
|
201 edittext.append("HG: branch merge") |
|
202 edittext.extend(["HG: changed %s" % f for f in changed]) |
|
203 edittext.extend(["HG: removed %s" % f for f in removed]) |
|
204 if not changed and not remove: |
|
205 edittext.append("HG: no files changed") |
|
206 edittext.append("") |
|
207 # run editor in the repository root |
|
208 olddir = os.getcwd() |
|
209 os.chdir(self.root) |
|
210 text = ui.edit("\n".join(edittext), user) |
|
211 os.chdir(olddir) |
|
212 |
|
213 lines = [line.rstrip() for line in text.rstrip().splitlines()] |
|
214 while lines and not lines[0]: |
|
215 del lines[0] |
|
216 if not lines: |
|
217 return None |
|
218 text = '\n'.join(lines) |
|
219 if branchname: |
|
220 extra["branch"] = branchname |
|
221 n = self.changelog.add(mn, changed + removed, text, tr, p1, p2, |
|
222 user, date, extra) |
|
223 self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1, |
|
224 parent2=xp2) |
|
225 |
|
226 # substitute keywords |
|
227 for f in kwfmatches(ui, self, changed): |
|
228 data = self.wfile(f).read() |
|
229 if not util.binary(data): |
|
230 data, kwct = re_kw.subn(lambda m: |
|
231 kwexpand(m, self, f, changeid=hex(n)), |
|
232 data) |
|
233 if kwct: |
|
234 ui.debug(_('overwriting %s expanding keywords\n' |
|
235 % f)) |
|
236 self.wfile(f, 'w').write(data) |
|
237 |
|
238 tr.close() |
|
239 |
|
240 if use_dirstate or update_dirstate: |
|
241 self.dirstate.setparents(n) |
|
242 if use_dirstate: |
|
243 self.dirstate.update(new, "n") |
|
244 self.dirstate.forget(removed) |
|
245 |
|
246 self.hook("commit", node=hex(n), parent1=xp1, parent2=xp2) |
|
247 return n |
|
248 |
|
249 |
96 class kwfilelog(filelog.filelog): |
250 class kwfilelog(filelog.filelog): |
97 def __init__(self, opener, path, repo, |
251 def __init__(self, opener, path, repo, |
98 defversion=revlog.REVLOG_DEFAULT_VERSION): |
252 defversion=revlog.REVLOG_DEFAULT_VERSION): |
99 super(kwfilelog, self).__init__(opener, path, defversion) |
253 super(kwfilelog, self).__init__(opener, path, defversion) |
100 self._repo = repo |
254 self._repo = repo |
101 self._path = path |
255 self._path = path |
102 |
256 |
103 def read(self, node): |
257 def read(self, node): |
104 data = super(kwfilelog, self).read(node) |
258 data = super(kwfilelog, self).read(node) |
105 if not self._path.startswith('.hg') and not util.binary(data): |
259 if not util.binary(data) and \ |
106 for mf in kwfmatchers(ui, self._repo): |
260 kwfmatches(ui, self._repo, [self._path]): |
107 if mf(self._path): |
261 ui.debug(_('expanding keywords in %s\n' % self._path)) |
108 ui.debug(_('expanding keywords in %s\n' % self._path)) |
262 return re_kw.sub(lambda m: |
109 return re_kw.sub(lambda m: |
263 kwexpand(m, self._repo, self._path, |
110 kwexpand(m, self._repo, self._path, |
264 fileid=node, filelog=self), data) |
111 fileid=node, filelog=self), |
|
112 data) |
|
113 return data |
265 return data |
114 |
266 |
115 def size(self, rev): |
267 def size(self, rev): |
116 '''Overrides filelog's size() to use kwfilelog.read().''' |
268 '''Overrides filelog's size() to use kwfilelog.read().''' |
117 node = revlog.node(self, rev) |
269 node = revlog.node(self, rev) |