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