Mercurial > hg
annotate hgext/transplant.py @ 6858:8f256bf98219
Add support for multiple possible bisect results (issue1228, issue1182)
The real reason for both issue is that bisect can not handle cases where there
are multiple possibilities for the result.
Example (from issue1228):
rev 0 -> good
rev 1 -> skipped
rev 2 -> skipped
rev 3 -> skipped
rev 4 -> bad
Note that this patch does not only fix the reported Assertion Error but also
the problem of a non converging bisect:
hg init
for i in `seq 3`; do echo $i > $i; hg add $i; hg ci -m$i; done
hg bisect -b 2
hg bisect -g 0
hg bisect -s
From this state on, you can:
a) mark as bad forever (non converging!)
b) mark as good to get an inconsistent state
c) skip for the Assertion Error
Minor description and code edits by pmezard.
author | Bernhard Leiner <bleiner@gmail.com> |
---|---|
date | Sat, 02 Aug 2008 22:10:10 +0200 |
parents | e75aab656f46 |
children | f67d1468ac50 |
rev | line source |
---|---|
3714 | 1 # Patch transplanting extension for Mercurial |
2 # | |
4635
63b9d2deed48
Updated copyright notices and add "and others" to "hg version"
Thomas Arendsen Hein <thomas@intevation.de>
parents:
4516
diff
changeset
|
3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com> |
3714 | 4 # |
5 # This software may be used and distributed according to the terms | |
6 # of the GNU General Public License, incorporated herein by reference. | |
7 | |
3891 | 8 from mercurial.i18n import _ |
3877
abaee83ce0a6
Replace demandload with new demandimport
Matt Mackall <mpm@selenic.com>
parents:
3831
diff
changeset
|
9 import os, tempfile |
6212 | 10 from mercurial import bundlerepo, changegroup, cmdutil, hg, merge |
4049
863465381028
transplant: update to current writebundle API
Brendan Cully <brendan@kublai.com>
parents:
4035
diff
changeset
|
11 from mercurial import patch, revlog, util |
3714 | 12 |
13 '''patch transplanting tool | |
14 | |
15 This extension allows you to transplant patches from another branch. | |
16 | |
17 Transplanted patches are recorded in .hg/transplant/transplants, as a map | |
18 from a changeset hash to its hash in the source repository. | |
19 ''' | |
20 | |
21 class transplantentry: | |
22 def __init__(self, lnode, rnode): | |
23 self.lnode = lnode | |
24 self.rnode = rnode | |
25 | |
26 class transplants: | |
27 def __init__(self, path=None, transplantfile=None, opener=None): | |
28 self.path = path | |
29 self.transplantfile = transplantfile | |
30 self.opener = opener | |
31 | |
32 if not opener: | |
33 self.opener = util.opener(self.path) | |
34 self.transplants = [] | |
35 self.dirty = False | |
36 self.read() | |
37 | |
38 def read(self): | |
39 abspath = os.path.join(self.path, self.transplantfile) | |
40 if self.transplantfile and os.path.exists(abspath): | |
41 for line in self.opener(self.transplantfile).read().splitlines(): | |
42 lnode, rnode = map(revlog.bin, line.split(':')) | |
43 self.transplants.append(transplantentry(lnode, rnode)) | |
44 | |
45 def write(self): | |
46 if self.dirty and self.transplantfile: | |
47 if not os.path.isdir(self.path): | |
48 os.mkdir(self.path) | |
49 fp = self.opener(self.transplantfile, 'w') | |
50 for c in self.transplants: | |
51 l, r = map(revlog.hex, (c.lnode, c.rnode)) | |
52 fp.write(l + ':' + r + '\n') | |
53 fp.close() | |
54 self.dirty = False | |
55 | |
56 def get(self, rnode): | |
57 return [t for t in self.transplants if t.rnode == rnode] | |
58 | |
59 def set(self, lnode, rnode): | |
60 self.transplants.append(transplantentry(lnode, rnode)) | |
61 self.dirty = True | |
62 | |
63 def remove(self, transplant): | |
64 del self.transplants[self.transplants.index(transplant)] | |
65 self.dirty = True | |
66 | |
67 class transplanter: | |
68 def __init__(self, ui, repo): | |
69 self.ui = ui | |
70 self.path = repo.join('transplant') | |
71 self.opener = util.opener(self.path) | |
72 self.transplants = transplants(self.path, 'transplants', opener=self.opener) | |
73 | |
74 def applied(self, repo, node, parent): | |
75 '''returns True if a node is already an ancestor of parent | |
76 or has already been transplanted''' | |
77 if hasnode(repo, node): | |
78 if node in repo.changelog.reachable(parent, stop=node): | |
79 return True | |
80 for t in self.transplants.get(node): | |
81 # it might have been stripped | |
82 if not hasnode(repo, t.lnode): | |
83 self.transplants.remove(t) | |
84 return False | |
85 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode): | |
86 return True | |
87 return False | |
88 | |
89 def apply(self, repo, source, revmap, merges, opts={}): | |
90 '''apply the revisions in revmap one by one in revision order''' | |
91 revs = revmap.keys() | |
92 revs.sort() | |
93 | |
94 p1, p2 = repo.dirstate.parents() | |
95 pulls = [] | |
96 diffopts = patch.diffopts(self.ui, opts) | |
97 diffopts.git = True | |
98 | |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
99 lock = wlock = None |
3714 | 100 try: |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
101 wlock = repo.wlock() |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
102 lock = repo.lock() |
3714 | 103 for rev in revs: |
104 node = revmap[rev] | |
105 revstr = '%s:%s' % (rev, revlog.short(node)) | |
106 | |
107 if self.applied(repo, node, p1): | |
108 self.ui.warn(_('skipping already applied revision %s\n') % | |
109 revstr) | |
110 continue | |
111 | |
112 parents = source.changelog.parents(node) | |
113 if not opts.get('filter'): | |
114 # If the changeset parent is the same as the wdir's parent, | |
115 # just pull it. | |
116 if parents[0] == p1: | |
117 pulls.append(node) | |
118 p1 = node | |
119 continue | |
120 if pulls: | |
121 if source != repo: | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
122 repo.pull(source, heads=pulls) |
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
123 merge.update(repo, pulls[-1], False, False, None) |
3714 | 124 p1, p2 = repo.dirstate.parents() |
125 pulls = [] | |
126 | |
127 domerge = False | |
128 if node in merges: | |
129 # pulling all the merge revs at once would mean we couldn't | |
130 # transplant after the latest even if transplants before them | |
131 # fail. | |
132 domerge = True | |
133 if not hasnode(repo, node): | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
134 repo.pull(source, heads=[node]) |
3714 | 135 |
136 if parents[1] != revlog.nullid: | |
137 self.ui.note(_('skipping merge changeset %s:%s\n') | |
138 % (rev, revlog.short(node))) | |
139 patchfile = None | |
140 else: | |
141 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-') | |
142 fp = os.fdopen(fd, 'w') | |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
143 patch.diff(source, parents[0], node, fp=fp, opts=diffopts) |
3714 | 144 fp.close() |
145 | |
146 del revmap[rev] | |
147 if patchfile or domerge: | |
148 try: | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
149 n = self.applyone(repo, node, |
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
150 source.changelog.read(node), |
3714 | 151 patchfile, merge=domerge, |
152 log=opts.get('log'), | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
153 filter=opts.get('filter')) |
4251
e76e52145c3d
transplant: fix ignoring empty changesets (eg after filter)
Brendan Cully <brendan@kublai.com>
parents:
4072
diff
changeset
|
154 if n and domerge: |
3714 | 155 self.ui.status(_('%s merged at %s\n') % (revstr, |
156 revlog.short(n))) | |
4251
e76e52145c3d
transplant: fix ignoring empty changesets (eg after filter)
Brendan Cully <brendan@kublai.com>
parents:
4072
diff
changeset
|
157 elif n: |
3714 | 158 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node), |
159 revlog.short(n))) | |
160 finally: | |
161 if patchfile: | |
162 os.unlink(patchfile) | |
163 if pulls: | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
164 repo.pull(source, heads=pulls) |
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
165 merge.update(repo, pulls[-1], False, False, None) |
3714 | 166 finally: |
167 self.saveseries(revmap, merges) | |
168 self.transplants.write() | |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
169 del lock, wlock |
3714 | 170 |
171 def filter(self, filter, changelog, patchfile): | |
172 '''arbitrarily rewrite changeset before applying it''' | |
173 | |
3752
f902f409cd81
transplant: "filtering %s\n"
Brendan Cully <brendan@kublai.com>
parents:
3726
diff
changeset
|
174 self.ui.status('filtering %s\n' % patchfile) |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
175 user, date, msg = (changelog[1], changelog[2], changelog[4]) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
176 |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
177 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-') |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
178 fp = os.fdopen(fd, 'w') |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
179 fp.write("# HG changeset patch\n") |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
180 fp.write("# User %s\n" % user) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
181 fp.write("# Date %d %d\n" % date) |
3831
2db191165e9a
transplant: don't add extra newlines to changelog entry in filter
Brendan Cully <brendan@kublai.com>
parents:
3759
diff
changeset
|
182 fp.write(changelog[4]) |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
183 fp.close() |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
184 |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
185 try: |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
186 util.system('%s %s %s' % (filter, util.shellquote(headerfile), |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
187 util.shellquote(patchfile)), |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
188 environ={'HGUSER': changelog[1]}, |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
189 onerr=util.Abort, errprefix=_('filter failed')) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
190 user, date, msg = self.parselog(file(headerfile))[1:4] |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
191 finally: |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
192 os.unlink(headerfile) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
193 |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
194 return (user, date, msg) |
3714 | 195 |
196 def applyone(self, repo, node, cl, patchfile, merge=False, log=False, | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
197 filter=None): |
3714 | 198 '''apply the patch in patchfile to the repository as a transplant''' |
199 (manifest, user, (time, timezone), files, message) = cl[:5] | |
200 date = "%d %d" % (time, timezone) | |
201 extra = {'transplant_source': node} | |
202 if filter: | |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
203 (user, date, message) = self.filter(filter, cl, patchfile) |
3714 | 204 |
205 if log: | |
206 message += '\n(transplanted from %s)' % revlog.hex(node) | |
207 | |
208 self.ui.status(_('applying %s\n') % revlog.short(node)) | |
209 self.ui.note('%s %s\n%s\n' % (user, date, message)) | |
210 | |
211 if not patchfile and not merge: | |
212 raise util.Abort(_('can only omit patchfile if merging')) | |
213 if patchfile: | |
214 try: | |
215 files = {} | |
3726
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
216 try: |
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
217 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root, |
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
218 files=files) |
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
219 if not files: |
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
220 self.ui.warn(_('%s: empty changeset') % revlog.hex(node)) |
4251
e76e52145c3d
transplant: fix ignoring empty changesets (eg after filter)
Brendan Cully <brendan@kublai.com>
parents:
4072
diff
changeset
|
221 return None |
3726
752884db5037
transplant: recover added/removed files after failed application
Brendan Cully <brendan@kublai.com>
parents:
3725
diff
changeset
|
222 finally: |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
223 files = patch.updatedir(self.ui, repo, files) |
3714 | 224 except Exception, inst: |
225 if filter: | |
226 os.unlink(patchfile) | |
3757
faed44bab17b
transplant: clobber old series when transplant fails
Brendan Cully <brendan@kublai.com>
parents:
3752
diff
changeset
|
227 seriespath = os.path.join(self.path, 'series') |
faed44bab17b
transplant: clobber old series when transplant fails
Brendan Cully <brendan@kublai.com>
parents:
3752
diff
changeset
|
228 if os.path.exists(seriespath): |
faed44bab17b
transplant: clobber old series when transplant fails
Brendan Cully <brendan@kublai.com>
parents:
3752
diff
changeset
|
229 os.unlink(seriespath) |
3714 | 230 p1 = repo.dirstate.parents()[0] |
231 p2 = node | |
3725
ccc7a9eb0e5e
transplant: preserve filter changes in --continue log
Brendan Cully <brendan@kublai.com>
parents:
3724
diff
changeset
|
232 self.log(user, date, message, p1, p2, merge=merge) |
3714 | 233 self.ui.write(str(inst) + '\n') |
234 raise util.Abort(_('Fix up the merge and run hg transplant --continue')) | |
235 else: | |
236 files = None | |
237 if merge: | |
238 p1, p2 = repo.dirstate.parents() | |
239 repo.dirstate.setparents(p1, node) | |
240 | |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
241 n = repo.commit(files, message, user, date, extra=extra) |
3714 | 242 if not merge: |
243 self.transplants.set(n, node) | |
244 | |
245 return n | |
246 | |
247 def resume(self, repo, source, opts=None): | |
248 '''recover last transaction and apply remaining changesets''' | |
249 if os.path.exists(os.path.join(self.path, 'journal')): | |
250 n, node = self.recover(repo) | |
3724
ea523d6f5f1a
transplant: fix --continue; add --continue test
Brendan Cully <brendan@kublai.com>
parents:
3723
diff
changeset
|
251 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node), |
ea523d6f5f1a
transplant: fix --continue; add --continue test
Brendan Cully <brendan@kublai.com>
parents:
3723
diff
changeset
|
252 revlog.short(n))) |
3714 | 253 seriespath = os.path.join(self.path, 'series') |
254 if not os.path.exists(seriespath): | |
3758
889f7e74a0d9
transplant: log source node when recovering too.
Brendan Cully <brendan@kublai.com>
parents:
3757
diff
changeset
|
255 self.transplants.write() |
3714 | 256 return |
257 nodes, merges = self.readseries() | |
258 revmap = {} | |
259 for n in nodes: | |
260 revmap[source.changelog.rev(n)] = n | |
261 os.unlink(seriespath) | |
262 | |
263 self.apply(repo, source, revmap, merges, opts) | |
264 | |
265 def recover(self, repo): | |
266 '''commit working directory using journal metadata''' | |
267 node, user, date, message, parents = self.readlog() | |
268 merge = len(parents) == 2 | |
269 | |
270 if not user or not date or not message or not parents[0]: | |
271 raise util.Abort(_('transplant log file is corrupt')) | |
272 | |
3758
889f7e74a0d9
transplant: log source node when recovering too.
Brendan Cully <brendan@kublai.com>
parents:
3757
diff
changeset
|
273 extra = {'transplant_source': node} |
3714 | 274 wlock = repo.wlock() |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
275 try: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
276 p1, p2 = repo.dirstate.parents() |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
277 if p1 != parents[0]: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
278 raise util.Abort( |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
279 _('working dir not at transplant parent %s') % |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
280 revlog.hex(parents[0])) |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
281 if merge: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
282 repo.dirstate.setparents(p1, parents[1]) |
4917
126f527b3ba3
Make repo locks recursive, eliminate all passing of lock/wlock
Matt Mackall <mpm@selenic.com>
parents:
4915
diff
changeset
|
283 n = repo.commit(None, message, user, date, extra=extra) |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
284 if not n: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
285 raise util.Abort(_('commit failed')) |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
286 if not merge: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
287 self.transplants.set(n, node) |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
288 self.unlog() |
3714 | 289 |
4915
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
290 return n, node |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
291 finally: |
97b734fb9c6f
Use try/finally pattern to cleanup locks and transactions
Matt Mackall <mpm@selenic.com>
parents:
4680
diff
changeset
|
292 del wlock |
3714 | 293 |
294 def readseries(self): | |
295 nodes = [] | |
296 merges = [] | |
297 cur = nodes | |
298 for line in self.opener('series').read().splitlines(): | |
299 if line.startswith('# Merges'): | |
300 cur = merges | |
301 continue | |
302 cur.append(revlog.bin(line)) | |
303 | |
304 return (nodes, merges) | |
305 | |
306 def saveseries(self, revmap, merges): | |
307 if not revmap: | |
308 return | |
309 | |
310 if not os.path.isdir(self.path): | |
311 os.mkdir(self.path) | |
312 series = self.opener('series', 'w') | |
313 revs = revmap.keys() | |
314 revs.sort() | |
315 for rev in revs: | |
316 series.write(revlog.hex(revmap[rev]) + '\n') | |
317 if merges: | |
318 series.write('# Merges\n') | |
319 for m in merges: | |
320 series.write(revlog.hex(m) + '\n') | |
321 series.close() | |
322 | |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
323 def parselog(self, fp): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
324 parents = [] |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
325 message = [] |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
326 node = revlog.nullid |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
327 inmsg = False |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
328 for line in fp.read().splitlines(): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
329 if inmsg: |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
330 message.append(line) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
331 elif line.startswith('# User '): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
332 user = line[7:] |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
333 elif line.startswith('# Date '): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
334 date = line[7:] |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
335 elif line.startswith('# Node ID '): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
336 node = revlog.bin(line[10:]) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
337 elif line.startswith('# Parent '): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
338 parents.append(revlog.bin(line[9:])) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
339 elif not line.startswith('#'): |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
340 inmsg = True |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
341 message.append(line) |
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
342 return (node, user, date, '\n'.join(message), parents) |
4516
96d8a56d4ef9
Removed trailing whitespace and tabs from python files
Thomas Arendsen Hein <thomas@intevation.de>
parents:
4251
diff
changeset
|
343 |
3725
ccc7a9eb0e5e
transplant: preserve filter changes in --continue log
Brendan Cully <brendan@kublai.com>
parents:
3724
diff
changeset
|
344 def log(self, user, date, message, p1, p2, merge=False): |
3714 | 345 '''journal changelog metadata for later recover''' |
346 | |
347 if not os.path.isdir(self.path): | |
348 os.mkdir(self.path) | |
349 fp = self.opener('journal', 'w') | |
3725
ccc7a9eb0e5e
transplant: preserve filter changes in --continue log
Brendan Cully <brendan@kublai.com>
parents:
3724
diff
changeset
|
350 fp.write('# User %s\n' % user) |
ccc7a9eb0e5e
transplant: preserve filter changes in --continue log
Brendan Cully <brendan@kublai.com>
parents:
3724
diff
changeset
|
351 fp.write('# Date %s\n' % date) |
3714 | 352 fp.write('# Node ID %s\n' % revlog.hex(p2)) |
353 fp.write('# Parent ' + revlog.hex(p1) + '\n') | |
354 if merge: | |
355 fp.write('# Parent ' + revlog.hex(p2) + '\n') | |
3725
ccc7a9eb0e5e
transplant: preserve filter changes in --continue log
Brendan Cully <brendan@kublai.com>
parents:
3724
diff
changeset
|
356 fp.write(message.rstrip() + '\n') |
3714 | 357 fp.close() |
358 | |
359 def readlog(self): | |
3759
e96f97ca0358
transplant: split filter args into changelog entry and patch
Brendan Cully <brendan@kublai.com>
parents:
3758
diff
changeset
|
360 return self.parselog(self.opener('journal')) |
3714 | 361 |
362 def unlog(self): | |
363 '''remove changelog journal''' | |
364 absdst = os.path.join(self.path, 'journal') | |
365 if os.path.exists(absdst): | |
366 os.unlink(absdst) | |
367 | |
368 def transplantfilter(self, repo, source, root): | |
369 def matchfn(node): | |
370 if self.applied(repo, node, root): | |
371 return False | |
372 if source.changelog.parents(node)[1] != revlog.nullid: | |
373 return False | |
374 extra = source.changelog.read(node)[5] | |
375 cnode = extra.get('transplant_source') | |
376 if cnode and self.applied(repo, cnode, root): | |
377 return False | |
378 return True | |
379 | |
380 return matchfn | |
381 | |
382 def hasnode(repo, node): | |
383 try: | |
384 return repo.changelog.rev(node) != None | |
385 except revlog.RevlogError: | |
386 return False | |
387 | |
388 def browserevs(ui, repo, nodes, opts): | |
389 '''interactively transplant changesets''' | |
390 def browsehelp(ui): | |
391 ui.write('y: transplant this changeset\n' | |
392 'n: skip this changeset\n' | |
393 'm: merge at this changeset\n' | |
394 'p: show patch\n' | |
395 'c: commit selected changesets\n' | |
396 'q: cancel transplant\n' | |
397 '?: show this help\n') | |
398 | |
3723
c828fca6f38a
transplant: show_changeset moved to cmdutil
Brendan Cully <brendan@kublai.com>
parents:
3714
diff
changeset
|
399 displayer = cmdutil.show_changeset(ui, repo, opts) |
3714 | 400 transplants = [] |
401 merges = [] | |
402 for node in nodes: | |
403 displayer.show(changenode=node) | |
404 action = None | |
405 while not action: | |
406 action = ui.prompt(_('apply changeset? [ynmpcq?]:')) | |
407 if action == '?': | |
408 browsehelp(ui) | |
409 action = None | |
410 elif action == 'p': | |
411 parent = repo.changelog.parents(node)[0] | |
412 patch.diff(repo, parent, node) | |
413 action = None | |
414 elif action not in ('y', 'n', 'm', 'c', 'q'): | |
415 ui.write('no such option\n') | |
416 action = None | |
417 if action == 'y': | |
418 transplants.append(node) | |
419 elif action == 'm': | |
420 merges.append(node) | |
421 elif action == 'c': | |
422 break | |
423 elif action == 'q': | |
424 transplants = () | |
425 merges = () | |
426 break | |
427 return (transplants, merges) | |
428 | |
429 def transplant(ui, repo, *revs, **opts): | |
430 '''transplant changesets from another branch | |
431 | |
432 Selected changesets will be applied on top of the current working | |
433 directory with the log of the original changeset. If --log is | |
434 specified, log messages will have a comment appended of the form: | |
435 | |
436 (transplanted from CHANGESETHASH) | |
437 | |
438 You can rewrite the changelog message with the --filter option. | |
439 Its argument will be invoked with the current changelog message | |
440 as $1 and the patch as $2. | |
441 | |
442 If --source is specified, selects changesets from the named | |
443 repository. If --branch is specified, selects changesets from the | |
444 branch holding the named revision, up to that revision. If --all | |
445 is specified, all changesets on the branch will be transplanted, | |
446 otherwise you will be prompted to select the changesets you want. | |
447 | |
448 hg transplant --branch REVISION --all will rebase the selected branch | |
449 (up to the named revision) onto your current working directory. | |
450 | |
451 You can optionally mark selected transplanted changesets as | |
452 merge changesets. You will not be prompted to transplant any | |
453 ancestors of a merged transplant, and you can merge descendants | |
454 of them normally instead of transplanting them. | |
455 | |
456 If no merges or revisions are provided, hg transplant will start | |
457 an interactive changeset browser. | |
458 | |
459 If a changeset application fails, you can fix the merge by hand and | |
460 then resume where you left off by calling hg transplant --continue. | |
461 ''' | |
462 def getoneitem(opts, item, errmsg): | |
463 val = opts.get(item) | |
464 if val: | |
465 if len(val) > 1: | |
466 raise util.Abort(errmsg) | |
467 else: | |
468 return val[0] | |
469 | |
470 def getremotechanges(repo, url): | |
471 sourcerepo = ui.expandpath(url) | |
472 source = hg.repository(ui, sourcerepo) | |
473 incoming = repo.findincoming(source, force=True) | |
474 if not incoming: | |
475 return (source, None, None) | |
476 | |
477 bundle = None | |
478 if not source.local(): | |
479 cg = source.changegroup(incoming, 'incoming') | |
4049
863465381028
transplant: update to current writebundle API
Brendan Cully <brendan@kublai.com>
parents:
4035
diff
changeset
|
480 bundle = changegroup.writebundle(cg, None, 'HG10UN') |
3714 | 481 source = bundlerepo.bundlerepository(ui, repo.root, bundle) |
482 | |
483 return (source, incoming, bundle) | |
484 | |
485 def incwalk(repo, incoming, branches, match=util.always): | |
486 if not branches: | |
487 branches=None | |
488 for node in repo.changelog.nodesbetween(incoming, branches)[0]: | |
489 if match(node): | |
490 yield node | |
491 | |
492 def transplantwalk(repo, root, branches, match=util.always): | |
493 if not branches: | |
494 branches = repo.heads() | |
495 ancestors = [] | |
496 for branch in branches: | |
497 ancestors.append(repo.changelog.ancestor(root, branch)) | |
498 for node in repo.changelog.nodesbetween(ancestors, branches)[0]: | |
499 if match(node): | |
500 yield node | |
501 | |
502 def checkopts(opts, revs): | |
503 if opts.get('continue'): | |
504 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')): | |
505 raise util.Abort(_('--continue is incompatible with branch, all or merge')) | |
506 return | |
507 if not (opts.get('source') or revs or | |
508 opts.get('merge') or opts.get('branch')): | |
509 raise util.Abort(_('no source URL, branch tag or revision list provided')) | |
510 if opts.get('all'): | |
511 if not opts.get('branch'): | |
512 raise util.Abort(_('--all requires a branch revision')) | |
513 if revs: | |
514 raise util.Abort(_('--all is incompatible with a revision list')) | |
515 | |
516 checkopts(opts, revs) | |
517 | |
518 if not opts.get('log'): | |
519 opts['log'] = ui.config('transplant', 'log') | |
520 if not opts.get('filter'): | |
521 opts['filter'] = ui.config('transplant', 'filter') | |
522 | |
523 tp = transplanter(ui, repo) | |
524 | |
525 p1, p2 = repo.dirstate.parents() | |
526 if p1 == revlog.nullid: | |
527 raise util.Abort(_('no revision checked out')) | |
528 if not opts.get('continue'): | |
529 if p2 != revlog.nullid: | |
530 raise util.Abort(_('outstanding uncommitted merges')) | |
531 m, a, r, d = repo.status()[:4] | |
532 if m or a or r or d: | |
533 raise util.Abort(_('outstanding local changes')) | |
534 | |
535 bundle = None | |
536 source = opts.get('source') | |
537 if source: | |
538 (source, incoming, bundle) = getremotechanges(repo, source) | |
539 else: | |
540 source = repo | |
541 | |
542 try: | |
543 if opts.get('continue'): | |
3724
ea523d6f5f1a
transplant: fix --continue; add --continue test
Brendan Cully <brendan@kublai.com>
parents:
3723
diff
changeset
|
544 tp.resume(repo, source, opts) |
3714 | 545 return |
546 | |
547 tf=tp.transplantfilter(repo, source, p1) | |
548 if opts.get('prune'): | |
549 prune = [source.lookup(r) | |
550 for r in cmdutil.revrange(source, opts.get('prune'))] | |
551 matchfn = lambda x: tf(x) and x not in prune | |
552 else: | |
553 matchfn = tf | |
554 branches = map(source.lookup, opts.get('branch', ())) | |
555 merges = map(source.lookup, opts.get('merge', ())) | |
556 revmap = {} | |
557 if revs: | |
558 for r in cmdutil.revrange(source, revs): | |
559 revmap[int(r)] = source.lookup(r) | |
560 elif opts.get('all') or not merges: | |
561 if source != repo: | |
562 alltransplants = incwalk(source, incoming, branches, match=matchfn) | |
563 else: | |
564 alltransplants = transplantwalk(source, p1, branches, match=matchfn) | |
565 if opts.get('all'): | |
566 revs = alltransplants | |
567 else: | |
568 revs, newmerges = browserevs(ui, source, alltransplants, opts) | |
569 merges.extend(newmerges) | |
570 for r in revs: | |
571 revmap[source.changelog.rev(r)] = r | |
572 for r in merges: | |
573 revmap[source.changelog.rev(r)] = r | |
574 | |
575 revs = revmap.keys() | |
576 revs.sort() | |
577 pulls = [] | |
578 | |
579 tp.apply(repo, source, revmap, merges, opts) | |
580 finally: | |
581 if bundle: | |
4072
e916bc0dfdd6
transplant: remote bundle source was not closed before deleting the fetched bundle.
Patrick Mezard <pmezard@gmail.com>
parents:
4049
diff
changeset
|
582 source.close() |
3714 | 583 os.unlink(bundle) |
584 | |
585 cmdtable = { | |
586 "transplant": | |
587 (transplant, | |
588 [('s', 'source', '', _('pull patches from REPOSITORY')), | |
589 ('b', 'branch', [], _('pull patches from branch BRANCH')), | |
590 ('a', 'all', None, _('pull all changesets up to BRANCH')), | |
591 ('p', 'prune', [], _('skip over REV')), | |
592 ('m', 'merge', [], _('merge at REV')), | |
593 ('', 'log', None, _('append transplant info to log message')), | |
594 ('c', 'continue', None, _('continue last transplant session after repair')), | |
595 ('', 'filter', '', _('filter changesets through FILTER'))], | |
3991
da3dc89f1e9a
Corrected synopsis for transplant.
Thomas Arendsen Hein <thomas@intevation.de>
parents:
3891
diff
changeset
|
596 _('hg transplant [-s REPOSITORY] [-b BRANCH [-a]] [-p REV] [-m REV] [REV]...')) |
3714 | 597 } |