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