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