##// END OF EJS Templates
configitems: register the 'convert.hg.saverev' config
Boris Feld -
r34167:9bda5ce9 default
parent child Browse files
Show More
@@ -1,558 +1,561 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 configitem('convert', 'hg.saverev',
80 default=False,
81 )
79 82
80 83 # Commands definition was moved elsewhere to ease demandload job.
81 84
82 85 @command('convert',
83 86 [('', 'authors', '',
84 87 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
85 88 _('FILE')),
86 89 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
87 90 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
88 91 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
89 92 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
90 93 ('', 'filemap', '', _('remap file names using contents of file'),
91 94 _('FILE')),
92 95 ('', 'full', None,
93 96 _('apply filemap changes by converting all files again')),
94 97 ('', 'splicemap', '', _('splice synthesized history into place'),
95 98 _('FILE')),
96 99 ('', 'branchmap', '', _('change branch names while converting'),
97 100 _('FILE')),
98 101 ('', 'branchsort', None, _('try to sort changesets by branches')),
99 102 ('', 'datesort', None, _('try to sort changesets by date')),
100 103 ('', 'sourcesort', None, _('preserve source changesets order')),
101 104 ('', 'closesort', None, _('try to reorder closed revisions'))],
102 105 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
103 106 norepo=True)
104 107 def convert(ui, src, dest=None, revmapfile=None, **opts):
105 108 """convert a foreign SCM repository to a Mercurial one.
106 109
107 110 Accepted source formats [identifiers]:
108 111
109 112 - Mercurial [hg]
110 113 - CVS [cvs]
111 114 - Darcs [darcs]
112 115 - git [git]
113 116 - Subversion [svn]
114 117 - Monotone [mtn]
115 118 - GNU Arch [gnuarch]
116 119 - Bazaar [bzr]
117 120 - Perforce [p4]
118 121
119 122 Accepted destination formats [identifiers]:
120 123
121 124 - Mercurial [hg]
122 125 - Subversion [svn] (history on branches is not preserved)
123 126
124 127 If no revision is given, all revisions will be converted.
125 128 Otherwise, convert will only import up to the named revision
126 129 (given in a format understood by the source).
127 130
128 131 If no destination directory name is specified, it defaults to the
129 132 basename of the source with ``-hg`` appended. If the destination
130 133 repository doesn't exist, it will be created.
131 134
132 135 By default, all sources except Mercurial will use --branchsort.
133 136 Mercurial uses --sourcesort to preserve original revision numbers
134 137 order. Sort modes have the following effects:
135 138
136 139 --branchsort convert from parent to child revision when possible,
137 140 which means branches are usually converted one after
138 141 the other. It generates more compact repositories.
139 142
140 143 --datesort sort revisions by date. Converted repositories have
141 144 good-looking changelogs but are often an order of
142 145 magnitude larger than the same ones generated by
143 146 --branchsort.
144 147
145 148 --sourcesort try to preserve source revisions order, only
146 149 supported by Mercurial sources.
147 150
148 151 --closesort try to move closed revisions as close as possible
149 152 to parent branches, only supported by Mercurial
150 153 sources.
151 154
152 155 If ``REVMAP`` isn't given, it will be put in a default location
153 156 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
154 157 text file that maps each source commit ID to the destination ID
155 158 for that revision, like so::
156 159
157 160 <source ID> <destination ID>
158 161
159 162 If the file doesn't exist, it's automatically created. It's
160 163 updated on each commit copied, so :hg:`convert` can be interrupted
161 164 and can be run repeatedly to copy new commits.
162 165
163 166 The authormap is a simple text file that maps each source commit
164 167 author to a destination commit author. It is handy for source SCMs
165 168 that use unix logins to identify authors (e.g.: CVS). One line per
166 169 author mapping and the line format is::
167 170
168 171 source author = destination author
169 172
170 173 Empty lines and lines starting with a ``#`` are ignored.
171 174
172 175 The filemap is a file that allows filtering and remapping of files
173 176 and directories. Each line can contain one of the following
174 177 directives::
175 178
176 179 include path/to/file-or-dir
177 180
178 181 exclude path/to/file-or-dir
179 182
180 183 rename path/to/source path/to/destination
181 184
182 185 Comment lines start with ``#``. A specified path matches if it
183 186 equals the full relative name of a file or one of its parent
184 187 directories. The ``include`` or ``exclude`` directive with the
185 188 longest matching path applies, so line order does not matter.
186 189
187 190 The ``include`` directive causes a file, or all files under a
188 191 directory, to be included in the destination repository. The default
189 192 if there are no ``include`` statements is to include everything.
190 193 If there are any ``include`` statements, nothing else is included.
191 194 The ``exclude`` directive causes files or directories to
192 195 be omitted. The ``rename`` directive renames a file or directory if
193 196 it is converted. To rename from a subdirectory into the root of
194 197 the repository, use ``.`` as the path to rename to.
195 198
196 199 ``--full`` will make sure the converted changesets contain exactly
197 200 the right files with the right content. It will make a full
198 201 conversion of all files, not just the ones that have
199 202 changed. Files that already are correct will not be changed. This
200 203 can be used to apply filemap changes when converting
201 204 incrementally. This is currently only supported for Mercurial and
202 205 Subversion.
203 206
204 207 The splicemap is a file that allows insertion of synthetic
205 208 history, letting you specify the parents of a revision. This is
206 209 useful if you want to e.g. give a Subversion merge two parents, or
207 210 graft two disconnected series of history together. Each entry
208 211 contains a key, followed by a space, followed by one or two
209 212 comma-separated values::
210 213
211 214 key parent1, parent2
212 215
213 216 The key is the revision ID in the source
214 217 revision control system whose parents should be modified (same
215 218 format as a key in .hg/shamap). The values are the revision IDs
216 219 (in either the source or destination revision control system) that
217 220 should be used as the new parents for that node. For example, if
218 221 you have merged "release-1.0" into "trunk", then you should
219 222 specify the revision on "trunk" as the first parent and the one on
220 223 the "release-1.0" branch as the second.
221 224
222 225 The branchmap is a file that allows you to rename a branch when it is
223 226 being brought in from whatever external repository. When used in
224 227 conjunction with a splicemap, it allows for a powerful combination
225 228 to help fix even the most badly mismanaged repositories and turn them
226 229 into nicely structured Mercurial repositories. The branchmap contains
227 230 lines of the form::
228 231
229 232 original_branch_name new_branch_name
230 233
231 234 where "original_branch_name" is the name of the branch in the
232 235 source repository, and "new_branch_name" is the name of the branch
233 236 is the destination repository. No whitespace is allowed in the new
234 237 branch name. This can be used to (for instance) move code in one
235 238 repository from "default" to a named branch.
236 239
237 240 Mercurial Source
238 241 ################
239 242
240 243 The Mercurial source recognizes the following configuration
241 244 options, which you can set on the command line with ``--config``:
242 245
243 246 :convert.hg.ignoreerrors: ignore integrity errors when reading.
244 247 Use it to fix Mercurial repositories with missing revlogs, by
245 248 converting from and to Mercurial. Default is False.
246 249
247 250 :convert.hg.saverev: store original revision ID in changeset
248 251 (forces target IDs to change). It takes a boolean argument and
249 252 defaults to False.
250 253
251 254 :convert.hg.startrev: specify the initial Mercurial revision.
252 255 The default is 0.
253 256
254 257 :convert.hg.revs: revset specifying the source revisions to convert.
255 258
256 259 CVS Source
257 260 ##########
258 261
259 262 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
260 263 to indicate the starting point of what will be converted. Direct
261 264 access to the repository files is not needed, unless of course the
262 265 repository is ``:local:``. The conversion uses the top level
263 266 directory in the sandbox to find the CVS repository, and then uses
264 267 CVS rlog commands to find files to convert. This means that unless
265 268 a filemap is given, all files under the starting directory will be
266 269 converted, and that any directory reorganization in the CVS
267 270 sandbox is ignored.
268 271
269 272 The following options can be used with ``--config``:
270 273
271 274 :convert.cvsps.cache: Set to False to disable remote log caching,
272 275 for testing and debugging purposes. Default is True.
273 276
274 277 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
275 278 allowed between commits with identical user and log message in
276 279 a single changeset. When very large files were checked in as
277 280 part of a changeset then the default may not be long enough.
278 281 The default is 60.
279 282
280 283 :convert.cvsps.logencoding: Specify encoding name to be used for
281 284 transcoding CVS log messages. Multiple encoding names can be
282 285 specified as a list (see :hg:`help config.Syntax`), but only
283 286 the first acceptable encoding in the list is used per CVS log
284 287 entries. This transcoding is executed before cvslog hook below.
285 288
286 289 :convert.cvsps.mergeto: Specify a regular expression to which
287 290 commit log messages are matched. If a match occurs, then the
288 291 conversion process will insert a dummy revision merging the
289 292 branch on which this log message occurs to the branch
290 293 indicated in the regex. Default is ``{{mergetobranch
291 294 ([-\\w]+)}}``
292 295
293 296 :convert.cvsps.mergefrom: Specify a regular expression to which
294 297 commit log messages are matched. If a match occurs, then the
295 298 conversion process will add the most recent revision on the
296 299 branch indicated in the regex as the second parent of the
297 300 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
298 301
299 302 :convert.localtimezone: use local time (as determined by the TZ
300 303 environment variable) for changeset date/times. The default
301 304 is False (use UTC).
302 305
303 306 :hooks.cvslog: Specify a Python function to be called at the end of
304 307 gathering the CVS log. The function is passed a list with the
305 308 log entries, and can modify the entries in-place, or add or
306 309 delete them.
307 310
308 311 :hooks.cvschangesets: Specify a Python function to be called after
309 312 the changesets are calculated from the CVS log. The
310 313 function is passed a list with the changeset entries, and can
311 314 modify the changesets in-place, or add or delete them.
312 315
313 316 An additional "debugcvsps" Mercurial command allows the builtin
314 317 changeset merging code to be run without doing a conversion. Its
315 318 parameters and output are similar to that of cvsps 2.1. Please see
316 319 the command help for more details.
317 320
318 321 Subversion Source
319 322 #################
320 323
321 324 Subversion source detects classical trunk/branches/tags layouts.
322 325 By default, the supplied ``svn://repo/path/`` source URL is
323 326 converted as a single branch. If ``svn://repo/path/trunk`` exists
324 327 it replaces the default branch. If ``svn://repo/path/branches``
325 328 exists, its subdirectories are listed as possible branches. If
326 329 ``svn://repo/path/tags`` exists, it is looked for tags referencing
327 330 converted branches. Default ``trunk``, ``branches`` and ``tags``
328 331 values can be overridden with following options. Set them to paths
329 332 relative to the source URL, or leave them blank to disable auto
330 333 detection.
331 334
332 335 The following options can be set with ``--config``:
333 336
334 337 :convert.svn.branches: specify the directory containing branches.
335 338 The default is ``branches``.
336 339
337 340 :convert.svn.tags: specify the directory containing tags. The
338 341 default is ``tags``.
339 342
340 343 :convert.svn.trunk: specify the name of the trunk branch. The
341 344 default is ``trunk``.
342 345
343 346 :convert.localtimezone: use local time (as determined by the TZ
344 347 environment variable) for changeset date/times. The default
345 348 is False (use UTC).
346 349
347 350 Source history can be retrieved starting at a specific revision,
348 351 instead of being integrally converted. Only single branch
349 352 conversions are supported.
350 353
351 354 :convert.svn.startrev: specify start Subversion revision number.
352 355 The default is 0.
353 356
354 357 Git Source
355 358 ##########
356 359
357 360 The Git importer converts commits from all reachable branches (refs
358 361 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
359 362 Branches are converted to bookmarks with the same name, with the
360 363 leading 'refs/heads' stripped. Git submodules are converted to Git
361 364 subrepos in Mercurial.
362 365
363 366 The following options can be set with ``--config``:
364 367
365 368 :convert.git.similarity: specify how similar files modified in a
366 369 commit must be to be imported as renames or copies, as a
367 370 percentage between ``0`` (disabled) and ``100`` (files must be
368 371 identical). For example, ``90`` means that a delete/add pair will
369 372 be imported as a rename if more than 90% of the file hasn't
370 373 changed. The default is ``50``.
371 374
372 375 :convert.git.findcopiesharder: while detecting copies, look at all
373 376 files in the working copy instead of just changed ones. This
374 377 is very expensive for large projects, and is only effective when
375 378 ``convert.git.similarity`` is greater than 0. The default is False.
376 379
377 380 :convert.git.renamelimit: perform rename and copy detection up to this
378 381 many changed files in a commit. Increasing this will make rename
379 382 and copy detection more accurate but will significantly slow down
380 383 computation on large projects. The option is only relevant if
381 384 ``convert.git.similarity`` is greater than 0. The default is
382 385 ``400``.
383 386
384 387 :convert.git.committeractions: list of actions to take when processing
385 388 author and committer values.
386 389
387 390 Git commits have separate author (who wrote the commit) and committer
388 391 (who applied the commit) fields. Not all destinations support separate
389 392 author and committer fields (including Mercurial). This config option
390 393 controls what to do with these author and committer fields during
391 394 conversion.
392 395
393 396 A value of ``messagedifferent`` will append a ``committer: ...``
394 397 line to the commit message if the Git committer is different from the
395 398 author. The prefix of that line can be specified using the syntax
396 399 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
397 400 When a prefix is specified, a space will always be inserted between the
398 401 prefix and the value.
399 402
400 403 ``messagealways`` behaves like ``messagedifferent`` except it will
401 404 always result in a ``committer: ...`` line being appended to the commit
402 405 message. This value is mutually exclusive with ``messagedifferent``.
403 406
404 407 ``dropcommitter`` will remove references to the committer. Only
405 408 references to the author will remain. Actions that add references
406 409 to the committer will have no effect when this is set.
407 410
408 411 ``replaceauthor`` will replace the value of the author field with
409 412 the committer. Other actions that add references to the committer
410 413 will still take effect when this is set.
411 414
412 415 The default is ``messagedifferent``.
413 416
414 417 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
415 418 the destination. Some Git repositories store extra metadata in commits.
416 419 By default, this non-default metadata will be lost during conversion.
417 420 Setting this config option can retain that metadata. Some built-in
418 421 keys such as ``parent`` and ``branch`` are not allowed to be copied.
419 422
420 423 :convert.git.remoteprefix: remote refs are converted as bookmarks with
421 424 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
422 425 is 'remote'.
423 426
424 427 :convert.git.saverev: whether to store the original Git commit ID in the
425 428 metadata of the destination commit. The default is True.
426 429
427 430 :convert.git.skipsubmodules: does not convert root level .gitmodules files
428 431 or files with 160000 mode indicating a submodule. Default is False.
429 432
430 433 Perforce Source
431 434 ###############
432 435
433 436 The Perforce (P4) importer can be given a p4 depot path or a
434 437 client specification as source. It will convert all files in the
435 438 source to a flat Mercurial repository, ignoring labels, branches
436 439 and integrations. Note that when a depot path is given you then
437 440 usually should specify a target directory, because otherwise the
438 441 target may be named ``...-hg``.
439 442
440 443 The following options can be set with ``--config``:
441 444
442 445 :convert.p4.encoding: specify the encoding to use when decoding standard
443 446 output of the Perforce command line tool. The default is default system
444 447 encoding.
445 448
446 449 :convert.p4.startrev: specify initial Perforce revision (a
447 450 Perforce changelist number).
448 451
449 452 Mercurial Destination
450 453 #####################
451 454
452 455 The Mercurial destination will recognize Mercurial subrepositories in the
453 456 destination directory, and update the .hgsubstate file automatically if the
454 457 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
455 458 Converting a repository with subrepositories requires converting a single
456 459 repository at a time, from the bottom up.
457 460
458 461 .. container:: verbose
459 462
460 463 An example showing how to convert a repository with subrepositories::
461 464
462 465 # so convert knows the type when it sees a non empty destination
463 466 $ hg init converted
464 467
465 468 $ hg convert orig/sub1 converted/sub1
466 469 $ hg convert orig/sub2 converted/sub2
467 470 $ hg convert orig converted
468 471
469 472 The following options are supported:
470 473
471 474 :convert.hg.clonebranches: dispatch source branches in separate
472 475 clones. The default is False.
473 476
474 477 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
475 478 ``default``.
476 479
477 480 :convert.hg.usebranchnames: preserve branch names. The default is
478 481 True.
479 482
480 483 :convert.hg.sourcename: records the given string as a 'convert_source' extra
481 484 value on each commit made in the target repository. The default is None.
482 485
483 486 All Destinations
484 487 ################
485 488
486 489 All destination types accept the following options:
487 490
488 491 :convert.skiptags: does not convert tags from the source repo to the target
489 492 repo. The default is False.
490 493 """
491 494 return convcmd.convert(ui, src, dest, revmapfile, **opts)
492 495
493 496 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
494 497 def debugsvnlog(ui, **opts):
495 498 return subversion.debugsvnlog(ui, **opts)
496 499
497 500 @command('debugcvsps',
498 501 [
499 502 # Main options shared with cvsps-2.1
500 503 ('b', 'branches', [], _('only return changes on specified branches')),
501 504 ('p', 'prefix', '', _('prefix to remove from file names')),
502 505 ('r', 'revisions', [],
503 506 _('only return changes after or between specified tags')),
504 507 ('u', 'update-cache', None, _("update cvs log cache")),
505 508 ('x', 'new-cache', None, _("create new cvs log cache")),
506 509 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
507 510 ('', 'root', '', _('specify cvsroot')),
508 511 # Options specific to builtin cvsps
509 512 ('', 'parents', '', _('show parent changesets')),
510 513 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
511 514 # Options that are ignored for compatibility with cvsps-2.1
512 515 ('A', 'cvs-direct', None, _('ignored for compatibility')),
513 516 ],
514 517 _('hg debugcvsps [OPTION]... [PATH]...'),
515 518 norepo=True)
516 519 def debugcvsps(ui, *args, **opts):
517 520 '''create changeset information from CVS
518 521
519 522 This command is intended as a debugging tool for the CVS to
520 523 Mercurial converter, and can be used as a direct replacement for
521 524 cvsps.
522 525
523 526 Hg debugcvsps reads the CVS rlog for current directory (or any
524 527 named directory) in the CVS repository, and converts the log to a
525 528 series of changesets based on matching commit log entries and
526 529 dates.'''
527 530 return cvsps.debugcvsps(ui, *args, **opts)
528 531
529 532 def kwconverted(ctx, name):
530 533 rev = ctx.extra().get('convert_revision', '')
531 534 if rev.startswith('svn:'):
532 535 if name == 'svnrev':
533 536 return str(subversion.revsplit(rev)[2])
534 537 elif name == 'svnpath':
535 538 return subversion.revsplit(rev)[1]
536 539 elif name == 'svnuuid':
537 540 return subversion.revsplit(rev)[0]
538 541 return rev
539 542
540 543 templatekeyword = registrar.templatekeyword()
541 544
542 545 @templatekeyword('svnrev')
543 546 def kwsvnrev(repo, ctx, **args):
544 547 """String. Converted subversion revision number."""
545 548 return kwconverted(ctx, 'svnrev')
546 549
547 550 @templatekeyword('svnpath')
548 551 def kwsvnpath(repo, ctx, **args):
549 552 """String. Converted subversion revision project path."""
550 553 return kwconverted(ctx, 'svnpath')
551 554
552 555 @templatekeyword('svnuuid')
553 556 def kwsvnuuid(repo, ctx, **args):
554 557 """String. Converted subversion revision repository identifier."""
555 558 return kwconverted(ctx, 'svnuuid')
556 559
557 560 # tell hggettext to extract docstrings from these functions:
558 561 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 52 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
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 self.saverev = ui.configbool('convert', 'hg.saverev', False)
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