##// END OF EJS Templates
transplant: show_changeset moved to cmdutil
Brendan Cully -
r3723:c828fca6 default
parent child Browse files
Show More
@@ -1,565 +1,565
1 # Patch transplanting extension for Mercurial
1 # Patch transplanting extension for Mercurial
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
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 from mercurial.demandload import *
8 from mercurial.demandload import *
9 from mercurial.i18n import gettext as _
9 from mercurial.i18n import gettext as _
10 demandload(globals(), 'os tempfile')
10 demandload(globals(), 'os tempfile')
11 demandload(globals(), 'mercurial:bundlerepo,cmdutil,commands,hg,merge,patch')
11 demandload(globals(), 'mercurial:bundlerepo,cmdutil,commands,hg,merge,patch')
12 demandload(globals(), 'mercurial:revlog,util')
12 demandload(globals(), 'mercurial:revlog,util')
13
13
14 '''patch transplanting tool
14 '''patch transplanting tool
15
15
16 This extension allows you to transplant patches from another branch.
16 This extension allows you to transplant patches from another branch.
17
17
18 Transplanted patches are recorded in .hg/transplant/transplants, as a map
18 Transplanted patches are recorded in .hg/transplant/transplants, as a map
19 from a changeset hash to its hash in the source repository.
19 from a changeset hash to its hash in the source repository.
20 '''
20 '''
21
21
22 class transplantentry:
22 class transplantentry:
23 def __init__(self, lnode, rnode):
23 def __init__(self, lnode, rnode):
24 self.lnode = lnode
24 self.lnode = lnode
25 self.rnode = rnode
25 self.rnode = rnode
26
26
27 class transplants:
27 class transplants:
28 def __init__(self, path=None, transplantfile=None, opener=None):
28 def __init__(self, path=None, transplantfile=None, opener=None):
29 self.path = path
29 self.path = path
30 self.transplantfile = transplantfile
30 self.transplantfile = transplantfile
31 self.opener = opener
31 self.opener = opener
32
32
33 if not opener:
33 if not opener:
34 self.opener = util.opener(self.path)
34 self.opener = util.opener(self.path)
35 self.transplants = []
35 self.transplants = []
36 self.dirty = False
36 self.dirty = False
37 self.read()
37 self.read()
38
38
39 def read(self):
39 def read(self):
40 abspath = os.path.join(self.path, self.transplantfile)
40 abspath = os.path.join(self.path, self.transplantfile)
41 if self.transplantfile and os.path.exists(abspath):
41 if self.transplantfile and os.path.exists(abspath):
42 for line in self.opener(self.transplantfile).read().splitlines():
42 for line in self.opener(self.transplantfile).read().splitlines():
43 lnode, rnode = map(revlog.bin, line.split(':'))
43 lnode, rnode = map(revlog.bin, line.split(':'))
44 self.transplants.append(transplantentry(lnode, rnode))
44 self.transplants.append(transplantentry(lnode, rnode))
45
45
46 def write(self):
46 def write(self):
47 if self.dirty and self.transplantfile:
47 if self.dirty and self.transplantfile:
48 if not os.path.isdir(self.path):
48 if not os.path.isdir(self.path):
49 os.mkdir(self.path)
49 os.mkdir(self.path)
50 fp = self.opener(self.transplantfile, 'w')
50 fp = self.opener(self.transplantfile, 'w')
51 for c in self.transplants:
51 for c in self.transplants:
52 l, r = map(revlog.hex, (c.lnode, c.rnode))
52 l, r = map(revlog.hex, (c.lnode, c.rnode))
53 fp.write(l + ':' + r + '\n')
53 fp.write(l + ':' + r + '\n')
54 fp.close()
54 fp.close()
55 self.dirty = False
55 self.dirty = False
56
56
57 def get(self, rnode):
57 def get(self, rnode):
58 return [t for t in self.transplants if t.rnode == rnode]
58 return [t for t in self.transplants if t.rnode == rnode]
59
59
60 def set(self, lnode, rnode):
60 def set(self, lnode, rnode):
61 self.transplants.append(transplantentry(lnode, rnode))
61 self.transplants.append(transplantentry(lnode, rnode))
62 self.dirty = True
62 self.dirty = True
63
63
64 def remove(self, transplant):
64 def remove(self, transplant):
65 del self.transplants[self.transplants.index(transplant)]
65 del self.transplants[self.transplants.index(transplant)]
66 self.dirty = True
66 self.dirty = True
67
67
68 class transplanter:
68 class transplanter:
69 def __init__(self, ui, repo):
69 def __init__(self, ui, repo):
70 self.ui = ui
70 self.ui = ui
71 self.path = repo.join('transplant')
71 self.path = repo.join('transplant')
72 self.opener = util.opener(self.path)
72 self.opener = util.opener(self.path)
73 self.transplants = transplants(self.path, 'transplants', opener=self.opener)
73 self.transplants = transplants(self.path, 'transplants', opener=self.opener)
74
74
75 def applied(self, repo, node, parent):
75 def applied(self, repo, node, parent):
76 '''returns True if a node is already an ancestor of parent
76 '''returns True if a node is already an ancestor of parent
77 or has already been transplanted'''
77 or has already been transplanted'''
78 if hasnode(repo, node):
78 if hasnode(repo, node):
79 if node in repo.changelog.reachable(parent, stop=node):
79 if node in repo.changelog.reachable(parent, stop=node):
80 return True
80 return True
81 for t in self.transplants.get(node):
81 for t in self.transplants.get(node):
82 # it might have been stripped
82 # it might have been stripped
83 if not hasnode(repo, t.lnode):
83 if not hasnode(repo, t.lnode):
84 self.transplants.remove(t)
84 self.transplants.remove(t)
85 return False
85 return False
86 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
86 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
87 return True
87 return True
88 return False
88 return False
89
89
90 def apply(self, repo, source, revmap, merges, opts={}):
90 def apply(self, repo, source, revmap, merges, opts={}):
91 '''apply the revisions in revmap one by one in revision order'''
91 '''apply the revisions in revmap one by one in revision order'''
92 revs = revmap.keys()
92 revs = revmap.keys()
93 revs.sort()
93 revs.sort()
94
94
95 p1, p2 = repo.dirstate.parents()
95 p1, p2 = repo.dirstate.parents()
96 pulls = []
96 pulls = []
97 diffopts = patch.diffopts(self.ui, opts)
97 diffopts = patch.diffopts(self.ui, opts)
98 diffopts.git = True
98 diffopts.git = True
99
99
100 lock = repo.lock()
100 lock = repo.lock()
101 wlock = repo.wlock()
101 wlock = repo.wlock()
102 try:
102 try:
103 for rev in revs:
103 for rev in revs:
104 node = revmap[rev]
104 node = revmap[rev]
105 revstr = '%s:%s' % (rev, revlog.short(node))
105 revstr = '%s:%s' % (rev, revlog.short(node))
106
106
107 if self.applied(repo, node, p1):
107 if self.applied(repo, node, p1):
108 self.ui.warn(_('skipping already applied revision %s\n') %
108 self.ui.warn(_('skipping already applied revision %s\n') %
109 revstr)
109 revstr)
110 continue
110 continue
111
111
112 parents = source.changelog.parents(node)
112 parents = source.changelog.parents(node)
113 if not opts.get('filter'):
113 if not opts.get('filter'):
114 # If the changeset parent is the same as the wdir's parent,
114 # If the changeset parent is the same as the wdir's parent,
115 # just pull it.
115 # just pull it.
116 if parents[0] == p1:
116 if parents[0] == p1:
117 pulls.append(node)
117 pulls.append(node)
118 p1 = node
118 p1 = node
119 continue
119 continue
120 if pulls:
120 if pulls:
121 if source != repo:
121 if source != repo:
122 repo.pull(source, heads=pulls, lock=lock)
122 repo.pull(source, heads=pulls, lock=lock)
123 merge.update(repo, pulls[-1], wlock=wlock)
123 merge.update(repo, pulls[-1], wlock=wlock)
124 p1, p2 = repo.dirstate.parents()
124 p1, p2 = repo.dirstate.parents()
125 pulls = []
125 pulls = []
126
126
127 domerge = False
127 domerge = False
128 if node in merges:
128 if node in merges:
129 # pulling all the merge revs at once would mean we couldn't
129 # pulling all the merge revs at once would mean we couldn't
130 # transplant after the latest even if transplants before them
130 # transplant after the latest even if transplants before them
131 # fail.
131 # fail.
132 domerge = True
132 domerge = True
133 if not hasnode(repo, node):
133 if not hasnode(repo, node):
134 repo.pull(source, heads=[node], lock=lock)
134 repo.pull(source, heads=[node], lock=lock)
135
135
136 if parents[1] != revlog.nullid:
136 if parents[1] != revlog.nullid:
137 self.ui.note(_('skipping merge changeset %s:%s\n')
137 self.ui.note(_('skipping merge changeset %s:%s\n')
138 % (rev, revlog.short(node)))
138 % (rev, revlog.short(node)))
139 patchfile = None
139 patchfile = None
140 else:
140 else:
141 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
141 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
142 fp = os.fdopen(fd, 'w')
142 fp = os.fdopen(fd, 'w')
143 patch.export(source, [node], fp=fp, opts=diffopts)
143 patch.export(source, [node], fp=fp, opts=diffopts)
144 fp.close()
144 fp.close()
145
145
146 del revmap[rev]
146 del revmap[rev]
147 if patchfile or domerge:
147 if patchfile or domerge:
148 try:
148 try:
149 n = self.applyone(repo, node, source.changelog.read(node),
149 n = self.applyone(repo, node, source.changelog.read(node),
150 patchfile, merge=domerge,
150 patchfile, merge=domerge,
151 log=opts.get('log'),
151 log=opts.get('log'),
152 filter=opts.get('filter'),
152 filter=opts.get('filter'),
153 lock=lock, wlock=wlock)
153 lock=lock, wlock=wlock)
154 if domerge:
154 if domerge:
155 self.ui.status(_('%s merged at %s\n') % (revstr,
155 self.ui.status(_('%s merged at %s\n') % (revstr,
156 revlog.short(n)))
156 revlog.short(n)))
157 else:
157 else:
158 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node),
158 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node),
159 revlog.short(n)))
159 revlog.short(n)))
160 finally:
160 finally:
161 if patchfile:
161 if patchfile:
162 os.unlink(patchfile)
162 os.unlink(patchfile)
163 if pulls:
163 if pulls:
164 repo.pull(source, heads=pulls, lock=lock)
164 repo.pull(source, heads=pulls, lock=lock)
165 merge.update(repo, pulls[-1], wlock=wlock)
165 merge.update(repo, pulls[-1], wlock=wlock)
166 finally:
166 finally:
167 self.saveseries(revmap, merges)
167 self.saveseries(revmap, merges)
168 self.transplants.write()
168 self.transplants.write()
169
169
170 def filter(self, filter, changelog, patchfile):
170 def filter(self, filter, changelog, patchfile):
171 '''arbitrarily rewrite changeset before applying it'''
171 '''arbitrarily rewrite changeset before applying it'''
172
172
173 self.ui.status('filtering %s' % patchfile)
173 self.ui.status('filtering %s' % patchfile)
174 util.system('%s %s' % (filter, util.shellquote(patchfile)),
174 util.system('%s %s' % (filter, util.shellquote(patchfile)),
175 environ={'HGUSER': changelog[1]},
175 environ={'HGUSER': changelog[1]},
176 onerr=util.Abort, errprefix=_('filter failed'))
176 onerr=util.Abort, errprefix=_('filter failed'))
177
177
178 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
178 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
179 filter=None, lock=None, wlock=None):
179 filter=None, lock=None, wlock=None):
180 '''apply the patch in patchfile to the repository as a transplant'''
180 '''apply the patch in patchfile to the repository as a transplant'''
181 (manifest, user, (time, timezone), files, message) = cl[:5]
181 (manifest, user, (time, timezone), files, message) = cl[:5]
182 date = "%d %d" % (time, timezone)
182 date = "%d %d" % (time, timezone)
183 extra = {'transplant_source': node}
183 extra = {'transplant_source': node}
184 if filter:
184 if filter:
185 self.filter(filter, cl, patchfile)
185 self.filter(filter, cl, patchfile)
186 patchfile, message, user, date = patch.extract(self.ui, file(patchfile))
186 patchfile, message, user, date = patch.extract(self.ui, file(patchfile))
187
187
188 if log:
188 if log:
189 message += '\n(transplanted from %s)' % revlog.hex(node)
189 message += '\n(transplanted from %s)' % revlog.hex(node)
190 cl = list(cl)
190 cl = list(cl)
191 cl[4] = message
191 cl[4] = message
192
192
193 self.ui.status(_('applying %s\n') % revlog.short(node))
193 self.ui.status(_('applying %s\n') % revlog.short(node))
194 self.ui.note('%s %s\n%s\n' % (user, date, message))
194 self.ui.note('%s %s\n%s\n' % (user, date, message))
195
195
196 if not patchfile and not merge:
196 if not patchfile and not merge:
197 raise util.Abort(_('can only omit patchfile if merging'))
197 raise util.Abort(_('can only omit patchfile if merging'))
198 if patchfile:
198 if patchfile:
199 try:
199 try:
200 files = {}
200 files = {}
201 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
201 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
202 files=files)
202 files=files)
203 if not files:
203 if not files:
204 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
204 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
205 return
205 return
206 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
206 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
207 if filter:
207 if filter:
208 os.unlink(patchfile)
208 os.unlink(patchfile)
209 except Exception, inst:
209 except Exception, inst:
210 if filter:
210 if filter:
211 os.unlink(patchfile)
211 os.unlink(patchfile)
212 p1 = repo.dirstate.parents()[0]
212 p1 = repo.dirstate.parents()[0]
213 p2 = node
213 p2 = node
214 self.log(cl, p1, p2, merge=merge)
214 self.log(cl, p1, p2, merge=merge)
215 self.ui.write(str(inst) + '\n')
215 self.ui.write(str(inst) + '\n')
216 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
216 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
217 else:
217 else:
218 files = None
218 files = None
219 if merge:
219 if merge:
220 p1, p2 = repo.dirstate.parents()
220 p1, p2 = repo.dirstate.parents()
221 repo.dirstate.setparents(p1, node)
221 repo.dirstate.setparents(p1, node)
222
222
223 n = repo.commit(files, message, user, date, lock=lock, wlock=wlock,
223 n = repo.commit(files, message, user, date, lock=lock, wlock=wlock,
224 extra=extra)
224 extra=extra)
225 if not merge:
225 if not merge:
226 self.transplants.set(n, node)
226 self.transplants.set(n, node)
227
227
228 return n
228 return n
229
229
230 def resume(self, repo, source, opts=None):
230 def resume(self, repo, source, opts=None):
231 '''recover last transaction and apply remaining changesets'''
231 '''recover last transaction and apply remaining changesets'''
232 if os.path.exists(os.path.join(self.path, 'journal')):
232 if os.path.exists(os.path.join(self.path, 'journal')):
233 n, node = self.recover(repo)
233 n, node = self.recover(repo)
234 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
234 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
235 revlog.short(n)))
235 revlog.short(n)))
236 seriespath = os.path.join(self.path, 'series')
236 seriespath = os.path.join(self.path, 'series')
237 if not os.path.exists(seriespath):
237 if not os.path.exists(seriespath):
238 return
238 return
239 nodes, merges = self.readseries()
239 nodes, merges = self.readseries()
240 revmap = {}
240 revmap = {}
241 for n in nodes:
241 for n in nodes:
242 revmap[source.changelog.rev(n)] = n
242 revmap[source.changelog.rev(n)] = n
243 os.unlink(seriespath)
243 os.unlink(seriespath)
244
244
245 self.apply(repo, source, revmap, merges, opts)
245 self.apply(repo, source, revmap, merges, opts)
246
246
247 def recover(self, repo):
247 def recover(self, repo):
248 '''commit working directory using journal metadata'''
248 '''commit working directory using journal metadata'''
249 node, user, date, message, parents = self.readlog()
249 node, user, date, message, parents = self.readlog()
250 merge = len(parents) == 2
250 merge = len(parents) == 2
251
251
252 if not user or not date or not message or not parents[0]:
252 if not user or not date or not message or not parents[0]:
253 raise util.Abort(_('transplant log file is corrupt'))
253 raise util.Abort(_('transplant log file is corrupt'))
254
254
255 wlock = repo.wlock()
255 wlock = repo.wlock()
256 p1, p2 = repo.dirstate.parents()
256 p1, p2 = repo.dirstate.parents()
257 if p1 != parents[0]:
257 if p1 != parents[0]:
258 raise util.Abort(_('working dir not at transplant parent %s') %
258 raise util.Abort(_('working dir not at transplant parent %s') %
259 revlog.hex(parents[0]))
259 revlog.hex(parents[0]))
260 if merge:
260 if merge:
261 repo.dirstate.setparents(p1, parents[1])
261 repo.dirstate.setparents(p1, parents[1])
262 n = repo.commit(None, message, user, date, wlock=wlock)
262 n = repo.commit(None, message, user, date, wlock=wlock)
263 if not n:
263 if not n:
264 raise util.Abort(_('commit failed'))
264 raise util.Abort(_('commit failed'))
265 if not merge:
265 if not merge:
266 self.transplants.set(n, node)
266 self.transplants.set(n, node)
267 self.unlog()
267 self.unlog()
268
268
269 return n, node
269 return n, node
270
270
271 def readseries(self):
271 def readseries(self):
272 nodes = []
272 nodes = []
273 merges = []
273 merges = []
274 cur = nodes
274 cur = nodes
275 for line in self.opener('series').read().splitlines():
275 for line in self.opener('series').read().splitlines():
276 if line.startswith('# Merges'):
276 if line.startswith('# Merges'):
277 cur = merges
277 cur = merges
278 continue
278 continue
279 cur.append(revlog.bin(line))
279 cur.append(revlog.bin(line))
280
280
281 return (nodes, merges)
281 return (nodes, merges)
282
282
283 def saveseries(self, revmap, merges):
283 def saveseries(self, revmap, merges):
284 if not revmap:
284 if not revmap:
285 return
285 return
286
286
287 if not os.path.isdir(self.path):
287 if not os.path.isdir(self.path):
288 os.mkdir(self.path)
288 os.mkdir(self.path)
289 series = self.opener('series', 'w')
289 series = self.opener('series', 'w')
290 revs = revmap.keys()
290 revs = revmap.keys()
291 revs.sort()
291 revs.sort()
292 for rev in revs:
292 for rev in revs:
293 series.write(revlog.hex(revmap[rev]) + '\n')
293 series.write(revlog.hex(revmap[rev]) + '\n')
294 if merges:
294 if merges:
295 series.write('# Merges\n')
295 series.write('# Merges\n')
296 for m in merges:
296 for m in merges:
297 series.write(revlog.hex(m) + '\n')
297 series.write(revlog.hex(m) + '\n')
298 series.close()
298 series.close()
299
299
300 def log(self, changelog, p1, p2, merge=False):
300 def log(self, changelog, p1, p2, merge=False):
301 '''journal changelog metadata for later recover'''
301 '''journal changelog metadata for later recover'''
302
302
303 if not os.path.isdir(self.path):
303 if not os.path.isdir(self.path):
304 os.mkdir(self.path)
304 os.mkdir(self.path)
305 fp = self.opener('journal', 'w')
305 fp = self.opener('journal', 'w')
306 fp.write('# User %s\n' % changelog[1])
306 fp.write('# User %s\n' % changelog[1])
307 fp.write('# Date %d %d\n' % changelog[2])
307 fp.write('# Date %d %d\n' % changelog[2])
308 fp.write('# Node ID %s\n' % revlog.hex(p2))
308 fp.write('# Node ID %s\n' % revlog.hex(p2))
309 fp.write('# Parent ' + revlog.hex(p1) + '\n')
309 fp.write('# Parent ' + revlog.hex(p1) + '\n')
310 if merge:
310 if merge:
311 fp.write('# Parent ' + revlog.hex(p2) + '\n')
311 fp.write('# Parent ' + revlog.hex(p2) + '\n')
312 fp.write(changelog[4].rstrip() + '\n')
312 fp.write(changelog[4].rstrip() + '\n')
313 fp.close()
313 fp.close()
314
314
315 def readlog(self):
315 def readlog(self):
316 parents = []
316 parents = []
317 message = []
317 message = []
318 for line in self.opener('journal').read().splitlines():
318 for line in self.opener('journal').read().splitlines():
319 if line.startswith('# User '):
319 if line.startswith('# User '):
320 user = line[7:]
320 user = line[7:]
321 elif line.startswith('# Date '):
321 elif line.startswith('# Date '):
322 date = line[7:]
322 date = line[7:]
323 elif line.startswith('# Node ID '):
323 elif line.startswith('# Node ID '):
324 node = revlog.bin(line[10:])
324 node = revlog.bin(line[10:])
325 elif line.startswith('# Parent '):
325 elif line.startswith('# Parent '):
326 parents.append(revlog.bin(line[9:]))
326 parents.append(revlog.bin(line[9:]))
327 else:
327 else:
328 message.append(line)
328 message.append(line)
329 return (node, user, date, '\n'.join(message), parents)
329 return (node, user, date, '\n'.join(message), parents)
330
330
331 def unlog(self):
331 def unlog(self):
332 '''remove changelog journal'''
332 '''remove changelog journal'''
333 absdst = os.path.join(self.path, 'journal')
333 absdst = os.path.join(self.path, 'journal')
334 if os.path.exists(absdst):
334 if os.path.exists(absdst):
335 os.unlink(absdst)
335 os.unlink(absdst)
336
336
337 def transplantfilter(self, repo, source, root):
337 def transplantfilter(self, repo, source, root):
338 def matchfn(node):
338 def matchfn(node):
339 if self.applied(repo, node, root):
339 if self.applied(repo, node, root):
340 return False
340 return False
341 if source.changelog.parents(node)[1] != revlog.nullid:
341 if source.changelog.parents(node)[1] != revlog.nullid:
342 return False
342 return False
343 extra = source.changelog.read(node)[5]
343 extra = source.changelog.read(node)[5]
344 cnode = extra.get('transplant_source')
344 cnode = extra.get('transplant_source')
345 if cnode and self.applied(repo, cnode, root):
345 if cnode and self.applied(repo, cnode, root):
346 return False
346 return False
347 return True
347 return True
348
348
349 return matchfn
349 return matchfn
350
350
351 def hasnode(repo, node):
351 def hasnode(repo, node):
352 try:
352 try:
353 return repo.changelog.rev(node) != None
353 return repo.changelog.rev(node) != None
354 except revlog.RevlogError:
354 except revlog.RevlogError:
355 return False
355 return False
356
356
357 def browserevs(ui, repo, nodes, opts):
357 def browserevs(ui, repo, nodes, opts):
358 '''interactively transplant changesets'''
358 '''interactively transplant changesets'''
359 def browsehelp(ui):
359 def browsehelp(ui):
360 ui.write('y: transplant this changeset\n'
360 ui.write('y: transplant this changeset\n'
361 'n: skip this changeset\n'
361 'n: skip this changeset\n'
362 'm: merge at this changeset\n'
362 'm: merge at this changeset\n'
363 'p: show patch\n'
363 'p: show patch\n'
364 'c: commit selected changesets\n'
364 'c: commit selected changesets\n'
365 'q: cancel transplant\n'
365 'q: cancel transplant\n'
366 '?: show this help\n')
366 '?: show this help\n')
367
367
368 displayer = commands.show_changeset(ui, repo, opts)
368 displayer = cmdutil.show_changeset(ui, repo, opts)
369 transplants = []
369 transplants = []
370 merges = []
370 merges = []
371 for node in nodes:
371 for node in nodes:
372 displayer.show(changenode=node)
372 displayer.show(changenode=node)
373 action = None
373 action = None
374 while not action:
374 while not action:
375 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
375 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
376 if action == '?':
376 if action == '?':
377 browsehelp(ui)
377 browsehelp(ui)
378 action = None
378 action = None
379 elif action == 'p':
379 elif action == 'p':
380 parent = repo.changelog.parents(node)[0]
380 parent = repo.changelog.parents(node)[0]
381 patch.diff(repo, parent, node)
381 patch.diff(repo, parent, node)
382 action = None
382 action = None
383 elif action not in ('y', 'n', 'm', 'c', 'q'):
383 elif action not in ('y', 'n', 'm', 'c', 'q'):
384 ui.write('no such option\n')
384 ui.write('no such option\n')
385 action = None
385 action = None
386 if action == 'y':
386 if action == 'y':
387 transplants.append(node)
387 transplants.append(node)
388 elif action == 'm':
388 elif action == 'm':
389 merges.append(node)
389 merges.append(node)
390 elif action == 'c':
390 elif action == 'c':
391 break
391 break
392 elif action == 'q':
392 elif action == 'q':
393 transplants = ()
393 transplants = ()
394 merges = ()
394 merges = ()
395 break
395 break
396 return (transplants, merges)
396 return (transplants, merges)
397
397
398 def transplant(ui, repo, *revs, **opts):
398 def transplant(ui, repo, *revs, **opts):
399 '''transplant changesets from another branch
399 '''transplant changesets from another branch
400
400
401 Selected changesets will be applied on top of the current working
401 Selected changesets will be applied on top of the current working
402 directory with the log of the original changeset. If --log is
402 directory with the log of the original changeset. If --log is
403 specified, log messages will have a comment appended of the form:
403 specified, log messages will have a comment appended of the form:
404
404
405 (transplanted from CHANGESETHASH)
405 (transplanted from CHANGESETHASH)
406
406
407 You can rewrite the changelog message with the --filter option.
407 You can rewrite the changelog message with the --filter option.
408 Its argument will be invoked with the current changelog message
408 Its argument will be invoked with the current changelog message
409 as $1 and the patch as $2.
409 as $1 and the patch as $2.
410
410
411 If --source is specified, selects changesets from the named
411 If --source is specified, selects changesets from the named
412 repository. If --branch is specified, selects changesets from the
412 repository. If --branch is specified, selects changesets from the
413 branch holding the named revision, up to that revision. If --all
413 branch holding the named revision, up to that revision. If --all
414 is specified, all changesets on the branch will be transplanted,
414 is specified, all changesets on the branch will be transplanted,
415 otherwise you will be prompted to select the changesets you want.
415 otherwise you will be prompted to select the changesets you want.
416
416
417 hg transplant --branch REVISION --all will rebase the selected branch
417 hg transplant --branch REVISION --all will rebase the selected branch
418 (up to the named revision) onto your current working directory.
418 (up to the named revision) onto your current working directory.
419
419
420 You can optionally mark selected transplanted changesets as
420 You can optionally mark selected transplanted changesets as
421 merge changesets. You will not be prompted to transplant any
421 merge changesets. You will not be prompted to transplant any
422 ancestors of a merged transplant, and you can merge descendants
422 ancestors of a merged transplant, and you can merge descendants
423 of them normally instead of transplanting them.
423 of them normally instead of transplanting them.
424
424
425 If no merges or revisions are provided, hg transplant will start
425 If no merges or revisions are provided, hg transplant will start
426 an interactive changeset browser.
426 an interactive changeset browser.
427
427
428 If a changeset application fails, you can fix the merge by hand and
428 If a changeset application fails, you can fix the merge by hand and
429 then resume where you left off by calling hg transplant --continue.
429 then resume where you left off by calling hg transplant --continue.
430 '''
430 '''
431 def getoneitem(opts, item, errmsg):
431 def getoneitem(opts, item, errmsg):
432 val = opts.get(item)
432 val = opts.get(item)
433 if val:
433 if val:
434 if len(val) > 1:
434 if len(val) > 1:
435 raise util.Abort(errmsg)
435 raise util.Abort(errmsg)
436 else:
436 else:
437 return val[0]
437 return val[0]
438
438
439 def getremotechanges(repo, url):
439 def getremotechanges(repo, url):
440 sourcerepo = ui.expandpath(url)
440 sourcerepo = ui.expandpath(url)
441 source = hg.repository(ui, sourcerepo)
441 source = hg.repository(ui, sourcerepo)
442 incoming = repo.findincoming(source, force=True)
442 incoming = repo.findincoming(source, force=True)
443 if not incoming:
443 if not incoming:
444 return (source, None, None)
444 return (source, None, None)
445
445
446 bundle = None
446 bundle = None
447 if not source.local():
447 if not source.local():
448 cg = source.changegroup(incoming, 'incoming')
448 cg = source.changegroup(incoming, 'incoming')
449 bundle = commands.write_bundle(cg, compress=False)
449 bundle = commands.write_bundle(cg, compress=False)
450 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
450 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
451
451
452 return (source, incoming, bundle)
452 return (source, incoming, bundle)
453
453
454 def incwalk(repo, incoming, branches, match=util.always):
454 def incwalk(repo, incoming, branches, match=util.always):
455 if not branches:
455 if not branches:
456 branches=None
456 branches=None
457 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
457 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
458 if match(node):
458 if match(node):
459 yield node
459 yield node
460
460
461 def transplantwalk(repo, root, branches, match=util.always):
461 def transplantwalk(repo, root, branches, match=util.always):
462 if not branches:
462 if not branches:
463 branches = repo.heads()
463 branches = repo.heads()
464 ancestors = []
464 ancestors = []
465 for branch in branches:
465 for branch in branches:
466 ancestors.append(repo.changelog.ancestor(root, branch))
466 ancestors.append(repo.changelog.ancestor(root, branch))
467 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
467 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
468 if match(node):
468 if match(node):
469 yield node
469 yield node
470
470
471 def checkopts(opts, revs):
471 def checkopts(opts, revs):
472 if opts.get('continue'):
472 if opts.get('continue'):
473 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
473 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
474 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
474 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
475 return
475 return
476 if not (opts.get('source') or revs or
476 if not (opts.get('source') or revs or
477 opts.get('merge') or opts.get('branch')):
477 opts.get('merge') or opts.get('branch')):
478 raise util.Abort(_('no source URL, branch tag or revision list provided'))
478 raise util.Abort(_('no source URL, branch tag or revision list provided'))
479 if opts.get('all'):
479 if opts.get('all'):
480 if not opts.get('branch'):
480 if not opts.get('branch'):
481 raise util.Abort(_('--all requires a branch revision'))
481 raise util.Abort(_('--all requires a branch revision'))
482 if revs:
482 if revs:
483 raise util.Abort(_('--all is incompatible with a revision list'))
483 raise util.Abort(_('--all is incompatible with a revision list'))
484
484
485 checkopts(opts, revs)
485 checkopts(opts, revs)
486
486
487 if not opts.get('log'):
487 if not opts.get('log'):
488 opts['log'] = ui.config('transplant', 'log')
488 opts['log'] = ui.config('transplant', 'log')
489 if not opts.get('filter'):
489 if not opts.get('filter'):
490 opts['filter'] = ui.config('transplant', 'filter')
490 opts['filter'] = ui.config('transplant', 'filter')
491
491
492 tp = transplanter(ui, repo)
492 tp = transplanter(ui, repo)
493
493
494 p1, p2 = repo.dirstate.parents()
494 p1, p2 = repo.dirstate.parents()
495 if p1 == revlog.nullid:
495 if p1 == revlog.nullid:
496 raise util.Abort(_('no revision checked out'))
496 raise util.Abort(_('no revision checked out'))
497 if not opts.get('continue'):
497 if not opts.get('continue'):
498 if p2 != revlog.nullid:
498 if p2 != revlog.nullid:
499 raise util.Abort(_('outstanding uncommitted merges'))
499 raise util.Abort(_('outstanding uncommitted merges'))
500 m, a, r, d = repo.status()[:4]
500 m, a, r, d = repo.status()[:4]
501 if m or a or r or d:
501 if m or a or r or d:
502 raise util.Abort(_('outstanding local changes'))
502 raise util.Abort(_('outstanding local changes'))
503
503
504 bundle = None
504 bundle = None
505 source = opts.get('source')
505 source = opts.get('source')
506 if source:
506 if source:
507 (source, incoming, bundle) = getremotechanges(repo, source)
507 (source, incoming, bundle) = getremotechanges(repo, source)
508 else:
508 else:
509 source = repo
509 source = repo
510
510
511 try:
511 try:
512 if opts.get('continue'):
512 if opts.get('continue'):
513 n, node = tp.resume(repo, source, opts)
513 n, node = tp.resume(repo, source, opts)
514 return
514 return
515
515
516 tf=tp.transplantfilter(repo, source, p1)
516 tf=tp.transplantfilter(repo, source, p1)
517 if opts.get('prune'):
517 if opts.get('prune'):
518 prune = [source.lookup(r)
518 prune = [source.lookup(r)
519 for r in cmdutil.revrange(source, opts.get('prune'))]
519 for r in cmdutil.revrange(source, opts.get('prune'))]
520 matchfn = lambda x: tf(x) and x not in prune
520 matchfn = lambda x: tf(x) and x not in prune
521 else:
521 else:
522 matchfn = tf
522 matchfn = tf
523 branches = map(source.lookup, opts.get('branch', ()))
523 branches = map(source.lookup, opts.get('branch', ()))
524 merges = map(source.lookup, opts.get('merge', ()))
524 merges = map(source.lookup, opts.get('merge', ()))
525 revmap = {}
525 revmap = {}
526 if revs:
526 if revs:
527 for r in cmdutil.revrange(source, revs):
527 for r in cmdutil.revrange(source, revs):
528 revmap[int(r)] = source.lookup(r)
528 revmap[int(r)] = source.lookup(r)
529 elif opts.get('all') or not merges:
529 elif opts.get('all') or not merges:
530 if source != repo:
530 if source != repo:
531 alltransplants = incwalk(source, incoming, branches, match=matchfn)
531 alltransplants = incwalk(source, incoming, branches, match=matchfn)
532 else:
532 else:
533 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
533 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
534 if opts.get('all'):
534 if opts.get('all'):
535 revs = alltransplants
535 revs = alltransplants
536 else:
536 else:
537 revs, newmerges = browserevs(ui, source, alltransplants, opts)
537 revs, newmerges = browserevs(ui, source, alltransplants, opts)
538 merges.extend(newmerges)
538 merges.extend(newmerges)
539 for r in revs:
539 for r in revs:
540 revmap[source.changelog.rev(r)] = r
540 revmap[source.changelog.rev(r)] = r
541 for r in merges:
541 for r in merges:
542 revmap[source.changelog.rev(r)] = r
542 revmap[source.changelog.rev(r)] = r
543
543
544 revs = revmap.keys()
544 revs = revmap.keys()
545 revs.sort()
545 revs.sort()
546 pulls = []
546 pulls = []
547
547
548 tp.apply(repo, source, revmap, merges, opts)
548 tp.apply(repo, source, revmap, merges, opts)
549 finally:
549 finally:
550 if bundle:
550 if bundle:
551 os.unlink(bundle)
551 os.unlink(bundle)
552
552
553 cmdtable = {
553 cmdtable = {
554 "transplant":
554 "transplant":
555 (transplant,
555 (transplant,
556 [('s', 'source', '', _('pull patches from REPOSITORY')),
556 [('s', 'source', '', _('pull patches from REPOSITORY')),
557 ('b', 'branch', [], _('pull patches from branch BRANCH')),
557 ('b', 'branch', [], _('pull patches from branch BRANCH')),
558 ('a', 'all', None, _('pull all changesets up to BRANCH')),
558 ('a', 'all', None, _('pull all changesets up to BRANCH')),
559 ('p', 'prune', [], _('skip over REV')),
559 ('p', 'prune', [], _('skip over REV')),
560 ('m', 'merge', [], _('merge at REV')),
560 ('m', 'merge', [], _('merge at REV')),
561 ('', 'log', None, _('append transplant info to log message')),
561 ('', 'log', None, _('append transplant info to log message')),
562 ('c', 'continue', None, _('continue last transplant session after repair')),
562 ('c', 'continue', None, _('continue last transplant session after repair')),
563 ('', 'filter', '', _('filter changesets through FILTER'))],
563 ('', 'filter', '', _('filter changesets through FILTER'))],
564 _('hg transplant [-s REPOSITORY] [-b BRANCH] [-p REV] [-m REV] [-n] REV...'))
564 _('hg transplant [-s REPOSITORY] [-b BRANCH] [-p REV] [-m REV] [-n] REV...'))
565 }
565 }
General Comments 0
You need to be logged in to leave comments. Login now