##// END OF EJS Templates
convert: add config option to use the local time zone...
Julian Cowley -
r17974:337d728e default
parent child Browse files
Show More
@@ -1,370 +1,378 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 testedwith = 'internal'
16 testedwith = 'internal'
17
17
18 # Commands definition was moved elsewhere to ease demandload job.
18 # Commands definition was moved elsewhere to ease demandload job.
19
19
20 def convert(ui, src, dest=None, revmapfile=None, **opts):
20 def convert(ui, src, dest=None, revmapfile=None, **opts):
21 """convert a foreign SCM repository to a Mercurial one.
21 """convert a foreign SCM repository to a Mercurial one.
22
22
23 Accepted source formats [identifiers]:
23 Accepted source formats [identifiers]:
24
24
25 - Mercurial [hg]
25 - Mercurial [hg]
26 - CVS [cvs]
26 - CVS [cvs]
27 - Darcs [darcs]
27 - Darcs [darcs]
28 - git [git]
28 - git [git]
29 - Subversion [svn]
29 - Subversion [svn]
30 - Monotone [mtn]
30 - Monotone [mtn]
31 - GNU Arch [gnuarch]
31 - GNU Arch [gnuarch]
32 - Bazaar [bzr]
32 - Bazaar [bzr]
33 - Perforce [p4]
33 - Perforce [p4]
34
34
35 Accepted destination formats [identifiers]:
35 Accepted destination formats [identifiers]:
36
36
37 - Mercurial [hg]
37 - Mercurial [hg]
38 - Subversion [svn] (history on branches is not preserved)
38 - Subversion [svn] (history on branches is not preserved)
39
39
40 If no revision is given, all revisions will be converted.
40 If no revision is given, all revisions will be converted.
41 Otherwise, convert will only import up to the named revision
41 Otherwise, convert will only import up to the named revision
42 (given in a format understood by the source).
42 (given in a format understood by the source).
43
43
44 If no destination directory name is specified, it defaults to the
44 If no destination directory name is specified, it defaults to the
45 basename of the source with ``-hg`` appended. If the destination
45 basename of the source with ``-hg`` appended. If the destination
46 repository doesn't exist, it will be created.
46 repository doesn't exist, it will be created.
47
47
48 By default, all sources except Mercurial will use --branchsort.
48 By default, all sources except Mercurial will use --branchsort.
49 Mercurial uses --sourcesort to preserve original revision numbers
49 Mercurial uses --sourcesort to preserve original revision numbers
50 order. Sort modes have the following effects:
50 order. Sort modes have the following effects:
51
51
52 --branchsort convert from parent to child revision when possible,
52 --branchsort convert from parent to child revision when possible,
53 which means branches are usually converted one after
53 which means branches are usually converted one after
54 the other. It generates more compact repositories.
54 the other. It generates more compact repositories.
55
55
56 --datesort sort revisions by date. Converted repositories have
56 --datesort sort revisions by date. Converted repositories have
57 good-looking changelogs but are often an order of
57 good-looking changelogs but are often an order of
58 magnitude larger than the same ones generated by
58 magnitude larger than the same ones generated by
59 --branchsort.
59 --branchsort.
60
60
61 --sourcesort try to preserve source revisions order, only
61 --sourcesort try to preserve source revisions order, only
62 supported by Mercurial sources.
62 supported by Mercurial sources.
63
63
64 If ``REVMAP`` isn't given, it will be put in a default location
64 If ``REVMAP`` isn't given, it will be put in a default location
65 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
65 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
66 text file that maps each source commit ID to the destination ID
66 text file that maps each source commit ID to the destination ID
67 for that revision, like so::
67 for that revision, like so::
68
68
69 <source ID> <destination ID>
69 <source ID> <destination ID>
70
70
71 If the file doesn't exist, it's automatically created. It's
71 If the file doesn't exist, it's automatically created. It's
72 updated on each commit copied, so :hg:`convert` can be interrupted
72 updated on each commit copied, so :hg:`convert` can be interrupted
73 and can be run repeatedly to copy new commits.
73 and can be run repeatedly to copy new commits.
74
74
75 The authormap is a simple text file that maps each source commit
75 The authormap is a simple text file that maps each source commit
76 author to a destination commit author. It is handy for source SCMs
76 author to a destination commit author. It is handy for source SCMs
77 that use unix logins to identify authors (e.g.: CVS). One line per
77 that use unix logins to identify authors (e.g.: CVS). One line per
78 author mapping and the line format is::
78 author mapping and the line format is::
79
79
80 source author = destination author
80 source author = destination author
81
81
82 Empty lines and lines starting with a ``#`` are ignored.
82 Empty lines and lines starting with a ``#`` are ignored.
83
83
84 The filemap is a file that allows filtering and remapping of files
84 The filemap is a file that allows filtering and remapping of files
85 and directories. Each line can contain one of the following
85 and directories. Each line can contain one of the following
86 directives::
86 directives::
87
87
88 include path/to/file-or-dir
88 include path/to/file-or-dir
89
89
90 exclude path/to/file-or-dir
90 exclude path/to/file-or-dir
91
91
92 rename path/to/source path/to/destination
92 rename path/to/source path/to/destination
93
93
94 Comment lines start with ``#``. A specified path matches if it
94 Comment lines start with ``#``. A specified path matches if it
95 equals the full relative name of a file or one of its parent
95 equals the full relative name of a file or one of its parent
96 directories. The ``include`` or ``exclude`` directive with the
96 directories. The ``include`` or ``exclude`` directive with the
97 longest matching path applies, so line order does not matter.
97 longest matching path applies, so line order does not matter.
98
98
99 The ``include`` directive causes a file, or all files under a
99 The ``include`` directive causes a file, or all files under a
100 directory, to be included in the destination repository, and the
100 directory, to be included in the destination repository, and the
101 exclusion of all other files and directories not explicitly
101 exclusion of all other files and directories not explicitly
102 included. The ``exclude`` directive causes files or directories to
102 included. The ``exclude`` directive causes files or directories to
103 be omitted. The ``rename`` directive renames a file or directory if
103 be omitted. The ``rename`` directive renames a file or directory if
104 it is converted. To rename from a subdirectory into the root of
104 it is converted. To rename from a subdirectory into the root of
105 the repository, use ``.`` as the path to rename to.
105 the repository, use ``.`` as the path to rename to.
106
106
107 The splicemap is a file that allows insertion of synthetic
107 The splicemap is a file that allows insertion of synthetic
108 history, letting you specify the parents of a revision. This is
108 history, letting you specify the parents of a revision. This is
109 useful if you want to e.g. give a Subversion merge two parents, or
109 useful if you want to e.g. give a Subversion merge two parents, or
110 graft two disconnected series of history together. Each entry
110 graft two disconnected series of history together. Each entry
111 contains a key, followed by a space, followed by one or two
111 contains a key, followed by a space, followed by one or two
112 comma-separated values::
112 comma-separated values::
113
113
114 key parent1, parent2
114 key parent1, parent2
115
115
116 The key is the revision ID in the source
116 The key is the revision ID in the source
117 revision control system whose parents should be modified (same
117 revision control system whose parents should be modified (same
118 format as a key in .hg/shamap). The values are the revision IDs
118 format as a key in .hg/shamap). The values are the revision IDs
119 (in either the source or destination revision control system) that
119 (in either the source or destination revision control system) that
120 should be used as the new parents for that node. For example, if
120 should be used as the new parents for that node. For example, if
121 you have merged "release-1.0" into "trunk", then you should
121 you have merged "release-1.0" into "trunk", then you should
122 specify the revision on "trunk" as the first parent and the one on
122 specify the revision on "trunk" as the first parent and the one on
123 the "release-1.0" branch as the second.
123 the "release-1.0" branch as the second.
124
124
125 The branchmap is a file that allows you to rename a branch when it is
125 The branchmap is a file that allows you to rename a branch when it is
126 being brought in from whatever external repository. When used in
126 being brought in from whatever external repository. When used in
127 conjunction with a splicemap, it allows for a powerful combination
127 conjunction with a splicemap, it allows for a powerful combination
128 to help fix even the most badly mismanaged repositories and turn them
128 to help fix even the most badly mismanaged repositories and turn them
129 into nicely structured Mercurial repositories. The branchmap contains
129 into nicely structured Mercurial repositories. The branchmap contains
130 lines of the form::
130 lines of the form::
131
131
132 original_branch_name new_branch_name
132 original_branch_name new_branch_name
133
133
134 where "original_branch_name" is the name of the branch in the
134 where "original_branch_name" is the name of the branch in the
135 source repository, and "new_branch_name" is the name of the branch
135 source repository, and "new_branch_name" is the name of the branch
136 is the destination repository. No whitespace is allowed in the
136 is the destination repository. No whitespace is allowed in the
137 branch names. This can be used to (for instance) move code in one
137 branch names. This can be used to (for instance) move code in one
138 repository from "default" to a named branch.
138 repository from "default" to a named branch.
139
139
140 Mercurial Source
140 Mercurial Source
141 ################
141 ################
142
142
143 The Mercurial source recognizes the following configuration
143 The Mercurial source recognizes the following configuration
144 options, which you can set on the command line with ``--config``:
144 options, which you can set on the command line with ``--config``:
145
145
146 :convert.hg.ignoreerrors: ignore integrity errors when reading.
146 :convert.hg.ignoreerrors: ignore integrity errors when reading.
147 Use it to fix Mercurial repositories with missing revlogs, by
147 Use it to fix Mercurial repositories with missing revlogs, by
148 converting from and to Mercurial. Default is False.
148 converting from and to Mercurial. Default is False.
149
149
150 :convert.hg.saverev: store original revision ID in changeset
150 :convert.hg.saverev: store original revision ID in changeset
151 (forces target IDs to change). It takes a boolean argument and
151 (forces target IDs to change). It takes a boolean argument and
152 defaults to False.
152 defaults to False.
153
153
154 :convert.hg.startrev: convert start revision and its descendants.
154 :convert.hg.startrev: convert start revision and its descendants.
155 It takes a hg revision identifier and defaults to 0.
155 It takes a hg revision identifier and defaults to 0.
156
156
157 CVS Source
157 CVS Source
158 ##########
158 ##########
159
159
160 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
160 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
161 to indicate the starting point of what will be converted. Direct
161 to indicate the starting point of what will be converted. Direct
162 access to the repository files is not needed, unless of course the
162 access to the repository files is not needed, unless of course the
163 repository is ``:local:``. The conversion uses the top level
163 repository is ``:local:``. The conversion uses the top level
164 directory in the sandbox to find the CVS repository, and then uses
164 directory in the sandbox to find the CVS repository, and then uses
165 CVS rlog commands to find files to convert. This means that unless
165 CVS rlog commands to find files to convert. This means that unless
166 a filemap is given, all files under the starting directory will be
166 a filemap is given, all files under the starting directory will be
167 converted, and that any directory reorganization in the CVS
167 converted, and that any directory reorganization in the CVS
168 sandbox is ignored.
168 sandbox is ignored.
169
169
170 The following options can be used with ``--config``:
170 The following options can be used with ``--config``:
171
171
172 :convert.cvsps.cache: Set to False to disable remote log caching,
172 :convert.cvsps.cache: Set to False to disable remote log caching,
173 for testing and debugging purposes. Default is True.
173 for testing and debugging purposes. Default is True.
174
174
175 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
175 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
176 allowed between commits with identical user and log message in
176 allowed between commits with identical user and log message in
177 a single changeset. When very large files were checked in as
177 a single changeset. When very large files were checked in as
178 part of a changeset then the default may not be long enough.
178 part of a changeset then the default may not be long enough.
179 The default is 60.
179 The default is 60.
180
180
181 :convert.cvsps.mergeto: Specify a regular expression to which
181 :convert.cvsps.mergeto: Specify a regular expression to which
182 commit log messages are matched. If a match occurs, then the
182 commit log messages are matched. If a match occurs, then the
183 conversion process will insert a dummy revision merging the
183 conversion process will insert a dummy revision merging the
184 branch on which this log message occurs to the branch
184 branch on which this log message occurs to the branch
185 indicated in the regex. Default is ``{{mergetobranch
185 indicated in the regex. Default is ``{{mergetobranch
186 ([-\\w]+)}}``
186 ([-\\w]+)}}``
187
187
188 :convert.cvsps.mergefrom: Specify a regular expression to which
188 :convert.cvsps.mergefrom: Specify a regular expression to which
189 commit log messages are matched. If a match occurs, then the
189 commit log messages are matched. If a match occurs, then the
190 conversion process will add the most recent revision on the
190 conversion process will add the most recent revision on the
191 branch indicated in the regex as the second parent of the
191 branch indicated in the regex as the second parent of the
192 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
192 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
193
193
194 :convert.localtimezone: use local time (as determined by the TZ
195 environment variable) for changeset date/times. The default
196 is False (use UTC).
197
194 :hook.cvslog: Specify a Python function to be called at the end of
198 :hook.cvslog: Specify a Python function to be called at the end of
195 gathering the CVS log. The function is passed a list with the
199 gathering the CVS log. The function is passed a list with the
196 log entries, and can modify the entries in-place, or add or
200 log entries, and can modify the entries in-place, or add or
197 delete them.
201 delete them.
198
202
199 :hook.cvschangesets: Specify a Python function to be called after
203 :hook.cvschangesets: Specify a Python function to be called after
200 the changesets are calculated from the CVS log. The
204 the changesets are calculated from the CVS log. The
201 function is passed a list with the changeset entries, and can
205 function is passed a list with the changeset entries, and can
202 modify the changesets in-place, or add or delete them.
206 modify the changesets in-place, or add or delete them.
203
207
204 An additional "debugcvsps" Mercurial command allows the builtin
208 An additional "debugcvsps" Mercurial command allows the builtin
205 changeset merging code to be run without doing a conversion. Its
209 changeset merging code to be run without doing a conversion. Its
206 parameters and output are similar to that of cvsps 2.1. Please see
210 parameters and output are similar to that of cvsps 2.1. Please see
207 the command help for more details.
211 the command help for more details.
208
212
209 Subversion Source
213 Subversion Source
210 #################
214 #################
211
215
212 Subversion source detects classical trunk/branches/tags layouts.
216 Subversion source detects classical trunk/branches/tags layouts.
213 By default, the supplied ``svn://repo/path/`` source URL is
217 By default, the supplied ``svn://repo/path/`` source URL is
214 converted as a single branch. If ``svn://repo/path/trunk`` exists
218 converted as a single branch. If ``svn://repo/path/trunk`` exists
215 it replaces the default branch. If ``svn://repo/path/branches``
219 it replaces the default branch. If ``svn://repo/path/branches``
216 exists, its subdirectories are listed as possible branches. If
220 exists, its subdirectories are listed as possible branches. If
217 ``svn://repo/path/tags`` exists, it is looked for tags referencing
221 ``svn://repo/path/tags`` exists, it is looked for tags referencing
218 converted branches. Default ``trunk``, ``branches`` and ``tags``
222 converted branches. Default ``trunk``, ``branches`` and ``tags``
219 values can be overridden with following options. Set them to paths
223 values can be overridden with following options. Set them to paths
220 relative to the source URL, or leave them blank to disable auto
224 relative to the source URL, or leave them blank to disable auto
221 detection.
225 detection.
222
226
223 The following options can be set with ``--config``:
227 The following options can be set with ``--config``:
224
228
225 :convert.svn.branches: specify the directory containing branches.
229 :convert.svn.branches: specify the directory containing branches.
226 The default is ``branches``.
230 The default is ``branches``.
227
231
228 :convert.svn.tags: specify the directory containing tags. The
232 :convert.svn.tags: specify the directory containing tags. The
229 default is ``tags``.
233 default is ``tags``.
230
234
231 :convert.svn.trunk: specify the name of the trunk branch. The
235 :convert.svn.trunk: specify the name of the trunk branch. The
232 default is ``trunk``.
236 default is ``trunk``.
233
237
238 :convert.localtimezone: use local time (as determined by the TZ
239 environment variable) for changeset date/times. The default
240 is False (use UTC).
241
234 Source history can be retrieved starting at a specific revision,
242 Source history can be retrieved starting at a specific revision,
235 instead of being integrally converted. Only single branch
243 instead of being integrally converted. Only single branch
236 conversions are supported.
244 conversions are supported.
237
245
238 :convert.svn.startrev: specify start Subversion revision number.
246 :convert.svn.startrev: specify start Subversion revision number.
239 The default is 0.
247 The default is 0.
240
248
241 Perforce Source
249 Perforce Source
242 ###############
250 ###############
243
251
244 The Perforce (P4) importer can be given a p4 depot path or a
252 The Perforce (P4) importer can be given a p4 depot path or a
245 client specification as source. It will convert all files in the
253 client specification as source. It will convert all files in the
246 source to a flat Mercurial repository, ignoring labels, branches
254 source to a flat Mercurial repository, ignoring labels, branches
247 and integrations. Note that when a depot path is given you then
255 and integrations. Note that when a depot path is given you then
248 usually should specify a target directory, because otherwise the
256 usually should specify a target directory, because otherwise the
249 target may be named ``...-hg``.
257 target may be named ``...-hg``.
250
258
251 It is possible to limit the amount of source history to be
259 It is possible to limit the amount of source history to be
252 converted by specifying an initial Perforce revision:
260 converted by specifying an initial Perforce revision:
253
261
254 :convert.p4.startrev: specify initial Perforce revision (a
262 :convert.p4.startrev: specify initial Perforce revision (a
255 Perforce changelist number).
263 Perforce changelist number).
256
264
257 Mercurial Destination
265 Mercurial Destination
258 #####################
266 #####################
259
267
260 The following options are supported:
268 The following options are supported:
261
269
262 :convert.hg.clonebranches: dispatch source branches in separate
270 :convert.hg.clonebranches: dispatch source branches in separate
263 clones. The default is False.
271 clones. The default is False.
264
272
265 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
273 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
266 ``default``.
274 ``default``.
267
275
268 :convert.hg.usebranchnames: preserve branch names. The default is
276 :convert.hg.usebranchnames: preserve branch names. The default is
269 True.
277 True.
270 """
278 """
271 return convcmd.convert(ui, src, dest, revmapfile, **opts)
279 return convcmd.convert(ui, src, dest, revmapfile, **opts)
272
280
273 def debugsvnlog(ui, **opts):
281 def debugsvnlog(ui, **opts):
274 return subversion.debugsvnlog(ui, **opts)
282 return subversion.debugsvnlog(ui, **opts)
275
283
276 def debugcvsps(ui, *args, **opts):
284 def debugcvsps(ui, *args, **opts):
277 '''create changeset information from CVS
285 '''create changeset information from CVS
278
286
279 This command is intended as a debugging tool for the CVS to
287 This command is intended as a debugging tool for the CVS to
280 Mercurial converter, and can be used as a direct replacement for
288 Mercurial converter, and can be used as a direct replacement for
281 cvsps.
289 cvsps.
282
290
283 Hg debugcvsps reads the CVS rlog for current directory (or any
291 Hg debugcvsps reads the CVS rlog for current directory (or any
284 named directory) in the CVS repository, and converts the log to a
292 named directory) in the CVS repository, and converts the log to a
285 series of changesets based on matching commit log entries and
293 series of changesets based on matching commit log entries and
286 dates.'''
294 dates.'''
287 return cvsps.debugcvsps(ui, *args, **opts)
295 return cvsps.debugcvsps(ui, *args, **opts)
288
296
289 commands.norepo += " convert debugsvnlog debugcvsps"
297 commands.norepo += " convert debugsvnlog debugcvsps"
290
298
291 cmdtable = {
299 cmdtable = {
292 "convert":
300 "convert":
293 (convert,
301 (convert,
294 [('', 'authors', '',
302 [('', 'authors', '',
295 _('username mapping filename (DEPRECATED, use --authormap instead)'),
303 _('username mapping filename (DEPRECATED, use --authormap instead)'),
296 _('FILE')),
304 _('FILE')),
297 ('s', 'source-type', '',
305 ('s', 'source-type', '',
298 _('source repository type'), _('TYPE')),
306 _('source repository type'), _('TYPE')),
299 ('d', 'dest-type', '',
307 ('d', 'dest-type', '',
300 _('destination repository type'), _('TYPE')),
308 _('destination repository type'), _('TYPE')),
301 ('r', 'rev', '',
309 ('r', 'rev', '',
302 _('import up to target revision REV'), _('REV')),
310 _('import up to target revision REV'), _('REV')),
303 ('A', 'authormap', '',
311 ('A', 'authormap', '',
304 _('remap usernames using this file'), _('FILE')),
312 _('remap usernames using this file'), _('FILE')),
305 ('', 'filemap', '',
313 ('', 'filemap', '',
306 _('remap file names using contents of file'), _('FILE')),
314 _('remap file names using contents of file'), _('FILE')),
307 ('', 'splicemap', '',
315 ('', 'splicemap', '',
308 _('splice synthesized history into place'), _('FILE')),
316 _('splice synthesized history into place'), _('FILE')),
309 ('', 'branchmap', '',
317 ('', 'branchmap', '',
310 _('change branch names while converting'), _('FILE')),
318 _('change branch names while converting'), _('FILE')),
311 ('', 'branchsort', None, _('try to sort changesets by branches')),
319 ('', 'branchsort', None, _('try to sort changesets by branches')),
312 ('', 'datesort', None, _('try to sort changesets by date')),
320 ('', 'datesort', None, _('try to sort changesets by date')),
313 ('', 'sourcesort', None, _('preserve source changesets order'))],
321 ('', 'sourcesort', None, _('preserve source changesets order'))],
314 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
322 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
315 "debugsvnlog":
323 "debugsvnlog":
316 (debugsvnlog,
324 (debugsvnlog,
317 [],
325 [],
318 'hg debugsvnlog'),
326 'hg debugsvnlog'),
319 "debugcvsps":
327 "debugcvsps":
320 (debugcvsps,
328 (debugcvsps,
321 [
329 [
322 # Main options shared with cvsps-2.1
330 # Main options shared with cvsps-2.1
323 ('b', 'branches', [], _('only return changes on specified branches')),
331 ('b', 'branches', [], _('only return changes on specified branches')),
324 ('p', 'prefix', '', _('prefix to remove from file names')),
332 ('p', 'prefix', '', _('prefix to remove from file names')),
325 ('r', 'revisions', [],
333 ('r', 'revisions', [],
326 _('only return changes after or between specified tags')),
334 _('only return changes after or between specified tags')),
327 ('u', 'update-cache', None, _("update cvs log cache")),
335 ('u', 'update-cache', None, _("update cvs log cache")),
328 ('x', 'new-cache', None, _("create new cvs log cache")),
336 ('x', 'new-cache', None, _("create new cvs log cache")),
329 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
337 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
330 ('', 'root', '', _('specify cvsroot')),
338 ('', 'root', '', _('specify cvsroot')),
331 # Options specific to builtin cvsps
339 # Options specific to builtin cvsps
332 ('', 'parents', '', _('show parent changesets')),
340 ('', 'parents', '', _('show parent changesets')),
333 ('', 'ancestors', '',
341 ('', 'ancestors', '',
334 _('show current changeset in ancestor branches')),
342 _('show current changeset in ancestor branches')),
335 # Options that are ignored for compatibility with cvsps-2.1
343 # Options that are ignored for compatibility with cvsps-2.1
336 ('A', 'cvs-direct', None, _('ignored for compatibility')),
344 ('A', 'cvs-direct', None, _('ignored for compatibility')),
337 ],
345 ],
338 _('hg debugcvsps [OPTION]... [PATH]...')),
346 _('hg debugcvsps [OPTION]... [PATH]...')),
339 }
347 }
340
348
341 def kwconverted(ctx, name):
349 def kwconverted(ctx, name):
342 rev = ctx.extra().get('convert_revision', '')
350 rev = ctx.extra().get('convert_revision', '')
343 if rev.startswith('svn:'):
351 if rev.startswith('svn:'):
344 if name == 'svnrev':
352 if name == 'svnrev':
345 return str(subversion.revsplit(rev)[2])
353 return str(subversion.revsplit(rev)[2])
346 elif name == 'svnpath':
354 elif name == 'svnpath':
347 return subversion.revsplit(rev)[1]
355 return subversion.revsplit(rev)[1]
348 elif name == 'svnuuid':
356 elif name == 'svnuuid':
349 return subversion.revsplit(rev)[0]
357 return subversion.revsplit(rev)[0]
350 return rev
358 return rev
351
359
352 def kwsvnrev(repo, ctx, **args):
360 def kwsvnrev(repo, ctx, **args):
353 """:svnrev: String. Converted subversion revision number."""
361 """:svnrev: String. Converted subversion revision number."""
354 return kwconverted(ctx, 'svnrev')
362 return kwconverted(ctx, 'svnrev')
355
363
356 def kwsvnpath(repo, ctx, **args):
364 def kwsvnpath(repo, ctx, **args):
357 """:svnpath: String. Converted subversion revision project path."""
365 """:svnpath: String. Converted subversion revision project path."""
358 return kwconverted(ctx, 'svnpath')
366 return kwconverted(ctx, 'svnpath')
359
367
360 def kwsvnuuid(repo, ctx, **args):
368 def kwsvnuuid(repo, ctx, **args):
361 """:svnuuid: String. Converted subversion revision repository identifier."""
369 """:svnuuid: String. Converted subversion revision repository identifier."""
362 return kwconverted(ctx, 'svnuuid')
370 return kwconverted(ctx, 'svnuuid')
363
371
364 def extsetup(ui):
372 def extsetup(ui):
365 templatekw.keywords['svnrev'] = kwsvnrev
373 templatekw.keywords['svnrev'] = kwsvnrev
366 templatekw.keywords['svnpath'] = kwsvnpath
374 templatekw.keywords['svnpath'] = kwsvnpath
367 templatekw.keywords['svnuuid'] = kwsvnuuid
375 templatekw.keywords['svnuuid'] = kwsvnuuid
368
376
369 # tell hggettext to extract docstrings from these functions:
377 # tell hggettext to extract docstrings from these functions:
370 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
378 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,448 +1,455 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 base64, errno, subprocess, os
8 import base64, errno, subprocess, os, datetime
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import util
10 from mercurial import util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 exc = abort and util.Abort or MissingTool
34 exc = abort and util.Abort or MissingTool
35 raise exc(_('cannot find required "%s" tool') % name)
35 raise exc(_('cannot find required "%s" tool') % name)
36
36
37 class NoRepo(Exception):
37 class NoRepo(Exception):
38 pass
38 pass
39
39
40 SKIPREV = 'SKIP'
40 SKIPREV = 'SKIP'
41
41
42 class commit(object):
42 class commit(object):
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 extra={}, sortkey=None):
44 extra={}, sortkey=None):
45 self.author = author or 'unknown'
45 self.author = author or 'unknown'
46 self.date = date or '0 0'
46 self.date = date or '0 0'
47 self.desc = desc
47 self.desc = desc
48 self.parents = parents
48 self.parents = parents
49 self.branch = branch
49 self.branch = branch
50 self.rev = rev
50 self.rev = rev
51 self.extra = extra
51 self.extra = extra
52 self.sortkey = sortkey
52 self.sortkey = sortkey
53
53
54 class converter_source(object):
54 class converter_source(object):
55 """Conversion source interface"""
55 """Conversion source interface"""
56
56
57 def __init__(self, ui, path=None, rev=None):
57 def __init__(self, ui, path=None, rev=None):
58 """Initialize conversion source (or raise NoRepo("message")
58 """Initialize conversion source (or raise NoRepo("message")
59 exception if path is not a valid repository)"""
59 exception if path is not a valid repository)"""
60 self.ui = ui
60 self.ui = ui
61 self.path = path
61 self.path = path
62 self.rev = rev
62 self.rev = rev
63
63
64 self.encoding = 'utf-8'
64 self.encoding = 'utf-8'
65
65
66 def before(self):
66 def before(self):
67 pass
67 pass
68
68
69 def after(self):
69 def after(self):
70 pass
70 pass
71
71
72 def setrevmap(self, revmap):
72 def setrevmap(self, revmap):
73 """set the map of already-converted revisions"""
73 """set the map of already-converted revisions"""
74 pass
74 pass
75
75
76 def getheads(self):
76 def getheads(self):
77 """Return a list of this repository's heads"""
77 """Return a list of this repository's heads"""
78 raise NotImplementedError
78 raise NotImplementedError
79
79
80 def getfile(self, name, rev):
80 def getfile(self, name, rev):
81 """Return a pair (data, mode) where data is the file content
81 """Return a pair (data, mode) where data is the file content
82 as a string and mode one of '', 'x' or 'l'. rev is the
82 as a string and mode one of '', 'x' or 'l'. rev is the
83 identifier returned by a previous call to getchanges(). Raise
83 identifier returned by a previous call to getchanges(). Raise
84 IOError to indicate that name was deleted in rev.
84 IOError to indicate that name was deleted in rev.
85 """
85 """
86 raise NotImplementedError
86 raise NotImplementedError
87
87
88 def getchanges(self, version):
88 def getchanges(self, version):
89 """Returns a tuple of (files, copies).
89 """Returns a tuple of (files, copies).
90
90
91 files is a sorted list of (filename, id) tuples for all files
91 files is a sorted list of (filename, id) tuples for all files
92 changed between version and its first parent returned by
92 changed between version and its first parent returned by
93 getcommit(). id is the source revision id of the file.
93 getcommit(). id is the source revision id of the file.
94
94
95 copies is a dictionary of dest: source
95 copies is a dictionary of dest: source
96 """
96 """
97 raise NotImplementedError
97 raise NotImplementedError
98
98
99 def getcommit(self, version):
99 def getcommit(self, version):
100 """Return the commit object for version"""
100 """Return the commit object for version"""
101 raise NotImplementedError
101 raise NotImplementedError
102
102
103 def gettags(self):
103 def gettags(self):
104 """Return the tags as a dictionary of name: revision
104 """Return the tags as a dictionary of name: revision
105
105
106 Tag names must be UTF-8 strings.
106 Tag names must be UTF-8 strings.
107 """
107 """
108 raise NotImplementedError
108 raise NotImplementedError
109
109
110 def recode(self, s, encoding=None):
110 def recode(self, s, encoding=None):
111 if not encoding:
111 if not encoding:
112 encoding = self.encoding or 'utf-8'
112 encoding = self.encoding or 'utf-8'
113
113
114 if isinstance(s, unicode):
114 if isinstance(s, unicode):
115 return s.encode("utf-8")
115 return s.encode("utf-8")
116 try:
116 try:
117 return s.decode(encoding).encode("utf-8")
117 return s.decode(encoding).encode("utf-8")
118 except UnicodeError:
118 except UnicodeError:
119 try:
119 try:
120 return s.decode("latin-1").encode("utf-8")
120 return s.decode("latin-1").encode("utf-8")
121 except UnicodeError:
121 except UnicodeError:
122 return s.decode(encoding, "replace").encode("utf-8")
122 return s.decode(encoding, "replace").encode("utf-8")
123
123
124 def getchangedfiles(self, rev, i):
124 def getchangedfiles(self, rev, i):
125 """Return the files changed by rev compared to parent[i].
125 """Return the files changed by rev compared to parent[i].
126
126
127 i is an index selecting one of the parents of rev. The return
127 i is an index selecting one of the parents of rev. The return
128 value should be the list of files that are different in rev and
128 value should be the list of files that are different in rev and
129 this parent.
129 this parent.
130
130
131 If rev has no parents, i is None.
131 If rev has no parents, i is None.
132
132
133 This function is only needed to support --filemap
133 This function is only needed to support --filemap
134 """
134 """
135 raise NotImplementedError
135 raise NotImplementedError
136
136
137 def converted(self, rev, sinkrev):
137 def converted(self, rev, sinkrev):
138 '''Notify the source that a revision has been converted.'''
138 '''Notify the source that a revision has been converted.'''
139 pass
139 pass
140
140
141 def hasnativeorder(self):
141 def hasnativeorder(self):
142 """Return true if this source has a meaningful, native revision
142 """Return true if this source has a meaningful, native revision
143 order. For instance, Mercurial revisions are store sequentially
143 order. For instance, Mercurial revisions are store sequentially
144 while there is no such global ordering with Darcs.
144 while there is no such global ordering with Darcs.
145 """
145 """
146 return False
146 return False
147
147
148 def lookuprev(self, rev):
148 def lookuprev(self, rev):
149 """If rev is a meaningful revision reference in source, return
149 """If rev is a meaningful revision reference in source, return
150 the referenced identifier in the same format used by getcommit().
150 the referenced identifier in the same format used by getcommit().
151 return None otherwise.
151 return None otherwise.
152 """
152 """
153 return None
153 return None
154
154
155 def getbookmarks(self):
155 def getbookmarks(self):
156 """Return the bookmarks as a dictionary of name: revision
156 """Return the bookmarks as a dictionary of name: revision
157
157
158 Bookmark names are to be UTF-8 strings.
158 Bookmark names are to be UTF-8 strings.
159 """
159 """
160 return {}
160 return {}
161
161
162 class converter_sink(object):
162 class converter_sink(object):
163 """Conversion sink (target) interface"""
163 """Conversion sink (target) interface"""
164
164
165 def __init__(self, ui, path):
165 def __init__(self, ui, path):
166 """Initialize conversion sink (or raise NoRepo("message")
166 """Initialize conversion sink (or raise NoRepo("message")
167 exception if path is not a valid repository)
167 exception if path is not a valid repository)
168
168
169 created is a list of paths to remove if a fatal error occurs
169 created is a list of paths to remove if a fatal error occurs
170 later"""
170 later"""
171 self.ui = ui
171 self.ui = ui
172 self.path = path
172 self.path = path
173 self.created = []
173 self.created = []
174
174
175 def getheads(self):
175 def getheads(self):
176 """Return a list of this repository's heads"""
176 """Return a list of this repository's heads"""
177 raise NotImplementedError
177 raise NotImplementedError
178
178
179 def revmapfile(self):
179 def revmapfile(self):
180 """Path to a file that will contain lines
180 """Path to a file that will contain lines
181 source_rev_id sink_rev_id
181 source_rev_id sink_rev_id
182 mapping equivalent revision identifiers for each system."""
182 mapping equivalent revision identifiers for each system."""
183 raise NotImplementedError
183 raise NotImplementedError
184
184
185 def authorfile(self):
185 def authorfile(self):
186 """Path to a file that will contain lines
186 """Path to a file that will contain lines
187 srcauthor=dstauthor
187 srcauthor=dstauthor
188 mapping equivalent authors identifiers for each system."""
188 mapping equivalent authors identifiers for each system."""
189 return None
189 return None
190
190
191 def putcommit(self, files, copies, parents, commit, source, revmap):
191 def putcommit(self, files, copies, parents, commit, source, revmap):
192 """Create a revision with all changed files listed in 'files'
192 """Create a revision with all changed files listed in 'files'
193 and having listed parents. 'commit' is a commit object
193 and having listed parents. 'commit' is a commit object
194 containing at a minimum the author, date, and message for this
194 containing at a minimum the author, date, and message for this
195 changeset. 'files' is a list of (path, version) tuples,
195 changeset. 'files' is a list of (path, version) tuples,
196 'copies' is a dictionary mapping destinations to sources,
196 'copies' is a dictionary mapping destinations to sources,
197 'source' is the source repository, and 'revmap' is a mapfile
197 'source' is the source repository, and 'revmap' is a mapfile
198 of source revisions to converted revisions. Only getfile() and
198 of source revisions to converted revisions. Only getfile() and
199 lookuprev() should be called on 'source'.
199 lookuprev() should be called on 'source'.
200
200
201 Note that the sink repository is not told to update itself to
201 Note that the sink repository is not told to update itself to
202 a particular revision (or even what that revision would be)
202 a particular revision (or even what that revision would be)
203 before it receives the file data.
203 before it receives the file data.
204 """
204 """
205 raise NotImplementedError
205 raise NotImplementedError
206
206
207 def puttags(self, tags):
207 def puttags(self, tags):
208 """Put tags into sink.
208 """Put tags into sink.
209
209
210 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
210 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
211 Return a pair (tag_revision, tag_parent_revision), or (None, None)
211 Return a pair (tag_revision, tag_parent_revision), or (None, None)
212 if nothing was changed.
212 if nothing was changed.
213 """
213 """
214 raise NotImplementedError
214 raise NotImplementedError
215
215
216 def setbranch(self, branch, pbranches):
216 def setbranch(self, branch, pbranches):
217 """Set the current branch name. Called before the first putcommit
217 """Set the current branch name. Called before the first putcommit
218 on the branch.
218 on the branch.
219 branch: branch name for subsequent commits
219 branch: branch name for subsequent commits
220 pbranches: (converted parent revision, parent branch) tuples"""
220 pbranches: (converted parent revision, parent branch) tuples"""
221 pass
221 pass
222
222
223 def setfilemapmode(self, active):
223 def setfilemapmode(self, active):
224 """Tell the destination that we're using a filemap
224 """Tell the destination that we're using a filemap
225
225
226 Some converter_sources (svn in particular) can claim that a file
226 Some converter_sources (svn in particular) can claim that a file
227 was changed in a revision, even if there was no change. This method
227 was changed in a revision, even if there was no change. This method
228 tells the destination that we're using a filemap and that it should
228 tells the destination that we're using a filemap and that it should
229 filter empty revisions.
229 filter empty revisions.
230 """
230 """
231 pass
231 pass
232
232
233 def before(self):
233 def before(self):
234 pass
234 pass
235
235
236 def after(self):
236 def after(self):
237 pass
237 pass
238
238
239 def putbookmarks(self, bookmarks):
239 def putbookmarks(self, bookmarks):
240 """Put bookmarks into sink.
240 """Put bookmarks into sink.
241
241
242 bookmarks: {bookmarkname: sink_rev_id, ...}
242 bookmarks: {bookmarkname: sink_rev_id, ...}
243 where bookmarkname is an UTF-8 string.
243 where bookmarkname is an UTF-8 string.
244 """
244 """
245 pass
245 pass
246
246
247 def hascommit(self, rev):
247 def hascommit(self, rev):
248 """Return True if the sink contains rev"""
248 """Return True if the sink contains rev"""
249 raise NotImplementedError
249 raise NotImplementedError
250
250
251 class commandline(object):
251 class commandline(object):
252 def __init__(self, ui, command):
252 def __init__(self, ui, command):
253 self.ui = ui
253 self.ui = ui
254 self.command = command
254 self.command = command
255
255
256 def prerun(self):
256 def prerun(self):
257 pass
257 pass
258
258
259 def postrun(self):
259 def postrun(self):
260 pass
260 pass
261
261
262 def _cmdline(self, cmd, *args, **kwargs):
262 def _cmdline(self, cmd, *args, **kwargs):
263 cmdline = [self.command, cmd] + list(args)
263 cmdline = [self.command, cmd] + list(args)
264 for k, v in kwargs.iteritems():
264 for k, v in kwargs.iteritems():
265 if len(k) == 1:
265 if len(k) == 1:
266 cmdline.append('-' + k)
266 cmdline.append('-' + k)
267 else:
267 else:
268 cmdline.append('--' + k.replace('_', '-'))
268 cmdline.append('--' + k.replace('_', '-'))
269 try:
269 try:
270 if len(k) == 1:
270 if len(k) == 1:
271 cmdline.append('' + v)
271 cmdline.append('' + v)
272 else:
272 else:
273 cmdline[-1] += '=' + v
273 cmdline[-1] += '=' + v
274 except TypeError:
274 except TypeError:
275 pass
275 pass
276 cmdline = [util.shellquote(arg) for arg in cmdline]
276 cmdline = [util.shellquote(arg) for arg in cmdline]
277 if not self.ui.debugflag:
277 if not self.ui.debugflag:
278 cmdline += ['2>', os.devnull]
278 cmdline += ['2>', os.devnull]
279 cmdline = ' '.join(cmdline)
279 cmdline = ' '.join(cmdline)
280 return cmdline
280 return cmdline
281
281
282 def _run(self, cmd, *args, **kwargs):
282 def _run(self, cmd, *args, **kwargs):
283 def popen(cmdline):
283 def popen(cmdline):
284 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
284 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
285 close_fds=util.closefds,
285 close_fds=util.closefds,
286 stdout=subprocess.PIPE)
286 stdout=subprocess.PIPE)
287 return p
287 return p
288 return self._dorun(popen, cmd, *args, **kwargs)
288 return self._dorun(popen, cmd, *args, **kwargs)
289
289
290 def _run2(self, cmd, *args, **kwargs):
290 def _run2(self, cmd, *args, **kwargs):
291 return self._dorun(util.popen2, cmd, *args, **kwargs)
291 return self._dorun(util.popen2, cmd, *args, **kwargs)
292
292
293 def _dorun(self, openfunc, cmd, *args, **kwargs):
293 def _dorun(self, openfunc, cmd, *args, **kwargs):
294 cmdline = self._cmdline(cmd, *args, **kwargs)
294 cmdline = self._cmdline(cmd, *args, **kwargs)
295 self.ui.debug('running: %s\n' % (cmdline,))
295 self.ui.debug('running: %s\n' % (cmdline,))
296 self.prerun()
296 self.prerun()
297 try:
297 try:
298 return openfunc(cmdline)
298 return openfunc(cmdline)
299 finally:
299 finally:
300 self.postrun()
300 self.postrun()
301
301
302 def run(self, cmd, *args, **kwargs):
302 def run(self, cmd, *args, **kwargs):
303 p = self._run(cmd, *args, **kwargs)
303 p = self._run(cmd, *args, **kwargs)
304 output = p.communicate()[0]
304 output = p.communicate()[0]
305 self.ui.debug(output)
305 self.ui.debug(output)
306 return output, p.returncode
306 return output, p.returncode
307
307
308 def runlines(self, cmd, *args, **kwargs):
308 def runlines(self, cmd, *args, **kwargs):
309 p = self._run(cmd, *args, **kwargs)
309 p = self._run(cmd, *args, **kwargs)
310 output = p.stdout.readlines()
310 output = p.stdout.readlines()
311 p.wait()
311 p.wait()
312 self.ui.debug(''.join(output))
312 self.ui.debug(''.join(output))
313 return output, p.returncode
313 return output, p.returncode
314
314
315 def checkexit(self, status, output=''):
315 def checkexit(self, status, output=''):
316 if status:
316 if status:
317 if output:
317 if output:
318 self.ui.warn(_('%s error:\n') % self.command)
318 self.ui.warn(_('%s error:\n') % self.command)
319 self.ui.warn(output)
319 self.ui.warn(output)
320 msg = util.explainexit(status)[0]
320 msg = util.explainexit(status)[0]
321 raise util.Abort('%s %s' % (self.command, msg))
321 raise util.Abort('%s %s' % (self.command, msg))
322
322
323 def run0(self, cmd, *args, **kwargs):
323 def run0(self, cmd, *args, **kwargs):
324 output, status = self.run(cmd, *args, **kwargs)
324 output, status = self.run(cmd, *args, **kwargs)
325 self.checkexit(status, output)
325 self.checkexit(status, output)
326 return output
326 return output
327
327
328 def runlines0(self, cmd, *args, **kwargs):
328 def runlines0(self, cmd, *args, **kwargs):
329 output, status = self.runlines(cmd, *args, **kwargs)
329 output, status = self.runlines(cmd, *args, **kwargs)
330 self.checkexit(status, ''.join(output))
330 self.checkexit(status, ''.join(output))
331 return output
331 return output
332
332
333 @propertycache
333 @propertycache
334 def argmax(self):
334 def argmax(self):
335 # POSIX requires at least 4096 bytes for ARG_MAX
335 # POSIX requires at least 4096 bytes for ARG_MAX
336 argmax = 4096
336 argmax = 4096
337 try:
337 try:
338 argmax = os.sysconf("SC_ARG_MAX")
338 argmax = os.sysconf("SC_ARG_MAX")
339 except (AttributeError, ValueError):
339 except (AttributeError, ValueError):
340 pass
340 pass
341
341
342 # Windows shells impose their own limits on command line length,
342 # Windows shells impose their own limits on command line length,
343 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
343 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
344 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
344 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
345 # details about cmd.exe limitations.
345 # details about cmd.exe limitations.
346
346
347 # Since ARG_MAX is for command line _and_ environment, lower our limit
347 # Since ARG_MAX is for command line _and_ environment, lower our limit
348 # (and make happy Windows shells while doing this).
348 # (and make happy Windows shells while doing this).
349 return argmax // 2 - 1
349 return argmax // 2 - 1
350
350
351 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
351 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
352 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
352 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
353 limit = self.argmax - cmdlen
353 limit = self.argmax - cmdlen
354 bytes = 0
354 bytes = 0
355 fl = []
355 fl = []
356 for fn in arglist:
356 for fn in arglist:
357 b = len(fn) + 3
357 b = len(fn) + 3
358 if bytes + b < limit or len(fl) == 0:
358 if bytes + b < limit or len(fl) == 0:
359 fl.append(fn)
359 fl.append(fn)
360 bytes += b
360 bytes += b
361 else:
361 else:
362 yield fl
362 yield fl
363 fl = [fn]
363 fl = [fn]
364 bytes = b
364 bytes = b
365 if fl:
365 if fl:
366 yield fl
366 yield fl
367
367
368 def xargs(self, arglist, cmd, *args, **kwargs):
368 def xargs(self, arglist, cmd, *args, **kwargs):
369 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
369 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
370 self.run0(cmd, *(list(args) + l), **kwargs)
370 self.run0(cmd, *(list(args) + l), **kwargs)
371
371
372 class mapfile(dict):
372 class mapfile(dict):
373 def __init__(self, ui, path):
373 def __init__(self, ui, path):
374 super(mapfile, self).__init__()
374 super(mapfile, self).__init__()
375 self.ui = ui
375 self.ui = ui
376 self.path = path
376 self.path = path
377 self.fp = None
377 self.fp = None
378 self.order = []
378 self.order = []
379 self._read()
379 self._read()
380
380
381 def _read(self):
381 def _read(self):
382 if not self.path:
382 if not self.path:
383 return
383 return
384 try:
384 try:
385 fp = open(self.path, 'r')
385 fp = open(self.path, 'r')
386 except IOError, err:
386 except IOError, err:
387 if err.errno != errno.ENOENT:
387 if err.errno != errno.ENOENT:
388 raise
388 raise
389 return
389 return
390 for i, line in enumerate(fp):
390 for i, line in enumerate(fp):
391 line = line.splitlines()[0].rstrip()
391 line = line.splitlines()[0].rstrip()
392 if not line:
392 if not line:
393 # Ignore blank lines
393 # Ignore blank lines
394 continue
394 continue
395 try:
395 try:
396 key, value = line.rsplit(' ', 1)
396 key, value = line.rsplit(' ', 1)
397 except ValueError:
397 except ValueError:
398 raise util.Abort(
398 raise util.Abort(
399 _('syntax error in %s(%d): key/value pair expected')
399 _('syntax error in %s(%d): key/value pair expected')
400 % (self.path, i + 1))
400 % (self.path, i + 1))
401 if key not in self:
401 if key not in self:
402 self.order.append(key)
402 self.order.append(key)
403 super(mapfile, self).__setitem__(key, value)
403 super(mapfile, self).__setitem__(key, value)
404 fp.close()
404 fp.close()
405
405
406 def __setitem__(self, key, value):
406 def __setitem__(self, key, value):
407 if self.fp is None:
407 if self.fp is None:
408 try:
408 try:
409 self.fp = open(self.path, 'a')
409 self.fp = open(self.path, 'a')
410 except IOError, err:
410 except IOError, err:
411 raise util.Abort(_('could not open map file %r: %s') %
411 raise util.Abort(_('could not open map file %r: %s') %
412 (self.path, err.strerror))
412 (self.path, err.strerror))
413 self.fp.write('%s %s\n' % (key, value))
413 self.fp.write('%s %s\n' % (key, value))
414 self.fp.flush()
414 self.fp.flush()
415 super(mapfile, self).__setitem__(key, value)
415 super(mapfile, self).__setitem__(key, value)
416
416
417 def close(self):
417 def close(self):
418 if self.fp:
418 if self.fp:
419 self.fp.close()
419 self.fp.close()
420 self.fp = None
420 self.fp = None
421
421
422 def parsesplicemap(path):
422 def parsesplicemap(path):
423 """Parse a splicemap, return a child/parents dictionary."""
423 """Parse a splicemap, return a child/parents dictionary."""
424 if not path:
424 if not path:
425 return {}
425 return {}
426 m = {}
426 m = {}
427 try:
427 try:
428 fp = open(path, 'r')
428 fp = open(path, 'r')
429 for i, line in enumerate(fp):
429 for i, line in enumerate(fp):
430 line = line.splitlines()[0].rstrip()
430 line = line.splitlines()[0].rstrip()
431 if not line:
431 if not line:
432 # Ignore blank lines
432 # Ignore blank lines
433 continue
433 continue
434 try:
434 try:
435 child, parents = line.split(' ', 1)
435 child, parents = line.split(' ', 1)
436 parents = parents.replace(',', ' ').split()
436 parents = parents.replace(',', ' ').split()
437 except ValueError:
437 except ValueError:
438 raise util.Abort(_('syntax error in %s(%d): child parent1'
438 raise util.Abort(_('syntax error in %s(%d): child parent1'
439 '[,parent2] expected') % (path, i + 1))
439 '[,parent2] expected') % (path, i + 1))
440 pp = []
440 pp = []
441 for p in parents:
441 for p in parents:
442 if p not in pp:
442 if p not in pp:
443 pp.append(p)
443 pp.append(p)
444 m[child] = pp
444 m[child] = pp
445 except IOError, e:
445 except IOError, e:
446 if e.errno != errno.ENOENT:
446 if e.errno != errno.ENOENT:
447 raise
447 raise
448 return m
448 return m
449
450 def makedatetimestamp(t):
451 """Like util.makedate() but for time t instead of current time"""
452 delta = (datetime.datetime.utcfromtimestamp(t) -
453 datetime.datetime.fromtimestamp(t))
454 tz = delta.days * 86400 + delta.seconds
455 return t, tz
@@ -1,272 +1,275 b''
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 os, re, socket, errno
8 import os, re, socket, errno
9 from cStringIO import StringIO
9 from cStringIO import StringIO
10 from mercurial import encoding, util
10 from mercurial import encoding, util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 from common import NoRepo, commit, converter_source, checktool
13 from common import NoRepo, commit, converter_source, checktool
14 from common import makedatetimestamp
14 import cvsps
15 import cvsps
15
16
16 class convert_cvs(converter_source):
17 class convert_cvs(converter_source):
17 def __init__(self, ui, path, rev=None):
18 def __init__(self, ui, path, rev=None):
18 super(convert_cvs, self).__init__(ui, path, rev=rev)
19 super(convert_cvs, self).__init__(ui, path, rev=rev)
19
20
20 cvs = os.path.join(path, "CVS")
21 cvs = os.path.join(path, "CVS")
21 if not os.path.exists(cvs):
22 if not os.path.exists(cvs):
22 raise NoRepo(_("%s does not look like a CVS checkout") % path)
23 raise NoRepo(_("%s does not look like a CVS checkout") % path)
23
24
24 checktool('cvs')
25 checktool('cvs')
25
26
26 self.changeset = None
27 self.changeset = None
27 self.files = {}
28 self.files = {}
28 self.tags = {}
29 self.tags = {}
29 self.lastbranch = {}
30 self.lastbranch = {}
30 self.socket = None
31 self.socket = None
31 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 self.encoding = encoding.encoding
34 self.encoding = encoding.encoding
34
35
35 self._connect()
36 self._connect()
36
37
37 def _parse(self):
38 def _parse(self):
38 if self.changeset is not None:
39 if self.changeset is not None:
39 return
40 return
40 self.changeset = {}
41 self.changeset = {}
41
42
42 maxrev = 0
43 maxrev = 0
43 if self.rev:
44 if self.rev:
44 # TODO: handle tags
45 # TODO: handle tags
45 try:
46 try:
46 # patchset number?
47 # patchset number?
47 maxrev = int(self.rev)
48 maxrev = int(self.rev)
48 except ValueError:
49 except ValueError:
49 raise util.Abort(_('revision %s is not a patchset number')
50 raise util.Abort(_('revision %s is not a patchset number')
50 % self.rev)
51 % self.rev)
51
52
52 d = os.getcwd()
53 d = os.getcwd()
53 try:
54 try:
54 os.chdir(self.path)
55 os.chdir(self.path)
55 id = None
56 id = None
56
57
57 cache = 'update'
58 cache = 'update'
58 if not self.ui.configbool('convert', 'cvsps.cache', True):
59 if not self.ui.configbool('convert', 'cvsps.cache', True):
59 cache = None
60 cache = None
60 db = cvsps.createlog(self.ui, cache=cache)
61 db = cvsps.createlog(self.ui, cache=cache)
61 db = cvsps.createchangeset(self.ui, db,
62 db = cvsps.createchangeset(self.ui, db,
62 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
63 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
63 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
64 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
64 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
65 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
65
66
66 for cs in db:
67 for cs in db:
67 if maxrev and cs.id > maxrev:
68 if maxrev and cs.id > maxrev:
68 break
69 break
69 id = str(cs.id)
70 id = str(cs.id)
70 cs.author = self.recode(cs.author)
71 cs.author = self.recode(cs.author)
71 self.lastbranch[cs.branch] = id
72 self.lastbranch[cs.branch] = id
72 cs.comment = self.recode(cs.comment)
73 cs.comment = self.recode(cs.comment)
74 if self.ui.configbool('convert', 'localtimezone'):
75 cs.date = makedatetimestamp(cs.date[0])
73 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
76 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
74 self.tags.update(dict.fromkeys(cs.tags, id))
77 self.tags.update(dict.fromkeys(cs.tags, id))
75
78
76 files = {}
79 files = {}
77 for f in cs.entries:
80 for f in cs.entries:
78 files[f.file] = "%s%s" % ('.'.join([str(x)
81 files[f.file] = "%s%s" % ('.'.join([str(x)
79 for x in f.revision]),
82 for x in f.revision]),
80 ['', '(DEAD)'][f.dead])
83 ['', '(DEAD)'][f.dead])
81
84
82 # add current commit to set
85 # add current commit to set
83 c = commit(author=cs.author, date=date,
86 c = commit(author=cs.author, date=date,
84 parents=[str(p.id) for p in cs.parents],
87 parents=[str(p.id) for p in cs.parents],
85 desc=cs.comment, branch=cs.branch or '')
88 desc=cs.comment, branch=cs.branch or '')
86 self.changeset[id] = c
89 self.changeset[id] = c
87 self.files[id] = files
90 self.files[id] = files
88
91
89 self.heads = self.lastbranch.values()
92 self.heads = self.lastbranch.values()
90 finally:
93 finally:
91 os.chdir(d)
94 os.chdir(d)
92
95
93 def _connect(self):
96 def _connect(self):
94 root = self.cvsroot
97 root = self.cvsroot
95 conntype = None
98 conntype = None
96 user, host = None, None
99 user, host = None, None
97 cmd = ['cvs', 'server']
100 cmd = ['cvs', 'server']
98
101
99 self.ui.status(_("connecting to %s\n") % root)
102 self.ui.status(_("connecting to %s\n") % root)
100
103
101 if root.startswith(":pserver:"):
104 if root.startswith(":pserver:"):
102 root = root[9:]
105 root = root[9:]
103 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
106 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
104 root)
107 root)
105 if m:
108 if m:
106 conntype = "pserver"
109 conntype = "pserver"
107 user, passw, serv, port, root = m.groups()
110 user, passw, serv, port, root = m.groups()
108 if not user:
111 if not user:
109 user = "anonymous"
112 user = "anonymous"
110 if not port:
113 if not port:
111 port = 2401
114 port = 2401
112 else:
115 else:
113 port = int(port)
116 port = int(port)
114 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
117 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
115 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
118 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
116
119
117 if not passw:
120 if not passw:
118 passw = "A"
121 passw = "A"
119 cvspass = os.path.expanduser("~/.cvspass")
122 cvspass = os.path.expanduser("~/.cvspass")
120 try:
123 try:
121 pf = open(cvspass)
124 pf = open(cvspass)
122 for line in pf.read().splitlines():
125 for line in pf.read().splitlines():
123 part1, part2 = line.split(' ', 1)
126 part1, part2 = line.split(' ', 1)
124 # /1 :pserver:user@example.com:2401/cvsroot/foo
127 # /1 :pserver:user@example.com:2401/cvsroot/foo
125 # Ah<Z
128 # Ah<Z
126 if part1 == '/1':
129 if part1 == '/1':
127 part1, part2 = part2.split(' ', 1)
130 part1, part2 = part2.split(' ', 1)
128 format = format1
131 format = format1
129 # :pserver:user@example.com:/cvsroot/foo Ah<Z
132 # :pserver:user@example.com:/cvsroot/foo Ah<Z
130 else:
133 else:
131 format = format0
134 format = format0
132 if part1 == format:
135 if part1 == format:
133 passw = part2
136 passw = part2
134 break
137 break
135 pf.close()
138 pf.close()
136 except IOError, inst:
139 except IOError, inst:
137 if inst.errno != errno.ENOENT:
140 if inst.errno != errno.ENOENT:
138 if not getattr(inst, 'filename', None):
141 if not getattr(inst, 'filename', None):
139 inst.filename = cvspass
142 inst.filename = cvspass
140 raise
143 raise
141
144
142 sck = socket.socket()
145 sck = socket.socket()
143 sck.connect((serv, port))
146 sck.connect((serv, port))
144 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
147 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
145 "END AUTH REQUEST", ""]))
148 "END AUTH REQUEST", ""]))
146 if sck.recv(128) != "I LOVE YOU\n":
149 if sck.recv(128) != "I LOVE YOU\n":
147 raise util.Abort(_("CVS pserver authentication failed"))
150 raise util.Abort(_("CVS pserver authentication failed"))
148
151
149 self.writep = self.readp = sck.makefile('r+')
152 self.writep = self.readp = sck.makefile('r+')
150
153
151 if not conntype and root.startswith(":local:"):
154 if not conntype and root.startswith(":local:"):
152 conntype = "local"
155 conntype = "local"
153 root = root[7:]
156 root = root[7:]
154
157
155 if not conntype:
158 if not conntype:
156 # :ext:user@host/home/user/path/to/cvsroot
159 # :ext:user@host/home/user/path/to/cvsroot
157 if root.startswith(":ext:"):
160 if root.startswith(":ext:"):
158 root = root[5:]
161 root = root[5:]
159 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
162 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
160 # Do not take Windows path "c:\foo\bar" for a connection strings
163 # Do not take Windows path "c:\foo\bar" for a connection strings
161 if os.path.isdir(root) or not m:
164 if os.path.isdir(root) or not m:
162 conntype = "local"
165 conntype = "local"
163 else:
166 else:
164 conntype = "rsh"
167 conntype = "rsh"
165 user, host, root = m.group(1), m.group(2), m.group(3)
168 user, host, root = m.group(1), m.group(2), m.group(3)
166
169
167 if conntype != "pserver":
170 if conntype != "pserver":
168 if conntype == "rsh":
171 if conntype == "rsh":
169 rsh = os.environ.get("CVS_RSH") or "ssh"
172 rsh = os.environ.get("CVS_RSH") or "ssh"
170 if user:
173 if user:
171 cmd = [rsh, '-l', user, host] + cmd
174 cmd = [rsh, '-l', user, host] + cmd
172 else:
175 else:
173 cmd = [rsh, host] + cmd
176 cmd = [rsh, host] + cmd
174
177
175 # popen2 does not support argument lists under Windows
178 # popen2 does not support argument lists under Windows
176 cmd = [util.shellquote(arg) for arg in cmd]
179 cmd = [util.shellquote(arg) for arg in cmd]
177 cmd = util.quotecommand(' '.join(cmd))
180 cmd = util.quotecommand(' '.join(cmd))
178 self.writep, self.readp = util.popen2(cmd)
181 self.writep, self.readp = util.popen2(cmd)
179
182
180 self.realroot = root
183 self.realroot = root
181
184
182 self.writep.write("Root %s\n" % root)
185 self.writep.write("Root %s\n" % root)
183 self.writep.write("Valid-responses ok error Valid-requests Mode"
186 self.writep.write("Valid-responses ok error Valid-requests Mode"
184 " M Mbinary E Checked-in Created Updated"
187 " M Mbinary E Checked-in Created Updated"
185 " Merged Removed\n")
188 " Merged Removed\n")
186 self.writep.write("valid-requests\n")
189 self.writep.write("valid-requests\n")
187 self.writep.flush()
190 self.writep.flush()
188 r = self.readp.readline()
191 r = self.readp.readline()
189 if not r.startswith("Valid-requests"):
192 if not r.startswith("Valid-requests"):
190 raise util.Abort(_('unexpected response from CVS server '
193 raise util.Abort(_('unexpected response from CVS server '
191 '(expected "Valid-requests", but got %r)')
194 '(expected "Valid-requests", but got %r)')
192 % r)
195 % r)
193 if "UseUnchanged" in r:
196 if "UseUnchanged" in r:
194 self.writep.write("UseUnchanged\n")
197 self.writep.write("UseUnchanged\n")
195 self.writep.flush()
198 self.writep.flush()
196 r = self.readp.readline()
199 r = self.readp.readline()
197
200
198 def getheads(self):
201 def getheads(self):
199 self._parse()
202 self._parse()
200 return self.heads
203 return self.heads
201
204
202 def getfile(self, name, rev):
205 def getfile(self, name, rev):
203
206
204 def chunkedread(fp, count):
207 def chunkedread(fp, count):
205 # file-objects returned by socket.makefile() do not handle
208 # file-objects returned by socket.makefile() do not handle
206 # large read() requests very well.
209 # large read() requests very well.
207 chunksize = 65536
210 chunksize = 65536
208 output = StringIO()
211 output = StringIO()
209 while count > 0:
212 while count > 0:
210 data = fp.read(min(count, chunksize))
213 data = fp.read(min(count, chunksize))
211 if not data:
214 if not data:
212 raise util.Abort(_("%d bytes missing from remote file")
215 raise util.Abort(_("%d bytes missing from remote file")
213 % count)
216 % count)
214 count -= len(data)
217 count -= len(data)
215 output.write(data)
218 output.write(data)
216 return output.getvalue()
219 return output.getvalue()
217
220
218 self._parse()
221 self._parse()
219 if rev.endswith("(DEAD)"):
222 if rev.endswith("(DEAD)"):
220 raise IOError
223 raise IOError
221
224
222 args = ("-N -P -kk -r %s --" % rev).split()
225 args = ("-N -P -kk -r %s --" % rev).split()
223 args.append(self.cvsrepo + '/' + name)
226 args.append(self.cvsrepo + '/' + name)
224 for x in args:
227 for x in args:
225 self.writep.write("Argument %s\n" % x)
228 self.writep.write("Argument %s\n" % x)
226 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
229 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
227 self.writep.flush()
230 self.writep.flush()
228
231
229 data = ""
232 data = ""
230 mode = None
233 mode = None
231 while True:
234 while True:
232 line = self.readp.readline()
235 line = self.readp.readline()
233 if line.startswith("Created ") or line.startswith("Updated "):
236 if line.startswith("Created ") or line.startswith("Updated "):
234 self.readp.readline() # path
237 self.readp.readline() # path
235 self.readp.readline() # entries
238 self.readp.readline() # entries
236 mode = self.readp.readline()[:-1]
239 mode = self.readp.readline()[:-1]
237 count = int(self.readp.readline()[:-1])
240 count = int(self.readp.readline()[:-1])
238 data = chunkedread(self.readp, count)
241 data = chunkedread(self.readp, count)
239 elif line.startswith(" "):
242 elif line.startswith(" "):
240 data += line[1:]
243 data += line[1:]
241 elif line.startswith("M "):
244 elif line.startswith("M "):
242 pass
245 pass
243 elif line.startswith("Mbinary "):
246 elif line.startswith("Mbinary "):
244 count = int(self.readp.readline()[:-1])
247 count = int(self.readp.readline()[:-1])
245 data = chunkedread(self.readp, count)
248 data = chunkedread(self.readp, count)
246 else:
249 else:
247 if line == "ok\n":
250 if line == "ok\n":
248 if mode is None:
251 if mode is None:
249 raise util.Abort(_('malformed response from CVS'))
252 raise util.Abort(_('malformed response from CVS'))
250 return (data, "x" in mode and "x" or "")
253 return (data, "x" in mode and "x" or "")
251 elif line.startswith("E "):
254 elif line.startswith("E "):
252 self.ui.warn(_("cvs server: %s\n") % line[2:])
255 self.ui.warn(_("cvs server: %s\n") % line[2:])
253 elif line.startswith("Remove"):
256 elif line.startswith("Remove"):
254 self.readp.readline()
257 self.readp.readline()
255 else:
258 else:
256 raise util.Abort(_("unknown CVS response: %s") % line)
259 raise util.Abort(_("unknown CVS response: %s") % line)
257
260
258 def getchanges(self, rev):
261 def getchanges(self, rev):
259 self._parse()
262 self._parse()
260 return sorted(self.files[rev].iteritems()), {}
263 return sorted(self.files[rev].iteritems()), {}
261
264
262 def getcommit(self, rev):
265 def getcommit(self, rev):
263 self._parse()
266 self._parse()
264 return self.changeset[rev]
267 return self.changeset[rev]
265
268
266 def gettags(self):
269 def gettags(self):
267 self._parse()
270 self._parse()
268 return self.tags
271 return self.tags
269
272
270 def getchangedfiles(self, rev, i):
273 def getchangedfiles(self, rev, i):
271 self._parse()
274 self._parse()
272 return sorted(self.files[rev])
275 return sorted(self.files[rev])
@@ -1,1251 +1,1254 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4
4
5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
6 import cPickle as pickle
6 import cPickle as pickle
7
7
8 from mercurial import strutil, scmutil, util, encoding
8 from mercurial import strutil, scmutil, util, encoding
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10
10
11 propertycache = util.propertycache
11 propertycache = util.propertycache
12
12
13 # Subversion stuff. Works best with very recent Python SVN bindings
13 # Subversion stuff. Works best with very recent Python SVN bindings
14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 # these bindings.
15 # these bindings.
16
16
17 from cStringIO import StringIO
17 from cStringIO import StringIO
18
18
19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 from common import commandline, converter_source, converter_sink, mapfile
20 from common import commandline, converter_source, converter_sink, mapfile
21 from common import makedatetimestamp
21
22
22 try:
23 try:
23 from svn.core import SubversionException, Pool
24 from svn.core import SubversionException, Pool
24 import svn
25 import svn
25 import svn.client
26 import svn.client
26 import svn.core
27 import svn.core
27 import svn.ra
28 import svn.ra
28 import svn.delta
29 import svn.delta
29 import transport
30 import transport
30 import warnings
31 import warnings
31 warnings.filterwarnings('ignore',
32 warnings.filterwarnings('ignore',
32 module='svn.core',
33 module='svn.core',
33 category=DeprecationWarning)
34 category=DeprecationWarning)
34
35
35 except ImportError:
36 except ImportError:
36 svn = None
37 svn = None
37
38
38 class SvnPathNotFound(Exception):
39 class SvnPathNotFound(Exception):
39 pass
40 pass
40
41
41 def revsplit(rev):
42 def revsplit(rev):
42 """Parse a revision string and return (uuid, path, revnum)."""
43 """Parse a revision string and return (uuid, path, revnum)."""
43 url, revnum = rev.rsplit('@', 1)
44 url, revnum = rev.rsplit('@', 1)
44 parts = url.split('/', 1)
45 parts = url.split('/', 1)
45 mod = ''
46 mod = ''
46 if len(parts) > 1:
47 if len(parts) > 1:
47 mod = '/' + parts[1]
48 mod = '/' + parts[1]
48 return parts[0][4:], mod, int(revnum)
49 return parts[0][4:], mod, int(revnum)
49
50
50 def quote(s):
51 def quote(s):
51 # As of svn 1.7, many svn calls expect "canonical" paths. In
52 # As of svn 1.7, many svn calls expect "canonical" paths. In
52 # theory, we should call svn.core.*canonicalize() on all paths
53 # theory, we should call svn.core.*canonicalize() on all paths
53 # before passing them to the API. Instead, we assume the base url
54 # before passing them to the API. Instead, we assume the base url
54 # is canonical and copy the behaviour of svn URL encoding function
55 # is canonical and copy the behaviour of svn URL encoding function
55 # so we can extend it safely with new components. The "safe"
56 # so we can extend it safely with new components. The "safe"
56 # characters were taken from the "svn_uri__char_validity" table in
57 # characters were taken from the "svn_uri__char_validity" table in
57 # libsvn_subr/path.c.
58 # libsvn_subr/path.c.
58 return urllib.quote(s, "!$&'()*+,-./:=@_~")
59 return urllib.quote(s, "!$&'()*+,-./:=@_~")
59
60
60 def geturl(path):
61 def geturl(path):
61 try:
62 try:
62 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
63 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
63 except SubversionException:
64 except SubversionException:
64 # svn.client.url_from_path() fails with local repositories
65 # svn.client.url_from_path() fails with local repositories
65 pass
66 pass
66 if os.path.isdir(path):
67 if os.path.isdir(path):
67 path = os.path.normpath(os.path.abspath(path))
68 path = os.path.normpath(os.path.abspath(path))
68 if os.name == 'nt':
69 if os.name == 'nt':
69 path = '/' + util.normpath(path)
70 path = '/' + util.normpath(path)
70 # Module URL is later compared with the repository URL returned
71 # Module URL is later compared with the repository URL returned
71 # by svn API, which is UTF-8.
72 # by svn API, which is UTF-8.
72 path = encoding.tolocal(path)
73 path = encoding.tolocal(path)
73 path = 'file://%s' % quote(path)
74 path = 'file://%s' % quote(path)
74 return svn.core.svn_path_canonicalize(path)
75 return svn.core.svn_path_canonicalize(path)
75
76
76 def optrev(number):
77 def optrev(number):
77 optrev = svn.core.svn_opt_revision_t()
78 optrev = svn.core.svn_opt_revision_t()
78 optrev.kind = svn.core.svn_opt_revision_number
79 optrev.kind = svn.core.svn_opt_revision_number
79 optrev.value.number = number
80 optrev.value.number = number
80 return optrev
81 return optrev
81
82
82 class changedpath(object):
83 class changedpath(object):
83 def __init__(self, p):
84 def __init__(self, p):
84 self.copyfrom_path = p.copyfrom_path
85 self.copyfrom_path = p.copyfrom_path
85 self.copyfrom_rev = p.copyfrom_rev
86 self.copyfrom_rev = p.copyfrom_rev
86 self.action = p.action
87 self.action = p.action
87
88
88 def get_log_child(fp, url, paths, start, end, limit=0,
89 def get_log_child(fp, url, paths, start, end, limit=0,
89 discover_changed_paths=True, strict_node_history=False):
90 discover_changed_paths=True, strict_node_history=False):
90 protocol = -1
91 protocol = -1
91 def receiver(orig_paths, revnum, author, date, message, pool):
92 def receiver(orig_paths, revnum, author, date, message, pool):
92 if orig_paths is not None:
93 if orig_paths is not None:
93 for k, v in orig_paths.iteritems():
94 for k, v in orig_paths.iteritems():
94 orig_paths[k] = changedpath(v)
95 orig_paths[k] = changedpath(v)
95 pickle.dump((orig_paths, revnum, author, date, message),
96 pickle.dump((orig_paths, revnum, author, date, message),
96 fp, protocol)
97 fp, protocol)
97
98
98 try:
99 try:
99 # Use an ra of our own so that our parent can consume
100 # Use an ra of our own so that our parent can consume
100 # our results without confusing the server.
101 # our results without confusing the server.
101 t = transport.SvnRaTransport(url=url)
102 t = transport.SvnRaTransport(url=url)
102 svn.ra.get_log(t.ra, paths, start, end, limit,
103 svn.ra.get_log(t.ra, paths, start, end, limit,
103 discover_changed_paths,
104 discover_changed_paths,
104 strict_node_history,
105 strict_node_history,
105 receiver)
106 receiver)
106 except IOError:
107 except IOError:
107 # Caller may interrupt the iteration
108 # Caller may interrupt the iteration
108 pickle.dump(None, fp, protocol)
109 pickle.dump(None, fp, protocol)
109 except Exception, inst:
110 except Exception, inst:
110 pickle.dump(str(inst), fp, protocol)
111 pickle.dump(str(inst), fp, protocol)
111 else:
112 else:
112 pickle.dump(None, fp, protocol)
113 pickle.dump(None, fp, protocol)
113 fp.close()
114 fp.close()
114 # With large history, cleanup process goes crazy and suddenly
115 # With large history, cleanup process goes crazy and suddenly
115 # consumes *huge* amount of memory. The output file being closed,
116 # consumes *huge* amount of memory. The output file being closed,
116 # there is no need for clean termination.
117 # there is no need for clean termination.
117 os._exit(0)
118 os._exit(0)
118
119
119 def debugsvnlog(ui, **opts):
120 def debugsvnlog(ui, **opts):
120 """Fetch SVN log in a subprocess and channel them back to parent to
121 """Fetch SVN log in a subprocess and channel them back to parent to
121 avoid memory collection issues.
122 avoid memory collection issues.
122 """
123 """
123 if svn is None:
124 if svn is None:
124 raise util.Abort(_('debugsvnlog could not load Subversion python '
125 raise util.Abort(_('debugsvnlog could not load Subversion python '
125 'bindings'))
126 'bindings'))
126
127
127 util.setbinary(sys.stdin)
128 util.setbinary(sys.stdin)
128 util.setbinary(sys.stdout)
129 util.setbinary(sys.stdout)
129 args = decodeargs(sys.stdin.read())
130 args = decodeargs(sys.stdin.read())
130 get_log_child(sys.stdout, *args)
131 get_log_child(sys.stdout, *args)
131
132
132 class logstream(object):
133 class logstream(object):
133 """Interruptible revision log iterator."""
134 """Interruptible revision log iterator."""
134 def __init__(self, stdout):
135 def __init__(self, stdout):
135 self._stdout = stdout
136 self._stdout = stdout
136
137
137 def __iter__(self):
138 def __iter__(self):
138 while True:
139 while True:
139 try:
140 try:
140 entry = pickle.load(self._stdout)
141 entry = pickle.load(self._stdout)
141 except EOFError:
142 except EOFError:
142 raise util.Abort(_('Mercurial failed to run itself, check'
143 raise util.Abort(_('Mercurial failed to run itself, check'
143 ' hg executable is in PATH'))
144 ' hg executable is in PATH'))
144 try:
145 try:
145 orig_paths, revnum, author, date, message = entry
146 orig_paths, revnum, author, date, message = entry
146 except (TypeError, ValueError):
147 except (TypeError, ValueError):
147 if entry is None:
148 if entry is None:
148 break
149 break
149 raise util.Abort(_("log stream exception '%s'") % entry)
150 raise util.Abort(_("log stream exception '%s'") % entry)
150 yield entry
151 yield entry
151
152
152 def close(self):
153 def close(self):
153 if self._stdout:
154 if self._stdout:
154 self._stdout.close()
155 self._stdout.close()
155 self._stdout = None
156 self._stdout = None
156
157
157
158
158 # Check to see if the given path is a local Subversion repo. Verify this by
159 # Check to see if the given path is a local Subversion repo. Verify this by
159 # looking for several svn-specific files and directories in the given
160 # looking for several svn-specific files and directories in the given
160 # directory.
161 # directory.
161 def filecheck(ui, path, proto):
162 def filecheck(ui, path, proto):
162 for x in ('locks', 'hooks', 'format', 'db'):
163 for x in ('locks', 'hooks', 'format', 'db'):
163 if not os.path.exists(os.path.join(path, x)):
164 if not os.path.exists(os.path.join(path, x)):
164 return False
165 return False
165 return True
166 return True
166
167
167 # Check to see if a given path is the root of an svn repo over http. We verify
168 # Check to see if a given path is the root of an svn repo over http. We verify
168 # this by requesting a version-controlled URL we know can't exist and looking
169 # this by requesting a version-controlled URL we know can't exist and looking
169 # for the svn-specific "not found" XML.
170 # for the svn-specific "not found" XML.
170 def httpcheck(ui, path, proto):
171 def httpcheck(ui, path, proto):
171 try:
172 try:
172 opener = urllib2.build_opener()
173 opener = urllib2.build_opener()
173 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
174 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
174 data = rsp.read()
175 data = rsp.read()
175 except urllib2.HTTPError, inst:
176 except urllib2.HTTPError, inst:
176 if inst.code != 404:
177 if inst.code != 404:
177 # Except for 404 we cannot know for sure this is not an svn repo
178 # Except for 404 we cannot know for sure this is not an svn repo
178 ui.warn(_('svn: cannot probe remote repository, assume it could '
179 ui.warn(_('svn: cannot probe remote repository, assume it could '
179 'be a subversion repository. Use --source-type if you '
180 'be a subversion repository. Use --source-type if you '
180 'know better.\n'))
181 'know better.\n'))
181 return True
182 return True
182 data = inst.fp.read()
183 data = inst.fp.read()
183 except Exception:
184 except Exception:
184 # Could be urllib2.URLError if the URL is invalid or anything else.
185 # Could be urllib2.URLError if the URL is invalid or anything else.
185 return False
186 return False
186 return '<m:human-readable errcode="160013">' in data
187 return '<m:human-readable errcode="160013">' in data
187
188
188 protomap = {'http': httpcheck,
189 protomap = {'http': httpcheck,
189 'https': httpcheck,
190 'https': httpcheck,
190 'file': filecheck,
191 'file': filecheck,
191 }
192 }
192 def issvnurl(ui, url):
193 def issvnurl(ui, url):
193 try:
194 try:
194 proto, path = url.split('://', 1)
195 proto, path = url.split('://', 1)
195 if proto == 'file':
196 if proto == 'file':
196 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
197 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
197 and path[2:6].lower() == '%3a/'):
198 and path[2:6].lower() == '%3a/'):
198 path = path[:2] + ':/' + path[6:]
199 path = path[:2] + ':/' + path[6:]
199 path = urllib.url2pathname(path)
200 path = urllib.url2pathname(path)
200 except ValueError:
201 except ValueError:
201 proto = 'file'
202 proto = 'file'
202 path = os.path.abspath(url)
203 path = os.path.abspath(url)
203 if proto == 'file':
204 if proto == 'file':
204 path = util.pconvert(path)
205 path = util.pconvert(path)
205 check = protomap.get(proto, lambda *args: False)
206 check = protomap.get(proto, lambda *args: False)
206 while '/' in path:
207 while '/' in path:
207 if check(ui, path, proto):
208 if check(ui, path, proto):
208 return True
209 return True
209 path = path.rsplit('/', 1)[0]
210 path = path.rsplit('/', 1)[0]
210 return False
211 return False
211
212
212 # SVN conversion code stolen from bzr-svn and tailor
213 # SVN conversion code stolen from bzr-svn and tailor
213 #
214 #
214 # Subversion looks like a versioned filesystem, branches structures
215 # Subversion looks like a versioned filesystem, branches structures
215 # are defined by conventions and not enforced by the tool. First,
216 # are defined by conventions and not enforced by the tool. First,
216 # we define the potential branches (modules) as "trunk" and "branches"
217 # we define the potential branches (modules) as "trunk" and "branches"
217 # children directories. Revisions are then identified by their
218 # children directories. Revisions are then identified by their
218 # module and revision number (and a repository identifier).
219 # module and revision number (and a repository identifier).
219 #
220 #
220 # The revision graph is really a tree (or a forest). By default, a
221 # The revision graph is really a tree (or a forest). By default, a
221 # revision parent is the previous revision in the same module. If the
222 # revision parent is the previous revision in the same module. If the
222 # module directory is copied/moved from another module then the
223 # module directory is copied/moved from another module then the
223 # revision is the module root and its parent the source revision in
224 # revision is the module root and its parent the source revision in
224 # the parent module. A revision has at most one parent.
225 # the parent module. A revision has at most one parent.
225 #
226 #
226 class svn_source(converter_source):
227 class svn_source(converter_source):
227 def __init__(self, ui, url, rev=None):
228 def __init__(self, ui, url, rev=None):
228 super(svn_source, self).__init__(ui, url, rev=rev)
229 super(svn_source, self).__init__(ui, url, rev=rev)
229
230
230 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
231 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
231 (os.path.exists(url) and
232 (os.path.exists(url) and
232 os.path.exists(os.path.join(url, '.svn'))) or
233 os.path.exists(os.path.join(url, '.svn'))) or
233 issvnurl(ui, url)):
234 issvnurl(ui, url)):
234 raise NoRepo(_("%s does not look like a Subversion repository")
235 raise NoRepo(_("%s does not look like a Subversion repository")
235 % url)
236 % url)
236 if svn is None:
237 if svn is None:
237 raise MissingTool(_('could not load Subversion python bindings'))
238 raise MissingTool(_('could not load Subversion python bindings'))
238
239
239 try:
240 try:
240 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
241 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
241 if version < (1, 4):
242 if version < (1, 4):
242 raise MissingTool(_('Subversion python bindings %d.%d found, '
243 raise MissingTool(_('Subversion python bindings %d.%d found, '
243 '1.4 or later required') % version)
244 '1.4 or later required') % version)
244 except AttributeError:
245 except AttributeError:
245 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
246 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
246 'or later required'))
247 'or later required'))
247
248
248 self.lastrevs = {}
249 self.lastrevs = {}
249
250
250 latest = None
251 latest = None
251 try:
252 try:
252 # Support file://path@rev syntax. Useful e.g. to convert
253 # Support file://path@rev syntax. Useful e.g. to convert
253 # deleted branches.
254 # deleted branches.
254 at = url.rfind('@')
255 at = url.rfind('@')
255 if at >= 0:
256 if at >= 0:
256 latest = int(url[at + 1:])
257 latest = int(url[at + 1:])
257 url = url[:at]
258 url = url[:at]
258 except ValueError:
259 except ValueError:
259 pass
260 pass
260 self.url = geturl(url)
261 self.url = geturl(url)
261 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
262 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
262 try:
263 try:
263 self.transport = transport.SvnRaTransport(url=self.url)
264 self.transport = transport.SvnRaTransport(url=self.url)
264 self.ra = self.transport.ra
265 self.ra = self.transport.ra
265 self.ctx = self.transport.client
266 self.ctx = self.transport.client
266 self.baseurl = svn.ra.get_repos_root(self.ra)
267 self.baseurl = svn.ra.get_repos_root(self.ra)
267 # Module is either empty or a repository path starting with
268 # Module is either empty or a repository path starting with
268 # a slash and not ending with a slash.
269 # a slash and not ending with a slash.
269 self.module = urllib.unquote(self.url[len(self.baseurl):])
270 self.module = urllib.unquote(self.url[len(self.baseurl):])
270 self.prevmodule = None
271 self.prevmodule = None
271 self.rootmodule = self.module
272 self.rootmodule = self.module
272 self.commits = {}
273 self.commits = {}
273 self.paths = {}
274 self.paths = {}
274 self.uuid = svn.ra.get_uuid(self.ra)
275 self.uuid = svn.ra.get_uuid(self.ra)
275 except SubversionException:
276 except SubversionException:
276 ui.traceback()
277 ui.traceback()
277 raise NoRepo(_("%s does not look like a Subversion repository")
278 raise NoRepo(_("%s does not look like a Subversion repository")
278 % self.url)
279 % self.url)
279
280
280 if rev:
281 if rev:
281 try:
282 try:
282 latest = int(rev)
283 latest = int(rev)
283 except ValueError:
284 except ValueError:
284 raise util.Abort(_('svn: revision %s is not an integer') % rev)
285 raise util.Abort(_('svn: revision %s is not an integer') % rev)
285
286
286 self.trunkname = self.ui.config('convert', 'svn.trunk',
287 self.trunkname = self.ui.config('convert', 'svn.trunk',
287 'trunk').strip('/')
288 'trunk').strip('/')
288 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
289 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
289 try:
290 try:
290 self.startrev = int(self.startrev)
291 self.startrev = int(self.startrev)
291 if self.startrev < 0:
292 if self.startrev < 0:
292 self.startrev = 0
293 self.startrev = 0
293 except ValueError:
294 except ValueError:
294 raise util.Abort(_('svn: start revision %s is not an integer')
295 raise util.Abort(_('svn: start revision %s is not an integer')
295 % self.startrev)
296 % self.startrev)
296
297
297 try:
298 try:
298 self.head = self.latest(self.module, latest)
299 self.head = self.latest(self.module, latest)
299 except SvnPathNotFound:
300 except SvnPathNotFound:
300 self.head = None
301 self.head = None
301 if not self.head:
302 if not self.head:
302 raise util.Abort(_('no revision found in module %s')
303 raise util.Abort(_('no revision found in module %s')
303 % self.module)
304 % self.module)
304 self.last_changed = self.revnum(self.head)
305 self.last_changed = self.revnum(self.head)
305
306
306 self._changescache = None
307 self._changescache = None
307
308
308 if os.path.exists(os.path.join(url, '.svn/entries')):
309 if os.path.exists(os.path.join(url, '.svn/entries')):
309 self.wc = url
310 self.wc = url
310 else:
311 else:
311 self.wc = None
312 self.wc = None
312 self.convertfp = None
313 self.convertfp = None
313
314
314 def setrevmap(self, revmap):
315 def setrevmap(self, revmap):
315 lastrevs = {}
316 lastrevs = {}
316 for revid in revmap.iterkeys():
317 for revid in revmap.iterkeys():
317 uuid, module, revnum = revsplit(revid)
318 uuid, module, revnum = revsplit(revid)
318 lastrevnum = lastrevs.setdefault(module, revnum)
319 lastrevnum = lastrevs.setdefault(module, revnum)
319 if revnum > lastrevnum:
320 if revnum > lastrevnum:
320 lastrevs[module] = revnum
321 lastrevs[module] = revnum
321 self.lastrevs = lastrevs
322 self.lastrevs = lastrevs
322
323
323 def exists(self, path, optrev):
324 def exists(self, path, optrev):
324 try:
325 try:
325 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
326 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
326 optrev, False, self.ctx)
327 optrev, False, self.ctx)
327 return True
328 return True
328 except SubversionException:
329 except SubversionException:
329 return False
330 return False
330
331
331 def getheads(self):
332 def getheads(self):
332
333
333 def isdir(path, revnum):
334 def isdir(path, revnum):
334 kind = self._checkpath(path, revnum)
335 kind = self._checkpath(path, revnum)
335 return kind == svn.core.svn_node_dir
336 return kind == svn.core.svn_node_dir
336
337
337 def getcfgpath(name, rev):
338 def getcfgpath(name, rev):
338 cfgpath = self.ui.config('convert', 'svn.' + name)
339 cfgpath = self.ui.config('convert', 'svn.' + name)
339 if cfgpath is not None and cfgpath.strip() == '':
340 if cfgpath is not None and cfgpath.strip() == '':
340 return None
341 return None
341 path = (cfgpath or name).strip('/')
342 path = (cfgpath or name).strip('/')
342 if not self.exists(path, rev):
343 if not self.exists(path, rev):
343 if self.module.endswith(path) and name == 'trunk':
344 if self.module.endswith(path) and name == 'trunk':
344 # we are converting from inside this directory
345 # we are converting from inside this directory
345 return None
346 return None
346 if cfgpath:
347 if cfgpath:
347 raise util.Abort(_('expected %s to be at %r, but not found')
348 raise util.Abort(_('expected %s to be at %r, but not found')
348 % (name, path))
349 % (name, path))
349 return None
350 return None
350 self.ui.note(_('found %s at %r\n') % (name, path))
351 self.ui.note(_('found %s at %r\n') % (name, path))
351 return path
352 return path
352
353
353 rev = optrev(self.last_changed)
354 rev = optrev(self.last_changed)
354 oldmodule = ''
355 oldmodule = ''
355 trunk = getcfgpath('trunk', rev)
356 trunk = getcfgpath('trunk', rev)
356 self.tags = getcfgpath('tags', rev)
357 self.tags = getcfgpath('tags', rev)
357 branches = getcfgpath('branches', rev)
358 branches = getcfgpath('branches', rev)
358
359
359 # If the project has a trunk or branches, we will extract heads
360 # If the project has a trunk or branches, we will extract heads
360 # from them. We keep the project root otherwise.
361 # from them. We keep the project root otherwise.
361 if trunk:
362 if trunk:
362 oldmodule = self.module or ''
363 oldmodule = self.module or ''
363 self.module += '/' + trunk
364 self.module += '/' + trunk
364 self.head = self.latest(self.module, self.last_changed)
365 self.head = self.latest(self.module, self.last_changed)
365 if not self.head:
366 if not self.head:
366 raise util.Abort(_('no revision found in module %s')
367 raise util.Abort(_('no revision found in module %s')
367 % self.module)
368 % self.module)
368
369
369 # First head in the list is the module's head
370 # First head in the list is the module's head
370 self.heads = [self.head]
371 self.heads = [self.head]
371 if self.tags is not None:
372 if self.tags is not None:
372 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
373 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
373
374
374 # Check if branches bring a few more heads to the list
375 # Check if branches bring a few more heads to the list
375 if branches:
376 if branches:
376 rpath = self.url.strip('/')
377 rpath = self.url.strip('/')
377 branchnames = svn.client.ls(rpath + '/' + quote(branches),
378 branchnames = svn.client.ls(rpath + '/' + quote(branches),
378 rev, False, self.ctx)
379 rev, False, self.ctx)
379 for branch in branchnames.keys():
380 for branch in branchnames.keys():
380 module = '%s/%s/%s' % (oldmodule, branches, branch)
381 module = '%s/%s/%s' % (oldmodule, branches, branch)
381 if not isdir(module, self.last_changed):
382 if not isdir(module, self.last_changed):
382 continue
383 continue
383 brevid = self.latest(module, self.last_changed)
384 brevid = self.latest(module, self.last_changed)
384 if not brevid:
385 if not brevid:
385 self.ui.note(_('ignoring empty branch %s\n') % branch)
386 self.ui.note(_('ignoring empty branch %s\n') % branch)
386 continue
387 continue
387 self.ui.note(_('found branch %s at %d\n') %
388 self.ui.note(_('found branch %s at %d\n') %
388 (branch, self.revnum(brevid)))
389 (branch, self.revnum(brevid)))
389 self.heads.append(brevid)
390 self.heads.append(brevid)
390
391
391 if self.startrev and self.heads:
392 if self.startrev and self.heads:
392 if len(self.heads) > 1:
393 if len(self.heads) > 1:
393 raise util.Abort(_('svn: start revision is not supported '
394 raise util.Abort(_('svn: start revision is not supported '
394 'with more than one branch'))
395 'with more than one branch'))
395 revnum = self.revnum(self.heads[0])
396 revnum = self.revnum(self.heads[0])
396 if revnum < self.startrev:
397 if revnum < self.startrev:
397 raise util.Abort(
398 raise util.Abort(
398 _('svn: no revision found after start revision %d')
399 _('svn: no revision found after start revision %d')
399 % self.startrev)
400 % self.startrev)
400
401
401 return self.heads
402 return self.heads
402
403
403 def getchanges(self, rev):
404 def getchanges(self, rev):
404 if self._changescache and self._changescache[0] == rev:
405 if self._changescache and self._changescache[0] == rev:
405 return self._changescache[1]
406 return self._changescache[1]
406 self._changescache = None
407 self._changescache = None
407 (paths, parents) = self.paths[rev]
408 (paths, parents) = self.paths[rev]
408 if parents:
409 if parents:
409 files, self.removed, copies = self.expandpaths(rev, paths, parents)
410 files, self.removed, copies = self.expandpaths(rev, paths, parents)
410 else:
411 else:
411 # Perform a full checkout on roots
412 # Perform a full checkout on roots
412 uuid, module, revnum = revsplit(rev)
413 uuid, module, revnum = revsplit(rev)
413 entries = svn.client.ls(self.baseurl + quote(module),
414 entries = svn.client.ls(self.baseurl + quote(module),
414 optrev(revnum), True, self.ctx)
415 optrev(revnum), True, self.ctx)
415 files = [n for n, e in entries.iteritems()
416 files = [n for n, e in entries.iteritems()
416 if e.kind == svn.core.svn_node_file]
417 if e.kind == svn.core.svn_node_file]
417 copies = {}
418 copies = {}
418 self.removed = set()
419 self.removed = set()
419
420
420 files.sort()
421 files.sort()
421 files = zip(files, [rev] * len(files))
422 files = zip(files, [rev] * len(files))
422
423
423 # caller caches the result, so free it here to release memory
424 # caller caches the result, so free it here to release memory
424 del self.paths[rev]
425 del self.paths[rev]
425 return (files, copies)
426 return (files, copies)
426
427
427 def getchangedfiles(self, rev, i):
428 def getchangedfiles(self, rev, i):
428 changes = self.getchanges(rev)
429 changes = self.getchanges(rev)
429 self._changescache = (rev, changes)
430 self._changescache = (rev, changes)
430 return [f[0] for f in changes[0]]
431 return [f[0] for f in changes[0]]
431
432
432 def getcommit(self, rev):
433 def getcommit(self, rev):
433 if rev not in self.commits:
434 if rev not in self.commits:
434 uuid, module, revnum = revsplit(rev)
435 uuid, module, revnum = revsplit(rev)
435 self.module = module
436 self.module = module
436 self.reparent(module)
437 self.reparent(module)
437 # We assume that:
438 # We assume that:
438 # - requests for revisions after "stop" come from the
439 # - requests for revisions after "stop" come from the
439 # revision graph backward traversal. Cache all of them
440 # revision graph backward traversal. Cache all of them
440 # down to stop, they will be used eventually.
441 # down to stop, they will be used eventually.
441 # - requests for revisions before "stop" come to get
442 # - requests for revisions before "stop" come to get
442 # isolated branches parents. Just fetch what is needed.
443 # isolated branches parents. Just fetch what is needed.
443 stop = self.lastrevs.get(module, 0)
444 stop = self.lastrevs.get(module, 0)
444 if revnum < stop:
445 if revnum < stop:
445 stop = revnum + 1
446 stop = revnum + 1
446 self._fetch_revisions(revnum, stop)
447 self._fetch_revisions(revnum, stop)
447 if rev not in self.commits:
448 if rev not in self.commits:
448 raise util.Abort(_('svn: revision %s not found') % revnum)
449 raise util.Abort(_('svn: revision %s not found') % revnum)
449 commit = self.commits[rev]
450 commit = self.commits[rev]
450 # caller caches the result, so free it here to release memory
451 # caller caches the result, so free it here to release memory
451 del self.commits[rev]
452 del self.commits[rev]
452 return commit
453 return commit
453
454
454 def gettags(self):
455 def gettags(self):
455 tags = {}
456 tags = {}
456 if self.tags is None:
457 if self.tags is None:
457 return tags
458 return tags
458
459
459 # svn tags are just a convention, project branches left in a
460 # svn tags are just a convention, project branches left in a
460 # 'tags' directory. There is no other relationship than
461 # 'tags' directory. There is no other relationship than
461 # ancestry, which is expensive to discover and makes them hard
462 # ancestry, which is expensive to discover and makes them hard
462 # to update incrementally. Worse, past revisions may be
463 # to update incrementally. Worse, past revisions may be
463 # referenced by tags far away in the future, requiring a deep
464 # referenced by tags far away in the future, requiring a deep
464 # history traversal on every calculation. Current code
465 # history traversal on every calculation. Current code
465 # performs a single backward traversal, tracking moves within
466 # performs a single backward traversal, tracking moves within
466 # the tags directory (tag renaming) and recording a new tag
467 # the tags directory (tag renaming) and recording a new tag
467 # everytime a project is copied from outside the tags
468 # everytime a project is copied from outside the tags
468 # directory. It also lists deleted tags, this behaviour may
469 # directory. It also lists deleted tags, this behaviour may
469 # change in the future.
470 # change in the future.
470 pendings = []
471 pendings = []
471 tagspath = self.tags
472 tagspath = self.tags
472 start = svn.ra.get_latest_revnum(self.ra)
473 start = svn.ra.get_latest_revnum(self.ra)
473 stream = self._getlog([self.tags], start, self.startrev)
474 stream = self._getlog([self.tags], start, self.startrev)
474 try:
475 try:
475 for entry in stream:
476 for entry in stream:
476 origpaths, revnum, author, date, message = entry
477 origpaths, revnum, author, date, message = entry
477 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
478 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
478 in origpaths.iteritems() if e.copyfrom_path]
479 in origpaths.iteritems() if e.copyfrom_path]
479 # Apply moves/copies from more specific to general
480 # Apply moves/copies from more specific to general
480 copies.sort(reverse=True)
481 copies.sort(reverse=True)
481
482
482 srctagspath = tagspath
483 srctagspath = tagspath
483 if copies and copies[-1][2] == tagspath:
484 if copies and copies[-1][2] == tagspath:
484 # Track tags directory moves
485 # Track tags directory moves
485 srctagspath = copies.pop()[0]
486 srctagspath = copies.pop()[0]
486
487
487 for source, sourcerev, dest in copies:
488 for source, sourcerev, dest in copies:
488 if not dest.startswith(tagspath + '/'):
489 if not dest.startswith(tagspath + '/'):
489 continue
490 continue
490 for tag in pendings:
491 for tag in pendings:
491 if tag[0].startswith(dest):
492 if tag[0].startswith(dest):
492 tagpath = source + tag[0][len(dest):]
493 tagpath = source + tag[0][len(dest):]
493 tag[:2] = [tagpath, sourcerev]
494 tag[:2] = [tagpath, sourcerev]
494 break
495 break
495 else:
496 else:
496 pendings.append([source, sourcerev, dest])
497 pendings.append([source, sourcerev, dest])
497
498
498 # Filter out tags with children coming from different
499 # Filter out tags with children coming from different
499 # parts of the repository like:
500 # parts of the repository like:
500 # /tags/tag.1 (from /trunk:10)
501 # /tags/tag.1 (from /trunk:10)
501 # /tags/tag.1/foo (from /branches/foo:12)
502 # /tags/tag.1/foo (from /branches/foo:12)
502 # Here/tags/tag.1 discarded as well as its children.
503 # Here/tags/tag.1 discarded as well as its children.
503 # It happens with tools like cvs2svn. Such tags cannot
504 # It happens with tools like cvs2svn. Such tags cannot
504 # be represented in mercurial.
505 # be represented in mercurial.
505 addeds = dict((p, e.copyfrom_path) for p, e
506 addeds = dict((p, e.copyfrom_path) for p, e
506 in origpaths.iteritems()
507 in origpaths.iteritems()
507 if e.action == 'A' and e.copyfrom_path)
508 if e.action == 'A' and e.copyfrom_path)
508 badroots = set()
509 badroots = set()
509 for destroot in addeds:
510 for destroot in addeds:
510 for source, sourcerev, dest in pendings:
511 for source, sourcerev, dest in pendings:
511 if (not dest.startswith(destroot + '/')
512 if (not dest.startswith(destroot + '/')
512 or source.startswith(addeds[destroot] + '/')):
513 or source.startswith(addeds[destroot] + '/')):
513 continue
514 continue
514 badroots.add(destroot)
515 badroots.add(destroot)
515 break
516 break
516
517
517 for badroot in badroots:
518 for badroot in badroots:
518 pendings = [p for p in pendings if p[2] != badroot
519 pendings = [p for p in pendings if p[2] != badroot
519 and not p[2].startswith(badroot + '/')]
520 and not p[2].startswith(badroot + '/')]
520
521
521 # Tell tag renamings from tag creations
522 # Tell tag renamings from tag creations
522 renamings = []
523 renamings = []
523 for source, sourcerev, dest in pendings:
524 for source, sourcerev, dest in pendings:
524 tagname = dest.split('/')[-1]
525 tagname = dest.split('/')[-1]
525 if source.startswith(srctagspath):
526 if source.startswith(srctagspath):
526 renamings.append([source, sourcerev, tagname])
527 renamings.append([source, sourcerev, tagname])
527 continue
528 continue
528 if tagname in tags:
529 if tagname in tags:
529 # Keep the latest tag value
530 # Keep the latest tag value
530 continue
531 continue
531 # From revision may be fake, get one with changes
532 # From revision may be fake, get one with changes
532 try:
533 try:
533 tagid = self.latest(source, sourcerev)
534 tagid = self.latest(source, sourcerev)
534 if tagid and tagname not in tags:
535 if tagid and tagname not in tags:
535 tags[tagname] = tagid
536 tags[tagname] = tagid
536 except SvnPathNotFound:
537 except SvnPathNotFound:
537 # It happens when we are following directories
538 # It happens when we are following directories
538 # we assumed were copied with their parents
539 # we assumed were copied with their parents
539 # but were really created in the tag
540 # but were really created in the tag
540 # directory.
541 # directory.
541 pass
542 pass
542 pendings = renamings
543 pendings = renamings
543 tagspath = srctagspath
544 tagspath = srctagspath
544 finally:
545 finally:
545 stream.close()
546 stream.close()
546 return tags
547 return tags
547
548
548 def converted(self, rev, destrev):
549 def converted(self, rev, destrev):
549 if not self.wc:
550 if not self.wc:
550 return
551 return
551 if self.convertfp is None:
552 if self.convertfp is None:
552 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
553 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
553 'a')
554 'a')
554 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
555 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
555 self.convertfp.flush()
556 self.convertfp.flush()
556
557
557 def revid(self, revnum, module=None):
558 def revid(self, revnum, module=None):
558 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
559 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
559
560
560 def revnum(self, rev):
561 def revnum(self, rev):
561 return int(rev.split('@')[-1])
562 return int(rev.split('@')[-1])
562
563
563 def latest(self, path, stop=None):
564 def latest(self, path, stop=None):
564 """Find the latest revid affecting path, up to stop revision
565 """Find the latest revid affecting path, up to stop revision
565 number. If stop is None, default to repository latest
566 number. If stop is None, default to repository latest
566 revision. It may return a revision in a different module,
567 revision. It may return a revision in a different module,
567 since a branch may be moved without a change being
568 since a branch may be moved without a change being
568 reported. Return None if computed module does not belong to
569 reported. Return None if computed module does not belong to
569 rootmodule subtree.
570 rootmodule subtree.
570 """
571 """
571 def findchanges(path, start, stop=None):
572 def findchanges(path, start, stop=None):
572 stream = self._getlog([path], start, stop or 1)
573 stream = self._getlog([path], start, stop or 1)
573 try:
574 try:
574 for entry in stream:
575 for entry in stream:
575 paths, revnum, author, date, message = entry
576 paths, revnum, author, date, message = entry
576 if stop is None and paths:
577 if stop is None and paths:
577 # We do not know the latest changed revision,
578 # We do not know the latest changed revision,
578 # keep the first one with changed paths.
579 # keep the first one with changed paths.
579 break
580 break
580 if revnum <= stop:
581 if revnum <= stop:
581 break
582 break
582
583
583 for p in paths:
584 for p in paths:
584 if (not path.startswith(p) or
585 if (not path.startswith(p) or
585 not paths[p].copyfrom_path):
586 not paths[p].copyfrom_path):
586 continue
587 continue
587 newpath = paths[p].copyfrom_path + path[len(p):]
588 newpath = paths[p].copyfrom_path + path[len(p):]
588 self.ui.debug("branch renamed from %s to %s at %d\n" %
589 self.ui.debug("branch renamed from %s to %s at %d\n" %
589 (path, newpath, revnum))
590 (path, newpath, revnum))
590 path = newpath
591 path = newpath
591 break
592 break
592 if not paths:
593 if not paths:
593 revnum = None
594 revnum = None
594 return revnum, path
595 return revnum, path
595 finally:
596 finally:
596 stream.close()
597 stream.close()
597
598
598 if not path.startswith(self.rootmodule):
599 if not path.startswith(self.rootmodule):
599 # Requests on foreign branches may be forbidden at server level
600 # Requests on foreign branches may be forbidden at server level
600 self.ui.debug('ignoring foreign branch %r\n' % path)
601 self.ui.debug('ignoring foreign branch %r\n' % path)
601 return None
602 return None
602
603
603 if stop is None:
604 if stop is None:
604 stop = svn.ra.get_latest_revnum(self.ra)
605 stop = svn.ra.get_latest_revnum(self.ra)
605 try:
606 try:
606 prevmodule = self.reparent('')
607 prevmodule = self.reparent('')
607 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
608 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
608 self.reparent(prevmodule)
609 self.reparent(prevmodule)
609 except SubversionException:
610 except SubversionException:
610 dirent = None
611 dirent = None
611 if not dirent:
612 if not dirent:
612 raise SvnPathNotFound(_('%s not found up to revision %d')
613 raise SvnPathNotFound(_('%s not found up to revision %d')
613 % (path, stop))
614 % (path, stop))
614
615
615 # stat() gives us the previous revision on this line of
616 # stat() gives us the previous revision on this line of
616 # development, but it might be in *another module*. Fetch the
617 # development, but it might be in *another module*. Fetch the
617 # log and detect renames down to the latest revision.
618 # log and detect renames down to the latest revision.
618 revnum, realpath = findchanges(path, stop, dirent.created_rev)
619 revnum, realpath = findchanges(path, stop, dirent.created_rev)
619 if revnum is None:
620 if revnum is None:
620 # Tools like svnsync can create empty revision, when
621 # Tools like svnsync can create empty revision, when
621 # synchronizing only a subtree for instance. These empty
622 # synchronizing only a subtree for instance. These empty
622 # revisions created_rev still have their original values
623 # revisions created_rev still have their original values
623 # despite all changes having disappeared and can be
624 # despite all changes having disappeared and can be
624 # returned by ra.stat(), at least when stating the root
625 # returned by ra.stat(), at least when stating the root
625 # module. In that case, do not trust created_rev and scan
626 # module. In that case, do not trust created_rev and scan
626 # the whole history.
627 # the whole history.
627 revnum, realpath = findchanges(path, stop)
628 revnum, realpath = findchanges(path, stop)
628 if revnum is None:
629 if revnum is None:
629 self.ui.debug('ignoring empty branch %r\n' % realpath)
630 self.ui.debug('ignoring empty branch %r\n' % realpath)
630 return None
631 return None
631
632
632 if not realpath.startswith(self.rootmodule):
633 if not realpath.startswith(self.rootmodule):
633 self.ui.debug('ignoring foreign branch %r\n' % realpath)
634 self.ui.debug('ignoring foreign branch %r\n' % realpath)
634 return None
635 return None
635 return self.revid(revnum, realpath)
636 return self.revid(revnum, realpath)
636
637
637 def reparent(self, module):
638 def reparent(self, module):
638 """Reparent the svn transport and return the previous parent."""
639 """Reparent the svn transport and return the previous parent."""
639 if self.prevmodule == module:
640 if self.prevmodule == module:
640 return module
641 return module
641 svnurl = self.baseurl + quote(module)
642 svnurl = self.baseurl + quote(module)
642 prevmodule = self.prevmodule
643 prevmodule = self.prevmodule
643 if prevmodule is None:
644 if prevmodule is None:
644 prevmodule = ''
645 prevmodule = ''
645 self.ui.debug("reparent to %s\n" % svnurl)
646 self.ui.debug("reparent to %s\n" % svnurl)
646 svn.ra.reparent(self.ra, svnurl)
647 svn.ra.reparent(self.ra, svnurl)
647 self.prevmodule = module
648 self.prevmodule = module
648 return prevmodule
649 return prevmodule
649
650
650 def expandpaths(self, rev, paths, parents):
651 def expandpaths(self, rev, paths, parents):
651 changed, removed = set(), set()
652 changed, removed = set(), set()
652 copies = {}
653 copies = {}
653
654
654 new_module, revnum = revsplit(rev)[1:]
655 new_module, revnum = revsplit(rev)[1:]
655 if new_module != self.module:
656 if new_module != self.module:
656 self.module = new_module
657 self.module = new_module
657 self.reparent(self.module)
658 self.reparent(self.module)
658
659
659 for i, (path, ent) in enumerate(paths):
660 for i, (path, ent) in enumerate(paths):
660 self.ui.progress(_('scanning paths'), i, item=path,
661 self.ui.progress(_('scanning paths'), i, item=path,
661 total=len(paths))
662 total=len(paths))
662 entrypath = self.getrelpath(path)
663 entrypath = self.getrelpath(path)
663
664
664 kind = self._checkpath(entrypath, revnum)
665 kind = self._checkpath(entrypath, revnum)
665 if kind == svn.core.svn_node_file:
666 if kind == svn.core.svn_node_file:
666 changed.add(self.recode(entrypath))
667 changed.add(self.recode(entrypath))
667 if not ent.copyfrom_path or not parents:
668 if not ent.copyfrom_path or not parents:
668 continue
669 continue
669 # Copy sources not in parent revisions cannot be
670 # Copy sources not in parent revisions cannot be
670 # represented, ignore their origin for now
671 # represented, ignore their origin for now
671 pmodule, prevnum = revsplit(parents[0])[1:]
672 pmodule, prevnum = revsplit(parents[0])[1:]
672 if ent.copyfrom_rev < prevnum:
673 if ent.copyfrom_rev < prevnum:
673 continue
674 continue
674 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
675 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
675 if not copyfrom_path:
676 if not copyfrom_path:
676 continue
677 continue
677 self.ui.debug("copied to %s from %s@%s\n" %
678 self.ui.debug("copied to %s from %s@%s\n" %
678 (entrypath, copyfrom_path, ent.copyfrom_rev))
679 (entrypath, copyfrom_path, ent.copyfrom_rev))
679 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
680 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
680 elif kind == 0: # gone, but had better be a deleted *file*
681 elif kind == 0: # gone, but had better be a deleted *file*
681 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
682 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
682 pmodule, prevnum = revsplit(parents[0])[1:]
683 pmodule, prevnum = revsplit(parents[0])[1:]
683 parentpath = pmodule + "/" + entrypath
684 parentpath = pmodule + "/" + entrypath
684 fromkind = self._checkpath(entrypath, prevnum, pmodule)
685 fromkind = self._checkpath(entrypath, prevnum, pmodule)
685
686
686 if fromkind == svn.core.svn_node_file:
687 if fromkind == svn.core.svn_node_file:
687 removed.add(self.recode(entrypath))
688 removed.add(self.recode(entrypath))
688 elif fromkind == svn.core.svn_node_dir:
689 elif fromkind == svn.core.svn_node_dir:
689 oroot = parentpath.strip('/')
690 oroot = parentpath.strip('/')
690 nroot = path.strip('/')
691 nroot = path.strip('/')
691 children = self._iterfiles(oroot, prevnum)
692 children = self._iterfiles(oroot, prevnum)
692 for childpath in children:
693 for childpath in children:
693 childpath = childpath.replace(oroot, nroot)
694 childpath = childpath.replace(oroot, nroot)
694 childpath = self.getrelpath("/" + childpath, pmodule)
695 childpath = self.getrelpath("/" + childpath, pmodule)
695 if childpath:
696 if childpath:
696 removed.add(self.recode(childpath))
697 removed.add(self.recode(childpath))
697 else:
698 else:
698 self.ui.debug('unknown path in revision %d: %s\n' % \
699 self.ui.debug('unknown path in revision %d: %s\n' % \
699 (revnum, path))
700 (revnum, path))
700 elif kind == svn.core.svn_node_dir:
701 elif kind == svn.core.svn_node_dir:
701 if ent.action == 'M':
702 if ent.action == 'M':
702 # If the directory just had a prop change,
703 # If the directory just had a prop change,
703 # then we shouldn't need to look for its children.
704 # then we shouldn't need to look for its children.
704 continue
705 continue
705 if ent.action == 'R' and parents:
706 if ent.action == 'R' and parents:
706 # If a directory is replacing a file, mark the previous
707 # If a directory is replacing a file, mark the previous
707 # file as deleted
708 # file as deleted
708 pmodule, prevnum = revsplit(parents[0])[1:]
709 pmodule, prevnum = revsplit(parents[0])[1:]
709 pkind = self._checkpath(entrypath, prevnum, pmodule)
710 pkind = self._checkpath(entrypath, prevnum, pmodule)
710 if pkind == svn.core.svn_node_file:
711 if pkind == svn.core.svn_node_file:
711 removed.add(self.recode(entrypath))
712 removed.add(self.recode(entrypath))
712 elif pkind == svn.core.svn_node_dir:
713 elif pkind == svn.core.svn_node_dir:
713 # We do not know what files were kept or removed,
714 # We do not know what files were kept or removed,
714 # mark them all as changed.
715 # mark them all as changed.
715 for childpath in self._iterfiles(pmodule, prevnum):
716 for childpath in self._iterfiles(pmodule, prevnum):
716 childpath = self.getrelpath("/" + childpath)
717 childpath = self.getrelpath("/" + childpath)
717 if childpath:
718 if childpath:
718 changed.add(self.recode(childpath))
719 changed.add(self.recode(childpath))
719
720
720 for childpath in self._iterfiles(path, revnum):
721 for childpath in self._iterfiles(path, revnum):
721 childpath = self.getrelpath("/" + childpath)
722 childpath = self.getrelpath("/" + childpath)
722 if childpath:
723 if childpath:
723 changed.add(self.recode(childpath))
724 changed.add(self.recode(childpath))
724
725
725 # Handle directory copies
726 # Handle directory copies
726 if not ent.copyfrom_path or not parents:
727 if not ent.copyfrom_path or not parents:
727 continue
728 continue
728 # Copy sources not in parent revisions cannot be
729 # Copy sources not in parent revisions cannot be
729 # represented, ignore their origin for now
730 # represented, ignore their origin for now
730 pmodule, prevnum = revsplit(parents[0])[1:]
731 pmodule, prevnum = revsplit(parents[0])[1:]
731 if ent.copyfrom_rev < prevnum:
732 if ent.copyfrom_rev < prevnum:
732 continue
733 continue
733 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
734 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
734 if not copyfrompath:
735 if not copyfrompath:
735 continue
736 continue
736 self.ui.debug("mark %s came from %s:%d\n"
737 self.ui.debug("mark %s came from %s:%d\n"
737 % (path, copyfrompath, ent.copyfrom_rev))
738 % (path, copyfrompath, ent.copyfrom_rev))
738 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
739 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
739 for childpath in children:
740 for childpath in children:
740 childpath = self.getrelpath("/" + childpath, pmodule)
741 childpath = self.getrelpath("/" + childpath, pmodule)
741 if not childpath:
742 if not childpath:
742 continue
743 continue
743 copytopath = path + childpath[len(copyfrompath):]
744 copytopath = path + childpath[len(copyfrompath):]
744 copytopath = self.getrelpath(copytopath)
745 copytopath = self.getrelpath(copytopath)
745 copies[self.recode(copytopath)] = self.recode(childpath)
746 copies[self.recode(copytopath)] = self.recode(childpath)
746
747
747 self.ui.progress(_('scanning paths'), None)
748 self.ui.progress(_('scanning paths'), None)
748 changed.update(removed)
749 changed.update(removed)
749 return (list(changed), removed, copies)
750 return (list(changed), removed, copies)
750
751
751 def _fetch_revisions(self, from_revnum, to_revnum):
752 def _fetch_revisions(self, from_revnum, to_revnum):
752 if from_revnum < to_revnum:
753 if from_revnum < to_revnum:
753 from_revnum, to_revnum = to_revnum, from_revnum
754 from_revnum, to_revnum = to_revnum, from_revnum
754
755
755 self.child_cset = None
756 self.child_cset = None
756
757
757 def parselogentry(orig_paths, revnum, author, date, message):
758 def parselogentry(orig_paths, revnum, author, date, message):
758 """Return the parsed commit object or None, and True if
759 """Return the parsed commit object or None, and True if
759 the revision is a branch root.
760 the revision is a branch root.
760 """
761 """
761 self.ui.debug("parsing revision %d (%d changes)\n" %
762 self.ui.debug("parsing revision %d (%d changes)\n" %
762 (revnum, len(orig_paths)))
763 (revnum, len(orig_paths)))
763
764
764 branched = False
765 branched = False
765 rev = self.revid(revnum)
766 rev = self.revid(revnum)
766 # branch log might return entries for a parent we already have
767 # branch log might return entries for a parent we already have
767
768
768 if rev in self.commits or revnum < to_revnum:
769 if rev in self.commits or revnum < to_revnum:
769 return None, branched
770 return None, branched
770
771
771 parents = []
772 parents = []
772 # check whether this revision is the start of a branch or part
773 # check whether this revision is the start of a branch or part
773 # of a branch renaming
774 # of a branch renaming
774 orig_paths = sorted(orig_paths.iteritems())
775 orig_paths = sorted(orig_paths.iteritems())
775 root_paths = [(p, e) for p, e in orig_paths
776 root_paths = [(p, e) for p, e in orig_paths
776 if self.module.startswith(p)]
777 if self.module.startswith(p)]
777 if root_paths:
778 if root_paths:
778 path, ent = root_paths[-1]
779 path, ent = root_paths[-1]
779 if ent.copyfrom_path:
780 if ent.copyfrom_path:
780 branched = True
781 branched = True
781 newpath = ent.copyfrom_path + self.module[len(path):]
782 newpath = ent.copyfrom_path + self.module[len(path):]
782 # ent.copyfrom_rev may not be the actual last revision
783 # ent.copyfrom_rev may not be the actual last revision
783 previd = self.latest(newpath, ent.copyfrom_rev)
784 previd = self.latest(newpath, ent.copyfrom_rev)
784 if previd is not None:
785 if previd is not None:
785 prevmodule, prevnum = revsplit(previd)[1:]
786 prevmodule, prevnum = revsplit(previd)[1:]
786 if prevnum >= self.startrev:
787 if prevnum >= self.startrev:
787 parents = [previd]
788 parents = [previd]
788 self.ui.note(
789 self.ui.note(
789 _('found parent of branch %s at %d: %s\n') %
790 _('found parent of branch %s at %d: %s\n') %
790 (self.module, prevnum, prevmodule))
791 (self.module, prevnum, prevmodule))
791 else:
792 else:
792 self.ui.debug("no copyfrom path, don't know what to do.\n")
793 self.ui.debug("no copyfrom path, don't know what to do.\n")
793
794
794 paths = []
795 paths = []
795 # filter out unrelated paths
796 # filter out unrelated paths
796 for path, ent in orig_paths:
797 for path, ent in orig_paths:
797 if self.getrelpath(path) is None:
798 if self.getrelpath(path) is None:
798 continue
799 continue
799 paths.append((path, ent))
800 paths.append((path, ent))
800
801
801 # Example SVN datetime. Includes microseconds.
802 # Example SVN datetime. Includes microseconds.
802 # ISO-8601 conformant
803 # ISO-8601 conformant
803 # '2007-01-04T17:35:00.902377Z'
804 # '2007-01-04T17:35:00.902377Z'
804 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
805 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
806 if self.ui.configbool('convert', 'localtimezone'):
807 date = makedatetimestamp(date[0])
805
808
806 log = message and self.recode(message) or ''
809 log = message and self.recode(message) or ''
807 author = author and self.recode(author) or ''
810 author = author and self.recode(author) or ''
808 try:
811 try:
809 branch = self.module.split("/")[-1]
812 branch = self.module.split("/")[-1]
810 if branch == self.trunkname:
813 if branch == self.trunkname:
811 branch = None
814 branch = None
812 except IndexError:
815 except IndexError:
813 branch = None
816 branch = None
814
817
815 cset = commit(author=author,
818 cset = commit(author=author,
816 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
819 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
817 desc=log,
820 desc=log,
818 parents=parents,
821 parents=parents,
819 branch=branch,
822 branch=branch,
820 rev=rev)
823 rev=rev)
821
824
822 self.commits[rev] = cset
825 self.commits[rev] = cset
823 # The parents list is *shared* among self.paths and the
826 # The parents list is *shared* among self.paths and the
824 # commit object. Both will be updated below.
827 # commit object. Both will be updated below.
825 self.paths[rev] = (paths, cset.parents)
828 self.paths[rev] = (paths, cset.parents)
826 if self.child_cset and not self.child_cset.parents:
829 if self.child_cset and not self.child_cset.parents:
827 self.child_cset.parents[:] = [rev]
830 self.child_cset.parents[:] = [rev]
828 self.child_cset = cset
831 self.child_cset = cset
829 return cset, branched
832 return cset, branched
830
833
831 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
834 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
832 (self.module, from_revnum, to_revnum))
835 (self.module, from_revnum, to_revnum))
833
836
834 try:
837 try:
835 firstcset = None
838 firstcset = None
836 lastonbranch = False
839 lastonbranch = False
837 stream = self._getlog([self.module], from_revnum, to_revnum)
840 stream = self._getlog([self.module], from_revnum, to_revnum)
838 try:
841 try:
839 for entry in stream:
842 for entry in stream:
840 paths, revnum, author, date, message = entry
843 paths, revnum, author, date, message = entry
841 if revnum < self.startrev:
844 if revnum < self.startrev:
842 lastonbranch = True
845 lastonbranch = True
843 break
846 break
844 if not paths:
847 if not paths:
845 self.ui.debug('revision %d has no entries\n' % revnum)
848 self.ui.debug('revision %d has no entries\n' % revnum)
846 # If we ever leave the loop on an empty
849 # If we ever leave the loop on an empty
847 # revision, do not try to get a parent branch
850 # revision, do not try to get a parent branch
848 lastonbranch = lastonbranch or revnum == 0
851 lastonbranch = lastonbranch or revnum == 0
849 continue
852 continue
850 cset, lastonbranch = parselogentry(paths, revnum, author,
853 cset, lastonbranch = parselogentry(paths, revnum, author,
851 date, message)
854 date, message)
852 if cset:
855 if cset:
853 firstcset = cset
856 firstcset = cset
854 if lastonbranch:
857 if lastonbranch:
855 break
858 break
856 finally:
859 finally:
857 stream.close()
860 stream.close()
858
861
859 if not lastonbranch and firstcset and not firstcset.parents:
862 if not lastonbranch and firstcset and not firstcset.parents:
860 # The first revision of the sequence (the last fetched one)
863 # The first revision of the sequence (the last fetched one)
861 # has invalid parents if not a branch root. Find the parent
864 # has invalid parents if not a branch root. Find the parent
862 # revision now, if any.
865 # revision now, if any.
863 try:
866 try:
864 firstrevnum = self.revnum(firstcset.rev)
867 firstrevnum = self.revnum(firstcset.rev)
865 if firstrevnum > 1:
868 if firstrevnum > 1:
866 latest = self.latest(self.module, firstrevnum - 1)
869 latest = self.latest(self.module, firstrevnum - 1)
867 if latest:
870 if latest:
868 firstcset.parents.append(latest)
871 firstcset.parents.append(latest)
869 except SvnPathNotFound:
872 except SvnPathNotFound:
870 pass
873 pass
871 except SubversionException, (inst, num):
874 except SubversionException, (inst, num):
872 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
875 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
873 raise util.Abort(_('svn: branch has no revision %s')
876 raise util.Abort(_('svn: branch has no revision %s')
874 % to_revnum)
877 % to_revnum)
875 raise
878 raise
876
879
877 def getfile(self, file, rev):
880 def getfile(self, file, rev):
878 # TODO: ra.get_file transmits the whole file instead of diffs.
881 # TODO: ra.get_file transmits the whole file instead of diffs.
879 if file in self.removed:
882 if file in self.removed:
880 raise IOError
883 raise IOError
881 mode = ''
884 mode = ''
882 try:
885 try:
883 new_module, revnum = revsplit(rev)[1:]
886 new_module, revnum = revsplit(rev)[1:]
884 if self.module != new_module:
887 if self.module != new_module:
885 self.module = new_module
888 self.module = new_module
886 self.reparent(self.module)
889 self.reparent(self.module)
887 io = StringIO()
890 io = StringIO()
888 info = svn.ra.get_file(self.ra, file, revnum, io)
891 info = svn.ra.get_file(self.ra, file, revnum, io)
889 data = io.getvalue()
892 data = io.getvalue()
890 # ra.get_file() seems to keep a reference on the input buffer
893 # ra.get_file() seems to keep a reference on the input buffer
891 # preventing collection. Release it explicitly.
894 # preventing collection. Release it explicitly.
892 io.close()
895 io.close()
893 if isinstance(info, list):
896 if isinstance(info, list):
894 info = info[-1]
897 info = info[-1]
895 mode = ("svn:executable" in info) and 'x' or ''
898 mode = ("svn:executable" in info) and 'x' or ''
896 mode = ("svn:special" in info) and 'l' or mode
899 mode = ("svn:special" in info) and 'l' or mode
897 except SubversionException, e:
900 except SubversionException, e:
898 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
901 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
899 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
902 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
900 if e.apr_err in notfound: # File not found
903 if e.apr_err in notfound: # File not found
901 raise IOError
904 raise IOError
902 raise
905 raise
903 if mode == 'l':
906 if mode == 'l':
904 link_prefix = "link "
907 link_prefix = "link "
905 if data.startswith(link_prefix):
908 if data.startswith(link_prefix):
906 data = data[len(link_prefix):]
909 data = data[len(link_prefix):]
907 return data, mode
910 return data, mode
908
911
909 def _iterfiles(self, path, revnum):
912 def _iterfiles(self, path, revnum):
910 """Enumerate all files in path at revnum, recursively."""
913 """Enumerate all files in path at revnum, recursively."""
911 path = path.strip('/')
914 path = path.strip('/')
912 pool = Pool()
915 pool = Pool()
913 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
916 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
914 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
917 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
915 if path:
918 if path:
916 path += '/'
919 path += '/'
917 return ((path + p) for p, e in entries.iteritems()
920 return ((path + p) for p, e in entries.iteritems()
918 if e.kind == svn.core.svn_node_file)
921 if e.kind == svn.core.svn_node_file)
919
922
920 def getrelpath(self, path, module=None):
923 def getrelpath(self, path, module=None):
921 if module is None:
924 if module is None:
922 module = self.module
925 module = self.module
923 # Given the repository url of this wc, say
926 # Given the repository url of this wc, say
924 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
927 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
925 # extract the "entry" portion (a relative path) from what
928 # extract the "entry" portion (a relative path) from what
926 # svn log --xml says, i.e.
929 # svn log --xml says, i.e.
927 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
930 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
928 # that is to say "tests/PloneTestCase.py"
931 # that is to say "tests/PloneTestCase.py"
929 if path.startswith(module):
932 if path.startswith(module):
930 relative = path.rstrip('/')[len(module):]
933 relative = path.rstrip('/')[len(module):]
931 if relative.startswith('/'):
934 if relative.startswith('/'):
932 return relative[1:]
935 return relative[1:]
933 elif relative == '':
936 elif relative == '':
934 return relative
937 return relative
935
938
936 # The path is outside our tracked tree...
939 # The path is outside our tracked tree...
937 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
940 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
938 return None
941 return None
939
942
940 def _checkpath(self, path, revnum, module=None):
943 def _checkpath(self, path, revnum, module=None):
941 if module is not None:
944 if module is not None:
942 prevmodule = self.reparent('')
945 prevmodule = self.reparent('')
943 path = module + '/' + path
946 path = module + '/' + path
944 try:
947 try:
945 # ra.check_path does not like leading slashes very much, it leads
948 # ra.check_path does not like leading slashes very much, it leads
946 # to PROPFIND subversion errors
949 # to PROPFIND subversion errors
947 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
950 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
948 finally:
951 finally:
949 if module is not None:
952 if module is not None:
950 self.reparent(prevmodule)
953 self.reparent(prevmodule)
951
954
952 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
955 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
953 strict_node_history=False):
956 strict_node_history=False):
954 # Normalize path names, svn >= 1.5 only wants paths relative to
957 # Normalize path names, svn >= 1.5 only wants paths relative to
955 # supplied URL
958 # supplied URL
956 relpaths = []
959 relpaths = []
957 for p in paths:
960 for p in paths:
958 if not p.startswith('/'):
961 if not p.startswith('/'):
959 p = self.module + '/' + p
962 p = self.module + '/' + p
960 relpaths.append(p.strip('/'))
963 relpaths.append(p.strip('/'))
961 args = [self.baseurl, relpaths, start, end, limit,
964 args = [self.baseurl, relpaths, start, end, limit,
962 discover_changed_paths, strict_node_history]
965 discover_changed_paths, strict_node_history]
963 arg = encodeargs(args)
966 arg = encodeargs(args)
964 hgexe = util.hgexecutable()
967 hgexe = util.hgexecutable()
965 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
968 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
966 stdin, stdout = util.popen2(util.quotecommand(cmd))
969 stdin, stdout = util.popen2(util.quotecommand(cmd))
967 stdin.write(arg)
970 stdin.write(arg)
968 try:
971 try:
969 stdin.close()
972 stdin.close()
970 except IOError:
973 except IOError:
971 raise util.Abort(_('Mercurial failed to run itself, check'
974 raise util.Abort(_('Mercurial failed to run itself, check'
972 ' hg executable is in PATH'))
975 ' hg executable is in PATH'))
973 return logstream(stdout)
976 return logstream(stdout)
974
977
975 pre_revprop_change = '''#!/bin/sh
978 pre_revprop_change = '''#!/bin/sh
976
979
977 REPOS="$1"
980 REPOS="$1"
978 REV="$2"
981 REV="$2"
979 USER="$3"
982 USER="$3"
980 PROPNAME="$4"
983 PROPNAME="$4"
981 ACTION="$5"
984 ACTION="$5"
982
985
983 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
986 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
984 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
987 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
985 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
988 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
986
989
987 echo "Changing prohibited revision property" >&2
990 echo "Changing prohibited revision property" >&2
988 exit 1
991 exit 1
989 '''
992 '''
990
993
991 class svn_sink(converter_sink, commandline):
994 class svn_sink(converter_sink, commandline):
992 commit_re = re.compile(r'Committed revision (\d+).', re.M)
995 commit_re = re.compile(r'Committed revision (\d+).', re.M)
993 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
996 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
994
997
995 def prerun(self):
998 def prerun(self):
996 if self.wc:
999 if self.wc:
997 os.chdir(self.wc)
1000 os.chdir(self.wc)
998
1001
999 def postrun(self):
1002 def postrun(self):
1000 if self.wc:
1003 if self.wc:
1001 os.chdir(self.cwd)
1004 os.chdir(self.cwd)
1002
1005
1003 def join(self, name):
1006 def join(self, name):
1004 return os.path.join(self.wc, '.svn', name)
1007 return os.path.join(self.wc, '.svn', name)
1005
1008
1006 def revmapfile(self):
1009 def revmapfile(self):
1007 return self.join('hg-shamap')
1010 return self.join('hg-shamap')
1008
1011
1009 def authorfile(self):
1012 def authorfile(self):
1010 return self.join('hg-authormap')
1013 return self.join('hg-authormap')
1011
1014
1012 def __init__(self, ui, path):
1015 def __init__(self, ui, path):
1013
1016
1014 converter_sink.__init__(self, ui, path)
1017 converter_sink.__init__(self, ui, path)
1015 commandline.__init__(self, ui, 'svn')
1018 commandline.__init__(self, ui, 'svn')
1016 self.delete = []
1019 self.delete = []
1017 self.setexec = []
1020 self.setexec = []
1018 self.delexec = []
1021 self.delexec = []
1019 self.copies = []
1022 self.copies = []
1020 self.wc = None
1023 self.wc = None
1021 self.cwd = os.getcwd()
1024 self.cwd = os.getcwd()
1022
1025
1023 created = False
1026 created = False
1024 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1027 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1025 self.wc = os.path.realpath(path)
1028 self.wc = os.path.realpath(path)
1026 self.run0('update')
1029 self.run0('update')
1027 else:
1030 else:
1028 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1031 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1029 path = os.path.realpath(path)
1032 path = os.path.realpath(path)
1030 if os.path.isdir(os.path.dirname(path)):
1033 if os.path.isdir(os.path.dirname(path)):
1031 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1034 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1032 ui.status(_('initializing svn repository %r\n') %
1035 ui.status(_('initializing svn repository %r\n') %
1033 os.path.basename(path))
1036 os.path.basename(path))
1034 commandline(ui, 'svnadmin').run0('create', path)
1037 commandline(ui, 'svnadmin').run0('create', path)
1035 created = path
1038 created = path
1036 path = util.normpath(path)
1039 path = util.normpath(path)
1037 if not path.startswith('/'):
1040 if not path.startswith('/'):
1038 path = '/' + path
1041 path = '/' + path
1039 path = 'file://' + path
1042 path = 'file://' + path
1040
1043
1041 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1044 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1042 ui.status(_('initializing svn working copy %r\n')
1045 ui.status(_('initializing svn working copy %r\n')
1043 % os.path.basename(wcpath))
1046 % os.path.basename(wcpath))
1044 self.run0('checkout', path, wcpath)
1047 self.run0('checkout', path, wcpath)
1045
1048
1046 self.wc = wcpath
1049 self.wc = wcpath
1047 self.opener = scmutil.opener(self.wc)
1050 self.opener = scmutil.opener(self.wc)
1048 self.wopener = scmutil.opener(self.wc)
1051 self.wopener = scmutil.opener(self.wc)
1049 self.childmap = mapfile(ui, self.join('hg-childmap'))
1052 self.childmap = mapfile(ui, self.join('hg-childmap'))
1050 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1053 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1051
1054
1052 if created:
1055 if created:
1053 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1056 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1054 fp = open(hook, 'w')
1057 fp = open(hook, 'w')
1055 fp.write(pre_revprop_change)
1058 fp.write(pre_revprop_change)
1056 fp.close()
1059 fp.close()
1057 util.setflags(hook, False, True)
1060 util.setflags(hook, False, True)
1058
1061
1059 output = self.run0('info')
1062 output = self.run0('info')
1060 self.uuid = self.uuid_re.search(output).group(1).strip()
1063 self.uuid = self.uuid_re.search(output).group(1).strip()
1061
1064
1062 def wjoin(self, *names):
1065 def wjoin(self, *names):
1063 return os.path.join(self.wc, *names)
1066 return os.path.join(self.wc, *names)
1064
1067
1065 @propertycache
1068 @propertycache
1066 def manifest(self):
1069 def manifest(self):
1067 # As of svn 1.7, the "add" command fails when receiving
1070 # As of svn 1.7, the "add" command fails when receiving
1068 # already tracked entries, so we have to track and filter them
1071 # already tracked entries, so we have to track and filter them
1069 # ourselves.
1072 # ourselves.
1070 m = set()
1073 m = set()
1071 output = self.run0('ls', recursive=True, xml=True)
1074 output = self.run0('ls', recursive=True, xml=True)
1072 doc = xml.dom.minidom.parseString(output)
1075 doc = xml.dom.minidom.parseString(output)
1073 for e in doc.getElementsByTagName('entry'):
1076 for e in doc.getElementsByTagName('entry'):
1074 for n in e.childNodes:
1077 for n in e.childNodes:
1075 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1078 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1076 continue
1079 continue
1077 name = ''.join(c.data for c in n.childNodes
1080 name = ''.join(c.data for c in n.childNodes
1078 if c.nodeType == c.TEXT_NODE)
1081 if c.nodeType == c.TEXT_NODE)
1079 # Entries are compared with names coming from
1082 # Entries are compared with names coming from
1080 # mercurial, so bytes with undefined encoding. Our
1083 # mercurial, so bytes with undefined encoding. Our
1081 # best bet is to assume they are in local
1084 # best bet is to assume they are in local
1082 # encoding. They will be passed to command line calls
1085 # encoding. They will be passed to command line calls
1083 # later anyway, so they better be.
1086 # later anyway, so they better be.
1084 m.add(encoding.tolocal(name.encode('utf-8')))
1087 m.add(encoding.tolocal(name.encode('utf-8')))
1085 break
1088 break
1086 return m
1089 return m
1087
1090
1088 def putfile(self, filename, flags, data):
1091 def putfile(self, filename, flags, data):
1089 if 'l' in flags:
1092 if 'l' in flags:
1090 self.wopener.symlink(data, filename)
1093 self.wopener.symlink(data, filename)
1091 else:
1094 else:
1092 try:
1095 try:
1093 if os.path.islink(self.wjoin(filename)):
1096 if os.path.islink(self.wjoin(filename)):
1094 os.unlink(filename)
1097 os.unlink(filename)
1095 except OSError:
1098 except OSError:
1096 pass
1099 pass
1097 self.wopener.write(filename, data)
1100 self.wopener.write(filename, data)
1098
1101
1099 if self.is_exec:
1102 if self.is_exec:
1100 if self.is_exec(self.wjoin(filename)):
1103 if self.is_exec(self.wjoin(filename)):
1101 if 'x' not in flags:
1104 if 'x' not in flags:
1102 self.delexec.append(filename)
1105 self.delexec.append(filename)
1103 else:
1106 else:
1104 if 'x' in flags:
1107 if 'x' in flags:
1105 self.setexec.append(filename)
1108 self.setexec.append(filename)
1106 util.setflags(self.wjoin(filename), False, 'x' in flags)
1109 util.setflags(self.wjoin(filename), False, 'x' in flags)
1107
1110
1108 def _copyfile(self, source, dest):
1111 def _copyfile(self, source, dest):
1109 # SVN's copy command pukes if the destination file exists, but
1112 # SVN's copy command pukes if the destination file exists, but
1110 # our copyfile method expects to record a copy that has
1113 # our copyfile method expects to record a copy that has
1111 # already occurred. Cross the semantic gap.
1114 # already occurred. Cross the semantic gap.
1112 wdest = self.wjoin(dest)
1115 wdest = self.wjoin(dest)
1113 exists = os.path.lexists(wdest)
1116 exists = os.path.lexists(wdest)
1114 if exists:
1117 if exists:
1115 fd, tempname = tempfile.mkstemp(
1118 fd, tempname = tempfile.mkstemp(
1116 prefix='hg-copy-', dir=os.path.dirname(wdest))
1119 prefix='hg-copy-', dir=os.path.dirname(wdest))
1117 os.close(fd)
1120 os.close(fd)
1118 os.unlink(tempname)
1121 os.unlink(tempname)
1119 os.rename(wdest, tempname)
1122 os.rename(wdest, tempname)
1120 try:
1123 try:
1121 self.run0('copy', source, dest)
1124 self.run0('copy', source, dest)
1122 finally:
1125 finally:
1123 self.manifest.add(dest)
1126 self.manifest.add(dest)
1124 if exists:
1127 if exists:
1125 try:
1128 try:
1126 os.unlink(wdest)
1129 os.unlink(wdest)
1127 except OSError:
1130 except OSError:
1128 pass
1131 pass
1129 os.rename(tempname, wdest)
1132 os.rename(tempname, wdest)
1130
1133
1131 def dirs_of(self, files):
1134 def dirs_of(self, files):
1132 dirs = set()
1135 dirs = set()
1133 for f in files:
1136 for f in files:
1134 if os.path.isdir(self.wjoin(f)):
1137 if os.path.isdir(self.wjoin(f)):
1135 dirs.add(f)
1138 dirs.add(f)
1136 for i in strutil.rfindall(f, '/'):
1139 for i in strutil.rfindall(f, '/'):
1137 dirs.add(f[:i])
1140 dirs.add(f[:i])
1138 return dirs
1141 return dirs
1139
1142
1140 def add_dirs(self, files):
1143 def add_dirs(self, files):
1141 add_dirs = [d for d in sorted(self.dirs_of(files))
1144 add_dirs = [d for d in sorted(self.dirs_of(files))
1142 if d not in self.manifest]
1145 if d not in self.manifest]
1143 if add_dirs:
1146 if add_dirs:
1144 self.manifest.update(add_dirs)
1147 self.manifest.update(add_dirs)
1145 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1148 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1146 return add_dirs
1149 return add_dirs
1147
1150
1148 def add_files(self, files):
1151 def add_files(self, files):
1149 files = [f for f in files if f not in self.manifest]
1152 files = [f for f in files if f not in self.manifest]
1150 if files:
1153 if files:
1151 self.manifest.update(files)
1154 self.manifest.update(files)
1152 self.xargs(files, 'add', quiet=True)
1155 self.xargs(files, 'add', quiet=True)
1153 return files
1156 return files
1154
1157
1155 def tidy_dirs(self, names):
1158 def tidy_dirs(self, names):
1156 deleted = []
1159 deleted = []
1157 for d in sorted(self.dirs_of(names), reverse=True):
1160 for d in sorted(self.dirs_of(names), reverse=True):
1158 wd = self.wjoin(d)
1161 wd = self.wjoin(d)
1159 if os.listdir(wd) == '.svn':
1162 if os.listdir(wd) == '.svn':
1160 self.run0('delete', d)
1163 self.run0('delete', d)
1161 self.manifest.remove(d)
1164 self.manifest.remove(d)
1162 deleted.append(d)
1165 deleted.append(d)
1163 return deleted
1166 return deleted
1164
1167
1165 def addchild(self, parent, child):
1168 def addchild(self, parent, child):
1166 self.childmap[parent] = child
1169 self.childmap[parent] = child
1167
1170
1168 def revid(self, rev):
1171 def revid(self, rev):
1169 return u"svn:%s@%s" % (self.uuid, rev)
1172 return u"svn:%s@%s" % (self.uuid, rev)
1170
1173
1171 def putcommit(self, files, copies, parents, commit, source, revmap):
1174 def putcommit(self, files, copies, parents, commit, source, revmap):
1172 for parent in parents:
1175 for parent in parents:
1173 try:
1176 try:
1174 return self.revid(self.childmap[parent])
1177 return self.revid(self.childmap[parent])
1175 except KeyError:
1178 except KeyError:
1176 pass
1179 pass
1177
1180
1178 # Apply changes to working copy
1181 # Apply changes to working copy
1179 for f, v in files:
1182 for f, v in files:
1180 try:
1183 try:
1181 data, mode = source.getfile(f, v)
1184 data, mode = source.getfile(f, v)
1182 except IOError:
1185 except IOError:
1183 self.delete.append(f)
1186 self.delete.append(f)
1184 else:
1187 else:
1185 self.putfile(f, mode, data)
1188 self.putfile(f, mode, data)
1186 if f in copies:
1189 if f in copies:
1187 self.copies.append([copies[f], f])
1190 self.copies.append([copies[f], f])
1188 files = [f[0] for f in files]
1191 files = [f[0] for f in files]
1189
1192
1190 entries = set(self.delete)
1193 entries = set(self.delete)
1191 files = frozenset(files)
1194 files = frozenset(files)
1192 entries.update(self.add_dirs(files.difference(entries)))
1195 entries.update(self.add_dirs(files.difference(entries)))
1193 if self.copies:
1196 if self.copies:
1194 for s, d in self.copies:
1197 for s, d in self.copies:
1195 self._copyfile(s, d)
1198 self._copyfile(s, d)
1196 self.copies = []
1199 self.copies = []
1197 if self.delete:
1200 if self.delete:
1198 self.xargs(self.delete, 'delete')
1201 self.xargs(self.delete, 'delete')
1199 for f in self.delete:
1202 for f in self.delete:
1200 self.manifest.remove(f)
1203 self.manifest.remove(f)
1201 self.delete = []
1204 self.delete = []
1202 entries.update(self.add_files(files.difference(entries)))
1205 entries.update(self.add_files(files.difference(entries)))
1203 entries.update(self.tidy_dirs(entries))
1206 entries.update(self.tidy_dirs(entries))
1204 if self.delexec:
1207 if self.delexec:
1205 self.xargs(self.delexec, 'propdel', 'svn:executable')
1208 self.xargs(self.delexec, 'propdel', 'svn:executable')
1206 self.delexec = []
1209 self.delexec = []
1207 if self.setexec:
1210 if self.setexec:
1208 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1211 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1209 self.setexec = []
1212 self.setexec = []
1210
1213
1211 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1214 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1212 fp = os.fdopen(fd, 'w')
1215 fp = os.fdopen(fd, 'w')
1213 fp.write(commit.desc)
1216 fp.write(commit.desc)
1214 fp.close()
1217 fp.close()
1215 try:
1218 try:
1216 output = self.run0('commit',
1219 output = self.run0('commit',
1217 username=util.shortuser(commit.author),
1220 username=util.shortuser(commit.author),
1218 file=messagefile,
1221 file=messagefile,
1219 encoding='utf-8')
1222 encoding='utf-8')
1220 try:
1223 try:
1221 rev = self.commit_re.search(output).group(1)
1224 rev = self.commit_re.search(output).group(1)
1222 except AttributeError:
1225 except AttributeError:
1223 if not files:
1226 if not files:
1224 return parents[0]
1227 return parents[0]
1225 self.ui.warn(_('unexpected svn output:\n'))
1228 self.ui.warn(_('unexpected svn output:\n'))
1226 self.ui.warn(output)
1229 self.ui.warn(output)
1227 raise util.Abort(_('unable to cope with svn output'))
1230 raise util.Abort(_('unable to cope with svn output'))
1228 if commit.rev:
1231 if commit.rev:
1229 self.run('propset', 'hg:convert-rev', commit.rev,
1232 self.run('propset', 'hg:convert-rev', commit.rev,
1230 revprop=True, revision=rev)
1233 revprop=True, revision=rev)
1231 if commit.branch and commit.branch != 'default':
1234 if commit.branch and commit.branch != 'default':
1232 self.run('propset', 'hg:convert-branch', commit.branch,
1235 self.run('propset', 'hg:convert-branch', commit.branch,
1233 revprop=True, revision=rev)
1236 revprop=True, revision=rev)
1234 for parent in parents:
1237 for parent in parents:
1235 self.addchild(parent, rev)
1238 self.addchild(parent, rev)
1236 return self.revid(rev)
1239 return self.revid(rev)
1237 finally:
1240 finally:
1238 os.unlink(messagefile)
1241 os.unlink(messagefile)
1239
1242
1240 def puttags(self, tags):
1243 def puttags(self, tags):
1241 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1244 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1242 return None, None
1245 return None, None
1243
1246
1244 def hascommit(self, rev):
1247 def hascommit(self, rev):
1245 # This is not correct as one can convert to an existing subversion
1248 # This is not correct as one can convert to an existing subversion
1246 # repository and childmap would not list all revisions. Too bad.
1249 # repository and childmap would not list all revisions. Too bad.
1247 if rev in self.childmap:
1250 if rev in self.childmap:
1248 return True
1251 return True
1249 raise util.Abort(_('splice map revision %s not found in subversion '
1252 raise util.Abort(_('splice map revision %s not found in subversion '
1250 'child map (revision lookups are not implemented)')
1253 'child map (revision lookups are not implemented)')
1251 % rev)
1254 % rev)
@@ -1,461 +1,468 b''
1
1
2 $ "$TESTDIR/hghave" cvs || exit 80
2 $ "$TESTDIR/hghave" cvs || exit 80
3 $ cvscall()
3 $ cvscall()
4 > {
4 > {
5 > cvs -f "$@"
5 > cvs -f "$@"
6 > }
6 > }
7 $ hgcat()
7 $ hgcat()
8 > {
8 > {
9 > hg --cwd src-hg cat -r tip "$1"
9 > hg --cwd src-hg cat -r tip "$1"
10 > }
10 > }
11 $ echo "[extensions]" >> $HGRCPATH
11 $ echo "[extensions]" >> $HGRCPATH
12 $ echo "convert = " >> $HGRCPATH
12 $ echo "convert = " >> $HGRCPATH
13 $ echo "graphlog = " >> $HGRCPATH
13 $ echo "graphlog = " >> $HGRCPATH
14 $ cat > cvshooks.py <<EOF
14 $ cat > cvshooks.py <<EOF
15 > def cvslog(ui,repo,hooktype,log):
15 > def cvslog(ui,repo,hooktype,log):
16 > print "%s hook: %d entries"%(hooktype,len(log))
16 > print "%s hook: %d entries"%(hooktype,len(log))
17 >
17 >
18 > def cvschangesets(ui,repo,hooktype,changesets):
18 > def cvschangesets(ui,repo,hooktype,changesets):
19 > print "%s hook: %d changesets"%(hooktype,len(changesets))
19 > print "%s hook: %d changesets"%(hooktype,len(changesets))
20 > EOF
20 > EOF
21 $ hookpath=`pwd`
21 $ hookpath=`pwd`
22 $ echo "[hooks]" >> $HGRCPATH
22 $ echo "[hooks]" >> $HGRCPATH
23 $ echo "cvslog=python:$hookpath/cvshooks.py:cvslog" >> $HGRCPATH
23 $ echo "cvslog=python:$hookpath/cvshooks.py:cvslog" >> $HGRCPATH
24 $ echo "cvschangesets=python:$hookpath/cvshooks.py:cvschangesets" >> $HGRCPATH
24 $ echo "cvschangesets=python:$hookpath/cvshooks.py:cvschangesets" >> $HGRCPATH
25
25
26 create cvs repository
26 create cvs repository
27
27
28 $ mkdir cvsrepo
28 $ mkdir cvsrepo
29 $ cd cvsrepo
29 $ cd cvsrepo
30 $ CVSROOT=`pwd`
30 $ CVSROOT=`pwd`
31 $ export CVSROOT
31 $ export CVSROOT
32 $ CVS_OPTIONS=-f
32 $ CVS_OPTIONS=-f
33 $ export CVS_OPTIONS
33 $ export CVS_OPTIONS
34 $ cd ..
34 $ cd ..
35 $ cvscall -q -d "$CVSROOT" init
35 $ cvscall -q -d "$CVSROOT" init
36
36
37 create source directory
37 create source directory
38
38
39 $ mkdir src-temp
39 $ mkdir src-temp
40 $ cd src-temp
40 $ cd src-temp
41 $ echo a > a
41 $ echo a > a
42 $ mkdir b
42 $ mkdir b
43 $ cd b
43 $ cd b
44 $ echo c > c
44 $ echo c > c
45 $ cd ..
45 $ cd ..
46
46
47 import source directory
47 import source directory
48
48
49 $ cvscall -q import -m import src INITIAL start
49 $ cvscall -q import -m import src INITIAL start
50 N src/a
50 N src/a
51 N src/b/c
51 N src/b/c
52
52
53 No conflicts created by this import
53 No conflicts created by this import
54
54
55 $ cd ..
55 $ cd ..
56
56
57 checkout source directory
57 checkout source directory
58
58
59 $ cvscall -q checkout src
59 $ cvscall -q checkout src
60 U src/a
60 U src/a
61 U src/b/c
61 U src/b/c
62
62
63 commit a new revision changing b/c
63 commit a new revision changing b/c
64
64
65 $ cd src
65 $ cd src
66 $ sleep 1
66 $ sleep 1
67 $ echo c >> b/c
67 $ echo c >> b/c
68 $ cvscall -q commit -mci0 . | grep '<--'
68 $ cvscall -q commit -mci0 . | grep '<--'
69 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
69 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
70 $ cd ..
70 $ cd ..
71
71
72 convert fresh repo
72 convert fresh repo and also check localtimezone option
73
74 NOTE: This doesn't check all time zones -- it merely determines that
75 the configuration option is taking effect.
73
76
74 $ hg convert src src-hg
77 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
78 since it does not use DST (unlike other U.S. time zones) and is always
79 a fixed difference from UTC.
80
81 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
75 initializing destination src-hg repository
82 initializing destination src-hg repository
76 connecting to $TESTTMP/cvsrepo
83 connecting to $TESTTMP/cvsrepo
77 scanning source...
84 scanning source...
78 collecting CVS rlog
85 collecting CVS rlog
79 5 log entries
86 5 log entries
80 cvslog hook: 5 entries
87 cvslog hook: 5 entries
81 creating changesets
88 creating changesets
82 3 changeset entries
89 3 changeset entries
83 cvschangesets hook: 3 changesets
90 cvschangesets hook: 3 changesets
84 sorting...
91 sorting...
85 converting...
92 converting...
86 2 Initial revision
93 2 Initial revision
87 1 import
94 1 import
88 0 ci0
95 0 ci0
89 updating tags
96 updating tags
90 $ hgcat a
97 $ hgcat a
91 a
98 a
92 $ hgcat b/c
99 $ hgcat b/c
93 c
100 c
94 c
101 c
95
102
96 convert fresh repo with --filemap
103 convert fresh repo with --filemap
97
104
98 $ echo include b/c > filemap
105 $ echo include b/c > filemap
99 $ hg convert --filemap filemap src src-filemap
106 $ hg convert --filemap filemap src src-filemap
100 initializing destination src-filemap repository
107 initializing destination src-filemap repository
101 connecting to $TESTTMP/cvsrepo
108 connecting to $TESTTMP/cvsrepo
102 scanning source...
109 scanning source...
103 collecting CVS rlog
110 collecting CVS rlog
104 5 log entries
111 5 log entries
105 cvslog hook: 5 entries
112 cvslog hook: 5 entries
106 creating changesets
113 creating changesets
107 3 changeset entries
114 3 changeset entries
108 cvschangesets hook: 3 changesets
115 cvschangesets hook: 3 changesets
109 sorting...
116 sorting...
110 converting...
117 converting...
111 2 Initial revision
118 2 Initial revision
112 1 import
119 1 import
113 filtering out empty revision
120 filtering out empty revision
114 repository tip rolled back to revision 0 (undo commit)
121 repository tip rolled back to revision 0 (undo commit)
115 0 ci0
122 0 ci0
116 updating tags
123 updating tags
117 $ hgcat b/c
124 $ hgcat b/c
118 c
125 c
119 c
126 c
120 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
127 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
121 2 update tags files: .hgtags
128 2 update tags files: .hgtags
122 1 ci0 files: b/c
129 1 ci0 files: b/c
123 0 Initial revision files: b/c
130 0 Initial revision files: b/c
124
131
125 convert full repository (issue1649)
132 convert full repository (issue1649)
126
133
127 $ cvscall -q -d "$CVSROOT" checkout -d srcfull "." | grep -v CVSROOT
134 $ cvscall -q -d "$CVSROOT" checkout -d srcfull "." | grep -v CVSROOT
128 U srcfull/src/a
135 U srcfull/src/a
129 U srcfull/src/b/c
136 U srcfull/src/b/c
130 $ ls srcfull
137 $ ls srcfull
131 CVS
138 CVS
132 CVSROOT
139 CVSROOT
133 src
140 src
134 $ hg convert srcfull srcfull-hg \
141 $ hg convert srcfull srcfull-hg \
135 > | grep -v 'log entries' | grep -v 'hook:' \
142 > | grep -v 'log entries' | grep -v 'hook:' \
136 > | grep -v '^[0-3] .*' # filter instable changeset order
143 > | grep -v '^[0-3] .*' # filter instable changeset order
137 initializing destination srcfull-hg repository
144 initializing destination srcfull-hg repository
138 connecting to $TESTTMP/cvsrepo
145 connecting to $TESTTMP/cvsrepo
139 scanning source...
146 scanning source...
140 collecting CVS rlog
147 collecting CVS rlog
141 creating changesets
148 creating changesets
142 4 changeset entries
149 4 changeset entries
143 sorting...
150 sorting...
144 converting...
151 converting...
145 updating tags
152 updating tags
146 $ hg cat -r tip --cwd srcfull-hg src/a
153 $ hg cat -r tip --cwd srcfull-hg src/a
147 a
154 a
148 $ hg cat -r tip --cwd srcfull-hg src/b/c
155 $ hg cat -r tip --cwd srcfull-hg src/b/c
149 c
156 c
150 c
157 c
151
158
152 commit new file revisions
159 commit new file revisions
153
160
154 $ cd src
161 $ cd src
155 $ echo a >> a
162 $ echo a >> a
156 $ echo c >> b/c
163 $ echo c >> b/c
157 $ cvscall -q commit -mci1 . | grep '<--'
164 $ cvscall -q commit -mci1 . | grep '<--'
158 $TESTTMP/cvsrepo/src/a,v <-- a
165 $TESTTMP/cvsrepo/src/a,v <-- a
159 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
166 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
160 $ cd ..
167 $ cd ..
161
168
162 convert again
169 convert again
163
170
164 $ hg convert src src-hg
171 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
165 connecting to $TESTTMP/cvsrepo
172 connecting to $TESTTMP/cvsrepo
166 scanning source...
173 scanning source...
167 collecting CVS rlog
174 collecting CVS rlog
168 7 log entries
175 7 log entries
169 cvslog hook: 7 entries
176 cvslog hook: 7 entries
170 creating changesets
177 creating changesets
171 4 changeset entries
178 4 changeset entries
172 cvschangesets hook: 4 changesets
179 cvschangesets hook: 4 changesets
173 sorting...
180 sorting...
174 converting...
181 converting...
175 0 ci1
182 0 ci1
176 $ hgcat a
183 $ hgcat a
177 a
184 a
178 a
185 a
179 $ hgcat b/c
186 $ hgcat b/c
180 c
187 c
181 c
188 c
182 c
189 c
183
190
184 convert again with --filemap
191 convert again with --filemap
185
192
186 $ hg convert --filemap filemap src src-filemap
193 $ hg convert --filemap filemap src src-filemap
187 connecting to $TESTTMP/cvsrepo
194 connecting to $TESTTMP/cvsrepo
188 scanning source...
195 scanning source...
189 collecting CVS rlog
196 collecting CVS rlog
190 7 log entries
197 7 log entries
191 cvslog hook: 7 entries
198 cvslog hook: 7 entries
192 creating changesets
199 creating changesets
193 4 changeset entries
200 4 changeset entries
194 cvschangesets hook: 4 changesets
201 cvschangesets hook: 4 changesets
195 sorting...
202 sorting...
196 converting...
203 converting...
197 0 ci1
204 0 ci1
198 $ hgcat b/c
205 $ hgcat b/c
199 c
206 c
200 c
207 c
201 c
208 c
202 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
209 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
203 3 ci1 files: b/c
210 3 ci1 files: b/c
204 2 update tags files: .hgtags
211 2 update tags files: .hgtags
205 1 ci0 files: b/c
212 1 ci0 files: b/c
206 0 Initial revision files: b/c
213 0 Initial revision files: b/c
207
214
208 commit branch
215 commit branch
209
216
210 $ cd src
217 $ cd src
211 $ cvs -q update -r1.1 b/c
218 $ cvs -q update -r1.1 b/c
212 U b/c
219 U b/c
213 $ cvs -q tag -b branch
220 $ cvs -q tag -b branch
214 T a
221 T a
215 T b/c
222 T b/c
216 $ cvs -q update -r branch > /dev/null
223 $ cvs -q update -r branch > /dev/null
217 $ echo d >> b/c
224 $ echo d >> b/c
218 $ cvs -q commit -mci2 . | grep '<--'
225 $ cvs -q commit -mci2 . | grep '<--'
219 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
226 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
220 $ cd ..
227 $ cd ..
221
228
222 convert again
229 convert again
223
230
224 $ hg convert src src-hg
231 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
225 connecting to $TESTTMP/cvsrepo
232 connecting to $TESTTMP/cvsrepo
226 scanning source...
233 scanning source...
227 collecting CVS rlog
234 collecting CVS rlog
228 8 log entries
235 8 log entries
229 cvslog hook: 8 entries
236 cvslog hook: 8 entries
230 creating changesets
237 creating changesets
231 5 changeset entries
238 5 changeset entries
232 cvschangesets hook: 5 changesets
239 cvschangesets hook: 5 changesets
233 sorting...
240 sorting...
234 converting...
241 converting...
235 0 ci2
242 0 ci2
236 $ hgcat b/c
243 $ hgcat b/c
237 c
244 c
238 d
245 d
239
246
240 convert again with --filemap
247 convert again with --filemap
241
248
242 $ hg convert --filemap filemap src src-filemap
249 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True --filemap filemap src src-filemap
243 connecting to $TESTTMP/cvsrepo
250 connecting to $TESTTMP/cvsrepo
244 scanning source...
251 scanning source...
245 collecting CVS rlog
252 collecting CVS rlog
246 8 log entries
253 8 log entries
247 cvslog hook: 8 entries
254 cvslog hook: 8 entries
248 creating changesets
255 creating changesets
249 5 changeset entries
256 5 changeset entries
250 cvschangesets hook: 5 changesets
257 cvschangesets hook: 5 changesets
251 sorting...
258 sorting...
252 converting...
259 converting...
253 0 ci2
260 0 ci2
254 $ hgcat b/c
261 $ hgcat b/c
255 c
262 c
256 d
263 d
257 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
264 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
258 4 ci2 files: b/c
265 4 ci2 files: b/c
259 3 ci1 files: b/c
266 3 ci1 files: b/c
260 2 update tags files: .hgtags
267 2 update tags files: .hgtags
261 1 ci0 files: b/c
268 1 ci0 files: b/c
262 0 Initial revision files: b/c
269 0 Initial revision files: b/c
263
270
264 commit a new revision with funny log message
271 commit a new revision with funny log message
265
272
266 $ cd src
273 $ cd src
267 $ sleep 1
274 $ sleep 1
268 $ echo e >> a
275 $ echo e >> a
269 $ cvscall -q commit -m'funny
276 $ cvscall -q commit -m'funny
270 > ----------------------------
277 > ----------------------------
271 > log message' . | grep '<--' |\
278 > log message' . | grep '<--' |\
272 > sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
279 > sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
273 checking in src/a,v
280 checking in src/a,v
274
281
275 commit new file revisions with some fuzz
282 commit new file revisions with some fuzz
276
283
277 $ sleep 1
284 $ sleep 1
278 $ echo f >> a
285 $ echo f >> a
279 $ cvscall -q commit -mfuzzy . | grep '<--'
286 $ cvscall -q commit -mfuzzy . | grep '<--'
280 $TESTTMP/cvsrepo/src/a,v <-- a
287 $TESTTMP/cvsrepo/src/a,v <-- a
281 $ sleep 4 # the two changes will be split if fuzz < 4
288 $ sleep 4 # the two changes will be split if fuzz < 4
282 $ echo g >> b/c
289 $ echo g >> b/c
283 $ cvscall -q commit -mfuzzy . | grep '<--'
290 $ cvscall -q commit -mfuzzy . | grep '<--'
284 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
291 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
285 $ cd ..
292 $ cd ..
286
293
287 convert again
294 convert again
288
295
289 $ hg convert --config convert.cvsps.fuzz=2 src src-hg
296 $ TZ=US/Hawaii hg convert --config convert.cvsps.fuzz=2 --config convert.localtimezone=True src src-hg
290 connecting to $TESTTMP/cvsrepo
297 connecting to $TESTTMP/cvsrepo
291 scanning source...
298 scanning source...
292 collecting CVS rlog
299 collecting CVS rlog
293 11 log entries
300 11 log entries
294 cvslog hook: 11 entries
301 cvslog hook: 11 entries
295 creating changesets
302 creating changesets
296 8 changeset entries
303 8 changeset entries
297 cvschangesets hook: 8 changesets
304 cvschangesets hook: 8 changesets
298 sorting...
305 sorting...
299 converting...
306 converting...
300 2 funny
307 2 funny
301 1 fuzzy
308 1 fuzzy
302 0 fuzzy
309 0 fuzzy
303 $ hg -R src-hg glog --template '{rev} ({branches}) {desc} files: {files}\n'
310 $ hg -R src-hg glog --template '{rev} ({branches}) {desc} date: {date|date} files: {files}\n'
304 o 8 (branch) fuzzy files: b/c
311 o 8 (branch) fuzzy date: * -1000 files: b/c (glob)
305 |
312 |
306 o 7 (branch) fuzzy files: a
313 o 7 (branch) fuzzy date: * -1000 files: a (glob)
307 |
314 |
308 o 6 (branch) funny
315 o 6 (branch) funny
309 | ----------------------------
316 | ----------------------------
310 | log message files: a
317 | log message date: * -1000 files: a (glob)
311 o 5 (branch) ci2 files: b/c
318 o 5 (branch) ci2 date: * -1000 files: b/c (glob)
312
319
313 o 4 () ci1 files: a b/c
320 o 4 () ci1 date: * -1000 files: a b/c (glob)
314 |
321 |
315 o 3 () update tags files: .hgtags
322 o 3 () update tags date: * +0000 files: .hgtags (glob)
316 |
323 |
317 o 2 () ci0 files: b/c
324 o 2 () ci0 date: * -1000 files: b/c (glob)
318 |
325 |
319 | o 1 (INITIAL) import files:
326 | o 1 (INITIAL) import date: * -1000 files: (glob)
320 |/
327 |/
321 o 0 () Initial revision files: a b/c
328 o 0 () Initial revision date: * -1000 files: a b/c (glob)
322
329
323
330
324 testing debugcvsps
331 testing debugcvsps
325
332
326 $ cd src
333 $ cd src
327 $ hg debugcvsps --fuzz=2
334 $ hg debugcvsps --fuzz=2
328 collecting CVS rlog
335 collecting CVS rlog
329 11 log entries
336 11 log entries
330 cvslog hook: 11 entries
337 cvslog hook: 11 entries
331 creating changesets
338 creating changesets
332 10 changeset entries
339 10 changeset entries
333 cvschangesets hook: 10 changesets
340 cvschangesets hook: 10 changesets
334 ---------------------
341 ---------------------
335 PatchSet 1
342 PatchSet 1
336 Date: * (glob)
343 Date: * (glob)
337 Author: * (glob)
344 Author: * (glob)
338 Branch: HEAD
345 Branch: HEAD
339 Tag: (none)
346 Tag: (none)
340 Branchpoints: INITIAL
347 Branchpoints: INITIAL
341 Log:
348 Log:
342 Initial revision
349 Initial revision
343
350
344 Members:
351 Members:
345 a:INITIAL->1.1
352 a:INITIAL->1.1
346
353
347 ---------------------
354 ---------------------
348 PatchSet 2
355 PatchSet 2
349 Date: * (glob)
356 Date: * (glob)
350 Author: * (glob)
357 Author: * (glob)
351 Branch: HEAD
358 Branch: HEAD
352 Tag: (none)
359 Tag: (none)
353 Branchpoints: INITIAL, branch
360 Branchpoints: INITIAL, branch
354 Log:
361 Log:
355 Initial revision
362 Initial revision
356
363
357 Members:
364 Members:
358 b/c:INITIAL->1.1
365 b/c:INITIAL->1.1
359
366
360 ---------------------
367 ---------------------
361 PatchSet 3
368 PatchSet 3
362 Date: * (glob)
369 Date: * (glob)
363 Author: * (glob)
370 Author: * (glob)
364 Branch: INITIAL
371 Branch: INITIAL
365 Tag: start
372 Tag: start
366 Log:
373 Log:
367 import
374 import
368
375
369 Members:
376 Members:
370 a:1.1->1.1.1.1
377 a:1.1->1.1.1.1
371 b/c:1.1->1.1.1.1
378 b/c:1.1->1.1.1.1
372
379
373 ---------------------
380 ---------------------
374 PatchSet 4
381 PatchSet 4
375 Date: * (glob)
382 Date: * (glob)
376 Author: * (glob)
383 Author: * (glob)
377 Branch: HEAD
384 Branch: HEAD
378 Tag: (none)
385 Tag: (none)
379 Log:
386 Log:
380 ci0
387 ci0
381
388
382 Members:
389 Members:
383 b/c:1.1->1.2
390 b/c:1.1->1.2
384
391
385 ---------------------
392 ---------------------
386 PatchSet 5
393 PatchSet 5
387 Date: * (glob)
394 Date: * (glob)
388 Author: * (glob)
395 Author: * (glob)
389 Branch: HEAD
396 Branch: HEAD
390 Tag: (none)
397 Tag: (none)
391 Branchpoints: branch
398 Branchpoints: branch
392 Log:
399 Log:
393 ci1
400 ci1
394
401
395 Members:
402 Members:
396 a:1.1->1.2
403 a:1.1->1.2
397
404
398 ---------------------
405 ---------------------
399 PatchSet 6
406 PatchSet 6
400 Date: * (glob)
407 Date: * (glob)
401 Author: * (glob)
408 Author: * (glob)
402 Branch: HEAD
409 Branch: HEAD
403 Tag: (none)
410 Tag: (none)
404 Log:
411 Log:
405 ci1
412 ci1
406
413
407 Members:
414 Members:
408 b/c:1.2->1.3
415 b/c:1.2->1.3
409
416
410 ---------------------
417 ---------------------
411 PatchSet 7
418 PatchSet 7
412 Date: * (glob)
419 Date: * (glob)
413 Author: * (glob)
420 Author: * (glob)
414 Branch: branch
421 Branch: branch
415 Tag: (none)
422 Tag: (none)
416 Log:
423 Log:
417 ci2
424 ci2
418
425
419 Members:
426 Members:
420 b/c:1.1->1.1.2.1
427 b/c:1.1->1.1.2.1
421
428
422 ---------------------
429 ---------------------
423 PatchSet 8
430 PatchSet 8
424 Date: * (glob)
431 Date: * (glob)
425 Author: * (glob)
432 Author: * (glob)
426 Branch: branch
433 Branch: branch
427 Tag: (none)
434 Tag: (none)
428 Log:
435 Log:
429 funny
436 funny
430 ----------------------------
437 ----------------------------
431 log message
438 log message
432
439
433 Members:
440 Members:
434 a:1.2->1.2.2.1
441 a:1.2->1.2.2.1
435
442
436 ---------------------
443 ---------------------
437 PatchSet 9
444 PatchSet 9
438 Date: * (glob)
445 Date: * (glob)
439 Author: * (glob)
446 Author: * (glob)
440 Branch: branch
447 Branch: branch
441 Tag: (none)
448 Tag: (none)
442 Log:
449 Log:
443 fuzzy
450 fuzzy
444
451
445 Members:
452 Members:
446 a:1.2.2.1->1.2.2.2
453 a:1.2.2.1->1.2.2.2
447
454
448 ---------------------
455 ---------------------
449 PatchSet 10
456 PatchSet 10
450 Date: * (glob)
457 Date: * (glob)
451 Author: * (glob)
458 Author: * (glob)
452 Branch: branch
459 Branch: branch
453 Tag: (none)
460 Tag: (none)
454 Log:
461 Log:
455 fuzzy
462 fuzzy
456
463
457 Members:
464 Members:
458 b/c:1.1.2.1->1.1.2.2
465 b/c:1.1.2.1->1.1.2.2
459
466
460
467
461 $ cd ..
468 $ cd ..
@@ -1,203 +1,210 b''
1
1
2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
3
3
4 $ cat >> $HGRCPATH <<EOF
4 $ cat >> $HGRCPATH <<EOF
5 > [extensions]
5 > [extensions]
6 > convert =
6 > convert =
7 > graphlog =
7 > graphlog =
8 > [convert]
8 > [convert]
9 > svn.trunk = mytrunk
9 > svn.trunk = mytrunk
10 > EOF
10 > EOF
11
11
12 $ svnadmin create svn-repo
12 $ svnadmin create svn-repo
13 $ SVNREPOPATH=`pwd`/svn-repo
13 $ SVNREPOPATH=`pwd`/svn-repo
14 #if windows
14 #if windows
15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
16 #else
16 #else
17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
18 #endif
18 #endif
19
19
20 Now test that it works with trunk/tags layout, but no branches yet.
20 Now test that it works with trunk/tags layout, but no branches yet.
21
21
22 Initial svn import
22 Initial svn import
23
23
24 $ mkdir projB
24 $ mkdir projB
25 $ cd projB
25 $ cd projB
26 $ mkdir mytrunk
26 $ mkdir mytrunk
27 $ mkdir tags
27 $ mkdir tags
28 $ cd ..
28 $ cd ..
29
29
30 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
30 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
31
31
32 Adding projB/mytrunk (glob)
32 Adding projB/mytrunk (glob)
33 Adding projB/tags (glob)
33 Adding projB/tags (glob)
34 Committed revision 1.
34 Committed revision 1.
35
35
36 Update svn repository
36 Update svn repository
37
37
38 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
38 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
39 Checked out revision 1.
39 Checked out revision 1.
40 $ cd B
40 $ cd B
41 $ echo hello > 'letter .txt'
41 $ echo hello > 'letter .txt'
42 $ svn add 'letter .txt'
42 $ svn add 'letter .txt'
43 A letter .txt
43 A letter .txt
44 $ svn ci -m hello
44 $ svn ci -m hello
45 Adding letter .txt
45 Adding letter .txt
46 Transmitting file data .
46 Transmitting file data .
47 Committed revision 2.
47 Committed revision 2.
48
48
49 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
49 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
50 $ svn ci -m world
50 $ svn ci -m world
51 Sending letter .txt
51 Sending letter .txt
52 Transmitting file data .
52 Transmitting file data .
53 Committed revision 3.
53 Committed revision 3.
54
54
55 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
55 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
56
56
57 Committed revision 4.
57 Committed revision 4.
58
58
59 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
59 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
60 $ svn ci -m "nice day"
60 $ svn ci -m "nice day"
61 Sending letter .txt
61 Sending letter .txt
62 Transmitting file data .
62 Transmitting file data .
63 Committed revision 5.
63 Committed revision 5.
64 $ cd ..
64 $ cd ..
65
65
66 Convert to hg once
66 Convert to hg once and also test localtimezone option
67
68 NOTE: This doesn't check all time zones -- it merely determines that
69 the configuration option is taking effect.
67
70
68 $ hg convert "$SVNREPOURL/proj%20B" B-hg
71 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
72 since it does not use DST (unlike other U.S. time zones) and is always
73 a fixed difference from UTC.
74
75 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
69 initializing destination B-hg repository
76 initializing destination B-hg repository
70 scanning source...
77 scanning source...
71 sorting...
78 sorting...
72 converting...
79 converting...
73 3 init projB
80 3 init projB
74 2 hello
81 2 hello
75 1 world
82 1 world
76 0 nice day
83 0 nice day
77 updating tags
84 updating tags
78
85
79 Update svn repository again
86 Update svn repository again
80
87
81 $ cd B
88 $ cd B
82 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
89 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
83 $ echo "nice to meet you" > letter2.txt
90 $ echo "nice to meet you" > letter2.txt
84 $ svn add letter2.txt
91 $ svn add letter2.txt
85 A letter2.txt
92 A letter2.txt
86 $ svn ci -m "second letter"
93 $ svn ci -m "second letter"
87 Sending letter .txt
94 Sending letter .txt
88 Adding letter2.txt
95 Adding letter2.txt
89 Transmitting file data ..
96 Transmitting file data ..
90 Committed revision 6.
97 Committed revision 6.
91
98
92 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
99 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
93
100
94 Committed revision 7.
101 Committed revision 7.
95
102
96 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
103 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
97 $ svn ci -m "work in progress"
104 $ svn ci -m "work in progress"
98 Sending letter2.txt
105 Sending letter2.txt
99 Transmitting file data .
106 Transmitting file data .
100 Committed revision 8.
107 Committed revision 8.
101 $ cd ..
108 $ cd ..
102
109
103 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
110 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
104 initializing destination dest repository
111 initializing destination dest repository
105 abort: no revision found in module /proj B/non-existent-path
112 abort: no revision found in module /proj B/non-existent-path
106 [255]
113 [255]
107
114
108 ########################################
115 ########################################
109
116
110 Test incremental conversion
117 Test incremental conversion
111
118
112 $ hg convert "$SVNREPOURL/proj%20B" B-hg
119 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
113 scanning source...
120 scanning source...
114 sorting...
121 sorting...
115 converting...
122 converting...
116 1 second letter
123 1 second letter
117 0 work in progress
124 0 work in progress
118 updating tags
125 updating tags
119
126
120 $ cd B-hg
127 $ cd B-hg
121 $ hg glog --template '{rev} {desc|firstline} files: {files}\n'
128 $ hg glog --template '{rev} {desc|firstline} date: {date|date} files: {files}\n'
122 o 7 update tags files: .hgtags
129 o 7 update tags date: * +0000 files: .hgtags (glob)
123 |
130 |
124 o 6 work in progress files: letter2.txt
131 o 6 work in progress date: * -1000 files: letter2.txt (glob)
125 |
132 |
126 o 5 second letter files: letter .txt letter2.txt
133 o 5 second letter date: * -1000 files: letter .txt letter2.txt (glob)
127 |
134 |
128 o 4 update tags files: .hgtags
135 o 4 update tags date: * +0000 files: .hgtags (glob)
129 |
136 |
130 o 3 nice day files: letter .txt
137 o 3 nice day date: * -1000 files: letter .txt (glob)
131 |
138 |
132 o 2 world files: letter .txt
139 o 2 world date: * -1000 files: letter .txt (glob)
133 |
140 |
134 o 1 hello files: letter .txt
141 o 1 hello date: * -1000 files: letter .txt (glob)
135 |
142 |
136 o 0 init projB files:
143 o 0 init projB date: * -1000 files: (glob)
137
144
138 $ hg tags -q
145 $ hg tags -q
139 tip
146 tip
140 v0.2
147 v0.2
141 v0.1
148 v0.1
142 $ cd ..
149 $ cd ..
143
150
144 Test filemap
151 Test filemap
145 $ echo 'include letter2.txt' > filemap
152 $ echo 'include letter2.txt' > filemap
146 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
153 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
147 initializing destination fmap repository
154 initializing destination fmap repository
148 scanning source...
155 scanning source...
149 sorting...
156 sorting...
150 converting...
157 converting...
151 5 init projB
158 5 init projB
152 4 hello
159 4 hello
153 3 world
160 3 world
154 2 nice day
161 2 nice day
155 1 second letter
162 1 second letter
156 0 work in progress
163 0 work in progress
157 $ hg -R fmap branch -q
164 $ hg -R fmap branch -q
158 default
165 default
159 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
166 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
160 o 1 work in progress files: letter2.txt
167 o 1 work in progress files: letter2.txt
161 |
168 |
162 o 0 second letter files: letter2.txt
169 o 0 second letter files: letter2.txt
163
170
164
171
165 Test stop revision
172 Test stop revision
166 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
173 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
167 initializing destination stoprev repository
174 initializing destination stoprev repository
168 scanning source...
175 scanning source...
169 sorting...
176 sorting...
170 converting...
177 converting...
171 0 init projB
178 0 init projB
172 $ hg -R stoprev branch -q
179 $ hg -R stoprev branch -q
173 default
180 default
174
181
175 Check convert_revision extra-records.
182 Check convert_revision extra-records.
176 This is also the only place testing more than one extra field in a revision.
183 This is also the only place testing more than one extra field in a revision.
177
184
178 $ cd stoprev
185 $ cd stoprev
179 $ hg tip --debug | grep extra
186 $ hg tip --debug | grep extra
180 extra: branch=default
187 extra: branch=default
181 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
188 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
182 $ cd ..
189 $ cd ..
183
190
184 Test converting empty heads (issue3347)
191 Test converting empty heads (issue3347)
185
192
186 $ svnadmin create svn-empty
193 $ svnadmin create svn-empty
187 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
194 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
188 $ hg --config convert.svn.trunk= convert svn-empty
195 $ hg --config convert.svn.trunk= convert svn-empty
189 assuming destination svn-empty-hg
196 assuming destination svn-empty-hg
190 initializing destination svn-empty-hg repository
197 initializing destination svn-empty-hg repository
191 scanning source...
198 scanning source...
192 sorting...
199 sorting...
193 converting...
200 converting...
194 1 init projA
201 1 init projA
195 0 adddir
202 0 adddir
196 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
203 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
197 assuming destination trunk-hg
204 assuming destination trunk-hg
198 initializing destination trunk-hg repository
205 initializing destination trunk-hg repository
199 scanning source...
206 scanning source...
200 sorting...
207 sorting...
201 converting...
208 converting...
202 1 init projA
209 1 init projA
203 0 adddir
210 0 adddir
@@ -1,447 +1,455 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > convert=
3 > convert=
4 > [convert]
4 > [convert]
5 > hg.saverev=False
5 > hg.saverev=False
6 > EOF
6 > EOF
7 $ hg help convert
7 $ hg help convert
8 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
8 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
9
9
10 convert a foreign SCM repository to a Mercurial one.
10 convert a foreign SCM repository to a Mercurial one.
11
11
12 Accepted source formats [identifiers]:
12 Accepted source formats [identifiers]:
13
13
14 - Mercurial [hg]
14 - Mercurial [hg]
15 - CVS [cvs]
15 - CVS [cvs]
16 - Darcs [darcs]
16 - Darcs [darcs]
17 - git [git]
17 - git [git]
18 - Subversion [svn]
18 - Subversion [svn]
19 - Monotone [mtn]
19 - Monotone [mtn]
20 - GNU Arch [gnuarch]
20 - GNU Arch [gnuarch]
21 - Bazaar [bzr]
21 - Bazaar [bzr]
22 - Perforce [p4]
22 - Perforce [p4]
23
23
24 Accepted destination formats [identifiers]:
24 Accepted destination formats [identifiers]:
25
25
26 - Mercurial [hg]
26 - Mercurial [hg]
27 - Subversion [svn] (history on branches is not preserved)
27 - Subversion [svn] (history on branches is not preserved)
28
28
29 If no revision is given, all revisions will be converted. Otherwise,
29 If no revision is given, all revisions will be converted. Otherwise,
30 convert will only import up to the named revision (given in a format
30 convert will only import up to the named revision (given in a format
31 understood by the source).
31 understood by the source).
32
32
33 If no destination directory name is specified, it defaults to the basename
33 If no destination directory name is specified, it defaults to the basename
34 of the source with "-hg" appended. If the destination repository doesn't
34 of the source with "-hg" appended. If the destination repository doesn't
35 exist, it will be created.
35 exist, it will be created.
36
36
37 By default, all sources except Mercurial will use --branchsort. Mercurial
37 By default, all sources except Mercurial will use --branchsort. Mercurial
38 uses --sourcesort to preserve original revision numbers order. Sort modes
38 uses --sourcesort to preserve original revision numbers order. Sort modes
39 have the following effects:
39 have the following effects:
40
40
41 --branchsort convert from parent to child revision when possible, which
41 --branchsort convert from parent to child revision when possible, which
42 means branches are usually converted one after the other.
42 means branches are usually converted one after the other.
43 It generates more compact repositories.
43 It generates more compact repositories.
44 --datesort sort revisions by date. Converted repositories have good-
44 --datesort sort revisions by date. Converted repositories have good-
45 looking changelogs but are often an order of magnitude
45 looking changelogs but are often an order of magnitude
46 larger than the same ones generated by --branchsort.
46 larger than the same ones generated by --branchsort.
47 --sourcesort try to preserve source revisions order, only supported by
47 --sourcesort try to preserve source revisions order, only supported by
48 Mercurial sources.
48 Mercurial sources.
49
49
50 If "REVMAP" isn't given, it will be put in a default location
50 If "REVMAP" isn't given, it will be put in a default location
51 ("<dest>/.hg/shamap" by default). The "REVMAP" is a simple text file that
51 ("<dest>/.hg/shamap" by default). The "REVMAP" is a simple text file that
52 maps each source commit ID to the destination ID for that revision, like
52 maps each source commit ID to the destination ID for that revision, like
53 so:
53 so:
54
54
55 <source ID> <destination ID>
55 <source ID> <destination ID>
56
56
57 If the file doesn't exist, it's automatically created. It's updated on
57 If the file doesn't exist, it's automatically created. It's updated on
58 each commit copied, so "hg convert" can be interrupted and can be run
58 each commit copied, so "hg convert" can be interrupted and can be run
59 repeatedly to copy new commits.
59 repeatedly to copy new commits.
60
60
61 The authormap is a simple text file that maps each source commit author to
61 The authormap is a simple text file that maps each source commit author to
62 a destination commit author. It is handy for source SCMs that use unix
62 a destination commit author. It is handy for source SCMs that use unix
63 logins to identify authors (e.g.: CVS). One line per author mapping and
63 logins to identify authors (e.g.: CVS). One line per author mapping and
64 the line format is:
64 the line format is:
65
65
66 source author = destination author
66 source author = destination author
67
67
68 Empty lines and lines starting with a "#" are ignored.
68 Empty lines and lines starting with a "#" are ignored.
69
69
70 The filemap is a file that allows filtering and remapping of files and
70 The filemap is a file that allows filtering and remapping of files and
71 directories. Each line can contain one of the following directives:
71 directories. Each line can contain one of the following directives:
72
72
73 include path/to/file-or-dir
73 include path/to/file-or-dir
74
74
75 exclude path/to/file-or-dir
75 exclude path/to/file-or-dir
76
76
77 rename path/to/source path/to/destination
77 rename path/to/source path/to/destination
78
78
79 Comment lines start with "#". A specified path matches if it equals the
79 Comment lines start with "#". A specified path matches if it equals the
80 full relative name of a file or one of its parent directories. The
80 full relative name of a file or one of its parent directories. The
81 "include" or "exclude" directive with the longest matching path applies,
81 "include" or "exclude" directive with the longest matching path applies,
82 so line order does not matter.
82 so line order does not matter.
83
83
84 The "include" directive causes a file, or all files under a directory, to
84 The "include" directive causes a file, or all files under a directory, to
85 be included in the destination repository, and the exclusion of all other
85 be included in the destination repository, and the exclusion of all other
86 files and directories not explicitly included. The "exclude" directive
86 files and directories not explicitly included. The "exclude" directive
87 causes files or directories to be omitted. The "rename" directive renames
87 causes files or directories to be omitted. The "rename" directive renames
88 a file or directory if it is converted. To rename from a subdirectory into
88 a file or directory if it is converted. To rename from a subdirectory into
89 the root of the repository, use "." as the path to rename to.
89 the root of the repository, use "." as the path to rename to.
90
90
91 The splicemap is a file that allows insertion of synthetic history,
91 The splicemap is a file that allows insertion of synthetic history,
92 letting you specify the parents of a revision. This is useful if you want
92 letting you specify the parents of a revision. This is useful if you want
93 to e.g. give a Subversion merge two parents, or graft two disconnected
93 to e.g. give a Subversion merge two parents, or graft two disconnected
94 series of history together. Each entry contains a key, followed by a
94 series of history together. Each entry contains a key, followed by a
95 space, followed by one or two comma-separated values:
95 space, followed by one or two comma-separated values:
96
96
97 key parent1, parent2
97 key parent1, parent2
98
98
99 The key is the revision ID in the source revision control system whose
99 The key is the revision ID in the source revision control system whose
100 parents should be modified (same format as a key in .hg/shamap). The
100 parents should be modified (same format as a key in .hg/shamap). The
101 values are the revision IDs (in either the source or destination revision
101 values are the revision IDs (in either the source or destination revision
102 control system) that should be used as the new parents for that node. For
102 control system) that should be used as the new parents for that node. For
103 example, if you have merged "release-1.0" into "trunk", then you should
103 example, if you have merged "release-1.0" into "trunk", then you should
104 specify the revision on "trunk" as the first parent and the one on the
104 specify the revision on "trunk" as the first parent and the one on the
105 "release-1.0" branch as the second.
105 "release-1.0" branch as the second.
106
106
107 The branchmap is a file that allows you to rename a branch when it is
107 The branchmap is a file that allows you to rename a branch when it is
108 being brought in from whatever external repository. When used in
108 being brought in from whatever external repository. When used in
109 conjunction with a splicemap, it allows for a powerful combination to help
109 conjunction with a splicemap, it allows for a powerful combination to help
110 fix even the most badly mismanaged repositories and turn them into nicely
110 fix even the most badly mismanaged repositories and turn them into nicely
111 structured Mercurial repositories. The branchmap contains lines of the
111 structured Mercurial repositories. The branchmap contains lines of the
112 form:
112 form:
113
113
114 original_branch_name new_branch_name
114 original_branch_name new_branch_name
115
115
116 where "original_branch_name" is the name of the branch in the source
116 where "original_branch_name" is the name of the branch in the source
117 repository, and "new_branch_name" is the name of the branch is the
117 repository, and "new_branch_name" is the name of the branch is the
118 destination repository. No whitespace is allowed in the branch names. This
118 destination repository. No whitespace is allowed in the branch names. This
119 can be used to (for instance) move code in one repository from "default"
119 can be used to (for instance) move code in one repository from "default"
120 to a named branch.
120 to a named branch.
121
121
122 Mercurial Source
122 Mercurial Source
123 ################
123 ################
124
124
125 The Mercurial source recognizes the following configuration options, which
125 The Mercurial source recognizes the following configuration options, which
126 you can set on the command line with "--config":
126 you can set on the command line with "--config":
127
127
128 convert.hg.ignoreerrors
128 convert.hg.ignoreerrors
129 ignore integrity errors when reading. Use it to fix
129 ignore integrity errors when reading. Use it to fix
130 Mercurial repositories with missing revlogs, by converting
130 Mercurial repositories with missing revlogs, by converting
131 from and to Mercurial. Default is False.
131 from and to Mercurial. Default is False.
132 convert.hg.saverev
132 convert.hg.saverev
133 store original revision ID in changeset (forces target IDs
133 store original revision ID in changeset (forces target IDs
134 to change). It takes a boolean argument and defaults to
134 to change). It takes a boolean argument and defaults to
135 False.
135 False.
136 convert.hg.startrev
136 convert.hg.startrev
137 convert start revision and its descendants. It takes a hg
137 convert start revision and its descendants. It takes a hg
138 revision identifier and defaults to 0.
138 revision identifier and defaults to 0.
139
139
140 CVS Source
140 CVS Source
141 ##########
141 ##########
142
142
143 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
143 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
144 indicate the starting point of what will be converted. Direct access to
144 indicate the starting point of what will be converted. Direct access to
145 the repository files is not needed, unless of course the repository is
145 the repository files is not needed, unless of course the repository is
146 ":local:". The conversion uses the top level directory in the sandbox to
146 ":local:". The conversion uses the top level directory in the sandbox to
147 find the CVS repository, and then uses CVS rlog commands to find files to
147 find the CVS repository, and then uses CVS rlog commands to find files to
148 convert. This means that unless a filemap is given, all files under the
148 convert. This means that unless a filemap is given, all files under the
149 starting directory will be converted, and that any directory
149 starting directory will be converted, and that any directory
150 reorganization in the CVS sandbox is ignored.
150 reorganization in the CVS sandbox is ignored.
151
151
152 The following options can be used with "--config":
152 The following options can be used with "--config":
153
153
154 convert.cvsps.cache
154 convert.cvsps.cache
155 Set to False to disable remote log caching, for testing and
155 Set to False to disable remote log caching, for testing and
156 debugging purposes. Default is True.
156 debugging purposes. Default is True.
157 convert.cvsps.fuzz
157 convert.cvsps.fuzz
158 Specify the maximum time (in seconds) that is allowed
158 Specify the maximum time (in seconds) that is allowed
159 between commits with identical user and log message in a
159 between commits with identical user and log message in a
160 single changeset. When very large files were checked in as
160 single changeset. When very large files were checked in as
161 part of a changeset then the default may not be long enough.
161 part of a changeset then the default may not be long enough.
162 The default is 60.
162 The default is 60.
163 convert.cvsps.mergeto
163 convert.cvsps.mergeto
164 Specify a regular expression to which commit log messages
164 Specify a regular expression to which commit log messages
165 are matched. If a match occurs, then the conversion process
165 are matched. If a match occurs, then the conversion process
166 will insert a dummy revision merging the branch on which
166 will insert a dummy revision merging the branch on which
167 this log message occurs to the branch indicated in the
167 this log message occurs to the branch indicated in the
168 regex. Default is "{{mergetobranch ([-\w]+)}}"
168 regex. Default is "{{mergetobranch ([-\w]+)}}"
169 convert.cvsps.mergefrom
169 convert.cvsps.mergefrom
170 Specify a regular expression to which commit log messages
170 Specify a regular expression to which commit log messages
171 are matched. If a match occurs, then the conversion process
171 are matched. If a match occurs, then the conversion process
172 will add the most recent revision on the branch indicated in
172 will add the most recent revision on the branch indicated in
173 the regex as the second parent of the changeset. Default is
173 the regex as the second parent of the changeset. Default is
174 "{{mergefrombranch ([-\w]+)}}"
174 "{{mergefrombranch ([-\w]+)}}"
175 convert.localtimezone
176 use local time (as determined by the TZ environment
177 variable) for changeset date/times. The default is False
178 (use UTC).
175 hook.cvslog Specify a Python function to be called at the end of
179 hook.cvslog Specify a Python function to be called at the end of
176 gathering the CVS log. The function is passed a list with
180 gathering the CVS log. The function is passed a list with
177 the log entries, and can modify the entries in-place, or add
181 the log entries, and can modify the entries in-place, or add
178 or delete them.
182 or delete them.
179 hook.cvschangesets
183 hook.cvschangesets
180 Specify a Python function to be called after the changesets
184 Specify a Python function to be called after the changesets
181 are calculated from the CVS log. The function is passed a
185 are calculated from the CVS log. The function is passed a
182 list with the changeset entries, and can modify the
186 list with the changeset entries, and can modify the
183 changesets in-place, or add or delete them.
187 changesets in-place, or add or delete them.
184
188
185 An additional "debugcvsps" Mercurial command allows the builtin changeset
189 An additional "debugcvsps" Mercurial command allows the builtin changeset
186 merging code to be run without doing a conversion. Its parameters and
190 merging code to be run without doing a conversion. Its parameters and
187 output are similar to that of cvsps 2.1. Please see the command help for
191 output are similar to that of cvsps 2.1. Please see the command help for
188 more details.
192 more details.
189
193
190 Subversion Source
194 Subversion Source
191 #################
195 #################
192
196
193 Subversion source detects classical trunk/branches/tags layouts. By
197 Subversion source detects classical trunk/branches/tags layouts. By
194 default, the supplied "svn://repo/path/" source URL is converted as a
198 default, the supplied "svn://repo/path/" source URL is converted as a
195 single branch. If "svn://repo/path/trunk" exists it replaces the default
199 single branch. If "svn://repo/path/trunk" exists it replaces the default
196 branch. If "svn://repo/path/branches" exists, its subdirectories are
200 branch. If "svn://repo/path/branches" exists, its subdirectories are
197 listed as possible branches. If "svn://repo/path/tags" exists, it is
201 listed as possible branches. If "svn://repo/path/tags" exists, it is
198 looked for tags referencing converted branches. Default "trunk",
202 looked for tags referencing converted branches. Default "trunk",
199 "branches" and "tags" values can be overridden with following options. Set
203 "branches" and "tags" values can be overridden with following options. Set
200 them to paths relative to the source URL, or leave them blank to disable
204 them to paths relative to the source URL, or leave them blank to disable
201 auto detection.
205 auto detection.
202
206
203 The following options can be set with "--config":
207 The following options can be set with "--config":
204
208
205 convert.svn.branches
209 convert.svn.branches
206 specify the directory containing branches. The default is
210 specify the directory containing branches. The default is
207 "branches".
211 "branches".
208 convert.svn.tags
212 convert.svn.tags
209 specify the directory containing tags. The default is
213 specify the directory containing tags. The default is
210 "tags".
214 "tags".
211 convert.svn.trunk
215 convert.svn.trunk
212 specify the name of the trunk branch. The default is
216 specify the name of the trunk branch. The default is
213 "trunk".
217 "trunk".
218 convert.localtimezone
219 use local time (as determined by the TZ environment
220 variable) for changeset date/times. The default is False
221 (use UTC).
214
222
215 Source history can be retrieved starting at a specific revision, instead
223 Source history can be retrieved starting at a specific revision, instead
216 of being integrally converted. Only single branch conversions are
224 of being integrally converted. Only single branch conversions are
217 supported.
225 supported.
218
226
219 convert.svn.startrev
227 convert.svn.startrev
220 specify start Subversion revision number. The default is 0.
228 specify start Subversion revision number. The default is 0.
221
229
222 Perforce Source
230 Perforce Source
223 ###############
231 ###############
224
232
225 The Perforce (P4) importer can be given a p4 depot path or a client
233 The Perforce (P4) importer can be given a p4 depot path or a client
226 specification as source. It will convert all files in the source to a flat
234 specification as source. It will convert all files in the source to a flat
227 Mercurial repository, ignoring labels, branches and integrations. Note
235 Mercurial repository, ignoring labels, branches and integrations. Note
228 that when a depot path is given you then usually should specify a target
236 that when a depot path is given you then usually should specify a target
229 directory, because otherwise the target may be named "...-hg".
237 directory, because otherwise the target may be named "...-hg".
230
238
231 It is possible to limit the amount of source history to be converted by
239 It is possible to limit the amount of source history to be converted by
232 specifying an initial Perforce revision:
240 specifying an initial Perforce revision:
233
241
234 convert.p4.startrev
242 convert.p4.startrev
235 specify initial Perforce revision (a Perforce changelist
243 specify initial Perforce revision (a Perforce changelist
236 number).
244 number).
237
245
238 Mercurial Destination
246 Mercurial Destination
239 #####################
247 #####################
240
248
241 The following options are supported:
249 The following options are supported:
242
250
243 convert.hg.clonebranches
251 convert.hg.clonebranches
244 dispatch source branches in separate clones. The default is
252 dispatch source branches in separate clones. The default is
245 False.
253 False.
246 convert.hg.tagsbranch
254 convert.hg.tagsbranch
247 branch name for tag revisions, defaults to "default".
255 branch name for tag revisions, defaults to "default".
248 convert.hg.usebranchnames
256 convert.hg.usebranchnames
249 preserve branch names. The default is True.
257 preserve branch names. The default is True.
250
258
251 options:
259 options:
252
260
253 -s --source-type TYPE source repository type
261 -s --source-type TYPE source repository type
254 -d --dest-type TYPE destination repository type
262 -d --dest-type TYPE destination repository type
255 -r --rev REV import up to target revision REV
263 -r --rev REV import up to target revision REV
256 -A --authormap FILE remap usernames using this file
264 -A --authormap FILE remap usernames using this file
257 --filemap FILE remap file names using contents of file
265 --filemap FILE remap file names using contents of file
258 --splicemap FILE splice synthesized history into place
266 --splicemap FILE splice synthesized history into place
259 --branchmap FILE change branch names while converting
267 --branchmap FILE change branch names while converting
260 --branchsort try to sort changesets by branches
268 --branchsort try to sort changesets by branches
261 --datesort try to sort changesets by date
269 --datesort try to sort changesets by date
262 --sourcesort preserve source changesets order
270 --sourcesort preserve source changesets order
263
271
264 use "hg -v help convert" to show the global options
272 use "hg -v help convert" to show the global options
265 $ hg init a
273 $ hg init a
266 $ cd a
274 $ cd a
267 $ echo a > a
275 $ echo a > a
268 $ hg ci -d'0 0' -Ama
276 $ hg ci -d'0 0' -Ama
269 adding a
277 adding a
270 $ hg cp a b
278 $ hg cp a b
271 $ hg ci -d'1 0' -mb
279 $ hg ci -d'1 0' -mb
272 $ hg rm a
280 $ hg rm a
273 $ hg ci -d'2 0' -mc
281 $ hg ci -d'2 0' -mc
274 $ hg mv b a
282 $ hg mv b a
275 $ hg ci -d'3 0' -md
283 $ hg ci -d'3 0' -md
276 $ echo a >> a
284 $ echo a >> a
277 $ hg ci -d'4 0' -me
285 $ hg ci -d'4 0' -me
278 $ cd ..
286 $ cd ..
279 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
287 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
280 assuming destination a-hg
288 assuming destination a-hg
281 initializing destination a-hg repository
289 initializing destination a-hg repository
282 scanning source...
290 scanning source...
283 sorting...
291 sorting...
284 converting...
292 converting...
285 4 a
293 4 a
286 3 b
294 3 b
287 2 c
295 2 c
288 1 d
296 1 d
289 0 e
297 0 e
290 $ hg --cwd a-hg pull ../a
298 $ hg --cwd a-hg pull ../a
291 pulling from ../a
299 pulling from ../a
292 searching for changes
300 searching for changes
293 no changes found
301 no changes found
294
302
295 conversion to existing file should fail
303 conversion to existing file should fail
296
304
297 $ touch bogusfile
305 $ touch bogusfile
298 $ hg convert a bogusfile
306 $ hg convert a bogusfile
299 initializing destination bogusfile repository
307 initializing destination bogusfile repository
300 abort: cannot create new bundle repository
308 abort: cannot create new bundle repository
301 [255]
309 [255]
302
310
303 #if unix-permissions
311 #if unix-permissions
304
312
305 conversion to dir without permissions should fail
313 conversion to dir without permissions should fail
306
314
307 $ mkdir bogusdir
315 $ mkdir bogusdir
308 $ chmod 000 bogusdir
316 $ chmod 000 bogusdir
309
317
310 $ hg convert a bogusdir
318 $ hg convert a bogusdir
311 abort: Permission denied: bogusdir
319 abort: Permission denied: bogusdir
312 [255]
320 [255]
313
321
314 user permissions should succeed
322 user permissions should succeed
315
323
316 $ chmod 700 bogusdir
324 $ chmod 700 bogusdir
317 $ hg convert a bogusdir
325 $ hg convert a bogusdir
318 initializing destination bogusdir repository
326 initializing destination bogusdir repository
319 scanning source...
327 scanning source...
320 sorting...
328 sorting...
321 converting...
329 converting...
322 4 a
330 4 a
323 3 b
331 3 b
324 2 c
332 2 c
325 1 d
333 1 d
326 0 e
334 0 e
327
335
328 #endif
336 #endif
329
337
330 test pre and post conversion actions
338 test pre and post conversion actions
331
339
332 $ echo 'include b' > filemap
340 $ echo 'include b' > filemap
333 $ hg convert --debug --filemap filemap a partialb | \
341 $ hg convert --debug --filemap filemap a partialb | \
334 > grep 'run hg'
342 > grep 'run hg'
335 run hg source pre-conversion action
343 run hg source pre-conversion action
336 run hg sink pre-conversion action
344 run hg sink pre-conversion action
337 run hg sink post-conversion action
345 run hg sink post-conversion action
338 run hg source post-conversion action
346 run hg source post-conversion action
339
347
340 converting empty dir should fail "nicely
348 converting empty dir should fail "nicely
341
349
342 $ mkdir emptydir
350 $ mkdir emptydir
343
351
344 override $PATH to ensure p4 not visible; use $PYTHON in case we're
352 override $PATH to ensure p4 not visible; use $PYTHON in case we're
345 running from a devel copy, not a temp installation
353 running from a devel copy, not a temp installation
346
354
347 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
355 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
348 assuming destination emptydir-hg
356 assuming destination emptydir-hg
349 initializing destination emptydir-hg repository
357 initializing destination emptydir-hg repository
350 emptydir does not look like a CVS checkout
358 emptydir does not look like a CVS checkout
351 emptydir does not look like a Git repository
359 emptydir does not look like a Git repository
352 emptydir does not look like a Subversion repository
360 emptydir does not look like a Subversion repository
353 emptydir is not a local Mercurial repository
361 emptydir is not a local Mercurial repository
354 emptydir does not look like a darcs repository
362 emptydir does not look like a darcs repository
355 emptydir does not look like a monotone repository
363 emptydir does not look like a monotone repository
356 emptydir does not look like a GNU Arch repository
364 emptydir does not look like a GNU Arch repository
357 emptydir does not look like a Bazaar repository
365 emptydir does not look like a Bazaar repository
358 cannot find required "p4" tool
366 cannot find required "p4" tool
359 abort: emptydir: missing or unsupported repository
367 abort: emptydir: missing or unsupported repository
360 [255]
368 [255]
361
369
362 convert with imaginary source type
370 convert with imaginary source type
363
371
364 $ hg convert --source-type foo a a-foo
372 $ hg convert --source-type foo a a-foo
365 initializing destination a-foo repository
373 initializing destination a-foo repository
366 abort: foo: invalid source repository type
374 abort: foo: invalid source repository type
367 [255]
375 [255]
368
376
369 convert with imaginary sink type
377 convert with imaginary sink type
370
378
371 $ hg convert --dest-type foo a a-foo
379 $ hg convert --dest-type foo a a-foo
372 abort: foo: invalid destination repository type
380 abort: foo: invalid destination repository type
373 [255]
381 [255]
374
382
375 testing: convert must not produce duplicate entries in fncache
383 testing: convert must not produce duplicate entries in fncache
376
384
377 $ hg convert a b
385 $ hg convert a b
378 initializing destination b repository
386 initializing destination b repository
379 scanning source...
387 scanning source...
380 sorting...
388 sorting...
381 converting...
389 converting...
382 4 a
390 4 a
383 3 b
391 3 b
384 2 c
392 2 c
385 1 d
393 1 d
386 0 e
394 0 e
387
395
388 contents of fncache file:
396 contents of fncache file:
389
397
390 $ cat b/.hg/store/fncache | sort
398 $ cat b/.hg/store/fncache | sort
391 data/a.i
399 data/a.i
392 data/b.i
400 data/b.i
393
401
394 test bogus URL
402 test bogus URL
395
403
396 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
404 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
397 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
405 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
398 [255]
406 [255]
399
407
400 test revset converted() lookup
408 test revset converted() lookup
401
409
402 $ hg --config convert.hg.saverev=True convert a c
410 $ hg --config convert.hg.saverev=True convert a c
403 initializing destination c repository
411 initializing destination c repository
404 scanning source...
412 scanning source...
405 sorting...
413 sorting...
406 converting...
414 converting...
407 4 a
415 4 a
408 3 b
416 3 b
409 2 c
417 2 c
410 1 d
418 1 d
411 0 e
419 0 e
412 $ echo f > c/f
420 $ echo f > c/f
413 $ hg -R c ci -d'0 0' -Amf
421 $ hg -R c ci -d'0 0' -Amf
414 adding f
422 adding f
415 created new head
423 created new head
416 $ hg -R c log -r "converted(09d945a62ce6)"
424 $ hg -R c log -r "converted(09d945a62ce6)"
417 changeset: 1:98c3dd46a874
425 changeset: 1:98c3dd46a874
418 user: test
426 user: test
419 date: Thu Jan 01 00:00:01 1970 +0000
427 date: Thu Jan 01 00:00:01 1970 +0000
420 summary: b
428 summary: b
421
429
422 $ hg -R c log -r "converted()"
430 $ hg -R c log -r "converted()"
423 changeset: 0:31ed57b2037c
431 changeset: 0:31ed57b2037c
424 user: test
432 user: test
425 date: Thu Jan 01 00:00:00 1970 +0000
433 date: Thu Jan 01 00:00:00 1970 +0000
426 summary: a
434 summary: a
427
435
428 changeset: 1:98c3dd46a874
436 changeset: 1:98c3dd46a874
429 user: test
437 user: test
430 date: Thu Jan 01 00:00:01 1970 +0000
438 date: Thu Jan 01 00:00:01 1970 +0000
431 summary: b
439 summary: b
432
440
433 changeset: 2:3b9ca06ef716
441 changeset: 2:3b9ca06ef716
434 user: test
442 user: test
435 date: Thu Jan 01 00:00:02 1970 +0000
443 date: Thu Jan 01 00:00:02 1970 +0000
436 summary: c
444 summary: c
437
445
438 changeset: 3:4e0debd37cf2
446 changeset: 3:4e0debd37cf2
439 user: test
447 user: test
440 date: Thu Jan 01 00:00:03 1970 +0000
448 date: Thu Jan 01 00:00:03 1970 +0000
441 summary: d
449 summary: d
442
450
443 changeset: 4:9de3bc9349c5
451 changeset: 4:9de3bc9349c5
444 user: test
452 user: test
445 date: Thu Jan 01 00:00:04 1970 +0000
453 date: Thu Jan 01 00:00:04 1970 +0000
446 summary: e
454 summary: e
447
455
General Comments 0
You need to be logged in to leave comments. Login now