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