##// END OF EJS Templates
bookmarks: add support for pull --bookmark to import remote bookmarks
Matt Mackall -
r11378:cb21fb1b default
parent child Browse files
Show More
@@ -1,415 +1,446 b''
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your .hgrc::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, hex, short
33 from mercurial import util, commands, repair, extensions, pushkey
33 from mercurial import util, commands, repair, extensions, pushkey, hg
34 34 import os
35 35
36 36 def write(repo):
37 37 '''Write bookmarks
38 38
39 39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 40 in a format equal to those of localtags.
41 41
42 42 We also store a backup of the previous state in undo.bookmarks that
43 43 can be copied back on rollback.
44 44 '''
45 45 refs = repo._bookmarks
46 46 if os.path.exists(repo.join('bookmarks')):
47 47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 48 if repo._bookmarkcurrent not in refs:
49 49 setcurrent(repo, None)
50 50 wlock = repo.wlock()
51 51 try:
52 52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 53 for refspec, node in refs.iteritems():
54 54 file.write("%s %s\n" % (hex(node), refspec))
55 55 file.rename()
56 56 finally:
57 57 wlock.release()
58 58
59 59 def setcurrent(repo, mark):
60 60 '''Set the name of the bookmark that we are currently on
61 61
62 62 Set the name of the bookmark that we are on (hg update <bookmark>).
63 63 The name is recorded in .hg/bookmarks.current
64 64 '''
65 65 current = repo._bookmarkcurrent
66 66 if current == mark:
67 67 return
68 68
69 69 refs = repo._bookmarks
70 70
71 71 # do not update if we do update to a rev equal to the current bookmark
72 72 if (mark and mark not in refs and
73 73 current and refs[current] == repo.changectx('.').node()):
74 74 return
75 75 if mark not in refs:
76 76 mark = ''
77 77 wlock = repo.wlock()
78 78 try:
79 79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
80 80 file.write(mark)
81 81 file.rename()
82 82 finally:
83 83 wlock.release()
84 84 repo._bookmarkcurrent = mark
85 85
86 86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
87 87 '''track a line of development with movable markers
88 88
89 89 Bookmarks are pointers to certain commits that move when
90 90 committing. Bookmarks are local. They can be renamed, copied and
91 91 deleted. It is possible to use bookmark names in :hg:`merge` and
92 92 :hg:`update` to merge and update respectively to a given bookmark.
93 93
94 94 You can use :hg:`bookmark NAME` to set a bookmark on the working
95 95 directory's parent revision with the given name. If you specify
96 96 a revision using -r REV (where REV may be an existing bookmark),
97 97 the bookmark is assigned to that revision.
98 98 '''
99 99 hexfn = ui.debugflag and hex or short
100 100 marks = repo._bookmarks
101 101 cur = repo.changectx('.').node()
102 102
103 103 if rename:
104 104 if rename not in marks:
105 105 raise util.Abort(_("a bookmark of this name does not exist"))
106 106 if mark in marks and not force:
107 107 raise util.Abort(_("a bookmark of the same name already exists"))
108 108 if mark is None:
109 109 raise util.Abort(_("new bookmark name required"))
110 110 marks[mark] = marks[rename]
111 111 del marks[rename]
112 112 if repo._bookmarkcurrent == rename:
113 113 setcurrent(repo, mark)
114 114 write(repo)
115 115 return
116 116
117 117 if delete:
118 118 if mark is None:
119 119 raise util.Abort(_("bookmark name required"))
120 120 if mark not in marks:
121 121 raise util.Abort(_("a bookmark of this name does not exist"))
122 122 if mark == repo._bookmarkcurrent:
123 123 setcurrent(repo, None)
124 124 del marks[mark]
125 125 write(repo)
126 126 return
127 127
128 128 if mark != None:
129 129 if "\n" in mark:
130 130 raise util.Abort(_("bookmark name cannot contain newlines"))
131 131 mark = mark.strip()
132 132 if mark in marks and not force:
133 133 raise util.Abort(_("a bookmark of the same name already exists"))
134 134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
135 135 and not force):
136 136 raise util.Abort(
137 137 _("a bookmark cannot have the name of an existing branch"))
138 138 if rev:
139 139 marks[mark] = repo.lookup(rev)
140 140 else:
141 141 marks[mark] = repo.changectx('.').node()
142 142 setcurrent(repo, mark)
143 143 write(repo)
144 144 return
145 145
146 146 if mark is None:
147 147 if rev:
148 148 raise util.Abort(_("bookmark name required"))
149 149 if len(marks) == 0:
150 150 ui.status(_("no bookmarks set\n"))
151 151 else:
152 152 for bmark, n in marks.iteritems():
153 153 if ui.configbool('bookmarks', 'track.current'):
154 154 current = repo._bookmarkcurrent
155 155 if bmark == current and n == cur:
156 156 prefix, label = '*', 'bookmarks.current'
157 157 else:
158 158 prefix, label = ' ', ''
159 159 else:
160 160 if n == cur:
161 161 prefix, label = '*', 'bookmarks.current'
162 162 else:
163 163 prefix, label = ' ', ''
164 164
165 165 if ui.quiet:
166 166 ui.write("%s\n" % bmark, label=label)
167 167 else:
168 168 ui.write(" %s %-25s %d:%s\n" % (
169 169 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
170 170 label=label)
171 171 return
172 172
173 173 def _revstostrip(changelog, node):
174 174 srev = changelog.rev(node)
175 175 tostrip = [srev]
176 176 saveheads = []
177 177 for r in xrange(srev, len(changelog)):
178 178 parents = changelog.parentrevs(r)
179 179 if parents[0] in tostrip or parents[1] in tostrip:
180 180 tostrip.append(r)
181 181 if parents[1] != nullrev:
182 182 for p in parents:
183 183 if p not in tostrip and p > srev:
184 184 saveheads.append(p)
185 185 return [r for r in tostrip if r not in saveheads]
186 186
187 187 def strip(oldstrip, ui, repo, node, backup="all"):
188 188 """Strip bookmarks if revisions are stripped using
189 189 the mercurial.strip method. This usually happens during
190 190 qpush and qpop"""
191 191 revisions = _revstostrip(repo.changelog, node)
192 192 marks = repo._bookmarks
193 193 update = []
194 194 for mark, n in marks.iteritems():
195 195 if repo.changelog.rev(n) in revisions:
196 196 update.append(mark)
197 197 oldstrip(ui, repo, node, backup)
198 198 if len(update) > 0:
199 199 for m in update:
200 200 marks[m] = repo.changectx('.').node()
201 201 write(repo)
202 202
203 203 def reposetup(ui, repo):
204 204 if not repo.local():
205 205 return
206 206
207 207 class bookmark_repo(repo.__class__):
208 208
209 209 @util.propertycache
210 210 def _bookmarks(self):
211 211 '''Parse .hg/bookmarks file and return a dictionary
212 212
213 213 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
214 214 in the .hg/bookmarks file. They are read returned as a dictionary
215 215 with name => hash values.
216 216 '''
217 217 try:
218 218 bookmarks = {}
219 219 for line in self.opener('bookmarks'):
220 220 sha, refspec = line.strip().split(' ', 1)
221 221 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
222 222 except:
223 223 pass
224 224 return bookmarks
225 225
226 226 @util.propertycache
227 227 def _bookmarkcurrent(self):
228 228 '''Get the current bookmark
229 229
230 230 If we use gittishsh branches we have a current bookmark that
231 231 we are on. This function returns the name of the bookmark. It
232 232 is stored in .hg/bookmarks.current
233 233 '''
234 234 mark = None
235 235 if os.path.exists(self.join('bookmarks.current')):
236 236 file = self.opener('bookmarks.current')
237 237 # No readline() in posixfile_nt, reading everything is cheap
238 238 mark = (file.readlines() or [''])[0]
239 239 if mark == '':
240 240 mark = None
241 241 file.close()
242 242 return mark
243 243
244 244 def rollback(self, *args):
245 245 if os.path.exists(self.join('undo.bookmarks')):
246 246 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
247 247 return super(bookmark_repo, self).rollback(*args)
248 248
249 249 def lookup(self, key):
250 250 if key in self._bookmarks:
251 251 key = self._bookmarks[key]
252 252 return super(bookmark_repo, self).lookup(key)
253 253
254 254 def _bookmarksupdate(self, parents, node):
255 255 marks = self._bookmarks
256 256 update = False
257 257 if ui.configbool('bookmarks', 'track.current'):
258 258 mark = self._bookmarkcurrent
259 259 if mark and marks[mark] in parents:
260 260 marks[mark] = node
261 261 update = True
262 262 else:
263 263 for mark, n in marks.items():
264 264 if n in parents:
265 265 marks[mark] = node
266 266 update = True
267 267 if update:
268 268 write(self)
269 269
270 270 def commitctx(self, ctx, error=False):
271 271 """Add a revision to the repository and
272 272 move the bookmark"""
273 273 wlock = self.wlock() # do both commit and bookmark with lock held
274 274 try:
275 275 node = super(bookmark_repo, self).commitctx(ctx, error)
276 276 if node is None:
277 277 return None
278 278 parents = self.changelog.parents(node)
279 279 if parents[1] == nullid:
280 280 parents = (parents[0],)
281 281
282 282 self._bookmarksupdate(parents, node)
283 283 return node
284 284 finally:
285 285 wlock.release()
286 286
287 287 def pull(self, remote, heads=None, force=False):
288 288 result = super(bookmark_repo, self).pull(remote, heads, force)
289 289
290 290 self.ui.debug("checking for updated bookmarks\n")
291 291 rb = remote.listkeys('bookmarks')
292 292 changes = 0
293 293 for k in rb.keys():
294 294 if k in self._bookmarks:
295 295 nr, nl = rb[k], self._bookmarks[k]
296 296 if nr in self:
297 297 cr = self[nr]
298 298 cl = self[nl]
299 299 if cl.rev() >= cr.rev():
300 300 continue
301 301 if cr in cl.descendants():
302 302 self._bookmarks[k] = cr.node()
303 303 changes += 1
304 304 self.ui.status(_("updating bookmark %s\n") % k)
305 305 else:
306 306 self.ui.warn(_("not updating divergent"
307 307 " bookmark %s\n") % k)
308 308 if changes:
309 309 write(repo)
310 310
311 311 return result
312 312
313 313 def push(self, remote, force=False, revs=None, newbranch=False):
314 314 result = super(bookmark_repo, self).push(remote, force, revs,
315 315 newbranch)
316 316
317 317 self.ui.debug("checking for updated bookmarks\n")
318 318 rb = remote.listkeys('bookmarks')
319 319 for k in rb.keys():
320 320 if k in self._bookmarks:
321 321 nr, nl = rb[k], self._bookmarks[k]
322 322 if nr in self:
323 323 cr = self[nr]
324 324 cl = self[nl]
325 325 if cl in cr.descendants():
326 326 r = remote.pushkey('bookmarks', k, nr, nl)
327 327 if r:
328 328 self.ui.status(_("updating bookmark %s\n") % k)
329 329 else:
330 330 self.ui.warn(_("failed to update bookmark"
331 331 " %s!\n") % k)
332 332
333 333 return result
334 334
335 335 def addchangegroup(self, source, srctype, url, emptyok=False):
336 336 parents = self.dirstate.parents()
337 337
338 338 result = super(bookmark_repo, self).addchangegroup(
339 339 source, srctype, url, emptyok)
340 340 if result > 1:
341 341 # We have more heads than before
342 342 return result
343 343 node = self.changelog.tip()
344 344
345 345 self._bookmarksupdate(parents, node)
346 346 return result
347 347
348 348 def _findtags(self):
349 349 """Merge bookmarks with normal tags"""
350 350 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
351 351 tags.update(self._bookmarks)
352 352 return (tags, tagtypes)
353 353
354 354 if hasattr(repo, 'invalidate'):
355 355 def invalidate(self):
356 356 super(bookmark_repo, self).invalidate()
357 357 for attr in ('_bookmarks', '_bookmarkcurrent'):
358 358 if attr in self.__dict__:
359 359 delattr(self, attr)
360 360
361 361 repo.__class__ = bookmark_repo
362 362
363 363 def listbookmarks(repo):
364 364 d = {}
365 365 for k, v in repo._bookmarks.iteritems():
366 366 d[k] = hex(v)
367 367 return d
368 368
369 369 def pushbookmark(repo, key, old, new):
370 370 w = repo.wlock()
371 371 try:
372 372 marks = repo._bookmarks
373 373 if hex(marks.get(key, '')) != old:
374 374 return False
375 375 if new == '':
376 376 del marks[key]
377 377 else:
378 378 if new not in repo:
379 379 return False
380 380 marks[key] = repo[new].node()
381 381 write(repo)
382 382 return True
383 383 finally:
384 384 w.release()
385 385
386 def pull(oldpull, ui, repo, source="default", **opts):
387 # translate bookmark args to rev args for actual pull
388 if opts.get('bookmark'):
389 # this is an unpleasant hack as pull will do this internally
390 source, branches = hg.parseurl(ui.expandpath(source),
391 opts.get('branch'))
392 other = hg.repository(hg.remoteui(repo, opts), source)
393 rb = other.listkeys('bookmarks')
394
395 for b in opts['bookmark']:
396 if b not in rb:
397 raise util.Abort(_('remote bookmark %s not found!') % b)
398 opts.setdefault('rev', []).append(b)
399
400 result = oldpull(ui, repo, source, **opts)
401
402 # update specified bookmarks
403 if opts.get('bookmark'):
404 for b in opts['bookmark']:
405 # explicit pull overrides local bookmark if any
406 ui.status(_("importing bookmark %s\n") % b)
407 repo._bookmarks[b] = repo[rb[b]].node()
408 write(repo)
409
410 return result
411
386 412 def uisetup(ui):
387 413 extensions.wrapfunction(repair, "strip", strip)
388 414 if ui.configbool('bookmarks', 'track.current'):
389 415 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
416
417 entry = extensions.wrapcommand(commands.table, 'pull', pull)
418 entry[1].append(('B', 'bookmark', [],
419 _("bookmark to import")))
420
390 421 pushkey.register('bookmarks', pushbookmark, listbookmarks)
391 422
392 423 def updatecurbookmark(orig, ui, repo, *args, **opts):
393 424 '''Set the current bookmark
394 425
395 426 If the user updates to a bookmark we update the .hg/bookmarks.current
396 427 file.
397 428 '''
398 429 res = orig(ui, repo, *args, **opts)
399 430 rev = opts['rev']
400 431 if not rev and len(args) > 0:
401 432 rev = args[0]
402 433 setcurrent(repo, rev)
403 434 return res
404 435
405 436 cmdtable = {
406 437 "bookmarks":
407 438 (bookmark,
408 439 [('f', 'force', False, _('force')),
409 440 ('r', 'rev', '', _('revision'), _('REV')),
410 441 ('d', 'delete', False, _('delete a given bookmark')),
411 442 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
412 443 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
413 444 }
414 445
415 446 colortable = {'bookmarks.current': 'green'}
General Comments 0
You need to be logged in to leave comments. Login now