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