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