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