##// END OF EJS Templates
configitems: register the 'convert.git.findcopiesharder' config
Boris Feld -
r34158:3e3327ce default
parent child Browse files
Show More
@@ -1,531 +1,534
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 configitem('convert', 'git.findcopiesharder',
53 default=False,
54 )
52 55
53 56 # Commands definition was moved elsewhere to ease demandload job.
54 57
55 58 @command('convert',
56 59 [('', 'authors', '',
57 60 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
58 61 _('FILE')),
59 62 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
60 63 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
61 64 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
62 65 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
63 66 ('', 'filemap', '', _('remap file names using contents of file'),
64 67 _('FILE')),
65 68 ('', 'full', None,
66 69 _('apply filemap changes by converting all files again')),
67 70 ('', 'splicemap', '', _('splice synthesized history into place'),
68 71 _('FILE')),
69 72 ('', 'branchmap', '', _('change branch names while converting'),
70 73 _('FILE')),
71 74 ('', 'branchsort', None, _('try to sort changesets by branches')),
72 75 ('', 'datesort', None, _('try to sort changesets by date')),
73 76 ('', 'sourcesort', None, _('preserve source changesets order')),
74 77 ('', 'closesort', None, _('try to reorder closed revisions'))],
75 78 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
76 79 norepo=True)
77 80 def convert(ui, src, dest=None, revmapfile=None, **opts):
78 81 """convert a foreign SCM repository to a Mercurial one.
79 82
80 83 Accepted source formats [identifiers]:
81 84
82 85 - Mercurial [hg]
83 86 - CVS [cvs]
84 87 - Darcs [darcs]
85 88 - git [git]
86 89 - Subversion [svn]
87 90 - Monotone [mtn]
88 91 - GNU Arch [gnuarch]
89 92 - Bazaar [bzr]
90 93 - Perforce [p4]
91 94
92 95 Accepted destination formats [identifiers]:
93 96
94 97 - Mercurial [hg]
95 98 - Subversion [svn] (history on branches is not preserved)
96 99
97 100 If no revision is given, all revisions will be converted.
98 101 Otherwise, convert will only import up to the named revision
99 102 (given in a format understood by the source).
100 103
101 104 If no destination directory name is specified, it defaults to the
102 105 basename of the source with ``-hg`` appended. If the destination
103 106 repository doesn't exist, it will be created.
104 107
105 108 By default, all sources except Mercurial will use --branchsort.
106 109 Mercurial uses --sourcesort to preserve original revision numbers
107 110 order. Sort modes have the following effects:
108 111
109 112 --branchsort convert from parent to child revision when possible,
110 113 which means branches are usually converted one after
111 114 the other. It generates more compact repositories.
112 115
113 116 --datesort sort revisions by date. Converted repositories have
114 117 good-looking changelogs but are often an order of
115 118 magnitude larger than the same ones generated by
116 119 --branchsort.
117 120
118 121 --sourcesort try to preserve source revisions order, only
119 122 supported by Mercurial sources.
120 123
121 124 --closesort try to move closed revisions as close as possible
122 125 to parent branches, only supported by Mercurial
123 126 sources.
124 127
125 128 If ``REVMAP`` isn't given, it will be put in a default location
126 129 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
127 130 text file that maps each source commit ID to the destination ID
128 131 for that revision, like so::
129 132
130 133 <source ID> <destination ID>
131 134
132 135 If the file doesn't exist, it's automatically created. It's
133 136 updated on each commit copied, so :hg:`convert` can be interrupted
134 137 and can be run repeatedly to copy new commits.
135 138
136 139 The authormap is a simple text file that maps each source commit
137 140 author to a destination commit author. It is handy for source SCMs
138 141 that use unix logins to identify authors (e.g.: CVS). One line per
139 142 author mapping and the line format is::
140 143
141 144 source author = destination author
142 145
143 146 Empty lines and lines starting with a ``#`` are ignored.
144 147
145 148 The filemap is a file that allows filtering and remapping of files
146 149 and directories. Each line can contain one of the following
147 150 directives::
148 151
149 152 include path/to/file-or-dir
150 153
151 154 exclude path/to/file-or-dir
152 155
153 156 rename path/to/source path/to/destination
154 157
155 158 Comment lines start with ``#``. A specified path matches if it
156 159 equals the full relative name of a file or one of its parent
157 160 directories. The ``include`` or ``exclude`` directive with the
158 161 longest matching path applies, so line order does not matter.
159 162
160 163 The ``include`` directive causes a file, or all files under a
161 164 directory, to be included in the destination repository. The default
162 165 if there are no ``include`` statements is to include everything.
163 166 If there are any ``include`` statements, nothing else is included.
164 167 The ``exclude`` directive causes files or directories to
165 168 be omitted. The ``rename`` directive renames a file or directory if
166 169 it is converted. To rename from a subdirectory into the root of
167 170 the repository, use ``.`` as the path to rename to.
168 171
169 172 ``--full`` will make sure the converted changesets contain exactly
170 173 the right files with the right content. It will make a full
171 174 conversion of all files, not just the ones that have
172 175 changed. Files that already are correct will not be changed. This
173 176 can be used to apply filemap changes when converting
174 177 incrementally. This is currently only supported for Mercurial and
175 178 Subversion.
176 179
177 180 The splicemap is a file that allows insertion of synthetic
178 181 history, letting you specify the parents of a revision. This is
179 182 useful if you want to e.g. give a Subversion merge two parents, or
180 183 graft two disconnected series of history together. Each entry
181 184 contains a key, followed by a space, followed by one or two
182 185 comma-separated values::
183 186
184 187 key parent1, parent2
185 188
186 189 The key is the revision ID in the source
187 190 revision control system whose parents should be modified (same
188 191 format as a key in .hg/shamap). The values are the revision IDs
189 192 (in either the source or destination revision control system) that
190 193 should be used as the new parents for that node. For example, if
191 194 you have merged "release-1.0" into "trunk", then you should
192 195 specify the revision on "trunk" as the first parent and the one on
193 196 the "release-1.0" branch as the second.
194 197
195 198 The branchmap is a file that allows you to rename a branch when it is
196 199 being brought in from whatever external repository. When used in
197 200 conjunction with a splicemap, it allows for a powerful combination
198 201 to help fix even the most badly mismanaged repositories and turn them
199 202 into nicely structured Mercurial repositories. The branchmap contains
200 203 lines of the form::
201 204
202 205 original_branch_name new_branch_name
203 206
204 207 where "original_branch_name" is the name of the branch in the
205 208 source repository, and "new_branch_name" is the name of the branch
206 209 is the destination repository. No whitespace is allowed in the new
207 210 branch name. This can be used to (for instance) move code in one
208 211 repository from "default" to a named branch.
209 212
210 213 Mercurial Source
211 214 ################
212 215
213 216 The Mercurial source recognizes the following configuration
214 217 options, which you can set on the command line with ``--config``:
215 218
216 219 :convert.hg.ignoreerrors: ignore integrity errors when reading.
217 220 Use it to fix Mercurial repositories with missing revlogs, by
218 221 converting from and to Mercurial. Default is False.
219 222
220 223 :convert.hg.saverev: store original revision ID in changeset
221 224 (forces target IDs to change). It takes a boolean argument and
222 225 defaults to False.
223 226
224 227 :convert.hg.startrev: specify the initial Mercurial revision.
225 228 The default is 0.
226 229
227 230 :convert.hg.revs: revset specifying the source revisions to convert.
228 231
229 232 CVS Source
230 233 ##########
231 234
232 235 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
233 236 to indicate the starting point of what will be converted. Direct
234 237 access to the repository files is not needed, unless of course the
235 238 repository is ``:local:``. The conversion uses the top level
236 239 directory in the sandbox to find the CVS repository, and then uses
237 240 CVS rlog commands to find files to convert. This means that unless
238 241 a filemap is given, all files under the starting directory will be
239 242 converted, and that any directory reorganization in the CVS
240 243 sandbox is ignored.
241 244
242 245 The following options can be used with ``--config``:
243 246
244 247 :convert.cvsps.cache: Set to False to disable remote log caching,
245 248 for testing and debugging purposes. Default is True.
246 249
247 250 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
248 251 allowed between commits with identical user and log message in
249 252 a single changeset. When very large files were checked in as
250 253 part of a changeset then the default may not be long enough.
251 254 The default is 60.
252 255
253 256 :convert.cvsps.logencoding: Specify encoding name to be used for
254 257 transcoding CVS log messages. Multiple encoding names can be
255 258 specified as a list (see :hg:`help config.Syntax`), but only
256 259 the first acceptable encoding in the list is used per CVS log
257 260 entries. This transcoding is executed before cvslog hook below.
258 261
259 262 :convert.cvsps.mergeto: Specify a regular expression to which
260 263 commit log messages are matched. If a match occurs, then the
261 264 conversion process will insert a dummy revision merging the
262 265 branch on which this log message occurs to the branch
263 266 indicated in the regex. Default is ``{{mergetobranch
264 267 ([-\\w]+)}}``
265 268
266 269 :convert.cvsps.mergefrom: Specify a regular expression to which
267 270 commit log messages are matched. If a match occurs, then the
268 271 conversion process will add the most recent revision on the
269 272 branch indicated in the regex as the second parent of the
270 273 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
271 274
272 275 :convert.localtimezone: use local time (as determined by the TZ
273 276 environment variable) for changeset date/times. The default
274 277 is False (use UTC).
275 278
276 279 :hooks.cvslog: Specify a Python function to be called at the end of
277 280 gathering the CVS log. The function is passed a list with the
278 281 log entries, and can modify the entries in-place, or add or
279 282 delete them.
280 283
281 284 :hooks.cvschangesets: Specify a Python function to be called after
282 285 the changesets are calculated from the CVS log. The
283 286 function is passed a list with the changeset entries, and can
284 287 modify the changesets in-place, or add or delete them.
285 288
286 289 An additional "debugcvsps" Mercurial command allows the builtin
287 290 changeset merging code to be run without doing a conversion. Its
288 291 parameters and output are similar to that of cvsps 2.1. Please see
289 292 the command help for more details.
290 293
291 294 Subversion Source
292 295 #################
293 296
294 297 Subversion source detects classical trunk/branches/tags layouts.
295 298 By default, the supplied ``svn://repo/path/`` source URL is
296 299 converted as a single branch. If ``svn://repo/path/trunk`` exists
297 300 it replaces the default branch. If ``svn://repo/path/branches``
298 301 exists, its subdirectories are listed as possible branches. If
299 302 ``svn://repo/path/tags`` exists, it is looked for tags referencing
300 303 converted branches. Default ``trunk``, ``branches`` and ``tags``
301 304 values can be overridden with following options. Set them to paths
302 305 relative to the source URL, or leave them blank to disable auto
303 306 detection.
304 307
305 308 The following options can be set with ``--config``:
306 309
307 310 :convert.svn.branches: specify the directory containing branches.
308 311 The default is ``branches``.
309 312
310 313 :convert.svn.tags: specify the directory containing tags. The
311 314 default is ``tags``.
312 315
313 316 :convert.svn.trunk: specify the name of the trunk branch. The
314 317 default is ``trunk``.
315 318
316 319 :convert.localtimezone: use local time (as determined by the TZ
317 320 environment variable) for changeset date/times. The default
318 321 is False (use UTC).
319 322
320 323 Source history can be retrieved starting at a specific revision,
321 324 instead of being integrally converted. Only single branch
322 325 conversions are supported.
323 326
324 327 :convert.svn.startrev: specify start Subversion revision number.
325 328 The default is 0.
326 329
327 330 Git Source
328 331 ##########
329 332
330 333 The Git importer converts commits from all reachable branches (refs
331 334 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
332 335 Branches are converted to bookmarks with the same name, with the
333 336 leading 'refs/heads' stripped. Git submodules are converted to Git
334 337 subrepos in Mercurial.
335 338
336 339 The following options can be set with ``--config``:
337 340
338 341 :convert.git.similarity: specify how similar files modified in a
339 342 commit must be to be imported as renames or copies, as a
340 343 percentage between ``0`` (disabled) and ``100`` (files must be
341 344 identical). For example, ``90`` means that a delete/add pair will
342 345 be imported as a rename if more than 90% of the file hasn't
343 346 changed. The default is ``50``.
344 347
345 348 :convert.git.findcopiesharder: while detecting copies, look at all
346 349 files in the working copy instead of just changed ones. This
347 350 is very expensive for large projects, and is only effective when
348 351 ``convert.git.similarity`` is greater than 0. The default is False.
349 352
350 353 :convert.git.renamelimit: perform rename and copy detection up to this
351 354 many changed files in a commit. Increasing this will make rename
352 355 and copy detection more accurate but will significantly slow down
353 356 computation on large projects. The option is only relevant if
354 357 ``convert.git.similarity`` is greater than 0. The default is
355 358 ``400``.
356 359
357 360 :convert.git.committeractions: list of actions to take when processing
358 361 author and committer values.
359 362
360 363 Git commits have separate author (who wrote the commit) and committer
361 364 (who applied the commit) fields. Not all destinations support separate
362 365 author and committer fields (including Mercurial). This config option
363 366 controls what to do with these author and committer fields during
364 367 conversion.
365 368
366 369 A value of ``messagedifferent`` will append a ``committer: ...``
367 370 line to the commit message if the Git committer is different from the
368 371 author. The prefix of that line can be specified using the syntax
369 372 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
370 373 When a prefix is specified, a space will always be inserted between the
371 374 prefix and the value.
372 375
373 376 ``messagealways`` behaves like ``messagedifferent`` except it will
374 377 always result in a ``committer: ...`` line being appended to the commit
375 378 message. This value is mutually exclusive with ``messagedifferent``.
376 379
377 380 ``dropcommitter`` will remove references to the committer. Only
378 381 references to the author will remain. Actions that add references
379 382 to the committer will have no effect when this is set.
380 383
381 384 ``replaceauthor`` will replace the value of the author field with
382 385 the committer. Other actions that add references to the committer
383 386 will still take effect when this is set.
384 387
385 388 The default is ``messagedifferent``.
386 389
387 390 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
388 391 the destination. Some Git repositories store extra metadata in commits.
389 392 By default, this non-default metadata will be lost during conversion.
390 393 Setting this config option can retain that metadata. Some built-in
391 394 keys such as ``parent`` and ``branch`` are not allowed to be copied.
392 395
393 396 :convert.git.remoteprefix: remote refs are converted as bookmarks with
394 397 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
395 398 is 'remote'.
396 399
397 400 :convert.git.saverev: whether to store the original Git commit ID in the
398 401 metadata of the destination commit. The default is True.
399 402
400 403 :convert.git.skipsubmodules: does not convert root level .gitmodules files
401 404 or files with 160000 mode indicating a submodule. Default is False.
402 405
403 406 Perforce Source
404 407 ###############
405 408
406 409 The Perforce (P4) importer can be given a p4 depot path or a
407 410 client specification as source. It will convert all files in the
408 411 source to a flat Mercurial repository, ignoring labels, branches
409 412 and integrations. Note that when a depot path is given you then
410 413 usually should specify a target directory, because otherwise the
411 414 target may be named ``...-hg``.
412 415
413 416 The following options can be set with ``--config``:
414 417
415 418 :convert.p4.encoding: specify the encoding to use when decoding standard
416 419 output of the Perforce command line tool. The default is default system
417 420 encoding.
418 421
419 422 :convert.p4.startrev: specify initial Perforce revision (a
420 423 Perforce changelist number).
421 424
422 425 Mercurial Destination
423 426 #####################
424 427
425 428 The Mercurial destination will recognize Mercurial subrepositories in the
426 429 destination directory, and update the .hgsubstate file automatically if the
427 430 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
428 431 Converting a repository with subrepositories requires converting a single
429 432 repository at a time, from the bottom up.
430 433
431 434 .. container:: verbose
432 435
433 436 An example showing how to convert a repository with subrepositories::
434 437
435 438 # so convert knows the type when it sees a non empty destination
436 439 $ hg init converted
437 440
438 441 $ hg convert orig/sub1 converted/sub1
439 442 $ hg convert orig/sub2 converted/sub2
440 443 $ hg convert orig converted
441 444
442 445 The following options are supported:
443 446
444 447 :convert.hg.clonebranches: dispatch source branches in separate
445 448 clones. The default is False.
446 449
447 450 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
448 451 ``default``.
449 452
450 453 :convert.hg.usebranchnames: preserve branch names. The default is
451 454 True.
452 455
453 456 :convert.hg.sourcename: records the given string as a 'convert_source' extra
454 457 value on each commit made in the target repository. The default is None.
455 458
456 459 All Destinations
457 460 ################
458 461
459 462 All destination types accept the following options:
460 463
461 464 :convert.skiptags: does not convert tags from the source repo to the target
462 465 repo. The default is False.
463 466 """
464 467 return convcmd.convert(ui, src, dest, revmapfile, **opts)
465 468
466 469 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
467 470 def debugsvnlog(ui, **opts):
468 471 return subversion.debugsvnlog(ui, **opts)
469 472
470 473 @command('debugcvsps',
471 474 [
472 475 # Main options shared with cvsps-2.1
473 476 ('b', 'branches', [], _('only return changes on specified branches')),
474 477 ('p', 'prefix', '', _('prefix to remove from file names')),
475 478 ('r', 'revisions', [],
476 479 _('only return changes after or between specified tags')),
477 480 ('u', 'update-cache', None, _("update cvs log cache")),
478 481 ('x', 'new-cache', None, _("create new cvs log cache")),
479 482 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
480 483 ('', 'root', '', _('specify cvsroot')),
481 484 # Options specific to builtin cvsps
482 485 ('', 'parents', '', _('show parent changesets')),
483 486 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
484 487 # Options that are ignored for compatibility with cvsps-2.1
485 488 ('A', 'cvs-direct', None, _('ignored for compatibility')),
486 489 ],
487 490 _('hg debugcvsps [OPTION]... [PATH]...'),
488 491 norepo=True)
489 492 def debugcvsps(ui, *args, **opts):
490 493 '''create changeset information from CVS
491 494
492 495 This command is intended as a debugging tool for the CVS to
493 496 Mercurial converter, and can be used as a direct replacement for
494 497 cvsps.
495 498
496 499 Hg debugcvsps reads the CVS rlog for current directory (or any
497 500 named directory) in the CVS repository, and converts the log to a
498 501 series of changesets based on matching commit log entries and
499 502 dates.'''
500 503 return cvsps.debugcvsps(ui, *args, **opts)
501 504
502 505 def kwconverted(ctx, name):
503 506 rev = ctx.extra().get('convert_revision', '')
504 507 if rev.startswith('svn:'):
505 508 if name == 'svnrev':
506 509 return str(subversion.revsplit(rev)[2])
507 510 elif name == 'svnpath':
508 511 return subversion.revsplit(rev)[1]
509 512 elif name == 'svnuuid':
510 513 return subversion.revsplit(rev)[0]
511 514 return rev
512 515
513 516 templatekeyword = registrar.templatekeyword()
514 517
515 518 @templatekeyword('svnrev')
516 519 def kwsvnrev(repo, ctx, **args):
517 520 """String. Converted subversion revision number."""
518 521 return kwconverted(ctx, 'svnrev')
519 522
520 523 @templatekeyword('svnpath')
521 524 def kwsvnpath(repo, ctx, **args):
522 525 """String. Converted subversion revision project path."""
523 526 return kwconverted(ctx, 'svnpath')
524 527
525 528 @templatekeyword('svnuuid')
526 529 def kwsvnuuid(repo, ctx, **args):
527 530 """String. Converted subversion revision repository identifier."""
528 531 return kwconverted(ctx, 'svnuuid')
529 532
530 533 # tell hggettext to extract docstrings from these functions:
531 534 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,479 +1,478
1 1 # git.py - git support for the 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 from __future__ import absolute_import
8 8
9 9 import os
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import (
13 13 config,
14 14 error,
15 15 node as nodemod,
16 16 )
17 17
18 18 from . import (
19 19 common,
20 20 )
21 21
22 22 class submodule(object):
23 23 def __init__(self, path, node, url):
24 24 self.path = path
25 25 self.node = node
26 26 self.url = url
27 27
28 28 def hgsub(self):
29 29 return "%s = [git]%s" % (self.path, self.url)
30 30
31 31 def hgsubstate(self):
32 32 return "%s %s" % (self.node, self.path)
33 33
34 34 # Keys in extra fields that should not be copied if the user requests.
35 35 bannedextrakeys = {
36 36 # Git commit object built-ins.
37 37 'tree',
38 38 'parent',
39 39 'author',
40 40 'committer',
41 41 # Mercurial built-ins.
42 42 'branch',
43 43 'close',
44 44 }
45 45
46 46 class convert_git(common.converter_source, common.commandline):
47 47 # Windows does not support GIT_DIR= construct while other systems
48 48 # cannot remove environment variable. Just assume none have
49 49 # both issues.
50 50
51 51 def _gitcmd(self, cmd, *args, **kwargs):
52 52 return cmd('--git-dir=%s' % self.path, *args, **kwargs)
53 53
54 54 def gitrun0(self, *args, **kwargs):
55 55 return self._gitcmd(self.run0, *args, **kwargs)
56 56
57 57 def gitrun(self, *args, **kwargs):
58 58 return self._gitcmd(self.run, *args, **kwargs)
59 59
60 60 def gitrunlines0(self, *args, **kwargs):
61 61 return self._gitcmd(self.runlines0, *args, **kwargs)
62 62
63 63 def gitrunlines(self, *args, **kwargs):
64 64 return self._gitcmd(self.runlines, *args, **kwargs)
65 65
66 66 def gitpipe(self, *args, **kwargs):
67 67 return self._gitcmd(self._run3, *args, **kwargs)
68 68
69 69 def __init__(self, ui, path, revs=None):
70 70 super(convert_git, self).__init__(ui, path, revs=revs)
71 71 common.commandline.__init__(self, ui, 'git')
72 72
73 73 # Pass an absolute path to git to prevent from ever being interpreted
74 74 # as a URL
75 75 path = os.path.abspath(path)
76 76
77 77 if os.path.isdir(path + "/.git"):
78 78 path += "/.git"
79 79 if not os.path.exists(path + "/objects"):
80 80 raise common.NoRepo(_("%s does not look like a Git repository") %
81 81 path)
82 82
83 83 # The default value (50) is based on the default for 'git diff'.
84 84 similarity = ui.configint('convert', 'git.similarity', default=50)
85 85 if similarity < 0 or similarity > 100:
86 86 raise error.Abort(_('similarity must be between 0 and 100'))
87 87 if similarity > 0:
88 88 self.simopt = ['-C%d%%' % similarity]
89 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
90 False)
89 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder')
91 90 if findcopiesharder:
92 91 self.simopt.append('--find-copies-harder')
93 92
94 93 renamelimit = ui.configint('convert', 'git.renamelimit',
95 94 default=400)
96 95 self.simopt.append('-l%d' % renamelimit)
97 96 else:
98 97 self.simopt = []
99 98
100 99 common.checktool('git', 'git')
101 100
102 101 self.path = path
103 102 self.submodules = []
104 103
105 104 self.catfilepipe = self.gitpipe('cat-file', '--batch')
106 105
107 106 self.copyextrakeys = self.ui.configlist('convert', 'git.extrakeys')
108 107 banned = set(self.copyextrakeys) & bannedextrakeys
109 108 if banned:
110 109 raise error.Abort(_('copying of extra key is forbidden: %s') %
111 110 _(', ').join(sorted(banned)))
112 111
113 112 committeractions = self.ui.configlist('convert', 'git.committeractions')
114 113
115 114 messagedifferent = None
116 115 messagealways = None
117 116 for a in committeractions:
118 117 if a.startswith(('messagedifferent', 'messagealways')):
119 118 k = a
120 119 v = None
121 120 if '=' in a:
122 121 k, v = a.split('=', 1)
123 122
124 123 if k == 'messagedifferent':
125 124 messagedifferent = v or 'committer:'
126 125 elif k == 'messagealways':
127 126 messagealways = v or 'committer:'
128 127
129 128 if messagedifferent and messagealways:
130 129 raise error.Abort(_('committeractions cannot define both '
131 130 'messagedifferent and messagealways'))
132 131
133 132 dropcommitter = 'dropcommitter' in committeractions
134 133 replaceauthor = 'replaceauthor' in committeractions
135 134
136 135 if dropcommitter and replaceauthor:
137 136 raise error.Abort(_('committeractions cannot define both '
138 137 'dropcommitter and replaceauthor'))
139 138
140 139 if dropcommitter and messagealways:
141 140 raise error.Abort(_('committeractions cannot define both '
142 141 'dropcommitter and messagealways'))
143 142
144 143 if not messagedifferent and not messagealways:
145 144 messagedifferent = 'committer:'
146 145
147 146 self.committeractions = {
148 147 'dropcommitter': dropcommitter,
149 148 'replaceauthor': replaceauthor,
150 149 'messagedifferent': messagedifferent,
151 150 'messagealways': messagealways,
152 151 }
153 152
154 153 def after(self):
155 154 for f in self.catfilepipe:
156 155 f.close()
157 156
158 157 def getheads(self):
159 158 if not self.revs:
160 159 output, status = self.gitrun('rev-parse', '--branches', '--remotes')
161 160 heads = output.splitlines()
162 161 if status:
163 162 raise error.Abort(_('cannot retrieve git heads'))
164 163 else:
165 164 heads = []
166 165 for rev in self.revs:
167 166 rawhead, ret = self.gitrun('rev-parse', '--verify', rev)
168 167 heads.append(rawhead[:-1])
169 168 if ret:
170 169 raise error.Abort(_('cannot retrieve git head "%s"') % rev)
171 170 return heads
172 171
173 172 def catfile(self, rev, type):
174 173 if rev == nodemod.nullhex:
175 174 raise IOError
176 175 self.catfilepipe[0].write(rev+'\n')
177 176 self.catfilepipe[0].flush()
178 177 info = self.catfilepipe[1].readline().split()
179 178 if info[1] != type:
180 179 raise error.Abort(_('cannot read %r object at %s') % (type, rev))
181 180 size = int(info[2])
182 181 data = self.catfilepipe[1].read(size)
183 182 if len(data) < size:
184 183 raise error.Abort(_('cannot read %r object at %s: unexpected size')
185 184 % (type, rev))
186 185 # read the trailing newline
187 186 self.catfilepipe[1].read(1)
188 187 return data
189 188
190 189 def getfile(self, name, rev):
191 190 if rev == nodemod.nullhex:
192 191 return None, None
193 192 if name == '.hgsub':
194 193 data = '\n'.join([m.hgsub() for m in self.submoditer()])
195 194 mode = ''
196 195 elif name == '.hgsubstate':
197 196 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
198 197 mode = ''
199 198 else:
200 199 data = self.catfile(rev, "blob")
201 200 mode = self.modecache[(name, rev)]
202 201 return data, mode
203 202
204 203 def submoditer(self):
205 204 null = nodemod.nullhex
206 205 for m in sorted(self.submodules, key=lambda p: p.path):
207 206 if m.node != null:
208 207 yield m
209 208
210 209 def parsegitmodules(self, content):
211 210 """Parse the formatted .gitmodules file, example file format:
212 211 [submodule "sub"]\n
213 212 \tpath = sub\n
214 213 \turl = git://giturl\n
215 214 """
216 215 self.submodules = []
217 216 c = config.config()
218 217 # Each item in .gitmodules starts with whitespace that cant be parsed
219 218 c.parse('.gitmodules', '\n'.join(line.strip() for line in
220 219 content.split('\n')))
221 220 for sec in c.sections():
222 221 s = c[sec]
223 222 if 'url' in s and 'path' in s:
224 223 self.submodules.append(submodule(s['path'], '', s['url']))
225 224
226 225 def retrievegitmodules(self, version):
227 226 modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules'))
228 227 if ret:
229 228 # This can happen if a file is in the repo that has permissions
230 229 # 160000, but there is no .gitmodules file.
231 230 self.ui.warn(_("warning: cannot read submodules config file in "
232 231 "%s\n") % version)
233 232 return
234 233
235 234 try:
236 235 self.parsegitmodules(modules)
237 236 except error.ParseError:
238 237 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
239 238 % version)
240 239 return
241 240
242 241 for m in self.submodules:
243 242 node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path))
244 243 if ret:
245 244 continue
246 245 m.node = node.strip()
247 246
248 247 def getchanges(self, version, full):
249 248 if full:
250 249 raise error.Abort(_("convert from git does not support --full"))
251 250 self.modecache = {}
252 251 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
253 252 output, status = self.gitrun(*cmd)
254 253 if status:
255 254 raise error.Abort(_('cannot read changes in %s') % version)
256 255 changes = []
257 256 copies = {}
258 257 seen = set()
259 258 entry = None
260 259 subexists = [False]
261 260 subdeleted = [False]
262 261 difftree = output.split('\x00')
263 262 lcount = len(difftree)
264 263 i = 0
265 264
266 265 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules',
267 266 False)
268 267 def add(entry, f, isdest):
269 268 seen.add(f)
270 269 h = entry[3]
271 270 p = (entry[1] == "100755")
272 271 s = (entry[1] == "120000")
273 272 renamesource = (not isdest and entry[4][0] == 'R')
274 273
275 274 if f == '.gitmodules':
276 275 if skipsubmodules:
277 276 return
278 277
279 278 subexists[0] = True
280 279 if entry[4] == 'D' or renamesource:
281 280 subdeleted[0] = True
282 281 changes.append(('.hgsub', nodemod.nullhex))
283 282 else:
284 283 changes.append(('.hgsub', ''))
285 284 elif entry[1] == '160000' or entry[0] == ':160000':
286 285 if not skipsubmodules:
287 286 subexists[0] = True
288 287 else:
289 288 if renamesource:
290 289 h = nodemod.nullhex
291 290 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
292 291 changes.append((f, h))
293 292
294 293 while i < lcount:
295 294 l = difftree[i]
296 295 i += 1
297 296 if not entry:
298 297 if not l.startswith(':'):
299 298 continue
300 299 entry = l.split()
301 300 continue
302 301 f = l
303 302 if entry[4][0] == 'C':
304 303 copysrc = f
305 304 copydest = difftree[i]
306 305 i += 1
307 306 f = copydest
308 307 copies[copydest] = copysrc
309 308 if f not in seen:
310 309 add(entry, f, False)
311 310 # A file can be copied multiple times, or modified and copied
312 311 # simultaneously. So f can be repeated even if fdest isn't.
313 312 if entry[4][0] == 'R':
314 313 # rename: next line is the destination
315 314 fdest = difftree[i]
316 315 i += 1
317 316 if fdest not in seen:
318 317 add(entry, fdest, True)
319 318 # .gitmodules isn't imported at all, so it being copied to
320 319 # and fro doesn't really make sense
321 320 if f != '.gitmodules' and fdest != '.gitmodules':
322 321 copies[fdest] = f
323 322 entry = None
324 323
325 324 if subexists[0]:
326 325 if subdeleted[0]:
327 326 changes.append(('.hgsubstate', nodemod.nullhex))
328 327 else:
329 328 self.retrievegitmodules(version)
330 329 changes.append(('.hgsubstate', ''))
331 330 return (changes, copies, set())
332 331
333 332 def getcommit(self, version):
334 333 c = self.catfile(version, "commit") # read the commit hash
335 334 end = c.find("\n\n")
336 335 message = c[end + 2:]
337 336 message = self.recode(message)
338 337 l = c[:end].splitlines()
339 338 parents = []
340 339 author = committer = None
341 340 extra = {}
342 341 for e in l[1:]:
343 342 n, v = e.split(" ", 1)
344 343 if n == "author":
345 344 p = v.split()
346 345 tm, tz = p[-2:]
347 346 author = " ".join(p[:-2])
348 347 if author[0] == "<": author = author[1:-1]
349 348 author = self.recode(author)
350 349 if n == "committer":
351 350 p = v.split()
352 351 tm, tz = p[-2:]
353 352 committer = " ".join(p[:-2])
354 353 if committer[0] == "<": committer = committer[1:-1]
355 354 committer = self.recode(committer)
356 355 if n == "parent":
357 356 parents.append(v)
358 357 if n in self.copyextrakeys:
359 358 extra[n] = v
360 359
361 360 if self.committeractions['dropcommitter']:
362 361 committer = None
363 362 elif self.committeractions['replaceauthor']:
364 363 author = committer
365 364
366 365 if committer:
367 366 messagealways = self.committeractions['messagealways']
368 367 messagedifferent = self.committeractions['messagedifferent']
369 368 if messagealways:
370 369 message += '\n%s %s\n' % (messagealways, committer)
371 370 elif messagedifferent and author != committer:
372 371 message += '\n%s %s\n' % (messagedifferent, committer)
373 372
374 373 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
375 374 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
376 375 date = tm + " " + str(tz)
377 376 saverev = self.ui.configbool('convert', 'git.saverev', True)
378 377
379 378 c = common.commit(parents=parents, date=date, author=author,
380 379 desc=message,
381 380 rev=version,
382 381 extra=extra,
383 382 saverev=saverev)
384 383 return c
385 384
386 385 def numcommits(self):
387 386 output, ret = self.gitrunlines('rev-list', '--all')
388 387 if ret:
389 388 raise error.Abort(_('cannot retrieve number of commits in %s') \
390 389 % self.path)
391 390 return len(output)
392 391
393 392 def gettags(self):
394 393 tags = {}
395 394 alltags = {}
396 395 output, status = self.gitrunlines('ls-remote', '--tags', self.path)
397 396
398 397 if status:
399 398 raise error.Abort(_('cannot read tags from %s') % self.path)
400 399 prefix = 'refs/tags/'
401 400
402 401 # Build complete list of tags, both annotated and bare ones
403 402 for line in output:
404 403 line = line.strip()
405 404 if line.startswith("error:") or line.startswith("fatal:"):
406 405 raise error.Abort(_('cannot read tags from %s') % self.path)
407 406 node, tag = line.split(None, 1)
408 407 if not tag.startswith(prefix):
409 408 continue
410 409 alltags[tag[len(prefix):]] = node
411 410
412 411 # Filter out tag objects for annotated tag refs
413 412 for tag in alltags:
414 413 if tag.endswith('^{}'):
415 414 tags[tag[:-3]] = alltags[tag]
416 415 else:
417 416 if tag + '^{}' in alltags:
418 417 continue
419 418 else:
420 419 tags[tag] = alltags[tag]
421 420
422 421 return tags
423 422
424 423 def getchangedfiles(self, version, i):
425 424 changes = []
426 425 if i is None:
427 426 output, status = self.gitrunlines('diff-tree', '--root', '-m',
428 427 '-r', version)
429 428 if status:
430 429 raise error.Abort(_('cannot read changes in %s') % version)
431 430 for l in output:
432 431 if "\t" not in l:
433 432 continue
434 433 m, f = l[:-1].split("\t")
435 434 changes.append(f)
436 435 else:
437 436 output, status = self.gitrunlines('diff-tree', '--name-only',
438 437 '--root', '-r', version,
439 438 '%s^%s' % (version, i + 1), '--')
440 439 if status:
441 440 raise error.Abort(_('cannot read changes in %s') % version)
442 441 changes = [f.rstrip('\n') for f in output]
443 442
444 443 return changes
445 444
446 445 def getbookmarks(self):
447 446 bookmarks = {}
448 447
449 448 # Handle local and remote branches
450 449 remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote')
451 450 reftypes = [
452 451 # (git prefix, hg prefix)
453 452 ('refs/remotes/origin/', remoteprefix + '/'),
454 453 ('refs/heads/', '')
455 454 ]
456 455
457 456 exclude = {
458 457 'refs/remotes/origin/HEAD',
459 458 }
460 459
461 460 try:
462 461 output, status = self.gitrunlines('show-ref')
463 462 for line in output:
464 463 line = line.strip()
465 464 rev, name = line.split(None, 1)
466 465 # Process each type of branch
467 466 for gitprefix, hgprefix in reftypes:
468 467 if not name.startswith(gitprefix) or name in exclude:
469 468 continue
470 469 name = '%s%s' % (hgprefix, name[len(gitprefix):])
471 470 bookmarks[name] = rev
472 471 except Exception:
473 472 pass
474 473
475 474 return bookmarks
476 475
477 476 def checkrevformat(self, revstr, mapname='splicemap'):
478 477 """ git revision string is a 40 byte hex """
479 478 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now