##// END OF EJS Templates
bookmarks: Adds support for a --non-bookmarked option to push
Stefan Rusek -
r7478:4c3e0ad5 default
parent child Browse files
Show More
@@ -1,231 +1,250
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
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 '''mercurial bookmarks
8 '''mercurial bookmarks
9
9
10 Mercurial bookmarks are local moveable pointers to changesets. Every
10 Mercurial bookmarks are local moveable pointers to changesets. Every
11 bookmark points to a changeset identified by its hash. If you commit a
11 bookmark 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 is forwarded to the new changeset.
13 bookmark is forwarded 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
18
19 from mercurial.commands import templateopts, hex, short
19 from mercurial.commands import templateopts, hex, short
20 from mercurial import extensions
20 from mercurial.i18n import _
21 from mercurial.i18n import _
21 from mercurial import cmdutil, util, commands, changelog
22 from mercurial import cmdutil, util, commands, changelog
22 from mercurial.node import nullid, nullrev
23 from mercurial.node import nullid, nullrev
23 from mercurial.repo import RepoError
24 from mercurial.repo import RepoError
24 import mercurial, mercurial.localrepo, mercurial.repair, os
25 import mercurial, mercurial.localrepo, mercurial.repair, os
25
26
26 def parse(repo):
27 def parse(repo):
27 '''Parse .hg/bookmarks file and return a dictionary
28 '''Parse .hg/bookmarks file and return a dictionary
28
29
29 Bookmarks are stored as {HASH}\s{NAME}\n (localtags format) values
30 Bookmarks are stored as {HASH}\s{NAME}\n (localtags format) values
30 in the .hg/bookmarks file. They are read by the parse() method and
31 in the .hg/bookmarks file. They are read by the parse() method and
31 returned as a dictionary with name => hash values.
32 returned as a dictionary with name => hash values.
32
33
33 The parsed dictionary is cached until a write() operation is done.
34 The parsed dictionary is cached until a write() operation is done.
34 '''
35 '''
35 try:
36 try:
36 if repo._bookmarks:
37 if repo._bookmarks:
37 return repo._bookmarks
38 return repo._bookmarks
38 repo._bookmarks = {}
39 repo._bookmarks = {}
39 for line in repo.opener('bookmarks'):
40 for line in repo.opener('bookmarks'):
40 sha, refspec = line.strip().split(' ', 1)
41 sha, refspec = line.strip().split(' ', 1)
41 repo._bookmarks[refspec] = repo.lookup(sha)
42 repo._bookmarks[refspec] = repo.lookup(sha)
42 except:
43 except:
43 pass
44 pass
44 return repo._bookmarks
45 return repo._bookmarks
45
46
46 def write(repo, refs):
47 def write(repo, refs):
47 '''Write bookmarks
48 '''Write bookmarks
48
49
49 Write the given bookmark => hash dictionary to the .hg/bookmarks file
50 Write the given bookmark => hash dictionary to the .hg/bookmarks file
50 in a format equal to those of localtags.
51 in a format equal to those of localtags.
51
52
52 We also store a backup of the previous state in undo.bookmarks that
53 We also store a backup of the previous state in undo.bookmarks that
53 can be copied back on rollback.
54 can be copied back on rollback.
54 '''
55 '''
55 if os.path.exists(repo.join('bookmarks')):
56 if os.path.exists(repo.join('bookmarks')):
56 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
57 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
57 file = repo.opener('bookmarks', 'w+')
58 file = repo.opener('bookmarks', 'w+')
58 for refspec, node in refs.items():
59 for refspec, node in refs.items():
59 file.write("%s %s\n" % (hex(node), refspec))
60 file.write("%s %s\n" % (hex(node), refspec))
60 file.close()
61 file.close()
61
62
62 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
63 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
63 '''mercurial bookmarks
64 '''mercurial bookmarks
64
65
65 Bookmarks are pointers to certain commits that move when
66 Bookmarks are pointers to certain commits that move when
66 commiting. Bookmarks are local. They can be renamed, copied and
67 commiting. Bookmarks are local. They can be renamed, copied and
67 deleted. It is possible to use bookmark names in 'hg merge' and 'hg
68 deleted. It is possible to use bookmark names in 'hg merge' and 'hg
68 update' to update to a given bookmark.
69 update' to update to a given bookmark.
69
70
70 You can use 'hg bookmark NAME' to set a bookmark on the current
71 You can use 'hg bookmark NAME' to set a bookmark on the current
71 tip with the given name. If you specify a revision using -r REV
72 tip with the given name. If you specify a revision using -r REV
72 (where REV may be an existing bookmark), the bookmark is set to
73 (where REV may be an existing bookmark), the bookmark is set to
73 that revision.
74 that revision.
74 '''
75 '''
75 hexfn = ui.debugflag and hex or short
76 hexfn = ui.debugflag and hex or short
76 marks = parse(repo)
77 marks = parse(repo)
77 cur = repo.changectx('.').node()
78 cur = repo.changectx('.').node()
78
79
79 if rename:
80 if rename:
80 if rename not in marks:
81 if rename not in marks:
81 raise util.Abort(_("a bookmark of this name does not exist"))
82 raise util.Abort(_("a bookmark of this name does not exist"))
82 if mark in marks and not force:
83 if mark in marks and not force:
83 raise util.Abort(_("a bookmark of the same name already exists"))
84 raise util.Abort(_("a bookmark of the same name already exists"))
84 if mark is None:
85 if mark is None:
85 raise util.Abort(_("new bookmark name required"))
86 raise util.Abort(_("new bookmark name required"))
86 marks[mark] = marks[rename]
87 marks[mark] = marks[rename]
87 del marks[rename]
88 del marks[rename]
88 write(repo, marks)
89 write(repo, marks)
89 return
90 return
90
91
91 if delete:
92 if delete:
92 if mark == None:
93 if mark == None:
93 raise util.Abort(_("bookmark name required"))
94 raise util.Abort(_("bookmark name required"))
94 if mark not in marks:
95 if mark not in marks:
95 raise util.Abort(_("a bookmark of this name does not exist"))
96 raise util.Abort(_("a bookmark of this name does not exist"))
96 del marks[mark]
97 del marks[mark]
97 write(repo, marks)
98 write(repo, marks)
98 return
99 return
99
100
100 if mark != None:
101 if mark != None:
101 if "\n" in mark:
102 if "\n" in mark:
102 raise util.Abort(_("bookmark name cannot contain newlines"))
103 raise util.Abort(_("bookmark name cannot contain newlines"))
103 mark = mark.strip()
104 mark = mark.strip()
104 if mark in marks and not force:
105 if mark in marks and not force:
105 raise util.Abort(_("a bookmark of the same name already exists"))
106 raise util.Abort(_("a bookmark of the same name already exists"))
106 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
107 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
107 and not force):
108 and not force):
108 raise util.Abort(
109 raise util.Abort(
109 _("a bookmark cannot have the name of an existing branch"))
110 _("a bookmark cannot have the name of an existing branch"))
110 if rev:
111 if rev:
111 marks[mark] = repo.lookup(rev)
112 marks[mark] = repo.lookup(rev)
112 else:
113 else:
113 marks[mark] = repo.changectx('.').node()
114 marks[mark] = repo.changectx('.').node()
114 write(repo, marks)
115 write(repo, marks)
115 return
116 return
116
117
117 if mark == None:
118 if mark == None:
118 if rev:
119 if rev:
119 raise util.Abort(_("bookmark name required"))
120 raise util.Abort(_("bookmark name required"))
120 if len(marks) == 0:
121 if len(marks) == 0:
121 ui.status("no bookmarks set\n")
122 ui.status("no bookmarks set\n")
122 else:
123 else:
123 for bmark, n in marks.iteritems():
124 for bmark, n in marks.iteritems():
124 prefix = (n == cur) and '*' or ' '
125 prefix = (n == cur) and '*' or ' '
125 ui.write(" %s %-25s %d:%s\n" % (
126 ui.write(" %s %-25s %d:%s\n" % (
126 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
127 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
127 return
128 return
128
129
129 def _revstostrip(changelog, node):
130 def _revstostrip(changelog, node):
130 srev = changelog.rev(node)
131 srev = changelog.rev(node)
131 tostrip = [srev]
132 tostrip = [srev]
132 saveheads = []
133 saveheads = []
133 for r in xrange(srev, len(changelog)):
134 for r in xrange(srev, len(changelog)):
134 parents = changelog.parentrevs(r)
135 parents = changelog.parentrevs(r)
135 if parents[0] in tostrip or parents[1] in tostrip:
136 if parents[0] in tostrip or parents[1] in tostrip:
136 tostrip.append(r)
137 tostrip.append(r)
137 if parents[1] != nullrev:
138 if parents[1] != nullrev:
138 for p in parents:
139 for p in parents:
139 if p not in tostrip and p > srev:
140 if p not in tostrip and p > srev:
140 saveheads.append(p)
141 saveheads.append(p)
141 return [r for r in tostrip if r not in saveheads]
142 return [r for r in tostrip if r not in saveheads]
142
143
143 def strip(ui, repo, node, backup="all"):
144 def strip(ui, repo, node, backup="all"):
144 """Strip bookmarks if revisions are stripped using
145 """Strip bookmarks if revisions are stripped using
145 the mercurial.strip method. This usually happens during
146 the mercurial.strip method. This usually happens during
146 qpush and qpop"""
147 qpush and qpop"""
147 revisions = _revstostrip(repo.changelog, node)
148 revisions = _revstostrip(repo.changelog, node)
148 marks = parse(repo)
149 marks = parse(repo)
149 update = []
150 update = []
150 for mark, n in marks.items():
151 for mark, n in marks.items():
151 if repo.changelog.rev(n) in revisions:
152 if repo.changelog.rev(n) in revisions:
152 update.append(mark)
153 update.append(mark)
153 oldstrip(ui, repo, node, backup)
154 oldstrip(ui, repo, node, backup)
154 if len(update) > 0:
155 if len(update) > 0:
155 for m in update:
156 for m in update:
156 marks[m] = repo.changectx('.').node()
157 marks[m] = repo.changectx('.').node()
157 write(repo, marks)
158 write(repo, marks)
158
159
159 oldstrip = mercurial.repair.strip
160 oldstrip = mercurial.repair.strip
160 mercurial.repair.strip = strip
161 mercurial.repair.strip = strip
161
162
162 def reposetup(ui, repo):
163 def reposetup(ui, repo):
163 if not isinstance(repo, mercurial.localrepo.localrepository):
164 if not isinstance(repo, mercurial.localrepo.localrepository):
164 return
165 return
165
166
166 # init a bookmark cache as otherwise we would get a infinite reading
167 # init a bookmark cache as otherwise we would get a infinite reading
167 # in lookup()
168 # in lookup()
168 repo._bookmarks = None
169 repo._bookmarks = None
169
170
170 class bookmark_repo(repo.__class__):
171 class bookmark_repo(repo.__class__):
171 def rollback(self):
172 def rollback(self):
172 if os.path.exists(self.join('undo.bookmarks')):
173 if os.path.exists(self.join('undo.bookmarks')):
173 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
174 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
174 return super(bookmark_repo, self).rollback()
175 return super(bookmark_repo, self).rollback()
175
176
176 def lookup(self, key):
177 def lookup(self, key):
177 if self._bookmarks is None:
178 if self._bookmarks is None:
178 self._bookmarks = parse(self)
179 self._bookmarks = parse(self)
179 if key in self._bookmarks:
180 if key in self._bookmarks:
180 key = self._bookmarks[key]
181 key = self._bookmarks[key]
181 return super(bookmark_repo, self).lookup(key)
182 return super(bookmark_repo, self).lookup(key)
182
183
183 def commit(self, *k, **kw):
184 def commit(self, *k, **kw):
184 """Add a revision to the repository and
185 """Add a revision to the repository and
185 move the bookmark"""
186 move the bookmark"""
186 node = super(bookmark_repo, self).commit(*k, **kw)
187 node = super(bookmark_repo, self).commit(*k, **kw)
187 if node == None:
188 if node == None:
188 return None
189 return None
189 parents = repo.changelog.parents(node)
190 parents = repo.changelog.parents(node)
190 if parents[1] == nullid:
191 if parents[1] == nullid:
191 parents = (parents[0],)
192 parents = (parents[0],)
192 marks = parse(repo)
193 marks = parse(repo)
193 update = False
194 update = False
194 for mark, n in marks.items():
195 for mark, n in marks.items():
195 if n in parents:
196 if n in parents:
196 marks[mark] = node
197 marks[mark] = node
197 update = True
198 update = True
198 if update:
199 if update:
199 write(repo, marks)
200 write(repo, marks)
200 return node
201 return node
201
202
202 def addchangegroup(self, source, srctype, url, emptyok=False):
203 def addchangegroup(self, source, srctype, url, emptyok=False):
203 parents = repo.dirstate.parents()
204 parents = repo.dirstate.parents()
204
205
205 result = super(bookmark_repo, self).addchangegroup(
206 result = super(bookmark_repo, self).addchangegroup(
206 source, srctype, url, emptyok)
207 source, srctype, url, emptyok)
207 if result > 1:
208 if result > 1:
208 # We have more heads than before
209 # We have more heads than before
209 return result
210 return result
210 node = repo.changelog.tip()
211 node = repo.changelog.tip()
211 marks = parse(repo)
212 marks = parse(repo)
212 update = False
213 update = False
213 for mark, n in marks.items():
214 for mark, n in marks.items():
214 if n in parents:
215 if n in parents:
215 marks[mark] = node
216 marks[mark] = node
216 update = True
217 update = True
217 if update:
218 if update:
218 write(repo, marks)
219 write(repo, marks)
219 return result
220 return result
220
221
221 repo.__class__ = bookmark_repo
222 repo.__class__ = bookmark_repo
222
223
224 def pushnonbookmarked(orig, ui, repo, *args, **opts):
225 'Call push with only the heads that are not bookmarked'
226 if opts.get('non_bookmarked'):
227 if opts.get('rev'):
228 heads = [repo.lookup(r) for r in opts.get('rev')]
229 else:
230 heads = repo.heads()
231
232 markheads = parse(repo).values()
233 opts['rev'] = [head for head in heads if not(head in markheads)]
234
235 orig(ui, repo, *args, **opts)
236
237 def uisetup(ui):
238 'Replace push with a decorator to provide --non-bookmarked option'
239 entry = extensions.wrapcommand(commands.table, 'push', pushnonbookmarked)
240 entry[1].append(('', 'non-bookmarked', None, _("push all heads that are not bookmarked")))
241
223 cmdtable = {
242 cmdtable = {
224 "bookmarks":
243 "bookmarks":
225 (bookmark,
244 (bookmark,
226 [('f', 'force', False, _('force')),
245 [('f', 'force', False, _('force')),
227 ('r', 'rev', '', _('revision')),
246 ('r', 'rev', '', _('revision')),
228 ('d', 'delete', False, _('delete a given bookmark')),
247 ('d', 'delete', False, _('delete a given bookmark')),
229 ('m', 'rename', '', _('rename a given bookmark'))],
248 ('m', 'rename', '', _('rename a given bookmark'))],
230 _('hg bookmarks [-d] [-m NAME] [-r NAME] [NAME]')),
249 _('hg bookmarks [-d] [-m NAME] [-r NAME] [NAME]')),
231 }
250 }
General Comments 0
You need to be logged in to leave comments. Login now