##// END OF EJS Templates
convert: register missed subversion config items...
Augie Fackler -
r34891:effae88b default
parent child Browse files
Show More
@@ -1,597 +1,606
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''import revisions from foreign VCS repositories into Mercurial'''
8 '''import revisions from foreign VCS repositories into Mercurial'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 registrar,
14 registrar,
15 )
15 )
16
16
17 from . import (
17 from . import (
18 convcmd,
18 convcmd,
19 cvsps,
19 cvsps,
20 subversion,
20 subversion,
21 )
21 )
22
22
23 cmdtable = {}
23 cmdtable = {}
24 command = registrar.command(cmdtable)
24 command = registrar.command(cmdtable)
25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 # be specifying the version(s) of Mercurial they are tested with, or
27 # be specifying the version(s) of Mercurial they are tested with, or
28 # leave the attribute unspecified.
28 # leave the attribute unspecified.
29 testedwith = 'ships-with-hg-core'
29 testedwith = 'ships-with-hg-core'
30
30
31 configtable = {}
31 configtable = {}
32 configitem = registrar.configitem(configtable)
32 configitem = registrar.configitem(configtable)
33
33
34 configitem('convert', 'cvsps.cache',
34 configitem('convert', 'cvsps.cache',
35 default=True,
35 default=True,
36 )
36 )
37 configitem('convert', 'cvsps.fuzz',
37 configitem('convert', 'cvsps.fuzz',
38 default=60,
38 default=60,
39 )
39 )
40 configitem('convert', 'cvsps.logencoding',
40 configitem('convert', 'cvsps.logencoding',
41 default=None,
41 default=None,
42 )
42 )
43 configitem('convert', 'cvsps.mergefrom',
43 configitem('convert', 'cvsps.mergefrom',
44 default=None,
44 default=None,
45 )
45 )
46 configitem('convert', 'cvsps.mergeto',
46 configitem('convert', 'cvsps.mergeto',
47 default=None,
47 default=None,
48 )
48 )
49 configitem('convert', 'git.committeractions',
49 configitem('convert', 'git.committeractions',
50 default=lambda: ['messagedifferent'],
50 default=lambda: ['messagedifferent'],
51 )
51 )
52 configitem('convert', 'git.extrakeys',
52 configitem('convert', 'git.extrakeys',
53 default=list,
53 default=list,
54 )
54 )
55 configitem('convert', 'git.findcopiesharder',
55 configitem('convert', 'git.findcopiesharder',
56 default=False,
56 default=False,
57 )
57 )
58 configitem('convert', 'git.remoteprefix',
58 configitem('convert', 'git.remoteprefix',
59 default='remote',
59 default='remote',
60 )
60 )
61 configitem('convert', 'git.renamelimit',
61 configitem('convert', 'git.renamelimit',
62 default=400,
62 default=400,
63 )
63 )
64 configitem('convert', 'git.saverev',
64 configitem('convert', 'git.saverev',
65 default=True,
65 default=True,
66 )
66 )
67 configitem('convert', 'git.similarity',
67 configitem('convert', 'git.similarity',
68 default=50,
68 default=50,
69 )
69 )
70 configitem('convert', 'git.skipsubmodules',
70 configitem('convert', 'git.skipsubmodules',
71 default=False,
71 default=False,
72 )
72 )
73 configitem('convert', 'hg.clonebranches',
73 configitem('convert', 'hg.clonebranches',
74 default=False,
74 default=False,
75 )
75 )
76 configitem('convert', 'hg.ignoreerrors',
76 configitem('convert', 'hg.ignoreerrors',
77 default=False,
77 default=False,
78 )
78 )
79 configitem('convert', 'hg.revs',
79 configitem('convert', 'hg.revs',
80 default=None,
80 default=None,
81 )
81 )
82 configitem('convert', 'hg.saverev',
82 configitem('convert', 'hg.saverev',
83 default=False,
83 default=False,
84 )
84 )
85 configitem('convert', 'hg.sourcename',
85 configitem('convert', 'hg.sourcename',
86 default=None,
86 default=None,
87 )
87 )
88 configitem('convert', 'hg.startrev',
88 configitem('convert', 'hg.startrev',
89 default=None,
89 default=None,
90 )
90 )
91 configitem('convert', 'hg.tagsbranch',
91 configitem('convert', 'hg.tagsbranch',
92 default='default',
92 default='default',
93 )
93 )
94 configitem('convert', 'hg.usebranchnames',
94 configitem('convert', 'hg.usebranchnames',
95 default=True,
95 default=True,
96 )
96 )
97 configitem('convert', 'ignoreancestorcheck',
97 configitem('convert', 'ignoreancestorcheck',
98 default=False,
98 default=False,
99 )
99 )
100 configitem('convert', 'localtimezone',
100 configitem('convert', 'localtimezone',
101 default=False,
101 default=False,
102 )
102 )
103 configitem('convert', 'p4.encoding',
103 configitem('convert', 'p4.encoding',
104 default=lambda: convcmd.orig_encoding,
104 default=lambda: convcmd.orig_encoding,
105 )
105 )
106 configitem('convert', 'p4.startrev',
106 configitem('convert', 'p4.startrev',
107 default=0,
107 default=0,
108 )
108 )
109 configitem('convert', 'skiptags',
109 configitem('convert', 'skiptags',
110 default=False,
110 default=False,
111 )
111 )
112 configitem('convert', 'svn.debugsvnlog',
112 configitem('convert', 'svn.debugsvnlog',
113 default=True,
113 default=True,
114 )
114 )
115 configitem('convert', 'svn.trunk',
116 default=None,
117 )
118 configitem('convert', 'svn.tags',
119 default=None,
120 )
121 configitem('convert', 'svn.branches',
122 default=None,
123 )
115 configitem('convert', 'svn.startrev',
124 configitem('convert', 'svn.startrev',
116 default=0,
125 default=0,
117 )
126 )
118
127
119 # Commands definition was moved elsewhere to ease demandload job.
128 # Commands definition was moved elsewhere to ease demandload job.
120
129
121 @command('convert',
130 @command('convert',
122 [('', 'authors', '',
131 [('', 'authors', '',
123 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
132 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
124 _('FILE')),
133 _('FILE')),
125 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
134 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
126 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
135 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
127 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
136 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
128 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
137 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
129 ('', 'filemap', '', _('remap file names using contents of file'),
138 ('', 'filemap', '', _('remap file names using contents of file'),
130 _('FILE')),
139 _('FILE')),
131 ('', 'full', None,
140 ('', 'full', None,
132 _('apply filemap changes by converting all files again')),
141 _('apply filemap changes by converting all files again')),
133 ('', 'splicemap', '', _('splice synthesized history into place'),
142 ('', 'splicemap', '', _('splice synthesized history into place'),
134 _('FILE')),
143 _('FILE')),
135 ('', 'branchmap', '', _('change branch names while converting'),
144 ('', 'branchmap', '', _('change branch names while converting'),
136 _('FILE')),
145 _('FILE')),
137 ('', 'branchsort', None, _('try to sort changesets by branches')),
146 ('', 'branchsort', None, _('try to sort changesets by branches')),
138 ('', 'datesort', None, _('try to sort changesets by date')),
147 ('', 'datesort', None, _('try to sort changesets by date')),
139 ('', 'sourcesort', None, _('preserve source changesets order')),
148 ('', 'sourcesort', None, _('preserve source changesets order')),
140 ('', 'closesort', None, _('try to reorder closed revisions'))],
149 ('', 'closesort', None, _('try to reorder closed revisions'))],
141 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
150 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
142 norepo=True)
151 norepo=True)
143 def convert(ui, src, dest=None, revmapfile=None, **opts):
152 def convert(ui, src, dest=None, revmapfile=None, **opts):
144 """convert a foreign SCM repository to a Mercurial one.
153 """convert a foreign SCM repository to a Mercurial one.
145
154
146 Accepted source formats [identifiers]:
155 Accepted source formats [identifiers]:
147
156
148 - Mercurial [hg]
157 - Mercurial [hg]
149 - CVS [cvs]
158 - CVS [cvs]
150 - Darcs [darcs]
159 - Darcs [darcs]
151 - git [git]
160 - git [git]
152 - Subversion [svn]
161 - Subversion [svn]
153 - Monotone [mtn]
162 - Monotone [mtn]
154 - GNU Arch [gnuarch]
163 - GNU Arch [gnuarch]
155 - Bazaar [bzr]
164 - Bazaar [bzr]
156 - Perforce [p4]
165 - Perforce [p4]
157
166
158 Accepted destination formats [identifiers]:
167 Accepted destination formats [identifiers]:
159
168
160 - Mercurial [hg]
169 - Mercurial [hg]
161 - Subversion [svn] (history on branches is not preserved)
170 - Subversion [svn] (history on branches is not preserved)
162
171
163 If no revision is given, all revisions will be converted.
172 If no revision is given, all revisions will be converted.
164 Otherwise, convert will only import up to the named revision
173 Otherwise, convert will only import up to the named revision
165 (given in a format understood by the source).
174 (given in a format understood by the source).
166
175
167 If no destination directory name is specified, it defaults to the
176 If no destination directory name is specified, it defaults to the
168 basename of the source with ``-hg`` appended. If the destination
177 basename of the source with ``-hg`` appended. If the destination
169 repository doesn't exist, it will be created.
178 repository doesn't exist, it will be created.
170
179
171 By default, all sources except Mercurial will use --branchsort.
180 By default, all sources except Mercurial will use --branchsort.
172 Mercurial uses --sourcesort to preserve original revision numbers
181 Mercurial uses --sourcesort to preserve original revision numbers
173 order. Sort modes have the following effects:
182 order. Sort modes have the following effects:
174
183
175 --branchsort convert from parent to child revision when possible,
184 --branchsort convert from parent to child revision when possible,
176 which means branches are usually converted one after
185 which means branches are usually converted one after
177 the other. It generates more compact repositories.
186 the other. It generates more compact repositories.
178
187
179 --datesort sort revisions by date. Converted repositories have
188 --datesort sort revisions by date. Converted repositories have
180 good-looking changelogs but are often an order of
189 good-looking changelogs but are often an order of
181 magnitude larger than the same ones generated by
190 magnitude larger than the same ones generated by
182 --branchsort.
191 --branchsort.
183
192
184 --sourcesort try to preserve source revisions order, only
193 --sourcesort try to preserve source revisions order, only
185 supported by Mercurial sources.
194 supported by Mercurial sources.
186
195
187 --closesort try to move closed revisions as close as possible
196 --closesort try to move closed revisions as close as possible
188 to parent branches, only supported by Mercurial
197 to parent branches, only supported by Mercurial
189 sources.
198 sources.
190
199
191 If ``REVMAP`` isn't given, it will be put in a default location
200 If ``REVMAP`` isn't given, it will be put in a default location
192 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
201 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
193 text file that maps each source commit ID to the destination ID
202 text file that maps each source commit ID to the destination ID
194 for that revision, like so::
203 for that revision, like so::
195
204
196 <source ID> <destination ID>
205 <source ID> <destination ID>
197
206
198 If the file doesn't exist, it's automatically created. It's
207 If the file doesn't exist, it's automatically created. It's
199 updated on each commit copied, so :hg:`convert` can be interrupted
208 updated on each commit copied, so :hg:`convert` can be interrupted
200 and can be run repeatedly to copy new commits.
209 and can be run repeatedly to copy new commits.
201
210
202 The authormap is a simple text file that maps each source commit
211 The authormap is a simple text file that maps each source commit
203 author to a destination commit author. It is handy for source SCMs
212 author to a destination commit author. It is handy for source SCMs
204 that use unix logins to identify authors (e.g.: CVS). One line per
213 that use unix logins to identify authors (e.g.: CVS). One line per
205 author mapping and the line format is::
214 author mapping and the line format is::
206
215
207 source author = destination author
216 source author = destination author
208
217
209 Empty lines and lines starting with a ``#`` are ignored.
218 Empty lines and lines starting with a ``#`` are ignored.
210
219
211 The filemap is a file that allows filtering and remapping of files
220 The filemap is a file that allows filtering and remapping of files
212 and directories. Each line can contain one of the following
221 and directories. Each line can contain one of the following
213 directives::
222 directives::
214
223
215 include path/to/file-or-dir
224 include path/to/file-or-dir
216
225
217 exclude path/to/file-or-dir
226 exclude path/to/file-or-dir
218
227
219 rename path/to/source path/to/destination
228 rename path/to/source path/to/destination
220
229
221 Comment lines start with ``#``. A specified path matches if it
230 Comment lines start with ``#``. A specified path matches if it
222 equals the full relative name of a file or one of its parent
231 equals the full relative name of a file or one of its parent
223 directories. The ``include`` or ``exclude`` directive with the
232 directories. The ``include`` or ``exclude`` directive with the
224 longest matching path applies, so line order does not matter.
233 longest matching path applies, so line order does not matter.
225
234
226 The ``include`` directive causes a file, or all files under a
235 The ``include`` directive causes a file, or all files under a
227 directory, to be included in the destination repository. The default
236 directory, to be included in the destination repository. The default
228 if there are no ``include`` statements is to include everything.
237 if there are no ``include`` statements is to include everything.
229 If there are any ``include`` statements, nothing else is included.
238 If there are any ``include`` statements, nothing else is included.
230 The ``exclude`` directive causes files or directories to
239 The ``exclude`` directive causes files or directories to
231 be omitted. The ``rename`` directive renames a file or directory if
240 be omitted. The ``rename`` directive renames a file or directory if
232 it is converted. To rename from a subdirectory into the root of
241 it is converted. To rename from a subdirectory into the root of
233 the repository, use ``.`` as the path to rename to.
242 the repository, use ``.`` as the path to rename to.
234
243
235 ``--full`` will make sure the converted changesets contain exactly
244 ``--full`` will make sure the converted changesets contain exactly
236 the right files with the right content. It will make a full
245 the right files with the right content. It will make a full
237 conversion of all files, not just the ones that have
246 conversion of all files, not just the ones that have
238 changed. Files that already are correct will not be changed. This
247 changed. Files that already are correct will not be changed. This
239 can be used to apply filemap changes when converting
248 can be used to apply filemap changes when converting
240 incrementally. This is currently only supported for Mercurial and
249 incrementally. This is currently only supported for Mercurial and
241 Subversion.
250 Subversion.
242
251
243 The splicemap is a file that allows insertion of synthetic
252 The splicemap is a file that allows insertion of synthetic
244 history, letting you specify the parents of a revision. This is
253 history, letting you specify the parents of a revision. This is
245 useful if you want to e.g. give a Subversion merge two parents, or
254 useful if you want to e.g. give a Subversion merge two parents, or
246 graft two disconnected series of history together. Each entry
255 graft two disconnected series of history together. Each entry
247 contains a key, followed by a space, followed by one or two
256 contains a key, followed by a space, followed by one or two
248 comma-separated values::
257 comma-separated values::
249
258
250 key parent1, parent2
259 key parent1, parent2
251
260
252 The key is the revision ID in the source
261 The key is the revision ID in the source
253 revision control system whose parents should be modified (same
262 revision control system whose parents should be modified (same
254 format as a key in .hg/shamap). The values are the revision IDs
263 format as a key in .hg/shamap). The values are the revision IDs
255 (in either the source or destination revision control system) that
264 (in either the source or destination revision control system) that
256 should be used as the new parents for that node. For example, if
265 should be used as the new parents for that node. For example, if
257 you have merged "release-1.0" into "trunk", then you should
266 you have merged "release-1.0" into "trunk", then you should
258 specify the revision on "trunk" as the first parent and the one on
267 specify the revision on "trunk" as the first parent and the one on
259 the "release-1.0" branch as the second.
268 the "release-1.0" branch as the second.
260
269
261 The branchmap is a file that allows you to rename a branch when it is
270 The branchmap is a file that allows you to rename a branch when it is
262 being brought in from whatever external repository. When used in
271 being brought in from whatever external repository. When used in
263 conjunction with a splicemap, it allows for a powerful combination
272 conjunction with a splicemap, it allows for a powerful combination
264 to help fix even the most badly mismanaged repositories and turn them
273 to help fix even the most badly mismanaged repositories and turn them
265 into nicely structured Mercurial repositories. The branchmap contains
274 into nicely structured Mercurial repositories. The branchmap contains
266 lines of the form::
275 lines of the form::
267
276
268 original_branch_name new_branch_name
277 original_branch_name new_branch_name
269
278
270 where "original_branch_name" is the name of the branch in the
279 where "original_branch_name" is the name of the branch in the
271 source repository, and "new_branch_name" is the name of the branch
280 source repository, and "new_branch_name" is the name of the branch
272 is the destination repository. No whitespace is allowed in the new
281 is the destination repository. No whitespace is allowed in the new
273 branch name. This can be used to (for instance) move code in one
282 branch name. This can be used to (for instance) move code in one
274 repository from "default" to a named branch.
283 repository from "default" to a named branch.
275
284
276 Mercurial Source
285 Mercurial Source
277 ################
286 ################
278
287
279 The Mercurial source recognizes the following configuration
288 The Mercurial source recognizes the following configuration
280 options, which you can set on the command line with ``--config``:
289 options, which you can set on the command line with ``--config``:
281
290
282 :convert.hg.ignoreerrors: ignore integrity errors when reading.
291 :convert.hg.ignoreerrors: ignore integrity errors when reading.
283 Use it to fix Mercurial repositories with missing revlogs, by
292 Use it to fix Mercurial repositories with missing revlogs, by
284 converting from and to Mercurial. Default is False.
293 converting from and to Mercurial. Default is False.
285
294
286 :convert.hg.saverev: store original revision ID in changeset
295 :convert.hg.saverev: store original revision ID in changeset
287 (forces target IDs to change). It takes a boolean argument and
296 (forces target IDs to change). It takes a boolean argument and
288 defaults to False.
297 defaults to False.
289
298
290 :convert.hg.startrev: specify the initial Mercurial revision.
299 :convert.hg.startrev: specify the initial Mercurial revision.
291 The default is 0.
300 The default is 0.
292
301
293 :convert.hg.revs: revset specifying the source revisions to convert.
302 :convert.hg.revs: revset specifying the source revisions to convert.
294
303
295 CVS Source
304 CVS Source
296 ##########
305 ##########
297
306
298 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
307 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
299 to indicate the starting point of what will be converted. Direct
308 to indicate the starting point of what will be converted. Direct
300 access to the repository files is not needed, unless of course the
309 access to the repository files is not needed, unless of course the
301 repository is ``:local:``. The conversion uses the top level
310 repository is ``:local:``. The conversion uses the top level
302 directory in the sandbox to find the CVS repository, and then uses
311 directory in the sandbox to find the CVS repository, and then uses
303 CVS rlog commands to find files to convert. This means that unless
312 CVS rlog commands to find files to convert. This means that unless
304 a filemap is given, all files under the starting directory will be
313 a filemap is given, all files under the starting directory will be
305 converted, and that any directory reorganization in the CVS
314 converted, and that any directory reorganization in the CVS
306 sandbox is ignored.
315 sandbox is ignored.
307
316
308 The following options can be used with ``--config``:
317 The following options can be used with ``--config``:
309
318
310 :convert.cvsps.cache: Set to False to disable remote log caching,
319 :convert.cvsps.cache: Set to False to disable remote log caching,
311 for testing and debugging purposes. Default is True.
320 for testing and debugging purposes. Default is True.
312
321
313 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
322 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
314 allowed between commits with identical user and log message in
323 allowed between commits with identical user and log message in
315 a single changeset. When very large files were checked in as
324 a single changeset. When very large files were checked in as
316 part of a changeset then the default may not be long enough.
325 part of a changeset then the default may not be long enough.
317 The default is 60.
326 The default is 60.
318
327
319 :convert.cvsps.logencoding: Specify encoding name to be used for
328 :convert.cvsps.logencoding: Specify encoding name to be used for
320 transcoding CVS log messages. Multiple encoding names can be
329 transcoding CVS log messages. Multiple encoding names can be
321 specified as a list (see :hg:`help config.Syntax`), but only
330 specified as a list (see :hg:`help config.Syntax`), but only
322 the first acceptable encoding in the list is used per CVS log
331 the first acceptable encoding in the list is used per CVS log
323 entries. This transcoding is executed before cvslog hook below.
332 entries. This transcoding is executed before cvslog hook below.
324
333
325 :convert.cvsps.mergeto: Specify a regular expression to which
334 :convert.cvsps.mergeto: Specify a regular expression to which
326 commit log messages are matched. If a match occurs, then the
335 commit log messages are matched. If a match occurs, then the
327 conversion process will insert a dummy revision merging the
336 conversion process will insert a dummy revision merging the
328 branch on which this log message occurs to the branch
337 branch on which this log message occurs to the branch
329 indicated in the regex. Default is ``{{mergetobranch
338 indicated in the regex. Default is ``{{mergetobranch
330 ([-\\w]+)}}``
339 ([-\\w]+)}}``
331
340
332 :convert.cvsps.mergefrom: Specify a regular expression to which
341 :convert.cvsps.mergefrom: Specify a regular expression to which
333 commit log messages are matched. If a match occurs, then the
342 commit log messages are matched. If a match occurs, then the
334 conversion process will add the most recent revision on the
343 conversion process will add the most recent revision on the
335 branch indicated in the regex as the second parent of the
344 branch indicated in the regex as the second parent of the
336 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
345 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
337
346
338 :convert.localtimezone: use local time (as determined by the TZ
347 :convert.localtimezone: use local time (as determined by the TZ
339 environment variable) for changeset date/times. The default
348 environment variable) for changeset date/times. The default
340 is False (use UTC).
349 is False (use UTC).
341
350
342 :hooks.cvslog: Specify a Python function to be called at the end of
351 :hooks.cvslog: Specify a Python function to be called at the end of
343 gathering the CVS log. The function is passed a list with the
352 gathering the CVS log. The function is passed a list with the
344 log entries, and can modify the entries in-place, or add or
353 log entries, and can modify the entries in-place, or add or
345 delete them.
354 delete them.
346
355
347 :hooks.cvschangesets: Specify a Python function to be called after
356 :hooks.cvschangesets: Specify a Python function to be called after
348 the changesets are calculated from the CVS log. The
357 the changesets are calculated from the CVS log. The
349 function is passed a list with the changeset entries, and can
358 function is passed a list with the changeset entries, and can
350 modify the changesets in-place, or add or delete them.
359 modify the changesets in-place, or add or delete them.
351
360
352 An additional "debugcvsps" Mercurial command allows the builtin
361 An additional "debugcvsps" Mercurial command allows the builtin
353 changeset merging code to be run without doing a conversion. Its
362 changeset merging code to be run without doing a conversion. Its
354 parameters and output are similar to that of cvsps 2.1. Please see
363 parameters and output are similar to that of cvsps 2.1. Please see
355 the command help for more details.
364 the command help for more details.
356
365
357 Subversion Source
366 Subversion Source
358 #################
367 #################
359
368
360 Subversion source detects classical trunk/branches/tags layouts.
369 Subversion source detects classical trunk/branches/tags layouts.
361 By default, the supplied ``svn://repo/path/`` source URL is
370 By default, the supplied ``svn://repo/path/`` source URL is
362 converted as a single branch. If ``svn://repo/path/trunk`` exists
371 converted as a single branch. If ``svn://repo/path/trunk`` exists
363 it replaces the default branch. If ``svn://repo/path/branches``
372 it replaces the default branch. If ``svn://repo/path/branches``
364 exists, its subdirectories are listed as possible branches. If
373 exists, its subdirectories are listed as possible branches. If
365 ``svn://repo/path/tags`` exists, it is looked for tags referencing
374 ``svn://repo/path/tags`` exists, it is looked for tags referencing
366 converted branches. Default ``trunk``, ``branches`` and ``tags``
375 converted branches. Default ``trunk``, ``branches`` and ``tags``
367 values can be overridden with following options. Set them to paths
376 values can be overridden with following options. Set them to paths
368 relative to the source URL, or leave them blank to disable auto
377 relative to the source URL, or leave them blank to disable auto
369 detection.
378 detection.
370
379
371 The following options can be set with ``--config``:
380 The following options can be set with ``--config``:
372
381
373 :convert.svn.branches: specify the directory containing branches.
382 :convert.svn.branches: specify the directory containing branches.
374 The default is ``branches``.
383 The default is ``branches``.
375
384
376 :convert.svn.tags: specify the directory containing tags. The
385 :convert.svn.tags: specify the directory containing tags. The
377 default is ``tags``.
386 default is ``tags``.
378
387
379 :convert.svn.trunk: specify the name of the trunk branch. The
388 :convert.svn.trunk: specify the name of the trunk branch. The
380 default is ``trunk``.
389 default is ``trunk``.
381
390
382 :convert.localtimezone: use local time (as determined by the TZ
391 :convert.localtimezone: use local time (as determined by the TZ
383 environment variable) for changeset date/times. The default
392 environment variable) for changeset date/times. The default
384 is False (use UTC).
393 is False (use UTC).
385
394
386 Source history can be retrieved starting at a specific revision,
395 Source history can be retrieved starting at a specific revision,
387 instead of being integrally converted. Only single branch
396 instead of being integrally converted. Only single branch
388 conversions are supported.
397 conversions are supported.
389
398
390 :convert.svn.startrev: specify start Subversion revision number.
399 :convert.svn.startrev: specify start Subversion revision number.
391 The default is 0.
400 The default is 0.
392
401
393 Git Source
402 Git Source
394 ##########
403 ##########
395
404
396 The Git importer converts commits from all reachable branches (refs
405 The Git importer converts commits from all reachable branches (refs
397 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
406 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
398 Branches are converted to bookmarks with the same name, with the
407 Branches are converted to bookmarks with the same name, with the
399 leading 'refs/heads' stripped. Git submodules are converted to Git
408 leading 'refs/heads' stripped. Git submodules are converted to Git
400 subrepos in Mercurial.
409 subrepos in Mercurial.
401
410
402 The following options can be set with ``--config``:
411 The following options can be set with ``--config``:
403
412
404 :convert.git.similarity: specify how similar files modified in a
413 :convert.git.similarity: specify how similar files modified in a
405 commit must be to be imported as renames or copies, as a
414 commit must be to be imported as renames or copies, as a
406 percentage between ``0`` (disabled) and ``100`` (files must be
415 percentage between ``0`` (disabled) and ``100`` (files must be
407 identical). For example, ``90`` means that a delete/add pair will
416 identical). For example, ``90`` means that a delete/add pair will
408 be imported as a rename if more than 90% of the file hasn't
417 be imported as a rename if more than 90% of the file hasn't
409 changed. The default is ``50``.
418 changed. The default is ``50``.
410
419
411 :convert.git.findcopiesharder: while detecting copies, look at all
420 :convert.git.findcopiesharder: while detecting copies, look at all
412 files in the working copy instead of just changed ones. This
421 files in the working copy instead of just changed ones. This
413 is very expensive for large projects, and is only effective when
422 is very expensive for large projects, and is only effective when
414 ``convert.git.similarity`` is greater than 0. The default is False.
423 ``convert.git.similarity`` is greater than 0. The default is False.
415
424
416 :convert.git.renamelimit: perform rename and copy detection up to this
425 :convert.git.renamelimit: perform rename and copy detection up to this
417 many changed files in a commit. Increasing this will make rename
426 many changed files in a commit. Increasing this will make rename
418 and copy detection more accurate but will significantly slow down
427 and copy detection more accurate but will significantly slow down
419 computation on large projects. The option is only relevant if
428 computation on large projects. The option is only relevant if
420 ``convert.git.similarity`` is greater than 0. The default is
429 ``convert.git.similarity`` is greater than 0. The default is
421 ``400``.
430 ``400``.
422
431
423 :convert.git.committeractions: list of actions to take when processing
432 :convert.git.committeractions: list of actions to take when processing
424 author and committer values.
433 author and committer values.
425
434
426 Git commits have separate author (who wrote the commit) and committer
435 Git commits have separate author (who wrote the commit) and committer
427 (who applied the commit) fields. Not all destinations support separate
436 (who applied the commit) fields. Not all destinations support separate
428 author and committer fields (including Mercurial). This config option
437 author and committer fields (including Mercurial). This config option
429 controls what to do with these author and committer fields during
438 controls what to do with these author and committer fields during
430 conversion.
439 conversion.
431
440
432 A value of ``messagedifferent`` will append a ``committer: ...``
441 A value of ``messagedifferent`` will append a ``committer: ...``
433 line to the commit message if the Git committer is different from the
442 line to the commit message if the Git committer is different from the
434 author. The prefix of that line can be specified using the syntax
443 author. The prefix of that line can be specified using the syntax
435 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
444 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
436 When a prefix is specified, a space will always be inserted between the
445 When a prefix is specified, a space will always be inserted between the
437 prefix and the value.
446 prefix and the value.
438
447
439 ``messagealways`` behaves like ``messagedifferent`` except it will
448 ``messagealways`` behaves like ``messagedifferent`` except it will
440 always result in a ``committer: ...`` line being appended to the commit
449 always result in a ``committer: ...`` line being appended to the commit
441 message. This value is mutually exclusive with ``messagedifferent``.
450 message. This value is mutually exclusive with ``messagedifferent``.
442
451
443 ``dropcommitter`` will remove references to the committer. Only
452 ``dropcommitter`` will remove references to the committer. Only
444 references to the author will remain. Actions that add references
453 references to the author will remain. Actions that add references
445 to the committer will have no effect when this is set.
454 to the committer will have no effect when this is set.
446
455
447 ``replaceauthor`` will replace the value of the author field with
456 ``replaceauthor`` will replace the value of the author field with
448 the committer. Other actions that add references to the committer
457 the committer. Other actions that add references to the committer
449 will still take effect when this is set.
458 will still take effect when this is set.
450
459
451 The default is ``messagedifferent``.
460 The default is ``messagedifferent``.
452
461
453 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
462 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
454 the destination. Some Git repositories store extra metadata in commits.
463 the destination. Some Git repositories store extra metadata in commits.
455 By default, this non-default metadata will be lost during conversion.
464 By default, this non-default metadata will be lost during conversion.
456 Setting this config option can retain that metadata. Some built-in
465 Setting this config option can retain that metadata. Some built-in
457 keys such as ``parent`` and ``branch`` are not allowed to be copied.
466 keys such as ``parent`` and ``branch`` are not allowed to be copied.
458
467
459 :convert.git.remoteprefix: remote refs are converted as bookmarks with
468 :convert.git.remoteprefix: remote refs are converted as bookmarks with
460 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
469 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
461 is 'remote'.
470 is 'remote'.
462
471
463 :convert.git.saverev: whether to store the original Git commit ID in the
472 :convert.git.saverev: whether to store the original Git commit ID in the
464 metadata of the destination commit. The default is True.
473 metadata of the destination commit. The default is True.
465
474
466 :convert.git.skipsubmodules: does not convert root level .gitmodules files
475 :convert.git.skipsubmodules: does not convert root level .gitmodules files
467 or files with 160000 mode indicating a submodule. Default is False.
476 or files with 160000 mode indicating a submodule. Default is False.
468
477
469 Perforce Source
478 Perforce Source
470 ###############
479 ###############
471
480
472 The Perforce (P4) importer can be given a p4 depot path or a
481 The Perforce (P4) importer can be given a p4 depot path or a
473 client specification as source. It will convert all files in the
482 client specification as source. It will convert all files in the
474 source to a flat Mercurial repository, ignoring labels, branches
483 source to a flat Mercurial repository, ignoring labels, branches
475 and integrations. Note that when a depot path is given you then
484 and integrations. Note that when a depot path is given you then
476 usually should specify a target directory, because otherwise the
485 usually should specify a target directory, because otherwise the
477 target may be named ``...-hg``.
486 target may be named ``...-hg``.
478
487
479 The following options can be set with ``--config``:
488 The following options can be set with ``--config``:
480
489
481 :convert.p4.encoding: specify the encoding to use when decoding standard
490 :convert.p4.encoding: specify the encoding to use when decoding standard
482 output of the Perforce command line tool. The default is default system
491 output of the Perforce command line tool. The default is default system
483 encoding.
492 encoding.
484
493
485 :convert.p4.startrev: specify initial Perforce revision (a
494 :convert.p4.startrev: specify initial Perforce revision (a
486 Perforce changelist number).
495 Perforce changelist number).
487
496
488 Mercurial Destination
497 Mercurial Destination
489 #####################
498 #####################
490
499
491 The Mercurial destination will recognize Mercurial subrepositories in the
500 The Mercurial destination will recognize Mercurial subrepositories in the
492 destination directory, and update the .hgsubstate file automatically if the
501 destination directory, and update the .hgsubstate file automatically if the
493 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
502 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
494 Converting a repository with subrepositories requires converting a single
503 Converting a repository with subrepositories requires converting a single
495 repository at a time, from the bottom up.
504 repository at a time, from the bottom up.
496
505
497 .. container:: verbose
506 .. container:: verbose
498
507
499 An example showing how to convert a repository with subrepositories::
508 An example showing how to convert a repository with subrepositories::
500
509
501 # so convert knows the type when it sees a non empty destination
510 # so convert knows the type when it sees a non empty destination
502 $ hg init converted
511 $ hg init converted
503
512
504 $ hg convert orig/sub1 converted/sub1
513 $ hg convert orig/sub1 converted/sub1
505 $ hg convert orig/sub2 converted/sub2
514 $ hg convert orig/sub2 converted/sub2
506 $ hg convert orig converted
515 $ hg convert orig converted
507
516
508 The following options are supported:
517 The following options are supported:
509
518
510 :convert.hg.clonebranches: dispatch source branches in separate
519 :convert.hg.clonebranches: dispatch source branches in separate
511 clones. The default is False.
520 clones. The default is False.
512
521
513 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
522 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
514 ``default``.
523 ``default``.
515
524
516 :convert.hg.usebranchnames: preserve branch names. The default is
525 :convert.hg.usebranchnames: preserve branch names. The default is
517 True.
526 True.
518
527
519 :convert.hg.sourcename: records the given string as a 'convert_source' extra
528 :convert.hg.sourcename: records the given string as a 'convert_source' extra
520 value on each commit made in the target repository. The default is None.
529 value on each commit made in the target repository. The default is None.
521
530
522 All Destinations
531 All Destinations
523 ################
532 ################
524
533
525 All destination types accept the following options:
534 All destination types accept the following options:
526
535
527 :convert.skiptags: does not convert tags from the source repo to the target
536 :convert.skiptags: does not convert tags from the source repo to the target
528 repo. The default is False.
537 repo. The default is False.
529 """
538 """
530 return convcmd.convert(ui, src, dest, revmapfile, **opts)
539 return convcmd.convert(ui, src, dest, revmapfile, **opts)
531
540
532 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
541 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
533 def debugsvnlog(ui, **opts):
542 def debugsvnlog(ui, **opts):
534 return subversion.debugsvnlog(ui, **opts)
543 return subversion.debugsvnlog(ui, **opts)
535
544
536 @command('debugcvsps',
545 @command('debugcvsps',
537 [
546 [
538 # Main options shared with cvsps-2.1
547 # Main options shared with cvsps-2.1
539 ('b', 'branches', [], _('only return changes on specified branches')),
548 ('b', 'branches', [], _('only return changes on specified branches')),
540 ('p', 'prefix', '', _('prefix to remove from file names')),
549 ('p', 'prefix', '', _('prefix to remove from file names')),
541 ('r', 'revisions', [],
550 ('r', 'revisions', [],
542 _('only return changes after or between specified tags')),
551 _('only return changes after or between specified tags')),
543 ('u', 'update-cache', None, _("update cvs log cache")),
552 ('u', 'update-cache', None, _("update cvs log cache")),
544 ('x', 'new-cache', None, _("create new cvs log cache")),
553 ('x', 'new-cache', None, _("create new cvs log cache")),
545 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
554 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
546 ('', 'root', '', _('specify cvsroot')),
555 ('', 'root', '', _('specify cvsroot')),
547 # Options specific to builtin cvsps
556 # Options specific to builtin cvsps
548 ('', 'parents', '', _('show parent changesets')),
557 ('', 'parents', '', _('show parent changesets')),
549 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
558 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
550 # Options that are ignored for compatibility with cvsps-2.1
559 # Options that are ignored for compatibility with cvsps-2.1
551 ('A', 'cvs-direct', None, _('ignored for compatibility')),
560 ('A', 'cvs-direct', None, _('ignored for compatibility')),
552 ],
561 ],
553 _('hg debugcvsps [OPTION]... [PATH]...'),
562 _('hg debugcvsps [OPTION]... [PATH]...'),
554 norepo=True)
563 norepo=True)
555 def debugcvsps(ui, *args, **opts):
564 def debugcvsps(ui, *args, **opts):
556 '''create changeset information from CVS
565 '''create changeset information from CVS
557
566
558 This command is intended as a debugging tool for the CVS to
567 This command is intended as a debugging tool for the CVS to
559 Mercurial converter, and can be used as a direct replacement for
568 Mercurial converter, and can be used as a direct replacement for
560 cvsps.
569 cvsps.
561
570
562 Hg debugcvsps reads the CVS rlog for current directory (or any
571 Hg debugcvsps reads the CVS rlog for current directory (or any
563 named directory) in the CVS repository, and converts the log to a
572 named directory) in the CVS repository, and converts the log to a
564 series of changesets based on matching commit log entries and
573 series of changesets based on matching commit log entries and
565 dates.'''
574 dates.'''
566 return cvsps.debugcvsps(ui, *args, **opts)
575 return cvsps.debugcvsps(ui, *args, **opts)
567
576
568 def kwconverted(ctx, name):
577 def kwconverted(ctx, name):
569 rev = ctx.extra().get('convert_revision', '')
578 rev = ctx.extra().get('convert_revision', '')
570 if rev.startswith('svn:'):
579 if rev.startswith('svn:'):
571 if name == 'svnrev':
580 if name == 'svnrev':
572 return str(subversion.revsplit(rev)[2])
581 return str(subversion.revsplit(rev)[2])
573 elif name == 'svnpath':
582 elif name == 'svnpath':
574 return subversion.revsplit(rev)[1]
583 return subversion.revsplit(rev)[1]
575 elif name == 'svnuuid':
584 elif name == 'svnuuid':
576 return subversion.revsplit(rev)[0]
585 return subversion.revsplit(rev)[0]
577 return rev
586 return rev
578
587
579 templatekeyword = registrar.templatekeyword()
588 templatekeyword = registrar.templatekeyword()
580
589
581 @templatekeyword('svnrev')
590 @templatekeyword('svnrev')
582 def kwsvnrev(repo, ctx, **args):
591 def kwsvnrev(repo, ctx, **args):
583 """String. Converted subversion revision number."""
592 """String. Converted subversion revision number."""
584 return kwconverted(ctx, 'svnrev')
593 return kwconverted(ctx, 'svnrev')
585
594
586 @templatekeyword('svnpath')
595 @templatekeyword('svnpath')
587 def kwsvnpath(repo, ctx, **args):
596 def kwsvnpath(repo, ctx, **args):
588 """String. Converted subversion revision project path."""
597 """String. Converted subversion revision project path."""
589 return kwconverted(ctx, 'svnpath')
598 return kwconverted(ctx, 'svnpath')
590
599
591 @templatekeyword('svnuuid')
600 @templatekeyword('svnuuid')
592 def kwsvnuuid(repo, ctx, **args):
601 def kwsvnuuid(repo, ctx, **args):
593 """String. Converted subversion revision repository identifier."""
602 """String. Converted subversion revision repository identifier."""
594 return kwconverted(ctx, 'svnuuid')
603 return kwconverted(ctx, 'svnuuid')
595
604
596 # tell hggettext to extract docstrings from these functions:
605 # tell hggettext to extract docstrings from these functions:
597 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
606 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,1353 +1,1355
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 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import os
6 import os
7 import re
7 import re
8 import tempfile
8 import tempfile
9 import xml.dom.minidom
9 import xml.dom.minidom
10
10
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 from mercurial import (
12 from mercurial import (
13 encoding,
13 encoding,
14 error,
14 error,
15 pycompat,
15 pycompat,
16 util,
16 util,
17 vfs as vfsmod,
17 vfs as vfsmod,
18 )
18 )
19
19
20 from . import common
20 from . import common
21
21
22 pickle = util.pickle
22 pickle = util.pickle
23 stringio = util.stringio
23 stringio = util.stringio
24 propertycache = util.propertycache
24 propertycache = util.propertycache
25 urlerr = util.urlerr
25 urlerr = util.urlerr
26 urlreq = util.urlreq
26 urlreq = util.urlreq
27
27
28 commandline = common.commandline
28 commandline = common.commandline
29 commit = common.commit
29 commit = common.commit
30 converter_sink = common.converter_sink
30 converter_sink = common.converter_sink
31 converter_source = common.converter_source
31 converter_source = common.converter_source
32 decodeargs = common.decodeargs
32 decodeargs = common.decodeargs
33 encodeargs = common.encodeargs
33 encodeargs = common.encodeargs
34 makedatetimestamp = common.makedatetimestamp
34 makedatetimestamp = common.makedatetimestamp
35 mapfile = common.mapfile
35 mapfile = common.mapfile
36 MissingTool = common.MissingTool
36 MissingTool = common.MissingTool
37 NoRepo = common.NoRepo
37 NoRepo = common.NoRepo
38
38
39 # Subversion stuff. Works best with very recent Python SVN bindings
39 # Subversion stuff. Works best with very recent Python SVN bindings
40 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
40 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
41 # these bindings.
41 # these bindings.
42
42
43 try:
43 try:
44 import svn
44 import svn
45 import svn.client
45 import svn.client
46 import svn.core
46 import svn.core
47 import svn.ra
47 import svn.ra
48 import svn.delta
48 import svn.delta
49 from . import transport
49 from . import transport
50 import warnings
50 import warnings
51 warnings.filterwarnings('ignore',
51 warnings.filterwarnings('ignore',
52 module='svn.core',
52 module='svn.core',
53 category=DeprecationWarning)
53 category=DeprecationWarning)
54 svn.core.SubversionException # trigger import to catch error
54 svn.core.SubversionException # trigger import to catch error
55
55
56 except ImportError:
56 except ImportError:
57 svn = None
57 svn = None
58
58
59 class SvnPathNotFound(Exception):
59 class SvnPathNotFound(Exception):
60 pass
60 pass
61
61
62 def revsplit(rev):
62 def revsplit(rev):
63 """Parse a revision string and return (uuid, path, revnum).
63 """Parse a revision string and return (uuid, path, revnum).
64 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
64 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
65 ... b'/proj%20B/mytrunk/mytrunk@1')
65 ... b'/proj%20B/mytrunk/mytrunk@1')
66 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
66 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
67 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
67 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
68 ('', '', 1)
68 ('', '', 1)
69 >>> revsplit(b'@7')
69 >>> revsplit(b'@7')
70 ('', '', 7)
70 ('', '', 7)
71 >>> revsplit(b'7')
71 >>> revsplit(b'7')
72 ('', '', 0)
72 ('', '', 0)
73 >>> revsplit(b'bad')
73 >>> revsplit(b'bad')
74 ('', '', 0)
74 ('', '', 0)
75 """
75 """
76 parts = rev.rsplit('@', 1)
76 parts = rev.rsplit('@', 1)
77 revnum = 0
77 revnum = 0
78 if len(parts) > 1:
78 if len(parts) > 1:
79 revnum = int(parts[1])
79 revnum = int(parts[1])
80 parts = parts[0].split('/', 1)
80 parts = parts[0].split('/', 1)
81 uuid = ''
81 uuid = ''
82 mod = ''
82 mod = ''
83 if len(parts) > 1 and parts[0].startswith('svn:'):
83 if len(parts) > 1 and parts[0].startswith('svn:'):
84 uuid = parts[0][4:]
84 uuid = parts[0][4:]
85 mod = '/' + parts[1]
85 mod = '/' + parts[1]
86 return uuid, mod, revnum
86 return uuid, mod, revnum
87
87
88 def quote(s):
88 def quote(s):
89 # As of svn 1.7, many svn calls expect "canonical" paths. In
89 # As of svn 1.7, many svn calls expect "canonical" paths. In
90 # theory, we should call svn.core.*canonicalize() on all paths
90 # theory, we should call svn.core.*canonicalize() on all paths
91 # before passing them to the API. Instead, we assume the base url
91 # before passing them to the API. Instead, we assume the base url
92 # is canonical and copy the behaviour of svn URL encoding function
92 # is canonical and copy the behaviour of svn URL encoding function
93 # so we can extend it safely with new components. The "safe"
93 # so we can extend it safely with new components. The "safe"
94 # characters were taken from the "svn_uri__char_validity" table in
94 # characters were taken from the "svn_uri__char_validity" table in
95 # libsvn_subr/path.c.
95 # libsvn_subr/path.c.
96 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
96 return urlreq.quote(s, "!$&'()*+,-./:=@_~")
97
97
98 def geturl(path):
98 def geturl(path):
99 try:
99 try:
100 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
100 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
101 except svn.core.SubversionException:
101 except svn.core.SubversionException:
102 # svn.client.url_from_path() fails with local repositories
102 # svn.client.url_from_path() fails with local repositories
103 pass
103 pass
104 if os.path.isdir(path):
104 if os.path.isdir(path):
105 path = os.path.normpath(os.path.abspath(path))
105 path = os.path.normpath(os.path.abspath(path))
106 if pycompat.iswindows:
106 if pycompat.iswindows:
107 path = '/' + util.normpath(path)
107 path = '/' + util.normpath(path)
108 # Module URL is later compared with the repository URL returned
108 # Module URL is later compared with the repository URL returned
109 # by svn API, which is UTF-8.
109 # by svn API, which is UTF-8.
110 path = encoding.tolocal(path)
110 path = encoding.tolocal(path)
111 path = 'file://%s' % quote(path)
111 path = 'file://%s' % quote(path)
112 return svn.core.svn_path_canonicalize(path)
112 return svn.core.svn_path_canonicalize(path)
113
113
114 def optrev(number):
114 def optrev(number):
115 optrev = svn.core.svn_opt_revision_t()
115 optrev = svn.core.svn_opt_revision_t()
116 optrev.kind = svn.core.svn_opt_revision_number
116 optrev.kind = svn.core.svn_opt_revision_number
117 optrev.value.number = number
117 optrev.value.number = number
118 return optrev
118 return optrev
119
119
120 class changedpath(object):
120 class changedpath(object):
121 def __init__(self, p):
121 def __init__(self, p):
122 self.copyfrom_path = p.copyfrom_path
122 self.copyfrom_path = p.copyfrom_path
123 self.copyfrom_rev = p.copyfrom_rev
123 self.copyfrom_rev = p.copyfrom_rev
124 self.action = p.action
124 self.action = p.action
125
125
126 def get_log_child(fp, url, paths, start, end, limit=0,
126 def get_log_child(fp, url, paths, start, end, limit=0,
127 discover_changed_paths=True, strict_node_history=False):
127 discover_changed_paths=True, strict_node_history=False):
128 protocol = -1
128 protocol = -1
129 def receiver(orig_paths, revnum, author, date, message, pool):
129 def receiver(orig_paths, revnum, author, date, message, pool):
130 paths = {}
130 paths = {}
131 if orig_paths is not None:
131 if orig_paths is not None:
132 for k, v in orig_paths.iteritems():
132 for k, v in orig_paths.iteritems():
133 paths[k] = changedpath(v)
133 paths[k] = changedpath(v)
134 pickle.dump((paths, revnum, author, date, message),
134 pickle.dump((paths, revnum, author, date, message),
135 fp, protocol)
135 fp, protocol)
136
136
137 try:
137 try:
138 # Use an ra of our own so that our parent can consume
138 # Use an ra of our own so that our parent can consume
139 # our results without confusing the server.
139 # our results without confusing the server.
140 t = transport.SvnRaTransport(url=url)
140 t = transport.SvnRaTransport(url=url)
141 svn.ra.get_log(t.ra, paths, start, end, limit,
141 svn.ra.get_log(t.ra, paths, start, end, limit,
142 discover_changed_paths,
142 discover_changed_paths,
143 strict_node_history,
143 strict_node_history,
144 receiver)
144 receiver)
145 except IOError:
145 except IOError:
146 # Caller may interrupt the iteration
146 # Caller may interrupt the iteration
147 pickle.dump(None, fp, protocol)
147 pickle.dump(None, fp, protocol)
148 except Exception as inst:
148 except Exception as inst:
149 pickle.dump(str(inst), fp, protocol)
149 pickle.dump(str(inst), fp, protocol)
150 else:
150 else:
151 pickle.dump(None, fp, protocol)
151 pickle.dump(None, fp, protocol)
152 fp.close()
152 fp.close()
153 # With large history, cleanup process goes crazy and suddenly
153 # With large history, cleanup process goes crazy and suddenly
154 # consumes *huge* amount of memory. The output file being closed,
154 # consumes *huge* amount of memory. The output file being closed,
155 # there is no need for clean termination.
155 # there is no need for clean termination.
156 os._exit(0)
156 os._exit(0)
157
157
158 def debugsvnlog(ui, **opts):
158 def debugsvnlog(ui, **opts):
159 """Fetch SVN log in a subprocess and channel them back to parent to
159 """Fetch SVN log in a subprocess and channel them back to parent to
160 avoid memory collection issues.
160 avoid memory collection issues.
161 """
161 """
162 if svn is None:
162 if svn is None:
163 raise error.Abort(_('debugsvnlog could not load Subversion python '
163 raise error.Abort(_('debugsvnlog could not load Subversion python '
164 'bindings'))
164 'bindings'))
165
165
166 args = decodeargs(ui.fin.read())
166 args = decodeargs(ui.fin.read())
167 get_log_child(ui.fout, *args)
167 get_log_child(ui.fout, *args)
168
168
169 class logstream(object):
169 class logstream(object):
170 """Interruptible revision log iterator."""
170 """Interruptible revision log iterator."""
171 def __init__(self, stdout):
171 def __init__(self, stdout):
172 self._stdout = stdout
172 self._stdout = stdout
173
173
174 def __iter__(self):
174 def __iter__(self):
175 while True:
175 while True:
176 try:
176 try:
177 entry = pickle.load(self._stdout)
177 entry = pickle.load(self._stdout)
178 except EOFError:
178 except EOFError:
179 raise error.Abort(_('Mercurial failed to run itself, check'
179 raise error.Abort(_('Mercurial failed to run itself, check'
180 ' hg executable is in PATH'))
180 ' hg executable is in PATH'))
181 try:
181 try:
182 orig_paths, revnum, author, date, message = entry
182 orig_paths, revnum, author, date, message = entry
183 except (TypeError, ValueError):
183 except (TypeError, ValueError):
184 if entry is None:
184 if entry is None:
185 break
185 break
186 raise error.Abort(_("log stream exception '%s'") % entry)
186 raise error.Abort(_("log stream exception '%s'") % entry)
187 yield entry
187 yield entry
188
188
189 def close(self):
189 def close(self):
190 if self._stdout:
190 if self._stdout:
191 self._stdout.close()
191 self._stdout.close()
192 self._stdout = None
192 self._stdout = None
193
193
194 class directlogstream(list):
194 class directlogstream(list):
195 """Direct revision log iterator.
195 """Direct revision log iterator.
196 This can be used for debugging and development but it will probably leak
196 This can be used for debugging and development but it will probably leak
197 memory and is not suitable for real conversions."""
197 memory and is not suitable for real conversions."""
198 def __init__(self, url, paths, start, end, limit=0,
198 def __init__(self, url, paths, start, end, limit=0,
199 discover_changed_paths=True, strict_node_history=False):
199 discover_changed_paths=True, strict_node_history=False):
200
200
201 def receiver(orig_paths, revnum, author, date, message, pool):
201 def receiver(orig_paths, revnum, author, date, message, pool):
202 paths = {}
202 paths = {}
203 if orig_paths is not None:
203 if orig_paths is not None:
204 for k, v in orig_paths.iteritems():
204 for k, v in orig_paths.iteritems():
205 paths[k] = changedpath(v)
205 paths[k] = changedpath(v)
206 self.append((paths, revnum, author, date, message))
206 self.append((paths, revnum, author, date, message))
207
207
208 # Use an ra of our own so that our parent can consume
208 # Use an ra of our own so that our parent can consume
209 # our results without confusing the server.
209 # our results without confusing the server.
210 t = transport.SvnRaTransport(url=url)
210 t = transport.SvnRaTransport(url=url)
211 svn.ra.get_log(t.ra, paths, start, end, limit,
211 svn.ra.get_log(t.ra, paths, start, end, limit,
212 discover_changed_paths,
212 discover_changed_paths,
213 strict_node_history,
213 strict_node_history,
214 receiver)
214 receiver)
215
215
216 def close(self):
216 def close(self):
217 pass
217 pass
218
218
219 # Check to see if the given path is a local Subversion repo. Verify this by
219 # Check to see if the given path is a local Subversion repo. Verify this by
220 # looking for several svn-specific files and directories in the given
220 # looking for several svn-specific files and directories in the given
221 # directory.
221 # directory.
222 def filecheck(ui, path, proto):
222 def filecheck(ui, path, proto):
223 for x in ('locks', 'hooks', 'format', 'db'):
223 for x in ('locks', 'hooks', 'format', 'db'):
224 if not os.path.exists(os.path.join(path, x)):
224 if not os.path.exists(os.path.join(path, x)):
225 return False
225 return False
226 return True
226 return True
227
227
228 # Check to see if a given path is the root of an svn repo over http. We verify
228 # Check to see if a given path is the root of an svn repo over http. We verify
229 # this by requesting a version-controlled URL we know can't exist and looking
229 # this by requesting a version-controlled URL we know can't exist and looking
230 # for the svn-specific "not found" XML.
230 # for the svn-specific "not found" XML.
231 def httpcheck(ui, path, proto):
231 def httpcheck(ui, path, proto):
232 try:
232 try:
233 opener = urlreq.buildopener()
233 opener = urlreq.buildopener()
234 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
234 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
235 data = rsp.read()
235 data = rsp.read()
236 except urlerr.httperror as inst:
236 except urlerr.httperror as inst:
237 if inst.code != 404:
237 if inst.code != 404:
238 # Except for 404 we cannot know for sure this is not an svn repo
238 # Except for 404 we cannot know for sure this is not an svn repo
239 ui.warn(_('svn: cannot probe remote repository, assume it could '
239 ui.warn(_('svn: cannot probe remote repository, assume it could '
240 'be a subversion repository. Use --source-type if you '
240 'be a subversion repository. Use --source-type if you '
241 'know better.\n'))
241 'know better.\n'))
242 return True
242 return True
243 data = inst.fp.read()
243 data = inst.fp.read()
244 except Exception:
244 except Exception:
245 # Could be urlerr.urlerror if the URL is invalid or anything else.
245 # Could be urlerr.urlerror if the URL is invalid or anything else.
246 return False
246 return False
247 return '<m:human-readable errcode="160013">' in data
247 return '<m:human-readable errcode="160013">' in data
248
248
249 protomap = {'http': httpcheck,
249 protomap = {'http': httpcheck,
250 'https': httpcheck,
250 'https': httpcheck,
251 'file': filecheck,
251 'file': filecheck,
252 }
252 }
253 def issvnurl(ui, url):
253 def issvnurl(ui, url):
254 try:
254 try:
255 proto, path = url.split('://', 1)
255 proto, path = url.split('://', 1)
256 if proto == 'file':
256 if proto == 'file':
257 if (pycompat.iswindows and path[:1] == '/'
257 if (pycompat.iswindows and path[:1] == '/'
258 and path[1:2].isalpha() and path[2:6].lower() == '%3a/'):
258 and path[1:2].isalpha() and path[2:6].lower() == '%3a/'):
259 path = path[:2] + ':/' + path[6:]
259 path = path[:2] + ':/' + path[6:]
260 path = urlreq.url2pathname(path)
260 path = urlreq.url2pathname(path)
261 except ValueError:
261 except ValueError:
262 proto = 'file'
262 proto = 'file'
263 path = os.path.abspath(url)
263 path = os.path.abspath(url)
264 if proto == 'file':
264 if proto == 'file':
265 path = util.pconvert(path)
265 path = util.pconvert(path)
266 check = protomap.get(proto, lambda *args: False)
266 check = protomap.get(proto, lambda *args: False)
267 while '/' in path:
267 while '/' in path:
268 if check(ui, path, proto):
268 if check(ui, path, proto):
269 return True
269 return True
270 path = path.rsplit('/', 1)[0]
270 path = path.rsplit('/', 1)[0]
271 return False
271 return False
272
272
273 # SVN conversion code stolen from bzr-svn and tailor
273 # SVN conversion code stolen from bzr-svn and tailor
274 #
274 #
275 # Subversion looks like a versioned filesystem, branches structures
275 # Subversion looks like a versioned filesystem, branches structures
276 # are defined by conventions and not enforced by the tool. First,
276 # are defined by conventions and not enforced by the tool. First,
277 # we define the potential branches (modules) as "trunk" and "branches"
277 # we define the potential branches (modules) as "trunk" and "branches"
278 # children directories. Revisions are then identified by their
278 # children directories. Revisions are then identified by their
279 # module and revision number (and a repository identifier).
279 # module and revision number (and a repository identifier).
280 #
280 #
281 # The revision graph is really a tree (or a forest). By default, a
281 # The revision graph is really a tree (or a forest). By default, a
282 # revision parent is the previous revision in the same module. If the
282 # revision parent is the previous revision in the same module. If the
283 # module directory is copied/moved from another module then the
283 # module directory is copied/moved from another module then the
284 # revision is the module root and its parent the source revision in
284 # revision is the module root and its parent the source revision in
285 # the parent module. A revision has at most one parent.
285 # the parent module. A revision has at most one parent.
286 #
286 #
287 class svn_source(converter_source):
287 class svn_source(converter_source):
288 def __init__(self, ui, url, revs=None):
288 def __init__(self, ui, url, revs=None):
289 super(svn_source, self).__init__(ui, url, revs=revs)
289 super(svn_source, self).__init__(ui, url, revs=revs)
290
290
291 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
291 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
292 (os.path.exists(url) and
292 (os.path.exists(url) and
293 os.path.exists(os.path.join(url, '.svn'))) or
293 os.path.exists(os.path.join(url, '.svn'))) or
294 issvnurl(ui, url)):
294 issvnurl(ui, url)):
295 raise NoRepo(_("%s does not look like a Subversion repository")
295 raise NoRepo(_("%s does not look like a Subversion repository")
296 % url)
296 % url)
297 if svn is None:
297 if svn is None:
298 raise MissingTool(_('could not load Subversion python bindings'))
298 raise MissingTool(_('could not load Subversion python bindings'))
299
299
300 try:
300 try:
301 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
301 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
302 if version < (1, 4):
302 if version < (1, 4):
303 raise MissingTool(_('Subversion python bindings %d.%d found, '
303 raise MissingTool(_('Subversion python bindings %d.%d found, '
304 '1.4 or later required') % version)
304 '1.4 or later required') % version)
305 except AttributeError:
305 except AttributeError:
306 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
306 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
307 'or later required'))
307 'or later required'))
308
308
309 self.lastrevs = {}
309 self.lastrevs = {}
310
310
311 latest = None
311 latest = None
312 try:
312 try:
313 # Support file://path@rev syntax. Useful e.g. to convert
313 # Support file://path@rev syntax. Useful e.g. to convert
314 # deleted branches.
314 # deleted branches.
315 at = url.rfind('@')
315 at = url.rfind('@')
316 if at >= 0:
316 if at >= 0:
317 latest = int(url[at + 1:])
317 latest = int(url[at + 1:])
318 url = url[:at]
318 url = url[:at]
319 except ValueError:
319 except ValueError:
320 pass
320 pass
321 self.url = geturl(url)
321 self.url = geturl(url)
322 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
322 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
323 try:
323 try:
324 self.transport = transport.SvnRaTransport(url=self.url)
324 self.transport = transport.SvnRaTransport(url=self.url)
325 self.ra = self.transport.ra
325 self.ra = self.transport.ra
326 self.ctx = self.transport.client
326 self.ctx = self.transport.client
327 self.baseurl = svn.ra.get_repos_root(self.ra)
327 self.baseurl = svn.ra.get_repos_root(self.ra)
328 # Module is either empty or a repository path starting with
328 # Module is either empty or a repository path starting with
329 # a slash and not ending with a slash.
329 # a slash and not ending with a slash.
330 self.module = urlreq.unquote(self.url[len(self.baseurl):])
330 self.module = urlreq.unquote(self.url[len(self.baseurl):])
331 self.prevmodule = None
331 self.prevmodule = None
332 self.rootmodule = self.module
332 self.rootmodule = self.module
333 self.commits = {}
333 self.commits = {}
334 self.paths = {}
334 self.paths = {}
335 self.uuid = svn.ra.get_uuid(self.ra)
335 self.uuid = svn.ra.get_uuid(self.ra)
336 except svn.core.SubversionException:
336 except svn.core.SubversionException:
337 ui.traceback()
337 ui.traceback()
338 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
338 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
339 svn.core.SVN_VER_MINOR,
339 svn.core.SVN_VER_MINOR,
340 svn.core.SVN_VER_MICRO)
340 svn.core.SVN_VER_MICRO)
341 raise NoRepo(_("%s does not look like a Subversion repository "
341 raise NoRepo(_("%s does not look like a Subversion repository "
342 "to libsvn version %s")
342 "to libsvn version %s")
343 % (self.url, svnversion))
343 % (self.url, svnversion))
344
344
345 if revs:
345 if revs:
346 if len(revs) > 1:
346 if len(revs) > 1:
347 raise error.Abort(_('subversion source does not support '
347 raise error.Abort(_('subversion source does not support '
348 'specifying multiple revisions'))
348 'specifying multiple revisions'))
349 try:
349 try:
350 latest = int(revs[0])
350 latest = int(revs[0])
351 except ValueError:
351 except ValueError:
352 raise error.Abort(_('svn: revision %s is not an integer') %
352 raise error.Abort(_('svn: revision %s is not an integer') %
353 revs[0])
353 revs[0])
354
354
355 self.trunkname = self.ui.config('convert', 'svn.trunk',
355 trunkcfg = self.ui.config('convert', 'svn.trunk')
356 'trunk').strip('/')
356 if trunkcfg is None:
357 trunkcfg = 'trunk'
358 self.trunkname = trunkcfg.strip('/')
357 self.startrev = self.ui.config('convert', 'svn.startrev')
359 self.startrev = self.ui.config('convert', 'svn.startrev')
358 try:
360 try:
359 self.startrev = int(self.startrev)
361 self.startrev = int(self.startrev)
360 if self.startrev < 0:
362 if self.startrev < 0:
361 self.startrev = 0
363 self.startrev = 0
362 except ValueError:
364 except ValueError:
363 raise error.Abort(_('svn: start revision %s is not an integer')
365 raise error.Abort(_('svn: start revision %s is not an integer')
364 % self.startrev)
366 % self.startrev)
365
367
366 try:
368 try:
367 self.head = self.latest(self.module, latest)
369 self.head = self.latest(self.module, latest)
368 except SvnPathNotFound:
370 except SvnPathNotFound:
369 self.head = None
371 self.head = None
370 if not self.head:
372 if not self.head:
371 raise error.Abort(_('no revision found in module %s')
373 raise error.Abort(_('no revision found in module %s')
372 % self.module)
374 % self.module)
373 self.last_changed = self.revnum(self.head)
375 self.last_changed = self.revnum(self.head)
374
376
375 self._changescache = (None, None)
377 self._changescache = (None, None)
376
378
377 if os.path.exists(os.path.join(url, '.svn/entries')):
379 if os.path.exists(os.path.join(url, '.svn/entries')):
378 self.wc = url
380 self.wc = url
379 else:
381 else:
380 self.wc = None
382 self.wc = None
381 self.convertfp = None
383 self.convertfp = None
382
384
383 def setrevmap(self, revmap):
385 def setrevmap(self, revmap):
384 lastrevs = {}
386 lastrevs = {}
385 for revid in revmap.iterkeys():
387 for revid in revmap.iterkeys():
386 uuid, module, revnum = revsplit(revid)
388 uuid, module, revnum = revsplit(revid)
387 lastrevnum = lastrevs.setdefault(module, revnum)
389 lastrevnum = lastrevs.setdefault(module, revnum)
388 if revnum > lastrevnum:
390 if revnum > lastrevnum:
389 lastrevs[module] = revnum
391 lastrevs[module] = revnum
390 self.lastrevs = lastrevs
392 self.lastrevs = lastrevs
391
393
392 def exists(self, path, optrev):
394 def exists(self, path, optrev):
393 try:
395 try:
394 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
396 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
395 optrev, False, self.ctx)
397 optrev, False, self.ctx)
396 return True
398 return True
397 except svn.core.SubversionException:
399 except svn.core.SubversionException:
398 return False
400 return False
399
401
400 def getheads(self):
402 def getheads(self):
401
403
402 def isdir(path, revnum):
404 def isdir(path, revnum):
403 kind = self._checkpath(path, revnum)
405 kind = self._checkpath(path, revnum)
404 return kind == svn.core.svn_node_dir
406 return kind == svn.core.svn_node_dir
405
407
406 def getcfgpath(name, rev):
408 def getcfgpath(name, rev):
407 cfgpath = self.ui.config('convert', 'svn.' + name)
409 cfgpath = self.ui.config('convert', 'svn.' + name)
408 if cfgpath is not None and cfgpath.strip() == '':
410 if cfgpath is not None and cfgpath.strip() == '':
409 return None
411 return None
410 path = (cfgpath or name).strip('/')
412 path = (cfgpath or name).strip('/')
411 if not self.exists(path, rev):
413 if not self.exists(path, rev):
412 if self.module.endswith(path) and name == 'trunk':
414 if self.module.endswith(path) and name == 'trunk':
413 # we are converting from inside this directory
415 # we are converting from inside this directory
414 return None
416 return None
415 if cfgpath:
417 if cfgpath:
416 raise error.Abort(_('expected %s to be at %r, but not found'
418 raise error.Abort(_('expected %s to be at %r, but not found'
417 ) % (name, path))
419 ) % (name, path))
418 return None
420 return None
419 self.ui.note(_('found %s at %r\n') % (name, path))
421 self.ui.note(_('found %s at %r\n') % (name, path))
420 return path
422 return path
421
423
422 rev = optrev(self.last_changed)
424 rev = optrev(self.last_changed)
423 oldmodule = ''
425 oldmodule = ''
424 trunk = getcfgpath('trunk', rev)
426 trunk = getcfgpath('trunk', rev)
425 self.tags = getcfgpath('tags', rev)
427 self.tags = getcfgpath('tags', rev)
426 branches = getcfgpath('branches', rev)
428 branches = getcfgpath('branches', rev)
427
429
428 # If the project has a trunk or branches, we will extract heads
430 # If the project has a trunk or branches, we will extract heads
429 # from them. We keep the project root otherwise.
431 # from them. We keep the project root otherwise.
430 if trunk:
432 if trunk:
431 oldmodule = self.module or ''
433 oldmodule = self.module or ''
432 self.module += '/' + trunk
434 self.module += '/' + trunk
433 self.head = self.latest(self.module, self.last_changed)
435 self.head = self.latest(self.module, self.last_changed)
434 if not self.head:
436 if not self.head:
435 raise error.Abort(_('no revision found in module %s')
437 raise error.Abort(_('no revision found in module %s')
436 % self.module)
438 % self.module)
437
439
438 # First head in the list is the module's head
440 # First head in the list is the module's head
439 self.heads = [self.head]
441 self.heads = [self.head]
440 if self.tags is not None:
442 if self.tags is not None:
441 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
443 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
442
444
443 # Check if branches bring a few more heads to the list
445 # Check if branches bring a few more heads to the list
444 if branches:
446 if branches:
445 rpath = self.url.strip('/')
447 rpath = self.url.strip('/')
446 branchnames = svn.client.ls(rpath + '/' + quote(branches),
448 branchnames = svn.client.ls(rpath + '/' + quote(branches),
447 rev, False, self.ctx)
449 rev, False, self.ctx)
448 for branch in sorted(branchnames):
450 for branch in sorted(branchnames):
449 module = '%s/%s/%s' % (oldmodule, branches, branch)
451 module = '%s/%s/%s' % (oldmodule, branches, branch)
450 if not isdir(module, self.last_changed):
452 if not isdir(module, self.last_changed):
451 continue
453 continue
452 brevid = self.latest(module, self.last_changed)
454 brevid = self.latest(module, self.last_changed)
453 if not brevid:
455 if not brevid:
454 self.ui.note(_('ignoring empty branch %s\n') % branch)
456 self.ui.note(_('ignoring empty branch %s\n') % branch)
455 continue
457 continue
456 self.ui.note(_('found branch %s at %d\n') %
458 self.ui.note(_('found branch %s at %d\n') %
457 (branch, self.revnum(brevid)))
459 (branch, self.revnum(brevid)))
458 self.heads.append(brevid)
460 self.heads.append(brevid)
459
461
460 if self.startrev and self.heads:
462 if self.startrev and self.heads:
461 if len(self.heads) > 1:
463 if len(self.heads) > 1:
462 raise error.Abort(_('svn: start revision is not supported '
464 raise error.Abort(_('svn: start revision is not supported '
463 'with more than one branch'))
465 'with more than one branch'))
464 revnum = self.revnum(self.heads[0])
466 revnum = self.revnum(self.heads[0])
465 if revnum < self.startrev:
467 if revnum < self.startrev:
466 raise error.Abort(
468 raise error.Abort(
467 _('svn: no revision found after start revision %d')
469 _('svn: no revision found after start revision %d')
468 % self.startrev)
470 % self.startrev)
469
471
470 return self.heads
472 return self.heads
471
473
472 def _getchanges(self, rev, full):
474 def _getchanges(self, rev, full):
473 (paths, parents) = self.paths[rev]
475 (paths, parents) = self.paths[rev]
474 copies = {}
476 copies = {}
475 if parents:
477 if parents:
476 files, self.removed, copies = self.expandpaths(rev, paths, parents)
478 files, self.removed, copies = self.expandpaths(rev, paths, parents)
477 if full or not parents:
479 if full or not parents:
478 # Perform a full checkout on roots
480 # Perform a full checkout on roots
479 uuid, module, revnum = revsplit(rev)
481 uuid, module, revnum = revsplit(rev)
480 entries = svn.client.ls(self.baseurl + quote(module),
482 entries = svn.client.ls(self.baseurl + quote(module),
481 optrev(revnum), True, self.ctx)
483 optrev(revnum), True, self.ctx)
482 files = [n for n, e in entries.iteritems()
484 files = [n for n, e in entries.iteritems()
483 if e.kind == svn.core.svn_node_file]
485 if e.kind == svn.core.svn_node_file]
484 self.removed = set()
486 self.removed = set()
485
487
486 files.sort()
488 files.sort()
487 files = zip(files, [rev] * len(files))
489 files = zip(files, [rev] * len(files))
488 return (files, copies)
490 return (files, copies)
489
491
490 def getchanges(self, rev, full):
492 def getchanges(self, rev, full):
491 # reuse cache from getchangedfiles
493 # reuse cache from getchangedfiles
492 if self._changescache[0] == rev and not full:
494 if self._changescache[0] == rev and not full:
493 (files, copies) = self._changescache[1]
495 (files, copies) = self._changescache[1]
494 else:
496 else:
495 (files, copies) = self._getchanges(rev, full)
497 (files, copies) = self._getchanges(rev, full)
496 # caller caches the result, so free it here to release memory
498 # caller caches the result, so free it here to release memory
497 del self.paths[rev]
499 del self.paths[rev]
498 return (files, copies, set())
500 return (files, copies, set())
499
501
500 def getchangedfiles(self, rev, i):
502 def getchangedfiles(self, rev, i):
501 # called from filemap - cache computed values for reuse in getchanges
503 # called from filemap - cache computed values for reuse in getchanges
502 (files, copies) = self._getchanges(rev, False)
504 (files, copies) = self._getchanges(rev, False)
503 self._changescache = (rev, (files, copies))
505 self._changescache = (rev, (files, copies))
504 return [f[0] for f in files]
506 return [f[0] for f in files]
505
507
506 def getcommit(self, rev):
508 def getcommit(self, rev):
507 if rev not in self.commits:
509 if rev not in self.commits:
508 uuid, module, revnum = revsplit(rev)
510 uuid, module, revnum = revsplit(rev)
509 self.module = module
511 self.module = module
510 self.reparent(module)
512 self.reparent(module)
511 # We assume that:
513 # We assume that:
512 # - requests for revisions after "stop" come from the
514 # - requests for revisions after "stop" come from the
513 # revision graph backward traversal. Cache all of them
515 # revision graph backward traversal. Cache all of them
514 # down to stop, they will be used eventually.
516 # down to stop, they will be used eventually.
515 # - requests for revisions before "stop" come to get
517 # - requests for revisions before "stop" come to get
516 # isolated branches parents. Just fetch what is needed.
518 # isolated branches parents. Just fetch what is needed.
517 stop = self.lastrevs.get(module, 0)
519 stop = self.lastrevs.get(module, 0)
518 if revnum < stop:
520 if revnum < stop:
519 stop = revnum + 1
521 stop = revnum + 1
520 self._fetch_revisions(revnum, stop)
522 self._fetch_revisions(revnum, stop)
521 if rev not in self.commits:
523 if rev not in self.commits:
522 raise error.Abort(_('svn: revision %s not found') % revnum)
524 raise error.Abort(_('svn: revision %s not found') % revnum)
523 revcommit = self.commits[rev]
525 revcommit = self.commits[rev]
524 # caller caches the result, so free it here to release memory
526 # caller caches the result, so free it here to release memory
525 del self.commits[rev]
527 del self.commits[rev]
526 return revcommit
528 return revcommit
527
529
528 def checkrevformat(self, revstr, mapname='splicemap'):
530 def checkrevformat(self, revstr, mapname='splicemap'):
529 """ fails if revision format does not match the correct format"""
531 """ fails if revision format does not match the correct format"""
530 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
532 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
531 r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
533 r'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
532 r'{12,12}(.*)\@[0-9]+$',revstr):
534 r'{12,12}(.*)\@[0-9]+$',revstr):
533 raise error.Abort(_('%s entry %s is not a valid revision'
535 raise error.Abort(_('%s entry %s is not a valid revision'
534 ' identifier') % (mapname, revstr))
536 ' identifier') % (mapname, revstr))
535
537
536 def numcommits(self):
538 def numcommits(self):
537 return int(self.head.rsplit('@', 1)[1]) - self.startrev
539 return int(self.head.rsplit('@', 1)[1]) - self.startrev
538
540
539 def gettags(self):
541 def gettags(self):
540 tags = {}
542 tags = {}
541 if self.tags is None:
543 if self.tags is None:
542 return tags
544 return tags
543
545
544 # svn tags are just a convention, project branches left in a
546 # svn tags are just a convention, project branches left in a
545 # 'tags' directory. There is no other relationship than
547 # 'tags' directory. There is no other relationship than
546 # ancestry, which is expensive to discover and makes them hard
548 # ancestry, which is expensive to discover and makes them hard
547 # to update incrementally. Worse, past revisions may be
549 # to update incrementally. Worse, past revisions may be
548 # referenced by tags far away in the future, requiring a deep
550 # referenced by tags far away in the future, requiring a deep
549 # history traversal on every calculation. Current code
551 # history traversal on every calculation. Current code
550 # performs a single backward traversal, tracking moves within
552 # performs a single backward traversal, tracking moves within
551 # the tags directory (tag renaming) and recording a new tag
553 # the tags directory (tag renaming) and recording a new tag
552 # everytime a project is copied from outside the tags
554 # everytime a project is copied from outside the tags
553 # directory. It also lists deleted tags, this behaviour may
555 # directory. It also lists deleted tags, this behaviour may
554 # change in the future.
556 # change in the future.
555 pendings = []
557 pendings = []
556 tagspath = self.tags
558 tagspath = self.tags
557 start = svn.ra.get_latest_revnum(self.ra)
559 start = svn.ra.get_latest_revnum(self.ra)
558 stream = self._getlog([self.tags], start, self.startrev)
560 stream = self._getlog([self.tags], start, self.startrev)
559 try:
561 try:
560 for entry in stream:
562 for entry in stream:
561 origpaths, revnum, author, date, message = entry
563 origpaths, revnum, author, date, message = entry
562 if not origpaths:
564 if not origpaths:
563 origpaths = []
565 origpaths = []
564 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
566 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
565 in origpaths.iteritems() if e.copyfrom_path]
567 in origpaths.iteritems() if e.copyfrom_path]
566 # Apply moves/copies from more specific to general
568 # Apply moves/copies from more specific to general
567 copies.sort(reverse=True)
569 copies.sort(reverse=True)
568
570
569 srctagspath = tagspath
571 srctagspath = tagspath
570 if copies and copies[-1][2] == tagspath:
572 if copies and copies[-1][2] == tagspath:
571 # Track tags directory moves
573 # Track tags directory moves
572 srctagspath = copies.pop()[0]
574 srctagspath = copies.pop()[0]
573
575
574 for source, sourcerev, dest in copies:
576 for source, sourcerev, dest in copies:
575 if not dest.startswith(tagspath + '/'):
577 if not dest.startswith(tagspath + '/'):
576 continue
578 continue
577 for tag in pendings:
579 for tag in pendings:
578 if tag[0].startswith(dest):
580 if tag[0].startswith(dest):
579 tagpath = source + tag[0][len(dest):]
581 tagpath = source + tag[0][len(dest):]
580 tag[:2] = [tagpath, sourcerev]
582 tag[:2] = [tagpath, sourcerev]
581 break
583 break
582 else:
584 else:
583 pendings.append([source, sourcerev, dest])
585 pendings.append([source, sourcerev, dest])
584
586
585 # Filter out tags with children coming from different
587 # Filter out tags with children coming from different
586 # parts of the repository like:
588 # parts of the repository like:
587 # /tags/tag.1 (from /trunk:10)
589 # /tags/tag.1 (from /trunk:10)
588 # /tags/tag.1/foo (from /branches/foo:12)
590 # /tags/tag.1/foo (from /branches/foo:12)
589 # Here/tags/tag.1 discarded as well as its children.
591 # Here/tags/tag.1 discarded as well as its children.
590 # It happens with tools like cvs2svn. Such tags cannot
592 # It happens with tools like cvs2svn. Such tags cannot
591 # be represented in mercurial.
593 # be represented in mercurial.
592 addeds = dict((p, e.copyfrom_path) for p, e
594 addeds = dict((p, e.copyfrom_path) for p, e
593 in origpaths.iteritems()
595 in origpaths.iteritems()
594 if e.action == 'A' and e.copyfrom_path)
596 if e.action == 'A' and e.copyfrom_path)
595 badroots = set()
597 badroots = set()
596 for destroot in addeds:
598 for destroot in addeds:
597 for source, sourcerev, dest in pendings:
599 for source, sourcerev, dest in pendings:
598 if (not dest.startswith(destroot + '/')
600 if (not dest.startswith(destroot + '/')
599 or source.startswith(addeds[destroot] + '/')):
601 or source.startswith(addeds[destroot] + '/')):
600 continue
602 continue
601 badroots.add(destroot)
603 badroots.add(destroot)
602 break
604 break
603
605
604 for badroot in badroots:
606 for badroot in badroots:
605 pendings = [p for p in pendings if p[2] != badroot
607 pendings = [p for p in pendings if p[2] != badroot
606 and not p[2].startswith(badroot + '/')]
608 and not p[2].startswith(badroot + '/')]
607
609
608 # Tell tag renamings from tag creations
610 # Tell tag renamings from tag creations
609 renamings = []
611 renamings = []
610 for source, sourcerev, dest in pendings:
612 for source, sourcerev, dest in pendings:
611 tagname = dest.split('/')[-1]
613 tagname = dest.split('/')[-1]
612 if source.startswith(srctagspath):
614 if source.startswith(srctagspath):
613 renamings.append([source, sourcerev, tagname])
615 renamings.append([source, sourcerev, tagname])
614 continue
616 continue
615 if tagname in tags:
617 if tagname in tags:
616 # Keep the latest tag value
618 # Keep the latest tag value
617 continue
619 continue
618 # From revision may be fake, get one with changes
620 # From revision may be fake, get one with changes
619 try:
621 try:
620 tagid = self.latest(source, sourcerev)
622 tagid = self.latest(source, sourcerev)
621 if tagid and tagname not in tags:
623 if tagid and tagname not in tags:
622 tags[tagname] = tagid
624 tags[tagname] = tagid
623 except SvnPathNotFound:
625 except SvnPathNotFound:
624 # It happens when we are following directories
626 # It happens when we are following directories
625 # we assumed were copied with their parents
627 # we assumed were copied with their parents
626 # but were really created in the tag
628 # but were really created in the tag
627 # directory.
629 # directory.
628 pass
630 pass
629 pendings = renamings
631 pendings = renamings
630 tagspath = srctagspath
632 tagspath = srctagspath
631 finally:
633 finally:
632 stream.close()
634 stream.close()
633 return tags
635 return tags
634
636
635 def converted(self, rev, destrev):
637 def converted(self, rev, destrev):
636 if not self.wc:
638 if not self.wc:
637 return
639 return
638 if self.convertfp is None:
640 if self.convertfp is None:
639 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
641 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
640 'a')
642 'a')
641 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
643 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
642 self.convertfp.flush()
644 self.convertfp.flush()
643
645
644 def revid(self, revnum, module=None):
646 def revid(self, revnum, module=None):
645 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
647 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
646
648
647 def revnum(self, rev):
649 def revnum(self, rev):
648 return int(rev.split('@')[-1])
650 return int(rev.split('@')[-1])
649
651
650 def latest(self, path, stop=None):
652 def latest(self, path, stop=None):
651 """Find the latest revid affecting path, up to stop revision
653 """Find the latest revid affecting path, up to stop revision
652 number. If stop is None, default to repository latest
654 number. If stop is None, default to repository latest
653 revision. It may return a revision in a different module,
655 revision. It may return a revision in a different module,
654 since a branch may be moved without a change being
656 since a branch may be moved without a change being
655 reported. Return None if computed module does not belong to
657 reported. Return None if computed module does not belong to
656 rootmodule subtree.
658 rootmodule subtree.
657 """
659 """
658 def findchanges(path, start, stop=None):
660 def findchanges(path, start, stop=None):
659 stream = self._getlog([path], start, stop or 1)
661 stream = self._getlog([path], start, stop or 1)
660 try:
662 try:
661 for entry in stream:
663 for entry in stream:
662 paths, revnum, author, date, message = entry
664 paths, revnum, author, date, message = entry
663 if stop is None and paths:
665 if stop is None and paths:
664 # We do not know the latest changed revision,
666 # We do not know the latest changed revision,
665 # keep the first one with changed paths.
667 # keep the first one with changed paths.
666 break
668 break
667 if revnum <= stop:
669 if revnum <= stop:
668 break
670 break
669
671
670 for p in paths:
672 for p in paths:
671 if (not path.startswith(p) or
673 if (not path.startswith(p) or
672 not paths[p].copyfrom_path):
674 not paths[p].copyfrom_path):
673 continue
675 continue
674 newpath = paths[p].copyfrom_path + path[len(p):]
676 newpath = paths[p].copyfrom_path + path[len(p):]
675 self.ui.debug("branch renamed from %s to %s at %d\n" %
677 self.ui.debug("branch renamed from %s to %s at %d\n" %
676 (path, newpath, revnum))
678 (path, newpath, revnum))
677 path = newpath
679 path = newpath
678 break
680 break
679 if not paths:
681 if not paths:
680 revnum = None
682 revnum = None
681 return revnum, path
683 return revnum, path
682 finally:
684 finally:
683 stream.close()
685 stream.close()
684
686
685 if not path.startswith(self.rootmodule):
687 if not path.startswith(self.rootmodule):
686 # Requests on foreign branches may be forbidden at server level
688 # Requests on foreign branches may be forbidden at server level
687 self.ui.debug('ignoring foreign branch %r\n' % path)
689 self.ui.debug('ignoring foreign branch %r\n' % path)
688 return None
690 return None
689
691
690 if stop is None:
692 if stop is None:
691 stop = svn.ra.get_latest_revnum(self.ra)
693 stop = svn.ra.get_latest_revnum(self.ra)
692 try:
694 try:
693 prevmodule = self.reparent('')
695 prevmodule = self.reparent('')
694 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
696 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
695 self.reparent(prevmodule)
697 self.reparent(prevmodule)
696 except svn.core.SubversionException:
698 except svn.core.SubversionException:
697 dirent = None
699 dirent = None
698 if not dirent:
700 if not dirent:
699 raise SvnPathNotFound(_('%s not found up to revision %d')
701 raise SvnPathNotFound(_('%s not found up to revision %d')
700 % (path, stop))
702 % (path, stop))
701
703
702 # stat() gives us the previous revision on this line of
704 # stat() gives us the previous revision on this line of
703 # development, but it might be in *another module*. Fetch the
705 # development, but it might be in *another module*. Fetch the
704 # log and detect renames down to the latest revision.
706 # log and detect renames down to the latest revision.
705 revnum, realpath = findchanges(path, stop, dirent.created_rev)
707 revnum, realpath = findchanges(path, stop, dirent.created_rev)
706 if revnum is None:
708 if revnum is None:
707 # Tools like svnsync can create empty revision, when
709 # Tools like svnsync can create empty revision, when
708 # synchronizing only a subtree for instance. These empty
710 # synchronizing only a subtree for instance. These empty
709 # revisions created_rev still have their original values
711 # revisions created_rev still have their original values
710 # despite all changes having disappeared and can be
712 # despite all changes having disappeared and can be
711 # returned by ra.stat(), at least when stating the root
713 # returned by ra.stat(), at least when stating the root
712 # module. In that case, do not trust created_rev and scan
714 # module. In that case, do not trust created_rev and scan
713 # the whole history.
715 # the whole history.
714 revnum, realpath = findchanges(path, stop)
716 revnum, realpath = findchanges(path, stop)
715 if revnum is None:
717 if revnum is None:
716 self.ui.debug('ignoring empty branch %r\n' % realpath)
718 self.ui.debug('ignoring empty branch %r\n' % realpath)
717 return None
719 return None
718
720
719 if not realpath.startswith(self.rootmodule):
721 if not realpath.startswith(self.rootmodule):
720 self.ui.debug('ignoring foreign branch %r\n' % realpath)
722 self.ui.debug('ignoring foreign branch %r\n' % realpath)
721 return None
723 return None
722 return self.revid(revnum, realpath)
724 return self.revid(revnum, realpath)
723
725
724 def reparent(self, module):
726 def reparent(self, module):
725 """Reparent the svn transport and return the previous parent."""
727 """Reparent the svn transport and return the previous parent."""
726 if self.prevmodule == module:
728 if self.prevmodule == module:
727 return module
729 return module
728 svnurl = self.baseurl + quote(module)
730 svnurl = self.baseurl + quote(module)
729 prevmodule = self.prevmodule
731 prevmodule = self.prevmodule
730 if prevmodule is None:
732 if prevmodule is None:
731 prevmodule = ''
733 prevmodule = ''
732 self.ui.debug("reparent to %s\n" % svnurl)
734 self.ui.debug("reparent to %s\n" % svnurl)
733 svn.ra.reparent(self.ra, svnurl)
735 svn.ra.reparent(self.ra, svnurl)
734 self.prevmodule = module
736 self.prevmodule = module
735 return prevmodule
737 return prevmodule
736
738
737 def expandpaths(self, rev, paths, parents):
739 def expandpaths(self, rev, paths, parents):
738 changed, removed = set(), set()
740 changed, removed = set(), set()
739 copies = {}
741 copies = {}
740
742
741 new_module, revnum = revsplit(rev)[1:]
743 new_module, revnum = revsplit(rev)[1:]
742 if new_module != self.module:
744 if new_module != self.module:
743 self.module = new_module
745 self.module = new_module
744 self.reparent(self.module)
746 self.reparent(self.module)
745
747
746 for i, (path, ent) in enumerate(paths):
748 for i, (path, ent) in enumerate(paths):
747 self.ui.progress(_('scanning paths'), i, item=path,
749 self.ui.progress(_('scanning paths'), i, item=path,
748 total=len(paths), unit=_('paths'))
750 total=len(paths), unit=_('paths'))
749 entrypath = self.getrelpath(path)
751 entrypath = self.getrelpath(path)
750
752
751 kind = self._checkpath(entrypath, revnum)
753 kind = self._checkpath(entrypath, revnum)
752 if kind == svn.core.svn_node_file:
754 if kind == svn.core.svn_node_file:
753 changed.add(self.recode(entrypath))
755 changed.add(self.recode(entrypath))
754 if not ent.copyfrom_path or not parents:
756 if not ent.copyfrom_path or not parents:
755 continue
757 continue
756 # Copy sources not in parent revisions cannot be
758 # Copy sources not in parent revisions cannot be
757 # represented, ignore their origin for now
759 # represented, ignore their origin for now
758 pmodule, prevnum = revsplit(parents[0])[1:]
760 pmodule, prevnum = revsplit(parents[0])[1:]
759 if ent.copyfrom_rev < prevnum:
761 if ent.copyfrom_rev < prevnum:
760 continue
762 continue
761 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
763 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
762 if not copyfrom_path:
764 if not copyfrom_path:
763 continue
765 continue
764 self.ui.debug("copied to %s from %s@%s\n" %
766 self.ui.debug("copied to %s from %s@%s\n" %
765 (entrypath, copyfrom_path, ent.copyfrom_rev))
767 (entrypath, copyfrom_path, ent.copyfrom_rev))
766 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
768 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
767 elif kind == 0: # gone, but had better be a deleted *file*
769 elif kind == 0: # gone, but had better be a deleted *file*
768 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
770 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
769 pmodule, prevnum = revsplit(parents[0])[1:]
771 pmodule, prevnum = revsplit(parents[0])[1:]
770 parentpath = pmodule + "/" + entrypath
772 parentpath = pmodule + "/" + entrypath
771 fromkind = self._checkpath(entrypath, prevnum, pmodule)
773 fromkind = self._checkpath(entrypath, prevnum, pmodule)
772
774
773 if fromkind == svn.core.svn_node_file:
775 if fromkind == svn.core.svn_node_file:
774 removed.add(self.recode(entrypath))
776 removed.add(self.recode(entrypath))
775 elif fromkind == svn.core.svn_node_dir:
777 elif fromkind == svn.core.svn_node_dir:
776 oroot = parentpath.strip('/')
778 oroot = parentpath.strip('/')
777 nroot = path.strip('/')
779 nroot = path.strip('/')
778 children = self._iterfiles(oroot, prevnum)
780 children = self._iterfiles(oroot, prevnum)
779 for childpath in children:
781 for childpath in children:
780 childpath = childpath.replace(oroot, nroot)
782 childpath = childpath.replace(oroot, nroot)
781 childpath = self.getrelpath("/" + childpath, pmodule)
783 childpath = self.getrelpath("/" + childpath, pmodule)
782 if childpath:
784 if childpath:
783 removed.add(self.recode(childpath))
785 removed.add(self.recode(childpath))
784 else:
786 else:
785 self.ui.debug('unknown path in revision %d: %s\n' % \
787 self.ui.debug('unknown path in revision %d: %s\n' % \
786 (revnum, path))
788 (revnum, path))
787 elif kind == svn.core.svn_node_dir:
789 elif kind == svn.core.svn_node_dir:
788 if ent.action == 'M':
790 if ent.action == 'M':
789 # If the directory just had a prop change,
791 # If the directory just had a prop change,
790 # then we shouldn't need to look for its children.
792 # then we shouldn't need to look for its children.
791 continue
793 continue
792 if ent.action == 'R' and parents:
794 if ent.action == 'R' and parents:
793 # If a directory is replacing a file, mark the previous
795 # If a directory is replacing a file, mark the previous
794 # file as deleted
796 # file as deleted
795 pmodule, prevnum = revsplit(parents[0])[1:]
797 pmodule, prevnum = revsplit(parents[0])[1:]
796 pkind = self._checkpath(entrypath, prevnum, pmodule)
798 pkind = self._checkpath(entrypath, prevnum, pmodule)
797 if pkind == svn.core.svn_node_file:
799 if pkind == svn.core.svn_node_file:
798 removed.add(self.recode(entrypath))
800 removed.add(self.recode(entrypath))
799 elif pkind == svn.core.svn_node_dir:
801 elif pkind == svn.core.svn_node_dir:
800 # We do not know what files were kept or removed,
802 # We do not know what files were kept or removed,
801 # mark them all as changed.
803 # mark them all as changed.
802 for childpath in self._iterfiles(pmodule, prevnum):
804 for childpath in self._iterfiles(pmodule, prevnum):
803 childpath = self.getrelpath("/" + childpath)
805 childpath = self.getrelpath("/" + childpath)
804 if childpath:
806 if childpath:
805 changed.add(self.recode(childpath))
807 changed.add(self.recode(childpath))
806
808
807 for childpath in self._iterfiles(path, revnum):
809 for childpath in self._iterfiles(path, revnum):
808 childpath = self.getrelpath("/" + childpath)
810 childpath = self.getrelpath("/" + childpath)
809 if childpath:
811 if childpath:
810 changed.add(self.recode(childpath))
812 changed.add(self.recode(childpath))
811
813
812 # Handle directory copies
814 # Handle directory copies
813 if not ent.copyfrom_path or not parents:
815 if not ent.copyfrom_path or not parents:
814 continue
816 continue
815 # Copy sources not in parent revisions cannot be
817 # Copy sources not in parent revisions cannot be
816 # represented, ignore their origin for now
818 # represented, ignore their origin for now
817 pmodule, prevnum = revsplit(parents[0])[1:]
819 pmodule, prevnum = revsplit(parents[0])[1:]
818 if ent.copyfrom_rev < prevnum:
820 if ent.copyfrom_rev < prevnum:
819 continue
821 continue
820 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
822 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
821 if not copyfrompath:
823 if not copyfrompath:
822 continue
824 continue
823 self.ui.debug("mark %s came from %s:%d\n"
825 self.ui.debug("mark %s came from %s:%d\n"
824 % (path, copyfrompath, ent.copyfrom_rev))
826 % (path, copyfrompath, ent.copyfrom_rev))
825 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
827 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
826 for childpath in children:
828 for childpath in children:
827 childpath = self.getrelpath("/" + childpath, pmodule)
829 childpath = self.getrelpath("/" + childpath, pmodule)
828 if not childpath:
830 if not childpath:
829 continue
831 continue
830 copytopath = path + childpath[len(copyfrompath):]
832 copytopath = path + childpath[len(copyfrompath):]
831 copytopath = self.getrelpath(copytopath)
833 copytopath = self.getrelpath(copytopath)
832 copies[self.recode(copytopath)] = self.recode(childpath)
834 copies[self.recode(copytopath)] = self.recode(childpath)
833
835
834 self.ui.progress(_('scanning paths'), None)
836 self.ui.progress(_('scanning paths'), None)
835 changed.update(removed)
837 changed.update(removed)
836 return (list(changed), removed, copies)
838 return (list(changed), removed, copies)
837
839
838 def _fetch_revisions(self, from_revnum, to_revnum):
840 def _fetch_revisions(self, from_revnum, to_revnum):
839 if from_revnum < to_revnum:
841 if from_revnum < to_revnum:
840 from_revnum, to_revnum = to_revnum, from_revnum
842 from_revnum, to_revnum = to_revnum, from_revnum
841
843
842 self.child_cset = None
844 self.child_cset = None
843
845
844 def parselogentry(orig_paths, revnum, author, date, message):
846 def parselogentry(orig_paths, revnum, author, date, message):
845 """Return the parsed commit object or None, and True if
847 """Return the parsed commit object or None, and True if
846 the revision is a branch root.
848 the revision is a branch root.
847 """
849 """
848 self.ui.debug("parsing revision %d (%d changes)\n" %
850 self.ui.debug("parsing revision %d (%d changes)\n" %
849 (revnum, len(orig_paths)))
851 (revnum, len(orig_paths)))
850
852
851 branched = False
853 branched = False
852 rev = self.revid(revnum)
854 rev = self.revid(revnum)
853 # branch log might return entries for a parent we already have
855 # branch log might return entries for a parent we already have
854
856
855 if rev in self.commits or revnum < to_revnum:
857 if rev in self.commits or revnum < to_revnum:
856 return None, branched
858 return None, branched
857
859
858 parents = []
860 parents = []
859 # check whether this revision is the start of a branch or part
861 # check whether this revision is the start of a branch or part
860 # of a branch renaming
862 # of a branch renaming
861 orig_paths = sorted(orig_paths.iteritems())
863 orig_paths = sorted(orig_paths.iteritems())
862 root_paths = [(p, e) for p, e in orig_paths
864 root_paths = [(p, e) for p, e in orig_paths
863 if self.module.startswith(p)]
865 if self.module.startswith(p)]
864 if root_paths:
866 if root_paths:
865 path, ent = root_paths[-1]
867 path, ent = root_paths[-1]
866 if ent.copyfrom_path:
868 if ent.copyfrom_path:
867 branched = True
869 branched = True
868 newpath = ent.copyfrom_path + self.module[len(path):]
870 newpath = ent.copyfrom_path + self.module[len(path):]
869 # ent.copyfrom_rev may not be the actual last revision
871 # ent.copyfrom_rev may not be the actual last revision
870 previd = self.latest(newpath, ent.copyfrom_rev)
872 previd = self.latest(newpath, ent.copyfrom_rev)
871 if previd is not None:
873 if previd is not None:
872 prevmodule, prevnum = revsplit(previd)[1:]
874 prevmodule, prevnum = revsplit(previd)[1:]
873 if prevnum >= self.startrev:
875 if prevnum >= self.startrev:
874 parents = [previd]
876 parents = [previd]
875 self.ui.note(
877 self.ui.note(
876 _('found parent of branch %s at %d: %s\n') %
878 _('found parent of branch %s at %d: %s\n') %
877 (self.module, prevnum, prevmodule))
879 (self.module, prevnum, prevmodule))
878 else:
880 else:
879 self.ui.debug("no copyfrom path, don't know what to do.\n")
881 self.ui.debug("no copyfrom path, don't know what to do.\n")
880
882
881 paths = []
883 paths = []
882 # filter out unrelated paths
884 # filter out unrelated paths
883 for path, ent in orig_paths:
885 for path, ent in orig_paths:
884 if self.getrelpath(path) is None:
886 if self.getrelpath(path) is None:
885 continue
887 continue
886 paths.append((path, ent))
888 paths.append((path, ent))
887
889
888 # Example SVN datetime. Includes microseconds.
890 # Example SVN datetime. Includes microseconds.
889 # ISO-8601 conformant
891 # ISO-8601 conformant
890 # '2007-01-04T17:35:00.902377Z'
892 # '2007-01-04T17:35:00.902377Z'
891 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
893 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
892 if self.ui.configbool('convert', 'localtimezone'):
894 if self.ui.configbool('convert', 'localtimezone'):
893 date = makedatetimestamp(date[0])
895 date = makedatetimestamp(date[0])
894
896
895 if message:
897 if message:
896 log = self.recode(message)
898 log = self.recode(message)
897 else:
899 else:
898 log = ''
900 log = ''
899
901
900 if author:
902 if author:
901 author = self.recode(author)
903 author = self.recode(author)
902 else:
904 else:
903 author = ''
905 author = ''
904
906
905 try:
907 try:
906 branch = self.module.split("/")[-1]
908 branch = self.module.split("/")[-1]
907 if branch == self.trunkname:
909 if branch == self.trunkname:
908 branch = None
910 branch = None
909 except IndexError:
911 except IndexError:
910 branch = None
912 branch = None
911
913
912 cset = commit(author=author,
914 cset = commit(author=author,
913 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
915 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
914 desc=log,
916 desc=log,
915 parents=parents,
917 parents=parents,
916 branch=branch,
918 branch=branch,
917 rev=rev)
919 rev=rev)
918
920
919 self.commits[rev] = cset
921 self.commits[rev] = cset
920 # The parents list is *shared* among self.paths and the
922 # The parents list is *shared* among self.paths and the
921 # commit object. Both will be updated below.
923 # commit object. Both will be updated below.
922 self.paths[rev] = (paths, cset.parents)
924 self.paths[rev] = (paths, cset.parents)
923 if self.child_cset and not self.child_cset.parents:
925 if self.child_cset and not self.child_cset.parents:
924 self.child_cset.parents[:] = [rev]
926 self.child_cset.parents[:] = [rev]
925 self.child_cset = cset
927 self.child_cset = cset
926 return cset, branched
928 return cset, branched
927
929
928 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
930 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
929 (self.module, from_revnum, to_revnum))
931 (self.module, from_revnum, to_revnum))
930
932
931 try:
933 try:
932 firstcset = None
934 firstcset = None
933 lastonbranch = False
935 lastonbranch = False
934 stream = self._getlog([self.module], from_revnum, to_revnum)
936 stream = self._getlog([self.module], from_revnum, to_revnum)
935 try:
937 try:
936 for entry in stream:
938 for entry in stream:
937 paths, revnum, author, date, message = entry
939 paths, revnum, author, date, message = entry
938 if revnum < self.startrev:
940 if revnum < self.startrev:
939 lastonbranch = True
941 lastonbranch = True
940 break
942 break
941 if not paths:
943 if not paths:
942 self.ui.debug('revision %d has no entries\n' % revnum)
944 self.ui.debug('revision %d has no entries\n' % revnum)
943 # If we ever leave the loop on an empty
945 # If we ever leave the loop on an empty
944 # revision, do not try to get a parent branch
946 # revision, do not try to get a parent branch
945 lastonbranch = lastonbranch or revnum == 0
947 lastonbranch = lastonbranch or revnum == 0
946 continue
948 continue
947 cset, lastonbranch = parselogentry(paths, revnum, author,
949 cset, lastonbranch = parselogentry(paths, revnum, author,
948 date, message)
950 date, message)
949 if cset:
951 if cset:
950 firstcset = cset
952 firstcset = cset
951 if lastonbranch:
953 if lastonbranch:
952 break
954 break
953 finally:
955 finally:
954 stream.close()
956 stream.close()
955
957
956 if not lastonbranch and firstcset and not firstcset.parents:
958 if not lastonbranch and firstcset and not firstcset.parents:
957 # The first revision of the sequence (the last fetched one)
959 # The first revision of the sequence (the last fetched one)
958 # has invalid parents if not a branch root. Find the parent
960 # has invalid parents if not a branch root. Find the parent
959 # revision now, if any.
961 # revision now, if any.
960 try:
962 try:
961 firstrevnum = self.revnum(firstcset.rev)
963 firstrevnum = self.revnum(firstcset.rev)
962 if firstrevnum > 1:
964 if firstrevnum > 1:
963 latest = self.latest(self.module, firstrevnum - 1)
965 latest = self.latest(self.module, firstrevnum - 1)
964 if latest:
966 if latest:
965 firstcset.parents.append(latest)
967 firstcset.parents.append(latest)
966 except SvnPathNotFound:
968 except SvnPathNotFound:
967 pass
969 pass
968 except svn.core.SubversionException as xxx_todo_changeme:
970 except svn.core.SubversionException as xxx_todo_changeme:
969 (inst, num) = xxx_todo_changeme.args
971 (inst, num) = xxx_todo_changeme.args
970 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
972 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
971 raise error.Abort(_('svn: branch has no revision %s')
973 raise error.Abort(_('svn: branch has no revision %s')
972 % to_revnum)
974 % to_revnum)
973 raise
975 raise
974
976
975 def getfile(self, file, rev):
977 def getfile(self, file, rev):
976 # TODO: ra.get_file transmits the whole file instead of diffs.
978 # TODO: ra.get_file transmits the whole file instead of diffs.
977 if file in self.removed:
979 if file in self.removed:
978 return None, None
980 return None, None
979 mode = ''
981 mode = ''
980 try:
982 try:
981 new_module, revnum = revsplit(rev)[1:]
983 new_module, revnum = revsplit(rev)[1:]
982 if self.module != new_module:
984 if self.module != new_module:
983 self.module = new_module
985 self.module = new_module
984 self.reparent(self.module)
986 self.reparent(self.module)
985 io = stringio()
987 io = stringio()
986 info = svn.ra.get_file(self.ra, file, revnum, io)
988 info = svn.ra.get_file(self.ra, file, revnum, io)
987 data = io.getvalue()
989 data = io.getvalue()
988 # ra.get_file() seems to keep a reference on the input buffer
990 # ra.get_file() seems to keep a reference on the input buffer
989 # preventing collection. Release it explicitly.
991 # preventing collection. Release it explicitly.
990 io.close()
992 io.close()
991 if isinstance(info, list):
993 if isinstance(info, list):
992 info = info[-1]
994 info = info[-1]
993 mode = ("svn:executable" in info) and 'x' or ''
995 mode = ("svn:executable" in info) and 'x' or ''
994 mode = ("svn:special" in info) and 'l' or mode
996 mode = ("svn:special" in info) and 'l' or mode
995 except svn.core.SubversionException as e:
997 except svn.core.SubversionException as e:
996 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
998 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
997 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
999 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
998 if e.apr_err in notfound: # File not found
1000 if e.apr_err in notfound: # File not found
999 return None, None
1001 return None, None
1000 raise
1002 raise
1001 if mode == 'l':
1003 if mode == 'l':
1002 link_prefix = "link "
1004 link_prefix = "link "
1003 if data.startswith(link_prefix):
1005 if data.startswith(link_prefix):
1004 data = data[len(link_prefix):]
1006 data = data[len(link_prefix):]
1005 return data, mode
1007 return data, mode
1006
1008
1007 def _iterfiles(self, path, revnum):
1009 def _iterfiles(self, path, revnum):
1008 """Enumerate all files in path at revnum, recursively."""
1010 """Enumerate all files in path at revnum, recursively."""
1009 path = path.strip('/')
1011 path = path.strip('/')
1010 pool = svn.core.Pool()
1012 pool = svn.core.Pool()
1011 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1013 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1012 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1014 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1013 if path:
1015 if path:
1014 path += '/'
1016 path += '/'
1015 return ((path + p) for p, e in entries.iteritems()
1017 return ((path + p) for p, e in entries.iteritems()
1016 if e.kind == svn.core.svn_node_file)
1018 if e.kind == svn.core.svn_node_file)
1017
1019
1018 def getrelpath(self, path, module=None):
1020 def getrelpath(self, path, module=None):
1019 if module is None:
1021 if module is None:
1020 module = self.module
1022 module = self.module
1021 # Given the repository url of this wc, say
1023 # Given the repository url of this wc, say
1022 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1024 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1023 # extract the "entry" portion (a relative path) from what
1025 # extract the "entry" portion (a relative path) from what
1024 # svn log --xml says, i.e.
1026 # svn log --xml says, i.e.
1025 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1027 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1026 # that is to say "tests/PloneTestCase.py"
1028 # that is to say "tests/PloneTestCase.py"
1027 if path.startswith(module):
1029 if path.startswith(module):
1028 relative = path.rstrip('/')[len(module):]
1030 relative = path.rstrip('/')[len(module):]
1029 if relative.startswith('/'):
1031 if relative.startswith('/'):
1030 return relative[1:]
1032 return relative[1:]
1031 elif relative == '':
1033 elif relative == '':
1032 return relative
1034 return relative
1033
1035
1034 # The path is outside our tracked tree...
1036 # The path is outside our tracked tree...
1035 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1037 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1036 return None
1038 return None
1037
1039
1038 def _checkpath(self, path, revnum, module=None):
1040 def _checkpath(self, path, revnum, module=None):
1039 if module is not None:
1041 if module is not None:
1040 prevmodule = self.reparent('')
1042 prevmodule = self.reparent('')
1041 path = module + '/' + path
1043 path = module + '/' + path
1042 try:
1044 try:
1043 # ra.check_path does not like leading slashes very much, it leads
1045 # ra.check_path does not like leading slashes very much, it leads
1044 # to PROPFIND subversion errors
1046 # to PROPFIND subversion errors
1045 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1047 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1046 finally:
1048 finally:
1047 if module is not None:
1049 if module is not None:
1048 self.reparent(prevmodule)
1050 self.reparent(prevmodule)
1049
1051
1050 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1052 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1051 strict_node_history=False):
1053 strict_node_history=False):
1052 # Normalize path names, svn >= 1.5 only wants paths relative to
1054 # Normalize path names, svn >= 1.5 only wants paths relative to
1053 # supplied URL
1055 # supplied URL
1054 relpaths = []
1056 relpaths = []
1055 for p in paths:
1057 for p in paths:
1056 if not p.startswith('/'):
1058 if not p.startswith('/'):
1057 p = self.module + '/' + p
1059 p = self.module + '/' + p
1058 relpaths.append(p.strip('/'))
1060 relpaths.append(p.strip('/'))
1059 args = [self.baseurl, relpaths, start, end, limit,
1061 args = [self.baseurl, relpaths, start, end, limit,
1060 discover_changed_paths, strict_node_history]
1062 discover_changed_paths, strict_node_history]
1061 # developer config: convert.svn.debugsvnlog
1063 # developer config: convert.svn.debugsvnlog
1062 if not self.ui.configbool('convert', 'svn.debugsvnlog'):
1064 if not self.ui.configbool('convert', 'svn.debugsvnlog'):
1063 return directlogstream(*args)
1065 return directlogstream(*args)
1064 arg = encodeargs(args)
1066 arg = encodeargs(args)
1065 hgexe = util.hgexecutable()
1067 hgexe = util.hgexecutable()
1066 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1068 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1067 stdin, stdout = util.popen2(util.quotecommand(cmd))
1069 stdin, stdout = util.popen2(util.quotecommand(cmd))
1068 stdin.write(arg)
1070 stdin.write(arg)
1069 try:
1071 try:
1070 stdin.close()
1072 stdin.close()
1071 except IOError:
1073 except IOError:
1072 raise error.Abort(_('Mercurial failed to run itself, check'
1074 raise error.Abort(_('Mercurial failed to run itself, check'
1073 ' hg executable is in PATH'))
1075 ' hg executable is in PATH'))
1074 return logstream(stdout)
1076 return logstream(stdout)
1075
1077
1076 pre_revprop_change = '''#!/bin/sh
1078 pre_revprop_change = '''#!/bin/sh
1077
1079
1078 REPOS="$1"
1080 REPOS="$1"
1079 REV="$2"
1081 REV="$2"
1080 USER="$3"
1082 USER="$3"
1081 PROPNAME="$4"
1083 PROPNAME="$4"
1082 ACTION="$5"
1084 ACTION="$5"
1083
1085
1084 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1086 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1085 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1087 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1086 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1088 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1087
1089
1088 echo "Changing prohibited revision property" >&2
1090 echo "Changing prohibited revision property" >&2
1089 exit 1
1091 exit 1
1090 '''
1092 '''
1091
1093
1092 class svn_sink(converter_sink, commandline):
1094 class svn_sink(converter_sink, commandline):
1093 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1095 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1094 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1096 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1095
1097
1096 def prerun(self):
1098 def prerun(self):
1097 if self.wc:
1099 if self.wc:
1098 os.chdir(self.wc)
1100 os.chdir(self.wc)
1099
1101
1100 def postrun(self):
1102 def postrun(self):
1101 if self.wc:
1103 if self.wc:
1102 os.chdir(self.cwd)
1104 os.chdir(self.cwd)
1103
1105
1104 def join(self, name):
1106 def join(self, name):
1105 return os.path.join(self.wc, '.svn', name)
1107 return os.path.join(self.wc, '.svn', name)
1106
1108
1107 def revmapfile(self):
1109 def revmapfile(self):
1108 return self.join('hg-shamap')
1110 return self.join('hg-shamap')
1109
1111
1110 def authorfile(self):
1112 def authorfile(self):
1111 return self.join('hg-authormap')
1113 return self.join('hg-authormap')
1112
1114
1113 def __init__(self, ui, path):
1115 def __init__(self, ui, path):
1114
1116
1115 converter_sink.__init__(self, ui, path)
1117 converter_sink.__init__(self, ui, path)
1116 commandline.__init__(self, ui, 'svn')
1118 commandline.__init__(self, ui, 'svn')
1117 self.delete = []
1119 self.delete = []
1118 self.setexec = []
1120 self.setexec = []
1119 self.delexec = []
1121 self.delexec = []
1120 self.copies = []
1122 self.copies = []
1121 self.wc = None
1123 self.wc = None
1122 self.cwd = pycompat.getcwd()
1124 self.cwd = pycompat.getcwd()
1123
1125
1124 created = False
1126 created = False
1125 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1127 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1126 self.wc = os.path.realpath(path)
1128 self.wc = os.path.realpath(path)
1127 self.run0('update')
1129 self.run0('update')
1128 else:
1130 else:
1129 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1131 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1130 path = os.path.realpath(path)
1132 path = os.path.realpath(path)
1131 if os.path.isdir(os.path.dirname(path)):
1133 if os.path.isdir(os.path.dirname(path)):
1132 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1134 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1133 ui.status(_('initializing svn repository %r\n') %
1135 ui.status(_('initializing svn repository %r\n') %
1134 os.path.basename(path))
1136 os.path.basename(path))
1135 commandline(ui, 'svnadmin').run0('create', path)
1137 commandline(ui, 'svnadmin').run0('create', path)
1136 created = path
1138 created = path
1137 path = util.normpath(path)
1139 path = util.normpath(path)
1138 if not path.startswith('/'):
1140 if not path.startswith('/'):
1139 path = '/' + path
1141 path = '/' + path
1140 path = 'file://' + path
1142 path = 'file://' + path
1141
1143
1142 wcpath = os.path.join(pycompat.getcwd(), os.path.basename(path) +
1144 wcpath = os.path.join(pycompat.getcwd(), os.path.basename(path) +
1143 '-wc')
1145 '-wc')
1144 ui.status(_('initializing svn working copy %r\n')
1146 ui.status(_('initializing svn working copy %r\n')
1145 % os.path.basename(wcpath))
1147 % os.path.basename(wcpath))
1146 self.run0('checkout', path, wcpath)
1148 self.run0('checkout', path, wcpath)
1147
1149
1148 self.wc = wcpath
1150 self.wc = wcpath
1149 self.opener = vfsmod.vfs(self.wc)
1151 self.opener = vfsmod.vfs(self.wc)
1150 self.wopener = vfsmod.vfs(self.wc)
1152 self.wopener = vfsmod.vfs(self.wc)
1151 self.childmap = mapfile(ui, self.join('hg-childmap'))
1153 self.childmap = mapfile(ui, self.join('hg-childmap'))
1152 if util.checkexec(self.wc):
1154 if util.checkexec(self.wc):
1153 self.is_exec = util.isexec
1155 self.is_exec = util.isexec
1154 else:
1156 else:
1155 self.is_exec = None
1157 self.is_exec = None
1156
1158
1157 if created:
1159 if created:
1158 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1160 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1159 fp = open(hook, 'w')
1161 fp = open(hook, 'w')
1160 fp.write(pre_revprop_change)
1162 fp.write(pre_revprop_change)
1161 fp.close()
1163 fp.close()
1162 util.setflags(hook, False, True)
1164 util.setflags(hook, False, True)
1163
1165
1164 output = self.run0('info')
1166 output = self.run0('info')
1165 self.uuid = self.uuid_re.search(output).group(1).strip()
1167 self.uuid = self.uuid_re.search(output).group(1).strip()
1166
1168
1167 def wjoin(self, *names):
1169 def wjoin(self, *names):
1168 return os.path.join(self.wc, *names)
1170 return os.path.join(self.wc, *names)
1169
1171
1170 @propertycache
1172 @propertycache
1171 def manifest(self):
1173 def manifest(self):
1172 # As of svn 1.7, the "add" command fails when receiving
1174 # As of svn 1.7, the "add" command fails when receiving
1173 # already tracked entries, so we have to track and filter them
1175 # already tracked entries, so we have to track and filter them
1174 # ourselves.
1176 # ourselves.
1175 m = set()
1177 m = set()
1176 output = self.run0('ls', recursive=True, xml=True)
1178 output = self.run0('ls', recursive=True, xml=True)
1177 doc = xml.dom.minidom.parseString(output)
1179 doc = xml.dom.minidom.parseString(output)
1178 for e in doc.getElementsByTagName('entry'):
1180 for e in doc.getElementsByTagName('entry'):
1179 for n in e.childNodes:
1181 for n in e.childNodes:
1180 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1182 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1181 continue
1183 continue
1182 name = ''.join(c.data for c in n.childNodes
1184 name = ''.join(c.data for c in n.childNodes
1183 if c.nodeType == c.TEXT_NODE)
1185 if c.nodeType == c.TEXT_NODE)
1184 # Entries are compared with names coming from
1186 # Entries are compared with names coming from
1185 # mercurial, so bytes with undefined encoding. Our
1187 # mercurial, so bytes with undefined encoding. Our
1186 # best bet is to assume they are in local
1188 # best bet is to assume they are in local
1187 # encoding. They will be passed to command line calls
1189 # encoding. They will be passed to command line calls
1188 # later anyway, so they better be.
1190 # later anyway, so they better be.
1189 m.add(encoding.unitolocal(name))
1191 m.add(encoding.unitolocal(name))
1190 break
1192 break
1191 return m
1193 return m
1192
1194
1193 def putfile(self, filename, flags, data):
1195 def putfile(self, filename, flags, data):
1194 if 'l' in flags:
1196 if 'l' in flags:
1195 self.wopener.symlink(data, filename)
1197 self.wopener.symlink(data, filename)
1196 else:
1198 else:
1197 try:
1199 try:
1198 if os.path.islink(self.wjoin(filename)):
1200 if os.path.islink(self.wjoin(filename)):
1199 os.unlink(filename)
1201 os.unlink(filename)
1200 except OSError:
1202 except OSError:
1201 pass
1203 pass
1202 self.wopener.write(filename, data)
1204 self.wopener.write(filename, data)
1203
1205
1204 if self.is_exec:
1206 if self.is_exec:
1205 if self.is_exec(self.wjoin(filename)):
1207 if self.is_exec(self.wjoin(filename)):
1206 if 'x' not in flags:
1208 if 'x' not in flags:
1207 self.delexec.append(filename)
1209 self.delexec.append(filename)
1208 else:
1210 else:
1209 if 'x' in flags:
1211 if 'x' in flags:
1210 self.setexec.append(filename)
1212 self.setexec.append(filename)
1211 util.setflags(self.wjoin(filename), False, 'x' in flags)
1213 util.setflags(self.wjoin(filename), False, 'x' in flags)
1212
1214
1213 def _copyfile(self, source, dest):
1215 def _copyfile(self, source, dest):
1214 # SVN's copy command pukes if the destination file exists, but
1216 # SVN's copy command pukes if the destination file exists, but
1215 # our copyfile method expects to record a copy that has
1217 # our copyfile method expects to record a copy that has
1216 # already occurred. Cross the semantic gap.
1218 # already occurred. Cross the semantic gap.
1217 wdest = self.wjoin(dest)
1219 wdest = self.wjoin(dest)
1218 exists = os.path.lexists(wdest)
1220 exists = os.path.lexists(wdest)
1219 if exists:
1221 if exists:
1220 fd, tempname = tempfile.mkstemp(
1222 fd, tempname = tempfile.mkstemp(
1221 prefix='hg-copy-', dir=os.path.dirname(wdest))
1223 prefix='hg-copy-', dir=os.path.dirname(wdest))
1222 os.close(fd)
1224 os.close(fd)
1223 os.unlink(tempname)
1225 os.unlink(tempname)
1224 os.rename(wdest, tempname)
1226 os.rename(wdest, tempname)
1225 try:
1227 try:
1226 self.run0('copy', source, dest)
1228 self.run0('copy', source, dest)
1227 finally:
1229 finally:
1228 self.manifest.add(dest)
1230 self.manifest.add(dest)
1229 if exists:
1231 if exists:
1230 try:
1232 try:
1231 os.unlink(wdest)
1233 os.unlink(wdest)
1232 except OSError:
1234 except OSError:
1233 pass
1235 pass
1234 os.rename(tempname, wdest)
1236 os.rename(tempname, wdest)
1235
1237
1236 def dirs_of(self, files):
1238 def dirs_of(self, files):
1237 dirs = set()
1239 dirs = set()
1238 for f in files:
1240 for f in files:
1239 if os.path.isdir(self.wjoin(f)):
1241 if os.path.isdir(self.wjoin(f)):
1240 dirs.add(f)
1242 dirs.add(f)
1241 i = len(f)
1243 i = len(f)
1242 for i in iter(lambda: f.rfind('/', 0, i), -1):
1244 for i in iter(lambda: f.rfind('/', 0, i), -1):
1243 dirs.add(f[:i])
1245 dirs.add(f[:i])
1244 return dirs
1246 return dirs
1245
1247
1246 def add_dirs(self, files):
1248 def add_dirs(self, files):
1247 add_dirs = [d for d in sorted(self.dirs_of(files))
1249 add_dirs = [d for d in sorted(self.dirs_of(files))
1248 if d not in self.manifest]
1250 if d not in self.manifest]
1249 if add_dirs:
1251 if add_dirs:
1250 self.manifest.update(add_dirs)
1252 self.manifest.update(add_dirs)
1251 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1253 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1252 return add_dirs
1254 return add_dirs
1253
1255
1254 def add_files(self, files):
1256 def add_files(self, files):
1255 files = [f for f in files if f not in self.manifest]
1257 files = [f for f in files if f not in self.manifest]
1256 if files:
1258 if files:
1257 self.manifest.update(files)
1259 self.manifest.update(files)
1258 self.xargs(files, 'add', quiet=True)
1260 self.xargs(files, 'add', quiet=True)
1259 return files
1261 return files
1260
1262
1261 def addchild(self, parent, child):
1263 def addchild(self, parent, child):
1262 self.childmap[parent] = child
1264 self.childmap[parent] = child
1263
1265
1264 def revid(self, rev):
1266 def revid(self, rev):
1265 return u"svn:%s@%s" % (self.uuid, rev)
1267 return u"svn:%s@%s" % (self.uuid, rev)
1266
1268
1267 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1269 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1268 cleanp2):
1270 cleanp2):
1269 for parent in parents:
1271 for parent in parents:
1270 try:
1272 try:
1271 return self.revid(self.childmap[parent])
1273 return self.revid(self.childmap[parent])
1272 except KeyError:
1274 except KeyError:
1273 pass
1275 pass
1274
1276
1275 # Apply changes to working copy
1277 # Apply changes to working copy
1276 for f, v in files:
1278 for f, v in files:
1277 data, mode = source.getfile(f, v)
1279 data, mode = source.getfile(f, v)
1278 if data is None:
1280 if data is None:
1279 self.delete.append(f)
1281 self.delete.append(f)
1280 else:
1282 else:
1281 self.putfile(f, mode, data)
1283 self.putfile(f, mode, data)
1282 if f in copies:
1284 if f in copies:
1283 self.copies.append([copies[f], f])
1285 self.copies.append([copies[f], f])
1284 if full:
1286 if full:
1285 self.delete.extend(sorted(self.manifest.difference(files)))
1287 self.delete.extend(sorted(self.manifest.difference(files)))
1286 files = [f[0] for f in files]
1288 files = [f[0] for f in files]
1287
1289
1288 entries = set(self.delete)
1290 entries = set(self.delete)
1289 files = frozenset(files)
1291 files = frozenset(files)
1290 entries.update(self.add_dirs(files.difference(entries)))
1292 entries.update(self.add_dirs(files.difference(entries)))
1291 if self.copies:
1293 if self.copies:
1292 for s, d in self.copies:
1294 for s, d in self.copies:
1293 self._copyfile(s, d)
1295 self._copyfile(s, d)
1294 self.copies = []
1296 self.copies = []
1295 if self.delete:
1297 if self.delete:
1296 self.xargs(self.delete, 'delete')
1298 self.xargs(self.delete, 'delete')
1297 for f in self.delete:
1299 for f in self.delete:
1298 self.manifest.remove(f)
1300 self.manifest.remove(f)
1299 self.delete = []
1301 self.delete = []
1300 entries.update(self.add_files(files.difference(entries)))
1302 entries.update(self.add_files(files.difference(entries)))
1301 if self.delexec:
1303 if self.delexec:
1302 self.xargs(self.delexec, 'propdel', 'svn:executable')
1304 self.xargs(self.delexec, 'propdel', 'svn:executable')
1303 self.delexec = []
1305 self.delexec = []
1304 if self.setexec:
1306 if self.setexec:
1305 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1307 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1306 self.setexec = []
1308 self.setexec = []
1307
1309
1308 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1310 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1309 fp = os.fdopen(fd, pycompat.sysstr('w'))
1311 fp = os.fdopen(fd, pycompat.sysstr('w'))
1310 fp.write(commit.desc)
1312 fp.write(commit.desc)
1311 fp.close()
1313 fp.close()
1312 try:
1314 try:
1313 output = self.run0('commit',
1315 output = self.run0('commit',
1314 username=util.shortuser(commit.author),
1316 username=util.shortuser(commit.author),
1315 file=messagefile,
1317 file=messagefile,
1316 encoding='utf-8')
1318 encoding='utf-8')
1317 try:
1319 try:
1318 rev = self.commit_re.search(output).group(1)
1320 rev = self.commit_re.search(output).group(1)
1319 except AttributeError:
1321 except AttributeError:
1320 if parents and not files:
1322 if parents and not files:
1321 return parents[0]
1323 return parents[0]
1322 self.ui.warn(_('unexpected svn output:\n'))
1324 self.ui.warn(_('unexpected svn output:\n'))
1323 self.ui.warn(output)
1325 self.ui.warn(output)
1324 raise error.Abort(_('unable to cope with svn output'))
1326 raise error.Abort(_('unable to cope with svn output'))
1325 if commit.rev:
1327 if commit.rev:
1326 self.run('propset', 'hg:convert-rev', commit.rev,
1328 self.run('propset', 'hg:convert-rev', commit.rev,
1327 revprop=True, revision=rev)
1329 revprop=True, revision=rev)
1328 if commit.branch and commit.branch != 'default':
1330 if commit.branch and commit.branch != 'default':
1329 self.run('propset', 'hg:convert-branch', commit.branch,
1331 self.run('propset', 'hg:convert-branch', commit.branch,
1330 revprop=True, revision=rev)
1332 revprop=True, revision=rev)
1331 for parent in parents:
1333 for parent in parents:
1332 self.addchild(parent, rev)
1334 self.addchild(parent, rev)
1333 return self.revid(rev)
1335 return self.revid(rev)
1334 finally:
1336 finally:
1335 os.unlink(messagefile)
1337 os.unlink(messagefile)
1336
1338
1337 def puttags(self, tags):
1339 def puttags(self, tags):
1338 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1340 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1339 return None, None
1341 return None, None
1340
1342
1341 def hascommitfrommap(self, rev):
1343 def hascommitfrommap(self, rev):
1342 # We trust that revisions referenced in a map still is present
1344 # We trust that revisions referenced in a map still is present
1343 # TODO: implement something better if necessary and feasible
1345 # TODO: implement something better if necessary and feasible
1344 return True
1346 return True
1345
1347
1346 def hascommitforsplicemap(self, rev):
1348 def hascommitforsplicemap(self, rev):
1347 # This is not correct as one can convert to an existing subversion
1349 # This is not correct as one can convert to an existing subversion
1348 # repository and childmap would not list all revisions. Too bad.
1350 # repository and childmap would not list all revisions. Too bad.
1349 if rev in self.childmap:
1351 if rev in self.childmap:
1350 return True
1352 return True
1351 raise error.Abort(_('splice map revision %s not found in subversion '
1353 raise error.Abort(_('splice map revision %s not found in subversion '
1352 'child map (revision lookups are not implemented)')
1354 'child map (revision lookups are not implemented)')
1353 % rev)
1355 % rev)
General Comments 0
You need to be logged in to leave comments. Login now