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