##// END OF EJS Templates
hggettext: handle i18nfunctions declaration for docstrings translations
Patrick Mezard -
r12823:80deae3b stable
parent child Browse files
Show More
@@ -1,568 +1,571 b''
1 # Mercurial extension to provide the 'hg bookmark' command
1 # Mercurial extension to provide the 'hg bookmark' command
2 #
2 #
3 # Copyright 2008 David Soria Parra <dsp@php.net>
3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''track a line of development with movable markers
8 '''track a line of development with movable markers
9
9
10 Bookmarks are local movable markers to changesets. Every bookmark
10 Bookmarks are local movable markers to changesets. Every bookmark
11 points to a changeset identified by its hash. If you commit a
11 points to a changeset identified by its hash. If you commit a
12 changeset that is based on a changeset that has a bookmark on it, the
12 changeset that is based on a changeset that has a bookmark on it, the
13 bookmark shifts to the new changeset.
13 bookmark shifts to the new changeset.
14
14
15 It is possible to use bookmark names in every revision lookup (e.g.
15 It is possible to use bookmark names in every revision lookup (e.g.
16 :hg:`merge`, :hg:`update`).
16 :hg:`merge`, :hg:`update`).
17
17
18 By default, when several bookmarks point to the same changeset, they
18 By default, when several bookmarks point to the same changeset, they
19 will all move forward together. It is possible to obtain a more
19 will all move forward together. It is possible to obtain a more
20 git-like experience by adding the following configuration option to
20 git-like experience by adding the following configuration option to
21 your configuration file::
21 your configuration file::
22
22
23 [bookmarks]
23 [bookmarks]
24 track.current = True
24 track.current = True
25
25
26 This will cause Mercurial to track the bookmark that you are currently
26 This will cause Mercurial to track the bookmark that you are currently
27 using, and only update it. This is similar to git's approach to
27 using, and only update it. This is similar to git's approach to
28 branching.
28 branching.
29 '''
29 '''
30
30
31 from mercurial.i18n import _
31 from mercurial.i18n import _
32 from mercurial.node import nullid, nullrev, bin, hex, short
32 from mercurial.node import nullid, nullrev, bin, hex, short
33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 from mercurial import revset
34 from mercurial import revset
35 import os
35 import os
36
36
37 def write(repo):
37 def write(repo):
38 '''Write bookmarks
38 '''Write bookmarks
39
39
40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
41 in a format equal to those of localtags.
41 in a format equal to those of localtags.
42
42
43 We also store a backup of the previous state in undo.bookmarks that
43 We also store a backup of the previous state in undo.bookmarks that
44 can be copied back on rollback.
44 can be copied back on rollback.
45 '''
45 '''
46 refs = repo._bookmarks
46 refs = repo._bookmarks
47 if os.path.exists(repo.join('bookmarks')):
47 if os.path.exists(repo.join('bookmarks')):
48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
49 if repo._bookmarkcurrent not in refs:
49 if repo._bookmarkcurrent not in refs:
50 setcurrent(repo, None)
50 setcurrent(repo, None)
51 wlock = repo.wlock()
51 wlock = repo.wlock()
52 try:
52 try:
53 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 file = repo.opener('bookmarks', 'w', atomictemp=True)
54 for refspec, node in refs.iteritems():
54 for refspec, node in refs.iteritems():
55 file.write("%s %s\n" % (hex(node), refspec))
55 file.write("%s %s\n" % (hex(node), refspec))
56 file.rename()
56 file.rename()
57
57
58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
59 try:
59 try:
60 os.utime(repo.sjoin('00changelog.i'), None)
60 os.utime(repo.sjoin('00changelog.i'), None)
61 except OSError:
61 except OSError:
62 pass
62 pass
63
63
64 finally:
64 finally:
65 wlock.release()
65 wlock.release()
66
66
67 def setcurrent(repo, mark):
67 def setcurrent(repo, mark):
68 '''Set the name of the bookmark that we are currently on
68 '''Set the name of the bookmark that we are currently on
69
69
70 Set the name of the bookmark that we are on (hg update <bookmark>).
70 Set the name of the bookmark that we are on (hg update <bookmark>).
71 The name is recorded in .hg/bookmarks.current
71 The name is recorded in .hg/bookmarks.current
72 '''
72 '''
73 current = repo._bookmarkcurrent
73 current = repo._bookmarkcurrent
74 if current == mark:
74 if current == mark:
75 return
75 return
76
76
77 refs = repo._bookmarks
77 refs = repo._bookmarks
78
78
79 # do not update if we do update to a rev equal to the current bookmark
79 # do not update if we do update to a rev equal to the current bookmark
80 if (mark and mark not in refs and
80 if (mark and mark not in refs and
81 current and refs[current] == repo.changectx('.').node()):
81 current and refs[current] == repo.changectx('.').node()):
82 return
82 return
83 if mark not in refs:
83 if mark not in refs:
84 mark = ''
84 mark = ''
85 wlock = repo.wlock()
85 wlock = repo.wlock()
86 try:
86 try:
87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
88 file.write(mark)
88 file.write(mark)
89 file.rename()
89 file.rename()
90 finally:
90 finally:
91 wlock.release()
91 wlock.release()
92 repo._bookmarkcurrent = mark
92 repo._bookmarkcurrent = mark
93
93
94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
95 '''track a line of development with movable markers
95 '''track a line of development with movable markers
96
96
97 Bookmarks are pointers to certain commits that move when
97 Bookmarks are pointers to certain commits that move when
98 committing. Bookmarks are local. They can be renamed, copied and
98 committing. Bookmarks are local. They can be renamed, copied and
99 deleted. It is possible to use bookmark names in :hg:`merge` and
99 deleted. It is possible to use bookmark names in :hg:`merge` and
100 :hg:`update` to merge and update respectively to a given bookmark.
100 :hg:`update` to merge and update respectively to a given bookmark.
101
101
102 You can use :hg:`bookmark NAME` to set a bookmark on the working
102 You can use :hg:`bookmark NAME` to set a bookmark on the working
103 directory's parent revision with the given name. If you specify
103 directory's parent revision with the given name. If you specify
104 a revision using -r REV (where REV may be an existing bookmark),
104 a revision using -r REV (where REV may be an existing bookmark),
105 the bookmark is assigned to that revision.
105 the bookmark is assigned to that revision.
106
106
107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
108 push` and :hg:`help pull`). This requires the bookmark extension to be
108 push` and :hg:`help pull`). This requires the bookmark extension to be
109 enabled for both the local and remote repositories.
109 enabled for both the local and remote repositories.
110 '''
110 '''
111 hexfn = ui.debugflag and hex or short
111 hexfn = ui.debugflag and hex or short
112 marks = repo._bookmarks
112 marks = repo._bookmarks
113 cur = repo.changectx('.').node()
113 cur = repo.changectx('.').node()
114
114
115 if rename:
115 if rename:
116 if rename not in marks:
116 if rename not in marks:
117 raise util.Abort(_("a bookmark of this name does not exist"))
117 raise util.Abort(_("a bookmark of this name does not exist"))
118 if mark in marks and not force:
118 if mark in marks and not force:
119 raise util.Abort(_("a bookmark of the same name already exists"))
119 raise util.Abort(_("a bookmark of the same name already exists"))
120 if mark is None:
120 if mark is None:
121 raise util.Abort(_("new bookmark name required"))
121 raise util.Abort(_("new bookmark name required"))
122 marks[mark] = marks[rename]
122 marks[mark] = marks[rename]
123 del marks[rename]
123 del marks[rename]
124 if repo._bookmarkcurrent == rename:
124 if repo._bookmarkcurrent == rename:
125 setcurrent(repo, mark)
125 setcurrent(repo, mark)
126 write(repo)
126 write(repo)
127 return
127 return
128
128
129 if delete:
129 if delete:
130 if mark is None:
130 if mark is None:
131 raise util.Abort(_("bookmark name required"))
131 raise util.Abort(_("bookmark name required"))
132 if mark not in marks:
132 if mark not in marks:
133 raise util.Abort(_("a bookmark of this name does not exist"))
133 raise util.Abort(_("a bookmark of this name does not exist"))
134 if mark == repo._bookmarkcurrent:
134 if mark == repo._bookmarkcurrent:
135 setcurrent(repo, None)
135 setcurrent(repo, None)
136 del marks[mark]
136 del marks[mark]
137 write(repo)
137 write(repo)
138 return
138 return
139
139
140 if mark != None:
140 if mark != None:
141 if "\n" in mark:
141 if "\n" in mark:
142 raise util.Abort(_("bookmark name cannot contain newlines"))
142 raise util.Abort(_("bookmark name cannot contain newlines"))
143 mark = mark.strip()
143 mark = mark.strip()
144 if not mark:
144 if not mark:
145 raise util.Abort(_("bookmark names cannot consist entirely of "
145 raise util.Abort(_("bookmark names cannot consist entirely of "
146 "whitespace"))
146 "whitespace"))
147 if mark in marks and not force:
147 if mark in marks and not force:
148 raise util.Abort(_("a bookmark of the same name already exists"))
148 raise util.Abort(_("a bookmark of the same name already exists"))
149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
150 and not force):
150 and not force):
151 raise util.Abort(
151 raise util.Abort(
152 _("a bookmark cannot have the name of an existing branch"))
152 _("a bookmark cannot have the name of an existing branch"))
153 if rev:
153 if rev:
154 marks[mark] = repo.lookup(rev)
154 marks[mark] = repo.lookup(rev)
155 else:
155 else:
156 marks[mark] = repo.changectx('.').node()
156 marks[mark] = repo.changectx('.').node()
157 setcurrent(repo, mark)
157 setcurrent(repo, mark)
158 write(repo)
158 write(repo)
159 return
159 return
160
160
161 if mark is None:
161 if mark is None:
162 if rev:
162 if rev:
163 raise util.Abort(_("bookmark name required"))
163 raise util.Abort(_("bookmark name required"))
164 if len(marks) == 0:
164 if len(marks) == 0:
165 ui.status(_("no bookmarks set\n"))
165 ui.status(_("no bookmarks set\n"))
166 else:
166 else:
167 for bmark, n in marks.iteritems():
167 for bmark, n in marks.iteritems():
168 if ui.configbool('bookmarks', 'track.current'):
168 if ui.configbool('bookmarks', 'track.current'):
169 current = repo._bookmarkcurrent
169 current = repo._bookmarkcurrent
170 if bmark == current and n == cur:
170 if bmark == current and n == cur:
171 prefix, label = '*', 'bookmarks.current'
171 prefix, label = '*', 'bookmarks.current'
172 else:
172 else:
173 prefix, label = ' ', ''
173 prefix, label = ' ', ''
174 else:
174 else:
175 if n == cur:
175 if n == cur:
176 prefix, label = '*', 'bookmarks.current'
176 prefix, label = '*', 'bookmarks.current'
177 else:
177 else:
178 prefix, label = ' ', ''
178 prefix, label = ' ', ''
179
179
180 if ui.quiet:
180 if ui.quiet:
181 ui.write("%s\n" % bmark, label=label)
181 ui.write("%s\n" % bmark, label=label)
182 else:
182 else:
183 ui.write(" %s %-25s %d:%s\n" % (
183 ui.write(" %s %-25s %d:%s\n" % (
184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
185 label=label)
185 label=label)
186 return
186 return
187
187
188 def _revstostrip(changelog, node):
188 def _revstostrip(changelog, node):
189 srev = changelog.rev(node)
189 srev = changelog.rev(node)
190 tostrip = [srev]
190 tostrip = [srev]
191 saveheads = []
191 saveheads = []
192 for r in xrange(srev, len(changelog)):
192 for r in xrange(srev, len(changelog)):
193 parents = changelog.parentrevs(r)
193 parents = changelog.parentrevs(r)
194 if parents[0] in tostrip or parents[1] in tostrip:
194 if parents[0] in tostrip or parents[1] in tostrip:
195 tostrip.append(r)
195 tostrip.append(r)
196 if parents[1] != nullrev:
196 if parents[1] != nullrev:
197 for p in parents:
197 for p in parents:
198 if p not in tostrip and p > srev:
198 if p not in tostrip and p > srev:
199 saveheads.append(p)
199 saveheads.append(p)
200 return [r for r in tostrip if r not in saveheads]
200 return [r for r in tostrip if r not in saveheads]
201
201
202 def strip(oldstrip, ui, repo, node, backup="all"):
202 def strip(oldstrip, ui, repo, node, backup="all"):
203 """Strip bookmarks if revisions are stripped using
203 """Strip bookmarks if revisions are stripped using
204 the mercurial.strip method. This usually happens during
204 the mercurial.strip method. This usually happens during
205 qpush and qpop"""
205 qpush and qpop"""
206 revisions = _revstostrip(repo.changelog, node)
206 revisions = _revstostrip(repo.changelog, node)
207 marks = repo._bookmarks
207 marks = repo._bookmarks
208 update = []
208 update = []
209 for mark, n in marks.iteritems():
209 for mark, n in marks.iteritems():
210 if repo.changelog.rev(n) in revisions:
210 if repo.changelog.rev(n) in revisions:
211 update.append(mark)
211 update.append(mark)
212 oldstrip(ui, repo, node, backup)
212 oldstrip(ui, repo, node, backup)
213 if len(update) > 0:
213 if len(update) > 0:
214 for m in update:
214 for m in update:
215 marks[m] = repo.changectx('.').node()
215 marks[m] = repo.changectx('.').node()
216 write(repo)
216 write(repo)
217
217
218 def reposetup(ui, repo):
218 def reposetup(ui, repo):
219 if not repo.local():
219 if not repo.local():
220 return
220 return
221
221
222 class bookmark_repo(repo.__class__):
222 class bookmark_repo(repo.__class__):
223
223
224 @util.propertycache
224 @util.propertycache
225 def _bookmarks(self):
225 def _bookmarks(self):
226 '''Parse .hg/bookmarks file and return a dictionary
226 '''Parse .hg/bookmarks file and return a dictionary
227
227
228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
229 in the .hg/bookmarks file.
229 in the .hg/bookmarks file.
230 Read the file and return a (name=>nodeid) dictionary
230 Read the file and return a (name=>nodeid) dictionary
231 '''
231 '''
232 try:
232 try:
233 bookmarks = {}
233 bookmarks = {}
234 for line in self.opener('bookmarks'):
234 for line in self.opener('bookmarks'):
235 sha, refspec = line.strip().split(' ', 1)
235 sha, refspec = line.strip().split(' ', 1)
236 bookmarks[refspec] = self.changelog.lookup(sha)
236 bookmarks[refspec] = self.changelog.lookup(sha)
237 except:
237 except:
238 pass
238 pass
239 return bookmarks
239 return bookmarks
240
240
241 @util.propertycache
241 @util.propertycache
242 def _bookmarkcurrent(self):
242 def _bookmarkcurrent(self):
243 '''Get the current bookmark
243 '''Get the current bookmark
244
244
245 If we use gittishsh branches we have a current bookmark that
245 If we use gittishsh branches we have a current bookmark that
246 we are on. This function returns the name of the bookmark. It
246 we are on. This function returns the name of the bookmark. It
247 is stored in .hg/bookmarks.current
247 is stored in .hg/bookmarks.current
248 '''
248 '''
249 mark = None
249 mark = None
250 if os.path.exists(self.join('bookmarks.current')):
250 if os.path.exists(self.join('bookmarks.current')):
251 file = self.opener('bookmarks.current')
251 file = self.opener('bookmarks.current')
252 # No readline() in posixfile_nt, reading everything is cheap
252 # No readline() in posixfile_nt, reading everything is cheap
253 mark = (file.readlines() or [''])[0]
253 mark = (file.readlines() or [''])[0]
254 if mark == '':
254 if mark == '':
255 mark = None
255 mark = None
256 file.close()
256 file.close()
257 return mark
257 return mark
258
258
259 def rollback(self, *args):
259 def rollback(self, *args):
260 if os.path.exists(self.join('undo.bookmarks')):
260 if os.path.exists(self.join('undo.bookmarks')):
261 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
261 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
262 return super(bookmark_repo, self).rollback(*args)
262 return super(bookmark_repo, self).rollback(*args)
263
263
264 def lookup(self, key):
264 def lookup(self, key):
265 if key in self._bookmarks:
265 if key in self._bookmarks:
266 key = self._bookmarks[key]
266 key = self._bookmarks[key]
267 return super(bookmark_repo, self).lookup(key)
267 return super(bookmark_repo, self).lookup(key)
268
268
269 def _bookmarksupdate(self, parents, node):
269 def _bookmarksupdate(self, parents, node):
270 marks = self._bookmarks
270 marks = self._bookmarks
271 update = False
271 update = False
272 if ui.configbool('bookmarks', 'track.current'):
272 if ui.configbool('bookmarks', 'track.current'):
273 mark = self._bookmarkcurrent
273 mark = self._bookmarkcurrent
274 if mark and marks[mark] in parents:
274 if mark and marks[mark] in parents:
275 marks[mark] = node
275 marks[mark] = node
276 update = True
276 update = True
277 else:
277 else:
278 for mark, n in marks.items():
278 for mark, n in marks.items():
279 if n in parents:
279 if n in parents:
280 marks[mark] = node
280 marks[mark] = node
281 update = True
281 update = True
282 if update:
282 if update:
283 write(self)
283 write(self)
284
284
285 def commitctx(self, ctx, error=False):
285 def commitctx(self, ctx, error=False):
286 """Add a revision to the repository and
286 """Add a revision to the repository and
287 move the bookmark"""
287 move the bookmark"""
288 wlock = self.wlock() # do both commit and bookmark with lock held
288 wlock = self.wlock() # do both commit and bookmark with lock held
289 try:
289 try:
290 node = super(bookmark_repo, self).commitctx(ctx, error)
290 node = super(bookmark_repo, self).commitctx(ctx, error)
291 if node is None:
291 if node is None:
292 return None
292 return None
293 parents = self.changelog.parents(node)
293 parents = self.changelog.parents(node)
294 if parents[1] == nullid:
294 if parents[1] == nullid:
295 parents = (parents[0],)
295 parents = (parents[0],)
296
296
297 self._bookmarksupdate(parents, node)
297 self._bookmarksupdate(parents, node)
298 return node
298 return node
299 finally:
299 finally:
300 wlock.release()
300 wlock.release()
301
301
302 def pull(self, remote, heads=None, force=False):
302 def pull(self, remote, heads=None, force=False):
303 result = super(bookmark_repo, self).pull(remote, heads, force)
303 result = super(bookmark_repo, self).pull(remote, heads, force)
304
304
305 self.ui.debug("checking for updated bookmarks\n")
305 self.ui.debug("checking for updated bookmarks\n")
306 rb = remote.listkeys('bookmarks')
306 rb = remote.listkeys('bookmarks')
307 changed = False
307 changed = False
308 for k in rb.keys():
308 for k in rb.keys():
309 if k in self._bookmarks:
309 if k in self._bookmarks:
310 nr, nl = rb[k], self._bookmarks[k]
310 nr, nl = rb[k], self._bookmarks[k]
311 if nr in self:
311 if nr in self:
312 cr = self[nr]
312 cr = self[nr]
313 cl = self[nl]
313 cl = self[nl]
314 if cl.rev() >= cr.rev():
314 if cl.rev() >= cr.rev():
315 continue
315 continue
316 if cr in cl.descendants():
316 if cr in cl.descendants():
317 self._bookmarks[k] = cr.node()
317 self._bookmarks[k] = cr.node()
318 changed = True
318 changed = True
319 self.ui.status(_("updating bookmark %s\n") % k)
319 self.ui.status(_("updating bookmark %s\n") % k)
320 else:
320 else:
321 self.ui.warn(_("not updating divergent"
321 self.ui.warn(_("not updating divergent"
322 " bookmark %s\n") % k)
322 " bookmark %s\n") % k)
323 if changed:
323 if changed:
324 write(repo)
324 write(repo)
325
325
326 return result
326 return result
327
327
328 def push(self, remote, force=False, revs=None, newbranch=False):
328 def push(self, remote, force=False, revs=None, newbranch=False):
329 result = super(bookmark_repo, self).push(remote, force, revs,
329 result = super(bookmark_repo, self).push(remote, force, revs,
330 newbranch)
330 newbranch)
331
331
332 self.ui.debug("checking for updated bookmarks\n")
332 self.ui.debug("checking for updated bookmarks\n")
333 rb = remote.listkeys('bookmarks')
333 rb = remote.listkeys('bookmarks')
334 for k in rb.keys():
334 for k in rb.keys():
335 if k in self._bookmarks:
335 if k in self._bookmarks:
336 nr, nl = rb[k], self._bookmarks[k]
336 nr, nl = rb[k], self._bookmarks[k]
337 if nr in self:
337 if nr in self:
338 cr = self[nr]
338 cr = self[nr]
339 cl = self[nl]
339 cl = self[nl]
340 if cl in cr.descendants():
340 if cl in cr.descendants():
341 r = remote.pushkey('bookmarks', k, nr, nl)
341 r = remote.pushkey('bookmarks', k, nr, nl)
342 if r:
342 if r:
343 self.ui.status(_("updating bookmark %s\n") % k)
343 self.ui.status(_("updating bookmark %s\n") % k)
344 else:
344 else:
345 self.ui.warn(_('updating bookmark %s'
345 self.ui.warn(_('updating bookmark %s'
346 ' failed!\n') % k)
346 ' failed!\n') % k)
347
347
348 return result
348 return result
349
349
350 def addchangegroup(self, *args, **kwargs):
350 def addchangegroup(self, *args, **kwargs):
351 parents = self.dirstate.parents()
351 parents = self.dirstate.parents()
352
352
353 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
353 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
354 if result > 1:
354 if result > 1:
355 # We have more heads than before
355 # We have more heads than before
356 return result
356 return result
357 node = self.changelog.tip()
357 node = self.changelog.tip()
358
358
359 self._bookmarksupdate(parents, node)
359 self._bookmarksupdate(parents, node)
360 return result
360 return result
361
361
362 def _findtags(self):
362 def _findtags(self):
363 """Merge bookmarks with normal tags"""
363 """Merge bookmarks with normal tags"""
364 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
364 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
365 tags.update(self._bookmarks)
365 tags.update(self._bookmarks)
366 return (tags, tagtypes)
366 return (tags, tagtypes)
367
367
368 if hasattr(repo, 'invalidate'):
368 if hasattr(repo, 'invalidate'):
369 def invalidate(self):
369 def invalidate(self):
370 super(bookmark_repo, self).invalidate()
370 super(bookmark_repo, self).invalidate()
371 for attr in ('_bookmarks', '_bookmarkcurrent'):
371 for attr in ('_bookmarks', '_bookmarkcurrent'):
372 if attr in self.__dict__:
372 if attr in self.__dict__:
373 delattr(self, attr)
373 delattr(self, attr)
374
374
375 repo.__class__ = bookmark_repo
375 repo.__class__ = bookmark_repo
376
376
377 def listbookmarks(repo):
377 def listbookmarks(repo):
378 # We may try to list bookmarks on a repo type that does not
378 # We may try to list bookmarks on a repo type that does not
379 # support it (e.g., statichttprepository).
379 # support it (e.g., statichttprepository).
380 if not hasattr(repo, '_bookmarks'):
380 if not hasattr(repo, '_bookmarks'):
381 return {}
381 return {}
382
382
383 d = {}
383 d = {}
384 for k, v in repo._bookmarks.iteritems():
384 for k, v in repo._bookmarks.iteritems():
385 d[k] = hex(v)
385 d[k] = hex(v)
386 return d
386 return d
387
387
388 def pushbookmark(repo, key, old, new):
388 def pushbookmark(repo, key, old, new):
389 w = repo.wlock()
389 w = repo.wlock()
390 try:
390 try:
391 marks = repo._bookmarks
391 marks = repo._bookmarks
392 if hex(marks.get(key, '')) != old:
392 if hex(marks.get(key, '')) != old:
393 return False
393 return False
394 if new == '':
394 if new == '':
395 del marks[key]
395 del marks[key]
396 else:
396 else:
397 if new not in repo:
397 if new not in repo:
398 return False
398 return False
399 marks[key] = repo[new].node()
399 marks[key] = repo[new].node()
400 write(repo)
400 write(repo)
401 return True
401 return True
402 finally:
402 finally:
403 w.release()
403 w.release()
404
404
405 def pull(oldpull, ui, repo, source="default", **opts):
405 def pull(oldpull, ui, repo, source="default", **opts):
406 # translate bookmark args to rev args for actual pull
406 # translate bookmark args to rev args for actual pull
407 if opts.get('bookmark'):
407 if opts.get('bookmark'):
408 # this is an unpleasant hack as pull will do this internally
408 # this is an unpleasant hack as pull will do this internally
409 source, branches = hg.parseurl(ui.expandpath(source),
409 source, branches = hg.parseurl(ui.expandpath(source),
410 opts.get('branch'))
410 opts.get('branch'))
411 other = hg.repository(hg.remoteui(repo, opts), source)
411 other = hg.repository(hg.remoteui(repo, opts), source)
412 rb = other.listkeys('bookmarks')
412 rb = other.listkeys('bookmarks')
413
413
414 for b in opts['bookmark']:
414 for b in opts['bookmark']:
415 if b not in rb:
415 if b not in rb:
416 raise util.Abort(_('remote bookmark %s not found!') % b)
416 raise util.Abort(_('remote bookmark %s not found!') % b)
417 opts.setdefault('rev', []).append(b)
417 opts.setdefault('rev', []).append(b)
418
418
419 result = oldpull(ui, repo, source, **opts)
419 result = oldpull(ui, repo, source, **opts)
420
420
421 # update specified bookmarks
421 # update specified bookmarks
422 if opts.get('bookmark'):
422 if opts.get('bookmark'):
423 for b in opts['bookmark']:
423 for b in opts['bookmark']:
424 # explicit pull overrides local bookmark if any
424 # explicit pull overrides local bookmark if any
425 ui.status(_("importing bookmark %s\n") % b)
425 ui.status(_("importing bookmark %s\n") % b)
426 repo._bookmarks[b] = repo[rb[b]].node()
426 repo._bookmarks[b] = repo[rb[b]].node()
427 write(repo)
427 write(repo)
428
428
429 return result
429 return result
430
430
431 def push(oldpush, ui, repo, dest=None, **opts):
431 def push(oldpush, ui, repo, dest=None, **opts):
432 dopush = True
432 dopush = True
433 if opts.get('bookmark'):
433 if opts.get('bookmark'):
434 dopush = False
434 dopush = False
435 for b in opts['bookmark']:
435 for b in opts['bookmark']:
436 if b in repo._bookmarks:
436 if b in repo._bookmarks:
437 dopush = True
437 dopush = True
438 opts.setdefault('rev', []).append(b)
438 opts.setdefault('rev', []).append(b)
439
439
440 result = 0
440 result = 0
441 if dopush:
441 if dopush:
442 result = oldpush(ui, repo, dest, **opts)
442 result = oldpush(ui, repo, dest, **opts)
443
443
444 if opts.get('bookmark'):
444 if opts.get('bookmark'):
445 # this is an unpleasant hack as push will do this internally
445 # this is an unpleasant hack as push will do this internally
446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
446 dest = ui.expandpath(dest or 'default-push', dest or 'default')
447 dest, branches = hg.parseurl(dest, opts.get('branch'))
447 dest, branches = hg.parseurl(dest, opts.get('branch'))
448 other = hg.repository(hg.remoteui(repo, opts), dest)
448 other = hg.repository(hg.remoteui(repo, opts), dest)
449 rb = other.listkeys('bookmarks')
449 rb = other.listkeys('bookmarks')
450 for b in opts['bookmark']:
450 for b in opts['bookmark']:
451 # explicit push overrides remote bookmark if any
451 # explicit push overrides remote bookmark if any
452 if b in repo._bookmarks:
452 if b in repo._bookmarks:
453 ui.status(_("exporting bookmark %s\n") % b)
453 ui.status(_("exporting bookmark %s\n") % b)
454 new = repo[b].hex()
454 new = repo[b].hex()
455 elif b in rb:
455 elif b in rb:
456 ui.status(_("deleting remote bookmark %s\n") % b)
456 ui.status(_("deleting remote bookmark %s\n") % b)
457 new = '' # delete
457 new = '' # delete
458 else:
458 else:
459 ui.warn(_('bookmark %s does not exist on the local '
459 ui.warn(_('bookmark %s does not exist on the local '
460 'or remote repository!\n') % b)
460 'or remote repository!\n') % b)
461 return 2
461 return 2
462 old = rb.get(b, '')
462 old = rb.get(b, '')
463 r = other.pushkey('bookmarks', b, old, new)
463 r = other.pushkey('bookmarks', b, old, new)
464 if not r:
464 if not r:
465 ui.warn(_('updating bookmark %s failed!\n') % b)
465 ui.warn(_('updating bookmark %s failed!\n') % b)
466 if not result:
466 if not result:
467 result = 2
467 result = 2
468
468
469 return result
469 return result
470
470
471 def diffbookmarks(ui, repo, remote):
471 def diffbookmarks(ui, repo, remote):
472 ui.status(_("searching for changes\n"))
472 ui.status(_("searching for changes\n"))
473
473
474 lmarks = repo.listkeys('bookmarks')
474 lmarks = repo.listkeys('bookmarks')
475 rmarks = remote.listkeys('bookmarks')
475 rmarks = remote.listkeys('bookmarks')
476
476
477 diff = sorted(set(rmarks) - set(lmarks))
477 diff = sorted(set(rmarks) - set(lmarks))
478 for k in diff:
478 for k in diff:
479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
479 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
480
480
481 if len(diff) <= 0:
481 if len(diff) <= 0:
482 ui.status(_("no changes found\n"))
482 ui.status(_("no changes found\n"))
483 return 1
483 return 1
484 return 0
484 return 0
485
485
486 def incoming(oldincoming, ui, repo, source="default", **opts):
486 def incoming(oldincoming, ui, repo, source="default", **opts):
487 if opts.get('bookmarks'):
487 if opts.get('bookmarks'):
488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
488 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
489 other = hg.repository(hg.remoteui(repo, opts), source)
489 other = hg.repository(hg.remoteui(repo, opts), source)
490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
490 ui.status(_('comparing with %s\n') % url.hidepassword(source))
491 return diffbookmarks(ui, repo, other)
491 return diffbookmarks(ui, repo, other)
492 else:
492 else:
493 return oldincoming(ui, repo, source, **opts)
493 return oldincoming(ui, repo, source, **opts)
494
494
495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
495 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
496 if opts.get('bookmarks'):
496 if opts.get('bookmarks'):
497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
497 dest = ui.expandpath(dest or 'default-push', dest or 'default')
498 dest, branches = hg.parseurl(dest, opts.get('branch'))
498 dest, branches = hg.parseurl(dest, opts.get('branch'))
499 other = hg.repository(hg.remoteui(repo, opts), dest)
499 other = hg.repository(hg.remoteui(repo, opts), dest)
500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
500 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
501 return diffbookmarks(ui, other, repo)
501 return diffbookmarks(ui, other, repo)
502 else:
502 else:
503 return oldoutgoing(ui, repo, dest, **opts)
503 return oldoutgoing(ui, repo, dest, **opts)
504
504
505 def uisetup(ui):
505 def uisetup(ui):
506 extensions.wrapfunction(repair, "strip", strip)
506 extensions.wrapfunction(repair, "strip", strip)
507 if ui.configbool('bookmarks', 'track.current'):
507 if ui.configbool('bookmarks', 'track.current'):
508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
508 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
509
509
510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
510 entry = extensions.wrapcommand(commands.table, 'pull', pull)
511 entry[1].append(('B', 'bookmark', [],
511 entry[1].append(('B', 'bookmark', [],
512 _("bookmark to import"),
512 _("bookmark to import"),
513 _('BOOKMARK')))
513 _('BOOKMARK')))
514 entry = extensions.wrapcommand(commands.table, 'push', push)
514 entry = extensions.wrapcommand(commands.table, 'push', push)
515 entry[1].append(('B', 'bookmark', [],
515 entry[1].append(('B', 'bookmark', [],
516 _("bookmark to export"),
516 _("bookmark to export"),
517 _('BOOKMARK')))
517 _('BOOKMARK')))
518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
518 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
519 entry[1].append(('B', 'bookmarks', False,
519 entry[1].append(('B', 'bookmarks', False,
520 _("compare bookmark")))
520 _("compare bookmark")))
521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
521 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
522 entry[1].append(('B', 'bookmarks', False,
522 entry[1].append(('B', 'bookmarks', False,
523 _("compare bookmark")))
523 _("compare bookmark")))
524
524
525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
525 pushkey.register('bookmarks', pushbookmark, listbookmarks)
526
526
527 def updatecurbookmark(orig, ui, repo, *args, **opts):
527 def updatecurbookmark(orig, ui, repo, *args, **opts):
528 '''Set the current bookmark
528 '''Set the current bookmark
529
529
530 If the user updates to a bookmark we update the .hg/bookmarks.current
530 If the user updates to a bookmark we update the .hg/bookmarks.current
531 file.
531 file.
532 '''
532 '''
533 res = orig(ui, repo, *args, **opts)
533 res = orig(ui, repo, *args, **opts)
534 rev = opts['rev']
534 rev = opts['rev']
535 if not rev and len(args) > 0:
535 if not rev and len(args) > 0:
536 rev = args[0]
536 rev = args[0]
537 setcurrent(repo, rev)
537 setcurrent(repo, rev)
538 return res
538 return res
539
539
540 def bmrevset(repo, subset, x):
540 def bmrevset(repo, subset, x):
541 """``bookmark([name])``
541 """``bookmark([name])``
542 The named bookmark or all bookmarks.
542 The named bookmark or all bookmarks.
543 """
543 """
544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
545 if args:
545 if args:
546 bm = revset.getstring(args[0],
546 bm = revset.getstring(args[0],
547 _('the argument to bookmark must be a string'))
547 _('the argument to bookmark must be a string'))
548 bmrev = listbookmarks(repo).get(bm, None)
548 bmrev = listbookmarks(repo).get(bm, None)
549 if bmrev:
549 if bmrev:
550 bmrev = repo.changelog.rev(bin(bmrev))
550 bmrev = repo.changelog.rev(bin(bmrev))
551 return [r for r in subset if r == bmrev]
551 return [r for r in subset if r == bmrev]
552 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
552 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
553 return [r for r in subset if r in bms]
553 return [r for r in subset if r in bms]
554
554
555 def extsetup(ui):
555 def extsetup(ui):
556 revset.symbols['bookmark'] = bmrevset
556 revset.symbols['bookmark'] = bmrevset
557
557
558 cmdtable = {
558 cmdtable = {
559 "bookmarks":
559 "bookmarks":
560 (bookmark,
560 (bookmark,
561 [('f', 'force', False, _('force')),
561 [('f', 'force', False, _('force')),
562 ('r', 'rev', '', _('revision'), _('REV')),
562 ('r', 'rev', '', _('revision'), _('REV')),
563 ('d', 'delete', False, _('delete a given bookmark')),
563 ('d', 'delete', False, _('delete a given bookmark')),
564 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
564 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
565 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
565 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
566 }
566 }
567
567
568 colortable = {'bookmarks.current': 'green'}
568 colortable = {'bookmarks.current': 'green'}
569
570 # tell hggettext to extract docstrings from these functions:
571 i18nfunctions = [bmrevset]
@@ -1,627 +1,630 b''
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to transplant changesets from another branch
8 '''command to transplant changesets from another branch
9
9
10 This extension allows you to transplant patches from another branch.
10 This extension allows you to transplant patches from another branch.
11
11
12 Transplanted patches are recorded in .hg/transplant/transplants, as a
12 Transplanted patches are recorded in .hg/transplant/transplants, as a
13 map from a changeset hash to its hash in the source repository.
13 map from a changeset hash to its hash in the source repository.
14 '''
14 '''
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 import os, tempfile
17 import os, tempfile
18 from mercurial import bundlerepo, cmdutil, hg, merge, match
18 from mercurial import bundlerepo, cmdutil, hg, merge, match
19 from mercurial import patch, revlog, util, error
19 from mercurial import patch, revlog, util, error
20 from mercurial import revset
20 from mercurial import revset
21
21
22 class transplantentry(object):
22 class transplantentry(object):
23 def __init__(self, lnode, rnode):
23 def __init__(self, lnode, rnode):
24 self.lnode = lnode
24 self.lnode = lnode
25 self.rnode = rnode
25 self.rnode = rnode
26
26
27 class transplants(object):
27 class transplants(object):
28 def __init__(self, path=None, transplantfile=None, opener=None):
28 def __init__(self, path=None, transplantfile=None, opener=None):
29 self.path = path
29 self.path = path
30 self.transplantfile = transplantfile
30 self.transplantfile = transplantfile
31 self.opener = opener
31 self.opener = opener
32
32
33 if not opener:
33 if not opener:
34 self.opener = util.opener(self.path)
34 self.opener = util.opener(self.path)
35 self.transplants = {}
35 self.transplants = {}
36 self.dirty = False
36 self.dirty = False
37 self.read()
37 self.read()
38
38
39 def read(self):
39 def read(self):
40 abspath = os.path.join(self.path, self.transplantfile)
40 abspath = os.path.join(self.path, self.transplantfile)
41 if self.transplantfile and os.path.exists(abspath):
41 if self.transplantfile and os.path.exists(abspath):
42 for line in self.opener(self.transplantfile).read().splitlines():
42 for line in self.opener(self.transplantfile).read().splitlines():
43 lnode, rnode = map(revlog.bin, line.split(':'))
43 lnode, rnode = map(revlog.bin, line.split(':'))
44 list = self.transplants.setdefault(rnode, [])
44 list = self.transplants.setdefault(rnode, [])
45 list.append(transplantentry(lnode, rnode))
45 list.append(transplantentry(lnode, rnode))
46
46
47 def write(self):
47 def write(self):
48 if self.dirty and self.transplantfile:
48 if self.dirty and self.transplantfile:
49 if not os.path.isdir(self.path):
49 if not os.path.isdir(self.path):
50 os.mkdir(self.path)
50 os.mkdir(self.path)
51 fp = self.opener(self.transplantfile, 'w')
51 fp = self.opener(self.transplantfile, 'w')
52 for list in self.transplants.itervalues():
52 for list in self.transplants.itervalues():
53 for t in list:
53 for t in list:
54 l, r = map(revlog.hex, (t.lnode, t.rnode))
54 l, r = map(revlog.hex, (t.lnode, t.rnode))
55 fp.write(l + ':' + r + '\n')
55 fp.write(l + ':' + r + '\n')
56 fp.close()
56 fp.close()
57 self.dirty = False
57 self.dirty = False
58
58
59 def get(self, rnode):
59 def get(self, rnode):
60 return self.transplants.get(rnode) or []
60 return self.transplants.get(rnode) or []
61
61
62 def set(self, lnode, rnode):
62 def set(self, lnode, rnode):
63 list = self.transplants.setdefault(rnode, [])
63 list = self.transplants.setdefault(rnode, [])
64 list.append(transplantentry(lnode, rnode))
64 list.append(transplantentry(lnode, rnode))
65 self.dirty = True
65 self.dirty = True
66
66
67 def remove(self, transplant):
67 def remove(self, transplant):
68 list = self.transplants.get(transplant.rnode)
68 list = self.transplants.get(transplant.rnode)
69 if list:
69 if list:
70 del list[list.index(transplant)]
70 del list[list.index(transplant)]
71 self.dirty = True
71 self.dirty = True
72
72
73 class transplanter(object):
73 class transplanter(object):
74 def __init__(self, ui, repo):
74 def __init__(self, ui, repo):
75 self.ui = ui
75 self.ui = ui
76 self.path = repo.join('transplant')
76 self.path = repo.join('transplant')
77 self.opener = util.opener(self.path)
77 self.opener = util.opener(self.path)
78 self.transplants = transplants(self.path, 'transplants',
78 self.transplants = transplants(self.path, 'transplants',
79 opener=self.opener)
79 opener=self.opener)
80
80
81 def applied(self, repo, node, parent):
81 def applied(self, repo, node, parent):
82 '''returns True if a node is already an ancestor of parent
82 '''returns True if a node is already an ancestor of parent
83 or has already been transplanted'''
83 or has already been transplanted'''
84 if hasnode(repo, node):
84 if hasnode(repo, node):
85 if node in repo.changelog.reachable(parent, stop=node):
85 if node in repo.changelog.reachable(parent, stop=node):
86 return True
86 return True
87 for t in self.transplants.get(node):
87 for t in self.transplants.get(node):
88 # it might have been stripped
88 # it might have been stripped
89 if not hasnode(repo, t.lnode):
89 if not hasnode(repo, t.lnode):
90 self.transplants.remove(t)
90 self.transplants.remove(t)
91 return False
91 return False
92 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
92 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
93 return True
93 return True
94 return False
94 return False
95
95
96 def apply(self, repo, source, revmap, merges, opts={}):
96 def apply(self, repo, source, revmap, merges, opts={}):
97 '''apply the revisions in revmap one by one in revision order'''
97 '''apply the revisions in revmap one by one in revision order'''
98 revs = sorted(revmap)
98 revs = sorted(revmap)
99 p1, p2 = repo.dirstate.parents()
99 p1, p2 = repo.dirstate.parents()
100 pulls = []
100 pulls = []
101 diffopts = patch.diffopts(self.ui, opts)
101 diffopts = patch.diffopts(self.ui, opts)
102 diffopts.git = True
102 diffopts.git = True
103
103
104 lock = wlock = None
104 lock = wlock = None
105 try:
105 try:
106 wlock = repo.wlock()
106 wlock = repo.wlock()
107 lock = repo.lock()
107 lock = repo.lock()
108 for rev in revs:
108 for rev in revs:
109 node = revmap[rev]
109 node = revmap[rev]
110 revstr = '%s:%s' % (rev, revlog.short(node))
110 revstr = '%s:%s' % (rev, revlog.short(node))
111
111
112 if self.applied(repo, node, p1):
112 if self.applied(repo, node, p1):
113 self.ui.warn(_('skipping already applied revision %s\n') %
113 self.ui.warn(_('skipping already applied revision %s\n') %
114 revstr)
114 revstr)
115 continue
115 continue
116
116
117 parents = source.changelog.parents(node)
117 parents = source.changelog.parents(node)
118 if not opts.get('filter'):
118 if not opts.get('filter'):
119 # If the changeset parent is the same as the
119 # If the changeset parent is the same as the
120 # wdir's parent, just pull it.
120 # wdir's parent, just pull it.
121 if parents[0] == p1:
121 if parents[0] == p1:
122 pulls.append(node)
122 pulls.append(node)
123 p1 = node
123 p1 = node
124 continue
124 continue
125 if pulls:
125 if pulls:
126 if source != repo:
126 if source != repo:
127 repo.pull(source, heads=pulls)
127 repo.pull(source, heads=pulls)
128 merge.update(repo, pulls[-1], False, False, None)
128 merge.update(repo, pulls[-1], False, False, None)
129 p1, p2 = repo.dirstate.parents()
129 p1, p2 = repo.dirstate.parents()
130 pulls = []
130 pulls = []
131
131
132 domerge = False
132 domerge = False
133 if node in merges:
133 if node in merges:
134 # pulling all the merge revs at once would mean we
134 # pulling all the merge revs at once would mean we
135 # couldn't transplant after the latest even if
135 # couldn't transplant after the latest even if
136 # transplants before them fail.
136 # transplants before them fail.
137 domerge = True
137 domerge = True
138 if not hasnode(repo, node):
138 if not hasnode(repo, node):
139 repo.pull(source, heads=[node])
139 repo.pull(source, heads=[node])
140
140
141 if parents[1] != revlog.nullid:
141 if parents[1] != revlog.nullid:
142 self.ui.note(_('skipping merge changeset %s:%s\n')
142 self.ui.note(_('skipping merge changeset %s:%s\n')
143 % (rev, revlog.short(node)))
143 % (rev, revlog.short(node)))
144 patchfile = None
144 patchfile = None
145 else:
145 else:
146 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
146 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
147 fp = os.fdopen(fd, 'w')
147 fp = os.fdopen(fd, 'w')
148 gen = patch.diff(source, parents[0], node, opts=diffopts)
148 gen = patch.diff(source, parents[0], node, opts=diffopts)
149 for chunk in gen:
149 for chunk in gen:
150 fp.write(chunk)
150 fp.write(chunk)
151 fp.close()
151 fp.close()
152
152
153 del revmap[rev]
153 del revmap[rev]
154 if patchfile or domerge:
154 if patchfile or domerge:
155 try:
155 try:
156 n = self.applyone(repo, node,
156 n = self.applyone(repo, node,
157 source.changelog.read(node),
157 source.changelog.read(node),
158 patchfile, merge=domerge,
158 patchfile, merge=domerge,
159 log=opts.get('log'),
159 log=opts.get('log'),
160 filter=opts.get('filter'))
160 filter=opts.get('filter'))
161 if n and domerge:
161 if n and domerge:
162 self.ui.status(_('%s merged at %s\n') % (revstr,
162 self.ui.status(_('%s merged at %s\n') % (revstr,
163 revlog.short(n)))
163 revlog.short(n)))
164 elif n:
164 elif n:
165 self.ui.status(_('%s transplanted to %s\n')
165 self.ui.status(_('%s transplanted to %s\n')
166 % (revlog.short(node),
166 % (revlog.short(node),
167 revlog.short(n)))
167 revlog.short(n)))
168 finally:
168 finally:
169 if patchfile:
169 if patchfile:
170 os.unlink(patchfile)
170 os.unlink(patchfile)
171 if pulls:
171 if pulls:
172 repo.pull(source, heads=pulls)
172 repo.pull(source, heads=pulls)
173 merge.update(repo, pulls[-1], False, False, None)
173 merge.update(repo, pulls[-1], False, False, None)
174 finally:
174 finally:
175 self.saveseries(revmap, merges)
175 self.saveseries(revmap, merges)
176 self.transplants.write()
176 self.transplants.write()
177 lock.release()
177 lock.release()
178 wlock.release()
178 wlock.release()
179
179
180 def filter(self, filter, changelog, patchfile):
180 def filter(self, filter, changelog, patchfile):
181 '''arbitrarily rewrite changeset before applying it'''
181 '''arbitrarily rewrite changeset before applying it'''
182
182
183 self.ui.status(_('filtering %s\n') % patchfile)
183 self.ui.status(_('filtering %s\n') % patchfile)
184 user, date, msg = (changelog[1], changelog[2], changelog[4])
184 user, date, msg = (changelog[1], changelog[2], changelog[4])
185
185
186 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
186 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
187 fp = os.fdopen(fd, 'w')
187 fp = os.fdopen(fd, 'w')
188 fp.write("# HG changeset patch\n")
188 fp.write("# HG changeset patch\n")
189 fp.write("# User %s\n" % user)
189 fp.write("# User %s\n" % user)
190 fp.write("# Date %d %d\n" % date)
190 fp.write("# Date %d %d\n" % date)
191 fp.write(msg + '\n')
191 fp.write(msg + '\n')
192 fp.close()
192 fp.close()
193
193
194 try:
194 try:
195 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
195 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
196 util.shellquote(patchfile)),
196 util.shellquote(patchfile)),
197 environ={'HGUSER': changelog[1]},
197 environ={'HGUSER': changelog[1]},
198 onerr=util.Abort, errprefix=_('filter failed'))
198 onerr=util.Abort, errprefix=_('filter failed'))
199 user, date, msg = self.parselog(file(headerfile))[1:4]
199 user, date, msg = self.parselog(file(headerfile))[1:4]
200 finally:
200 finally:
201 os.unlink(headerfile)
201 os.unlink(headerfile)
202
202
203 return (user, date, msg)
203 return (user, date, msg)
204
204
205 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
205 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
206 filter=None):
206 filter=None):
207 '''apply the patch in patchfile to the repository as a transplant'''
207 '''apply the patch in patchfile to the repository as a transplant'''
208 (manifest, user, (time, timezone), files, message) = cl[:5]
208 (manifest, user, (time, timezone), files, message) = cl[:5]
209 date = "%d %d" % (time, timezone)
209 date = "%d %d" % (time, timezone)
210 extra = {'transplant_source': node}
210 extra = {'transplant_source': node}
211 if filter:
211 if filter:
212 (user, date, message) = self.filter(filter, cl, patchfile)
212 (user, date, message) = self.filter(filter, cl, patchfile)
213
213
214 if log:
214 if log:
215 # we don't translate messages inserted into commits
215 # we don't translate messages inserted into commits
216 message += '\n(transplanted from %s)' % revlog.hex(node)
216 message += '\n(transplanted from %s)' % revlog.hex(node)
217
217
218 self.ui.status(_('applying %s\n') % revlog.short(node))
218 self.ui.status(_('applying %s\n') % revlog.short(node))
219 self.ui.note('%s %s\n%s\n' % (user, date, message))
219 self.ui.note('%s %s\n%s\n' % (user, date, message))
220
220
221 if not patchfile and not merge:
221 if not patchfile and not merge:
222 raise util.Abort(_('can only omit patchfile if merging'))
222 raise util.Abort(_('can only omit patchfile if merging'))
223 if patchfile:
223 if patchfile:
224 try:
224 try:
225 files = {}
225 files = {}
226 try:
226 try:
227 patch.patch(patchfile, self.ui, cwd=repo.root,
227 patch.patch(patchfile, self.ui, cwd=repo.root,
228 files=files, eolmode=None)
228 files=files, eolmode=None)
229 if not files:
229 if not files:
230 self.ui.warn(_('%s: empty changeset')
230 self.ui.warn(_('%s: empty changeset')
231 % revlog.hex(node))
231 % revlog.hex(node))
232 return None
232 return None
233 finally:
233 finally:
234 files = cmdutil.updatedir(self.ui, repo, files)
234 files = cmdutil.updatedir(self.ui, repo, files)
235 except Exception, inst:
235 except Exception, inst:
236 seriespath = os.path.join(self.path, 'series')
236 seriespath = os.path.join(self.path, 'series')
237 if os.path.exists(seriespath):
237 if os.path.exists(seriespath):
238 os.unlink(seriespath)
238 os.unlink(seriespath)
239 p1 = repo.dirstate.parents()[0]
239 p1 = repo.dirstate.parents()[0]
240 p2 = node
240 p2 = node
241 self.log(user, date, message, p1, p2, merge=merge)
241 self.log(user, date, message, p1, p2, merge=merge)
242 self.ui.write(str(inst) + '\n')
242 self.ui.write(str(inst) + '\n')
243 raise util.Abort(_('fix up the merge and run '
243 raise util.Abort(_('fix up the merge and run '
244 'hg transplant --continue'))
244 'hg transplant --continue'))
245 else:
245 else:
246 files = None
246 files = None
247 if merge:
247 if merge:
248 p1, p2 = repo.dirstate.parents()
248 p1, p2 = repo.dirstate.parents()
249 repo.dirstate.setparents(p1, node)
249 repo.dirstate.setparents(p1, node)
250 m = match.always(repo.root, '')
250 m = match.always(repo.root, '')
251 else:
251 else:
252 m = match.exact(repo.root, '', files)
252 m = match.exact(repo.root, '', files)
253
253
254 n = repo.commit(message, user, date, extra=extra, match=m)
254 n = repo.commit(message, user, date, extra=extra, match=m)
255 if not n:
255 if not n:
256 # Crash here to prevent an unclear crash later, in
256 # Crash here to prevent an unclear crash later, in
257 # transplants.write(). This can happen if patch.patch()
257 # transplants.write(). This can happen if patch.patch()
258 # does nothing but claims success or if repo.status() fails
258 # does nothing but claims success or if repo.status() fails
259 # to report changes done by patch.patch(). These both
259 # to report changes done by patch.patch(). These both
260 # appear to be bugs in other parts of Mercurial, but dying
260 # appear to be bugs in other parts of Mercurial, but dying
261 # here, as soon as we can detect the problem, is preferable
261 # here, as soon as we can detect the problem, is preferable
262 # to silently dropping changesets on the floor.
262 # to silently dropping changesets on the floor.
263 raise RuntimeError('nothing committed after transplant')
263 raise RuntimeError('nothing committed after transplant')
264 if not merge:
264 if not merge:
265 self.transplants.set(n, node)
265 self.transplants.set(n, node)
266
266
267 return n
267 return n
268
268
269 def resume(self, repo, source, opts=None):
269 def resume(self, repo, source, opts=None):
270 '''recover last transaction and apply remaining changesets'''
270 '''recover last transaction and apply remaining changesets'''
271 if os.path.exists(os.path.join(self.path, 'journal')):
271 if os.path.exists(os.path.join(self.path, 'journal')):
272 n, node = self.recover(repo)
272 n, node = self.recover(repo)
273 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
273 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
274 revlog.short(n)))
274 revlog.short(n)))
275 seriespath = os.path.join(self.path, 'series')
275 seriespath = os.path.join(self.path, 'series')
276 if not os.path.exists(seriespath):
276 if not os.path.exists(seriespath):
277 self.transplants.write()
277 self.transplants.write()
278 return
278 return
279 nodes, merges = self.readseries()
279 nodes, merges = self.readseries()
280 revmap = {}
280 revmap = {}
281 for n in nodes:
281 for n in nodes:
282 revmap[source.changelog.rev(n)] = n
282 revmap[source.changelog.rev(n)] = n
283 os.unlink(seriespath)
283 os.unlink(seriespath)
284
284
285 self.apply(repo, source, revmap, merges, opts)
285 self.apply(repo, source, revmap, merges, opts)
286
286
287 def recover(self, repo):
287 def recover(self, repo):
288 '''commit working directory using journal metadata'''
288 '''commit working directory using journal metadata'''
289 node, user, date, message, parents = self.readlog()
289 node, user, date, message, parents = self.readlog()
290 merge = len(parents) == 2
290 merge = len(parents) == 2
291
291
292 if not user or not date or not message or not parents[0]:
292 if not user or not date or not message or not parents[0]:
293 raise util.Abort(_('transplant log file is corrupt'))
293 raise util.Abort(_('transplant log file is corrupt'))
294
294
295 extra = {'transplant_source': node}
295 extra = {'transplant_source': node}
296 wlock = repo.wlock()
296 wlock = repo.wlock()
297 try:
297 try:
298 p1, p2 = repo.dirstate.parents()
298 p1, p2 = repo.dirstate.parents()
299 if p1 != parents[0]:
299 if p1 != parents[0]:
300 raise util.Abort(
300 raise util.Abort(
301 _('working dir not at transplant parent %s') %
301 _('working dir not at transplant parent %s') %
302 revlog.hex(parents[0]))
302 revlog.hex(parents[0]))
303 if merge:
303 if merge:
304 repo.dirstate.setparents(p1, parents[1])
304 repo.dirstate.setparents(p1, parents[1])
305 n = repo.commit(message, user, date, extra=extra)
305 n = repo.commit(message, user, date, extra=extra)
306 if not n:
306 if not n:
307 raise util.Abort(_('commit failed'))
307 raise util.Abort(_('commit failed'))
308 if not merge:
308 if not merge:
309 self.transplants.set(n, node)
309 self.transplants.set(n, node)
310 self.unlog()
310 self.unlog()
311
311
312 return n, node
312 return n, node
313 finally:
313 finally:
314 wlock.release()
314 wlock.release()
315
315
316 def readseries(self):
316 def readseries(self):
317 nodes = []
317 nodes = []
318 merges = []
318 merges = []
319 cur = nodes
319 cur = nodes
320 for line in self.opener('series').read().splitlines():
320 for line in self.opener('series').read().splitlines():
321 if line.startswith('# Merges'):
321 if line.startswith('# Merges'):
322 cur = merges
322 cur = merges
323 continue
323 continue
324 cur.append(revlog.bin(line))
324 cur.append(revlog.bin(line))
325
325
326 return (nodes, merges)
326 return (nodes, merges)
327
327
328 def saveseries(self, revmap, merges):
328 def saveseries(self, revmap, merges):
329 if not revmap:
329 if not revmap:
330 return
330 return
331
331
332 if not os.path.isdir(self.path):
332 if not os.path.isdir(self.path):
333 os.mkdir(self.path)
333 os.mkdir(self.path)
334 series = self.opener('series', 'w')
334 series = self.opener('series', 'w')
335 for rev in sorted(revmap):
335 for rev in sorted(revmap):
336 series.write(revlog.hex(revmap[rev]) + '\n')
336 series.write(revlog.hex(revmap[rev]) + '\n')
337 if merges:
337 if merges:
338 series.write('# Merges\n')
338 series.write('# Merges\n')
339 for m in merges:
339 for m in merges:
340 series.write(revlog.hex(m) + '\n')
340 series.write(revlog.hex(m) + '\n')
341 series.close()
341 series.close()
342
342
343 def parselog(self, fp):
343 def parselog(self, fp):
344 parents = []
344 parents = []
345 message = []
345 message = []
346 node = revlog.nullid
346 node = revlog.nullid
347 inmsg = False
347 inmsg = False
348 for line in fp.read().splitlines():
348 for line in fp.read().splitlines():
349 if inmsg:
349 if inmsg:
350 message.append(line)
350 message.append(line)
351 elif line.startswith('# User '):
351 elif line.startswith('# User '):
352 user = line[7:]
352 user = line[7:]
353 elif line.startswith('# Date '):
353 elif line.startswith('# Date '):
354 date = line[7:]
354 date = line[7:]
355 elif line.startswith('# Node ID '):
355 elif line.startswith('# Node ID '):
356 node = revlog.bin(line[10:])
356 node = revlog.bin(line[10:])
357 elif line.startswith('# Parent '):
357 elif line.startswith('# Parent '):
358 parents.append(revlog.bin(line[9:]))
358 parents.append(revlog.bin(line[9:]))
359 elif not line.startswith('# '):
359 elif not line.startswith('# '):
360 inmsg = True
360 inmsg = True
361 message.append(line)
361 message.append(line)
362 return (node, user, date, '\n'.join(message), parents)
362 return (node, user, date, '\n'.join(message), parents)
363
363
364 def log(self, user, date, message, p1, p2, merge=False):
364 def log(self, user, date, message, p1, p2, merge=False):
365 '''journal changelog metadata for later recover'''
365 '''journal changelog metadata for later recover'''
366
366
367 if not os.path.isdir(self.path):
367 if not os.path.isdir(self.path):
368 os.mkdir(self.path)
368 os.mkdir(self.path)
369 fp = self.opener('journal', 'w')
369 fp = self.opener('journal', 'w')
370 fp.write('# User %s\n' % user)
370 fp.write('# User %s\n' % user)
371 fp.write('# Date %s\n' % date)
371 fp.write('# Date %s\n' % date)
372 fp.write('# Node ID %s\n' % revlog.hex(p2))
372 fp.write('# Node ID %s\n' % revlog.hex(p2))
373 fp.write('# Parent ' + revlog.hex(p1) + '\n')
373 fp.write('# Parent ' + revlog.hex(p1) + '\n')
374 if merge:
374 if merge:
375 fp.write('# Parent ' + revlog.hex(p2) + '\n')
375 fp.write('# Parent ' + revlog.hex(p2) + '\n')
376 fp.write(message.rstrip() + '\n')
376 fp.write(message.rstrip() + '\n')
377 fp.close()
377 fp.close()
378
378
379 def readlog(self):
379 def readlog(self):
380 return self.parselog(self.opener('journal'))
380 return self.parselog(self.opener('journal'))
381
381
382 def unlog(self):
382 def unlog(self):
383 '''remove changelog journal'''
383 '''remove changelog journal'''
384 absdst = os.path.join(self.path, 'journal')
384 absdst = os.path.join(self.path, 'journal')
385 if os.path.exists(absdst):
385 if os.path.exists(absdst):
386 os.unlink(absdst)
386 os.unlink(absdst)
387
387
388 def transplantfilter(self, repo, source, root):
388 def transplantfilter(self, repo, source, root):
389 def matchfn(node):
389 def matchfn(node):
390 if self.applied(repo, node, root):
390 if self.applied(repo, node, root):
391 return False
391 return False
392 if source.changelog.parents(node)[1] != revlog.nullid:
392 if source.changelog.parents(node)[1] != revlog.nullid:
393 return False
393 return False
394 extra = source.changelog.read(node)[5]
394 extra = source.changelog.read(node)[5]
395 cnode = extra.get('transplant_source')
395 cnode = extra.get('transplant_source')
396 if cnode and self.applied(repo, cnode, root):
396 if cnode and self.applied(repo, cnode, root):
397 return False
397 return False
398 return True
398 return True
399
399
400 return matchfn
400 return matchfn
401
401
402 def hasnode(repo, node):
402 def hasnode(repo, node):
403 try:
403 try:
404 return repo.changelog.rev(node) != None
404 return repo.changelog.rev(node) != None
405 except error.RevlogError:
405 except error.RevlogError:
406 return False
406 return False
407
407
408 def browserevs(ui, repo, nodes, opts):
408 def browserevs(ui, repo, nodes, opts):
409 '''interactively transplant changesets'''
409 '''interactively transplant changesets'''
410 def browsehelp(ui):
410 def browsehelp(ui):
411 ui.write(_('y: transplant this changeset\n'
411 ui.write(_('y: transplant this changeset\n'
412 'n: skip this changeset\n'
412 'n: skip this changeset\n'
413 'm: merge at this changeset\n'
413 'm: merge at this changeset\n'
414 'p: show patch\n'
414 'p: show patch\n'
415 'c: commit selected changesets\n'
415 'c: commit selected changesets\n'
416 'q: cancel transplant\n'
416 'q: cancel transplant\n'
417 '?: show this help\n'))
417 '?: show this help\n'))
418
418
419 displayer = cmdutil.show_changeset(ui, repo, opts)
419 displayer = cmdutil.show_changeset(ui, repo, opts)
420 transplants = []
420 transplants = []
421 merges = []
421 merges = []
422 for node in nodes:
422 for node in nodes:
423 displayer.show(repo[node])
423 displayer.show(repo[node])
424 action = None
424 action = None
425 while not action:
425 while not action:
426 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
426 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
427 if action == '?':
427 if action == '?':
428 browsehelp(ui)
428 browsehelp(ui)
429 action = None
429 action = None
430 elif action == 'p':
430 elif action == 'p':
431 parent = repo.changelog.parents(node)[0]
431 parent = repo.changelog.parents(node)[0]
432 for chunk in patch.diff(repo, parent, node):
432 for chunk in patch.diff(repo, parent, node):
433 ui.write(chunk)
433 ui.write(chunk)
434 action = None
434 action = None
435 elif action not in ('y', 'n', 'm', 'c', 'q'):
435 elif action not in ('y', 'n', 'm', 'c', 'q'):
436 ui.write(_('no such option\n'))
436 ui.write(_('no such option\n'))
437 action = None
437 action = None
438 if action == 'y':
438 if action == 'y':
439 transplants.append(node)
439 transplants.append(node)
440 elif action == 'm':
440 elif action == 'm':
441 merges.append(node)
441 merges.append(node)
442 elif action == 'c':
442 elif action == 'c':
443 break
443 break
444 elif action == 'q':
444 elif action == 'q':
445 transplants = ()
445 transplants = ()
446 merges = ()
446 merges = ()
447 break
447 break
448 displayer.close()
448 displayer.close()
449 return (transplants, merges)
449 return (transplants, merges)
450
450
451 def transplant(ui, repo, *revs, **opts):
451 def transplant(ui, repo, *revs, **opts):
452 '''transplant changesets from another branch
452 '''transplant changesets from another branch
453
453
454 Selected changesets will be applied on top of the current working
454 Selected changesets will be applied on top of the current working
455 directory with the log of the original changeset. If --log is
455 directory with the log of the original changeset. If --log is
456 specified, log messages will have a comment appended of the form::
456 specified, log messages will have a comment appended of the form::
457
457
458 (transplanted from CHANGESETHASH)
458 (transplanted from CHANGESETHASH)
459
459
460 You can rewrite the changelog message with the --filter option.
460 You can rewrite the changelog message with the --filter option.
461 Its argument will be invoked with the current changelog message as
461 Its argument will be invoked with the current changelog message as
462 $1 and the patch as $2.
462 $1 and the patch as $2.
463
463
464 If --source/-s is specified, selects changesets from the named
464 If --source/-s is specified, selects changesets from the named
465 repository. If --branch/-b is specified, selects changesets from
465 repository. If --branch/-b is specified, selects changesets from
466 the branch holding the named revision, up to that revision. If
466 the branch holding the named revision, up to that revision. If
467 --all/-a is specified, all changesets on the branch will be
467 --all/-a is specified, all changesets on the branch will be
468 transplanted, otherwise you will be prompted to select the
468 transplanted, otherwise you will be prompted to select the
469 changesets you want.
469 changesets you want.
470
470
471 :hg:`transplant --branch REVISION --all` will rebase the selected
471 :hg:`transplant --branch REVISION --all` will rebase the selected
472 branch (up to the named revision) onto your current working
472 branch (up to the named revision) onto your current working
473 directory.
473 directory.
474
474
475 You can optionally mark selected transplanted changesets as merge
475 You can optionally mark selected transplanted changesets as merge
476 changesets. You will not be prompted to transplant any ancestors
476 changesets. You will not be prompted to transplant any ancestors
477 of a merged transplant, and you can merge descendants of them
477 of a merged transplant, and you can merge descendants of them
478 normally instead of transplanting them.
478 normally instead of transplanting them.
479
479
480 If no merges or revisions are provided, :hg:`transplant` will
480 If no merges or revisions are provided, :hg:`transplant` will
481 start an interactive changeset browser.
481 start an interactive changeset browser.
482
482
483 If a changeset application fails, you can fix the merge by hand
483 If a changeset application fails, you can fix the merge by hand
484 and then resume where you left off by calling :hg:`transplant
484 and then resume where you left off by calling :hg:`transplant
485 --continue/-c`.
485 --continue/-c`.
486 '''
486 '''
487 def incwalk(repo, incoming, branches, match=util.always):
487 def incwalk(repo, incoming, branches, match=util.always):
488 if not branches:
488 if not branches:
489 branches = None
489 branches = None
490 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
490 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
491 if match(node):
491 if match(node):
492 yield node
492 yield node
493
493
494 def transplantwalk(repo, root, branches, match=util.always):
494 def transplantwalk(repo, root, branches, match=util.always):
495 if not branches:
495 if not branches:
496 branches = repo.heads()
496 branches = repo.heads()
497 ancestors = []
497 ancestors = []
498 for branch in branches:
498 for branch in branches:
499 ancestors.append(repo.changelog.ancestor(root, branch))
499 ancestors.append(repo.changelog.ancestor(root, branch))
500 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
500 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
501 if match(node):
501 if match(node):
502 yield node
502 yield node
503
503
504 def checkopts(opts, revs):
504 def checkopts(opts, revs):
505 if opts.get('continue'):
505 if opts.get('continue'):
506 if opts.get('branch') or opts.get('all') or opts.get('merge'):
506 if opts.get('branch') or opts.get('all') or opts.get('merge'):
507 raise util.Abort(_('--continue is incompatible with '
507 raise util.Abort(_('--continue is incompatible with '
508 'branch, all or merge'))
508 'branch, all or merge'))
509 return
509 return
510 if not (opts.get('source') or revs or
510 if not (opts.get('source') or revs or
511 opts.get('merge') or opts.get('branch')):
511 opts.get('merge') or opts.get('branch')):
512 raise util.Abort(_('no source URL, branch tag or revision '
512 raise util.Abort(_('no source URL, branch tag or revision '
513 'list provided'))
513 'list provided'))
514 if opts.get('all'):
514 if opts.get('all'):
515 if not opts.get('branch'):
515 if not opts.get('branch'):
516 raise util.Abort(_('--all requires a branch revision'))
516 raise util.Abort(_('--all requires a branch revision'))
517 if revs:
517 if revs:
518 raise util.Abort(_('--all is incompatible with a '
518 raise util.Abort(_('--all is incompatible with a '
519 'revision list'))
519 'revision list'))
520
520
521 checkopts(opts, revs)
521 checkopts(opts, revs)
522
522
523 if not opts.get('log'):
523 if not opts.get('log'):
524 opts['log'] = ui.config('transplant', 'log')
524 opts['log'] = ui.config('transplant', 'log')
525 if not opts.get('filter'):
525 if not opts.get('filter'):
526 opts['filter'] = ui.config('transplant', 'filter')
526 opts['filter'] = ui.config('transplant', 'filter')
527
527
528 tp = transplanter(ui, repo)
528 tp = transplanter(ui, repo)
529
529
530 p1, p2 = repo.dirstate.parents()
530 p1, p2 = repo.dirstate.parents()
531 if len(repo) > 0 and p1 == revlog.nullid:
531 if len(repo) > 0 and p1 == revlog.nullid:
532 raise util.Abort(_('no revision checked out'))
532 raise util.Abort(_('no revision checked out'))
533 if not opts.get('continue'):
533 if not opts.get('continue'):
534 if p2 != revlog.nullid:
534 if p2 != revlog.nullid:
535 raise util.Abort(_('outstanding uncommitted merges'))
535 raise util.Abort(_('outstanding uncommitted merges'))
536 m, a, r, d = repo.status()[:4]
536 m, a, r, d = repo.status()[:4]
537 if m or a or r or d:
537 if m or a or r or d:
538 raise util.Abort(_('outstanding local changes'))
538 raise util.Abort(_('outstanding local changes'))
539
539
540 bundle = None
540 bundle = None
541 source = opts.get('source')
541 source = opts.get('source')
542 if source:
542 if source:
543 sourcerepo = ui.expandpath(source)
543 sourcerepo = ui.expandpath(source)
544 source = hg.repository(ui, sourcerepo)
544 source = hg.repository(ui, sourcerepo)
545 source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source,
545 source, incoming, bundle = bundlerepo.getremotechanges(ui, repo, source,
546 force=True)
546 force=True)
547 else:
547 else:
548 source = repo
548 source = repo
549
549
550 try:
550 try:
551 if opts.get('continue'):
551 if opts.get('continue'):
552 tp.resume(repo, source, opts)
552 tp.resume(repo, source, opts)
553 return
553 return
554
554
555 tf = tp.transplantfilter(repo, source, p1)
555 tf = tp.transplantfilter(repo, source, p1)
556 if opts.get('prune'):
556 if opts.get('prune'):
557 prune = [source.lookup(r)
557 prune = [source.lookup(r)
558 for r in cmdutil.revrange(source, opts.get('prune'))]
558 for r in cmdutil.revrange(source, opts.get('prune'))]
559 matchfn = lambda x: tf(x) and x not in prune
559 matchfn = lambda x: tf(x) and x not in prune
560 else:
560 else:
561 matchfn = tf
561 matchfn = tf
562 branches = map(source.lookup, opts.get('branch', ()))
562 branches = map(source.lookup, opts.get('branch', ()))
563 merges = map(source.lookup, opts.get('merge', ()))
563 merges = map(source.lookup, opts.get('merge', ()))
564 revmap = {}
564 revmap = {}
565 if revs:
565 if revs:
566 for r in cmdutil.revrange(source, revs):
566 for r in cmdutil.revrange(source, revs):
567 revmap[int(r)] = source.lookup(r)
567 revmap[int(r)] = source.lookup(r)
568 elif opts.get('all') or not merges:
568 elif opts.get('all') or not merges:
569 if source != repo:
569 if source != repo:
570 alltransplants = incwalk(source, incoming, branches,
570 alltransplants = incwalk(source, incoming, branches,
571 match=matchfn)
571 match=matchfn)
572 else:
572 else:
573 alltransplants = transplantwalk(source, p1, branches,
573 alltransplants = transplantwalk(source, p1, branches,
574 match=matchfn)
574 match=matchfn)
575 if opts.get('all'):
575 if opts.get('all'):
576 revs = alltransplants
576 revs = alltransplants
577 else:
577 else:
578 revs, newmerges = browserevs(ui, source, alltransplants, opts)
578 revs, newmerges = browserevs(ui, source, alltransplants, opts)
579 merges.extend(newmerges)
579 merges.extend(newmerges)
580 for r in revs:
580 for r in revs:
581 revmap[source.changelog.rev(r)] = r
581 revmap[source.changelog.rev(r)] = r
582 for r in merges:
582 for r in merges:
583 revmap[source.changelog.rev(r)] = r
583 revmap[source.changelog.rev(r)] = r
584
584
585 tp.apply(repo, source, revmap, merges, opts)
585 tp.apply(repo, source, revmap, merges, opts)
586 finally:
586 finally:
587 if bundle:
587 if bundle:
588 source.close()
588 source.close()
589 os.unlink(bundle)
589 os.unlink(bundle)
590
590
591 def revsettransplanted(repo, subset, x):
591 def revsettransplanted(repo, subset, x):
592 """``transplanted(set)``
592 """``transplanted(set)``
593 Transplanted changesets in set.
593 Transplanted changesets in set.
594 """
594 """
595 if x:
595 if x:
596 s = revset.getset(repo, subset, x)
596 s = revset.getset(repo, subset, x)
597 else:
597 else:
598 s = subset
598 s = subset
599 cs = set()
599 cs = set()
600 for r in xrange(0, len(repo)):
600 for r in xrange(0, len(repo)):
601 if repo[r].extra().get('transplant_source'):
601 if repo[r].extra().get('transplant_source'):
602 cs.add(r)
602 cs.add(r)
603 return [r for r in s if r in cs]
603 return [r for r in s if r in cs]
604
604
605 def extsetup(ui):
605 def extsetup(ui):
606 revset.symbols['transplanted'] = revsettransplanted
606 revset.symbols['transplanted'] = revsettransplanted
607
607
608 cmdtable = {
608 cmdtable = {
609 "transplant":
609 "transplant":
610 (transplant,
610 (transplant,
611 [('s', 'source', '',
611 [('s', 'source', '',
612 _('pull patches from REPO'), _('REPO')),
612 _('pull patches from REPO'), _('REPO')),
613 ('b', 'branch', [],
613 ('b', 'branch', [],
614 _('pull patches from branch BRANCH'), _('BRANCH')),
614 _('pull patches from branch BRANCH'), _('BRANCH')),
615 ('a', 'all', None, _('pull all changesets up to BRANCH')),
615 ('a', 'all', None, _('pull all changesets up to BRANCH')),
616 ('p', 'prune', [],
616 ('p', 'prune', [],
617 _('skip over REV'), _('REV')),
617 _('skip over REV'), _('REV')),
618 ('m', 'merge', [],
618 ('m', 'merge', [],
619 _('merge at REV'), _('REV')),
619 _('merge at REV'), _('REV')),
620 ('', 'log', None, _('append transplant info to log message')),
620 ('', 'log', None, _('append transplant info to log message')),
621 ('c', 'continue', None, _('continue last transplant session '
621 ('c', 'continue', None, _('continue last transplant session '
622 'after repair')),
622 'after repair')),
623 ('', 'filter', '',
623 ('', 'filter', '',
624 _('filter changesets through command'), _('CMD'))],
624 _('filter changesets through command'), _('CMD'))],
625 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
625 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
626 '[-m REV] [REV]...'))
626 '[-m REV] [REV]...'))
627 }
627 }
628
629 # tell hggettext to extract docstrings from these functions:
630 i18nfunctions = [revsettransplanted]
@@ -1,131 +1,137 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # hggettext - carefully extract docstrings for Mercurial
3 # hggettext - carefully extract docstrings for Mercurial
4 #
4 #
5 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
5 # Copyright 2009 Matt Mackall <mpm@selenic.com> and others
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # The normalize function is taken from pygettext which is distributed
10 # The normalize function is taken from pygettext which is distributed
11 # with Python under the Python License, which is GPL compatible.
11 # with Python under the Python License, which is GPL compatible.
12
12
13 """Extract docstrings from Mercurial commands.
13 """Extract docstrings from Mercurial commands.
14
14
15 Compared to pygettext, this script knows about the cmdtable and table
15 Compared to pygettext, this script knows about the cmdtable and table
16 dictionaries used by Mercurial, and will only extract docstrings from
16 dictionaries used by Mercurial, and will only extract docstrings from
17 functions mentioned therein.
17 functions mentioned therein.
18
18
19 Use xgettext like normal to extract strings marked as translatable and
19 Use xgettext like normal to extract strings marked as translatable and
20 join the message cataloges to get the final catalog.
20 join the message cataloges to get the final catalog.
21 """
21 """
22
22
23 import os, sys, inspect
23 import os, sys, inspect
24
24
25
25
26 def escape(s):
26 def escape(s):
27 # The order is important, the backslash must be escaped first
27 # The order is important, the backslash must be escaped first
28 # since the other replacements introduce new backslashes
28 # since the other replacements introduce new backslashes
29 # themselves.
29 # themselves.
30 s = s.replace('\\', '\\\\')
30 s = s.replace('\\', '\\\\')
31 s = s.replace('\n', '\\n')
31 s = s.replace('\n', '\\n')
32 s = s.replace('\r', '\\r')
32 s = s.replace('\r', '\\r')
33 s = s.replace('\t', '\\t')
33 s = s.replace('\t', '\\t')
34 s = s.replace('"', '\\"')
34 s = s.replace('"', '\\"')
35 return s
35 return s
36
36
37
37
38 def normalize(s):
38 def normalize(s):
39 # This converts the various Python string types into a format that
39 # This converts the various Python string types into a format that
40 # is appropriate for .po files, namely much closer to C style.
40 # is appropriate for .po files, namely much closer to C style.
41 lines = s.split('\n')
41 lines = s.split('\n')
42 if len(lines) == 1:
42 if len(lines) == 1:
43 s = '"' + escape(s) + '"'
43 s = '"' + escape(s) + '"'
44 else:
44 else:
45 if not lines[-1]:
45 if not lines[-1]:
46 del lines[-1]
46 del lines[-1]
47 lines[-1] = lines[-1] + '\n'
47 lines[-1] = lines[-1] + '\n'
48 lines = map(escape, lines)
48 lines = map(escape, lines)
49 lineterm = '\\n"\n"'
49 lineterm = '\\n"\n"'
50 s = '""\n"' + lineterm.join(lines) + '"'
50 s = '""\n"' + lineterm.join(lines) + '"'
51 return s
51 return s
52
52
53
53
54 def poentry(path, lineno, s):
54 def poentry(path, lineno, s):
55 return ('#: %s:%d\n' % (path, lineno) +
55 return ('#: %s:%d\n' % (path, lineno) +
56 'msgid %s\n' % normalize(s) +
56 'msgid %s\n' % normalize(s) +
57 'msgstr ""\n')
57 'msgstr ""\n')
58
58
59
59
60 def offset(src, doc, name, default):
60 def offset(src, doc, name, default):
61 """Compute offset or issue a warning on stdout."""
61 """Compute offset or issue a warning on stdout."""
62 # Backslashes in doc appear doubled in src.
62 # Backslashes in doc appear doubled in src.
63 end = src.find(doc.replace('\\', '\\\\'))
63 end = src.find(doc.replace('\\', '\\\\'))
64 if end == -1:
64 if end == -1:
65 # This can happen if the docstring contains unnecessary escape
65 # This can happen if the docstring contains unnecessary escape
66 # sequences such as \" in a triple-quoted string. The problem
66 # sequences such as \" in a triple-quoted string. The problem
67 # is that \" is turned into " and so doc wont appear in src.
67 # is that \" is turned into " and so doc wont appear in src.
68 sys.stderr.write("warning: unknown offset in %s, assuming %d lines\n"
68 sys.stderr.write("warning: unknown offset in %s, assuming %d lines\n"
69 % (name, default))
69 % (name, default))
70 return default
70 return default
71 else:
71 else:
72 return src.count('\n', 0, end)
72 return src.count('\n', 0, end)
73
73
74
74
75 def importpath(path):
75 def importpath(path):
76 """Import a path like foo/bar/baz.py and return the baz module."""
76 """Import a path like foo/bar/baz.py and return the baz module."""
77 if path.endswith('.py'):
77 if path.endswith('.py'):
78 path = path[:-3]
78 path = path[:-3]
79 if path.endswith('/__init__'):
79 if path.endswith('/__init__'):
80 path = path[:-9]
80 path = path[:-9]
81 path = path.replace('/', '.')
81 path = path.replace('/', '.')
82 mod = __import__(path)
82 mod = __import__(path)
83 for comp in path.split('.')[1:]:
83 for comp in path.split('.')[1:]:
84 mod = getattr(mod, comp)
84 mod = getattr(mod, comp)
85 return mod
85 return mod
86
86
87
87
88 def docstrings(path):
88 def docstrings(path):
89 """Extract docstrings from path.
89 """Extract docstrings from path.
90
90
91 This respects the Mercurial cmdtable/table convention and will
91 This respects the Mercurial cmdtable/table convention and will
92 only extract docstrings from functions mentioned in these tables.
92 only extract docstrings from functions mentioned in these tables.
93 """
93 """
94 mod = importpath(path)
94 mod = importpath(path)
95 if mod.__doc__:
95 if mod.__doc__:
96 src = open(path).read()
96 src = open(path).read()
97 lineno = 1 + offset(src, mod.__doc__, path, 7)
97 lineno = 1 + offset(src, mod.__doc__, path, 7)
98 print poentry(path, lineno, mod.__doc__)
98 print poentry(path, lineno, mod.__doc__)
99
99
100 functions = list(getattr(mod, 'i18nfunctions', []))
101 functions = [(f, True) for f in functions]
102
100 cmdtable = getattr(mod, 'cmdtable', {})
103 cmdtable = getattr(mod, 'cmdtable', {})
101 if not cmdtable:
104 if not cmdtable:
102 # Maybe we are processing mercurial.commands?
105 # Maybe we are processing mercurial.commands?
103 cmdtable = getattr(mod, 'table', {})
106 cmdtable = getattr(mod, 'table', {})
107 functions.extend((c[0], False) for c in cmdtable.itervalues())
104
108
105 for entry in cmdtable.itervalues():
109 for func, rstrip in functions:
106 func = entry[0]
107 if func.__doc__:
110 if func.__doc__:
108 src = inspect.getsource(func)
111 src = inspect.getsource(func)
109 name = "%s.%s" % (path, func.__name__)
112 name = "%s.%s" % (path, func.__name__)
110 lineno = func.func_code.co_firstlineno
113 lineno = func.func_code.co_firstlineno
111 lineno += offset(src, func.__doc__, name, 1)
114 doc = func.__doc__
112 print poentry(path, lineno, func.__doc__)
115 if rstrip:
116 doc = doc.rstrip()
117 lineno += offset(src, doc, name, 1)
118 print poentry(path, lineno, doc)
113
119
114
120
115 def rawtext(path):
121 def rawtext(path):
116 src = open(path).read()
122 src = open(path).read()
117 print poentry(path, 1, src)
123 print poentry(path, 1, src)
118
124
119
125
120 if __name__ == "__main__":
126 if __name__ == "__main__":
121 # It is very important that we import the Mercurial modules from
127 # It is very important that we import the Mercurial modules from
122 # the source tree where hggettext is executed. Otherwise we might
128 # the source tree where hggettext is executed. Otherwise we might
123 # accidentally import and extract strings from a Mercurial
129 # accidentally import and extract strings from a Mercurial
124 # installation mentioned in PYTHONPATH.
130 # installation mentioned in PYTHONPATH.
125 sys.path.insert(0, os.getcwd())
131 sys.path.insert(0, os.getcwd())
126 from mercurial import demandimport; demandimport.enable()
132 from mercurial import demandimport; demandimport.enable()
127 for path in sys.argv[1:]:
133 for path in sys.argv[1:]:
128 if path.endswith('.txt'):
134 if path.endswith('.txt'):
129 rawtext(path)
135 rawtext(path)
130 else:
136 else:
131 docstrings(path)
137 docstrings(path)
@@ -1,793 +1,796 b''
1 # revset.py - revision set queries for mercurial
1 # revset.py - revision set queries for mercurial
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import re
8 import re
9 import parser, util, error, discovery
9 import parser, util, error, discovery
10 import match as matchmod
10 import match as matchmod
11 from i18n import _
11 from i18n import _
12
12
13 elements = {
13 elements = {
14 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
14 "(": (20, ("group", 1, ")"), ("func", 1, ")")),
15 "-": (5, ("negate", 19), ("minus", 5)),
15 "-": (5, ("negate", 19), ("minus", 5)),
16 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
16 "::": (17, ("dagrangepre", 17), ("dagrange", 17),
17 ("dagrangepost", 17)),
17 ("dagrangepost", 17)),
18 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
18 "..": (17, ("dagrangepre", 17), ("dagrange", 17),
19 ("dagrangepost", 17)),
19 ("dagrangepost", 17)),
20 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
20 ":": (15, ("rangepre", 15), ("range", 15), ("rangepost", 15)),
21 "not": (10, ("not", 10)),
21 "not": (10, ("not", 10)),
22 "!": (10, ("not", 10)),
22 "!": (10, ("not", 10)),
23 "and": (5, None, ("and", 5)),
23 "and": (5, None, ("and", 5)),
24 "&": (5, None, ("and", 5)),
24 "&": (5, None, ("and", 5)),
25 "or": (4, None, ("or", 4)),
25 "or": (4, None, ("or", 4)),
26 "|": (4, None, ("or", 4)),
26 "|": (4, None, ("or", 4)),
27 "+": (4, None, ("or", 4)),
27 "+": (4, None, ("or", 4)),
28 ",": (2, None, ("list", 2)),
28 ",": (2, None, ("list", 2)),
29 ")": (0, None, None),
29 ")": (0, None, None),
30 "symbol": (0, ("symbol",), None),
30 "symbol": (0, ("symbol",), None),
31 "string": (0, ("string",), None),
31 "string": (0, ("string",), None),
32 "end": (0, None, None),
32 "end": (0, None, None),
33 }
33 }
34
34
35 keywords = set(['and', 'or', 'not'])
35 keywords = set(['and', 'or', 'not'])
36
36
37 def tokenize(program):
37 def tokenize(program):
38 pos, l = 0, len(program)
38 pos, l = 0, len(program)
39 while pos < l:
39 while pos < l:
40 c = program[pos]
40 c = program[pos]
41 if c.isspace(): # skip inter-token whitespace
41 if c.isspace(): # skip inter-token whitespace
42 pass
42 pass
43 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
43 elif c == ':' and program[pos:pos + 2] == '::': # look ahead carefully
44 yield ('::', None, pos)
44 yield ('::', None, pos)
45 pos += 1 # skip ahead
45 pos += 1 # skip ahead
46 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
46 elif c == '.' and program[pos:pos + 2] == '..': # look ahead carefully
47 yield ('..', None, pos)
47 yield ('..', None, pos)
48 pos += 1 # skip ahead
48 pos += 1 # skip ahead
49 elif c in "():,-|&+!": # handle simple operators
49 elif c in "():,-|&+!": # handle simple operators
50 yield (c, None, pos)
50 yield (c, None, pos)
51 elif (c in '"\'' or c == 'r' and
51 elif (c in '"\'' or c == 'r' and
52 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
52 program[pos:pos + 2] in ("r'", 'r"')): # handle quoted strings
53 if c == 'r':
53 if c == 'r':
54 pos += 1
54 pos += 1
55 c = program[pos]
55 c = program[pos]
56 decode = lambda x: x
56 decode = lambda x: x
57 else:
57 else:
58 decode = lambda x: x.decode('string-escape')
58 decode = lambda x: x.decode('string-escape')
59 pos += 1
59 pos += 1
60 s = pos
60 s = pos
61 while pos < l: # find closing quote
61 while pos < l: # find closing quote
62 d = program[pos]
62 d = program[pos]
63 if d == '\\': # skip over escaped characters
63 if d == '\\': # skip over escaped characters
64 pos += 2
64 pos += 2
65 continue
65 continue
66 if d == c:
66 if d == c:
67 yield ('string', decode(program[s:pos]), s)
67 yield ('string', decode(program[s:pos]), s)
68 break
68 break
69 pos += 1
69 pos += 1
70 else:
70 else:
71 raise error.ParseError(_("unterminated string"), s)
71 raise error.ParseError(_("unterminated string"), s)
72 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
72 elif c.isalnum() or c in '._' or ord(c) > 127: # gather up a symbol/keyword
73 s = pos
73 s = pos
74 pos += 1
74 pos += 1
75 while pos < l: # find end of symbol
75 while pos < l: # find end of symbol
76 d = program[pos]
76 d = program[pos]
77 if not (d.isalnum() or d in "._" or ord(d) > 127):
77 if not (d.isalnum() or d in "._" or ord(d) > 127):
78 break
78 break
79 if d == '.' and program[pos - 1] == '.': # special case for ..
79 if d == '.' and program[pos - 1] == '.': # special case for ..
80 pos -= 1
80 pos -= 1
81 break
81 break
82 pos += 1
82 pos += 1
83 sym = program[s:pos]
83 sym = program[s:pos]
84 if sym in keywords: # operator keywords
84 if sym in keywords: # operator keywords
85 yield (sym, None, s)
85 yield (sym, None, s)
86 else:
86 else:
87 yield ('symbol', sym, s)
87 yield ('symbol', sym, s)
88 pos -= 1
88 pos -= 1
89 else:
89 else:
90 raise error.ParseError(_("syntax error"), pos)
90 raise error.ParseError(_("syntax error"), pos)
91 pos += 1
91 pos += 1
92 yield ('end', None, pos)
92 yield ('end', None, pos)
93
93
94 # helpers
94 # helpers
95
95
96 def getstring(x, err):
96 def getstring(x, err):
97 if x and (x[0] == 'string' or x[0] == 'symbol'):
97 if x and (x[0] == 'string' or x[0] == 'symbol'):
98 return x[1]
98 return x[1]
99 raise error.ParseError(err)
99 raise error.ParseError(err)
100
100
101 def getlist(x):
101 def getlist(x):
102 if not x:
102 if not x:
103 return []
103 return []
104 if x[0] == 'list':
104 if x[0] == 'list':
105 return getlist(x[1]) + [x[2]]
105 return getlist(x[1]) + [x[2]]
106 return [x]
106 return [x]
107
107
108 def getargs(x, min, max, err):
108 def getargs(x, min, max, err):
109 l = getlist(x)
109 l = getlist(x)
110 if len(l) < min or len(l) > max:
110 if len(l) < min or len(l) > max:
111 raise error.ParseError(err)
111 raise error.ParseError(err)
112 return l
112 return l
113
113
114 def getset(repo, subset, x):
114 def getset(repo, subset, x):
115 if not x:
115 if not x:
116 raise error.ParseError(_("missing argument"))
116 raise error.ParseError(_("missing argument"))
117 return methods[x[0]](repo, subset, *x[1:])
117 return methods[x[0]](repo, subset, *x[1:])
118
118
119 # operator methods
119 # operator methods
120
120
121 def stringset(repo, subset, x):
121 def stringset(repo, subset, x):
122 x = repo[x].rev()
122 x = repo[x].rev()
123 if x == -1 and len(subset) == len(repo):
123 if x == -1 and len(subset) == len(repo):
124 return [-1]
124 return [-1]
125 if x in subset:
125 if x in subset:
126 return [x]
126 return [x]
127 return []
127 return []
128
128
129 def symbolset(repo, subset, x):
129 def symbolset(repo, subset, x):
130 if x in symbols:
130 if x in symbols:
131 raise error.ParseError(_("can't use %s here") % x)
131 raise error.ParseError(_("can't use %s here") % x)
132 return stringset(repo, subset, x)
132 return stringset(repo, subset, x)
133
133
134 def rangeset(repo, subset, x, y):
134 def rangeset(repo, subset, x, y):
135 m = getset(repo, subset, x)
135 m = getset(repo, subset, x)
136 if not m:
136 if not m:
137 m = getset(repo, range(len(repo)), x)
137 m = getset(repo, range(len(repo)), x)
138
138
139 n = getset(repo, subset, y)
139 n = getset(repo, subset, y)
140 if not n:
140 if not n:
141 n = getset(repo, range(len(repo)), y)
141 n = getset(repo, range(len(repo)), y)
142
142
143 if not m or not n:
143 if not m or not n:
144 return []
144 return []
145 m, n = m[0], n[-1]
145 m, n = m[0], n[-1]
146
146
147 if m < n:
147 if m < n:
148 r = range(m, n + 1)
148 r = range(m, n + 1)
149 else:
149 else:
150 r = range(m, n - 1, -1)
150 r = range(m, n - 1, -1)
151 s = set(subset)
151 s = set(subset)
152 return [x for x in r if x in s]
152 return [x for x in r if x in s]
153
153
154 def andset(repo, subset, x, y):
154 def andset(repo, subset, x, y):
155 return getset(repo, getset(repo, subset, x), y)
155 return getset(repo, getset(repo, subset, x), y)
156
156
157 def orset(repo, subset, x, y):
157 def orset(repo, subset, x, y):
158 s = set(getset(repo, subset, x))
158 s = set(getset(repo, subset, x))
159 s |= set(getset(repo, [r for r in subset if r not in s], y))
159 s |= set(getset(repo, [r for r in subset if r not in s], y))
160 return [r for r in subset if r in s]
160 return [r for r in subset if r in s]
161
161
162 def notset(repo, subset, x):
162 def notset(repo, subset, x):
163 s = set(getset(repo, subset, x))
163 s = set(getset(repo, subset, x))
164 return [r for r in subset if r not in s]
164 return [r for r in subset if r not in s]
165
165
166 def listset(repo, subset, a, b):
166 def listset(repo, subset, a, b):
167 raise error.ParseError(_("can't use a list in this context"))
167 raise error.ParseError(_("can't use a list in this context"))
168
168
169 def func(repo, subset, a, b):
169 def func(repo, subset, a, b):
170 if a[0] == 'symbol' and a[1] in symbols:
170 if a[0] == 'symbol' and a[1] in symbols:
171 return symbols[a[1]](repo, subset, b)
171 return symbols[a[1]](repo, subset, b)
172 raise error.ParseError(_("not a function: %s") % a[1])
172 raise error.ParseError(_("not a function: %s") % a[1])
173
173
174 # functions
174 # functions
175
175
176 def node(repo, subset, x):
176 def node(repo, subset, x):
177 """``id(string)``
177 """``id(string)``
178 Revision non-ambiguously specified by the given hex string prefix
178 Revision non-ambiguously specified by the given hex string prefix
179 """
179 """
180 # i18n: "id" is a keyword
180 # i18n: "id" is a keyword
181 l = getargs(x, 1, 1, _("id requires one argument"))
181 l = getargs(x, 1, 1, _("id requires one argument"))
182 # i18n: "id" is a keyword
182 # i18n: "id" is a keyword
183 n = getstring(l[0], _("id requires a string"))
183 n = getstring(l[0], _("id requires a string"))
184 if len(n) == 40:
184 if len(n) == 40:
185 rn = repo[n].rev()
185 rn = repo[n].rev()
186 else:
186 else:
187 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
187 rn = repo.changelog.rev(repo.changelog._partialmatch(n))
188 return [r for r in subset if r == rn]
188 return [r for r in subset if r == rn]
189
189
190 def rev(repo, subset, x):
190 def rev(repo, subset, x):
191 """``rev(number)``
191 """``rev(number)``
192 Revision with the given numeric identifier.
192 Revision with the given numeric identifier.
193 """
193 """
194 # i18n: "rev" is a keyword
194 # i18n: "rev" is a keyword
195 l = getargs(x, 1, 1, _("rev requires one argument"))
195 l = getargs(x, 1, 1, _("rev requires one argument"))
196 try:
196 try:
197 # i18n: "rev" is a keyword
197 # i18n: "rev" is a keyword
198 l = int(getstring(l[0], _("rev requires a number")))
198 l = int(getstring(l[0], _("rev requires a number")))
199 except ValueError:
199 except ValueError:
200 # i18n: "rev" is a keyword
200 # i18n: "rev" is a keyword
201 raise error.ParseError(_("rev expects a number"))
201 raise error.ParseError(_("rev expects a number"))
202 return [r for r in subset if r == l]
202 return [r for r in subset if r == l]
203
203
204 def p1(repo, subset, x):
204 def p1(repo, subset, x):
205 """``p1(set)``
205 """``p1(set)``
206 First parent of changesets in set.
206 First parent of changesets in set.
207 """
207 """
208 ps = set()
208 ps = set()
209 cl = repo.changelog
209 cl = repo.changelog
210 for r in getset(repo, range(len(repo)), x):
210 for r in getset(repo, range(len(repo)), x):
211 ps.add(cl.parentrevs(r)[0])
211 ps.add(cl.parentrevs(r)[0])
212 return [r for r in subset if r in ps]
212 return [r for r in subset if r in ps]
213
213
214 def p2(repo, subset, x):
214 def p2(repo, subset, x):
215 """``p2(set)``
215 """``p2(set)``
216 Second parent of changesets in set.
216 Second parent of changesets in set.
217 """
217 """
218 ps = set()
218 ps = set()
219 cl = repo.changelog
219 cl = repo.changelog
220 for r in getset(repo, range(len(repo)), x):
220 for r in getset(repo, range(len(repo)), x):
221 ps.add(cl.parentrevs(r)[1])
221 ps.add(cl.parentrevs(r)[1])
222 return [r for r in subset if r in ps]
222 return [r for r in subset if r in ps]
223
223
224 def parents(repo, subset, x):
224 def parents(repo, subset, x):
225 """``parents(set)``
225 """``parents(set)``
226 The set of all parents for all changesets in set.
226 The set of all parents for all changesets in set.
227 """
227 """
228 ps = set()
228 ps = set()
229 cl = repo.changelog
229 cl = repo.changelog
230 for r in getset(repo, range(len(repo)), x):
230 for r in getset(repo, range(len(repo)), x):
231 ps.update(cl.parentrevs(r))
231 ps.update(cl.parentrevs(r))
232 return [r for r in subset if r in ps]
232 return [r for r in subset if r in ps]
233
233
234 def maxrev(repo, subset, x):
234 def maxrev(repo, subset, x):
235 """``max(set)``
235 """``max(set)``
236 Changeset with highest revision number in set.
236 Changeset with highest revision number in set.
237 """
237 """
238 s = getset(repo, subset, x)
238 s = getset(repo, subset, x)
239 if s:
239 if s:
240 m = max(s)
240 m = max(s)
241 if m in subset:
241 if m in subset:
242 return [m]
242 return [m]
243 return []
243 return []
244
244
245 def minrev(repo, subset, x):
245 def minrev(repo, subset, x):
246 """``min(set)``
246 """``min(set)``
247 Changeset with lowest revision number in set.
247 Changeset with lowest revision number in set.
248 """
248 """
249 s = getset(repo, subset, x)
249 s = getset(repo, subset, x)
250 if s:
250 if s:
251 m = min(s)
251 m = min(s)
252 if m in subset:
252 if m in subset:
253 return [m]
253 return [m]
254 return []
254 return []
255
255
256 def limit(repo, subset, x):
256 def limit(repo, subset, x):
257 """``limit(set, n)``
257 """``limit(set, n)``
258 First n members of set.
258 First n members of set.
259 """
259 """
260 # i18n: "limit" is a keyword
260 # i18n: "limit" is a keyword
261 l = getargs(x, 2, 2, _("limit requires two arguments"))
261 l = getargs(x, 2, 2, _("limit requires two arguments"))
262 try:
262 try:
263 # i18n: "limit" is a keyword
263 # i18n: "limit" is a keyword
264 lim = int(getstring(l[1], _("limit requires a number")))
264 lim = int(getstring(l[1], _("limit requires a number")))
265 except ValueError:
265 except ValueError:
266 # i18n: "limit" is a keyword
266 # i18n: "limit" is a keyword
267 raise error.ParseError(_("limit expects a number"))
267 raise error.ParseError(_("limit expects a number"))
268 return getset(repo, subset, l[0])[:lim]
268 return getset(repo, subset, l[0])[:lim]
269
269
270 def children(repo, subset, x):
270 def children(repo, subset, x):
271 """``children(set)``
271 """``children(set)``
272 Child changesets of changesets in set.
272 Child changesets of changesets in set.
273 """
273 """
274 cs = set()
274 cs = set()
275 cl = repo.changelog
275 cl = repo.changelog
276 s = set(getset(repo, range(len(repo)), x))
276 s = set(getset(repo, range(len(repo)), x))
277 for r in xrange(0, len(repo)):
277 for r in xrange(0, len(repo)):
278 for p in cl.parentrevs(r):
278 for p in cl.parentrevs(r):
279 if p in s:
279 if p in s:
280 cs.add(r)
280 cs.add(r)
281 return [r for r in subset if r in cs]
281 return [r for r in subset if r in cs]
282
282
283 def branch(repo, subset, x):
283 def branch(repo, subset, x):
284 """``branch(set)``
284 """``branch(set)``
285 All changesets belonging to the branches of changesets in set.
285 All changesets belonging to the branches of changesets in set.
286 """
286 """
287 s = getset(repo, range(len(repo)), x)
287 s = getset(repo, range(len(repo)), x)
288 b = set()
288 b = set()
289 for r in s:
289 for r in s:
290 b.add(repo[r].branch())
290 b.add(repo[r].branch())
291 s = set(s)
291 s = set(s)
292 return [r for r in subset if r in s or repo[r].branch() in b]
292 return [r for r in subset if r in s or repo[r].branch() in b]
293
293
294 def ancestor(repo, subset, x):
294 def ancestor(repo, subset, x):
295 """``ancestor(single, single)``
295 """``ancestor(single, single)``
296 Greatest common ancestor of the two changesets.
296 Greatest common ancestor of the two changesets.
297 """
297 """
298 # i18n: "ancestor" is a keyword
298 # i18n: "ancestor" is a keyword
299 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
299 l = getargs(x, 2, 2, _("ancestor requires two arguments"))
300 r = range(len(repo))
300 r = range(len(repo))
301 a = getset(repo, r, l[0])
301 a = getset(repo, r, l[0])
302 b = getset(repo, r, l[1])
302 b = getset(repo, r, l[1])
303 if len(a) != 1 or len(b) != 1:
303 if len(a) != 1 or len(b) != 1:
304 # i18n: "ancestor" is a keyword
304 # i18n: "ancestor" is a keyword
305 raise error.ParseError(_("ancestor arguments must be single revisions"))
305 raise error.ParseError(_("ancestor arguments must be single revisions"))
306 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
306 an = [repo[a[0]].ancestor(repo[b[0]]).rev()]
307
307
308 return [r for r in an if r in subset]
308 return [r for r in an if r in subset]
309
309
310 def ancestors(repo, subset, x):
310 def ancestors(repo, subset, x):
311 """``ancestors(set)``
311 """``ancestors(set)``
312 Changesets that are ancestors of a changeset in set.
312 Changesets that are ancestors of a changeset in set.
313 """
313 """
314 args = getset(repo, range(len(repo)), x)
314 args = getset(repo, range(len(repo)), x)
315 if not args:
315 if not args:
316 return []
316 return []
317 s = set(repo.changelog.ancestors(*args)) | set(args)
317 s = set(repo.changelog.ancestors(*args)) | set(args)
318 return [r for r in subset if r in s]
318 return [r for r in subset if r in s]
319
319
320 def descendants(repo, subset, x):
320 def descendants(repo, subset, x):
321 """``descendants(set)``
321 """``descendants(set)``
322 Changesets which are descendants of changesets in set.
322 Changesets which are descendants of changesets in set.
323 """
323 """
324 args = getset(repo, range(len(repo)), x)
324 args = getset(repo, range(len(repo)), x)
325 if not args:
325 if not args:
326 return []
326 return []
327 s = set(repo.changelog.descendants(*args)) | set(args)
327 s = set(repo.changelog.descendants(*args)) | set(args)
328 return [r for r in subset if r in s]
328 return [r for r in subset if r in s]
329
329
330 def follow(repo, subset, x):
330 def follow(repo, subset, x):
331 """``follow()``
331 """``follow()``
332 An alias for ``::.`` (ancestors of the working copy's first parent).
332 An alias for ``::.`` (ancestors of the working copy's first parent).
333 """
333 """
334 # i18n: "follow" is a keyword
334 # i18n: "follow" is a keyword
335 getargs(x, 0, 0, _("follow takes no arguments"))
335 getargs(x, 0, 0, _("follow takes no arguments"))
336 p = repo['.'].rev()
336 p = repo['.'].rev()
337 s = set(repo.changelog.ancestors(p)) | set([p])
337 s = set(repo.changelog.ancestors(p)) | set([p])
338 return [r for r in subset if r in s]
338 return [r for r in subset if r in s]
339
339
340 def date(repo, subset, x):
340 def date(repo, subset, x):
341 """``date(interval)``
341 """``date(interval)``
342 Changesets within the interval, see :hg:`help dates`.
342 Changesets within the interval, see :hg:`help dates`.
343 """
343 """
344 # i18n: "date" is a keyword
344 # i18n: "date" is a keyword
345 ds = getstring(x, _("date requires a string"))
345 ds = getstring(x, _("date requires a string"))
346 dm = util.matchdate(ds)
346 dm = util.matchdate(ds)
347 return [r for r in subset if dm(repo[r].date()[0])]
347 return [r for r in subset if dm(repo[r].date()[0])]
348
348
349 def keyword(repo, subset, x):
349 def keyword(repo, subset, x):
350 """``keyword(string)``
350 """``keyword(string)``
351 Search commit message, user name, and names of changed files for
351 Search commit message, user name, and names of changed files for
352 string.
352 string.
353 """
353 """
354 # i18n: "keyword" is a keyword
354 # i18n: "keyword" is a keyword
355 kw = getstring(x, _("keyword requires a string")).lower()
355 kw = getstring(x, _("keyword requires a string")).lower()
356 l = []
356 l = []
357 for r in subset:
357 for r in subset:
358 c = repo[r]
358 c = repo[r]
359 t = " ".join(c.files() + [c.user(), c.description()])
359 t = " ".join(c.files() + [c.user(), c.description()])
360 if kw in t.lower():
360 if kw in t.lower():
361 l.append(r)
361 l.append(r)
362 return l
362 return l
363
363
364 def grep(repo, subset, x):
364 def grep(repo, subset, x):
365 """``grep(regex)``
365 """``grep(regex)``
366 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
366 Like ``keyword(string)`` but accepts a regex. Use ``grep(r'...')``
367 to ensure special escape characters are handled correctly.
367 to ensure special escape characters are handled correctly.
368 """
368 """
369 try:
369 try:
370 # i18n: "grep" is a keyword
370 # i18n: "grep" is a keyword
371 gr = re.compile(getstring(x, _("grep requires a string")))
371 gr = re.compile(getstring(x, _("grep requires a string")))
372 except re.error, e:
372 except re.error, e:
373 raise error.ParseError(_('invalid match pattern: %s') % e)
373 raise error.ParseError(_('invalid match pattern: %s') % e)
374 l = []
374 l = []
375 for r in subset:
375 for r in subset:
376 c = repo[r]
376 c = repo[r]
377 for e in c.files() + [c.user(), c.description()]:
377 for e in c.files() + [c.user(), c.description()]:
378 if gr.search(e):
378 if gr.search(e):
379 l.append(r)
379 l.append(r)
380 continue
380 continue
381 return l
381 return l
382
382
383 def author(repo, subset, x):
383 def author(repo, subset, x):
384 """``author(string)``
384 """``author(string)``
385 Alias for ``user(string)``.
385 Alias for ``user(string)``.
386 """
386 """
387 # i18n: "author" is a keyword
387 # i18n: "author" is a keyword
388 n = getstring(x, _("author requires a string")).lower()
388 n = getstring(x, _("author requires a string")).lower()
389 return [r for r in subset if n in repo[r].user().lower()]
389 return [r for r in subset if n in repo[r].user().lower()]
390
390
391 def user(repo, subset, x):
391 def user(repo, subset, x):
392 """``user(string)``
392 """``user(string)``
393 User name is string.
393 User name is string.
394 """
394 """
395 return author(repo, subset, x)
395 return author(repo, subset, x)
396
396
397 def hasfile(repo, subset, x):
397 def hasfile(repo, subset, x):
398 """``file(pattern)``
398 """``file(pattern)``
399 Changesets affecting files matched by pattern.
399 Changesets affecting files matched by pattern.
400 """
400 """
401 # i18n: "file" is a keyword
401 # i18n: "file" is a keyword
402 pat = getstring(x, _("file requires a pattern"))
402 pat = getstring(x, _("file requires a pattern"))
403 m = matchmod.match(repo.root, repo.getcwd(), [pat])
403 m = matchmod.match(repo.root, repo.getcwd(), [pat])
404 s = []
404 s = []
405 for r in subset:
405 for r in subset:
406 for f in repo[r].files():
406 for f in repo[r].files():
407 if m(f):
407 if m(f):
408 s.append(r)
408 s.append(r)
409 continue
409 continue
410 return s
410 return s
411
411
412 def contains(repo, subset, x):
412 def contains(repo, subset, x):
413 """``contains(pattern)``
413 """``contains(pattern)``
414 Revision contains pattern.
414 Revision contains pattern.
415 """
415 """
416 # i18n: "contains" is a keyword
416 # i18n: "contains" is a keyword
417 pat = getstring(x, _("contains requires a pattern"))
417 pat = getstring(x, _("contains requires a pattern"))
418 m = matchmod.match(repo.root, repo.getcwd(), [pat])
418 m = matchmod.match(repo.root, repo.getcwd(), [pat])
419 s = []
419 s = []
420 if m.files() == [pat]:
420 if m.files() == [pat]:
421 for r in subset:
421 for r in subset:
422 if pat in repo[r]:
422 if pat in repo[r]:
423 s.append(r)
423 s.append(r)
424 continue
424 continue
425 else:
425 else:
426 for r in subset:
426 for r in subset:
427 for f in repo[r].manifest():
427 for f in repo[r].manifest():
428 if m(f):
428 if m(f):
429 s.append(r)
429 s.append(r)
430 continue
430 continue
431 return s
431 return s
432
432
433 def checkstatus(repo, subset, pat, field):
433 def checkstatus(repo, subset, pat, field):
434 m = matchmod.match(repo.root, repo.getcwd(), [pat])
434 m = matchmod.match(repo.root, repo.getcwd(), [pat])
435 s = []
435 s = []
436 fast = (m.files() == [pat])
436 fast = (m.files() == [pat])
437 for r in subset:
437 for r in subset:
438 c = repo[r]
438 c = repo[r]
439 if fast:
439 if fast:
440 if pat not in c.files():
440 if pat not in c.files():
441 continue
441 continue
442 else:
442 else:
443 for f in c.files():
443 for f in c.files():
444 if m(f):
444 if m(f):
445 break
445 break
446 else:
446 else:
447 continue
447 continue
448 files = repo.status(c.p1().node(), c.node())[field]
448 files = repo.status(c.p1().node(), c.node())[field]
449 if fast:
449 if fast:
450 if pat in files:
450 if pat in files:
451 s.append(r)
451 s.append(r)
452 continue
452 continue
453 else:
453 else:
454 for f in files:
454 for f in files:
455 if m(f):
455 if m(f):
456 s.append(r)
456 s.append(r)
457 continue
457 continue
458 return s
458 return s
459
459
460 def modifies(repo, subset, x):
460 def modifies(repo, subset, x):
461 """``modifies(pattern)``
461 """``modifies(pattern)``
462 Changesets modifying files matched by pattern.
462 Changesets modifying files matched by pattern.
463 """
463 """
464 # i18n: "modifies" is a keyword
464 # i18n: "modifies" is a keyword
465 pat = getstring(x, _("modifies requires a pattern"))
465 pat = getstring(x, _("modifies requires a pattern"))
466 return checkstatus(repo, subset, pat, 0)
466 return checkstatus(repo, subset, pat, 0)
467
467
468 def adds(repo, subset, x):
468 def adds(repo, subset, x):
469 """``adds(pattern)``
469 """``adds(pattern)``
470 Changesets that add a file matching pattern.
470 Changesets that add a file matching pattern.
471 """
471 """
472 # i18n: "adds" is a keyword
472 # i18n: "adds" is a keyword
473 pat = getstring(x, _("adds requires a pattern"))
473 pat = getstring(x, _("adds requires a pattern"))
474 return checkstatus(repo, subset, pat, 1)
474 return checkstatus(repo, subset, pat, 1)
475
475
476 def removes(repo, subset, x):
476 def removes(repo, subset, x):
477 """``removes(pattern)``
477 """``removes(pattern)``
478 Changesets which remove files matching pattern.
478 Changesets which remove files matching pattern.
479 """
479 """
480 # i18n: "removes" is a keyword
480 # i18n: "removes" is a keyword
481 pat = getstring(x, _("removes requires a pattern"))
481 pat = getstring(x, _("removes requires a pattern"))
482 return checkstatus(repo, subset, pat, 2)
482 return checkstatus(repo, subset, pat, 2)
483
483
484 def merge(repo, subset, x):
484 def merge(repo, subset, x):
485 """``merge()``
485 """``merge()``
486 Changeset is a merge changeset.
486 Changeset is a merge changeset.
487 """
487 """
488 # i18n: "merge" is a keyword
488 # i18n: "merge" is a keyword
489 getargs(x, 0, 0, _("merge takes no arguments"))
489 getargs(x, 0, 0, _("merge takes no arguments"))
490 cl = repo.changelog
490 cl = repo.changelog
491 return [r for r in subset if cl.parentrevs(r)[1] != -1]
491 return [r for r in subset if cl.parentrevs(r)[1] != -1]
492
492
493 def closed(repo, subset, x):
493 def closed(repo, subset, x):
494 """``closed()``
494 """``closed()``
495 Changeset is closed.
495 Changeset is closed.
496 """
496 """
497 # i18n: "closed" is a keyword
497 # i18n: "closed" is a keyword
498 getargs(x, 0, 0, _("closed takes no arguments"))
498 getargs(x, 0, 0, _("closed takes no arguments"))
499 return [r for r in subset if repo[r].extra().get('close')]
499 return [r for r in subset if repo[r].extra().get('close')]
500
500
501 def head(repo, subset, x):
501 def head(repo, subset, x):
502 """``head()``
502 """``head()``
503 Changeset is a named branch head.
503 Changeset is a named branch head.
504 """
504 """
505 # i18n: "head" is a keyword
505 # i18n: "head" is a keyword
506 getargs(x, 0, 0, _("head takes no arguments"))
506 getargs(x, 0, 0, _("head takes no arguments"))
507 hs = set()
507 hs = set()
508 for b, ls in repo.branchmap().iteritems():
508 for b, ls in repo.branchmap().iteritems():
509 hs.update(repo[h].rev() for h in ls)
509 hs.update(repo[h].rev() for h in ls)
510 return [r for r in subset if r in hs]
510 return [r for r in subset if r in hs]
511
511
512 def reverse(repo, subset, x):
512 def reverse(repo, subset, x):
513 """``reverse(set)``
513 """``reverse(set)``
514 Reverse order of set.
514 Reverse order of set.
515 """
515 """
516 l = getset(repo, subset, x)
516 l = getset(repo, subset, x)
517 l.reverse()
517 l.reverse()
518 return l
518 return l
519
519
520 def present(repo, subset, x):
520 def present(repo, subset, x):
521 """``present(set)``
521 """``present(set)``
522 An empty set, if any revision in set isn't found; otherwise,
522 An empty set, if any revision in set isn't found; otherwise,
523 all revisions in set.
523 all revisions in set.
524 """
524 """
525 try:
525 try:
526 return getset(repo, subset, x)
526 return getset(repo, subset, x)
527 except error.RepoLookupError:
527 except error.RepoLookupError:
528 return []
528 return []
529
529
530 def sort(repo, subset, x):
530 def sort(repo, subset, x):
531 """``sort(set[, [-]key...])``
531 """``sort(set[, [-]key...])``
532 Sort set by keys. The default sort order is ascending, specify a key
532 Sort set by keys. The default sort order is ascending, specify a key
533 as ``-key`` to sort in descending order.
533 as ``-key`` to sort in descending order.
534
534
535 The keys can be:
535 The keys can be:
536
536
537 - ``rev`` for the revision number,
537 - ``rev`` for the revision number,
538 - ``branch`` for the branch name,
538 - ``branch`` for the branch name,
539 - ``desc`` for the commit message (description),
539 - ``desc`` for the commit message (description),
540 - ``user`` for user name (``author`` can be used as an alias),
540 - ``user`` for user name (``author`` can be used as an alias),
541 - ``date`` for the commit date
541 - ``date`` for the commit date
542 """
542 """
543 # i18n: "sort" is a keyword
543 # i18n: "sort" is a keyword
544 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
544 l = getargs(x, 1, 2, _("sort requires one or two arguments"))
545 keys = "rev"
545 keys = "rev"
546 if len(l) == 2:
546 if len(l) == 2:
547 keys = getstring(l[1], _("sort spec must be a string"))
547 keys = getstring(l[1], _("sort spec must be a string"))
548
548
549 s = l[0]
549 s = l[0]
550 keys = keys.split()
550 keys = keys.split()
551 l = []
551 l = []
552 def invert(s):
552 def invert(s):
553 return "".join(chr(255 - ord(c)) for c in s)
553 return "".join(chr(255 - ord(c)) for c in s)
554 for r in getset(repo, subset, s):
554 for r in getset(repo, subset, s):
555 c = repo[r]
555 c = repo[r]
556 e = []
556 e = []
557 for k in keys:
557 for k in keys:
558 if k == 'rev':
558 if k == 'rev':
559 e.append(r)
559 e.append(r)
560 elif k == '-rev':
560 elif k == '-rev':
561 e.append(-r)
561 e.append(-r)
562 elif k == 'branch':
562 elif k == 'branch':
563 e.append(c.branch())
563 e.append(c.branch())
564 elif k == '-branch':
564 elif k == '-branch':
565 e.append(invert(c.branch()))
565 e.append(invert(c.branch()))
566 elif k == 'desc':
566 elif k == 'desc':
567 e.append(c.description())
567 e.append(c.description())
568 elif k == '-desc':
568 elif k == '-desc':
569 e.append(invert(c.description()))
569 e.append(invert(c.description()))
570 elif k in 'user author':
570 elif k in 'user author':
571 e.append(c.user())
571 e.append(c.user())
572 elif k in '-user -author':
572 elif k in '-user -author':
573 e.append(invert(c.user()))
573 e.append(invert(c.user()))
574 elif k == 'date':
574 elif k == 'date':
575 e.append(c.date()[0])
575 e.append(c.date()[0])
576 elif k == '-date':
576 elif k == '-date':
577 e.append(-c.date()[0])
577 e.append(-c.date()[0])
578 else:
578 else:
579 raise error.ParseError(_("unknown sort key %r") % k)
579 raise error.ParseError(_("unknown sort key %r") % k)
580 e.append(r)
580 e.append(r)
581 l.append(e)
581 l.append(e)
582 l.sort()
582 l.sort()
583 return [e[-1] for e in l]
583 return [e[-1] for e in l]
584
584
585 def getall(repo, subset, x):
585 def getall(repo, subset, x):
586 """``all()``
586 """``all()``
587 All changesets, the same as ``0:tip``.
587 All changesets, the same as ``0:tip``.
588 """
588 """
589 # i18n: "all" is a keyword
589 # i18n: "all" is a keyword
590 getargs(x, 0, 0, _("all takes no arguments"))
590 getargs(x, 0, 0, _("all takes no arguments"))
591 return subset
591 return subset
592
592
593 def heads(repo, subset, x):
593 def heads(repo, subset, x):
594 """``heads(set)``
594 """``heads(set)``
595 Members of set with no children in set.
595 Members of set with no children in set.
596 """
596 """
597 s = getset(repo, subset, x)
597 s = getset(repo, subset, x)
598 ps = set(parents(repo, subset, x))
598 ps = set(parents(repo, subset, x))
599 return [r for r in s if r not in ps]
599 return [r for r in s if r not in ps]
600
600
601 def roots(repo, subset, x):
601 def roots(repo, subset, x):
602 """``roots(set)``
602 """``roots(set)``
603 Changesets with no parent changeset in set.
603 Changesets with no parent changeset in set.
604 """
604 """
605 s = getset(repo, subset, x)
605 s = getset(repo, subset, x)
606 cs = set(children(repo, subset, x))
606 cs = set(children(repo, subset, x))
607 return [r for r in s if r not in cs]
607 return [r for r in s if r not in cs]
608
608
609 def outgoing(repo, subset, x):
609 def outgoing(repo, subset, x):
610 """``outgoing([path])``
610 """``outgoing([path])``
611 Changesets not found in the specified destination repository, or the
611 Changesets not found in the specified destination repository, or the
612 default push location.
612 default push location.
613 """
613 """
614 import hg # avoid start-up nasties
614 import hg # avoid start-up nasties
615 # i18n: "outgoing" is a keyword
615 # i18n: "outgoing" is a keyword
616 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
616 l = getargs(x, 0, 1, _("outgoing requires a repository path"))
617 # i18n: "outgoing" is a keyword
617 # i18n: "outgoing" is a keyword
618 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
618 dest = l and getstring(l[0], _("outgoing requires a repository path")) or ''
619 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
619 dest = repo.ui.expandpath(dest or 'default-push', dest or 'default')
620 dest, branches = hg.parseurl(dest)
620 dest, branches = hg.parseurl(dest)
621 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
621 revs, checkout = hg.addbranchrevs(repo, repo, branches, [])
622 if revs:
622 if revs:
623 revs = [repo.lookup(rev) for rev in revs]
623 revs = [repo.lookup(rev) for rev in revs]
624 other = hg.repository(hg.remoteui(repo, {}), dest)
624 other = hg.repository(hg.remoteui(repo, {}), dest)
625 repo.ui.pushbuffer()
625 repo.ui.pushbuffer()
626 o = discovery.findoutgoing(repo, other)
626 o = discovery.findoutgoing(repo, other)
627 repo.ui.popbuffer()
627 repo.ui.popbuffer()
628 cl = repo.changelog
628 cl = repo.changelog
629 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
629 o = set([cl.rev(r) for r in repo.changelog.nodesbetween(o, revs)[0]])
630 return [r for r in subset if r in o]
630 return [r for r in subset if r in o]
631
631
632 def tag(repo, subset, x):
632 def tag(repo, subset, x):
633 """``tag(name)``
633 """``tag(name)``
634 The specified tag by name, or all tagged revisions if no name is given.
634 The specified tag by name, or all tagged revisions if no name is given.
635 """
635 """
636 # i18n: "tag" is a keyword
636 # i18n: "tag" is a keyword
637 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
637 args = getargs(x, 0, 1, _("tag takes one or no arguments"))
638 cl = repo.changelog
638 cl = repo.changelog
639 if args:
639 if args:
640 tn = getstring(args[0],
640 tn = getstring(args[0],
641 # i18n: "tag" is a keyword
641 # i18n: "tag" is a keyword
642 _('the argument to tag must be a string'))
642 _('the argument to tag must be a string'))
643 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
643 s = set([cl.rev(n) for t, n in repo.tagslist() if t == tn])
644 else:
644 else:
645 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
645 s = set([cl.rev(n) for t, n in repo.tagslist() if t != 'tip'])
646 return [r for r in subset if r in s]
646 return [r for r in subset if r in s]
647
647
648 def tagged(repo, subset, x):
648 def tagged(repo, subset, x):
649 return tag(repo, subset, x)
649 return tag(repo, subset, x)
650
650
651 symbols = {
651 symbols = {
652 "adds": adds,
652 "adds": adds,
653 "all": getall,
653 "all": getall,
654 "ancestor": ancestor,
654 "ancestor": ancestor,
655 "ancestors": ancestors,
655 "ancestors": ancestors,
656 "author": author,
656 "author": author,
657 "branch": branch,
657 "branch": branch,
658 "children": children,
658 "children": children,
659 "closed": closed,
659 "closed": closed,
660 "contains": contains,
660 "contains": contains,
661 "date": date,
661 "date": date,
662 "descendants": descendants,
662 "descendants": descendants,
663 "file": hasfile,
663 "file": hasfile,
664 "follow": follow,
664 "follow": follow,
665 "grep": grep,
665 "grep": grep,
666 "head": head,
666 "head": head,
667 "heads": heads,
667 "heads": heads,
668 "keyword": keyword,
668 "keyword": keyword,
669 "limit": limit,
669 "limit": limit,
670 "max": maxrev,
670 "max": maxrev,
671 "min": minrev,
671 "min": minrev,
672 "merge": merge,
672 "merge": merge,
673 "modifies": modifies,
673 "modifies": modifies,
674 "id": node,
674 "id": node,
675 "outgoing": outgoing,
675 "outgoing": outgoing,
676 "p1": p1,
676 "p1": p1,
677 "p2": p2,
677 "p2": p2,
678 "parents": parents,
678 "parents": parents,
679 "present": present,
679 "present": present,
680 "removes": removes,
680 "removes": removes,
681 "reverse": reverse,
681 "reverse": reverse,
682 "rev": rev,
682 "rev": rev,
683 "roots": roots,
683 "roots": roots,
684 "sort": sort,
684 "sort": sort,
685 "tag": tag,
685 "tag": tag,
686 "tagged": tagged,
686 "tagged": tagged,
687 "user": user,
687 "user": user,
688 }
688 }
689
689
690 methods = {
690 methods = {
691 "range": rangeset,
691 "range": rangeset,
692 "string": stringset,
692 "string": stringset,
693 "symbol": symbolset,
693 "symbol": symbolset,
694 "and": andset,
694 "and": andset,
695 "or": orset,
695 "or": orset,
696 "not": notset,
696 "not": notset,
697 "list": listset,
697 "list": listset,
698 "func": func,
698 "func": func,
699 }
699 }
700
700
701 def optimize(x, small):
701 def optimize(x, small):
702 if x == None:
702 if x == None:
703 return 0, x
703 return 0, x
704
704
705 smallbonus = 1
705 smallbonus = 1
706 if small:
706 if small:
707 smallbonus = .5
707 smallbonus = .5
708
708
709 op = x[0]
709 op = x[0]
710 if op == 'minus':
710 if op == 'minus':
711 return optimize(('and', x[1], ('not', x[2])), small)
711 return optimize(('and', x[1], ('not', x[2])), small)
712 elif op == 'dagrange':
712 elif op == 'dagrange':
713 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
713 return optimize(('and', ('func', ('symbol', 'descendants'), x[1]),
714 ('func', ('symbol', 'ancestors'), x[2])), small)
714 ('func', ('symbol', 'ancestors'), x[2])), small)
715 elif op == 'dagrangepre':
715 elif op == 'dagrangepre':
716 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
716 return optimize(('func', ('symbol', 'ancestors'), x[1]), small)
717 elif op == 'dagrangepost':
717 elif op == 'dagrangepost':
718 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
718 return optimize(('func', ('symbol', 'descendants'), x[1]), small)
719 elif op == 'rangepre':
719 elif op == 'rangepre':
720 return optimize(('range', ('string', '0'), x[1]), small)
720 return optimize(('range', ('string', '0'), x[1]), small)
721 elif op == 'rangepost':
721 elif op == 'rangepost':
722 return optimize(('range', x[1], ('string', 'tip')), small)
722 return optimize(('range', x[1], ('string', 'tip')), small)
723 elif op == 'negate':
723 elif op == 'negate':
724 return optimize(('string',
724 return optimize(('string',
725 '-' + getstring(x[1], _("can't negate that"))), small)
725 '-' + getstring(x[1], _("can't negate that"))), small)
726 elif op in 'string symbol negate':
726 elif op in 'string symbol negate':
727 return smallbonus, x # single revisions are small
727 return smallbonus, x # single revisions are small
728 elif op == 'and' or op == 'dagrange':
728 elif op == 'and' or op == 'dagrange':
729 wa, ta = optimize(x[1], True)
729 wa, ta = optimize(x[1], True)
730 wb, tb = optimize(x[2], True)
730 wb, tb = optimize(x[2], True)
731 w = min(wa, wb)
731 w = min(wa, wb)
732 if wa > wb:
732 if wa > wb:
733 return w, (op, tb, ta)
733 return w, (op, tb, ta)
734 return w, (op, ta, tb)
734 return w, (op, ta, tb)
735 elif op == 'or':
735 elif op == 'or':
736 wa, ta = optimize(x[1], False)
736 wa, ta = optimize(x[1], False)
737 wb, tb = optimize(x[2], False)
737 wb, tb = optimize(x[2], False)
738 if wb < wa:
738 if wb < wa:
739 wb, wa = wa, wb
739 wb, wa = wa, wb
740 return max(wa, wb), (op, ta, tb)
740 return max(wa, wb), (op, ta, tb)
741 elif op == 'not':
741 elif op == 'not':
742 o = optimize(x[1], not small)
742 o = optimize(x[1], not small)
743 return o[0], (op, o[1])
743 return o[0], (op, o[1])
744 elif op == 'group':
744 elif op == 'group':
745 return optimize(x[1], small)
745 return optimize(x[1], small)
746 elif op in 'range list':
746 elif op in 'range list':
747 wa, ta = optimize(x[1], small)
747 wa, ta = optimize(x[1], small)
748 wb, tb = optimize(x[2], small)
748 wb, tb = optimize(x[2], small)
749 return wa + wb, (op, ta, tb)
749 return wa + wb, (op, ta, tb)
750 elif op == 'func':
750 elif op == 'func':
751 f = getstring(x[1], _("not a symbol"))
751 f = getstring(x[1], _("not a symbol"))
752 wa, ta = optimize(x[2], small)
752 wa, ta = optimize(x[2], small)
753 if f in "grep date user author keyword branch file outgoing":
753 if f in "grep date user author keyword branch file outgoing":
754 w = 10 # slow
754 w = 10 # slow
755 elif f in "modifies adds removes":
755 elif f in "modifies adds removes":
756 w = 30 # slower
756 w = 30 # slower
757 elif f == "contains":
757 elif f == "contains":
758 w = 100 # very slow
758 w = 100 # very slow
759 elif f == "ancestor":
759 elif f == "ancestor":
760 w = 1 * smallbonus
760 w = 1 * smallbonus
761 elif f == "reverse limit":
761 elif f == "reverse limit":
762 w = 0
762 w = 0
763 elif f in "sort":
763 elif f in "sort":
764 w = 10 # assume most sorts look at changelog
764 w = 10 # assume most sorts look at changelog
765 else:
765 else:
766 w = 1
766 w = 1
767 return w + wa, (op, x[1], ta)
767 return w + wa, (op, x[1], ta)
768 return 1, x
768 return 1, x
769
769
770 parse = parser.parser(tokenize, elements).parse
770 parse = parser.parser(tokenize, elements).parse
771
771
772 def match(spec):
772 def match(spec):
773 if not spec:
773 if not spec:
774 raise error.ParseError(_("empty query"))
774 raise error.ParseError(_("empty query"))
775 tree = parse(spec)
775 tree = parse(spec)
776 weight, tree = optimize(tree, True)
776 weight, tree = optimize(tree, True)
777 def mfunc(repo, subset):
777 def mfunc(repo, subset):
778 return getset(repo, subset, tree)
778 return getset(repo, subset, tree)
779 return mfunc
779 return mfunc
780
780
781 def makedoc(topic, doc):
781 def makedoc(topic, doc):
782 """Generate and include predicates help in revsets topic."""
782 """Generate and include predicates help in revsets topic."""
783 predicates = []
783 predicates = []
784 for name in sorted(symbols):
784 for name in sorted(symbols):
785 text = symbols[name].__doc__
785 text = symbols[name].__doc__
786 if not text:
786 if not text:
787 continue
787 continue
788 lines = text.splitlines()
788 lines = text.splitlines()
789 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
789 lines[1:] = [(' ' + l.strip()) for l in lines[1:]]
790 predicates.append('\n'.join(lines))
790 predicates.append('\n'.join(lines))
791 predicates = '\n'.join(predicates)
791 predicates = '\n'.join(predicates)
792 doc = doc.replace('.. predicatesmarker', predicates)
792 doc = doc.replace('.. predicatesmarker', predicates)
793 return doc
793 return doc
794
795 # tell hggettext to extract docstrings from these functions:
796 i18nfunctions = symbols.values()
General Comments 0
You need to be logged in to leave comments. Login now