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