##// END OF EJS Templates
configitems: register the 'convert.hg.tagsbranch' config
Boris Feld -
r34170:84044b90 default
parent child Browse files
Show More
@@ -1,567 +1,570 b''
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.mergefrom',
41 41 default=None,
42 42 )
43 43 configitem('convert', 'cvsps.mergeto',
44 44 default=None,
45 45 )
46 46 configitem('convert', 'git.committeractions',
47 47 default=lambda: ['messagedifferent'],
48 48 )
49 49 configitem('convert', 'git.extrakeys',
50 50 default=list,
51 51 )
52 52 configitem('convert', 'git.findcopiesharder',
53 53 default=False,
54 54 )
55 55 configitem('convert', 'git.remoteprefix',
56 56 default='remote',
57 57 )
58 58 configitem('convert', 'git.renamelimit',
59 59 default=400,
60 60 )
61 61 configitem('convert', 'git.saverev',
62 62 default=True,
63 63 )
64 64 configitem('convert', 'git.similarity',
65 65 default=50,
66 66 )
67 67 configitem('convert', 'git.skipsubmodules',
68 68 default=False,
69 69 )
70 70 configitem('convert', 'hg.clonebranches',
71 71 default=False,
72 72 )
73 73 configitem('convert', 'hg.ignoreerrors',
74 74 default=False,
75 75 )
76 76 configitem('convert', 'hg.revs',
77 77 default=None,
78 78 )
79 79 configitem('convert', 'hg.saverev',
80 80 default=False,
81 81 )
82 82 configitem('convert', 'hg.sourcename',
83 83 default=None,
84 84 )
85 85 configitem('convert', 'hg.startrev',
86 86 default=None,
87 87 )
88 configitem('convert', 'hg.tagsbranch',
89 default='default',
90 )
88 91
89 92 # Commands definition was moved elsewhere to ease demandload job.
90 93
91 94 @command('convert',
92 95 [('', 'authors', '',
93 96 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
94 97 _('FILE')),
95 98 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
96 99 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
97 100 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
98 101 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
99 102 ('', 'filemap', '', _('remap file names using contents of file'),
100 103 _('FILE')),
101 104 ('', 'full', None,
102 105 _('apply filemap changes by converting all files again')),
103 106 ('', 'splicemap', '', _('splice synthesized history into place'),
104 107 _('FILE')),
105 108 ('', 'branchmap', '', _('change branch names while converting'),
106 109 _('FILE')),
107 110 ('', 'branchsort', None, _('try to sort changesets by branches')),
108 111 ('', 'datesort', None, _('try to sort changesets by date')),
109 112 ('', 'sourcesort', None, _('preserve source changesets order')),
110 113 ('', 'closesort', None, _('try to reorder closed revisions'))],
111 114 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
112 115 norepo=True)
113 116 def convert(ui, src, dest=None, revmapfile=None, **opts):
114 117 """convert a foreign SCM repository to a Mercurial one.
115 118
116 119 Accepted source formats [identifiers]:
117 120
118 121 - Mercurial [hg]
119 122 - CVS [cvs]
120 123 - Darcs [darcs]
121 124 - git [git]
122 125 - Subversion [svn]
123 126 - Monotone [mtn]
124 127 - GNU Arch [gnuarch]
125 128 - Bazaar [bzr]
126 129 - Perforce [p4]
127 130
128 131 Accepted destination formats [identifiers]:
129 132
130 133 - Mercurial [hg]
131 134 - Subversion [svn] (history on branches is not preserved)
132 135
133 136 If no revision is given, all revisions will be converted.
134 137 Otherwise, convert will only import up to the named revision
135 138 (given in a format understood by the source).
136 139
137 140 If no destination directory name is specified, it defaults to the
138 141 basename of the source with ``-hg`` appended. If the destination
139 142 repository doesn't exist, it will be created.
140 143
141 144 By default, all sources except Mercurial will use --branchsort.
142 145 Mercurial uses --sourcesort to preserve original revision numbers
143 146 order. Sort modes have the following effects:
144 147
145 148 --branchsort convert from parent to child revision when possible,
146 149 which means branches are usually converted one after
147 150 the other. It generates more compact repositories.
148 151
149 152 --datesort sort revisions by date. Converted repositories have
150 153 good-looking changelogs but are often an order of
151 154 magnitude larger than the same ones generated by
152 155 --branchsort.
153 156
154 157 --sourcesort try to preserve source revisions order, only
155 158 supported by Mercurial sources.
156 159
157 160 --closesort try to move closed revisions as close as possible
158 161 to parent branches, only supported by Mercurial
159 162 sources.
160 163
161 164 If ``REVMAP`` isn't given, it will be put in a default location
162 165 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
163 166 text file that maps each source commit ID to the destination ID
164 167 for that revision, like so::
165 168
166 169 <source ID> <destination ID>
167 170
168 171 If the file doesn't exist, it's automatically created. It's
169 172 updated on each commit copied, so :hg:`convert` can be interrupted
170 173 and can be run repeatedly to copy new commits.
171 174
172 175 The authormap is a simple text file that maps each source commit
173 176 author to a destination commit author. It is handy for source SCMs
174 177 that use unix logins to identify authors (e.g.: CVS). One line per
175 178 author mapping and the line format is::
176 179
177 180 source author = destination author
178 181
179 182 Empty lines and lines starting with a ``#`` are ignored.
180 183
181 184 The filemap is a file that allows filtering and remapping of files
182 185 and directories. Each line can contain one of the following
183 186 directives::
184 187
185 188 include path/to/file-or-dir
186 189
187 190 exclude path/to/file-or-dir
188 191
189 192 rename path/to/source path/to/destination
190 193
191 194 Comment lines start with ``#``. A specified path matches if it
192 195 equals the full relative name of a file or one of its parent
193 196 directories. The ``include`` or ``exclude`` directive with the
194 197 longest matching path applies, so line order does not matter.
195 198
196 199 The ``include`` directive causes a file, or all files under a
197 200 directory, to be included in the destination repository. The default
198 201 if there are no ``include`` statements is to include everything.
199 202 If there are any ``include`` statements, nothing else is included.
200 203 The ``exclude`` directive causes files or directories to
201 204 be omitted. The ``rename`` directive renames a file or directory if
202 205 it is converted. To rename from a subdirectory into the root of
203 206 the repository, use ``.`` as the path to rename to.
204 207
205 208 ``--full`` will make sure the converted changesets contain exactly
206 209 the right files with the right content. It will make a full
207 210 conversion of all files, not just the ones that have
208 211 changed. Files that already are correct will not be changed. This
209 212 can be used to apply filemap changes when converting
210 213 incrementally. This is currently only supported for Mercurial and
211 214 Subversion.
212 215
213 216 The splicemap is a file that allows insertion of synthetic
214 217 history, letting you specify the parents of a revision. This is
215 218 useful if you want to e.g. give a Subversion merge two parents, or
216 219 graft two disconnected series of history together. Each entry
217 220 contains a key, followed by a space, followed by one or two
218 221 comma-separated values::
219 222
220 223 key parent1, parent2
221 224
222 225 The key is the revision ID in the source
223 226 revision control system whose parents should be modified (same
224 227 format as a key in .hg/shamap). The values are the revision IDs
225 228 (in either the source or destination revision control system) that
226 229 should be used as the new parents for that node. For example, if
227 230 you have merged "release-1.0" into "trunk", then you should
228 231 specify the revision on "trunk" as the first parent and the one on
229 232 the "release-1.0" branch as the second.
230 233
231 234 The branchmap is a file that allows you to rename a branch when it is
232 235 being brought in from whatever external repository. When used in
233 236 conjunction with a splicemap, it allows for a powerful combination
234 237 to help fix even the most badly mismanaged repositories and turn them
235 238 into nicely structured Mercurial repositories. The branchmap contains
236 239 lines of the form::
237 240
238 241 original_branch_name new_branch_name
239 242
240 243 where "original_branch_name" is the name of the branch in the
241 244 source repository, and "new_branch_name" is the name of the branch
242 245 is the destination repository. No whitespace is allowed in the new
243 246 branch name. This can be used to (for instance) move code in one
244 247 repository from "default" to a named branch.
245 248
246 249 Mercurial Source
247 250 ################
248 251
249 252 The Mercurial source recognizes the following configuration
250 253 options, which you can set on the command line with ``--config``:
251 254
252 255 :convert.hg.ignoreerrors: ignore integrity errors when reading.
253 256 Use it to fix Mercurial repositories with missing revlogs, by
254 257 converting from and to Mercurial. Default is False.
255 258
256 259 :convert.hg.saverev: store original revision ID in changeset
257 260 (forces target IDs to change). It takes a boolean argument and
258 261 defaults to False.
259 262
260 263 :convert.hg.startrev: specify the initial Mercurial revision.
261 264 The default is 0.
262 265
263 266 :convert.hg.revs: revset specifying the source revisions to convert.
264 267
265 268 CVS Source
266 269 ##########
267 270
268 271 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
269 272 to indicate the starting point of what will be converted. Direct
270 273 access to the repository files is not needed, unless of course the
271 274 repository is ``:local:``. The conversion uses the top level
272 275 directory in the sandbox to find the CVS repository, and then uses
273 276 CVS rlog commands to find files to convert. This means that unless
274 277 a filemap is given, all files under the starting directory will be
275 278 converted, and that any directory reorganization in the CVS
276 279 sandbox is ignored.
277 280
278 281 The following options can be used with ``--config``:
279 282
280 283 :convert.cvsps.cache: Set to False to disable remote log caching,
281 284 for testing and debugging purposes. Default is True.
282 285
283 286 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
284 287 allowed between commits with identical user and log message in
285 288 a single changeset. When very large files were checked in as
286 289 part of a changeset then the default may not be long enough.
287 290 The default is 60.
288 291
289 292 :convert.cvsps.logencoding: Specify encoding name to be used for
290 293 transcoding CVS log messages. Multiple encoding names can be
291 294 specified as a list (see :hg:`help config.Syntax`), but only
292 295 the first acceptable encoding in the list is used per CVS log
293 296 entries. This transcoding is executed before cvslog hook below.
294 297
295 298 :convert.cvsps.mergeto: Specify a regular expression to which
296 299 commit log messages are matched. If a match occurs, then the
297 300 conversion process will insert a dummy revision merging the
298 301 branch on which this log message occurs to the branch
299 302 indicated in the regex. Default is ``{{mergetobranch
300 303 ([-\\w]+)}}``
301 304
302 305 :convert.cvsps.mergefrom: Specify a regular expression to which
303 306 commit log messages are matched. If a match occurs, then the
304 307 conversion process will add the most recent revision on the
305 308 branch indicated in the regex as the second parent of the
306 309 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
307 310
308 311 :convert.localtimezone: use local time (as determined by the TZ
309 312 environment variable) for changeset date/times. The default
310 313 is False (use UTC).
311 314
312 315 :hooks.cvslog: Specify a Python function to be called at the end of
313 316 gathering the CVS log. The function is passed a list with the
314 317 log entries, and can modify the entries in-place, or add or
315 318 delete them.
316 319
317 320 :hooks.cvschangesets: Specify a Python function to be called after
318 321 the changesets are calculated from the CVS log. The
319 322 function is passed a list with the changeset entries, and can
320 323 modify the changesets in-place, or add or delete them.
321 324
322 325 An additional "debugcvsps" Mercurial command allows the builtin
323 326 changeset merging code to be run without doing a conversion. Its
324 327 parameters and output are similar to that of cvsps 2.1. Please see
325 328 the command help for more details.
326 329
327 330 Subversion Source
328 331 #################
329 332
330 333 Subversion source detects classical trunk/branches/tags layouts.
331 334 By default, the supplied ``svn://repo/path/`` source URL is
332 335 converted as a single branch. If ``svn://repo/path/trunk`` exists
333 336 it replaces the default branch. If ``svn://repo/path/branches``
334 337 exists, its subdirectories are listed as possible branches. If
335 338 ``svn://repo/path/tags`` exists, it is looked for tags referencing
336 339 converted branches. Default ``trunk``, ``branches`` and ``tags``
337 340 values can be overridden with following options. Set them to paths
338 341 relative to the source URL, or leave them blank to disable auto
339 342 detection.
340 343
341 344 The following options can be set with ``--config``:
342 345
343 346 :convert.svn.branches: specify the directory containing branches.
344 347 The default is ``branches``.
345 348
346 349 :convert.svn.tags: specify the directory containing tags. The
347 350 default is ``tags``.
348 351
349 352 :convert.svn.trunk: specify the name of the trunk branch. The
350 353 default is ``trunk``.
351 354
352 355 :convert.localtimezone: use local time (as determined by the TZ
353 356 environment variable) for changeset date/times. The default
354 357 is False (use UTC).
355 358
356 359 Source history can be retrieved starting at a specific revision,
357 360 instead of being integrally converted. Only single branch
358 361 conversions are supported.
359 362
360 363 :convert.svn.startrev: specify start Subversion revision number.
361 364 The default is 0.
362 365
363 366 Git Source
364 367 ##########
365 368
366 369 The Git importer converts commits from all reachable branches (refs
367 370 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
368 371 Branches are converted to bookmarks with the same name, with the
369 372 leading 'refs/heads' stripped. Git submodules are converted to Git
370 373 subrepos in Mercurial.
371 374
372 375 The following options can be set with ``--config``:
373 376
374 377 :convert.git.similarity: specify how similar files modified in a
375 378 commit must be to be imported as renames or copies, as a
376 379 percentage between ``0`` (disabled) and ``100`` (files must be
377 380 identical). For example, ``90`` means that a delete/add pair will
378 381 be imported as a rename if more than 90% of the file hasn't
379 382 changed. The default is ``50``.
380 383
381 384 :convert.git.findcopiesharder: while detecting copies, look at all
382 385 files in the working copy instead of just changed ones. This
383 386 is very expensive for large projects, and is only effective when
384 387 ``convert.git.similarity`` is greater than 0. The default is False.
385 388
386 389 :convert.git.renamelimit: perform rename and copy detection up to this
387 390 many changed files in a commit. Increasing this will make rename
388 391 and copy detection more accurate but will significantly slow down
389 392 computation on large projects. The option is only relevant if
390 393 ``convert.git.similarity`` is greater than 0. The default is
391 394 ``400``.
392 395
393 396 :convert.git.committeractions: list of actions to take when processing
394 397 author and committer values.
395 398
396 399 Git commits have separate author (who wrote the commit) and committer
397 400 (who applied the commit) fields. Not all destinations support separate
398 401 author and committer fields (including Mercurial). This config option
399 402 controls what to do with these author and committer fields during
400 403 conversion.
401 404
402 405 A value of ``messagedifferent`` will append a ``committer: ...``
403 406 line to the commit message if the Git committer is different from the
404 407 author. The prefix of that line can be specified using the syntax
405 408 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
406 409 When a prefix is specified, a space will always be inserted between the
407 410 prefix and the value.
408 411
409 412 ``messagealways`` behaves like ``messagedifferent`` except it will
410 413 always result in a ``committer: ...`` line being appended to the commit
411 414 message. This value is mutually exclusive with ``messagedifferent``.
412 415
413 416 ``dropcommitter`` will remove references to the committer. Only
414 417 references to the author will remain. Actions that add references
415 418 to the committer will have no effect when this is set.
416 419
417 420 ``replaceauthor`` will replace the value of the author field with
418 421 the committer. Other actions that add references to the committer
419 422 will still take effect when this is set.
420 423
421 424 The default is ``messagedifferent``.
422 425
423 426 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
424 427 the destination. Some Git repositories store extra metadata in commits.
425 428 By default, this non-default metadata will be lost during conversion.
426 429 Setting this config option can retain that metadata. Some built-in
427 430 keys such as ``parent`` and ``branch`` are not allowed to be copied.
428 431
429 432 :convert.git.remoteprefix: remote refs are converted as bookmarks with
430 433 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
431 434 is 'remote'.
432 435
433 436 :convert.git.saverev: whether to store the original Git commit ID in the
434 437 metadata of the destination commit. The default is True.
435 438
436 439 :convert.git.skipsubmodules: does not convert root level .gitmodules files
437 440 or files with 160000 mode indicating a submodule. Default is False.
438 441
439 442 Perforce Source
440 443 ###############
441 444
442 445 The Perforce (P4) importer can be given a p4 depot path or a
443 446 client specification as source. It will convert all files in the
444 447 source to a flat Mercurial repository, ignoring labels, branches
445 448 and integrations. Note that when a depot path is given you then
446 449 usually should specify a target directory, because otherwise the
447 450 target may be named ``...-hg``.
448 451
449 452 The following options can be set with ``--config``:
450 453
451 454 :convert.p4.encoding: specify the encoding to use when decoding standard
452 455 output of the Perforce command line tool. The default is default system
453 456 encoding.
454 457
455 458 :convert.p4.startrev: specify initial Perforce revision (a
456 459 Perforce changelist number).
457 460
458 461 Mercurial Destination
459 462 #####################
460 463
461 464 The Mercurial destination will recognize Mercurial subrepositories in the
462 465 destination directory, and update the .hgsubstate file automatically if the
463 466 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
464 467 Converting a repository with subrepositories requires converting a single
465 468 repository at a time, from the bottom up.
466 469
467 470 .. container:: verbose
468 471
469 472 An example showing how to convert a repository with subrepositories::
470 473
471 474 # so convert knows the type when it sees a non empty destination
472 475 $ hg init converted
473 476
474 477 $ hg convert orig/sub1 converted/sub1
475 478 $ hg convert orig/sub2 converted/sub2
476 479 $ hg convert orig converted
477 480
478 481 The following options are supported:
479 482
480 483 :convert.hg.clonebranches: dispatch source branches in separate
481 484 clones. The default is False.
482 485
483 486 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
484 487 ``default``.
485 488
486 489 :convert.hg.usebranchnames: preserve branch names. The default is
487 490 True.
488 491
489 492 :convert.hg.sourcename: records the given string as a 'convert_source' extra
490 493 value on each commit made in the target repository. The default is None.
491 494
492 495 All Destinations
493 496 ################
494 497
495 498 All destination types accept the following options:
496 499
497 500 :convert.skiptags: does not convert tags from the source repo to the target
498 501 repo. The default is False.
499 502 """
500 503 return convcmd.convert(ui, src, dest, revmapfile, **opts)
501 504
502 505 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
503 506 def debugsvnlog(ui, **opts):
504 507 return subversion.debugsvnlog(ui, **opts)
505 508
506 509 @command('debugcvsps',
507 510 [
508 511 # Main options shared with cvsps-2.1
509 512 ('b', 'branches', [], _('only return changes on specified branches')),
510 513 ('p', 'prefix', '', _('prefix to remove from file names')),
511 514 ('r', 'revisions', [],
512 515 _('only return changes after or between specified tags')),
513 516 ('u', 'update-cache', None, _("update cvs log cache")),
514 517 ('x', 'new-cache', None, _("create new cvs log cache")),
515 518 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
516 519 ('', 'root', '', _('specify cvsroot')),
517 520 # Options specific to builtin cvsps
518 521 ('', 'parents', '', _('show parent changesets')),
519 522 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
520 523 # Options that are ignored for compatibility with cvsps-2.1
521 524 ('A', 'cvs-direct', None, _('ignored for compatibility')),
522 525 ],
523 526 _('hg debugcvsps [OPTION]... [PATH]...'),
524 527 norepo=True)
525 528 def debugcvsps(ui, *args, **opts):
526 529 '''create changeset information from CVS
527 530
528 531 This command is intended as a debugging tool for the CVS to
529 532 Mercurial converter, and can be used as a direct replacement for
530 533 cvsps.
531 534
532 535 Hg debugcvsps reads the CVS rlog for current directory (or any
533 536 named directory) in the CVS repository, and converts the log to a
534 537 series of changesets based on matching commit log entries and
535 538 dates.'''
536 539 return cvsps.debugcvsps(ui, *args, **opts)
537 540
538 541 def kwconverted(ctx, name):
539 542 rev = ctx.extra().get('convert_revision', '')
540 543 if rev.startswith('svn:'):
541 544 if name == 'svnrev':
542 545 return str(subversion.revsplit(rev)[2])
543 546 elif name == 'svnpath':
544 547 return subversion.revsplit(rev)[1]
545 548 elif name == 'svnuuid':
546 549 return subversion.revsplit(rev)[0]
547 550 return rev
548 551
549 552 templatekeyword = registrar.templatekeyword()
550 553
551 554 @templatekeyword('svnrev')
552 555 def kwsvnrev(repo, ctx, **args):
553 556 """String. Converted subversion revision number."""
554 557 return kwconverted(ctx, 'svnrev')
555 558
556 559 @templatekeyword('svnpath')
557 560 def kwsvnpath(repo, ctx, **args):
558 561 """String. Converted subversion revision project path."""
559 562 return kwconverted(ctx, 'svnpath')
560 563
561 564 @templatekeyword('svnuuid')
562 565 def kwsvnuuid(repo, ctx, **args):
563 566 """String. Converted subversion revision repository identifier."""
564 567 return kwconverted(ctx, 'svnuuid')
565 568
566 569 # tell hggettext to extract docstrings from these functions:
567 570 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,655 +1,655 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19 from __future__ import absolute_import
20 20
21 21 import os
22 22 import re
23 23 import time
24 24
25 25 from mercurial.i18n import _
26 26 from mercurial import (
27 27 bookmarks,
28 28 context,
29 29 error,
30 30 exchange,
31 31 hg,
32 32 lock as lockmod,
33 33 merge as mergemod,
34 34 node as nodemod,
35 35 phases,
36 36 scmutil,
37 37 util,
38 38 )
39 39 stringio = util.stringio
40 40
41 41 from . import common
42 42 mapfile = common.mapfile
43 43 NoRepo = common.NoRepo
44 44
45 45 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
46 46
47 47 class mercurial_sink(common.converter_sink):
48 48 def __init__(self, ui, path):
49 49 common.converter_sink.__init__(self, ui, path)
50 50 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
51 51 self.clonebranches = ui.configbool('convert', 'hg.clonebranches')
52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch')
53 53 self.lastbranch = None
54 54 if os.path.isdir(path) and len(os.listdir(path)) > 0:
55 55 try:
56 56 self.repo = hg.repository(self.ui, path)
57 57 if not self.repo.local():
58 58 raise NoRepo(_('%s is not a local Mercurial repository')
59 59 % path)
60 60 except error.RepoError as err:
61 61 ui.traceback()
62 62 raise NoRepo(err.args[0])
63 63 else:
64 64 try:
65 65 ui.status(_('initializing destination %s repository\n') % path)
66 66 self.repo = hg.repository(self.ui, path, create=True)
67 67 if not self.repo.local():
68 68 raise NoRepo(_('%s is not a local Mercurial repository')
69 69 % path)
70 70 self.created.append(path)
71 71 except error.RepoError:
72 72 ui.traceback()
73 73 raise NoRepo(_("could not create hg repository %s as sink")
74 74 % path)
75 75 self.lock = None
76 76 self.wlock = None
77 77 self.filemapmode = False
78 78 self.subrevmaps = {}
79 79
80 80 def before(self):
81 81 self.ui.debug('run hg sink pre-conversion action\n')
82 82 self.wlock = self.repo.wlock()
83 83 self.lock = self.repo.lock()
84 84
85 85 def after(self):
86 86 self.ui.debug('run hg sink post-conversion action\n')
87 87 if self.lock:
88 88 self.lock.release()
89 89 if self.wlock:
90 90 self.wlock.release()
91 91
92 92 def revmapfile(self):
93 93 return self.repo.vfs.join("shamap")
94 94
95 95 def authorfile(self):
96 96 return self.repo.vfs.join("authormap")
97 97
98 98 def setbranch(self, branch, pbranches):
99 99 if not self.clonebranches:
100 100 return
101 101
102 102 setbranch = (branch != self.lastbranch)
103 103 self.lastbranch = branch
104 104 if not branch:
105 105 branch = 'default'
106 106 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
107 107 if pbranches:
108 108 pbranch = pbranches[0][1]
109 109 else:
110 110 pbranch = 'default'
111 111
112 112 branchpath = os.path.join(self.path, branch)
113 113 if setbranch:
114 114 self.after()
115 115 try:
116 116 self.repo = hg.repository(self.ui, branchpath)
117 117 except Exception:
118 118 self.repo = hg.repository(self.ui, branchpath, create=True)
119 119 self.before()
120 120
121 121 # pbranches may bring revisions from other branches (merge parents)
122 122 # Make sure we have them, or pull them.
123 123 missings = {}
124 124 for b in pbranches:
125 125 try:
126 126 self.repo.lookup(b[0])
127 127 except Exception:
128 128 missings.setdefault(b[1], []).append(b[0])
129 129
130 130 if missings:
131 131 self.after()
132 132 for pbranch, heads in sorted(missings.iteritems()):
133 133 pbranchpath = os.path.join(self.path, pbranch)
134 134 prepo = hg.peer(self.ui, {}, pbranchpath)
135 135 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
136 136 exchange.pull(self.repo, prepo,
137 137 [prepo.lookup(h) for h in heads])
138 138 self.before()
139 139
140 140 def _rewritetags(self, source, revmap, data):
141 141 fp = stringio()
142 142 for line in data.splitlines():
143 143 s = line.split(' ', 1)
144 144 if len(s) != 2:
145 145 continue
146 146 revid = revmap.get(source.lookuprev(s[0]))
147 147 if not revid:
148 148 if s[0] == nodemod.nullhex:
149 149 revid = s[0]
150 150 else:
151 151 continue
152 152 fp.write('%s %s\n' % (revid, s[1]))
153 153 return fp.getvalue()
154 154
155 155 def _rewritesubstate(self, source, data):
156 156 fp = stringio()
157 157 for line in data.splitlines():
158 158 s = line.split(' ', 1)
159 159 if len(s) != 2:
160 160 continue
161 161
162 162 revid = s[0]
163 163 subpath = s[1]
164 164 if revid != nodemod.nullhex:
165 165 revmap = self.subrevmaps.get(subpath)
166 166 if revmap is None:
167 167 revmap = mapfile(self.ui,
168 168 self.repo.wjoin(subpath, '.hg/shamap'))
169 169 self.subrevmaps[subpath] = revmap
170 170
171 171 # It is reasonable that one or more of the subrepos don't
172 172 # need to be converted, in which case they can be cloned
173 173 # into place instead of converted. Therefore, only warn
174 174 # once.
175 175 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
176 176 if len(revmap) == 0:
177 177 sub = self.repo.wvfs.reljoin(subpath, '.hg')
178 178
179 179 if self.repo.wvfs.exists(sub):
180 180 self.ui.warn(msg % subpath)
181 181
182 182 newid = revmap.get(revid)
183 183 if not newid:
184 184 if len(revmap) > 0:
185 185 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
186 186 (revid, subpath))
187 187 else:
188 188 revid = newid
189 189
190 190 fp.write('%s %s\n' % (revid, subpath))
191 191
192 192 return fp.getvalue()
193 193
194 194 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
195 195 """Calculates the files from p2 that we need to pull in when merging p1
196 196 and p2, given that the merge is coming from the given source.
197 197
198 198 This prevents us from losing files that only exist in the target p2 and
199 199 that don't come from the source repo (like if you're merging multiple
200 200 repositories together).
201 201 """
202 202 anc = [p1ctx.ancestor(p2ctx)]
203 203 # Calculate what files are coming from p2
204 204 actions, diverge, rename = mergemod.calculateupdates(
205 205 self.repo, p1ctx, p2ctx, anc,
206 206 True, # branchmerge
207 207 True, # force
208 208 False, # acceptremote
209 209 False, # followcopies
210 210 )
211 211
212 212 for file, (action, info, msg) in actions.iteritems():
213 213 if source.targetfilebelongstosource(file):
214 214 # If the file belongs to the source repo, ignore the p2
215 215 # since it will be covered by the existing fileset.
216 216 continue
217 217
218 218 # If the file requires actual merging, abort. We don't have enough
219 219 # context to resolve merges correctly.
220 220 if action in ['m', 'dm', 'cd', 'dc']:
221 221 raise error.Abort(_("unable to convert merge commit "
222 222 "since target parents do not merge cleanly (file "
223 223 "%s, parents %s and %s)") % (file, p1ctx,
224 224 p2ctx))
225 225 elif action == 'k':
226 226 # 'keep' means nothing changed from p1
227 227 continue
228 228 else:
229 229 # Any other change means we want to take the p2 version
230 230 yield file
231 231
232 232 def putcommit(self, files, copies, parents, commit, source, revmap, full,
233 233 cleanp2):
234 234 files = dict(files)
235 235
236 236 def getfilectx(repo, memctx, f):
237 237 if p2ctx and f in p2files and f not in copies:
238 238 self.ui.debug('reusing %s from p2\n' % f)
239 239 try:
240 240 return p2ctx[f]
241 241 except error.ManifestLookupError:
242 242 # If the file doesn't exist in p2, then we're syncing a
243 243 # delete, so just return None.
244 244 return None
245 245 try:
246 246 v = files[f]
247 247 except KeyError:
248 248 return None
249 249 data, mode = source.getfile(f, v)
250 250 if data is None:
251 251 return None
252 252 if f == '.hgtags':
253 253 data = self._rewritetags(source, revmap, data)
254 254 if f == '.hgsubstate':
255 255 data = self._rewritesubstate(source, data)
256 256 return context.memfilectx(self.repo, f, data, 'l' in mode,
257 257 'x' in mode, copies.get(f))
258 258
259 259 pl = []
260 260 for p in parents:
261 261 if p not in pl:
262 262 pl.append(p)
263 263 parents = pl
264 264 nparents = len(parents)
265 265 if self.filemapmode and nparents == 1:
266 266 m1node = self.repo.changelog.read(nodemod.bin(parents[0]))[0]
267 267 parent = parents[0]
268 268
269 269 if len(parents) < 2:
270 270 parents.append(nodemod.nullid)
271 271 if len(parents) < 2:
272 272 parents.append(nodemod.nullid)
273 273 p2 = parents.pop(0)
274 274
275 275 text = commit.desc
276 276
277 277 sha1s = re.findall(sha1re, text)
278 278 for sha1 in sha1s:
279 279 oldrev = source.lookuprev(sha1)
280 280 newrev = revmap.get(oldrev)
281 281 if newrev is not None:
282 282 text = text.replace(sha1, newrev[:len(sha1)])
283 283
284 284 extra = commit.extra.copy()
285 285
286 286 sourcename = self.repo.ui.config('convert', 'hg.sourcename')
287 287 if sourcename:
288 288 extra['convert_source'] = sourcename
289 289
290 290 for label in ('source', 'transplant_source', 'rebase_source',
291 291 'intermediate-source'):
292 292 node = extra.get(label)
293 293
294 294 if node is None:
295 295 continue
296 296
297 297 # Only transplant stores its reference in binary
298 298 if label == 'transplant_source':
299 299 node = nodemod.hex(node)
300 300
301 301 newrev = revmap.get(node)
302 302 if newrev is not None:
303 303 if label == 'transplant_source':
304 304 newrev = nodemod.bin(newrev)
305 305
306 306 extra[label] = newrev
307 307
308 308 if self.branchnames and commit.branch:
309 309 extra['branch'] = commit.branch
310 310 if commit.rev and commit.saverev:
311 311 extra['convert_revision'] = commit.rev
312 312
313 313 while parents:
314 314 p1 = p2
315 315 p2 = parents.pop(0)
316 316 p1ctx = self.repo[p1]
317 317 p2ctx = None
318 318 if p2 != nodemod.nullid:
319 319 p2ctx = self.repo[p2]
320 320 fileset = set(files)
321 321 if full:
322 322 fileset.update(self.repo[p1])
323 323 fileset.update(self.repo[p2])
324 324
325 325 if p2ctx:
326 326 p2files = set(cleanp2)
327 327 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
328 328 p2files.add(file)
329 329 fileset.add(file)
330 330
331 331 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
332 332 getfilectx, commit.author, commit.date, extra)
333 333
334 334 # We won't know if the conversion changes the node until after the
335 335 # commit, so copy the source's phase for now.
336 336 self.repo.ui.setconfig('phases', 'new-commit',
337 337 phases.phasenames[commit.phase], 'convert')
338 338
339 339 with self.repo.transaction("convert") as tr:
340 340 node = nodemod.hex(self.repo.commitctx(ctx))
341 341
342 342 # If the node value has changed, but the phase is lower than
343 343 # draft, set it back to draft since it hasn't been exposed
344 344 # anywhere.
345 345 if commit.rev != node:
346 346 ctx = self.repo[node]
347 347 if ctx.phase() < phases.draft:
348 348 phases.registernew(self.repo, tr, phases.draft,
349 349 [ctx.node()])
350 350
351 351 text = "(octopus merge fixup)\n"
352 352 p2 = node
353 353
354 354 if self.filemapmode and nparents == 1:
355 355 man = self.repo.manifestlog._revlog
356 356 mnode = self.repo.changelog.read(nodemod.bin(p2))[0]
357 357 closed = 'close' in commit.extra
358 358 if not closed and not man.cmp(m1node, man.revision(mnode)):
359 359 self.ui.status(_("filtering out empty revision\n"))
360 360 self.repo.rollback(force=True)
361 361 return parent
362 362 return p2
363 363
364 364 def puttags(self, tags):
365 365 try:
366 366 parentctx = self.repo[self.tagsbranch]
367 367 tagparent = parentctx.node()
368 368 except error.RepoError:
369 369 parentctx = None
370 370 tagparent = nodemod.nullid
371 371
372 372 oldlines = set()
373 373 for branch, heads in self.repo.branchmap().iteritems():
374 374 for h in heads:
375 375 if '.hgtags' in self.repo[h]:
376 376 oldlines.update(
377 377 set(self.repo[h]['.hgtags'].data().splitlines(True)))
378 378 oldlines = sorted(list(oldlines))
379 379
380 380 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
381 381 if newlines == oldlines:
382 382 return None, None
383 383
384 384 # if the old and new tags match, then there is nothing to update
385 385 oldtags = set()
386 386 newtags = set()
387 387 for line in oldlines:
388 388 s = line.strip().split(' ', 1)
389 389 if len(s) != 2:
390 390 continue
391 391 oldtags.add(s[1])
392 392 for line in newlines:
393 393 s = line.strip().split(' ', 1)
394 394 if len(s) != 2:
395 395 continue
396 396 if s[1] not in oldtags:
397 397 newtags.add(s[1].strip())
398 398
399 399 if not newtags:
400 400 return None, None
401 401
402 402 data = "".join(newlines)
403 403 def getfilectx(repo, memctx, f):
404 404 return context.memfilectx(repo, f, data, False, False, None)
405 405
406 406 self.ui.status(_("updating tags\n"))
407 407 date = "%s 0" % int(time.mktime(time.gmtime()))
408 408 extra = {'branch': self.tagsbranch}
409 409 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
410 410 [".hgtags"], getfilectx, "convert-repo", date,
411 411 extra)
412 412 node = self.repo.commitctx(ctx)
413 413 return nodemod.hex(node), nodemod.hex(tagparent)
414 414
415 415 def setfilemapmode(self, active):
416 416 self.filemapmode = active
417 417
418 418 def putbookmarks(self, updatedbookmark):
419 419 if not len(updatedbookmark):
420 420 return
421 421 wlock = lock = tr = None
422 422 try:
423 423 wlock = self.repo.wlock()
424 424 lock = self.repo.lock()
425 425 tr = self.repo.transaction('bookmark')
426 426 self.ui.status(_("updating bookmarks\n"))
427 427 destmarks = self.repo._bookmarks
428 428 changes = [(bookmark, nodemod.bin(updatedbookmark[bookmark]))
429 429 for bookmark in updatedbookmark]
430 430 destmarks.applychanges(self.repo, tr, changes)
431 431 tr.close()
432 432 finally:
433 433 lockmod.release(lock, wlock, tr)
434 434
435 435 def hascommitfrommap(self, rev):
436 436 # the exact semantics of clonebranches is unclear so we can't say no
437 437 return rev in self.repo or self.clonebranches
438 438
439 439 def hascommitforsplicemap(self, rev):
440 440 if rev not in self.repo and self.clonebranches:
441 441 raise error.Abort(_('revision %s not found in destination '
442 442 'repository (lookups with clonebranches=true '
443 443 'are not implemented)') % rev)
444 444 return rev in self.repo
445 445
446 446 class mercurial_source(common.converter_source):
447 447 def __init__(self, ui, path, revs=None):
448 448 common.converter_source.__init__(self, ui, path, revs)
449 449 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors')
450 450 self.ignored = set()
451 451 self.saverev = ui.configbool('convert', 'hg.saverev')
452 452 try:
453 453 self.repo = hg.repository(self.ui, path)
454 454 # try to provoke an exception if this isn't really a hg
455 455 # repo, but some other bogus compatible-looking url
456 456 if not self.repo.local():
457 457 raise error.RepoError
458 458 except error.RepoError:
459 459 ui.traceback()
460 460 raise NoRepo(_("%s is not a local Mercurial repository") % path)
461 461 self.lastrev = None
462 462 self.lastctx = None
463 463 self._changescache = None, None
464 464 self.convertfp = None
465 465 # Restrict converted revisions to startrev descendants
466 466 startnode = ui.config('convert', 'hg.startrev')
467 467 hgrevs = ui.config('convert', 'hg.revs')
468 468 if hgrevs is None:
469 469 if startnode is not None:
470 470 try:
471 471 startnode = self.repo.lookup(startnode)
472 472 except error.RepoError:
473 473 raise error.Abort(_('%s is not a valid start revision')
474 474 % startnode)
475 475 startrev = self.repo.changelog.rev(startnode)
476 476 children = {startnode: 1}
477 477 for r in self.repo.changelog.descendants([startrev]):
478 478 children[self.repo.changelog.node(r)] = 1
479 479 self.keep = children.__contains__
480 480 else:
481 481 self.keep = util.always
482 482 if revs:
483 483 self._heads = [self.repo[r].node() for r in revs]
484 484 else:
485 485 self._heads = self.repo.heads()
486 486 else:
487 487 if revs or startnode is not None:
488 488 raise error.Abort(_('hg.revs cannot be combined with '
489 489 'hg.startrev or --rev'))
490 490 nodes = set()
491 491 parents = set()
492 492 for r in scmutil.revrange(self.repo, [hgrevs]):
493 493 ctx = self.repo[r]
494 494 nodes.add(ctx.node())
495 495 parents.update(p.node() for p in ctx.parents())
496 496 self.keep = nodes.__contains__
497 497 self._heads = nodes - parents
498 498
499 499 def _changectx(self, rev):
500 500 if self.lastrev != rev:
501 501 self.lastctx = self.repo[rev]
502 502 self.lastrev = rev
503 503 return self.lastctx
504 504
505 505 def _parents(self, ctx):
506 506 return [p for p in ctx.parents() if p and self.keep(p.node())]
507 507
508 508 def getheads(self):
509 509 return [nodemod.hex(h) for h in self._heads if self.keep(h)]
510 510
511 511 def getfile(self, name, rev):
512 512 try:
513 513 fctx = self._changectx(rev)[name]
514 514 return fctx.data(), fctx.flags()
515 515 except error.LookupError:
516 516 return None, None
517 517
518 518 def _changedfiles(self, ctx1, ctx2):
519 519 ma, r = [], []
520 520 maappend = ma.append
521 521 rappend = r.append
522 522 d = ctx1.manifest().diff(ctx2.manifest())
523 523 for f, ((node1, flag1), (node2, flag2)) in d.iteritems():
524 524 if node2 is None:
525 525 rappend(f)
526 526 else:
527 527 maappend(f)
528 528 return ma, r
529 529
530 530 def getchanges(self, rev, full):
531 531 ctx = self._changectx(rev)
532 532 parents = self._parents(ctx)
533 533 if full or not parents:
534 534 files = copyfiles = ctx.manifest()
535 535 if parents:
536 536 if self._changescache[0] == rev:
537 537 ma, r = self._changescache[1]
538 538 else:
539 539 ma, r = self._changedfiles(parents[0], ctx)
540 540 if not full:
541 541 files = ma + r
542 542 copyfiles = ma
543 543 # _getcopies() is also run for roots and before filtering so missing
544 544 # revlogs are detected early
545 545 copies = self._getcopies(ctx, parents, copyfiles)
546 546 cleanp2 = set()
547 547 if len(parents) == 2:
548 548 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
549 549 for f, value in d.iteritems():
550 550 if value is None:
551 551 cleanp2.add(f)
552 552 changes = [(f, rev) for f in files if f not in self.ignored]
553 553 changes.sort()
554 554 return changes, copies, cleanp2
555 555
556 556 def _getcopies(self, ctx, parents, files):
557 557 copies = {}
558 558 for name in files:
559 559 if name in self.ignored:
560 560 continue
561 561 try:
562 562 copysource, _copynode = ctx.filectx(name).renamed()
563 563 if copysource in self.ignored:
564 564 continue
565 565 # Ignore copy sources not in parent revisions
566 566 found = False
567 567 for p in parents:
568 568 if copysource in p:
569 569 found = True
570 570 break
571 571 if not found:
572 572 continue
573 573 copies[name] = copysource
574 574 except TypeError:
575 575 pass
576 576 except error.LookupError as e:
577 577 if not self.ignoreerrors:
578 578 raise
579 579 self.ignored.add(name)
580 580 self.ui.warn(_('ignoring: %s\n') % e)
581 581 return copies
582 582
583 583 def getcommit(self, rev):
584 584 ctx = self._changectx(rev)
585 585 _parents = self._parents(ctx)
586 586 parents = [p.hex() for p in _parents]
587 587 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
588 588 crev = rev
589 589
590 590 return common.commit(author=ctx.user(),
591 591 date=util.datestr(ctx.date(),
592 592 '%Y-%m-%d %H:%M:%S %1%2'),
593 593 desc=ctx.description(),
594 594 rev=crev,
595 595 parents=parents,
596 596 optparents=optparents,
597 597 branch=ctx.branch(),
598 598 extra=ctx.extra(),
599 599 sortkey=ctx.rev(),
600 600 saverev=self.saverev,
601 601 phase=ctx.phase())
602 602
603 603 def gettags(self):
604 604 # This will get written to .hgtags, filter non global tags out.
605 605 tags = [t for t in self.repo.tagslist()
606 606 if self.repo.tagtype(t[0]) == 'global']
607 607 return dict([(name, nodemod.hex(node)) for name, node in tags
608 608 if self.keep(node)])
609 609
610 610 def getchangedfiles(self, rev, i):
611 611 ctx = self._changectx(rev)
612 612 parents = self._parents(ctx)
613 613 if not parents and i is None:
614 614 i = 0
615 615 ma, r = ctx.manifest().keys(), []
616 616 else:
617 617 i = i or 0
618 618 ma, r = self._changedfiles(parents[i], ctx)
619 619 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
620 620
621 621 if i == 0:
622 622 self._changescache = (rev, (ma, r))
623 623
624 624 return ma + r
625 625
626 626 def converted(self, rev, destrev):
627 627 if self.convertfp is None:
628 628 self.convertfp = open(self.repo.vfs.join('shamap'), 'a')
629 629 self.convertfp.write('%s %s\n' % (destrev, rev))
630 630 self.convertfp.flush()
631 631
632 632 def before(self):
633 633 self.ui.debug('run hg source pre-conversion action\n')
634 634
635 635 def after(self):
636 636 self.ui.debug('run hg source post-conversion action\n')
637 637
638 638 def hasnativeorder(self):
639 639 return True
640 640
641 641 def hasnativeclose(self):
642 642 return True
643 643
644 644 def lookuprev(self, rev):
645 645 try:
646 646 return nodemod.hex(self.repo.lookup(rev))
647 647 except (error.RepoError, error.LookupError):
648 648 return None
649 649
650 650 def getbookmarks(self):
651 651 return bookmarks.listbookmarks(self.repo)
652 652
653 653 def checkrevformat(self, revstr, mapname='splicemap'):
654 654 """ Mercurial, revision string is a 40 byte hex """
655 655 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now