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