##// END OF EJS Templates
bookmarks: move parse() and current() into property definitions
Nicolas Dumazet -
r10109:be041d67 default
parent child Browse files
Show More
@@ -1,335 +1,327 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
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. hg
15 It is possible to use bookmark names in every revision lookup (e.g. hg
16 merge, hg update).
16 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 .hgrc::
21 your .hgrc::
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, hex, short
32 from mercurial.node import nullid, nullrev, hex, short
33 from mercurial import util, commands, localrepo, repair, extensions
33 from mercurial import util, commands, localrepo, repair, extensions
34 import os
34 import os
35
35
36 def parse(repo):
37 '''Parse .hg/bookmarks file and return a dictionary
38
39 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
40 in the .hg/bookmarks file. They are read by the parse() method and
41 returned as a dictionary with name => hash values.
42
43 The parsed dictionary is cached until a write() operation is done.
44 '''
45 try:
46 bookmarks = {}
47 for line in repo.opener('bookmarks'):
48 sha, refspec = line.strip().split(' ', 1)
49 bookmarks[refspec] = repo.lookup(sha)
50 except:
51 pass
52 return bookmarks
53
54 def write(repo):
36 def write(repo):
55 '''Write bookmarks
37 '''Write bookmarks
56
38
57 Write the given bookmark => hash dictionary to the .hg/bookmarks file
39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
58 in a format equal to those of localtags.
40 in a format equal to those of localtags.
59
41
60 We also store a backup of the previous state in undo.bookmarks that
42 We also store a backup of the previous state in undo.bookmarks that
61 can be copied back on rollback.
43 can be copied back on rollback.
62 '''
44 '''
63 refs = repo._bookmarks
45 refs = repo._bookmarks
64 if os.path.exists(repo.join('bookmarks')):
46 if os.path.exists(repo.join('bookmarks')):
65 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
66 if repo._bookmarkcurrent not in refs:
48 if repo._bookmarkcurrent not in refs:
67 setcurrent(repo, None)
49 setcurrent(repo, None)
68 wlock = repo.wlock()
50 wlock = repo.wlock()
69 try:
51 try:
70 file = repo.opener('bookmarks', 'w', atomictemp=True)
52 file = repo.opener('bookmarks', 'w', atomictemp=True)
71 for refspec, node in refs.iteritems():
53 for refspec, node in refs.iteritems():
72 file.write("%s %s\n" % (hex(node), refspec))
54 file.write("%s %s\n" % (hex(node), refspec))
73 file.rename()
55 file.rename()
74 finally:
56 finally:
75 wlock.release()
57 wlock.release()
76
58
77 def current(repo):
78 '''Get the current bookmark
79
80 If we use gittishsh branches we have a current bookmark that
81 we are on. This function returns the name of the bookmark. It
82 is stored in .hg/bookmarks.current
83 '''
84 mark = None
85 if os.path.exists(repo.join('bookmarks.current')):
86 file = repo.opener('bookmarks.current')
87 # No readline() in posixfile_nt, reading everything is cheap
88 mark = (file.readlines() or [''])[0]
89 if mark == '':
90 mark = None
91 file.close()
92 return mark
93
94 def setcurrent(repo, mark):
59 def setcurrent(repo, mark):
95 '''Set the name of the bookmark that we are currently on
60 '''Set the name of the bookmark that we are currently on
96
61
97 Set the name of the bookmark that we are on (hg update <bookmark>).
62 Set the name of the bookmark that we are on (hg update <bookmark>).
98 The name is recorded in .hg/bookmarks.current
63 The name is recorded in .hg/bookmarks.current
99 '''
64 '''
100 current = repo._bookmarkcurrent
65 current = repo._bookmarkcurrent
101 if current == mark:
66 if current == mark:
102 return
67 return
103
68
104 refs = repo._bookmarks
69 refs = repo._bookmarks
105
70
106 # do not update if we do update to a rev equal to the current bookmark
71 # do not update if we do update to a rev equal to the current bookmark
107 if (mark and mark not in refs and
72 if (mark and mark not in refs and
108 current and refs[current] == repo.changectx('.').node()):
73 current and refs[current] == repo.changectx('.').node()):
109 return
74 return
110 if mark not in refs:
75 if mark not in refs:
111 mark = ''
76 mark = ''
112 wlock = repo.wlock()
77 wlock = repo.wlock()
113 try:
78 try:
114 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
115 file.write(mark)
80 file.write(mark)
116 file.rename()
81 file.rename()
117 finally:
82 finally:
118 wlock.release()
83 wlock.release()
119 repo._bookmarkcurrent = mark
84 repo._bookmarkcurrent = mark
120
85
121 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
122 '''track a line of development with movable markers
87 '''track a line of development with movable markers
123
88
124 Bookmarks are pointers to certain commits that move when
89 Bookmarks are pointers to certain commits that move when
125 committing. Bookmarks are local. They can be renamed, copied and
90 committing. Bookmarks are local. They can be renamed, copied and
126 deleted. It is possible to use bookmark names in 'hg merge' and
91 deleted. It is possible to use bookmark names in 'hg merge' and
127 'hg update' to merge and update respectively to a given bookmark.
92 'hg update' to merge and update respectively to a given bookmark.
128
93
129 You can use 'hg bookmark NAME' to set a bookmark on the working
94 You can use 'hg bookmark NAME' to set a bookmark on the working
130 directory's parent revision with the given name. If you specify
95 directory's parent revision with the given name. If you specify
131 a revision using -r REV (where REV may be an existing bookmark),
96 a revision using -r REV (where REV may be an existing bookmark),
132 the bookmark is assigned to that revision.
97 the bookmark is assigned to that revision.
133 '''
98 '''
134 hexfn = ui.debugflag and hex or short
99 hexfn = ui.debugflag and hex or short
135 marks = repo._bookmarks
100 marks = repo._bookmarks
136 cur = repo.changectx('.').node()
101 cur = repo.changectx('.').node()
137
102
138 if rename:
103 if rename:
139 if rename not in marks:
104 if rename not in marks:
140 raise util.Abort(_("a bookmark of this name does not exist"))
105 raise util.Abort(_("a bookmark of this name does not exist"))
141 if mark in marks and not force:
106 if mark in marks and not force:
142 raise util.Abort(_("a bookmark of the same name already exists"))
107 raise util.Abort(_("a bookmark of the same name already exists"))
143 if mark is None:
108 if mark is None:
144 raise util.Abort(_("new bookmark name required"))
109 raise util.Abort(_("new bookmark name required"))
145 marks[mark] = marks[rename]
110 marks[mark] = marks[rename]
146 del marks[rename]
111 del marks[rename]
147 if repo._bookmarkcurrent == rename:
112 if repo._bookmarkcurrent == rename:
148 setcurrent(repo, mark)
113 setcurrent(repo, mark)
149 write(repo)
114 write(repo)
150 return
115 return
151
116
152 if delete:
117 if delete:
153 if mark is None:
118 if mark is None:
154 raise util.Abort(_("bookmark name required"))
119 raise util.Abort(_("bookmark name required"))
155 if mark not in marks:
120 if mark not in marks:
156 raise util.Abort(_("a bookmark of this name does not exist"))
121 raise util.Abort(_("a bookmark of this name does not exist"))
157 if mark == repo._bookmarkcurrent:
122 if mark == repo._bookmarkcurrent:
158 setcurrent(repo, None)
123 setcurrent(repo, None)
159 del marks[mark]
124 del marks[mark]
160 write(repo)
125 write(repo)
161 return
126 return
162
127
163 if mark != None:
128 if mark != None:
164 if "\n" in mark:
129 if "\n" in mark:
165 raise util.Abort(_("bookmark name cannot contain newlines"))
130 raise util.Abort(_("bookmark name cannot contain newlines"))
166 mark = mark.strip()
131 mark = mark.strip()
167 if mark in marks and not force:
132 if mark in marks and not force:
168 raise util.Abort(_("a bookmark of the same name already exists"))
133 raise util.Abort(_("a bookmark of the same name already exists"))
169 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
170 and not force):
135 and not force):
171 raise util.Abort(
136 raise util.Abort(
172 _("a bookmark cannot have the name of an existing branch"))
137 _("a bookmark cannot have the name of an existing branch"))
173 if rev:
138 if rev:
174 marks[mark] = repo.lookup(rev)
139 marks[mark] = repo.lookup(rev)
175 else:
140 else:
176 marks[mark] = repo.changectx('.').node()
141 marks[mark] = repo.changectx('.').node()
177 setcurrent(repo, mark)
142 setcurrent(repo, mark)
178 write(repo)
143 write(repo)
179 return
144 return
180
145
181 if mark is None:
146 if mark is None:
182 if rev:
147 if rev:
183 raise util.Abort(_("bookmark name required"))
148 raise util.Abort(_("bookmark name required"))
184 if len(marks) == 0:
149 if len(marks) == 0:
185 ui.status("no bookmarks set\n")
150 ui.status("no bookmarks set\n")
186 else:
151 else:
187 for bmark, n in marks.iteritems():
152 for bmark, n in marks.iteritems():
188 if ui.configbool('bookmarks', 'track.current'):
153 if ui.configbool('bookmarks', 'track.current'):
189 current = repo._bookmarkcurrent
154 current = repo._bookmarkcurrent
190 prefix = (bmark == current and n == cur) and '*' or ' '
155 prefix = (bmark == current and n == cur) and '*' or ' '
191 else:
156 else:
192 prefix = (n == cur) and '*' or ' '
157 prefix = (n == cur) and '*' or ' '
193
158
194 if ui.quiet:
159 if ui.quiet:
195 ui.write("%s\n" % bmark)
160 ui.write("%s\n" % bmark)
196 else:
161 else:
197 ui.write(" %s %-25s %d:%s\n" % (
162 ui.write(" %s %-25s %d:%s\n" % (
198 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
163 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
199 return
164 return
200
165
201 def _revstostrip(changelog, node):
166 def _revstostrip(changelog, node):
202 srev = changelog.rev(node)
167 srev = changelog.rev(node)
203 tostrip = [srev]
168 tostrip = [srev]
204 saveheads = []
169 saveheads = []
205 for r in xrange(srev, len(changelog)):
170 for r in xrange(srev, len(changelog)):
206 parents = changelog.parentrevs(r)
171 parents = changelog.parentrevs(r)
207 if parents[0] in tostrip or parents[1] in tostrip:
172 if parents[0] in tostrip or parents[1] in tostrip:
208 tostrip.append(r)
173 tostrip.append(r)
209 if parents[1] != nullrev:
174 if parents[1] != nullrev:
210 for p in parents:
175 for p in parents:
211 if p not in tostrip and p > srev:
176 if p not in tostrip and p > srev:
212 saveheads.append(p)
177 saveheads.append(p)
213 return [r for r in tostrip if r not in saveheads]
178 return [r for r in tostrip if r not in saveheads]
214
179
215 def strip(oldstrip, ui, repo, node, backup="all"):
180 def strip(oldstrip, ui, repo, node, backup="all"):
216 """Strip bookmarks if revisions are stripped using
181 """Strip bookmarks if revisions are stripped using
217 the mercurial.strip method. This usually happens during
182 the mercurial.strip method. This usually happens during
218 qpush and qpop"""
183 qpush and qpop"""
219 revisions = _revstostrip(repo.changelog, node)
184 revisions = _revstostrip(repo.changelog, node)
220 marks = repo._bookmarks
185 marks = repo._bookmarks
221 update = []
186 update = []
222 for mark, n in marks.iteritems():
187 for mark, n in marks.iteritems():
223 if repo.changelog.rev(n) in revisions:
188 if repo.changelog.rev(n) in revisions:
224 update.append(mark)
189 update.append(mark)
225 oldstrip(ui, repo, node, backup)
190 oldstrip(ui, repo, node, backup)
226 if len(update) > 0:
191 if len(update) > 0:
227 for m in update:
192 for m in update:
228 marks[m] = repo.changectx('.').node()
193 marks[m] = repo.changectx('.').node()
229 write(repo)
194 write(repo)
230
195
231 def reposetup(ui, repo):
196 def reposetup(ui, repo):
232 if not repo.local():
197 if not repo.local():
233 return
198 return
234
199
235 class bookmark_repo(repo.__class__):
200 class bookmark_repo(repo.__class__):
236
201
237 @util.propertycache
202 @util.propertycache
238 def _bookmarks(self):
203 def _bookmarks(self):
239 return parse(self)
204 '''Parse .hg/bookmarks file and return a dictionary
205
206 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
207 in the .hg/bookmarks file. They are read returned as a dictionary
208 with name => hash values.
209 '''
210 try:
211 bookmarks = {}
212 for line in self.opener('bookmarks'):
213 sha, refspec = line.strip().split(' ', 1)
214 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
215 except:
216 pass
217 return bookmarks
240
218
241 @util.propertycache
219 @util.propertycache
242 def _bookmarkcurrent(self):
220 def _bookmarkcurrent(self):
243 return current(self)
221 '''Get the current bookmark
222
223 If we use gittishsh branches we have a current bookmark that
224 we are on. This function returns the name of the bookmark. It
225 is stored in .hg/bookmarks.current
226 '''
227 mark = None
228 if os.path.exists(self.join('bookmarks.current')):
229 file = self.opener('bookmarks.current')
230 # No readline() in posixfile_nt, reading everything is cheap
231 mark = (file.readlines() or [''])[0]
232 if mark == '':
233 mark = None
234 file.close()
235 return mark
244
236
245 def rollback(self):
237 def rollback(self):
246 if os.path.exists(self.join('undo.bookmarks')):
238 if os.path.exists(self.join('undo.bookmarks')):
247 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
239 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
248 return super(bookmark_repo, self).rollback()
240 return super(bookmark_repo, self).rollback()
249
241
250 def lookup(self, key):
242 def lookup(self, key):
251 if key in self._bookmarks:
243 if key in self._bookmarks:
252 key = self._bookmarks[key]
244 key = self._bookmarks[key]
253 return super(bookmark_repo, self).lookup(key)
245 return super(bookmark_repo, self).lookup(key)
254
246
255 def _bookmarksupdate(self, parents, node):
247 def _bookmarksupdate(self, parents, node):
256 marks = self._bookmarks
248 marks = self._bookmarks
257 update = False
249 update = False
258 if ui.configbool('bookmarks', 'track.current'):
250 if ui.configbool('bookmarks', 'track.current'):
259 mark = self._bookmarkcurrent
251 mark = self._bookmarkcurrent
260 if mark and marks[mark] in parents:
252 if mark and marks[mark] in parents:
261 marks[mark] = node
253 marks[mark] = node
262 update = True
254 update = True
263 else:
255 else:
264 for mark, n in marks.items():
256 for mark, n in marks.items():
265 if n in parents:
257 if n in parents:
266 marks[mark] = node
258 marks[mark] = node
267 update = True
259 update = True
268 if update:
260 if update:
269 write(self)
261 write(self)
270
262
271 def commitctx(self, ctx, error=False):
263 def commitctx(self, ctx, error=False):
272 """Add a revision to the repository and
264 """Add a revision to the repository and
273 move the bookmark"""
265 move the bookmark"""
274 wlock = self.wlock() # do both commit and bookmark with lock held
266 wlock = self.wlock() # do both commit and bookmark with lock held
275 try:
267 try:
276 node = super(bookmark_repo, self).commitctx(ctx, error)
268 node = super(bookmark_repo, self).commitctx(ctx, error)
277 if node is None:
269 if node is None:
278 return None
270 return None
279 parents = self.changelog.parents(node)
271 parents = self.changelog.parents(node)
280 if parents[1] == nullid:
272 if parents[1] == nullid:
281 parents = (parents[0],)
273 parents = (parents[0],)
282
274
283 self._bookmarksupdate(parents, node)
275 self._bookmarksupdate(parents, node)
284 return node
276 return node
285 finally:
277 finally:
286 wlock.release()
278 wlock.release()
287
279
288 def addchangegroup(self, source, srctype, url, emptyok=False):
280 def addchangegroup(self, source, srctype, url, emptyok=False):
289 parents = self.dirstate.parents()
281 parents = self.dirstate.parents()
290
282
291 result = super(bookmark_repo, self).addchangegroup(
283 result = super(bookmark_repo, self).addchangegroup(
292 source, srctype, url, emptyok)
284 source, srctype, url, emptyok)
293 if result > 1:
285 if result > 1:
294 # We have more heads than before
286 # We have more heads than before
295 return result
287 return result
296 node = self.changelog.tip()
288 node = self.changelog.tip()
297
289
298 self._bookmarksupdate(parents, node)
290 self._bookmarksupdate(parents, node)
299 return result
291 return result
300
292
301 def _findtags(self):
293 def _findtags(self):
302 """Merge bookmarks with normal tags"""
294 """Merge bookmarks with normal tags"""
303 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
295 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
304 tags.update(self._bookmarks)
296 tags.update(self._bookmarks)
305 return (tags, tagtypes)
297 return (tags, tagtypes)
306
298
307 repo.__class__ = bookmark_repo
299 repo.__class__ = bookmark_repo
308
300
309 def uisetup(ui):
301 def uisetup(ui):
310 extensions.wrapfunction(repair, "strip", strip)
302 extensions.wrapfunction(repair, "strip", strip)
311 if ui.configbool('bookmarks', 'track.current'):
303 if ui.configbool('bookmarks', 'track.current'):
312 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
304 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
313
305
314 def updatecurbookmark(orig, ui, repo, *args, **opts):
306 def updatecurbookmark(orig, ui, repo, *args, **opts):
315 '''Set the current bookmark
307 '''Set the current bookmark
316
308
317 If the user updates to a bookmark we update the .hg/bookmarks.current
309 If the user updates to a bookmark we update the .hg/bookmarks.current
318 file.
310 file.
319 '''
311 '''
320 res = orig(ui, repo, *args, **opts)
312 res = orig(ui, repo, *args, **opts)
321 rev = opts['rev']
313 rev = opts['rev']
322 if not rev and len(args) > 0:
314 if not rev and len(args) > 0:
323 rev = args[0]
315 rev = args[0]
324 setcurrent(repo, rev)
316 setcurrent(repo, rev)
325 return res
317 return res
326
318
327 cmdtable = {
319 cmdtable = {
328 "bookmarks":
320 "bookmarks":
329 (bookmark,
321 (bookmark,
330 [('f', 'force', False, _('force')),
322 [('f', 'force', False, _('force')),
331 ('r', 'rev', '', _('revision')),
323 ('r', 'rev', '', _('revision')),
332 ('d', 'delete', False, _('delete a given bookmark')),
324 ('d', 'delete', False, _('delete a given bookmark')),
333 ('m', 'rename', '', _('rename a given bookmark'))],
325 ('m', 'rename', '', _('rename a given bookmark'))],
334 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
326 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
335 }
327 }
General Comments 0
You need to be logged in to leave comments. Login now