##// END OF EJS Templates
hgext: use templatekeyword to mark a function as template keyword...
FUJIWARA Katsunori -
r28540:012411b9 default
parent child Browse files
Show More
@@ -1,458 +1,458 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 '''import revisions from foreign VCS repositories into Mercurial'''
8 '''import revisions from foreign VCS repositories into Mercurial'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial import (
12 from mercurial import (
13 cmdutil,
13 cmdutil,
14 templatekw,
14 registrar,
15 )
15 )
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17
17
18 from . import (
18 from . import (
19 convcmd,
19 convcmd,
20 cvsps,
20 cvsps,
21 subversion,
21 subversion,
22 )
22 )
23
23
24 cmdtable = {}
24 cmdtable = {}
25 command = cmdutil.command(cmdtable)
25 command = cmdutil.command(cmdtable)
26 # Note for extension authors: ONLY specify testedwith = 'internal' for
26 # Note for extension authors: ONLY specify testedwith = 'internal' for
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
28 # be specifying the version(s) of Mercurial they are tested with, or
28 # be specifying the version(s) of Mercurial they are tested with, or
29 # leave the attribute unspecified.
29 # leave the attribute unspecified.
30 testedwith = 'internal'
30 testedwith = 'internal'
31
31
32 # Commands definition was moved elsewhere to ease demandload job.
32 # Commands definition was moved elsewhere to ease demandload job.
33
33
34 @command('convert',
34 @command('convert',
35 [('', 'authors', '',
35 [('', 'authors', '',
36 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
36 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
37 _('FILE')),
37 _('FILE')),
38 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
38 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
39 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
39 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
40 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
40 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
41 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
41 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
42 ('', 'filemap', '', _('remap file names using contents of file'),
42 ('', 'filemap', '', _('remap file names using contents of file'),
43 _('FILE')),
43 _('FILE')),
44 ('', 'full', None,
44 ('', 'full', None,
45 _('apply filemap changes by converting all files again')),
45 _('apply filemap changes by converting all files again')),
46 ('', 'splicemap', '', _('splice synthesized history into place'),
46 ('', 'splicemap', '', _('splice synthesized history into place'),
47 _('FILE')),
47 _('FILE')),
48 ('', 'branchmap', '', _('change branch names while converting'),
48 ('', 'branchmap', '', _('change branch names while converting'),
49 _('FILE')),
49 _('FILE')),
50 ('', 'branchsort', None, _('try to sort changesets by branches')),
50 ('', 'branchsort', None, _('try to sort changesets by branches')),
51 ('', 'datesort', None, _('try to sort changesets by date')),
51 ('', 'datesort', None, _('try to sort changesets by date')),
52 ('', 'sourcesort', None, _('preserve source changesets order')),
52 ('', 'sourcesort', None, _('preserve source changesets order')),
53 ('', 'closesort', None, _('try to reorder closed revisions'))],
53 ('', 'closesort', None, _('try to reorder closed revisions'))],
54 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
54 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
55 norepo=True)
55 norepo=True)
56 def convert(ui, src, dest=None, revmapfile=None, **opts):
56 def convert(ui, src, dest=None, revmapfile=None, **opts):
57 """convert a foreign SCM repository to a Mercurial one.
57 """convert a foreign SCM repository to a Mercurial one.
58
58
59 Accepted source formats [identifiers]:
59 Accepted source formats [identifiers]:
60
60
61 - Mercurial [hg]
61 - Mercurial [hg]
62 - CVS [cvs]
62 - CVS [cvs]
63 - Darcs [darcs]
63 - Darcs [darcs]
64 - git [git]
64 - git [git]
65 - Subversion [svn]
65 - Subversion [svn]
66 - Monotone [mtn]
66 - Monotone [mtn]
67 - GNU Arch [gnuarch]
67 - GNU Arch [gnuarch]
68 - Bazaar [bzr]
68 - Bazaar [bzr]
69 - Perforce [p4]
69 - Perforce [p4]
70
70
71 Accepted destination formats [identifiers]:
71 Accepted destination formats [identifiers]:
72
72
73 - Mercurial [hg]
73 - Mercurial [hg]
74 - Subversion [svn] (history on branches is not preserved)
74 - Subversion [svn] (history on branches is not preserved)
75
75
76 If no revision is given, all revisions will be converted.
76 If no revision is given, all revisions will be converted.
77 Otherwise, convert will only import up to the named revision
77 Otherwise, convert will only import up to the named revision
78 (given in a format understood by the source).
78 (given in a format understood by the source).
79
79
80 If no destination directory name is specified, it defaults to the
80 If no destination directory name is specified, it defaults to the
81 basename of the source with ``-hg`` appended. If the destination
81 basename of the source with ``-hg`` appended. If the destination
82 repository doesn't exist, it will be created.
82 repository doesn't exist, it will be created.
83
83
84 By default, all sources except Mercurial will use --branchsort.
84 By default, all sources except Mercurial will use --branchsort.
85 Mercurial uses --sourcesort to preserve original revision numbers
85 Mercurial uses --sourcesort to preserve original revision numbers
86 order. Sort modes have the following effects:
86 order. Sort modes have the following effects:
87
87
88 --branchsort convert from parent to child revision when possible,
88 --branchsort convert from parent to child revision when possible,
89 which means branches are usually converted one after
89 which means branches are usually converted one after
90 the other. It generates more compact repositories.
90 the other. It generates more compact repositories.
91
91
92 --datesort sort revisions by date. Converted repositories have
92 --datesort sort revisions by date. Converted repositories have
93 good-looking changelogs but are often an order of
93 good-looking changelogs but are often an order of
94 magnitude larger than the same ones generated by
94 magnitude larger than the same ones generated by
95 --branchsort.
95 --branchsort.
96
96
97 --sourcesort try to preserve source revisions order, only
97 --sourcesort try to preserve source revisions order, only
98 supported by Mercurial sources.
98 supported by Mercurial sources.
99
99
100 --closesort try to move closed revisions as close as possible
100 --closesort try to move closed revisions as close as possible
101 to parent branches, only supported by Mercurial
101 to parent branches, only supported by Mercurial
102 sources.
102 sources.
103
103
104 If ``REVMAP`` isn't given, it will be put in a default location
104 If ``REVMAP`` isn't given, it will be put in a default location
105 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
105 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
106 text file that maps each source commit ID to the destination ID
106 text file that maps each source commit ID to the destination ID
107 for that revision, like so::
107 for that revision, like so::
108
108
109 <source ID> <destination ID>
109 <source ID> <destination ID>
110
110
111 If the file doesn't exist, it's automatically created. It's
111 If the file doesn't exist, it's automatically created. It's
112 updated on each commit copied, so :hg:`convert` can be interrupted
112 updated on each commit copied, so :hg:`convert` can be interrupted
113 and can be run repeatedly to copy new commits.
113 and can be run repeatedly to copy new commits.
114
114
115 The authormap is a simple text file that maps each source commit
115 The authormap is a simple text file that maps each source commit
116 author to a destination commit author. It is handy for source SCMs
116 author to a destination commit author. It is handy for source SCMs
117 that use unix logins to identify authors (e.g.: CVS). One line per
117 that use unix logins to identify authors (e.g.: CVS). One line per
118 author mapping and the line format is::
118 author mapping and the line format is::
119
119
120 source author = destination author
120 source author = destination author
121
121
122 Empty lines and lines starting with a ``#`` are ignored.
122 Empty lines and lines starting with a ``#`` are ignored.
123
123
124 The filemap is a file that allows filtering and remapping of files
124 The filemap is a file that allows filtering and remapping of files
125 and directories. Each line can contain one of the following
125 and directories. Each line can contain one of the following
126 directives::
126 directives::
127
127
128 include path/to/file-or-dir
128 include path/to/file-or-dir
129
129
130 exclude path/to/file-or-dir
130 exclude path/to/file-or-dir
131
131
132 rename path/to/source path/to/destination
132 rename path/to/source path/to/destination
133
133
134 Comment lines start with ``#``. A specified path matches if it
134 Comment lines start with ``#``. A specified path matches if it
135 equals the full relative name of a file or one of its parent
135 equals the full relative name of a file or one of its parent
136 directories. The ``include`` or ``exclude`` directive with the
136 directories. The ``include`` or ``exclude`` directive with the
137 longest matching path applies, so line order does not matter.
137 longest matching path applies, so line order does not matter.
138
138
139 The ``include`` directive causes a file, or all files under a
139 The ``include`` directive causes a file, or all files under a
140 directory, to be included in the destination repository. The default
140 directory, to be included in the destination repository. The default
141 if there are no ``include`` statements is to include everything.
141 if there are no ``include`` statements is to include everything.
142 If there are any ``include`` statements, nothing else is included.
142 If there are any ``include`` statements, nothing else is included.
143 The ``exclude`` directive causes files or directories to
143 The ``exclude`` directive causes files or directories to
144 be omitted. The ``rename`` directive renames a file or directory if
144 be omitted. The ``rename`` directive renames a file or directory if
145 it is converted. To rename from a subdirectory into the root of
145 it is converted. To rename from a subdirectory into the root of
146 the repository, use ``.`` as the path to rename to.
146 the repository, use ``.`` as the path to rename to.
147
147
148 ``--full`` will make sure the converted changesets contain exactly
148 ``--full`` will make sure the converted changesets contain exactly
149 the right files with the right content. It will make a full
149 the right files with the right content. It will make a full
150 conversion of all files, not just the ones that have
150 conversion of all files, not just the ones that have
151 changed. Files that already are correct will not be changed. This
151 changed. Files that already are correct will not be changed. This
152 can be used to apply filemap changes when converting
152 can be used to apply filemap changes when converting
153 incrementally. This is currently only supported for Mercurial and
153 incrementally. This is currently only supported for Mercurial and
154 Subversion.
154 Subversion.
155
155
156 The splicemap is a file that allows insertion of synthetic
156 The splicemap is a file that allows insertion of synthetic
157 history, letting you specify the parents of a revision. This is
157 history, letting you specify the parents of a revision. This is
158 useful if you want to e.g. give a Subversion merge two parents, or
158 useful if you want to e.g. give a Subversion merge two parents, or
159 graft two disconnected series of history together. Each entry
159 graft two disconnected series of history together. Each entry
160 contains a key, followed by a space, followed by one or two
160 contains a key, followed by a space, followed by one or two
161 comma-separated values::
161 comma-separated values::
162
162
163 key parent1, parent2
163 key parent1, parent2
164
164
165 The key is the revision ID in the source
165 The key is the revision ID in the source
166 revision control system whose parents should be modified (same
166 revision control system whose parents should be modified (same
167 format as a key in .hg/shamap). The values are the revision IDs
167 format as a key in .hg/shamap). The values are the revision IDs
168 (in either the source or destination revision control system) that
168 (in either the source or destination revision control system) that
169 should be used as the new parents for that node. For example, if
169 should be used as the new parents for that node. For example, if
170 you have merged "release-1.0" into "trunk", then you should
170 you have merged "release-1.0" into "trunk", then you should
171 specify the revision on "trunk" as the first parent and the one on
171 specify the revision on "trunk" as the first parent and the one on
172 the "release-1.0" branch as the second.
172 the "release-1.0" branch as the second.
173
173
174 The branchmap is a file that allows you to rename a branch when it is
174 The branchmap is a file that allows you to rename a branch when it is
175 being brought in from whatever external repository. When used in
175 being brought in from whatever external repository. When used in
176 conjunction with a splicemap, it allows for a powerful combination
176 conjunction with a splicemap, it allows for a powerful combination
177 to help fix even the most badly mismanaged repositories and turn them
177 to help fix even the most badly mismanaged repositories and turn them
178 into nicely structured Mercurial repositories. The branchmap contains
178 into nicely structured Mercurial repositories. The branchmap contains
179 lines of the form::
179 lines of the form::
180
180
181 original_branch_name new_branch_name
181 original_branch_name new_branch_name
182
182
183 where "original_branch_name" is the name of the branch in the
183 where "original_branch_name" is the name of the branch in the
184 source repository, and "new_branch_name" is the name of the branch
184 source repository, and "new_branch_name" is the name of the branch
185 is the destination repository. No whitespace is allowed in the
185 is the destination repository. No whitespace is allowed in the
186 branch names. This can be used to (for instance) move code in one
186 branch names. This can be used to (for instance) move code in one
187 repository from "default" to a named branch.
187 repository from "default" to a named branch.
188
188
189 Mercurial Source
189 Mercurial Source
190 ################
190 ################
191
191
192 The Mercurial source recognizes the following configuration
192 The Mercurial source recognizes the following configuration
193 options, which you can set on the command line with ``--config``:
193 options, which you can set on the command line with ``--config``:
194
194
195 :convert.hg.ignoreerrors: ignore integrity errors when reading.
195 :convert.hg.ignoreerrors: ignore integrity errors when reading.
196 Use it to fix Mercurial repositories with missing revlogs, by
196 Use it to fix Mercurial repositories with missing revlogs, by
197 converting from and to Mercurial. Default is False.
197 converting from and to Mercurial. Default is False.
198
198
199 :convert.hg.saverev: store original revision ID in changeset
199 :convert.hg.saverev: store original revision ID in changeset
200 (forces target IDs to change). It takes a boolean argument and
200 (forces target IDs to change). It takes a boolean argument and
201 defaults to False.
201 defaults to False.
202
202
203 :convert.hg.startrev: specify the initial Mercurial revision.
203 :convert.hg.startrev: specify the initial Mercurial revision.
204 The default is 0.
204 The default is 0.
205
205
206 :convert.hg.revs: revset specifying the source revisions to convert.
206 :convert.hg.revs: revset specifying the source revisions to convert.
207
207
208 CVS Source
208 CVS Source
209 ##########
209 ##########
210
210
211 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
211 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
212 to indicate the starting point of what will be converted. Direct
212 to indicate the starting point of what will be converted. Direct
213 access to the repository files is not needed, unless of course the
213 access to the repository files is not needed, unless of course the
214 repository is ``:local:``. The conversion uses the top level
214 repository is ``:local:``. The conversion uses the top level
215 directory in the sandbox to find the CVS repository, and then uses
215 directory in the sandbox to find the CVS repository, and then uses
216 CVS rlog commands to find files to convert. This means that unless
216 CVS rlog commands to find files to convert. This means that unless
217 a filemap is given, all files under the starting directory will be
217 a filemap is given, all files under the starting directory will be
218 converted, and that any directory reorganization in the CVS
218 converted, and that any directory reorganization in the CVS
219 sandbox is ignored.
219 sandbox is ignored.
220
220
221 The following options can be used with ``--config``:
221 The following options can be used with ``--config``:
222
222
223 :convert.cvsps.cache: Set to False to disable remote log caching,
223 :convert.cvsps.cache: Set to False to disable remote log caching,
224 for testing and debugging purposes. Default is True.
224 for testing and debugging purposes. Default is True.
225
225
226 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
226 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
227 allowed between commits with identical user and log message in
227 allowed between commits with identical user and log message in
228 a single changeset. When very large files were checked in as
228 a single changeset. When very large files were checked in as
229 part of a changeset then the default may not be long enough.
229 part of a changeset then the default may not be long enough.
230 The default is 60.
230 The default is 60.
231
231
232 :convert.cvsps.mergeto: Specify a regular expression to which
232 :convert.cvsps.mergeto: Specify a regular expression to which
233 commit log messages are matched. If a match occurs, then the
233 commit log messages are matched. If a match occurs, then the
234 conversion process will insert a dummy revision merging the
234 conversion process will insert a dummy revision merging the
235 branch on which this log message occurs to the branch
235 branch on which this log message occurs to the branch
236 indicated in the regex. Default is ``{{mergetobranch
236 indicated in the regex. Default is ``{{mergetobranch
237 ([-\\w]+)}}``
237 ([-\\w]+)}}``
238
238
239 :convert.cvsps.mergefrom: Specify a regular expression to which
239 :convert.cvsps.mergefrom: Specify a regular expression to which
240 commit log messages are matched. If a match occurs, then the
240 commit log messages are matched. If a match occurs, then the
241 conversion process will add the most recent revision on the
241 conversion process will add the most recent revision on the
242 branch indicated in the regex as the second parent of the
242 branch indicated in the regex as the second parent of the
243 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
243 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
244
244
245 :convert.localtimezone: use local time (as determined by the TZ
245 :convert.localtimezone: use local time (as determined by the TZ
246 environment variable) for changeset date/times. The default
246 environment variable) for changeset date/times. The default
247 is False (use UTC).
247 is False (use UTC).
248
248
249 :hooks.cvslog: Specify a Python function to be called at the end of
249 :hooks.cvslog: Specify a Python function to be called at the end of
250 gathering the CVS log. The function is passed a list with the
250 gathering the CVS log. The function is passed a list with the
251 log entries, and can modify the entries in-place, or add or
251 log entries, and can modify the entries in-place, or add or
252 delete them.
252 delete them.
253
253
254 :hooks.cvschangesets: Specify a Python function to be called after
254 :hooks.cvschangesets: Specify a Python function to be called after
255 the changesets are calculated from the CVS log. The
255 the changesets are calculated from the CVS log. The
256 function is passed a list with the changeset entries, and can
256 function is passed a list with the changeset entries, and can
257 modify the changesets in-place, or add or delete them.
257 modify the changesets in-place, or add or delete them.
258
258
259 An additional "debugcvsps" Mercurial command allows the builtin
259 An additional "debugcvsps" Mercurial command allows the builtin
260 changeset merging code to be run without doing a conversion. Its
260 changeset merging code to be run without doing a conversion. Its
261 parameters and output are similar to that of cvsps 2.1. Please see
261 parameters and output are similar to that of cvsps 2.1. Please see
262 the command help for more details.
262 the command help for more details.
263
263
264 Subversion Source
264 Subversion Source
265 #################
265 #################
266
266
267 Subversion source detects classical trunk/branches/tags layouts.
267 Subversion source detects classical trunk/branches/tags layouts.
268 By default, the supplied ``svn://repo/path/`` source URL is
268 By default, the supplied ``svn://repo/path/`` source URL is
269 converted as a single branch. If ``svn://repo/path/trunk`` exists
269 converted as a single branch. If ``svn://repo/path/trunk`` exists
270 it replaces the default branch. If ``svn://repo/path/branches``
270 it replaces the default branch. If ``svn://repo/path/branches``
271 exists, its subdirectories are listed as possible branches. If
271 exists, its subdirectories are listed as possible branches. If
272 ``svn://repo/path/tags`` exists, it is looked for tags referencing
272 ``svn://repo/path/tags`` exists, it is looked for tags referencing
273 converted branches. Default ``trunk``, ``branches`` and ``tags``
273 converted branches. Default ``trunk``, ``branches`` and ``tags``
274 values can be overridden with following options. Set them to paths
274 values can be overridden with following options. Set them to paths
275 relative to the source URL, or leave them blank to disable auto
275 relative to the source URL, or leave them blank to disable auto
276 detection.
276 detection.
277
277
278 The following options can be set with ``--config``:
278 The following options can be set with ``--config``:
279
279
280 :convert.svn.branches: specify the directory containing branches.
280 :convert.svn.branches: specify the directory containing branches.
281 The default is ``branches``.
281 The default is ``branches``.
282
282
283 :convert.svn.tags: specify the directory containing tags. The
283 :convert.svn.tags: specify the directory containing tags. The
284 default is ``tags``.
284 default is ``tags``.
285
285
286 :convert.svn.trunk: specify the name of the trunk branch. The
286 :convert.svn.trunk: specify the name of the trunk branch. The
287 default is ``trunk``.
287 default is ``trunk``.
288
288
289 :convert.localtimezone: use local time (as determined by the TZ
289 :convert.localtimezone: use local time (as determined by the TZ
290 environment variable) for changeset date/times. The default
290 environment variable) for changeset date/times. The default
291 is False (use UTC).
291 is False (use UTC).
292
292
293 Source history can be retrieved starting at a specific revision,
293 Source history can be retrieved starting at a specific revision,
294 instead of being integrally converted. Only single branch
294 instead of being integrally converted. Only single branch
295 conversions are supported.
295 conversions are supported.
296
296
297 :convert.svn.startrev: specify start Subversion revision number.
297 :convert.svn.startrev: specify start Subversion revision number.
298 The default is 0.
298 The default is 0.
299
299
300 Git Source
300 Git Source
301 ##########
301 ##########
302
302
303 The Git importer converts commits from all reachable branches (refs
303 The Git importer converts commits from all reachable branches (refs
304 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
304 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
305 Branches are converted to bookmarks with the same name, with the
305 Branches are converted to bookmarks with the same name, with the
306 leading 'refs/heads' stripped. Git submodules are converted to Git
306 leading 'refs/heads' stripped. Git submodules are converted to Git
307 subrepos in Mercurial.
307 subrepos in Mercurial.
308
308
309 The following options can be set with ``--config``:
309 The following options can be set with ``--config``:
310
310
311 :convert.git.similarity: specify how similar files modified in a
311 :convert.git.similarity: specify how similar files modified in a
312 commit must be to be imported as renames or copies, as a
312 commit must be to be imported as renames or copies, as a
313 percentage between ``0`` (disabled) and ``100`` (files must be
313 percentage between ``0`` (disabled) and ``100`` (files must be
314 identical). For example, ``90`` means that a delete/add pair will
314 identical). For example, ``90`` means that a delete/add pair will
315 be imported as a rename if more than 90% of the file hasn't
315 be imported as a rename if more than 90% of the file hasn't
316 changed. The default is ``50``.
316 changed. The default is ``50``.
317
317
318 :convert.git.findcopiesharder: while detecting copies, look at all
318 :convert.git.findcopiesharder: while detecting copies, look at all
319 files in the working copy instead of just changed ones. This
319 files in the working copy instead of just changed ones. This
320 is very expensive for large projects, and is only effective when
320 is very expensive for large projects, and is only effective when
321 ``convert.git.similarity`` is greater than 0. The default is False.
321 ``convert.git.similarity`` is greater than 0. The default is False.
322
322
323 :convert.git.remoteprefix: remote refs are converted as bookmarks with
323 :convert.git.remoteprefix: remote refs are converted as bookmarks with
324 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
324 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
325 is 'remote'.
325 is 'remote'.
326
326
327 :convert.git.skipsubmodules: does not convert root level .gitmodules files
327 :convert.git.skipsubmodules: does not convert root level .gitmodules files
328 or files with 160000 mode indicating a submodule. Default is False.
328 or files with 160000 mode indicating a submodule. Default is False.
329
329
330 Perforce Source
330 Perforce Source
331 ###############
331 ###############
332
332
333 The Perforce (P4) importer can be given a p4 depot path or a
333 The Perforce (P4) importer can be given a p4 depot path or a
334 client specification as source. It will convert all files in the
334 client specification as source. It will convert all files in the
335 source to a flat Mercurial repository, ignoring labels, branches
335 source to a flat Mercurial repository, ignoring labels, branches
336 and integrations. Note that when a depot path is given you then
336 and integrations. Note that when a depot path is given you then
337 usually should specify a target directory, because otherwise the
337 usually should specify a target directory, because otherwise the
338 target may be named ``...-hg``.
338 target may be named ``...-hg``.
339
339
340 The following options can be set with ``--config``:
340 The following options can be set with ``--config``:
341
341
342 :convert.p4.encoding: specify the encoding to use when decoding standard
342 :convert.p4.encoding: specify the encoding to use when decoding standard
343 output of the Perforce command line tool. The default is default system
343 output of the Perforce command line tool. The default is default system
344 encoding.
344 encoding.
345
345
346 :convert.p4.startrev: specify initial Perforce revision (a
346 :convert.p4.startrev: specify initial Perforce revision (a
347 Perforce changelist number).
347 Perforce changelist number).
348
348
349 Mercurial Destination
349 Mercurial Destination
350 #####################
350 #####################
351
351
352 The Mercurial destination will recognize Mercurial subrepositories in the
352 The Mercurial destination will recognize Mercurial subrepositories in the
353 destination directory, and update the .hgsubstate file automatically if the
353 destination directory, and update the .hgsubstate file automatically if the
354 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
354 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
355 Converting a repository with subrepositories requires converting a single
355 Converting a repository with subrepositories requires converting a single
356 repository at a time, from the bottom up.
356 repository at a time, from the bottom up.
357
357
358 .. container:: verbose
358 .. container:: verbose
359
359
360 An example showing how to convert a repository with subrepositories::
360 An example showing how to convert a repository with subrepositories::
361
361
362 # so convert knows the type when it sees a non empty destination
362 # so convert knows the type when it sees a non empty destination
363 $ hg init converted
363 $ hg init converted
364
364
365 $ hg convert orig/sub1 converted/sub1
365 $ hg convert orig/sub1 converted/sub1
366 $ hg convert orig/sub2 converted/sub2
366 $ hg convert orig/sub2 converted/sub2
367 $ hg convert orig converted
367 $ hg convert orig converted
368
368
369 The following options are supported:
369 The following options are supported:
370
370
371 :convert.hg.clonebranches: dispatch source branches in separate
371 :convert.hg.clonebranches: dispatch source branches in separate
372 clones. The default is False.
372 clones. The default is False.
373
373
374 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
374 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
375 ``default``.
375 ``default``.
376
376
377 :convert.hg.usebranchnames: preserve branch names. The default is
377 :convert.hg.usebranchnames: preserve branch names. The default is
378 True.
378 True.
379
379
380 :convert.hg.sourcename: records the given string as a 'convert_source' extra
380 :convert.hg.sourcename: records the given string as a 'convert_source' extra
381 value on each commit made in the target repository. The default is None.
381 value on each commit made in the target repository. The default is None.
382
382
383 All Destinations
383 All Destinations
384 ################
384 ################
385
385
386 All destination types accept the following options:
386 All destination types accept the following options:
387
387
388 :convert.skiptags: does not convert tags from the source repo to the target
388 :convert.skiptags: does not convert tags from the source repo to the target
389 repo. The default is False.
389 repo. The default is False.
390 """
390 """
391 return convcmd.convert(ui, src, dest, revmapfile, **opts)
391 return convcmd.convert(ui, src, dest, revmapfile, **opts)
392
392
393 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
393 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
394 def debugsvnlog(ui, **opts):
394 def debugsvnlog(ui, **opts):
395 return subversion.debugsvnlog(ui, **opts)
395 return subversion.debugsvnlog(ui, **opts)
396
396
397 @command('debugcvsps',
397 @command('debugcvsps',
398 [
398 [
399 # Main options shared with cvsps-2.1
399 # Main options shared with cvsps-2.1
400 ('b', 'branches', [], _('only return changes on specified branches')),
400 ('b', 'branches', [], _('only return changes on specified branches')),
401 ('p', 'prefix', '', _('prefix to remove from file names')),
401 ('p', 'prefix', '', _('prefix to remove from file names')),
402 ('r', 'revisions', [],
402 ('r', 'revisions', [],
403 _('only return changes after or between specified tags')),
403 _('only return changes after or between specified tags')),
404 ('u', 'update-cache', None, _("update cvs log cache")),
404 ('u', 'update-cache', None, _("update cvs log cache")),
405 ('x', 'new-cache', None, _("create new cvs log cache")),
405 ('x', 'new-cache', None, _("create new cvs log cache")),
406 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
406 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
407 ('', 'root', '', _('specify cvsroot')),
407 ('', 'root', '', _('specify cvsroot')),
408 # Options specific to builtin cvsps
408 # Options specific to builtin cvsps
409 ('', 'parents', '', _('show parent changesets')),
409 ('', 'parents', '', _('show parent changesets')),
410 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
410 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
411 # Options that are ignored for compatibility with cvsps-2.1
411 # Options that are ignored for compatibility with cvsps-2.1
412 ('A', 'cvs-direct', None, _('ignored for compatibility')),
412 ('A', 'cvs-direct', None, _('ignored for compatibility')),
413 ],
413 ],
414 _('hg debugcvsps [OPTION]... [PATH]...'),
414 _('hg debugcvsps [OPTION]... [PATH]...'),
415 norepo=True)
415 norepo=True)
416 def debugcvsps(ui, *args, **opts):
416 def debugcvsps(ui, *args, **opts):
417 '''create changeset information from CVS
417 '''create changeset information from CVS
418
418
419 This command is intended as a debugging tool for the CVS to
419 This command is intended as a debugging tool for the CVS to
420 Mercurial converter, and can be used as a direct replacement for
420 Mercurial converter, and can be used as a direct replacement for
421 cvsps.
421 cvsps.
422
422
423 Hg debugcvsps reads the CVS rlog for current directory (or any
423 Hg debugcvsps reads the CVS rlog for current directory (or any
424 named directory) in the CVS repository, and converts the log to a
424 named directory) in the CVS repository, and converts the log to a
425 series of changesets based on matching commit log entries and
425 series of changesets based on matching commit log entries and
426 dates.'''
426 dates.'''
427 return cvsps.debugcvsps(ui, *args, **opts)
427 return cvsps.debugcvsps(ui, *args, **opts)
428
428
429 def kwconverted(ctx, name):
429 def kwconverted(ctx, name):
430 rev = ctx.extra().get('convert_revision', '')
430 rev = ctx.extra().get('convert_revision', '')
431 if rev.startswith('svn:'):
431 if rev.startswith('svn:'):
432 if name == 'svnrev':
432 if name == 'svnrev':
433 return str(subversion.revsplit(rev)[2])
433 return str(subversion.revsplit(rev)[2])
434 elif name == 'svnpath':
434 elif name == 'svnpath':
435 return subversion.revsplit(rev)[1]
435 return subversion.revsplit(rev)[1]
436 elif name == 'svnuuid':
436 elif name == 'svnuuid':
437 return subversion.revsplit(rev)[0]
437 return subversion.revsplit(rev)[0]
438 return rev
438 return rev
439
439
440 templatekeyword = registrar.templatekeyword()
441
442 @templatekeyword('svnrev')
440 def kwsvnrev(repo, ctx, **args):
443 def kwsvnrev(repo, ctx, **args):
441 """:svnrev: String. Converted subversion revision number."""
444 """String. Converted subversion revision number."""
442 return kwconverted(ctx, 'svnrev')
445 return kwconverted(ctx, 'svnrev')
443
446
447 @templatekeyword('svnpath')
444 def kwsvnpath(repo, ctx, **args):
448 def kwsvnpath(repo, ctx, **args):
445 """:svnpath: String. Converted subversion revision project path."""
449 """String. Converted subversion revision project path."""
446 return kwconverted(ctx, 'svnpath')
450 return kwconverted(ctx, 'svnpath')
447
451
452 @templatekeyword('svnuuid')
448 def kwsvnuuid(repo, ctx, **args):
453 def kwsvnuuid(repo, ctx, **args):
449 """:svnuuid: String. Converted subversion revision repository identifier."""
454 """String. Converted subversion revision repository identifier."""
450 return kwconverted(ctx, 'svnuuid')
455 return kwconverted(ctx, 'svnuuid')
451
456
452 def extsetup(ui):
453 templatekw.keywords['svnrev'] = kwsvnrev
454 templatekw.keywords['svnpath'] = kwsvnpath
455 templatekw.keywords['svnuuid'] = kwsvnuuid
456
457 # tell hggettext to extract docstrings from these functions:
457 # tell hggettext to extract docstrings from these functions:
458 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
458 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,742 +1,743 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 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 from __future__ import absolute_import
16 from __future__ import absolute_import
17
17
18 import os
18 import os
19 import tempfile
19 import tempfile
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import (
21 from mercurial import (
22 bundlerepo,
22 bundlerepo,
23 cmdutil,
23 cmdutil,
24 error,
24 error,
25 exchange,
25 exchange,
26 hg,
26 hg,
27 match,
27 match,
28 merge,
28 merge,
29 node as nodemod,
29 node as nodemod,
30 patch,
30 patch,
31 registrar,
31 registrar,
32 revlog,
32 revlog,
33 revset,
33 revset,
34 scmutil,
34 scmutil,
35 templatekw,
36 util,
35 util,
37 )
36 )
38
37
39 class TransplantError(error.Abort):
38 class TransplantError(error.Abort):
40 pass
39 pass
41
40
42 cmdtable = {}
41 cmdtable = {}
43 command = cmdutil.command(cmdtable)
42 command = cmdutil.command(cmdtable)
44 # Note for extension authors: ONLY specify testedwith = 'internal' for
43 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 # be specifying the version(s) of Mercurial they are tested with, or
45 # be specifying the version(s) of Mercurial they are tested with, or
47 # leave the attribute unspecified.
46 # leave the attribute unspecified.
48 testedwith = 'internal'
47 testedwith = 'internal'
49
48
50 class transplantentry(object):
49 class transplantentry(object):
51 def __init__(self, lnode, rnode):
50 def __init__(self, lnode, rnode):
52 self.lnode = lnode
51 self.lnode = lnode
53 self.rnode = rnode
52 self.rnode = rnode
54
53
55 class transplants(object):
54 class transplants(object):
56 def __init__(self, path=None, transplantfile=None, opener=None):
55 def __init__(self, path=None, transplantfile=None, opener=None):
57 self.path = path
56 self.path = path
58 self.transplantfile = transplantfile
57 self.transplantfile = transplantfile
59 self.opener = opener
58 self.opener = opener
60
59
61 if not opener:
60 if not opener:
62 self.opener = scmutil.opener(self.path)
61 self.opener = scmutil.opener(self.path)
63 self.transplants = {}
62 self.transplants = {}
64 self.dirty = False
63 self.dirty = False
65 self.read()
64 self.read()
66
65
67 def read(self):
66 def read(self):
68 abspath = os.path.join(self.path, self.transplantfile)
67 abspath = os.path.join(self.path, self.transplantfile)
69 if self.transplantfile and os.path.exists(abspath):
68 if self.transplantfile and os.path.exists(abspath):
70 for line in self.opener.read(self.transplantfile).splitlines():
69 for line in self.opener.read(self.transplantfile).splitlines():
71 lnode, rnode = map(revlog.bin, line.split(':'))
70 lnode, rnode = map(revlog.bin, line.split(':'))
72 list = self.transplants.setdefault(rnode, [])
71 list = self.transplants.setdefault(rnode, [])
73 list.append(transplantentry(lnode, rnode))
72 list.append(transplantentry(lnode, rnode))
74
73
75 def write(self):
74 def write(self):
76 if self.dirty and self.transplantfile:
75 if self.dirty and self.transplantfile:
77 if not os.path.isdir(self.path):
76 if not os.path.isdir(self.path):
78 os.mkdir(self.path)
77 os.mkdir(self.path)
79 fp = self.opener(self.transplantfile, 'w')
78 fp = self.opener(self.transplantfile, 'w')
80 for list in self.transplants.itervalues():
79 for list in self.transplants.itervalues():
81 for t in list:
80 for t in list:
82 l, r = map(nodemod.hex, (t.lnode, t.rnode))
81 l, r = map(nodemod.hex, (t.lnode, t.rnode))
83 fp.write(l + ':' + r + '\n')
82 fp.write(l + ':' + r + '\n')
84 fp.close()
83 fp.close()
85 self.dirty = False
84 self.dirty = False
86
85
87 def get(self, rnode):
86 def get(self, rnode):
88 return self.transplants.get(rnode) or []
87 return self.transplants.get(rnode) or []
89
88
90 def set(self, lnode, rnode):
89 def set(self, lnode, rnode):
91 list = self.transplants.setdefault(rnode, [])
90 list = self.transplants.setdefault(rnode, [])
92 list.append(transplantentry(lnode, rnode))
91 list.append(transplantentry(lnode, rnode))
93 self.dirty = True
92 self.dirty = True
94
93
95 def remove(self, transplant):
94 def remove(self, transplant):
96 list = self.transplants.get(transplant.rnode)
95 list = self.transplants.get(transplant.rnode)
97 if list:
96 if list:
98 del list[list.index(transplant)]
97 del list[list.index(transplant)]
99 self.dirty = True
98 self.dirty = True
100
99
101 class transplanter(object):
100 class transplanter(object):
102 def __init__(self, ui, repo, opts):
101 def __init__(self, ui, repo, opts):
103 self.ui = ui
102 self.ui = ui
104 self.path = repo.join('transplant')
103 self.path = repo.join('transplant')
105 self.opener = scmutil.opener(self.path)
104 self.opener = scmutil.opener(self.path)
106 self.transplants = transplants(self.path, 'transplants',
105 self.transplants = transplants(self.path, 'transplants',
107 opener=self.opener)
106 opener=self.opener)
108 def getcommiteditor():
107 def getcommiteditor():
109 editform = cmdutil.mergeeditform(repo[None], 'transplant')
108 editform = cmdutil.mergeeditform(repo[None], 'transplant')
110 return cmdutil.getcommiteditor(editform=editform, **opts)
109 return cmdutil.getcommiteditor(editform=editform, **opts)
111 self.getcommiteditor = getcommiteditor
110 self.getcommiteditor = getcommiteditor
112
111
113 def applied(self, repo, node, parent):
112 def applied(self, repo, node, parent):
114 '''returns True if a node is already an ancestor of parent
113 '''returns True if a node is already an ancestor of parent
115 or is parent or has already been transplanted'''
114 or is parent or has already been transplanted'''
116 if hasnode(repo, parent):
115 if hasnode(repo, parent):
117 parentrev = repo.changelog.rev(parent)
116 parentrev = repo.changelog.rev(parent)
118 if hasnode(repo, node):
117 if hasnode(repo, node):
119 rev = repo.changelog.rev(node)
118 rev = repo.changelog.rev(node)
120 reachable = repo.changelog.ancestors([parentrev], rev,
119 reachable = repo.changelog.ancestors([parentrev], rev,
121 inclusive=True)
120 inclusive=True)
122 if rev in reachable:
121 if rev in reachable:
123 return True
122 return True
124 for t in self.transplants.get(node):
123 for t in self.transplants.get(node):
125 # it might have been stripped
124 # it might have been stripped
126 if not hasnode(repo, t.lnode):
125 if not hasnode(repo, t.lnode):
127 self.transplants.remove(t)
126 self.transplants.remove(t)
128 return False
127 return False
129 lnoderev = repo.changelog.rev(t.lnode)
128 lnoderev = repo.changelog.rev(t.lnode)
130 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
129 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
131 inclusive=True):
130 inclusive=True):
132 return True
131 return True
133 return False
132 return False
134
133
135 def apply(self, repo, source, revmap, merges, opts=None):
134 def apply(self, repo, source, revmap, merges, opts=None):
136 '''apply the revisions in revmap one by one in revision order'''
135 '''apply the revisions in revmap one by one in revision order'''
137 if opts is None:
136 if opts is None:
138 opts = {}
137 opts = {}
139 revs = sorted(revmap)
138 revs = sorted(revmap)
140 p1, p2 = repo.dirstate.parents()
139 p1, p2 = repo.dirstate.parents()
141 pulls = []
140 pulls = []
142 diffopts = patch.difffeatureopts(self.ui, opts)
141 diffopts = patch.difffeatureopts(self.ui, opts)
143 diffopts.git = True
142 diffopts.git = True
144
143
145 lock = tr = None
144 lock = tr = None
146 try:
145 try:
147 lock = repo.lock()
146 lock = repo.lock()
148 tr = repo.transaction('transplant')
147 tr = repo.transaction('transplant')
149 for rev in revs:
148 for rev in revs:
150 node = revmap[rev]
149 node = revmap[rev]
151 revstr = '%s:%s' % (rev, nodemod.short(node))
150 revstr = '%s:%s' % (rev, nodemod.short(node))
152
151
153 if self.applied(repo, node, p1):
152 if self.applied(repo, node, p1):
154 self.ui.warn(_('skipping already applied revision %s\n') %
153 self.ui.warn(_('skipping already applied revision %s\n') %
155 revstr)
154 revstr)
156 continue
155 continue
157
156
158 parents = source.changelog.parents(node)
157 parents = source.changelog.parents(node)
159 if not (opts.get('filter') or opts.get('log')):
158 if not (opts.get('filter') or opts.get('log')):
160 # If the changeset parent is the same as the
159 # If the changeset parent is the same as the
161 # wdir's parent, just pull it.
160 # wdir's parent, just pull it.
162 if parents[0] == p1:
161 if parents[0] == p1:
163 pulls.append(node)
162 pulls.append(node)
164 p1 = node
163 p1 = node
165 continue
164 continue
166 if pulls:
165 if pulls:
167 if source != repo:
166 if source != repo:
168 exchange.pull(repo, source.peer(), heads=pulls)
167 exchange.pull(repo, source.peer(), heads=pulls)
169 merge.update(repo, pulls[-1], False, False)
168 merge.update(repo, pulls[-1], False, False)
170 p1, p2 = repo.dirstate.parents()
169 p1, p2 = repo.dirstate.parents()
171 pulls = []
170 pulls = []
172
171
173 domerge = False
172 domerge = False
174 if node in merges:
173 if node in merges:
175 # pulling all the merge revs at once would mean we
174 # pulling all the merge revs at once would mean we
176 # couldn't transplant after the latest even if
175 # couldn't transplant after the latest even if
177 # transplants before them fail.
176 # transplants before them fail.
178 domerge = True
177 domerge = True
179 if not hasnode(repo, node):
178 if not hasnode(repo, node):
180 exchange.pull(repo, source.peer(), heads=[node])
179 exchange.pull(repo, source.peer(), heads=[node])
181
180
182 skipmerge = False
181 skipmerge = False
183 if parents[1] != revlog.nullid:
182 if parents[1] != revlog.nullid:
184 if not opts.get('parent'):
183 if not opts.get('parent'):
185 self.ui.note(_('skipping merge changeset %s:%s\n')
184 self.ui.note(_('skipping merge changeset %s:%s\n')
186 % (rev, nodemod.short(node)))
185 % (rev, nodemod.short(node)))
187 skipmerge = True
186 skipmerge = True
188 else:
187 else:
189 parent = source.lookup(opts['parent'])
188 parent = source.lookup(opts['parent'])
190 if parent not in parents:
189 if parent not in parents:
191 raise error.Abort(_('%s is not a parent of %s') %
190 raise error.Abort(_('%s is not a parent of %s') %
192 (nodemod.short(parent),
191 (nodemod.short(parent),
193 nodemod.short(node)))
192 nodemod.short(node)))
194 else:
193 else:
195 parent = parents[0]
194 parent = parents[0]
196
195
197 if skipmerge:
196 if skipmerge:
198 patchfile = None
197 patchfile = None
199 else:
198 else:
200 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
199 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
201 fp = os.fdopen(fd, 'w')
200 fp = os.fdopen(fd, 'w')
202 gen = patch.diff(source, parent, node, opts=diffopts)
201 gen = patch.diff(source, parent, node, opts=diffopts)
203 for chunk in gen:
202 for chunk in gen:
204 fp.write(chunk)
203 fp.write(chunk)
205 fp.close()
204 fp.close()
206
205
207 del revmap[rev]
206 del revmap[rev]
208 if patchfile or domerge:
207 if patchfile or domerge:
209 try:
208 try:
210 try:
209 try:
211 n = self.applyone(repo, node,
210 n = self.applyone(repo, node,
212 source.changelog.read(node),
211 source.changelog.read(node),
213 patchfile, merge=domerge,
212 patchfile, merge=domerge,
214 log=opts.get('log'),
213 log=opts.get('log'),
215 filter=opts.get('filter'))
214 filter=opts.get('filter'))
216 except TransplantError:
215 except TransplantError:
217 # Do not rollback, it is up to the user to
216 # Do not rollback, it is up to the user to
218 # fix the merge or cancel everything
217 # fix the merge or cancel everything
219 tr.close()
218 tr.close()
220 raise
219 raise
221 if n and domerge:
220 if n and domerge:
222 self.ui.status(_('%s merged at %s\n') % (revstr,
221 self.ui.status(_('%s merged at %s\n') % (revstr,
223 nodemod.short(n)))
222 nodemod.short(n)))
224 elif n:
223 elif n:
225 self.ui.status(_('%s transplanted to %s\n')
224 self.ui.status(_('%s transplanted to %s\n')
226 % (nodemod.short(node),
225 % (nodemod.short(node),
227 nodemod.short(n)))
226 nodemod.short(n)))
228 finally:
227 finally:
229 if patchfile:
228 if patchfile:
230 os.unlink(patchfile)
229 os.unlink(patchfile)
231 tr.close()
230 tr.close()
232 if pulls:
231 if pulls:
233 exchange.pull(repo, source.peer(), heads=pulls)
232 exchange.pull(repo, source.peer(), heads=pulls)
234 merge.update(repo, pulls[-1], False, False)
233 merge.update(repo, pulls[-1], False, False)
235 finally:
234 finally:
236 self.saveseries(revmap, merges)
235 self.saveseries(revmap, merges)
237 self.transplants.write()
236 self.transplants.write()
238 if tr:
237 if tr:
239 tr.release()
238 tr.release()
240 if lock:
239 if lock:
241 lock.release()
240 lock.release()
242
241
243 def filter(self, filter, node, changelog, patchfile):
242 def filter(self, filter, node, changelog, patchfile):
244 '''arbitrarily rewrite changeset before applying it'''
243 '''arbitrarily rewrite changeset before applying it'''
245
244
246 self.ui.status(_('filtering %s\n') % patchfile)
245 self.ui.status(_('filtering %s\n') % patchfile)
247 user, date, msg = (changelog[1], changelog[2], changelog[4])
246 user, date, msg = (changelog[1], changelog[2], changelog[4])
248 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
247 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
249 fp = os.fdopen(fd, 'w')
248 fp = os.fdopen(fd, 'w')
250 fp.write("# HG changeset patch\n")
249 fp.write("# HG changeset patch\n")
251 fp.write("# User %s\n" % user)
250 fp.write("# User %s\n" % user)
252 fp.write("# Date %d %d\n" % date)
251 fp.write("# Date %d %d\n" % date)
253 fp.write(msg + '\n')
252 fp.write(msg + '\n')
254 fp.close()
253 fp.close()
255
254
256 try:
255 try:
257 self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile),
256 self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile),
258 util.shellquote(patchfile)),
257 util.shellquote(patchfile)),
259 environ={'HGUSER': changelog[1],
258 environ={'HGUSER': changelog[1],
260 'HGREVISION': nodemod.hex(node),
259 'HGREVISION': nodemod.hex(node),
261 },
260 },
262 onerr=error.Abort, errprefix=_('filter failed'))
261 onerr=error.Abort, errprefix=_('filter failed'))
263 user, date, msg = self.parselog(file(headerfile))[1:4]
262 user, date, msg = self.parselog(file(headerfile))[1:4]
264 finally:
263 finally:
265 os.unlink(headerfile)
264 os.unlink(headerfile)
266
265
267 return (user, date, msg)
266 return (user, date, msg)
268
267
269 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
268 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
270 filter=None):
269 filter=None):
271 '''apply the patch in patchfile to the repository as a transplant'''
270 '''apply the patch in patchfile to the repository as a transplant'''
272 (manifest, user, (time, timezone), files, message) = cl[:5]
271 (manifest, user, (time, timezone), files, message) = cl[:5]
273 date = "%d %d" % (time, timezone)
272 date = "%d %d" % (time, timezone)
274 extra = {'transplant_source': node}
273 extra = {'transplant_source': node}
275 if filter:
274 if filter:
276 (user, date, message) = self.filter(filter, node, cl, patchfile)
275 (user, date, message) = self.filter(filter, node, cl, patchfile)
277
276
278 if log:
277 if log:
279 # we don't translate messages inserted into commits
278 # we don't translate messages inserted into commits
280 message += '\n(transplanted from %s)' % nodemod.hex(node)
279 message += '\n(transplanted from %s)' % nodemod.hex(node)
281
280
282 self.ui.status(_('applying %s\n') % nodemod.short(node))
281 self.ui.status(_('applying %s\n') % nodemod.short(node))
283 self.ui.note('%s %s\n%s\n' % (user, date, message))
282 self.ui.note('%s %s\n%s\n' % (user, date, message))
284
283
285 if not patchfile and not merge:
284 if not patchfile and not merge:
286 raise error.Abort(_('can only omit patchfile if merging'))
285 raise error.Abort(_('can only omit patchfile if merging'))
287 if patchfile:
286 if patchfile:
288 try:
287 try:
289 files = set()
288 files = set()
290 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
289 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
291 files = list(files)
290 files = list(files)
292 except Exception as inst:
291 except Exception as inst:
293 seriespath = os.path.join(self.path, 'series')
292 seriespath = os.path.join(self.path, 'series')
294 if os.path.exists(seriespath):
293 if os.path.exists(seriespath):
295 os.unlink(seriespath)
294 os.unlink(seriespath)
296 p1 = repo.dirstate.p1()
295 p1 = repo.dirstate.p1()
297 p2 = node
296 p2 = node
298 self.log(user, date, message, p1, p2, merge=merge)
297 self.log(user, date, message, p1, p2, merge=merge)
299 self.ui.write(str(inst) + '\n')
298 self.ui.write(str(inst) + '\n')
300 raise TransplantError(_('fix up the working directory and run '
299 raise TransplantError(_('fix up the working directory and run '
301 'hg transplant --continue'))
300 'hg transplant --continue'))
302 else:
301 else:
303 files = None
302 files = None
304 if merge:
303 if merge:
305 p1, p2 = repo.dirstate.parents()
304 p1, p2 = repo.dirstate.parents()
306 repo.setparents(p1, node)
305 repo.setparents(p1, node)
307 m = match.always(repo.root, '')
306 m = match.always(repo.root, '')
308 else:
307 else:
309 m = match.exact(repo.root, '', files)
308 m = match.exact(repo.root, '', files)
310
309
311 n = repo.commit(message, user, date, extra=extra, match=m,
310 n = repo.commit(message, user, date, extra=extra, match=m,
312 editor=self.getcommiteditor())
311 editor=self.getcommiteditor())
313 if not n:
312 if not n:
314 self.ui.warn(_('skipping emptied changeset %s\n') %
313 self.ui.warn(_('skipping emptied changeset %s\n') %
315 nodemod.short(node))
314 nodemod.short(node))
316 return None
315 return None
317 if not merge:
316 if not merge:
318 self.transplants.set(n, node)
317 self.transplants.set(n, node)
319
318
320 return n
319 return n
321
320
322 def canresume(self):
321 def canresume(self):
323 return os.path.exists(os.path.join(self.path, 'journal'))
322 return os.path.exists(os.path.join(self.path, 'journal'))
324
323
325 def resume(self, repo, source, opts):
324 def resume(self, repo, source, opts):
326 '''recover last transaction and apply remaining changesets'''
325 '''recover last transaction and apply remaining changesets'''
327 if os.path.exists(os.path.join(self.path, 'journal')):
326 if os.path.exists(os.path.join(self.path, 'journal')):
328 n, node = self.recover(repo, source, opts)
327 n, node = self.recover(repo, source, opts)
329 if n:
328 if n:
330 self.ui.status(_('%s transplanted as %s\n') %
329 self.ui.status(_('%s transplanted as %s\n') %
331 (nodemod.short(node),
330 (nodemod.short(node),
332 nodemod.short(n)))
331 nodemod.short(n)))
333 else:
332 else:
334 self.ui.status(_('%s skipped due to empty diff\n')
333 self.ui.status(_('%s skipped due to empty diff\n')
335 % (nodemod.short(node),))
334 % (nodemod.short(node),))
336 seriespath = os.path.join(self.path, 'series')
335 seriespath = os.path.join(self.path, 'series')
337 if not os.path.exists(seriespath):
336 if not os.path.exists(seriespath):
338 self.transplants.write()
337 self.transplants.write()
339 return
338 return
340 nodes, merges = self.readseries()
339 nodes, merges = self.readseries()
341 revmap = {}
340 revmap = {}
342 for n in nodes:
341 for n in nodes:
343 revmap[source.changelog.rev(n)] = n
342 revmap[source.changelog.rev(n)] = n
344 os.unlink(seriespath)
343 os.unlink(seriespath)
345
344
346 self.apply(repo, source, revmap, merges, opts)
345 self.apply(repo, source, revmap, merges, opts)
347
346
348 def recover(self, repo, source, opts):
347 def recover(self, repo, source, opts):
349 '''commit working directory using journal metadata'''
348 '''commit working directory using journal metadata'''
350 node, user, date, message, parents = self.readlog()
349 node, user, date, message, parents = self.readlog()
351 merge = False
350 merge = False
352
351
353 if not user or not date or not message or not parents[0]:
352 if not user or not date or not message or not parents[0]:
354 raise error.Abort(_('transplant log file is corrupt'))
353 raise error.Abort(_('transplant log file is corrupt'))
355
354
356 parent = parents[0]
355 parent = parents[0]
357 if len(parents) > 1:
356 if len(parents) > 1:
358 if opts.get('parent'):
357 if opts.get('parent'):
359 parent = source.lookup(opts['parent'])
358 parent = source.lookup(opts['parent'])
360 if parent not in parents:
359 if parent not in parents:
361 raise error.Abort(_('%s is not a parent of %s') %
360 raise error.Abort(_('%s is not a parent of %s') %
362 (nodemod.short(parent),
361 (nodemod.short(parent),
363 nodemod.short(node)))
362 nodemod.short(node)))
364 else:
363 else:
365 merge = True
364 merge = True
366
365
367 extra = {'transplant_source': node}
366 extra = {'transplant_source': node}
368 try:
367 try:
369 p1, p2 = repo.dirstate.parents()
368 p1, p2 = repo.dirstate.parents()
370 if p1 != parent:
369 if p1 != parent:
371 raise error.Abort(_('working directory not at transplant '
370 raise error.Abort(_('working directory not at transplant '
372 'parent %s') % nodemod.hex(parent))
371 'parent %s') % nodemod.hex(parent))
373 if merge:
372 if merge:
374 repo.setparents(p1, parents[1])
373 repo.setparents(p1, parents[1])
375 modified, added, removed, deleted = repo.status()[:4]
374 modified, added, removed, deleted = repo.status()[:4]
376 if merge or modified or added or removed or deleted:
375 if merge or modified or added or removed or deleted:
377 n = repo.commit(message, user, date, extra=extra,
376 n = repo.commit(message, user, date, extra=extra,
378 editor=self.getcommiteditor())
377 editor=self.getcommiteditor())
379 if not n:
378 if not n:
380 raise error.Abort(_('commit failed'))
379 raise error.Abort(_('commit failed'))
381 if not merge:
380 if not merge:
382 self.transplants.set(n, node)
381 self.transplants.set(n, node)
383 else:
382 else:
384 n = None
383 n = None
385 self.unlog()
384 self.unlog()
386
385
387 return n, node
386 return n, node
388 finally:
387 finally:
389 # TODO: get rid of this meaningless try/finally enclosing.
388 # TODO: get rid of this meaningless try/finally enclosing.
390 # this is kept only to reduce changes in a patch.
389 # this is kept only to reduce changes in a patch.
391 pass
390 pass
392
391
393 def readseries(self):
392 def readseries(self):
394 nodes = []
393 nodes = []
395 merges = []
394 merges = []
396 cur = nodes
395 cur = nodes
397 for line in self.opener.read('series').splitlines():
396 for line in self.opener.read('series').splitlines():
398 if line.startswith('# Merges'):
397 if line.startswith('# Merges'):
399 cur = merges
398 cur = merges
400 continue
399 continue
401 cur.append(revlog.bin(line))
400 cur.append(revlog.bin(line))
402
401
403 return (nodes, merges)
402 return (nodes, merges)
404
403
405 def saveseries(self, revmap, merges):
404 def saveseries(self, revmap, merges):
406 if not revmap:
405 if not revmap:
407 return
406 return
408
407
409 if not os.path.isdir(self.path):
408 if not os.path.isdir(self.path):
410 os.mkdir(self.path)
409 os.mkdir(self.path)
411 series = self.opener('series', 'w')
410 series = self.opener('series', 'w')
412 for rev in sorted(revmap):
411 for rev in sorted(revmap):
413 series.write(nodemod.hex(revmap[rev]) + '\n')
412 series.write(nodemod.hex(revmap[rev]) + '\n')
414 if merges:
413 if merges:
415 series.write('# Merges\n')
414 series.write('# Merges\n')
416 for m in merges:
415 for m in merges:
417 series.write(nodemod.hex(m) + '\n')
416 series.write(nodemod.hex(m) + '\n')
418 series.close()
417 series.close()
419
418
420 def parselog(self, fp):
419 def parselog(self, fp):
421 parents = []
420 parents = []
422 message = []
421 message = []
423 node = revlog.nullid
422 node = revlog.nullid
424 inmsg = False
423 inmsg = False
425 user = None
424 user = None
426 date = None
425 date = None
427 for line in fp.read().splitlines():
426 for line in fp.read().splitlines():
428 if inmsg:
427 if inmsg:
429 message.append(line)
428 message.append(line)
430 elif line.startswith('# User '):
429 elif line.startswith('# User '):
431 user = line[7:]
430 user = line[7:]
432 elif line.startswith('# Date '):
431 elif line.startswith('# Date '):
433 date = line[7:]
432 date = line[7:]
434 elif line.startswith('# Node ID '):
433 elif line.startswith('# Node ID '):
435 node = revlog.bin(line[10:])
434 node = revlog.bin(line[10:])
436 elif line.startswith('# Parent '):
435 elif line.startswith('# Parent '):
437 parents.append(revlog.bin(line[9:]))
436 parents.append(revlog.bin(line[9:]))
438 elif not line.startswith('# '):
437 elif not line.startswith('# '):
439 inmsg = True
438 inmsg = True
440 message.append(line)
439 message.append(line)
441 if None in (user, date):
440 if None in (user, date):
442 raise error.Abort(_("filter corrupted changeset (no user or date)"))
441 raise error.Abort(_("filter corrupted changeset (no user or date)"))
443 return (node, user, date, '\n'.join(message), parents)
442 return (node, user, date, '\n'.join(message), parents)
444
443
445 def log(self, user, date, message, p1, p2, merge=False):
444 def log(self, user, date, message, p1, p2, merge=False):
446 '''journal changelog metadata for later recover'''
445 '''journal changelog metadata for later recover'''
447
446
448 if not os.path.isdir(self.path):
447 if not os.path.isdir(self.path):
449 os.mkdir(self.path)
448 os.mkdir(self.path)
450 fp = self.opener('journal', 'w')
449 fp = self.opener('journal', 'w')
451 fp.write('# User %s\n' % user)
450 fp.write('# User %s\n' % user)
452 fp.write('# Date %s\n' % date)
451 fp.write('# Date %s\n' % date)
453 fp.write('# Node ID %s\n' % nodemod.hex(p2))
452 fp.write('# Node ID %s\n' % nodemod.hex(p2))
454 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
453 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
455 if merge:
454 if merge:
456 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
455 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
457 fp.write(message.rstrip() + '\n')
456 fp.write(message.rstrip() + '\n')
458 fp.close()
457 fp.close()
459
458
460 def readlog(self):
459 def readlog(self):
461 return self.parselog(self.opener('journal'))
460 return self.parselog(self.opener('journal'))
462
461
463 def unlog(self):
462 def unlog(self):
464 '''remove changelog journal'''
463 '''remove changelog journal'''
465 absdst = os.path.join(self.path, 'journal')
464 absdst = os.path.join(self.path, 'journal')
466 if os.path.exists(absdst):
465 if os.path.exists(absdst):
467 os.unlink(absdst)
466 os.unlink(absdst)
468
467
469 def transplantfilter(self, repo, source, root):
468 def transplantfilter(self, repo, source, root):
470 def matchfn(node):
469 def matchfn(node):
471 if self.applied(repo, node, root):
470 if self.applied(repo, node, root):
472 return False
471 return False
473 if source.changelog.parents(node)[1] != revlog.nullid:
472 if source.changelog.parents(node)[1] != revlog.nullid:
474 return False
473 return False
475 extra = source.changelog.read(node)[5]
474 extra = source.changelog.read(node)[5]
476 cnode = extra.get('transplant_source')
475 cnode = extra.get('transplant_source')
477 if cnode and self.applied(repo, cnode, root):
476 if cnode and self.applied(repo, cnode, root):
478 return False
477 return False
479 return True
478 return True
480
479
481 return matchfn
480 return matchfn
482
481
483 def hasnode(repo, node):
482 def hasnode(repo, node):
484 try:
483 try:
485 return repo.changelog.rev(node) is not None
484 return repo.changelog.rev(node) is not None
486 except error.RevlogError:
485 except error.RevlogError:
487 return False
486 return False
488
487
489 def browserevs(ui, repo, nodes, opts):
488 def browserevs(ui, repo, nodes, opts):
490 '''interactively transplant changesets'''
489 '''interactively transplant changesets'''
491 displayer = cmdutil.show_changeset(ui, repo, opts)
490 displayer = cmdutil.show_changeset(ui, repo, opts)
492 transplants = []
491 transplants = []
493 merges = []
492 merges = []
494 prompt = _('apply changeset? [ynmpcq?]:'
493 prompt = _('apply changeset? [ynmpcq?]:'
495 '$$ &yes, transplant this changeset'
494 '$$ &yes, transplant this changeset'
496 '$$ &no, skip this changeset'
495 '$$ &no, skip this changeset'
497 '$$ &merge at this changeset'
496 '$$ &merge at this changeset'
498 '$$ show &patch'
497 '$$ show &patch'
499 '$$ &commit selected changesets'
498 '$$ &commit selected changesets'
500 '$$ &quit and cancel transplant'
499 '$$ &quit and cancel transplant'
501 '$$ &? (show this help)')
500 '$$ &? (show this help)')
502 for node in nodes:
501 for node in nodes:
503 displayer.show(repo[node])
502 displayer.show(repo[node])
504 action = None
503 action = None
505 while not action:
504 while not action:
506 action = 'ynmpcq?'[ui.promptchoice(prompt)]
505 action = 'ynmpcq?'[ui.promptchoice(prompt)]
507 if action == '?':
506 if action == '?':
508 for c, t in ui.extractchoices(prompt)[1]:
507 for c, t in ui.extractchoices(prompt)[1]:
509 ui.write('%s: %s\n' % (c, t))
508 ui.write('%s: %s\n' % (c, t))
510 action = None
509 action = None
511 elif action == 'p':
510 elif action == 'p':
512 parent = repo.changelog.parents(node)[0]
511 parent = repo.changelog.parents(node)[0]
513 for chunk in patch.diff(repo, parent, node):
512 for chunk in patch.diff(repo, parent, node):
514 ui.write(chunk)
513 ui.write(chunk)
515 action = None
514 action = None
516 if action == 'y':
515 if action == 'y':
517 transplants.append(node)
516 transplants.append(node)
518 elif action == 'm':
517 elif action == 'm':
519 merges.append(node)
518 merges.append(node)
520 elif action == 'c':
519 elif action == 'c':
521 break
520 break
522 elif action == 'q':
521 elif action == 'q':
523 transplants = ()
522 transplants = ()
524 merges = ()
523 merges = ()
525 break
524 break
526 displayer.close()
525 displayer.close()
527 return (transplants, merges)
526 return (transplants, merges)
528
527
529 @command('transplant',
528 @command('transplant',
530 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
529 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
531 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
530 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
532 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
531 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
533 ('p', 'prune', [], _('skip over REV'), _('REV')),
532 ('p', 'prune', [], _('skip over REV'), _('REV')),
534 ('m', 'merge', [], _('merge at REV'), _('REV')),
533 ('m', 'merge', [], _('merge at REV'), _('REV')),
535 ('', 'parent', '',
534 ('', 'parent', '',
536 _('parent to choose when transplanting merge'), _('REV')),
535 _('parent to choose when transplanting merge'), _('REV')),
537 ('e', 'edit', False, _('invoke editor on commit messages')),
536 ('e', 'edit', False, _('invoke editor on commit messages')),
538 ('', 'log', None, _('append transplant info to log message')),
537 ('', 'log', None, _('append transplant info to log message')),
539 ('c', 'continue', None, _('continue last transplant session '
538 ('c', 'continue', None, _('continue last transplant session '
540 'after fixing conflicts')),
539 'after fixing conflicts')),
541 ('', 'filter', '',
540 ('', 'filter', '',
542 _('filter changesets through command'), _('CMD'))],
541 _('filter changesets through command'), _('CMD'))],
543 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
542 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
544 '[-m REV] [REV]...'))
543 '[-m REV] [REV]...'))
545 def transplant(ui, repo, *revs, **opts):
544 def transplant(ui, repo, *revs, **opts):
546 '''transplant changesets from another branch
545 '''transplant changesets from another branch
547
546
548 Selected changesets will be applied on top of the current working
547 Selected changesets will be applied on top of the current working
549 directory with the log of the original changeset. The changesets
548 directory with the log of the original changeset. The changesets
550 are copied and will thus appear twice in the history with different
549 are copied and will thus appear twice in the history with different
551 identities.
550 identities.
552
551
553 Consider using the graft command if everything is inside the same
552 Consider using the graft command if everything is inside the same
554 repository - it will use merges and will usually give a better result.
553 repository - it will use merges and will usually give a better result.
555 Use the rebase extension if the changesets are unpublished and you want
554 Use the rebase extension if the changesets are unpublished and you want
556 to move them instead of copying them.
555 to move them instead of copying them.
557
556
558 If --log is specified, log messages will have a comment appended
557 If --log is specified, log messages will have a comment appended
559 of the form::
558 of the form::
560
559
561 (transplanted from CHANGESETHASH)
560 (transplanted from CHANGESETHASH)
562
561
563 You can rewrite the changelog message with the --filter option.
562 You can rewrite the changelog message with the --filter option.
564 Its argument will be invoked with the current changelog message as
563 Its argument will be invoked with the current changelog message as
565 $1 and the patch as $2.
564 $1 and the patch as $2.
566
565
567 --source/-s specifies another repository to use for selecting changesets,
566 --source/-s specifies another repository to use for selecting changesets,
568 just as if it temporarily had been pulled.
567 just as if it temporarily had been pulled.
569 If --branch/-b is specified, these revisions will be used as
568 If --branch/-b is specified, these revisions will be used as
570 heads when deciding which changesets to transplant, just as if only
569 heads when deciding which changesets to transplant, just as if only
571 these revisions had been pulled.
570 these revisions had been pulled.
572 If --all/-a is specified, all the revisions up to the heads specified
571 If --all/-a is specified, all the revisions up to the heads specified
573 with --branch will be transplanted.
572 with --branch will be transplanted.
574
573
575 Example:
574 Example:
576
575
577 - transplant all changes up to REV on top of your current revision::
576 - transplant all changes up to REV on top of your current revision::
578
577
579 hg transplant --branch REV --all
578 hg transplant --branch REV --all
580
579
581 You can optionally mark selected transplanted changesets as merge
580 You can optionally mark selected transplanted changesets as merge
582 changesets. You will not be prompted to transplant any ancestors
581 changesets. You will not be prompted to transplant any ancestors
583 of a merged transplant, and you can merge descendants of them
582 of a merged transplant, and you can merge descendants of them
584 normally instead of transplanting them.
583 normally instead of transplanting them.
585
584
586 Merge changesets may be transplanted directly by specifying the
585 Merge changesets may be transplanted directly by specifying the
587 proper parent changeset by calling :hg:`transplant --parent`.
586 proper parent changeset by calling :hg:`transplant --parent`.
588
587
589 If no merges or revisions are provided, :hg:`transplant` will
588 If no merges or revisions are provided, :hg:`transplant` will
590 start an interactive changeset browser.
589 start an interactive changeset browser.
591
590
592 If a changeset application fails, you can fix the merge by hand
591 If a changeset application fails, you can fix the merge by hand
593 and then resume where you left off by calling :hg:`transplant
592 and then resume where you left off by calling :hg:`transplant
594 --continue/-c`.
593 --continue/-c`.
595 '''
594 '''
596 with repo.wlock():
595 with repo.wlock():
597 return _dotransplant(ui, repo, *revs, **opts)
596 return _dotransplant(ui, repo, *revs, **opts)
598
597
599 def _dotransplant(ui, repo, *revs, **opts):
598 def _dotransplant(ui, repo, *revs, **opts):
600 def incwalk(repo, csets, match=util.always):
599 def incwalk(repo, csets, match=util.always):
601 for node in csets:
600 for node in csets:
602 if match(node):
601 if match(node):
603 yield node
602 yield node
604
603
605 def transplantwalk(repo, dest, heads, match=util.always):
604 def transplantwalk(repo, dest, heads, match=util.always):
606 '''Yield all nodes that are ancestors of a head but not ancestors
605 '''Yield all nodes that are ancestors of a head but not ancestors
607 of dest.
606 of dest.
608 If no heads are specified, the heads of repo will be used.'''
607 If no heads are specified, the heads of repo will be used.'''
609 if not heads:
608 if not heads:
610 heads = repo.heads()
609 heads = repo.heads()
611 ancestors = []
610 ancestors = []
612 ctx = repo[dest]
611 ctx = repo[dest]
613 for head in heads:
612 for head in heads:
614 ancestors.append(ctx.ancestor(repo[head]).node())
613 ancestors.append(ctx.ancestor(repo[head]).node())
615 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
614 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
616 if match(node):
615 if match(node):
617 yield node
616 yield node
618
617
619 def checkopts(opts, revs):
618 def checkopts(opts, revs):
620 if opts.get('continue'):
619 if opts.get('continue'):
621 if opts.get('branch') or opts.get('all') or opts.get('merge'):
620 if opts.get('branch') or opts.get('all') or opts.get('merge'):
622 raise error.Abort(_('--continue is incompatible with '
621 raise error.Abort(_('--continue is incompatible with '
623 '--branch, --all and --merge'))
622 '--branch, --all and --merge'))
624 return
623 return
625 if not (opts.get('source') or revs or
624 if not (opts.get('source') or revs or
626 opts.get('merge') or opts.get('branch')):
625 opts.get('merge') or opts.get('branch')):
627 raise error.Abort(_('no source URL, branch revision, or revision '
626 raise error.Abort(_('no source URL, branch revision, or revision '
628 'list provided'))
627 'list provided'))
629 if opts.get('all'):
628 if opts.get('all'):
630 if not opts.get('branch'):
629 if not opts.get('branch'):
631 raise error.Abort(_('--all requires a branch revision'))
630 raise error.Abort(_('--all requires a branch revision'))
632 if revs:
631 if revs:
633 raise error.Abort(_('--all is incompatible with a '
632 raise error.Abort(_('--all is incompatible with a '
634 'revision list'))
633 'revision list'))
635
634
636 checkopts(opts, revs)
635 checkopts(opts, revs)
637
636
638 if not opts.get('log'):
637 if not opts.get('log'):
639 # deprecated config: transplant.log
638 # deprecated config: transplant.log
640 opts['log'] = ui.config('transplant', 'log')
639 opts['log'] = ui.config('transplant', 'log')
641 if not opts.get('filter'):
640 if not opts.get('filter'):
642 # deprecated config: transplant.filter
641 # deprecated config: transplant.filter
643 opts['filter'] = ui.config('transplant', 'filter')
642 opts['filter'] = ui.config('transplant', 'filter')
644
643
645 tp = transplanter(ui, repo, opts)
644 tp = transplanter(ui, repo, opts)
646
645
647 p1, p2 = repo.dirstate.parents()
646 p1, p2 = repo.dirstate.parents()
648 if len(repo) > 0 and p1 == revlog.nullid:
647 if len(repo) > 0 and p1 == revlog.nullid:
649 raise error.Abort(_('no revision checked out'))
648 raise error.Abort(_('no revision checked out'))
650 if opts.get('continue'):
649 if opts.get('continue'):
651 if not tp.canresume():
650 if not tp.canresume():
652 raise error.Abort(_('no transplant to continue'))
651 raise error.Abort(_('no transplant to continue'))
653 else:
652 else:
654 cmdutil.checkunfinished(repo)
653 cmdutil.checkunfinished(repo)
655 if p2 != revlog.nullid:
654 if p2 != revlog.nullid:
656 raise error.Abort(_('outstanding uncommitted merges'))
655 raise error.Abort(_('outstanding uncommitted merges'))
657 m, a, r, d = repo.status()[:4]
656 m, a, r, d = repo.status()[:4]
658 if m or a or r or d:
657 if m or a or r or d:
659 raise error.Abort(_('outstanding local changes'))
658 raise error.Abort(_('outstanding local changes'))
660
659
661 sourcerepo = opts.get('source')
660 sourcerepo = opts.get('source')
662 if sourcerepo:
661 if sourcerepo:
663 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
662 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
664 heads = map(peer.lookup, opts.get('branch', ()))
663 heads = map(peer.lookup, opts.get('branch', ()))
665 target = set(heads)
664 target = set(heads)
666 for r in revs:
665 for r in revs:
667 try:
666 try:
668 target.add(peer.lookup(r))
667 target.add(peer.lookup(r))
669 except error.RepoError:
668 except error.RepoError:
670 pass
669 pass
671 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
670 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
672 onlyheads=sorted(target), force=True)
671 onlyheads=sorted(target), force=True)
673 else:
672 else:
674 source = repo
673 source = repo
675 heads = map(source.lookup, opts.get('branch', ()))
674 heads = map(source.lookup, opts.get('branch', ()))
676 cleanupfn = None
675 cleanupfn = None
677
676
678 try:
677 try:
679 if opts.get('continue'):
678 if opts.get('continue'):
680 tp.resume(repo, source, opts)
679 tp.resume(repo, source, opts)
681 return
680 return
682
681
683 tf = tp.transplantfilter(repo, source, p1)
682 tf = tp.transplantfilter(repo, source, p1)
684 if opts.get('prune'):
683 if opts.get('prune'):
685 prune = set(source.lookup(r)
684 prune = set(source.lookup(r)
686 for r in scmutil.revrange(source, opts.get('prune')))
685 for r in scmutil.revrange(source, opts.get('prune')))
687 matchfn = lambda x: tf(x) and x not in prune
686 matchfn = lambda x: tf(x) and x not in prune
688 else:
687 else:
689 matchfn = tf
688 matchfn = tf
690 merges = map(source.lookup, opts.get('merge', ()))
689 merges = map(source.lookup, opts.get('merge', ()))
691 revmap = {}
690 revmap = {}
692 if revs:
691 if revs:
693 for r in scmutil.revrange(source, revs):
692 for r in scmutil.revrange(source, revs):
694 revmap[int(r)] = source.lookup(r)
693 revmap[int(r)] = source.lookup(r)
695 elif opts.get('all') or not merges:
694 elif opts.get('all') or not merges:
696 if source != repo:
695 if source != repo:
697 alltransplants = incwalk(source, csets, match=matchfn)
696 alltransplants = incwalk(source, csets, match=matchfn)
698 else:
697 else:
699 alltransplants = transplantwalk(source, p1, heads,
698 alltransplants = transplantwalk(source, p1, heads,
700 match=matchfn)
699 match=matchfn)
701 if opts.get('all'):
700 if opts.get('all'):
702 revs = alltransplants
701 revs = alltransplants
703 else:
702 else:
704 revs, newmerges = browserevs(ui, source, alltransplants, opts)
703 revs, newmerges = browserevs(ui, source, alltransplants, opts)
705 merges.extend(newmerges)
704 merges.extend(newmerges)
706 for r in revs:
705 for r in revs:
707 revmap[source.changelog.rev(r)] = r
706 revmap[source.changelog.rev(r)] = r
708 for r in merges:
707 for r in merges:
709 revmap[source.changelog.rev(r)] = r
708 revmap[source.changelog.rev(r)] = r
710
709
711 tp.apply(repo, source, revmap, merges, opts)
710 tp.apply(repo, source, revmap, merges, opts)
712 finally:
711 finally:
713 if cleanupfn:
712 if cleanupfn:
714 cleanupfn()
713 cleanupfn()
715
714
716 revsetpredicate = registrar.revsetpredicate()
715 revsetpredicate = registrar.revsetpredicate()
717
716
718 @revsetpredicate('transplanted([set])')
717 @revsetpredicate('transplanted([set])')
719 def revsettransplanted(repo, subset, x):
718 def revsettransplanted(repo, subset, x):
720 """Transplanted changesets in set, or all transplanted changesets.
719 """Transplanted changesets in set, or all transplanted changesets.
721 """
720 """
722 if x:
721 if x:
723 s = revset.getset(repo, subset, x)
722 s = revset.getset(repo, subset, x)
724 else:
723 else:
725 s = subset
724 s = subset
726 return revset.baseset([r for r in s if
725 return revset.baseset([r for r in s if
727 repo[r].extra().get('transplant_source')])
726 repo[r].extra().get('transplant_source')])
728
727
728 templatekeyword = registrar.templatekeyword()
729
730 @templatekeyword('transplanted')
729 def kwtransplanted(repo, ctx, **args):
731 def kwtransplanted(repo, ctx, **args):
730 """:transplanted: String. The node identifier of the transplanted
732 """String. The node identifier of the transplanted
731 changeset if any."""
733 changeset if any."""
732 n = ctx.extra().get('transplant_source')
734 n = ctx.extra().get('transplant_source')
733 return n and nodemod.hex(n) or ''
735 return n and nodemod.hex(n) or ''
734
736
735 def extsetup(ui):
737 def extsetup(ui):
736 templatekw.keywords['transplanted'] = kwtransplanted
737 cmdutil.unfinishedstates.append(
738 cmdutil.unfinishedstates.append(
738 ['transplant/journal', True, False, _('transplant in progress'),
739 ['transplant/journal', True, False, _('transplant in progress'),
739 _("use 'hg transplant --continue' or 'hg update' to abort")])
740 _("use 'hg transplant --continue' or 'hg update' to abort")])
740
741
741 # tell hggettext to extract docstrings from these functions:
742 # tell hggettext to extract docstrings from these functions:
742 i18nfunctions = [revsettransplanted, kwtransplanted]
743 i18nfunctions = [revsettransplanted, kwtransplanted]
General Comments 0
You need to be logged in to leave comments. Login now