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