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