##// END OF EJS Templates
convert: transcode CVS log messages by specified encoding (issue5597)...
FUJIWARA Katsunori -
r33388:0823f098 default
parent child Browse files
Show More
@@ -1,503 +1,509 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''import revisions from foreign VCS repositories into Mercurial'''
8 '''import revisions from foreign VCS repositories into Mercurial'''
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 registrar,
14 registrar,
15 )
15 )
16
16
17 from . import (
17 from . import (
18 convcmd,
18 convcmd,
19 cvsps,
19 cvsps,
20 subversion,
20 subversion,
21 )
21 )
22
22
23 cmdtable = {}
23 cmdtable = {}
24 command = registrar.command(cmdtable)
24 command = registrar.command(cmdtable)
25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
25 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
26 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
27 # be specifying the version(s) of Mercurial they are tested with, or
27 # be specifying the version(s) of Mercurial they are tested with, or
28 # leave the attribute unspecified.
28 # leave the attribute unspecified.
29 testedwith = 'ships-with-hg-core'
29 testedwith = 'ships-with-hg-core'
30
30
31 # Commands definition was moved elsewhere to ease demandload job.
31 # Commands definition was moved elsewhere to ease demandload job.
32
32
33 @command('convert',
33 @command('convert',
34 [('', 'authors', '',
34 [('', 'authors', '',
35 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
35 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
36 _('FILE')),
36 _('FILE')),
37 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
37 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
38 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
38 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
39 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
39 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
40 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
40 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
41 ('', 'filemap', '', _('remap file names using contents of file'),
41 ('', 'filemap', '', _('remap file names using contents of file'),
42 _('FILE')),
42 _('FILE')),
43 ('', 'full', None,
43 ('', 'full', None,
44 _('apply filemap changes by converting all files again')),
44 _('apply filemap changes by converting all files again')),
45 ('', 'splicemap', '', _('splice synthesized history into place'),
45 ('', 'splicemap', '', _('splice synthesized history into place'),
46 _('FILE')),
46 _('FILE')),
47 ('', 'branchmap', '', _('change branch names while converting'),
47 ('', 'branchmap', '', _('change branch names while converting'),
48 _('FILE')),
48 _('FILE')),
49 ('', 'branchsort', None, _('try to sort changesets by branches')),
49 ('', 'branchsort', None, _('try to sort changesets by branches')),
50 ('', 'datesort', None, _('try to sort changesets by date')),
50 ('', 'datesort', None, _('try to sort changesets by date')),
51 ('', 'sourcesort', None, _('preserve source changesets order')),
51 ('', 'sourcesort', None, _('preserve source changesets order')),
52 ('', 'closesort', None, _('try to reorder closed revisions'))],
52 ('', 'closesort', None, _('try to reorder closed revisions'))],
53 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
53 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
54 norepo=True)
54 norepo=True)
55 def convert(ui, src, dest=None, revmapfile=None, **opts):
55 def convert(ui, src, dest=None, revmapfile=None, **opts):
56 """convert a foreign SCM repository to a Mercurial one.
56 """convert a foreign SCM repository to a Mercurial one.
57
57
58 Accepted source formats [identifiers]:
58 Accepted source formats [identifiers]:
59
59
60 - Mercurial [hg]
60 - Mercurial [hg]
61 - CVS [cvs]
61 - CVS [cvs]
62 - Darcs [darcs]
62 - Darcs [darcs]
63 - git [git]
63 - git [git]
64 - Subversion [svn]
64 - Subversion [svn]
65 - Monotone [mtn]
65 - Monotone [mtn]
66 - GNU Arch [gnuarch]
66 - GNU Arch [gnuarch]
67 - Bazaar [bzr]
67 - Bazaar [bzr]
68 - Perforce [p4]
68 - Perforce [p4]
69
69
70 Accepted destination formats [identifiers]:
70 Accepted destination formats [identifiers]:
71
71
72 - Mercurial [hg]
72 - Mercurial [hg]
73 - Subversion [svn] (history on branches is not preserved)
73 - Subversion [svn] (history on branches is not preserved)
74
74
75 If no revision is given, all revisions will be converted.
75 If no revision is given, all revisions will be converted.
76 Otherwise, convert will only import up to the named revision
76 Otherwise, convert will only import up to the named revision
77 (given in a format understood by the source).
77 (given in a format understood by the source).
78
78
79 If no destination directory name is specified, it defaults to the
79 If no destination directory name is specified, it defaults to the
80 basename of the source with ``-hg`` appended. If the destination
80 basename of the source with ``-hg`` appended. If the destination
81 repository doesn't exist, it will be created.
81 repository doesn't exist, it will be created.
82
82
83 By default, all sources except Mercurial will use --branchsort.
83 By default, all sources except Mercurial will use --branchsort.
84 Mercurial uses --sourcesort to preserve original revision numbers
84 Mercurial uses --sourcesort to preserve original revision numbers
85 order. Sort modes have the following effects:
85 order. Sort modes have the following effects:
86
86
87 --branchsort convert from parent to child revision when possible,
87 --branchsort convert from parent to child revision when possible,
88 which means branches are usually converted one after
88 which means branches are usually converted one after
89 the other. It generates more compact repositories.
89 the other. It generates more compact repositories.
90
90
91 --datesort sort revisions by date. Converted repositories have
91 --datesort sort revisions by date. Converted repositories have
92 good-looking changelogs but are often an order of
92 good-looking changelogs but are often an order of
93 magnitude larger than the same ones generated by
93 magnitude larger than the same ones generated by
94 --branchsort.
94 --branchsort.
95
95
96 --sourcesort try to preserve source revisions order, only
96 --sourcesort try to preserve source revisions order, only
97 supported by Mercurial sources.
97 supported by Mercurial sources.
98
98
99 --closesort try to move closed revisions as close as possible
99 --closesort try to move closed revisions as close as possible
100 to parent branches, only supported by Mercurial
100 to parent branches, only supported by Mercurial
101 sources.
101 sources.
102
102
103 If ``REVMAP`` isn't given, it will be put in a default location
103 If ``REVMAP`` isn't given, it will be put in a default location
104 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
104 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
105 text file that maps each source commit ID to the destination ID
105 text file that maps each source commit ID to the destination ID
106 for that revision, like so::
106 for that revision, like so::
107
107
108 <source ID> <destination ID>
108 <source ID> <destination ID>
109
109
110 If the file doesn't exist, it's automatically created. It's
110 If the file doesn't exist, it's automatically created. It's
111 updated on each commit copied, so :hg:`convert` can be interrupted
111 updated on each commit copied, so :hg:`convert` can be interrupted
112 and can be run repeatedly to copy new commits.
112 and can be run repeatedly to copy new commits.
113
113
114 The authormap is a simple text file that maps each source commit
114 The authormap is a simple text file that maps each source commit
115 author to a destination commit author. It is handy for source SCMs
115 author to a destination commit author. It is handy for source SCMs
116 that use unix logins to identify authors (e.g.: CVS). One line per
116 that use unix logins to identify authors (e.g.: CVS). One line per
117 author mapping and the line format is::
117 author mapping and the line format is::
118
118
119 source author = destination author
119 source author = destination author
120
120
121 Empty lines and lines starting with a ``#`` are ignored.
121 Empty lines and lines starting with a ``#`` are ignored.
122
122
123 The filemap is a file that allows filtering and remapping of files
123 The filemap is a file that allows filtering and remapping of files
124 and directories. Each line can contain one of the following
124 and directories. Each line can contain one of the following
125 directives::
125 directives::
126
126
127 include path/to/file-or-dir
127 include path/to/file-or-dir
128
128
129 exclude path/to/file-or-dir
129 exclude path/to/file-or-dir
130
130
131 rename path/to/source path/to/destination
131 rename path/to/source path/to/destination
132
132
133 Comment lines start with ``#``. A specified path matches if it
133 Comment lines start with ``#``. A specified path matches if it
134 equals the full relative name of a file or one of its parent
134 equals the full relative name of a file or one of its parent
135 directories. The ``include`` or ``exclude`` directive with the
135 directories. The ``include`` or ``exclude`` directive with the
136 longest matching path applies, so line order does not matter.
136 longest matching path applies, so line order does not matter.
137
137
138 The ``include`` directive causes a file, or all files under a
138 The ``include`` directive causes a file, or all files under a
139 directory, to be included in the destination repository. The default
139 directory, to be included in the destination repository. The default
140 if there are no ``include`` statements is to include everything.
140 if there are no ``include`` statements is to include everything.
141 If there are any ``include`` statements, nothing else is included.
141 If there are any ``include`` statements, nothing else is included.
142 The ``exclude`` directive causes files or directories to
142 The ``exclude`` directive causes files or directories to
143 be omitted. The ``rename`` directive renames a file or directory if
143 be omitted. The ``rename`` directive renames a file or directory if
144 it is converted. To rename from a subdirectory into the root of
144 it is converted. To rename from a subdirectory into the root of
145 the repository, use ``.`` as the path to rename to.
145 the repository, use ``.`` as the path to rename to.
146
146
147 ``--full`` will make sure the converted changesets contain exactly
147 ``--full`` will make sure the converted changesets contain exactly
148 the right files with the right content. It will make a full
148 the right files with the right content. It will make a full
149 conversion of all files, not just the ones that have
149 conversion of all files, not just the ones that have
150 changed. Files that already are correct will not be changed. This
150 changed. Files that already are correct will not be changed. This
151 can be used to apply filemap changes when converting
151 can be used to apply filemap changes when converting
152 incrementally. This is currently only supported for Mercurial and
152 incrementally. This is currently only supported for Mercurial and
153 Subversion.
153 Subversion.
154
154
155 The splicemap is a file that allows insertion of synthetic
155 The splicemap is a file that allows insertion of synthetic
156 history, letting you specify the parents of a revision. This is
156 history, letting you specify the parents of a revision. This is
157 useful if you want to e.g. give a Subversion merge two parents, or
157 useful if you want to e.g. give a Subversion merge two parents, or
158 graft two disconnected series of history together. Each entry
158 graft two disconnected series of history together. Each entry
159 contains a key, followed by a space, followed by one or two
159 contains a key, followed by a space, followed by one or two
160 comma-separated values::
160 comma-separated values::
161
161
162 key parent1, parent2
162 key parent1, parent2
163
163
164 The key is the revision ID in the source
164 The key is the revision ID in the source
165 revision control system whose parents should be modified (same
165 revision control system whose parents should be modified (same
166 format as a key in .hg/shamap). The values are the revision IDs
166 format as a key in .hg/shamap). The values are the revision IDs
167 (in either the source or destination revision control system) that
167 (in either the source or destination revision control system) that
168 should be used as the new parents for that node. For example, if
168 should be used as the new parents for that node. For example, if
169 you have merged "release-1.0" into "trunk", then you should
169 you have merged "release-1.0" into "trunk", then you should
170 specify the revision on "trunk" as the first parent and the one on
170 specify the revision on "trunk" as the first parent and the one on
171 the "release-1.0" branch as the second.
171 the "release-1.0" branch as the second.
172
172
173 The branchmap is a file that allows you to rename a branch when it is
173 The branchmap is a file that allows you to rename a branch when it is
174 being brought in from whatever external repository. When used in
174 being brought in from whatever external repository. When used in
175 conjunction with a splicemap, it allows for a powerful combination
175 conjunction with a splicemap, it allows for a powerful combination
176 to help fix even the most badly mismanaged repositories and turn them
176 to help fix even the most badly mismanaged repositories and turn them
177 into nicely structured Mercurial repositories. The branchmap contains
177 into nicely structured Mercurial repositories. The branchmap contains
178 lines of the form::
178 lines of the form::
179
179
180 original_branch_name new_branch_name
180 original_branch_name new_branch_name
181
181
182 where "original_branch_name" is the name of the branch in the
182 where "original_branch_name" is the name of the branch in the
183 source repository, and "new_branch_name" is the name of the branch
183 source repository, and "new_branch_name" is the name of the branch
184 is the destination repository. No whitespace is allowed in the new
184 is the destination repository. No whitespace is allowed in the new
185 branch name. This can be used to (for instance) move code in one
185 branch name. This can be used to (for instance) move code in one
186 repository from "default" to a named branch.
186 repository from "default" to a named branch.
187
187
188 Mercurial Source
188 Mercurial Source
189 ################
189 ################
190
190
191 The Mercurial source recognizes the following configuration
191 The Mercurial source recognizes the following configuration
192 options, which you can set on the command line with ``--config``:
192 options, which you can set on the command line with ``--config``:
193
193
194 :convert.hg.ignoreerrors: ignore integrity errors when reading.
194 :convert.hg.ignoreerrors: ignore integrity errors when reading.
195 Use it to fix Mercurial repositories with missing revlogs, by
195 Use it to fix Mercurial repositories with missing revlogs, by
196 converting from and to Mercurial. Default is False.
196 converting from and to Mercurial. Default is False.
197
197
198 :convert.hg.saverev: store original revision ID in changeset
198 :convert.hg.saverev: store original revision ID in changeset
199 (forces target IDs to change). It takes a boolean argument and
199 (forces target IDs to change). It takes a boolean argument and
200 defaults to False.
200 defaults to False.
201
201
202 :convert.hg.startrev: specify the initial Mercurial revision.
202 :convert.hg.startrev: specify the initial Mercurial revision.
203 The default is 0.
203 The default is 0.
204
204
205 :convert.hg.revs: revset specifying the source revisions to convert.
205 :convert.hg.revs: revset specifying the source revisions to convert.
206
206
207 CVS Source
207 CVS Source
208 ##########
208 ##########
209
209
210 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
210 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
211 to indicate the starting point of what will be converted. Direct
211 to indicate the starting point of what will be converted. Direct
212 access to the repository files is not needed, unless of course the
212 access to the repository files is not needed, unless of course the
213 repository is ``:local:``. The conversion uses the top level
213 repository is ``:local:``. The conversion uses the top level
214 directory in the sandbox to find the CVS repository, and then uses
214 directory in the sandbox to find the CVS repository, and then uses
215 CVS rlog commands to find files to convert. This means that unless
215 CVS rlog commands to find files to convert. This means that unless
216 a filemap is given, all files under the starting directory will be
216 a filemap is given, all files under the starting directory will be
217 converted, and that any directory reorganization in the CVS
217 converted, and that any directory reorganization in the CVS
218 sandbox is ignored.
218 sandbox is ignored.
219
219
220 The following options can be used with ``--config``:
220 The following options can be used with ``--config``:
221
221
222 :convert.cvsps.cache: Set to False to disable remote log caching,
222 :convert.cvsps.cache: Set to False to disable remote log caching,
223 for testing and debugging purposes. Default is True.
223 for testing and debugging purposes. Default is True.
224
224
225 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
225 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
226 allowed between commits with identical user and log message in
226 allowed between commits with identical user and log message in
227 a single changeset. When very large files were checked in as
227 a single changeset. When very large files were checked in as
228 part of a changeset then the default may not be long enough.
228 part of a changeset then the default may not be long enough.
229 The default is 60.
229 The default is 60.
230
230
231 :convert.cvsps.logencoding: Specify encoding name to be used for
232 transcoding CVS log messages. Multiple encoding names can be
233 specified as a list (see :hg:`help config.Syntax`), but only
234 the first acceptable encoding in the list is used per CVS log
235 entries. This transcoding is executed before cvslog hook below.
236
231 :convert.cvsps.mergeto: Specify a regular expression to which
237 :convert.cvsps.mergeto: Specify a regular expression to which
232 commit log messages are matched. If a match occurs, then the
238 commit log messages are matched. If a match occurs, then the
233 conversion process will insert a dummy revision merging the
239 conversion process will insert a dummy revision merging the
234 branch on which this log message occurs to the branch
240 branch on which this log message occurs to the branch
235 indicated in the regex. Default is ``{{mergetobranch
241 indicated in the regex. Default is ``{{mergetobranch
236 ([-\\w]+)}}``
242 ([-\\w]+)}}``
237
243
238 :convert.cvsps.mergefrom: Specify a regular expression to which
244 :convert.cvsps.mergefrom: Specify a regular expression to which
239 commit log messages are matched. If a match occurs, then the
245 commit log messages are matched. If a match occurs, then the
240 conversion process will add the most recent revision on the
246 conversion process will add the most recent revision on the
241 branch indicated in the regex as the second parent of the
247 branch indicated in the regex as the second parent of the
242 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
248 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
243
249
244 :convert.localtimezone: use local time (as determined by the TZ
250 :convert.localtimezone: use local time (as determined by the TZ
245 environment variable) for changeset date/times. The default
251 environment variable) for changeset date/times. The default
246 is False (use UTC).
252 is False (use UTC).
247
253
248 :hooks.cvslog: Specify a Python function to be called at the end of
254 :hooks.cvslog: Specify a Python function to be called at the end of
249 gathering the CVS log. The function is passed a list with the
255 gathering the CVS log. The function is passed a list with the
250 log entries, and can modify the entries in-place, or add or
256 log entries, and can modify the entries in-place, or add or
251 delete them.
257 delete them.
252
258
253 :hooks.cvschangesets: Specify a Python function to be called after
259 :hooks.cvschangesets: Specify a Python function to be called after
254 the changesets are calculated from the CVS log. The
260 the changesets are calculated from the CVS log. The
255 function is passed a list with the changeset entries, and can
261 function is passed a list with the changeset entries, and can
256 modify the changesets in-place, or add or delete them.
262 modify the changesets in-place, or add or delete them.
257
263
258 An additional "debugcvsps" Mercurial command allows the builtin
264 An additional "debugcvsps" Mercurial command allows the builtin
259 changeset merging code to be run without doing a conversion. Its
265 changeset merging code to be run without doing a conversion. Its
260 parameters and output are similar to that of cvsps 2.1. Please see
266 parameters and output are similar to that of cvsps 2.1. Please see
261 the command help for more details.
267 the command help for more details.
262
268
263 Subversion Source
269 Subversion Source
264 #################
270 #################
265
271
266 Subversion source detects classical trunk/branches/tags layouts.
272 Subversion source detects classical trunk/branches/tags layouts.
267 By default, the supplied ``svn://repo/path/`` source URL is
273 By default, the supplied ``svn://repo/path/`` source URL is
268 converted as a single branch. If ``svn://repo/path/trunk`` exists
274 converted as a single branch. If ``svn://repo/path/trunk`` exists
269 it replaces the default branch. If ``svn://repo/path/branches``
275 it replaces the default branch. If ``svn://repo/path/branches``
270 exists, its subdirectories are listed as possible branches. If
276 exists, its subdirectories are listed as possible branches. If
271 ``svn://repo/path/tags`` exists, it is looked for tags referencing
277 ``svn://repo/path/tags`` exists, it is looked for tags referencing
272 converted branches. Default ``trunk``, ``branches`` and ``tags``
278 converted branches. Default ``trunk``, ``branches`` and ``tags``
273 values can be overridden with following options. Set them to paths
279 values can be overridden with following options. Set them to paths
274 relative to the source URL, or leave them blank to disable auto
280 relative to the source URL, or leave them blank to disable auto
275 detection.
281 detection.
276
282
277 The following options can be set with ``--config``:
283 The following options can be set with ``--config``:
278
284
279 :convert.svn.branches: specify the directory containing branches.
285 :convert.svn.branches: specify the directory containing branches.
280 The default is ``branches``.
286 The default is ``branches``.
281
287
282 :convert.svn.tags: specify the directory containing tags. The
288 :convert.svn.tags: specify the directory containing tags. The
283 default is ``tags``.
289 default is ``tags``.
284
290
285 :convert.svn.trunk: specify the name of the trunk branch. The
291 :convert.svn.trunk: specify the name of the trunk branch. The
286 default is ``trunk``.
292 default is ``trunk``.
287
293
288 :convert.localtimezone: use local time (as determined by the TZ
294 :convert.localtimezone: use local time (as determined by the TZ
289 environment variable) for changeset date/times. The default
295 environment variable) for changeset date/times. The default
290 is False (use UTC).
296 is False (use UTC).
291
297
292 Source history can be retrieved starting at a specific revision,
298 Source history can be retrieved starting at a specific revision,
293 instead of being integrally converted. Only single branch
299 instead of being integrally converted. Only single branch
294 conversions are supported.
300 conversions are supported.
295
301
296 :convert.svn.startrev: specify start Subversion revision number.
302 :convert.svn.startrev: specify start Subversion revision number.
297 The default is 0.
303 The default is 0.
298
304
299 Git Source
305 Git Source
300 ##########
306 ##########
301
307
302 The Git importer converts commits from all reachable branches (refs
308 The Git importer converts commits from all reachable branches (refs
303 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
309 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
304 Branches are converted to bookmarks with the same name, with the
310 Branches are converted to bookmarks with the same name, with the
305 leading 'refs/heads' stripped. Git submodules are converted to Git
311 leading 'refs/heads' stripped. Git submodules are converted to Git
306 subrepos in Mercurial.
312 subrepos in Mercurial.
307
313
308 The following options can be set with ``--config``:
314 The following options can be set with ``--config``:
309
315
310 :convert.git.similarity: specify how similar files modified in a
316 :convert.git.similarity: specify how similar files modified in a
311 commit must be to be imported as renames or copies, as a
317 commit must be to be imported as renames or copies, as a
312 percentage between ``0`` (disabled) and ``100`` (files must be
318 percentage between ``0`` (disabled) and ``100`` (files must be
313 identical). For example, ``90`` means that a delete/add pair will
319 identical). For example, ``90`` means that a delete/add pair will
314 be imported as a rename if more than 90% of the file hasn't
320 be imported as a rename if more than 90% of the file hasn't
315 changed. The default is ``50``.
321 changed. The default is ``50``.
316
322
317 :convert.git.findcopiesharder: while detecting copies, look at all
323 :convert.git.findcopiesharder: while detecting copies, look at all
318 files in the working copy instead of just changed ones. This
324 files in the working copy instead of just changed ones. This
319 is very expensive for large projects, and is only effective when
325 is very expensive for large projects, and is only effective when
320 ``convert.git.similarity`` is greater than 0. The default is False.
326 ``convert.git.similarity`` is greater than 0. The default is False.
321
327
322 :convert.git.renamelimit: perform rename and copy detection up to this
328 :convert.git.renamelimit: perform rename and copy detection up to this
323 many changed files in a commit. Increasing this will make rename
329 many changed files in a commit. Increasing this will make rename
324 and copy detection more accurate but will significantly slow down
330 and copy detection more accurate but will significantly slow down
325 computation on large projects. The option is only relevant if
331 computation on large projects. The option is only relevant if
326 ``convert.git.similarity`` is greater than 0. The default is
332 ``convert.git.similarity`` is greater than 0. The default is
327 ``400``.
333 ``400``.
328
334
329 :convert.git.committeractions: list of actions to take when processing
335 :convert.git.committeractions: list of actions to take when processing
330 author and committer values.
336 author and committer values.
331
337
332 Git commits have separate author (who wrote the commit) and committer
338 Git commits have separate author (who wrote the commit) and committer
333 (who applied the commit) fields. Not all destinations support separate
339 (who applied the commit) fields. Not all destinations support separate
334 author and committer fields (including Mercurial). This config option
340 author and committer fields (including Mercurial). This config option
335 controls what to do with these author and committer fields during
341 controls what to do with these author and committer fields during
336 conversion.
342 conversion.
337
343
338 A value of ``messagedifferent`` will append a ``committer: ...``
344 A value of ``messagedifferent`` will append a ``committer: ...``
339 line to the commit message if the Git committer is different from the
345 line to the commit message if the Git committer is different from the
340 author. The prefix of that line can be specified using the syntax
346 author. The prefix of that line can be specified using the syntax
341 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
347 ``messagedifferent=<prefix>``. e.g. ``messagedifferent=git-committer:``.
342 When a prefix is specified, a space will always be inserted between the
348 When a prefix is specified, a space will always be inserted between the
343 prefix and the value.
349 prefix and the value.
344
350
345 ``messagealways`` behaves like ``messagedifferent`` except it will
351 ``messagealways`` behaves like ``messagedifferent`` except it will
346 always result in a ``committer: ...`` line being appended to the commit
352 always result in a ``committer: ...`` line being appended to the commit
347 message. This value is mutually exclusive with ``messagedifferent``.
353 message. This value is mutually exclusive with ``messagedifferent``.
348
354
349 ``dropcommitter`` will remove references to the committer. Only
355 ``dropcommitter`` will remove references to the committer. Only
350 references to the author will remain. Actions that add references
356 references to the author will remain. Actions that add references
351 to the committer will have no effect when this is set.
357 to the committer will have no effect when this is set.
352
358
353 ``replaceauthor`` will replace the value of the author field with
359 ``replaceauthor`` will replace the value of the author field with
354 the committer. Other actions that add references to the committer
360 the committer. Other actions that add references to the committer
355 will still take effect when this is set.
361 will still take effect when this is set.
356
362
357 The default is ``messagedifferent``.
363 The default is ``messagedifferent``.
358
364
359 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
365 :convert.git.extrakeys: list of extra keys from commit metadata to copy to
360 the destination. Some Git repositories store extra metadata in commits.
366 the destination. Some Git repositories store extra metadata in commits.
361 By default, this non-default metadata will be lost during conversion.
367 By default, this non-default metadata will be lost during conversion.
362 Setting this config option can retain that metadata. Some built-in
368 Setting this config option can retain that metadata. Some built-in
363 keys such as ``parent`` and ``branch`` are not allowed to be copied.
369 keys such as ``parent`` and ``branch`` are not allowed to be copied.
364
370
365 :convert.git.remoteprefix: remote refs are converted as bookmarks with
371 :convert.git.remoteprefix: remote refs are converted as bookmarks with
366 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
372 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
367 is 'remote'.
373 is 'remote'.
368
374
369 :convert.git.saverev: whether to store the original Git commit ID in the
375 :convert.git.saverev: whether to store the original Git commit ID in the
370 metadata of the destination commit. The default is True.
376 metadata of the destination commit. The default is True.
371
377
372 :convert.git.skipsubmodules: does not convert root level .gitmodules files
378 :convert.git.skipsubmodules: does not convert root level .gitmodules files
373 or files with 160000 mode indicating a submodule. Default is False.
379 or files with 160000 mode indicating a submodule. Default is False.
374
380
375 Perforce Source
381 Perforce Source
376 ###############
382 ###############
377
383
378 The Perforce (P4) importer can be given a p4 depot path or a
384 The Perforce (P4) importer can be given a p4 depot path or a
379 client specification as source. It will convert all files in the
385 client specification as source. It will convert all files in the
380 source to a flat Mercurial repository, ignoring labels, branches
386 source to a flat Mercurial repository, ignoring labels, branches
381 and integrations. Note that when a depot path is given you then
387 and integrations. Note that when a depot path is given you then
382 usually should specify a target directory, because otherwise the
388 usually should specify a target directory, because otherwise the
383 target may be named ``...-hg``.
389 target may be named ``...-hg``.
384
390
385 The following options can be set with ``--config``:
391 The following options can be set with ``--config``:
386
392
387 :convert.p4.encoding: specify the encoding to use when decoding standard
393 :convert.p4.encoding: specify the encoding to use when decoding standard
388 output of the Perforce command line tool. The default is default system
394 output of the Perforce command line tool. The default is default system
389 encoding.
395 encoding.
390
396
391 :convert.p4.startrev: specify initial Perforce revision (a
397 :convert.p4.startrev: specify initial Perforce revision (a
392 Perforce changelist number).
398 Perforce changelist number).
393
399
394 Mercurial Destination
400 Mercurial Destination
395 #####################
401 #####################
396
402
397 The Mercurial destination will recognize Mercurial subrepositories in the
403 The Mercurial destination will recognize Mercurial subrepositories in the
398 destination directory, and update the .hgsubstate file automatically if the
404 destination directory, and update the .hgsubstate file automatically if the
399 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
405 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
400 Converting a repository with subrepositories requires converting a single
406 Converting a repository with subrepositories requires converting a single
401 repository at a time, from the bottom up.
407 repository at a time, from the bottom up.
402
408
403 .. container:: verbose
409 .. container:: verbose
404
410
405 An example showing how to convert a repository with subrepositories::
411 An example showing how to convert a repository with subrepositories::
406
412
407 # so convert knows the type when it sees a non empty destination
413 # so convert knows the type when it sees a non empty destination
408 $ hg init converted
414 $ hg init converted
409
415
410 $ hg convert orig/sub1 converted/sub1
416 $ hg convert orig/sub1 converted/sub1
411 $ hg convert orig/sub2 converted/sub2
417 $ hg convert orig/sub2 converted/sub2
412 $ hg convert orig converted
418 $ hg convert orig converted
413
419
414 The following options are supported:
420 The following options are supported:
415
421
416 :convert.hg.clonebranches: dispatch source branches in separate
422 :convert.hg.clonebranches: dispatch source branches in separate
417 clones. The default is False.
423 clones. The default is False.
418
424
419 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
425 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
420 ``default``.
426 ``default``.
421
427
422 :convert.hg.usebranchnames: preserve branch names. The default is
428 :convert.hg.usebranchnames: preserve branch names. The default is
423 True.
429 True.
424
430
425 :convert.hg.sourcename: records the given string as a 'convert_source' extra
431 :convert.hg.sourcename: records the given string as a 'convert_source' extra
426 value on each commit made in the target repository. The default is None.
432 value on each commit made in the target repository. The default is None.
427
433
428 All Destinations
434 All Destinations
429 ################
435 ################
430
436
431 All destination types accept the following options:
437 All destination types accept the following options:
432
438
433 :convert.skiptags: does not convert tags from the source repo to the target
439 :convert.skiptags: does not convert tags from the source repo to the target
434 repo. The default is False.
440 repo. The default is False.
435 """
441 """
436 return convcmd.convert(ui, src, dest, revmapfile, **opts)
442 return convcmd.convert(ui, src, dest, revmapfile, **opts)
437
443
438 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
444 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
439 def debugsvnlog(ui, **opts):
445 def debugsvnlog(ui, **opts):
440 return subversion.debugsvnlog(ui, **opts)
446 return subversion.debugsvnlog(ui, **opts)
441
447
442 @command('debugcvsps',
448 @command('debugcvsps',
443 [
449 [
444 # Main options shared with cvsps-2.1
450 # Main options shared with cvsps-2.1
445 ('b', 'branches', [], _('only return changes on specified branches')),
451 ('b', 'branches', [], _('only return changes on specified branches')),
446 ('p', 'prefix', '', _('prefix to remove from file names')),
452 ('p', 'prefix', '', _('prefix to remove from file names')),
447 ('r', 'revisions', [],
453 ('r', 'revisions', [],
448 _('only return changes after or between specified tags')),
454 _('only return changes after or between specified tags')),
449 ('u', 'update-cache', None, _("update cvs log cache")),
455 ('u', 'update-cache', None, _("update cvs log cache")),
450 ('x', 'new-cache', None, _("create new cvs log cache")),
456 ('x', 'new-cache', None, _("create new cvs log cache")),
451 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
457 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
452 ('', 'root', '', _('specify cvsroot')),
458 ('', 'root', '', _('specify cvsroot')),
453 # Options specific to builtin cvsps
459 # Options specific to builtin cvsps
454 ('', 'parents', '', _('show parent changesets')),
460 ('', 'parents', '', _('show parent changesets')),
455 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
461 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
456 # Options that are ignored for compatibility with cvsps-2.1
462 # Options that are ignored for compatibility with cvsps-2.1
457 ('A', 'cvs-direct', None, _('ignored for compatibility')),
463 ('A', 'cvs-direct', None, _('ignored for compatibility')),
458 ],
464 ],
459 _('hg debugcvsps [OPTION]... [PATH]...'),
465 _('hg debugcvsps [OPTION]... [PATH]...'),
460 norepo=True)
466 norepo=True)
461 def debugcvsps(ui, *args, **opts):
467 def debugcvsps(ui, *args, **opts):
462 '''create changeset information from CVS
468 '''create changeset information from CVS
463
469
464 This command is intended as a debugging tool for the CVS to
470 This command is intended as a debugging tool for the CVS to
465 Mercurial converter, and can be used as a direct replacement for
471 Mercurial converter, and can be used as a direct replacement for
466 cvsps.
472 cvsps.
467
473
468 Hg debugcvsps reads the CVS rlog for current directory (or any
474 Hg debugcvsps reads the CVS rlog for current directory (or any
469 named directory) in the CVS repository, and converts the log to a
475 named directory) in the CVS repository, and converts the log to a
470 series of changesets based on matching commit log entries and
476 series of changesets based on matching commit log entries and
471 dates.'''
477 dates.'''
472 return cvsps.debugcvsps(ui, *args, **opts)
478 return cvsps.debugcvsps(ui, *args, **opts)
473
479
474 def kwconverted(ctx, name):
480 def kwconverted(ctx, name):
475 rev = ctx.extra().get('convert_revision', '')
481 rev = ctx.extra().get('convert_revision', '')
476 if rev.startswith('svn:'):
482 if rev.startswith('svn:'):
477 if name == 'svnrev':
483 if name == 'svnrev':
478 return str(subversion.revsplit(rev)[2])
484 return str(subversion.revsplit(rev)[2])
479 elif name == 'svnpath':
485 elif name == 'svnpath':
480 return subversion.revsplit(rev)[1]
486 return subversion.revsplit(rev)[1]
481 elif name == 'svnuuid':
487 elif name == 'svnuuid':
482 return subversion.revsplit(rev)[0]
488 return subversion.revsplit(rev)[0]
483 return rev
489 return rev
484
490
485 templatekeyword = registrar.templatekeyword()
491 templatekeyword = registrar.templatekeyword()
486
492
487 @templatekeyword('svnrev')
493 @templatekeyword('svnrev')
488 def kwsvnrev(repo, ctx, **args):
494 def kwsvnrev(repo, ctx, **args):
489 """String. Converted subversion revision number."""
495 """String. Converted subversion revision number."""
490 return kwconverted(ctx, 'svnrev')
496 return kwconverted(ctx, 'svnrev')
491
497
492 @templatekeyword('svnpath')
498 @templatekeyword('svnpath')
493 def kwsvnpath(repo, ctx, **args):
499 def kwsvnpath(repo, ctx, **args):
494 """String. Converted subversion revision project path."""
500 """String. Converted subversion revision project path."""
495 return kwconverted(ctx, 'svnpath')
501 return kwconverted(ctx, 'svnpath')
496
502
497 @templatekeyword('svnuuid')
503 @templatekeyword('svnuuid')
498 def kwsvnuuid(repo, ctx, **args):
504 def kwsvnuuid(repo, ctx, **args):
499 """String. Converted subversion revision repository identifier."""
505 """String. Converted subversion revision repository identifier."""
500 return kwconverted(ctx, 'svnuuid')
506 return kwconverted(ctx, 'svnuuid')
501
507
502 # tell hggettext to extract docstrings from these functions:
508 # tell hggettext to extract docstrings from these functions:
503 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
509 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,921 +1,951 b''
1 # Mercurial built-in replacement for cvsps.
1 # Mercurial built-in replacement for cvsps.
2 #
2 #
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import os
9 import os
10 import re
10 import re
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial import (
13 from mercurial import (
14 encoding,
14 encoding,
15 error,
15 hook,
16 hook,
16 pycompat,
17 pycompat,
17 util,
18 util,
18 )
19 )
19
20
20 pickle = util.pickle
21 pickle = util.pickle
21
22
22 class logentry(object):
23 class logentry(object):
23 '''Class logentry has the following attributes:
24 '''Class logentry has the following attributes:
24 .author - author name as CVS knows it
25 .author - author name as CVS knows it
25 .branch - name of branch this revision is on
26 .branch - name of branch this revision is on
26 .branches - revision tuple of branches starting at this revision
27 .branches - revision tuple of branches starting at this revision
27 .comment - commit message
28 .comment - commit message
28 .commitid - CVS commitid or None
29 .commitid - CVS commitid or None
29 .date - the commit date as a (time, tz) tuple
30 .date - the commit date as a (time, tz) tuple
30 .dead - true if file revision is dead
31 .dead - true if file revision is dead
31 .file - Name of file
32 .file - Name of file
32 .lines - a tuple (+lines, -lines) or None
33 .lines - a tuple (+lines, -lines) or None
33 .parent - Previous revision of this entry
34 .parent - Previous revision of this entry
34 .rcs - name of file as returned from CVS
35 .rcs - name of file as returned from CVS
35 .revision - revision number as tuple
36 .revision - revision number as tuple
36 .tags - list of tags on the file
37 .tags - list of tags on the file
37 .synthetic - is this a synthetic "file ... added on ..." revision?
38 .synthetic - is this a synthetic "file ... added on ..." revision?
38 .mergepoint - the branch that has been merged from (if present in
39 .mergepoint - the branch that has been merged from (if present in
39 rlog output) or None
40 rlog output) or None
40 .branchpoints - the branches that start at the current entry or empty
41 .branchpoints - the branches that start at the current entry or empty
41 '''
42 '''
42 def __init__(self, **entries):
43 def __init__(self, **entries):
43 self.synthetic = False
44 self.synthetic = False
44 self.__dict__.update(entries)
45 self.__dict__.update(entries)
45
46
46 def __repr__(self):
47 def __repr__(self):
47 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
48 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
48 return "%s(%s)"%(type(self).__name__, ", ".join(items))
49 return "%s(%s)"%(type(self).__name__, ", ".join(items))
49
50
50 class logerror(Exception):
51 class logerror(Exception):
51 pass
52 pass
52
53
53 def getrepopath(cvspath):
54 def getrepopath(cvspath):
54 """Return the repository path from a CVS path.
55 """Return the repository path from a CVS path.
55
56
56 >>> getrepopath('/foo/bar')
57 >>> getrepopath('/foo/bar')
57 '/foo/bar'
58 '/foo/bar'
58 >>> getrepopath('c:/foo/bar')
59 >>> getrepopath('c:/foo/bar')
59 '/foo/bar'
60 '/foo/bar'
60 >>> getrepopath(':pserver:10/foo/bar')
61 >>> getrepopath(':pserver:10/foo/bar')
61 '/foo/bar'
62 '/foo/bar'
62 >>> getrepopath(':pserver:10c:/foo/bar')
63 >>> getrepopath(':pserver:10c:/foo/bar')
63 '/foo/bar'
64 '/foo/bar'
64 >>> getrepopath(':pserver:/foo/bar')
65 >>> getrepopath(':pserver:/foo/bar')
65 '/foo/bar'
66 '/foo/bar'
66 >>> getrepopath(':pserver:c:/foo/bar')
67 >>> getrepopath(':pserver:c:/foo/bar')
67 '/foo/bar'
68 '/foo/bar'
68 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
69 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
69 '/foo/bar'
70 '/foo/bar'
70 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
71 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
71 '/foo/bar'
72 '/foo/bar'
72 >>> getrepopath('user@server/path/to/repository')
73 >>> getrepopath('user@server/path/to/repository')
73 '/path/to/repository'
74 '/path/to/repository'
74 """
75 """
75 # According to CVS manual, CVS paths are expressed like:
76 # According to CVS manual, CVS paths are expressed like:
76 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
77 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
77 #
78 #
78 # CVSpath is splitted into parts and then position of the first occurrence
79 # CVSpath is splitted into parts and then position of the first occurrence
79 # of the '/' char after the '@' is located. The solution is the rest of the
80 # of the '/' char after the '@' is located. The solution is the rest of the
80 # string after that '/' sign including it
81 # string after that '/' sign including it
81
82
82 parts = cvspath.split(':')
83 parts = cvspath.split(':')
83 atposition = parts[-1].find('@')
84 atposition = parts[-1].find('@')
84 start = 0
85 start = 0
85
86
86 if atposition != -1:
87 if atposition != -1:
87 start = atposition
88 start = atposition
88
89
89 repopath = parts[-1][parts[-1].find('/', start):]
90 repopath = parts[-1][parts[-1].find('/', start):]
90 return repopath
91 return repopath
91
92
92 def createlog(ui, directory=None, root="", rlog=True, cache=None):
93 def createlog(ui, directory=None, root="", rlog=True, cache=None):
93 '''Collect the CVS rlog'''
94 '''Collect the CVS rlog'''
94
95
95 # Because we store many duplicate commit log messages, reusing strings
96 # Because we store many duplicate commit log messages, reusing strings
96 # saves a lot of memory and pickle storage space.
97 # saves a lot of memory and pickle storage space.
97 _scache = {}
98 _scache = {}
98 def scache(s):
99 def scache(s):
99 "return a shared version of a string"
100 "return a shared version of a string"
100 return _scache.setdefault(s, s)
101 return _scache.setdefault(s, s)
101
102
102 ui.status(_('collecting CVS rlog\n'))
103 ui.status(_('collecting CVS rlog\n'))
103
104
104 log = [] # list of logentry objects containing the CVS state
105 log = [] # list of logentry objects containing the CVS state
105
106
106 # patterns to match in CVS (r)log output, by state of use
107 # patterns to match in CVS (r)log output, by state of use
107 re_00 = re.compile('RCS file: (.+)$')
108 re_00 = re.compile('RCS file: (.+)$')
108 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
109 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
109 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
110 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
110 re_03 = re.compile("(Cannot access.+CVSROOT)|"
111 re_03 = re.compile("(Cannot access.+CVSROOT)|"
111 "(can't create temporary directory.+)$")
112 "(can't create temporary directory.+)$")
112 re_10 = re.compile('Working file: (.+)$')
113 re_10 = re.compile('Working file: (.+)$')
113 re_20 = re.compile('symbolic names:')
114 re_20 = re.compile('symbolic names:')
114 re_30 = re.compile('\t(.+): ([\\d.]+)$')
115 re_30 = re.compile('\t(.+): ([\\d.]+)$')
115 re_31 = re.compile('----------------------------$')
116 re_31 = re.compile('----------------------------$')
116 re_32 = re.compile('======================================='
117 re_32 = re.compile('======================================='
117 '======================================$')
118 '======================================$')
118 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
119 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
119 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
120 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);'
120 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
121 r'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?'
121 r'(\s+commitid:\s+([^;]+);)?'
122 r'(\s+commitid:\s+([^;]+);)?'
122 r'(.*mergepoint:\s+([^;]+);)?')
123 r'(.*mergepoint:\s+([^;]+);)?')
123 re_70 = re.compile('branches: (.+);$')
124 re_70 = re.compile('branches: (.+);$')
124
125
125 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
126 file_added_re = re.compile(r'file [^/]+ was (initially )?added on branch')
126
127
127 prefix = '' # leading path to strip of what we get from CVS
128 prefix = '' # leading path to strip of what we get from CVS
128
129
129 if directory is None:
130 if directory is None:
130 # Current working directory
131 # Current working directory
131
132
132 # Get the real directory in the repository
133 # Get the real directory in the repository
133 try:
134 try:
134 prefix = open(os.path.join('CVS','Repository')).read().strip()
135 prefix = open(os.path.join('CVS','Repository')).read().strip()
135 directory = prefix
136 directory = prefix
136 if prefix == ".":
137 if prefix == ".":
137 prefix = ""
138 prefix = ""
138 except IOError:
139 except IOError:
139 raise logerror(_('not a CVS sandbox'))
140 raise logerror(_('not a CVS sandbox'))
140
141
141 if prefix and not prefix.endswith(pycompat.ossep):
142 if prefix and not prefix.endswith(pycompat.ossep):
142 prefix += pycompat.ossep
143 prefix += pycompat.ossep
143
144
144 # Use the Root file in the sandbox, if it exists
145 # Use the Root file in the sandbox, if it exists
145 try:
146 try:
146 root = open(os.path.join('CVS','Root')).read().strip()
147 root = open(os.path.join('CVS','Root')).read().strip()
147 except IOError:
148 except IOError:
148 pass
149 pass
149
150
150 if not root:
151 if not root:
151 root = encoding.environ.get('CVSROOT', '')
152 root = encoding.environ.get('CVSROOT', '')
152
153
153 # read log cache if one exists
154 # read log cache if one exists
154 oldlog = []
155 oldlog = []
155 date = None
156 date = None
156
157
157 if cache:
158 if cache:
158 cachedir = os.path.expanduser('~/.hg.cvsps')
159 cachedir = os.path.expanduser('~/.hg.cvsps')
159 if not os.path.exists(cachedir):
160 if not os.path.exists(cachedir):
160 os.mkdir(cachedir)
161 os.mkdir(cachedir)
161
162
162 # The cvsps cache pickle needs a uniquified name, based on the
163 # The cvsps cache pickle needs a uniquified name, based on the
163 # repository location. The address may have all sort of nasties
164 # repository location. The address may have all sort of nasties
164 # in it, slashes, colons and such. So here we take just the
165 # in it, slashes, colons and such. So here we take just the
165 # alphanumeric characters, concatenated in a way that does not
166 # alphanumeric characters, concatenated in a way that does not
166 # mix up the various components, so that
167 # mix up the various components, so that
167 # :pserver:user@server:/path
168 # :pserver:user@server:/path
168 # and
169 # and
169 # /pserver/user/server/path
170 # /pserver/user/server/path
170 # are mapped to different cache file names.
171 # are mapped to different cache file names.
171 cachefile = root.split(":") + [directory, "cache"]
172 cachefile = root.split(":") + [directory, "cache"]
172 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
173 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
173 cachefile = os.path.join(cachedir,
174 cachefile = os.path.join(cachedir,
174 '.'.join([s for s in cachefile if s]))
175 '.'.join([s for s in cachefile if s]))
175
176
176 if cache == 'update':
177 if cache == 'update':
177 try:
178 try:
178 ui.note(_('reading cvs log cache %s\n') % cachefile)
179 ui.note(_('reading cvs log cache %s\n') % cachefile)
179 oldlog = pickle.load(open(cachefile))
180 oldlog = pickle.load(open(cachefile))
180 for e in oldlog:
181 for e in oldlog:
181 if not (util.safehasattr(e, 'branchpoints') and
182 if not (util.safehasattr(e, 'branchpoints') and
182 util.safehasattr(e, 'commitid') and
183 util.safehasattr(e, 'commitid') and
183 util.safehasattr(e, 'mergepoint')):
184 util.safehasattr(e, 'mergepoint')):
184 ui.status(_('ignoring old cache\n'))
185 ui.status(_('ignoring old cache\n'))
185 oldlog = []
186 oldlog = []
186 break
187 break
187
188
188 ui.note(_('cache has %d log entries\n') % len(oldlog))
189 ui.note(_('cache has %d log entries\n') % len(oldlog))
189 except Exception as e:
190 except Exception as e:
190 ui.note(_('error reading cache: %r\n') % e)
191 ui.note(_('error reading cache: %r\n') % e)
191
192
192 if oldlog:
193 if oldlog:
193 date = oldlog[-1].date # last commit date as a (time,tz) tuple
194 date = oldlog[-1].date # last commit date as a (time,tz) tuple
194 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
195 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
195
196
196 # build the CVS commandline
197 # build the CVS commandline
197 cmd = ['cvs', '-q']
198 cmd = ['cvs', '-q']
198 if root:
199 if root:
199 cmd.append('-d%s' % root)
200 cmd.append('-d%s' % root)
200 p = util.normpath(getrepopath(root))
201 p = util.normpath(getrepopath(root))
201 if not p.endswith('/'):
202 if not p.endswith('/'):
202 p += '/'
203 p += '/'
203 if prefix:
204 if prefix:
204 # looks like normpath replaces "" by "."
205 # looks like normpath replaces "" by "."
205 prefix = p + util.normpath(prefix)
206 prefix = p + util.normpath(prefix)
206 else:
207 else:
207 prefix = p
208 prefix = p
208 cmd.append(['log', 'rlog'][rlog])
209 cmd.append(['log', 'rlog'][rlog])
209 if date:
210 if date:
210 # no space between option and date string
211 # no space between option and date string
211 cmd.append('-d>%s' % date)
212 cmd.append('-d>%s' % date)
212 cmd.append(directory)
213 cmd.append(directory)
213
214
214 # state machine begins here
215 # state machine begins here
215 tags = {} # dictionary of revisions on current file with their tags
216 tags = {} # dictionary of revisions on current file with their tags
216 branchmap = {} # mapping between branch names and revision numbers
217 branchmap = {} # mapping between branch names and revision numbers
217 rcsmap = {}
218 rcsmap = {}
218 state = 0
219 state = 0
219 store = False # set when a new record can be appended
220 store = False # set when a new record can be appended
220
221
221 cmd = [util.shellquote(arg) for arg in cmd]
222 cmd = [util.shellquote(arg) for arg in cmd]
222 ui.note(_("running %s\n") % (' '.join(cmd)))
223 ui.note(_("running %s\n") % (' '.join(cmd)))
223 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
224 ui.debug("prefix=%r directory=%r root=%r\n" % (prefix, directory, root))
224
225
225 pfp = util.popen(' '.join(cmd))
226 pfp = util.popen(' '.join(cmd))
226 peek = pfp.readline()
227 peek = pfp.readline()
227 while True:
228 while True:
228 line = peek
229 line = peek
229 if line == '':
230 if line == '':
230 break
231 break
231 peek = pfp.readline()
232 peek = pfp.readline()
232 if line.endswith('\n'):
233 if line.endswith('\n'):
233 line = line[:-1]
234 line = line[:-1]
234 #ui.debug('state=%d line=%r\n' % (state, line))
235 #ui.debug('state=%d line=%r\n' % (state, line))
235
236
236 if state == 0:
237 if state == 0:
237 # initial state, consume input until we see 'RCS file'
238 # initial state, consume input until we see 'RCS file'
238 match = re_00.match(line)
239 match = re_00.match(line)
239 if match:
240 if match:
240 rcs = match.group(1)
241 rcs = match.group(1)
241 tags = {}
242 tags = {}
242 if rlog:
243 if rlog:
243 filename = util.normpath(rcs[:-2])
244 filename = util.normpath(rcs[:-2])
244 if filename.startswith(prefix):
245 if filename.startswith(prefix):
245 filename = filename[len(prefix):]
246 filename = filename[len(prefix):]
246 if filename.startswith('/'):
247 if filename.startswith('/'):
247 filename = filename[1:]
248 filename = filename[1:]
248 if filename.startswith('Attic/'):
249 if filename.startswith('Attic/'):
249 filename = filename[6:]
250 filename = filename[6:]
250 else:
251 else:
251 filename = filename.replace('/Attic/', '/')
252 filename = filename.replace('/Attic/', '/')
252 state = 2
253 state = 2
253 continue
254 continue
254 state = 1
255 state = 1
255 continue
256 continue
256 match = re_01.match(line)
257 match = re_01.match(line)
257 if match:
258 if match:
258 raise logerror(match.group(1))
259 raise logerror(match.group(1))
259 match = re_02.match(line)
260 match = re_02.match(line)
260 if match:
261 if match:
261 raise logerror(match.group(2))
262 raise logerror(match.group(2))
262 if re_03.match(line):
263 if re_03.match(line):
263 raise logerror(line)
264 raise logerror(line)
264
265
265 elif state == 1:
266 elif state == 1:
266 # expect 'Working file' (only when using log instead of rlog)
267 # expect 'Working file' (only when using log instead of rlog)
267 match = re_10.match(line)
268 match = re_10.match(line)
268 assert match, _('RCS file must be followed by working file')
269 assert match, _('RCS file must be followed by working file')
269 filename = util.normpath(match.group(1))
270 filename = util.normpath(match.group(1))
270 state = 2
271 state = 2
271
272
272 elif state == 2:
273 elif state == 2:
273 # expect 'symbolic names'
274 # expect 'symbolic names'
274 if re_20.match(line):
275 if re_20.match(line):
275 branchmap = {}
276 branchmap = {}
276 state = 3
277 state = 3
277
278
278 elif state == 3:
279 elif state == 3:
279 # read the symbolic names and store as tags
280 # read the symbolic names and store as tags
280 match = re_30.match(line)
281 match = re_30.match(line)
281 if match:
282 if match:
282 rev = [int(x) for x in match.group(2).split('.')]
283 rev = [int(x) for x in match.group(2).split('.')]
283
284
284 # Convert magic branch number to an odd-numbered one
285 # Convert magic branch number to an odd-numbered one
285 revn = len(rev)
286 revn = len(rev)
286 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
287 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
287 rev = rev[:-2] + rev[-1:]
288 rev = rev[:-2] + rev[-1:]
288 rev = tuple(rev)
289 rev = tuple(rev)
289
290
290 if rev not in tags:
291 if rev not in tags:
291 tags[rev] = []
292 tags[rev] = []
292 tags[rev].append(match.group(1))
293 tags[rev].append(match.group(1))
293 branchmap[match.group(1)] = match.group(2)
294 branchmap[match.group(1)] = match.group(2)
294
295
295 elif re_31.match(line):
296 elif re_31.match(line):
296 state = 5
297 state = 5
297 elif re_32.match(line):
298 elif re_32.match(line):
298 state = 0
299 state = 0
299
300
300 elif state == 4:
301 elif state == 4:
301 # expecting '------' separator before first revision
302 # expecting '------' separator before first revision
302 if re_31.match(line):
303 if re_31.match(line):
303 state = 5
304 state = 5
304 else:
305 else:
305 assert not re_32.match(line), _('must have at least '
306 assert not re_32.match(line), _('must have at least '
306 'some revisions')
307 'some revisions')
307
308
308 elif state == 5:
309 elif state == 5:
309 # expecting revision number and possibly (ignored) lock indication
310 # expecting revision number and possibly (ignored) lock indication
310 # we create the logentry here from values stored in states 0 to 4,
311 # we create the logentry here from values stored in states 0 to 4,
311 # as this state is re-entered for subsequent revisions of a file.
312 # as this state is re-entered for subsequent revisions of a file.
312 match = re_50.match(line)
313 match = re_50.match(line)
313 assert match, _('expected revision number')
314 assert match, _('expected revision number')
314 e = logentry(rcs=scache(rcs),
315 e = logentry(rcs=scache(rcs),
315 file=scache(filename),
316 file=scache(filename),
316 revision=tuple([int(x) for x in
317 revision=tuple([int(x) for x in
317 match.group(1).split('.')]),
318 match.group(1).split('.')]),
318 branches=[],
319 branches=[],
319 parent=None,
320 parent=None,
320 commitid=None,
321 commitid=None,
321 mergepoint=None,
322 mergepoint=None,
322 branchpoints=set())
323 branchpoints=set())
323
324
324 state = 6
325 state = 6
325
326
326 elif state == 6:
327 elif state == 6:
327 # expecting date, author, state, lines changed
328 # expecting date, author, state, lines changed
328 match = re_60.match(line)
329 match = re_60.match(line)
329 assert match, _('revision must be followed by date line')
330 assert match, _('revision must be followed by date line')
330 d = match.group(1)
331 d = match.group(1)
331 if d[2] == '/':
332 if d[2] == '/':
332 # Y2K
333 # Y2K
333 d = '19' + d
334 d = '19' + d
334
335
335 if len(d.split()) != 3:
336 if len(d.split()) != 3:
336 # cvs log dates always in GMT
337 # cvs log dates always in GMT
337 d = d + ' UTC'
338 d = d + ' UTC'
338 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
339 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S',
339 '%Y/%m/%d %H:%M:%S',
340 '%Y/%m/%d %H:%M:%S',
340 '%Y-%m-%d %H:%M:%S'])
341 '%Y-%m-%d %H:%M:%S'])
341 e.author = scache(match.group(2))
342 e.author = scache(match.group(2))
342 e.dead = match.group(3).lower() == 'dead'
343 e.dead = match.group(3).lower() == 'dead'
343
344
344 if match.group(5):
345 if match.group(5):
345 if match.group(6):
346 if match.group(6):
346 e.lines = (int(match.group(5)), int(match.group(6)))
347 e.lines = (int(match.group(5)), int(match.group(6)))
347 else:
348 else:
348 e.lines = (int(match.group(5)), 0)
349 e.lines = (int(match.group(5)), 0)
349 elif match.group(6):
350 elif match.group(6):
350 e.lines = (0, int(match.group(6)))
351 e.lines = (0, int(match.group(6)))
351 else:
352 else:
352 e.lines = None
353 e.lines = None
353
354
354 if match.group(7): # cvs 1.12 commitid
355 if match.group(7): # cvs 1.12 commitid
355 e.commitid = match.group(8)
356 e.commitid = match.group(8)
356
357
357 if match.group(9): # cvsnt mergepoint
358 if match.group(9): # cvsnt mergepoint
358 myrev = match.group(10).split('.')
359 myrev = match.group(10).split('.')
359 if len(myrev) == 2: # head
360 if len(myrev) == 2: # head
360 e.mergepoint = 'HEAD'
361 e.mergepoint = 'HEAD'
361 else:
362 else:
362 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
363 myrev = '.'.join(myrev[:-2] + ['0', myrev[-2]])
363 branches = [b for b in branchmap if branchmap[b] == myrev]
364 branches = [b for b in branchmap if branchmap[b] == myrev]
364 assert len(branches) == 1, ('unknown branch: %s'
365 assert len(branches) == 1, ('unknown branch: %s'
365 % e.mergepoint)
366 % e.mergepoint)
366 e.mergepoint = branches[0]
367 e.mergepoint = branches[0]
367
368
368 e.comment = []
369 e.comment = []
369 state = 7
370 state = 7
370
371
371 elif state == 7:
372 elif state == 7:
372 # read the revision numbers of branches that start at this revision
373 # read the revision numbers of branches that start at this revision
373 # or store the commit log message otherwise
374 # or store the commit log message otherwise
374 m = re_70.match(line)
375 m = re_70.match(line)
375 if m:
376 if m:
376 e.branches = [tuple([int(y) for y in x.strip().split('.')])
377 e.branches = [tuple([int(y) for y in x.strip().split('.')])
377 for x in m.group(1).split(';')]
378 for x in m.group(1).split(';')]
378 state = 8
379 state = 8
379 elif re_31.match(line) and re_50.match(peek):
380 elif re_31.match(line) and re_50.match(peek):
380 state = 5
381 state = 5
381 store = True
382 store = True
382 elif re_32.match(line):
383 elif re_32.match(line):
383 state = 0
384 state = 0
384 store = True
385 store = True
385 else:
386 else:
386 e.comment.append(line)
387 e.comment.append(line)
387
388
388 elif state == 8:
389 elif state == 8:
389 # store commit log message
390 # store commit log message
390 if re_31.match(line):
391 if re_31.match(line):
391 cpeek = peek
392 cpeek = peek
392 if cpeek.endswith('\n'):
393 if cpeek.endswith('\n'):
393 cpeek = cpeek[:-1]
394 cpeek = cpeek[:-1]
394 if re_50.match(cpeek):
395 if re_50.match(cpeek):
395 state = 5
396 state = 5
396 store = True
397 store = True
397 else:
398 else:
398 e.comment.append(line)
399 e.comment.append(line)
399 elif re_32.match(line):
400 elif re_32.match(line):
400 state = 0
401 state = 0
401 store = True
402 store = True
402 else:
403 else:
403 e.comment.append(line)
404 e.comment.append(line)
404
405
405 # When a file is added on a branch B1, CVS creates a synthetic
406 # When a file is added on a branch B1, CVS creates a synthetic
406 # dead trunk revision 1.1 so that the branch has a root.
407 # dead trunk revision 1.1 so that the branch has a root.
407 # Likewise, if you merge such a file to a later branch B2 (one
408 # Likewise, if you merge such a file to a later branch B2 (one
408 # that already existed when the file was added on B1), CVS
409 # that already existed when the file was added on B1), CVS
409 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
410 # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop
410 # these revisions now, but mark them synthetic so
411 # these revisions now, but mark them synthetic so
411 # createchangeset() can take care of them.
412 # createchangeset() can take care of them.
412 if (store and
413 if (store and
413 e.dead and
414 e.dead and
414 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
415 e.revision[-1] == 1 and # 1.1 or 1.1.x.1
415 len(e.comment) == 1 and
416 len(e.comment) == 1 and
416 file_added_re.match(e.comment[0])):
417 file_added_re.match(e.comment[0])):
417 ui.debug('found synthetic revision in %s: %r\n'
418 ui.debug('found synthetic revision in %s: %r\n'
418 % (e.rcs, e.comment[0]))
419 % (e.rcs, e.comment[0]))
419 e.synthetic = True
420 e.synthetic = True
420
421
421 if store:
422 if store:
422 # clean up the results and save in the log.
423 # clean up the results and save in the log.
423 store = False
424 store = False
424 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
425 e.tags = sorted([scache(x) for x in tags.get(e.revision, [])])
425 e.comment = scache('\n'.join(e.comment))
426 e.comment = scache('\n'.join(e.comment))
426
427
427 revn = len(e.revision)
428 revn = len(e.revision)
428 if revn > 3 and (revn % 2) == 0:
429 if revn > 3 and (revn % 2) == 0:
429 e.branch = tags.get(e.revision[:-1], [None])[0]
430 e.branch = tags.get(e.revision[:-1], [None])[0]
430 else:
431 else:
431 e.branch = None
432 e.branch = None
432
433
433 # find the branches starting from this revision
434 # find the branches starting from this revision
434 branchpoints = set()
435 branchpoints = set()
435 for branch, revision in branchmap.iteritems():
436 for branch, revision in branchmap.iteritems():
436 revparts = tuple([int(i) for i in revision.split('.')])
437 revparts = tuple([int(i) for i in revision.split('.')])
437 if len(revparts) < 2: # bad tags
438 if len(revparts) < 2: # bad tags
438 continue
439 continue
439 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
440 if revparts[-2] == 0 and revparts[-1] % 2 == 0:
440 # normal branch
441 # normal branch
441 if revparts[:-2] == e.revision:
442 if revparts[:-2] == e.revision:
442 branchpoints.add(branch)
443 branchpoints.add(branch)
443 elif revparts == (1, 1, 1): # vendor branch
444 elif revparts == (1, 1, 1): # vendor branch
444 if revparts in e.branches:
445 if revparts in e.branches:
445 branchpoints.add(branch)
446 branchpoints.add(branch)
446 e.branchpoints = branchpoints
447 e.branchpoints = branchpoints
447
448
448 log.append(e)
449 log.append(e)
449
450
450 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
451 rcsmap[e.rcs.replace('/Attic/', '/')] = e.rcs
451
452
452 if len(log) % 100 == 0:
453 if len(log) % 100 == 0:
453 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
454 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
454
455
455 log.sort(key=lambda x: (x.rcs, x.revision))
456 log.sort(key=lambda x: (x.rcs, x.revision))
456
457
457 # find parent revisions of individual files
458 # find parent revisions of individual files
458 versions = {}
459 versions = {}
459 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
460 for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)):
460 rcs = e.rcs.replace('/Attic/', '/')
461 rcs = e.rcs.replace('/Attic/', '/')
461 if rcs in rcsmap:
462 if rcs in rcsmap:
462 e.rcs = rcsmap[rcs]
463 e.rcs = rcsmap[rcs]
463 branch = e.revision[:-1]
464 branch = e.revision[:-1]
464 versions[(e.rcs, branch)] = e.revision
465 versions[(e.rcs, branch)] = e.revision
465
466
466 for e in log:
467 for e in log:
467 branch = e.revision[:-1]
468 branch = e.revision[:-1]
468 p = versions.get((e.rcs, branch), None)
469 p = versions.get((e.rcs, branch), None)
469 if p is None:
470 if p is None:
470 p = e.revision[:-2]
471 p = e.revision[:-2]
471 e.parent = p
472 e.parent = p
472 versions[(e.rcs, branch)] = e.revision
473 versions[(e.rcs, branch)] = e.revision
473
474
474 # update the log cache
475 # update the log cache
475 if cache:
476 if cache:
476 if log:
477 if log:
477 # join up the old and new logs
478 # join up the old and new logs
478 log.sort(key=lambda x: x.date)
479 log.sort(key=lambda x: x.date)
479
480
480 if oldlog and oldlog[-1].date >= log[0].date:
481 if oldlog and oldlog[-1].date >= log[0].date:
481 raise logerror(_('log cache overlaps with new log entries,'
482 raise logerror(_('log cache overlaps with new log entries,'
482 ' re-run without cache.'))
483 ' re-run without cache.'))
483
484
484 log = oldlog + log
485 log = oldlog + log
485
486
486 # write the new cachefile
487 # write the new cachefile
487 ui.note(_('writing cvs log cache %s\n') % cachefile)
488 ui.note(_('writing cvs log cache %s\n') % cachefile)
488 pickle.dump(log, open(cachefile, 'w'))
489 pickle.dump(log, open(cachefile, 'w'))
489 else:
490 else:
490 log = oldlog
491 log = oldlog
491
492
492 ui.status(_('%d log entries\n') % len(log))
493 ui.status(_('%d log entries\n') % len(log))
493
494
495 encodings = ui.configlist('convert', 'cvsps.logencoding')
496 if encodings:
497 def revstr(r):
498 # this is needed, because logentry.revision is a tuple of "int"
499 # (e.g. (1, 2) for "1.2")
500 return '.'.join(pycompat.maplist(pycompat.bytestr, r))
501
502 for entry in log:
503 comment = entry.comment
504 for e in encodings:
505 try:
506 entry.comment = comment.decode(e).encode('utf-8')
507 if ui.debugflag:
508 ui.debug("transcoding by %s: %s of %s\n" %
509 (e, revstr(entry.revision), entry.file))
510 break
511 except UnicodeDecodeError:
512 pass # try next encoding
513 except LookupError as inst: # unknown encoding, maybe
514 raise error.Abort(inst,
515 hint=_('check convert.cvsps.logencoding'
516 ' configuration'))
517 else:
518 raise error.Abort(_("no encoding can transcode"
519 " CVS log message for %s of %s")
520 % (revstr(entry.revision), entry.file),
521 hint=_('check convert.cvsps.logencoding'
522 ' configuration'))
523
494 hook.hook(ui, None, "cvslog", True, log=log)
524 hook.hook(ui, None, "cvslog", True, log=log)
495
525
496 return log
526 return log
497
527
498
528
499 class changeset(object):
529 class changeset(object):
500 '''Class changeset has the following attributes:
530 '''Class changeset has the following attributes:
501 .id - integer identifying this changeset (list index)
531 .id - integer identifying this changeset (list index)
502 .author - author name as CVS knows it
532 .author - author name as CVS knows it
503 .branch - name of branch this changeset is on, or None
533 .branch - name of branch this changeset is on, or None
504 .comment - commit message
534 .comment - commit message
505 .commitid - CVS commitid or None
535 .commitid - CVS commitid or None
506 .date - the commit date as a (time,tz) tuple
536 .date - the commit date as a (time,tz) tuple
507 .entries - list of logentry objects in this changeset
537 .entries - list of logentry objects in this changeset
508 .parents - list of one or two parent changesets
538 .parents - list of one or two parent changesets
509 .tags - list of tags on this changeset
539 .tags - list of tags on this changeset
510 .synthetic - from synthetic revision "file ... added on branch ..."
540 .synthetic - from synthetic revision "file ... added on branch ..."
511 .mergepoint- the branch that has been merged from or None
541 .mergepoint- the branch that has been merged from or None
512 .branchpoints- the branches that start at the current entry or empty
542 .branchpoints- the branches that start at the current entry or empty
513 '''
543 '''
514 def __init__(self, **entries):
544 def __init__(self, **entries):
515 self.id = None
545 self.id = None
516 self.synthetic = False
546 self.synthetic = False
517 self.__dict__.update(entries)
547 self.__dict__.update(entries)
518
548
519 def __repr__(self):
549 def __repr__(self):
520 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
550 items = ("%s=%r"%(k, self.__dict__[k]) for k in sorted(self.__dict__))
521 return "%s(%s)"%(type(self).__name__, ", ".join(items))
551 return "%s(%s)"%(type(self).__name__, ", ".join(items))
522
552
523 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
553 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
524 '''Convert log into changesets.'''
554 '''Convert log into changesets.'''
525
555
526 ui.status(_('creating changesets\n'))
556 ui.status(_('creating changesets\n'))
527
557
528 # try to order commitids by date
558 # try to order commitids by date
529 mindate = {}
559 mindate = {}
530 for e in log:
560 for e in log:
531 if e.commitid:
561 if e.commitid:
532 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
562 mindate[e.commitid] = min(e.date, mindate.get(e.commitid))
533
563
534 # Merge changesets
564 # Merge changesets
535 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
565 log.sort(key=lambda x: (mindate.get(x.commitid), x.commitid, x.comment,
536 x.author, x.branch, x.date, x.branchpoints))
566 x.author, x.branch, x.date, x.branchpoints))
537
567
538 changesets = []
568 changesets = []
539 files = set()
569 files = set()
540 c = None
570 c = None
541 for i, e in enumerate(log):
571 for i, e in enumerate(log):
542
572
543 # Check if log entry belongs to the current changeset or not.
573 # Check if log entry belongs to the current changeset or not.
544
574
545 # Since CVS is file-centric, two different file revisions with
575 # Since CVS is file-centric, two different file revisions with
546 # different branchpoints should be treated as belonging to two
576 # different branchpoints should be treated as belonging to two
547 # different changesets (and the ordering is important and not
577 # different changesets (and the ordering is important and not
548 # honoured by cvsps at this point).
578 # honoured by cvsps at this point).
549 #
579 #
550 # Consider the following case:
580 # Consider the following case:
551 # foo 1.1 branchpoints: [MYBRANCH]
581 # foo 1.1 branchpoints: [MYBRANCH]
552 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
582 # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2]
553 #
583 #
554 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
584 # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a
555 # later version of foo may be in MYBRANCH2, so foo should be the
585 # later version of foo may be in MYBRANCH2, so foo should be the
556 # first changeset and bar the next and MYBRANCH and MYBRANCH2
586 # first changeset and bar the next and MYBRANCH and MYBRANCH2
557 # should both start off of the bar changeset. No provisions are
587 # should both start off of the bar changeset. No provisions are
558 # made to ensure that this is, in fact, what happens.
588 # made to ensure that this is, in fact, what happens.
559 if not (c and e.branchpoints == c.branchpoints and
589 if not (c and e.branchpoints == c.branchpoints and
560 (# cvs commitids
590 (# cvs commitids
561 (e.commitid is not None and e.commitid == c.commitid) or
591 (e.commitid is not None and e.commitid == c.commitid) or
562 (# no commitids, use fuzzy commit detection
592 (# no commitids, use fuzzy commit detection
563 (e.commitid is None or c.commitid is None) and
593 (e.commitid is None or c.commitid is None) and
564 e.comment == c.comment and
594 e.comment == c.comment and
565 e.author == c.author and
595 e.author == c.author and
566 e.branch == c.branch and
596 e.branch == c.branch and
567 ((c.date[0] + c.date[1]) <=
597 ((c.date[0] + c.date[1]) <=
568 (e.date[0] + e.date[1]) <=
598 (e.date[0] + e.date[1]) <=
569 (c.date[0] + c.date[1]) + fuzz) and
599 (c.date[0] + c.date[1]) + fuzz) and
570 e.file not in files))):
600 e.file not in files))):
571 c = changeset(comment=e.comment, author=e.author,
601 c = changeset(comment=e.comment, author=e.author,
572 branch=e.branch, date=e.date,
602 branch=e.branch, date=e.date,
573 entries=[], mergepoint=e.mergepoint,
603 entries=[], mergepoint=e.mergepoint,
574 branchpoints=e.branchpoints, commitid=e.commitid)
604 branchpoints=e.branchpoints, commitid=e.commitid)
575 changesets.append(c)
605 changesets.append(c)
576
606
577 files = set()
607 files = set()
578 if len(changesets) % 100 == 0:
608 if len(changesets) % 100 == 0:
579 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
609 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
580 ui.status(util.ellipsis(t, 80) + '\n')
610 ui.status(util.ellipsis(t, 80) + '\n')
581
611
582 c.entries.append(e)
612 c.entries.append(e)
583 files.add(e.file)
613 files.add(e.file)
584 c.date = e.date # changeset date is date of latest commit in it
614 c.date = e.date # changeset date is date of latest commit in it
585
615
586 # Mark synthetic changesets
616 # Mark synthetic changesets
587
617
588 for c in changesets:
618 for c in changesets:
589 # Synthetic revisions always get their own changeset, because
619 # Synthetic revisions always get their own changeset, because
590 # the log message includes the filename. E.g. if you add file3
620 # the log message includes the filename. E.g. if you add file3
591 # and file4 on a branch, you get four log entries and three
621 # and file4 on a branch, you get four log entries and three
592 # changesets:
622 # changesets:
593 # "File file3 was added on branch ..." (synthetic, 1 entry)
623 # "File file3 was added on branch ..." (synthetic, 1 entry)
594 # "File file4 was added on branch ..." (synthetic, 1 entry)
624 # "File file4 was added on branch ..." (synthetic, 1 entry)
595 # "Add file3 and file4 to fix ..." (real, 2 entries)
625 # "Add file3 and file4 to fix ..." (real, 2 entries)
596 # Hence the check for 1 entry here.
626 # Hence the check for 1 entry here.
597 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
627 c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic
598
628
599 # Sort files in each changeset
629 # Sort files in each changeset
600
630
601 def entitycompare(l, r):
631 def entitycompare(l, r):
602 'Mimic cvsps sorting order'
632 'Mimic cvsps sorting order'
603 l = l.file.split('/')
633 l = l.file.split('/')
604 r = r.file.split('/')
634 r = r.file.split('/')
605 nl = len(l)
635 nl = len(l)
606 nr = len(r)
636 nr = len(r)
607 n = min(nl, nr)
637 n = min(nl, nr)
608 for i in range(n):
638 for i in range(n):
609 if i + 1 == nl and nl < nr:
639 if i + 1 == nl and nl < nr:
610 return -1
640 return -1
611 elif i + 1 == nr and nl > nr:
641 elif i + 1 == nr and nl > nr:
612 return +1
642 return +1
613 elif l[i] < r[i]:
643 elif l[i] < r[i]:
614 return -1
644 return -1
615 elif l[i] > r[i]:
645 elif l[i] > r[i]:
616 return +1
646 return +1
617 return 0
647 return 0
618
648
619 for c in changesets:
649 for c in changesets:
620 c.entries.sort(entitycompare)
650 c.entries.sort(entitycompare)
621
651
622 # Sort changesets by date
652 # Sort changesets by date
623
653
624 odd = set()
654 odd = set()
625 def cscmp(l, r):
655 def cscmp(l, r):
626 d = sum(l.date) - sum(r.date)
656 d = sum(l.date) - sum(r.date)
627 if d:
657 if d:
628 return d
658 return d
629
659
630 # detect vendor branches and initial commits on a branch
660 # detect vendor branches and initial commits on a branch
631 le = {}
661 le = {}
632 for e in l.entries:
662 for e in l.entries:
633 le[e.rcs] = e.revision
663 le[e.rcs] = e.revision
634 re = {}
664 re = {}
635 for e in r.entries:
665 for e in r.entries:
636 re[e.rcs] = e.revision
666 re[e.rcs] = e.revision
637
667
638 d = 0
668 d = 0
639 for e in l.entries:
669 for e in l.entries:
640 if re.get(e.rcs, None) == e.parent:
670 if re.get(e.rcs, None) == e.parent:
641 assert not d
671 assert not d
642 d = 1
672 d = 1
643 break
673 break
644
674
645 for e in r.entries:
675 for e in r.entries:
646 if le.get(e.rcs, None) == e.parent:
676 if le.get(e.rcs, None) == e.parent:
647 if d:
677 if d:
648 odd.add((l, r))
678 odd.add((l, r))
649 d = -1
679 d = -1
650 break
680 break
651 # By this point, the changesets are sufficiently compared that
681 # By this point, the changesets are sufficiently compared that
652 # we don't really care about ordering. However, this leaves
682 # we don't really care about ordering. However, this leaves
653 # some race conditions in the tests, so we compare on the
683 # some race conditions in the tests, so we compare on the
654 # number of files modified, the files contained in each
684 # number of files modified, the files contained in each
655 # changeset, and the branchpoints in the change to ensure test
685 # changeset, and the branchpoints in the change to ensure test
656 # output remains stable.
686 # output remains stable.
657
687
658 # recommended replacement for cmp from
688 # recommended replacement for cmp from
659 # https://docs.python.org/3.0/whatsnew/3.0.html
689 # https://docs.python.org/3.0/whatsnew/3.0.html
660 c = lambda x, y: (x > y) - (x < y)
690 c = lambda x, y: (x > y) - (x < y)
661 # Sort bigger changes first.
691 # Sort bigger changes first.
662 if not d:
692 if not d:
663 d = c(len(l.entries), len(r.entries))
693 d = c(len(l.entries), len(r.entries))
664 # Try sorting by filename in the change.
694 # Try sorting by filename in the change.
665 if not d:
695 if not d:
666 d = c([e.file for e in l.entries], [e.file for e in r.entries])
696 d = c([e.file for e in l.entries], [e.file for e in r.entries])
667 # Try and put changes without a branch point before ones with
697 # Try and put changes without a branch point before ones with
668 # a branch point.
698 # a branch point.
669 if not d:
699 if not d:
670 d = c(len(l.branchpoints), len(r.branchpoints))
700 d = c(len(l.branchpoints), len(r.branchpoints))
671 return d
701 return d
672
702
673 changesets.sort(cscmp)
703 changesets.sort(cscmp)
674
704
675 # Collect tags
705 # Collect tags
676
706
677 globaltags = {}
707 globaltags = {}
678 for c in changesets:
708 for c in changesets:
679 for e in c.entries:
709 for e in c.entries:
680 for tag in e.tags:
710 for tag in e.tags:
681 # remember which is the latest changeset to have this tag
711 # remember which is the latest changeset to have this tag
682 globaltags[tag] = c
712 globaltags[tag] = c
683
713
684 for c in changesets:
714 for c in changesets:
685 tags = set()
715 tags = set()
686 for e in c.entries:
716 for e in c.entries:
687 tags.update(e.tags)
717 tags.update(e.tags)
688 # remember tags only if this is the latest changeset to have it
718 # remember tags only if this is the latest changeset to have it
689 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
719 c.tags = sorted(tag for tag in tags if globaltags[tag] is c)
690
720
691 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
721 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
692 # by inserting dummy changesets with two parents, and handle
722 # by inserting dummy changesets with two parents, and handle
693 # {{mergefrombranch BRANCHNAME}} by setting two parents.
723 # {{mergefrombranch BRANCHNAME}} by setting two parents.
694
724
695 if mergeto is None:
725 if mergeto is None:
696 mergeto = r'{{mergetobranch ([-\w]+)}}'
726 mergeto = r'{{mergetobranch ([-\w]+)}}'
697 if mergeto:
727 if mergeto:
698 mergeto = re.compile(mergeto)
728 mergeto = re.compile(mergeto)
699
729
700 if mergefrom is None:
730 if mergefrom is None:
701 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
731 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
702 if mergefrom:
732 if mergefrom:
703 mergefrom = re.compile(mergefrom)
733 mergefrom = re.compile(mergefrom)
704
734
705 versions = {} # changeset index where we saw any particular file version
735 versions = {} # changeset index where we saw any particular file version
706 branches = {} # changeset index where we saw a branch
736 branches = {} # changeset index where we saw a branch
707 n = len(changesets)
737 n = len(changesets)
708 i = 0
738 i = 0
709 while i < n:
739 while i < n:
710 c = changesets[i]
740 c = changesets[i]
711
741
712 for f in c.entries:
742 for f in c.entries:
713 versions[(f.rcs, f.revision)] = i
743 versions[(f.rcs, f.revision)] = i
714
744
715 p = None
745 p = None
716 if c.branch in branches:
746 if c.branch in branches:
717 p = branches[c.branch]
747 p = branches[c.branch]
718 else:
748 else:
719 # first changeset on a new branch
749 # first changeset on a new branch
720 # the parent is a changeset with the branch in its
750 # the parent is a changeset with the branch in its
721 # branchpoints such that it is the latest possible
751 # branchpoints such that it is the latest possible
722 # commit without any intervening, unrelated commits.
752 # commit without any intervening, unrelated commits.
723
753
724 for candidate in xrange(i):
754 for candidate in xrange(i):
725 if c.branch not in changesets[candidate].branchpoints:
755 if c.branch not in changesets[candidate].branchpoints:
726 if p is not None:
756 if p is not None:
727 break
757 break
728 continue
758 continue
729 p = candidate
759 p = candidate
730
760
731 c.parents = []
761 c.parents = []
732 if p is not None:
762 if p is not None:
733 p = changesets[p]
763 p = changesets[p]
734
764
735 # Ensure no changeset has a synthetic changeset as a parent.
765 # Ensure no changeset has a synthetic changeset as a parent.
736 while p.synthetic:
766 while p.synthetic:
737 assert len(p.parents) <= 1, \
767 assert len(p.parents) <= 1, \
738 _('synthetic changeset cannot have multiple parents')
768 _('synthetic changeset cannot have multiple parents')
739 if p.parents:
769 if p.parents:
740 p = p.parents[0]
770 p = p.parents[0]
741 else:
771 else:
742 p = None
772 p = None
743 break
773 break
744
774
745 if p is not None:
775 if p is not None:
746 c.parents.append(p)
776 c.parents.append(p)
747
777
748 if c.mergepoint:
778 if c.mergepoint:
749 if c.mergepoint == 'HEAD':
779 if c.mergepoint == 'HEAD':
750 c.mergepoint = None
780 c.mergepoint = None
751 c.parents.append(changesets[branches[c.mergepoint]])
781 c.parents.append(changesets[branches[c.mergepoint]])
752
782
753 if mergefrom:
783 if mergefrom:
754 m = mergefrom.search(c.comment)
784 m = mergefrom.search(c.comment)
755 if m:
785 if m:
756 m = m.group(1)
786 m = m.group(1)
757 if m == 'HEAD':
787 if m == 'HEAD':
758 m = None
788 m = None
759 try:
789 try:
760 candidate = changesets[branches[m]]
790 candidate = changesets[branches[m]]
761 except KeyError:
791 except KeyError:
762 ui.warn(_("warning: CVS commit message references "
792 ui.warn(_("warning: CVS commit message references "
763 "non-existent branch %r:\n%s\n")
793 "non-existent branch %r:\n%s\n")
764 % (m, c.comment))
794 % (m, c.comment))
765 if m in branches and c.branch != m and not candidate.synthetic:
795 if m in branches and c.branch != m and not candidate.synthetic:
766 c.parents.append(candidate)
796 c.parents.append(candidate)
767
797
768 if mergeto:
798 if mergeto:
769 m = mergeto.search(c.comment)
799 m = mergeto.search(c.comment)
770 if m:
800 if m:
771 if m.groups():
801 if m.groups():
772 m = m.group(1)
802 m = m.group(1)
773 if m == 'HEAD':
803 if m == 'HEAD':
774 m = None
804 m = None
775 else:
805 else:
776 m = None # if no group found then merge to HEAD
806 m = None # if no group found then merge to HEAD
777 if m in branches and c.branch != m:
807 if m in branches and c.branch != m:
778 # insert empty changeset for merge
808 # insert empty changeset for merge
779 cc = changeset(
809 cc = changeset(
780 author=c.author, branch=m, date=c.date,
810 author=c.author, branch=m, date=c.date,
781 comment='convert-repo: CVS merge from branch %s'
811 comment='convert-repo: CVS merge from branch %s'
782 % c.branch,
812 % c.branch,
783 entries=[], tags=[],
813 entries=[], tags=[],
784 parents=[changesets[branches[m]], c])
814 parents=[changesets[branches[m]], c])
785 changesets.insert(i + 1, cc)
815 changesets.insert(i + 1, cc)
786 branches[m] = i + 1
816 branches[m] = i + 1
787
817
788 # adjust our loop counters now we have inserted a new entry
818 # adjust our loop counters now we have inserted a new entry
789 n += 1
819 n += 1
790 i += 2
820 i += 2
791 continue
821 continue
792
822
793 branches[c.branch] = i
823 branches[c.branch] = i
794 i += 1
824 i += 1
795
825
796 # Drop synthetic changesets (safe now that we have ensured no other
826 # Drop synthetic changesets (safe now that we have ensured no other
797 # changesets can have them as parents).
827 # changesets can have them as parents).
798 i = 0
828 i = 0
799 while i < len(changesets):
829 while i < len(changesets):
800 if changesets[i].synthetic:
830 if changesets[i].synthetic:
801 del changesets[i]
831 del changesets[i]
802 else:
832 else:
803 i += 1
833 i += 1
804
834
805 # Number changesets
835 # Number changesets
806
836
807 for i, c in enumerate(changesets):
837 for i, c in enumerate(changesets):
808 c.id = i + 1
838 c.id = i + 1
809
839
810 if odd:
840 if odd:
811 for l, r in odd:
841 for l, r in odd:
812 if l.id is not None and r.id is not None:
842 if l.id is not None and r.id is not None:
813 ui.warn(_('changeset %d is both before and after %d\n')
843 ui.warn(_('changeset %d is both before and after %d\n')
814 % (l.id, r.id))
844 % (l.id, r.id))
815
845
816 ui.status(_('%d changeset entries\n') % len(changesets))
846 ui.status(_('%d changeset entries\n') % len(changesets))
817
847
818 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
848 hook.hook(ui, None, "cvschangesets", True, changesets=changesets)
819
849
820 return changesets
850 return changesets
821
851
822
852
823 def debugcvsps(ui, *args, **opts):
853 def debugcvsps(ui, *args, **opts):
824 '''Read CVS rlog for current directory or named path in
854 '''Read CVS rlog for current directory or named path in
825 repository, and convert the log to changesets based on matching
855 repository, and convert the log to changesets based on matching
826 commit log entries and dates.
856 commit log entries and dates.
827 '''
857 '''
828 if opts["new_cache"]:
858 if opts["new_cache"]:
829 cache = "write"
859 cache = "write"
830 elif opts["update_cache"]:
860 elif opts["update_cache"]:
831 cache = "update"
861 cache = "update"
832 else:
862 else:
833 cache = None
863 cache = None
834
864
835 revisions = opts["revisions"]
865 revisions = opts["revisions"]
836
866
837 try:
867 try:
838 if args:
868 if args:
839 log = []
869 log = []
840 for d in args:
870 for d in args:
841 log += createlog(ui, d, root=opts["root"], cache=cache)
871 log += createlog(ui, d, root=opts["root"], cache=cache)
842 else:
872 else:
843 log = createlog(ui, root=opts["root"], cache=cache)
873 log = createlog(ui, root=opts["root"], cache=cache)
844 except logerror as e:
874 except logerror as e:
845 ui.write("%r\n"%e)
875 ui.write("%r\n"%e)
846 return
876 return
847
877
848 changesets = createchangeset(ui, log, opts["fuzz"])
878 changesets = createchangeset(ui, log, opts["fuzz"])
849 del log
879 del log
850
880
851 # Print changesets (optionally filtered)
881 # Print changesets (optionally filtered)
852
882
853 off = len(revisions)
883 off = len(revisions)
854 branches = {} # latest version number in each branch
884 branches = {} # latest version number in each branch
855 ancestors = {} # parent branch
885 ancestors = {} # parent branch
856 for cs in changesets:
886 for cs in changesets:
857
887
858 if opts["ancestors"]:
888 if opts["ancestors"]:
859 if cs.branch not in branches and cs.parents and cs.parents[0].id:
889 if cs.branch not in branches and cs.parents and cs.parents[0].id:
860 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
890 ancestors[cs.branch] = (changesets[cs.parents[0].id - 1].branch,
861 cs.parents[0].id)
891 cs.parents[0].id)
862 branches[cs.branch] = cs.id
892 branches[cs.branch] = cs.id
863
893
864 # limit by branches
894 # limit by branches
865 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
895 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
866 continue
896 continue
867
897
868 if not off:
898 if not off:
869 # Note: trailing spaces on several lines here are needed to have
899 # Note: trailing spaces on several lines here are needed to have
870 # bug-for-bug compatibility with cvsps.
900 # bug-for-bug compatibility with cvsps.
871 ui.write('---------------------\n')
901 ui.write('---------------------\n')
872 ui.write(('PatchSet %d \n' % cs.id))
902 ui.write(('PatchSet %d \n' % cs.id))
873 ui.write(('Date: %s\n' % util.datestr(cs.date,
903 ui.write(('Date: %s\n' % util.datestr(cs.date,
874 '%Y/%m/%d %H:%M:%S %1%2')))
904 '%Y/%m/%d %H:%M:%S %1%2')))
875 ui.write(('Author: %s\n' % cs.author))
905 ui.write(('Author: %s\n' % cs.author))
876 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
906 ui.write(('Branch: %s\n' % (cs.branch or 'HEAD')))
877 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
907 ui.write(('Tag%s: %s \n' % (['', 's'][len(cs.tags) > 1],
878 ','.join(cs.tags) or '(none)')))
908 ','.join(cs.tags) or '(none)')))
879 if cs.branchpoints:
909 if cs.branchpoints:
880 ui.write(('Branchpoints: %s \n') %
910 ui.write(('Branchpoints: %s \n') %
881 ', '.join(sorted(cs.branchpoints)))
911 ', '.join(sorted(cs.branchpoints)))
882 if opts["parents"] and cs.parents:
912 if opts["parents"] and cs.parents:
883 if len(cs.parents) > 1:
913 if len(cs.parents) > 1:
884 ui.write(('Parents: %s\n' %
914 ui.write(('Parents: %s\n' %
885 (','.join([str(p.id) for p in cs.parents]))))
915 (','.join([str(p.id) for p in cs.parents]))))
886 else:
916 else:
887 ui.write(('Parent: %d\n' % cs.parents[0].id))
917 ui.write(('Parent: %d\n' % cs.parents[0].id))
888
918
889 if opts["ancestors"]:
919 if opts["ancestors"]:
890 b = cs.branch
920 b = cs.branch
891 r = []
921 r = []
892 while b:
922 while b:
893 b, c = ancestors[b]
923 b, c = ancestors[b]
894 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
924 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
895 if r:
925 if r:
896 ui.write(('Ancestors: %s\n' % (','.join(r))))
926 ui.write(('Ancestors: %s\n' % (','.join(r))))
897
927
898 ui.write(('Log:\n'))
928 ui.write(('Log:\n'))
899 ui.write('%s\n\n' % cs.comment)
929 ui.write('%s\n\n' % cs.comment)
900 ui.write(('Members: \n'))
930 ui.write(('Members: \n'))
901 for f in cs.entries:
931 for f in cs.entries:
902 fn = f.file
932 fn = f.file
903 if fn.startswith(opts["prefix"]):
933 if fn.startswith(opts["prefix"]):
904 fn = fn[len(opts["prefix"]):]
934 fn = fn[len(opts["prefix"]):]
905 ui.write('\t%s:%s->%s%s \n' % (
935 ui.write('\t%s:%s->%s%s \n' % (
906 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
936 fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
907 '.'.join([str(x) for x in f.revision]),
937 '.'.join([str(x) for x in f.revision]),
908 ['', '(DEAD)'][f.dead]))
938 ['', '(DEAD)'][f.dead]))
909 ui.write('\n')
939 ui.write('\n')
910
940
911 # have we seen the start tag?
941 # have we seen the start tag?
912 if revisions and off:
942 if revisions and off:
913 if revisions[0] == str(cs.id) or \
943 if revisions[0] == str(cs.id) or \
914 revisions[0] in cs.tags:
944 revisions[0] in cs.tags:
915 off = False
945 off = False
916
946
917 # see if we reached the end tag
947 # see if we reached the end tag
918 if len(revisions) > 1 and not off:
948 if len(revisions) > 1 and not off:
919 if revisions[1] == str(cs.id) or \
949 if revisions[1] == str(cs.id) or \
920 revisions[1] in cs.tags:
950 revisions[1] in cs.tags:
921 break
951 break
@@ -1,500 +1,654 b''
1 #require cvs
1 #require cvs
2
2
3 $ cvscall()
3 $ cvscall()
4 > {
4 > {
5 > cvs -f "$@"
5 > cvs -f "$@"
6 > }
6 > }
7 $ hgcat()
7 $ hgcat()
8 > {
8 > {
9 > hg --cwd src-hg cat -r tip "$1"
9 > hg --cwd src-hg cat -r tip "$1"
10 > }
10 > }
11 $ echo "[extensions]" >> $HGRCPATH
11 $ echo "[extensions]" >> $HGRCPATH
12 $ echo "convert = " >> $HGRCPATH
12 $ echo "convert = " >> $HGRCPATH
13 $ cat > cvshooks.py <<EOF
13 $ cat > cvshooks.py <<EOF
14 > def cvslog(ui,repo,hooktype,log):
14 > def cvslog(ui,repo,hooktype,log):
15 > print "%s hook: %d entries"%(hooktype,len(log))
15 > print "%s hook: %d entries"%(hooktype,len(log))
16 >
16 >
17 > def cvschangesets(ui,repo,hooktype,changesets):
17 > def cvschangesets(ui,repo,hooktype,changesets):
18 > print "%s hook: %d changesets"%(hooktype,len(changesets))
18 > print "%s hook: %d changesets"%(hooktype,len(changesets))
19 > EOF
19 > EOF
20 $ hookpath=`pwd`
20 $ hookpath=`pwd`
21 $ cat <<EOF >> $HGRCPATH
21 $ cat <<EOF >> $HGRCPATH
22 > [hooks]
22 > [hooks]
23 > cvslog = python:$hookpath/cvshooks.py:cvslog
23 > cvslog = python:$hookpath/cvshooks.py:cvslog
24 > cvschangesets = python:$hookpath/cvshooks.py:cvschangesets
24 > cvschangesets = python:$hookpath/cvshooks.py:cvschangesets
25 > EOF
25 > EOF
26
26
27 create cvs repository
27 create cvs repository
28
28
29 $ mkdir cvsrepo
29 $ mkdir cvsrepo
30 $ cd cvsrepo
30 $ cd cvsrepo
31 $ CVSROOT=`pwd`
31 $ CVSROOT=`pwd`
32 $ export CVSROOT
32 $ export CVSROOT
33 $ CVS_OPTIONS=-f
33 $ CVS_OPTIONS=-f
34 $ export CVS_OPTIONS
34 $ export CVS_OPTIONS
35 $ cd ..
35 $ cd ..
36 $ rmdir cvsrepo
36 $ rmdir cvsrepo
37 $ cvscall -q -d "$CVSROOT" init
37 $ cvscall -q -d "$CVSROOT" init
38
38
39 create source directory
39 create source directory
40
40
41 $ mkdir src-temp
41 $ mkdir src-temp
42 $ cd src-temp
42 $ cd src-temp
43 $ echo a > a
43 $ echo a > a
44 $ mkdir b
44 $ mkdir b
45 $ cd b
45 $ cd b
46 $ echo c > c
46 $ echo c > c
47 $ cd ..
47 $ cd ..
48
48
49 import source directory
49 import source directory
50
50
51 $ cvscall -q import -m import src INITIAL start
51 $ cvscall -q import -m import src INITIAL start
52 N src/a
52 N src/a
53 N src/b/c
53 N src/b/c
54
54
55 No conflicts created by this import
55 No conflicts created by this import
56
56
57 $ cd ..
57 $ cd ..
58
58
59 checkout source directory
59 checkout source directory
60
60
61 $ cvscall -q checkout src
61 $ cvscall -q checkout src
62 U src/a
62 U src/a
63 U src/b/c
63 U src/b/c
64
64
65 commit a new revision changing b/c
65 commit a new revision changing b/c
66
66
67 $ cd src
67 $ cd src
68 $ sleep 1
68 $ sleep 1
69 $ echo c >> b/c
69 $ echo c >> b/c
70 $ cvscall -q commit -mci0 . | grep '<--'
70 $ cvscall -q commit -mci0 . | grep '<--'
71 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
71 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
72 $ cd ..
72 $ cd ..
73
73
74 convert fresh repo and also check localtimezone option
74 convert fresh repo and also check localtimezone option
75
75
76 NOTE: This doesn't check all time zones -- it merely determines that
76 NOTE: This doesn't check all time zones -- it merely determines that
77 the configuration option is taking effect.
77 the configuration option is taking effect.
78
78
79 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
79 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
80 since it does not use DST (unlike other U.S. time zones) and is always
80 since it does not use DST (unlike other U.S. time zones) and is always
81 a fixed difference from UTC.
81 a fixed difference from UTC.
82
82
83 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
83 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
84 initializing destination src-hg repository
84 initializing destination src-hg repository
85 connecting to $TESTTMP/cvsrepo
85 connecting to $TESTTMP/cvsrepo
86 scanning source...
86 scanning source...
87 collecting CVS rlog
87 collecting CVS rlog
88 5 log entries
88 5 log entries
89 cvslog hook: 5 entries
89 cvslog hook: 5 entries
90 creating changesets
90 creating changesets
91 3 changeset entries
91 3 changeset entries
92 cvschangesets hook: 3 changesets
92 cvschangesets hook: 3 changesets
93 sorting...
93 sorting...
94 converting...
94 converting...
95 2 Initial revision
95 2 Initial revision
96 1 ci0
96 1 ci0
97 0 import
97 0 import
98 updating tags
98 updating tags
99 $ hgcat a
99 $ hgcat a
100 a
100 a
101 $ hgcat b/c
101 $ hgcat b/c
102 c
102 c
103 c
103 c
104
104
105 convert fresh repo with --filemap
105 convert fresh repo with --filemap
106
106
107 $ echo include b/c > filemap
107 $ echo include b/c > filemap
108 $ hg convert --filemap filemap src src-filemap
108 $ hg convert --filemap filemap src src-filemap
109 initializing destination src-filemap repository
109 initializing destination src-filemap repository
110 connecting to $TESTTMP/cvsrepo
110 connecting to $TESTTMP/cvsrepo
111 scanning source...
111 scanning source...
112 collecting CVS rlog
112 collecting CVS rlog
113 5 log entries
113 5 log entries
114 cvslog hook: 5 entries
114 cvslog hook: 5 entries
115 creating changesets
115 creating changesets
116 3 changeset entries
116 3 changeset entries
117 cvschangesets hook: 3 changesets
117 cvschangesets hook: 3 changesets
118 sorting...
118 sorting...
119 converting...
119 converting...
120 2 Initial revision
120 2 Initial revision
121 1 ci0
121 1 ci0
122 0 import
122 0 import
123 filtering out empty revision
123 filtering out empty revision
124 repository tip rolled back to revision 1 (undo convert)
124 repository tip rolled back to revision 1 (undo convert)
125 updating tags
125 updating tags
126 $ hgcat b/c
126 $ hgcat b/c
127 c
127 c
128 c
128 c
129 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
129 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
130 2 update tags files: .hgtags
130 2 update tags files: .hgtags
131 1 ci0 files: b/c
131 1 ci0 files: b/c
132 0 Initial revision files: b/c
132 0 Initial revision files: b/c
133
133
134 convert full repository (issue1649)
134 convert full repository (issue1649)
135
135
136 $ cvscall -q -d "$CVSROOT" checkout -d srcfull "." | grep -v CVSROOT
136 $ cvscall -q -d "$CVSROOT" checkout -d srcfull "." | grep -v CVSROOT
137 U srcfull/src/a
137 U srcfull/src/a
138 U srcfull/src/b/c
138 U srcfull/src/b/c
139 $ ls srcfull
139 $ ls srcfull
140 CVS
140 CVS
141 CVSROOT
141 CVSROOT
142 src
142 src
143 $ hg convert srcfull srcfull-hg \
143 $ hg convert srcfull srcfull-hg \
144 > | grep -v 'log entries' | grep -v 'hook:' \
144 > | grep -v 'log entries' | grep -v 'hook:' \
145 > | grep -v '^[0-3] .*' # filter instable changeset order
145 > | grep -v '^[0-3] .*' # filter instable changeset order
146 initializing destination srcfull-hg repository
146 initializing destination srcfull-hg repository
147 connecting to $TESTTMP/cvsrepo
147 connecting to $TESTTMP/cvsrepo
148 scanning source...
148 scanning source...
149 collecting CVS rlog
149 collecting CVS rlog
150 creating changesets
150 creating changesets
151 4 changeset entries
151 4 changeset entries
152 sorting...
152 sorting...
153 converting...
153 converting...
154 updating tags
154 updating tags
155 $ hg cat -r tip --cwd srcfull-hg src/a
155 $ hg cat -r tip --cwd srcfull-hg src/a
156 a
156 a
157 $ hg cat -r tip --cwd srcfull-hg src/b/c
157 $ hg cat -r tip --cwd srcfull-hg src/b/c
158 c
158 c
159 c
159 c
160
160
161 commit new file revisions
161 commit new file revisions
162
162
163 $ cd src
163 $ cd src
164 $ echo a >> a
164 $ echo a >> a
165 $ echo c >> b/c
165 $ echo c >> b/c
166 $ cvscall -q commit -mci1 . | grep '<--'
166 $ cvscall -q commit -mci1 . | grep '<--'
167 $TESTTMP/cvsrepo/src/a,v <-- a
167 $TESTTMP/cvsrepo/src/a,v <-- a
168 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
168 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
169 $ cd ..
169 $ cd ..
170
170
171 convert again
171 convert again
172
172
173 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
173 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
174 connecting to $TESTTMP/cvsrepo
174 connecting to $TESTTMP/cvsrepo
175 scanning source...
175 scanning source...
176 collecting CVS rlog
176 collecting CVS rlog
177 7 log entries
177 7 log entries
178 cvslog hook: 7 entries
178 cvslog hook: 7 entries
179 creating changesets
179 creating changesets
180 4 changeset entries
180 4 changeset entries
181 cvschangesets hook: 4 changesets
181 cvschangesets hook: 4 changesets
182 sorting...
182 sorting...
183 converting...
183 converting...
184 0 ci1
184 0 ci1
185 $ hgcat a
185 $ hgcat a
186 a
186 a
187 a
187 a
188 $ hgcat b/c
188 $ hgcat b/c
189 c
189 c
190 c
190 c
191 c
191 c
192
192
193 convert again with --filemap
193 convert again with --filemap
194
194
195 $ hg convert --filemap filemap src src-filemap
195 $ hg convert --filemap filemap src src-filemap
196 connecting to $TESTTMP/cvsrepo
196 connecting to $TESTTMP/cvsrepo
197 scanning source...
197 scanning source...
198 collecting CVS rlog
198 collecting CVS rlog
199 7 log entries
199 7 log entries
200 cvslog hook: 7 entries
200 cvslog hook: 7 entries
201 creating changesets
201 creating changesets
202 4 changeset entries
202 4 changeset entries
203 cvschangesets hook: 4 changesets
203 cvschangesets hook: 4 changesets
204 sorting...
204 sorting...
205 converting...
205 converting...
206 0 ci1
206 0 ci1
207 $ hgcat b/c
207 $ hgcat b/c
208 c
208 c
209 c
209 c
210 c
210 c
211 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
211 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
212 3 ci1 files: b/c
212 3 ci1 files: b/c
213 2 update tags files: .hgtags
213 2 update tags files: .hgtags
214 1 ci0 files: b/c
214 1 ci0 files: b/c
215 0 Initial revision files: b/c
215 0 Initial revision files: b/c
216
216
217 commit branch
217 commit branch
218
218
219 $ cd src
219 $ cd src
220 $ cvs -q update -r1.1 b/c
220 $ cvs -q update -r1.1 b/c
221 U b/c
221 U b/c
222 $ cvs -q tag -b branch
222 $ cvs -q tag -b branch
223 T a
223 T a
224 T b/c
224 T b/c
225 $ cvs -q update -r branch > /dev/null
225 $ cvs -q update -r branch > /dev/null
226 $ sleep 1
226 $ sleep 1
227 $ echo d >> b/c
227 $ echo d >> b/c
228 $ cvs -q commit -mci2 . | grep '<--'
228 $ cvs -q commit -mci2 . | grep '<--'
229 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
229 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
230 $ cd ..
230 $ cd ..
231
231
232 convert again
232 convert again
233
233
234 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
234 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
235 connecting to $TESTTMP/cvsrepo
235 connecting to $TESTTMP/cvsrepo
236 scanning source...
236 scanning source...
237 collecting CVS rlog
237 collecting CVS rlog
238 8 log entries
238 8 log entries
239 cvslog hook: 8 entries
239 cvslog hook: 8 entries
240 creating changesets
240 creating changesets
241 5 changeset entries
241 5 changeset entries
242 cvschangesets hook: 5 changesets
242 cvschangesets hook: 5 changesets
243 sorting...
243 sorting...
244 converting...
244 converting...
245 0 ci2
245 0 ci2
246 $ hgcat b/c
246 $ hgcat b/c
247 c
247 c
248 d
248 d
249
249
250 convert again with --filemap
250 convert again with --filemap
251
251
252 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True --filemap filemap src src-filemap
252 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True --filemap filemap src src-filemap
253 connecting to $TESTTMP/cvsrepo
253 connecting to $TESTTMP/cvsrepo
254 scanning source...
254 scanning source...
255 collecting CVS rlog
255 collecting CVS rlog
256 8 log entries
256 8 log entries
257 cvslog hook: 8 entries
257 cvslog hook: 8 entries
258 creating changesets
258 creating changesets
259 5 changeset entries
259 5 changeset entries
260 cvschangesets hook: 5 changesets
260 cvschangesets hook: 5 changesets
261 sorting...
261 sorting...
262 converting...
262 converting...
263 0 ci2
263 0 ci2
264 $ hgcat b/c
264 $ hgcat b/c
265 c
265 c
266 d
266 d
267 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
267 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
268 4 ci2 files: b/c
268 4 ci2 files: b/c
269 3 ci1 files: b/c
269 3 ci1 files: b/c
270 2 update tags files: .hgtags
270 2 update tags files: .hgtags
271 1 ci0 files: b/c
271 1 ci0 files: b/c
272 0 Initial revision files: b/c
272 0 Initial revision files: b/c
273
273
274 commit a new revision with funny log message
274 commit a new revision with funny log message
275
275
276 $ cd src
276 $ cd src
277 $ sleep 1
277 $ sleep 1
278 $ echo e >> a
278 $ echo e >> a
279 $ cvscall -q commit -m'funny
279 $ cvscall -q commit -m'funny
280 > ----------------------------
280 > ----------------------------
281 > log message' . | grep '<--' |\
281 > log message' . | grep '<--' |\
282 > sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
282 > sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
283 checking in src/a,v
283 checking in src/a,v
284
284
285 commit new file revisions with some fuzz
285 commit new file revisions with some fuzz
286
286
287 $ sleep 1
287 $ sleep 1
288 $ echo f >> a
288 $ echo f >> a
289 $ cvscall -q commit -mfuzzy . | grep '<--'
289 $ cvscall -q commit -mfuzzy . | grep '<--'
290 $TESTTMP/cvsrepo/src/a,v <-- a
290 $TESTTMP/cvsrepo/src/a,v <-- a
291 $ sleep 4 # the two changes will be split if fuzz < 4
291 $ sleep 4 # the two changes will be split if fuzz < 4
292 $ echo g >> b/c
292 $ echo g >> b/c
293 $ cvscall -q commit -mfuzzy . | grep '<--'
293 $ cvscall -q commit -mfuzzy . | grep '<--'
294 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
294 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
295 $ cd ..
295 $ cd ..
296
296
297 convert again
297 convert again
298
298
299 $ TZ=US/Hawaii hg convert --config convert.cvsps.fuzz=2 --config convert.localtimezone=True src src-hg
299 $ TZ=US/Hawaii hg convert --config convert.cvsps.fuzz=2 --config convert.localtimezone=True src src-hg
300 connecting to $TESTTMP/cvsrepo
300 connecting to $TESTTMP/cvsrepo
301 scanning source...
301 scanning source...
302 collecting CVS rlog
302 collecting CVS rlog
303 11 log entries
303 11 log entries
304 cvslog hook: 11 entries
304 cvslog hook: 11 entries
305 creating changesets
305 creating changesets
306 8 changeset entries
306 8 changeset entries
307 cvschangesets hook: 8 changesets
307 cvschangesets hook: 8 changesets
308 sorting...
308 sorting...
309 converting...
309 converting...
310 2 funny
310 2 funny
311 1 fuzzy
311 1 fuzzy
312 0 fuzzy
312 0 fuzzy
313 $ hg -R src-hg log -G --template '{rev} ({branches}) {desc} date: {date|date} files: {files}\n'
313 $ hg -R src-hg log -G --template '{rev} ({branches}) {desc} date: {date|date} files: {files}\n'
314 o 8 (branch) fuzzy date: * -1000 files: b/c (glob)
314 o 8 (branch) fuzzy date: * -1000 files: b/c (glob)
315 |
315 |
316 o 7 (branch) fuzzy date: * -1000 files: a (glob)
316 o 7 (branch) fuzzy date: * -1000 files: a (glob)
317 |
317 |
318 o 6 (branch) funny
318 o 6 (branch) funny
319 | ----------------------------
319 | ----------------------------
320 | log message date: * -1000 files: a (glob)
320 | log message date: * -1000 files: a (glob)
321 o 5 (branch) ci2 date: * -1000 files: b/c (glob)
321 o 5 (branch) ci2 date: * -1000 files: b/c (glob)
322
322
323 o 4 () ci1 date: * -1000 files: a b/c (glob)
323 o 4 () ci1 date: * -1000 files: a b/c (glob)
324 |
324 |
325 o 3 () update tags date: * +0000 files: .hgtags (glob)
325 o 3 () update tags date: * +0000 files: .hgtags (glob)
326 |
326 |
327 | o 2 (INITIAL) import date: * -1000 files: (glob)
327 | o 2 (INITIAL) import date: * -1000 files: (glob)
328 | |
328 | |
329 o | 1 () ci0 date: * -1000 files: b/c (glob)
329 o | 1 () ci0 date: * -1000 files: b/c (glob)
330 |/
330 |/
331 o 0 () Initial revision date: * -1000 files: a b/c (glob)
331 o 0 () Initial revision date: * -1000 files: a b/c (glob)
332
332
333
333
334 testing debugcvsps
334 testing debugcvsps
335
335
336 $ cd src
336 $ cd src
337 $ hg debugcvsps --fuzz=2 -x >/dev/null
337 $ hg debugcvsps --fuzz=2 -x >/dev/null
338
338
339 commit a new revision changing a and removing b/c
339 commit a new revision changing a and removing b/c
340
340
341 $ cvscall -q update -A
341 $ cvscall -q update -A
342 U a
342 U a
343 U b/c
343 U b/c
344 $ sleep 1
344 $ sleep 1
345 $ echo h >> a
345 $ echo h >> a
346 $ cvscall -Q remove -f b/c
346 $ cvscall -Q remove -f b/c
347 $ cvscall -q commit -mci | grep '<--'
347 $ cvscall -q commit -mci | grep '<--'
348 $TESTTMP/cvsrepo/src/a,v <-- a
348 $TESTTMP/cvsrepo/src/a,v <-- a
349 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
349 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
350
350
351 update and verify the cvsps cache
351 update and verify the cvsps cache
352
352
353 $ hg debugcvsps --fuzz=2 -u
353 $ hg debugcvsps --fuzz=2 -u
354 collecting CVS rlog
354 collecting CVS rlog
355 13 log entries
355 13 log entries
356 cvslog hook: 13 entries
356 cvslog hook: 13 entries
357 creating changesets
357 creating changesets
358 11 changeset entries
358 11 changeset entries
359 cvschangesets hook: 11 changesets
359 cvschangesets hook: 11 changesets
360 ---------------------
360 ---------------------
361 PatchSet 1
361 PatchSet 1
362 Date: * (glob)
362 Date: * (glob)
363 Author: * (glob)
363 Author: * (glob)
364 Branch: HEAD
364 Branch: HEAD
365 Tag: (none)
365 Tag: (none)
366 Branchpoints: INITIAL
366 Branchpoints: INITIAL
367 Log:
367 Log:
368 Initial revision
368 Initial revision
369
369
370 Members:
370 Members:
371 a:INITIAL->1.1
371 a:INITIAL->1.1
372
372
373 ---------------------
373 ---------------------
374 PatchSet 2
374 PatchSet 2
375 Date: * (glob)
375 Date: * (glob)
376 Author: * (glob)
376 Author: * (glob)
377 Branch: HEAD
377 Branch: HEAD
378 Tag: (none)
378 Tag: (none)
379 Branchpoints: INITIAL, branch
379 Branchpoints: INITIAL, branch
380 Log:
380 Log:
381 Initial revision
381 Initial revision
382
382
383 Members:
383 Members:
384 b/c:INITIAL->1.1
384 b/c:INITIAL->1.1
385
385
386 ---------------------
386 ---------------------
387 PatchSet 3
387 PatchSet 3
388 Date: * (glob)
388 Date: * (glob)
389 Author: * (glob)
389 Author: * (glob)
390 Branch: INITIAL
390 Branch: INITIAL
391 Tag: start
391 Tag: start
392 Log:
392 Log:
393 import
393 import
394
394
395 Members:
395 Members:
396 a:1.1->1.1.1.1
396 a:1.1->1.1.1.1
397 b/c:1.1->1.1.1.1
397 b/c:1.1->1.1.1.1
398
398
399 ---------------------
399 ---------------------
400 PatchSet 4
400 PatchSet 4
401 Date: * (glob)
401 Date: * (glob)
402 Author: * (glob)
402 Author: * (glob)
403 Branch: HEAD
403 Branch: HEAD
404 Tag: (none)
404 Tag: (none)
405 Log:
405 Log:
406 ci0
406 ci0
407
407
408 Members:
408 Members:
409 b/c:1.1->1.2
409 b/c:1.1->1.2
410
410
411 ---------------------
411 ---------------------
412 PatchSet 5
412 PatchSet 5
413 Date: * (glob)
413 Date: * (glob)
414 Author: * (glob)
414 Author: * (glob)
415 Branch: HEAD
415 Branch: HEAD
416 Tag: (none)
416 Tag: (none)
417 Branchpoints: branch
417 Branchpoints: branch
418 Log:
418 Log:
419 ci1
419 ci1
420
420
421 Members:
421 Members:
422 a:1.1->1.2
422 a:1.1->1.2
423
423
424 ---------------------
424 ---------------------
425 PatchSet 6
425 PatchSet 6
426 Date: * (glob)
426 Date: * (glob)
427 Author: * (glob)
427 Author: * (glob)
428 Branch: HEAD
428 Branch: HEAD
429 Tag: (none)
429 Tag: (none)
430 Log:
430 Log:
431 ci1
431 ci1
432
432
433 Members:
433 Members:
434 b/c:1.2->1.3
434 b/c:1.2->1.3
435
435
436 ---------------------
436 ---------------------
437 PatchSet 7
437 PatchSet 7
438 Date: * (glob)
438 Date: * (glob)
439 Author: * (glob)
439 Author: * (glob)
440 Branch: branch
440 Branch: branch
441 Tag: (none)
441 Tag: (none)
442 Log:
442 Log:
443 ci2
443 ci2
444
444
445 Members:
445 Members:
446 b/c:1.1->1.1.2.1
446 b/c:1.1->1.1.2.1
447
447
448 ---------------------
448 ---------------------
449 PatchSet 8
449 PatchSet 8
450 Date: * (glob)
450 Date: * (glob)
451 Author: * (glob)
451 Author: * (glob)
452 Branch: branch
452 Branch: branch
453 Tag: (none)
453 Tag: (none)
454 Log:
454 Log:
455 funny
455 funny
456 ----------------------------
456 ----------------------------
457 log message
457 log message
458
458
459 Members:
459 Members:
460 a:1.2->1.2.2.1
460 a:1.2->1.2.2.1
461
461
462 ---------------------
462 ---------------------
463 PatchSet 9
463 PatchSet 9
464 Date: * (glob)
464 Date: * (glob)
465 Author: * (glob)
465 Author: * (glob)
466 Branch: branch
466 Branch: branch
467 Tag: (none)
467 Tag: (none)
468 Log:
468 Log:
469 fuzzy
469 fuzzy
470
470
471 Members:
471 Members:
472 a:1.2.2.1->1.2.2.2
472 a:1.2.2.1->1.2.2.2
473
473
474 ---------------------
474 ---------------------
475 PatchSet 10
475 PatchSet 10
476 Date: * (glob)
476 Date: * (glob)
477 Author: * (glob)
477 Author: * (glob)
478 Branch: branch
478 Branch: branch
479 Tag: (none)
479 Tag: (none)
480 Log:
480 Log:
481 fuzzy
481 fuzzy
482
482
483 Members:
483 Members:
484 b/c:1.1.2.1->1.1.2.2
484 b/c:1.1.2.1->1.1.2.2
485
485
486 ---------------------
486 ---------------------
487 PatchSet 11
487 PatchSet 11
488 Date: * (glob)
488 Date: * (glob)
489 Author: * (glob)
489 Author: * (glob)
490 Branch: HEAD
490 Branch: HEAD
491 Tag: (none)
491 Tag: (none)
492 Log:
492 Log:
493 ci
493 ci
494
494
495 Members:
495 Members:
496 a:1.2->1.3
496 a:1.2->1.3
497 b/c:1.3->1.4(DEAD)
497 b/c:1.3->1.4(DEAD)
498
498
499
499
500 $ cd ..
500 $ cd ..
501
502 Test transcoding CVS log messages (issue5597)
503 =============================================
504
505 To emulate commit messages in (non-ascii) multiple encodings portably,
506 this test scenario writes CVS history file (*,v file) directly via
507 python code.
508
509 Commit messages of version 1.2 - 1.4 use u3042 in 3 encodings below.
510
511 |encoding |byte sequence | decodable as: |
512 | | | utf-8 euc-jp cp932 |
513 +----------+--------------+--------------------+
514 |utf-8 |\xe3\x81\x82 | o x x |
515 |euc-jp |\xa4\xa2 | x o o |
516 |cp932 |\x82\xa0 | x x o |
517
518 $ mkdir -p cvsrepo/transcoding
519 $ python <<EOF
520 > fp = open('cvsrepo/transcoding/file,v', 'w')
521 > fp.write(('''
522 > head 1.4;
523 > access;
524 > symbols
525 > start:1.1.1.1 INITIAL:1.1.1;
526 > locks; strict;
527 > comment @# @;
528 >
529 >
530 > 1.4
531 > date 2017.07.10.00.00.04; author nobody; state Exp;
532 > branches;
533 > next 1.3;
534 > commitid 10059635D016A510FFA;
535 >
536 > 1.3
537 > date 2017.07.10.00.00.03; author nobody; state Exp;
538 > branches;
539 > next 1.2;
540 > commitid 10059635CFF6A4FF34E;
541 >
542 > 1.2
543 > date 2017.07.10.00.00.02; author nobody; state Exp;
544 > branches;
545 > next 1.1;
546 > commitid 10059635CFD6A4D5095;
547 >
548 > 1.1
549 > date 2017.07.10.00.00.01; author nobody; state Exp;
550 > branches
551 > 1.1.1.1;
552 > next ;
553 > commitid 10059635CFB6A4A3C33;
554 >
555 > 1.1.1.1
556 > date 2017.07.10.00.00.01; author nobody; state Exp;
557 > branches;
558 > next ;
559 > commitid 10059635CFB6A4A3C33;
560 >
561 >
562 > desc
563 > @@
564 >
565 >
566 > 1.4
567 > log
568 > @''' + u'\u3042'.encode('cp932') + ''' (cp932)
569 > @
570 > text
571 > @1
572 > 2
573 > 3
574 > 4
575 > @
576 >
577 >
578 > 1.3
579 > log
580 > @''' + u'\u3042'.encode('euc-jp') + ''' (euc-jp)
581 > @
582 > text
583 > @d4 1
584 > @
585 >
586 >
587 > 1.2
588 > log
589 > @''' + u'\u3042'.encode('utf-8') + ''' (utf-8)
590 > @
591 > text
592 > @d3 1
593 > @
594 >
595 >
596 > 1.1
597 > log
598 > @Initial revision
599 > @
600 > text
601 > @d2 1
602 > @
603 >
604 >
605 > 1.1.1.1
606 > log
607 > @import
608 > @
609 > text
610 > @@
611 > ''').lstrip())
612 > EOF
613
614 $ cvscall -q checkout transcoding
615 U transcoding/file
616
617 Test converting in normal case
618 ------------------------------
619
620 (filtering by grep in order to check only form of debug messages)
621
622 $ hg convert --config convert.cvsps.logencoding=utf-8,euc-jp,cp932 -q --debug transcoding transcoding-hg | grep 'transcoding by'
623 transcoding by utf-8: 1.1 of file
624 transcoding by utf-8: 1.1.1.1 of file
625 transcoding by utf-8: 1.2 of file
626 transcoding by euc-jp: 1.3 of file
627 transcoding by cp932: 1.4 of file
628 $ hg -R transcoding-hg --encoding utf-8 log -T "{rev}: {desc}\n"
629 5: update tags
630 4: import
631 3: \xe3\x81\x82 (cp932) (esc)
632 2: \xe3\x81\x82 (euc-jp) (esc)
633 1: \xe3\x81\x82 (utf-8) (esc)
634 0: Initial revision
635 $ rm -rf transcoding-hg
636
637 Test converting in error cases
638 ------------------------------
639
640 unknown encoding in convert.cvsps.logencoding
641
642 $ hg convert --config convert.cvsps.logencoding=foobar -q transcoding transcoding-hg
643 abort: unknown encoding: foobar
644 (check convert.cvsps.logencoding configuration)
645 [255]
646 $ rm -rf transcoding-hg
647
648 no acceptable encoding in convert.cvsps.logencoding
649
650 $ hg convert --config convert.cvsps.logencoding=utf-8,euc-jp -q transcoding transcoding-hg
651 abort: no encoding can transcode CVS log message for 1.4 of file
652 (check convert.cvsps.logencoding configuration)
653 [255]
654 $ rm -rf transcoding-hg
@@ -1,610 +1,616 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > convert=
3 > convert=
4 > [convert]
4 > [convert]
5 > hg.saverev=False
5 > hg.saverev=False
6 > EOF
6 > EOF
7 $ hg help convert
7 $ hg help convert
8 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
8 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
9
9
10 convert a foreign SCM repository to a Mercurial one.
10 convert a foreign SCM repository to a Mercurial one.
11
11
12 Accepted source formats [identifiers]:
12 Accepted source formats [identifiers]:
13
13
14 - Mercurial [hg]
14 - Mercurial [hg]
15 - CVS [cvs]
15 - CVS [cvs]
16 - Darcs [darcs]
16 - Darcs [darcs]
17 - git [git]
17 - git [git]
18 - Subversion [svn]
18 - Subversion [svn]
19 - Monotone [mtn]
19 - Monotone [mtn]
20 - GNU Arch [gnuarch]
20 - GNU Arch [gnuarch]
21 - Bazaar [bzr]
21 - Bazaar [bzr]
22 - Perforce [p4]
22 - Perforce [p4]
23
23
24 Accepted destination formats [identifiers]:
24 Accepted destination formats [identifiers]:
25
25
26 - Mercurial [hg]
26 - Mercurial [hg]
27 - Subversion [svn] (history on branches is not preserved)
27 - Subversion [svn] (history on branches is not preserved)
28
28
29 If no revision is given, all revisions will be converted. Otherwise,
29 If no revision is given, all revisions will be converted. Otherwise,
30 convert will only import up to the named revision (given in a format
30 convert will only import up to the named revision (given in a format
31 understood by the source).
31 understood by the source).
32
32
33 If no destination directory name is specified, it defaults to the basename
33 If no destination directory name is specified, it defaults to the basename
34 of the source with "-hg" appended. If the destination repository doesn't
34 of the source with "-hg" appended. If the destination repository doesn't
35 exist, it will be created.
35 exist, it will be created.
36
36
37 By default, all sources except Mercurial will use --branchsort. Mercurial
37 By default, all sources except Mercurial will use --branchsort. Mercurial
38 uses --sourcesort to preserve original revision numbers order. Sort modes
38 uses --sourcesort to preserve original revision numbers order. Sort modes
39 have the following effects:
39 have the following effects:
40
40
41 --branchsort convert from parent to child revision when possible, which
41 --branchsort convert from parent to child revision when possible, which
42 means branches are usually converted one after the other.
42 means branches are usually converted one after the other.
43 It generates more compact repositories.
43 It generates more compact repositories.
44 --datesort sort revisions by date. Converted repositories have good-
44 --datesort sort revisions by date. Converted repositories have good-
45 looking changelogs but are often an order of magnitude
45 looking changelogs but are often an order of magnitude
46 larger than the same ones generated by --branchsort.
46 larger than the same ones generated by --branchsort.
47 --sourcesort try to preserve source revisions order, only supported by
47 --sourcesort try to preserve source revisions order, only supported by
48 Mercurial sources.
48 Mercurial sources.
49 --closesort try to move closed revisions as close as possible to parent
49 --closesort try to move closed revisions as close as possible to parent
50 branches, only supported by Mercurial sources.
50 branches, only supported by Mercurial sources.
51
51
52 If "REVMAP" isn't given, it will be put in a default location
52 If "REVMAP" isn't given, it will be put in a default location
53 ("<dest>/.hg/shamap" by default). The "REVMAP" is a simple text file that
53 ("<dest>/.hg/shamap" by default). The "REVMAP" is a simple text file that
54 maps each source commit ID to the destination ID for that revision, like
54 maps each source commit ID to the destination ID for that revision, like
55 so:
55 so:
56
56
57 <source ID> <destination ID>
57 <source ID> <destination ID>
58
58
59 If the file doesn't exist, it's automatically created. It's updated on
59 If the file doesn't exist, it's automatically created. It's updated on
60 each commit copied, so 'hg convert' can be interrupted and can be run
60 each commit copied, so 'hg convert' can be interrupted and can be run
61 repeatedly to copy new commits.
61 repeatedly to copy new commits.
62
62
63 The authormap is a simple text file that maps each source commit author to
63 The authormap is a simple text file that maps each source commit author to
64 a destination commit author. It is handy for source SCMs that use unix
64 a destination commit author. It is handy for source SCMs that use unix
65 logins to identify authors (e.g.: CVS). One line per author mapping and
65 logins to identify authors (e.g.: CVS). One line per author mapping and
66 the line format is:
66 the line format is:
67
67
68 source author = destination author
68 source author = destination author
69
69
70 Empty lines and lines starting with a "#" are ignored.
70 Empty lines and lines starting with a "#" are ignored.
71
71
72 The filemap is a file that allows filtering and remapping of files and
72 The filemap is a file that allows filtering and remapping of files and
73 directories. Each line can contain one of the following directives:
73 directories. Each line can contain one of the following directives:
74
74
75 include path/to/file-or-dir
75 include path/to/file-or-dir
76
76
77 exclude path/to/file-or-dir
77 exclude path/to/file-or-dir
78
78
79 rename path/to/source path/to/destination
79 rename path/to/source path/to/destination
80
80
81 Comment lines start with "#". A specified path matches if it equals the
81 Comment lines start with "#". A specified path matches if it equals the
82 full relative name of a file or one of its parent directories. The
82 full relative name of a file or one of its parent directories. The
83 "include" or "exclude" directive with the longest matching path applies,
83 "include" or "exclude" directive with the longest matching path applies,
84 so line order does not matter.
84 so line order does not matter.
85
85
86 The "include" directive causes a file, or all files under a directory, to
86 The "include" directive causes a file, or all files under a directory, to
87 be included in the destination repository. The default if there are no
87 be included in the destination repository. The default if there are no
88 "include" statements is to include everything. If there are any "include"
88 "include" statements is to include everything. If there are any "include"
89 statements, nothing else is included. The "exclude" directive causes files
89 statements, nothing else is included. The "exclude" directive causes files
90 or directories to be omitted. The "rename" directive renames a file or
90 or directories to be omitted. The "rename" directive renames a file or
91 directory if it is converted. To rename from a subdirectory into the root
91 directory if it is converted. To rename from a subdirectory into the root
92 of the repository, use "." as the path to rename to.
92 of the repository, use "." as the path to rename to.
93
93
94 "--full" will make sure the converted changesets contain exactly the right
94 "--full" will make sure the converted changesets contain exactly the right
95 files with the right content. It will make a full conversion of all files,
95 files with the right content. It will make a full conversion of all files,
96 not just the ones that have changed. Files that already are correct will
96 not just the ones that have changed. Files that already are correct will
97 not be changed. This can be used to apply filemap changes when converting
97 not be changed. This can be used to apply filemap changes when converting
98 incrementally. This is currently only supported for Mercurial and
98 incrementally. This is currently only supported for Mercurial and
99 Subversion.
99 Subversion.
100
100
101 The splicemap is a file that allows insertion of synthetic history,
101 The splicemap is a file that allows insertion of synthetic history,
102 letting you specify the parents of a revision. This is useful if you want
102 letting you specify the parents of a revision. This is useful if you want
103 to e.g. give a Subversion merge two parents, or graft two disconnected
103 to e.g. give a Subversion merge two parents, or graft two disconnected
104 series of history together. Each entry contains a key, followed by a
104 series of history together. Each entry contains a key, followed by a
105 space, followed by one or two comma-separated values:
105 space, followed by one or two comma-separated values:
106
106
107 key parent1, parent2
107 key parent1, parent2
108
108
109 The key is the revision ID in the source revision control system whose
109 The key is the revision ID in the source revision control system whose
110 parents should be modified (same format as a key in .hg/shamap). The
110 parents should be modified (same format as a key in .hg/shamap). The
111 values are the revision IDs (in either the source or destination revision
111 values are the revision IDs (in either the source or destination revision
112 control system) that should be used as the new parents for that node. For
112 control system) that should be used as the new parents for that node. For
113 example, if you have merged "release-1.0" into "trunk", then you should
113 example, if you have merged "release-1.0" into "trunk", then you should
114 specify the revision on "trunk" as the first parent and the one on the
114 specify the revision on "trunk" as the first parent and the one on the
115 "release-1.0" branch as the second.
115 "release-1.0" branch as the second.
116
116
117 The branchmap is a file that allows you to rename a branch when it is
117 The branchmap is a file that allows you to rename a branch when it is
118 being brought in from whatever external repository. When used in
118 being brought in from whatever external repository. When used in
119 conjunction with a splicemap, it allows for a powerful combination to help
119 conjunction with a splicemap, it allows for a powerful combination to help
120 fix even the most badly mismanaged repositories and turn them into nicely
120 fix even the most badly mismanaged repositories and turn them into nicely
121 structured Mercurial repositories. The branchmap contains lines of the
121 structured Mercurial repositories. The branchmap contains lines of the
122 form:
122 form:
123
123
124 original_branch_name new_branch_name
124 original_branch_name new_branch_name
125
125
126 where "original_branch_name" is the name of the branch in the source
126 where "original_branch_name" is the name of the branch in the source
127 repository, and "new_branch_name" is the name of the branch is the
127 repository, and "new_branch_name" is the name of the branch is the
128 destination repository. No whitespace is allowed in the new branch name.
128 destination repository. No whitespace is allowed in the new branch name.
129 This can be used to (for instance) move code in one repository from
129 This can be used to (for instance) move code in one repository from
130 "default" to a named branch.
130 "default" to a named branch.
131
131
132 Mercurial Source
132 Mercurial Source
133 ################
133 ################
134
134
135 The Mercurial source recognizes the following configuration options, which
135 The Mercurial source recognizes the following configuration options, which
136 you can set on the command line with "--config":
136 you can set on the command line with "--config":
137
137
138 convert.hg.ignoreerrors
138 convert.hg.ignoreerrors
139 ignore integrity errors when reading. Use it to fix
139 ignore integrity errors when reading. Use it to fix
140 Mercurial repositories with missing revlogs, by converting
140 Mercurial repositories with missing revlogs, by converting
141 from and to Mercurial. Default is False.
141 from and to Mercurial. Default is False.
142 convert.hg.saverev
142 convert.hg.saverev
143 store original revision ID in changeset (forces target IDs
143 store original revision ID in changeset (forces target IDs
144 to change). It takes a boolean argument and defaults to
144 to change). It takes a boolean argument and defaults to
145 False.
145 False.
146 convert.hg.startrev
146 convert.hg.startrev
147 specify the initial Mercurial revision. The default is 0.
147 specify the initial Mercurial revision. The default is 0.
148 convert.hg.revs
148 convert.hg.revs
149 revset specifying the source revisions to convert.
149 revset specifying the source revisions to convert.
150
150
151 CVS Source
151 CVS Source
152 ##########
152 ##########
153
153
154 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
154 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
155 indicate the starting point of what will be converted. Direct access to
155 indicate the starting point of what will be converted. Direct access to
156 the repository files is not needed, unless of course the repository is
156 the repository files is not needed, unless of course the repository is
157 ":local:". The conversion uses the top level directory in the sandbox to
157 ":local:". The conversion uses the top level directory in the sandbox to
158 find the CVS repository, and then uses CVS rlog commands to find files to
158 find the CVS repository, and then uses CVS rlog commands to find files to
159 convert. This means that unless a filemap is given, all files under the
159 convert. This means that unless a filemap is given, all files under the
160 starting directory will be converted, and that any directory
160 starting directory will be converted, and that any directory
161 reorganization in the CVS sandbox is ignored.
161 reorganization in the CVS sandbox is ignored.
162
162
163 The following options can be used with "--config":
163 The following options can be used with "--config":
164
164
165 convert.cvsps.cache
165 convert.cvsps.cache
166 Set to False to disable remote log caching, for testing and
166 Set to False to disable remote log caching, for testing and
167 debugging purposes. Default is True.
167 debugging purposes. Default is True.
168 convert.cvsps.fuzz
168 convert.cvsps.fuzz
169 Specify the maximum time (in seconds) that is allowed
169 Specify the maximum time (in seconds) that is allowed
170 between commits with identical user and log message in a
170 between commits with identical user and log message in a
171 single changeset. When very large files were checked in as
171 single changeset. When very large files were checked in as
172 part of a changeset then the default may not be long enough.
172 part of a changeset then the default may not be long enough.
173 The default is 60.
173 The default is 60.
174 convert.cvsps.logencoding
175 Specify encoding name to be used for transcoding CVS log
176 messages. Multiple encoding names can be specified as a list
177 (see 'hg help config.Syntax'), but only the first acceptable
178 encoding in the list is used per CVS log entries. This
179 transcoding is executed before cvslog hook below.
174 convert.cvsps.mergeto
180 convert.cvsps.mergeto
175 Specify a regular expression to which commit log messages
181 Specify a regular expression to which commit log messages
176 are matched. If a match occurs, then the conversion process
182 are matched. If a match occurs, then the conversion process
177 will insert a dummy revision merging the branch on which
183 will insert a dummy revision merging the branch on which
178 this log message occurs to the branch indicated in the
184 this log message occurs to the branch indicated in the
179 regex. Default is "{{mergetobranch ([-\w]+)}}"
185 regex. Default is "{{mergetobranch ([-\w]+)}}"
180 convert.cvsps.mergefrom
186 convert.cvsps.mergefrom
181 Specify a regular expression to which commit log messages
187 Specify a regular expression to which commit log messages
182 are matched. If a match occurs, then the conversion process
188 are matched. If a match occurs, then the conversion process
183 will add the most recent revision on the branch indicated in
189 will add the most recent revision on the branch indicated in
184 the regex as the second parent of the changeset. Default is
190 the regex as the second parent of the changeset. Default is
185 "{{mergefrombranch ([-\w]+)}}"
191 "{{mergefrombranch ([-\w]+)}}"
186 convert.localtimezone
192 convert.localtimezone
187 use local time (as determined by the TZ environment
193 use local time (as determined by the TZ environment
188 variable) for changeset date/times. The default is False
194 variable) for changeset date/times. The default is False
189 (use UTC).
195 (use UTC).
190 hooks.cvslog Specify a Python function to be called at the end of
196 hooks.cvslog Specify a Python function to be called at the end of
191 gathering the CVS log. The function is passed a list with
197 gathering the CVS log. The function is passed a list with
192 the log entries, and can modify the entries in-place, or add
198 the log entries, and can modify the entries in-place, or add
193 or delete them.
199 or delete them.
194 hooks.cvschangesets
200 hooks.cvschangesets
195 Specify a Python function to be called after the changesets
201 Specify a Python function to be called after the changesets
196 are calculated from the CVS log. The function is passed a
202 are calculated from the CVS log. The function is passed a
197 list with the changeset entries, and can modify the
203 list with the changeset entries, and can modify the
198 changesets in-place, or add or delete them.
204 changesets in-place, or add or delete them.
199
205
200 An additional "debugcvsps" Mercurial command allows the builtin changeset
206 An additional "debugcvsps" Mercurial command allows the builtin changeset
201 merging code to be run without doing a conversion. Its parameters and
207 merging code to be run without doing a conversion. Its parameters and
202 output are similar to that of cvsps 2.1. Please see the command help for
208 output are similar to that of cvsps 2.1. Please see the command help for
203 more details.
209 more details.
204
210
205 Subversion Source
211 Subversion Source
206 #################
212 #################
207
213
208 Subversion source detects classical trunk/branches/tags layouts. By
214 Subversion source detects classical trunk/branches/tags layouts. By
209 default, the supplied "svn://repo/path/" source URL is converted as a
215 default, the supplied "svn://repo/path/" source URL is converted as a
210 single branch. If "svn://repo/path/trunk" exists it replaces the default
216 single branch. If "svn://repo/path/trunk" exists it replaces the default
211 branch. If "svn://repo/path/branches" exists, its subdirectories are
217 branch. If "svn://repo/path/branches" exists, its subdirectories are
212 listed as possible branches. If "svn://repo/path/tags" exists, it is
218 listed as possible branches. If "svn://repo/path/tags" exists, it is
213 looked for tags referencing converted branches. Default "trunk",
219 looked for tags referencing converted branches. Default "trunk",
214 "branches" and "tags" values can be overridden with following options. Set
220 "branches" and "tags" values can be overridden with following options. Set
215 them to paths relative to the source URL, or leave them blank to disable
221 them to paths relative to the source URL, or leave them blank to disable
216 auto detection.
222 auto detection.
217
223
218 The following options can be set with "--config":
224 The following options can be set with "--config":
219
225
220 convert.svn.branches
226 convert.svn.branches
221 specify the directory containing branches. The default is
227 specify the directory containing branches. The default is
222 "branches".
228 "branches".
223 convert.svn.tags
229 convert.svn.tags
224 specify the directory containing tags. The default is
230 specify the directory containing tags. The default is
225 "tags".
231 "tags".
226 convert.svn.trunk
232 convert.svn.trunk
227 specify the name of the trunk branch. The default is
233 specify the name of the trunk branch. The default is
228 "trunk".
234 "trunk".
229 convert.localtimezone
235 convert.localtimezone
230 use local time (as determined by the TZ environment
236 use local time (as determined by the TZ environment
231 variable) for changeset date/times. The default is False
237 variable) for changeset date/times. The default is False
232 (use UTC).
238 (use UTC).
233
239
234 Source history can be retrieved starting at a specific revision, instead
240 Source history can be retrieved starting at a specific revision, instead
235 of being integrally converted. Only single branch conversions are
241 of being integrally converted. Only single branch conversions are
236 supported.
242 supported.
237
243
238 convert.svn.startrev
244 convert.svn.startrev
239 specify start Subversion revision number. The default is 0.
245 specify start Subversion revision number. The default is 0.
240
246
241 Git Source
247 Git Source
242 ##########
248 ##########
243
249
244 The Git importer converts commits from all reachable branches (refs in
250 The Git importer converts commits from all reachable branches (refs in
245 refs/heads) and remotes (refs in refs/remotes) to Mercurial. Branches are
251 refs/heads) and remotes (refs in refs/remotes) to Mercurial. Branches are
246 converted to bookmarks with the same name, with the leading 'refs/heads'
252 converted to bookmarks with the same name, with the leading 'refs/heads'
247 stripped. Git submodules are converted to Git subrepos in Mercurial.
253 stripped. Git submodules are converted to Git subrepos in Mercurial.
248
254
249 The following options can be set with "--config":
255 The following options can be set with "--config":
250
256
251 convert.git.similarity
257 convert.git.similarity
252 specify how similar files modified in a commit must be to be
258 specify how similar files modified in a commit must be to be
253 imported as renames or copies, as a percentage between "0"
259 imported as renames or copies, as a percentage between "0"
254 (disabled) and "100" (files must be identical). For example,
260 (disabled) and "100" (files must be identical). For example,
255 "90" means that a delete/add pair will be imported as a
261 "90" means that a delete/add pair will be imported as a
256 rename if more than 90% of the file hasn't changed. The
262 rename if more than 90% of the file hasn't changed. The
257 default is "50".
263 default is "50".
258 convert.git.findcopiesharder
264 convert.git.findcopiesharder
259 while detecting copies, look at all files in the working
265 while detecting copies, look at all files in the working
260 copy instead of just changed ones. This is very expensive
266 copy instead of just changed ones. This is very expensive
261 for large projects, and is only effective when
267 for large projects, and is only effective when
262 "convert.git.similarity" is greater than 0. The default is
268 "convert.git.similarity" is greater than 0. The default is
263 False.
269 False.
264 convert.git.renamelimit
270 convert.git.renamelimit
265 perform rename and copy detection up to this many changed
271 perform rename and copy detection up to this many changed
266 files in a commit. Increasing this will make rename and copy
272 files in a commit. Increasing this will make rename and copy
267 detection more accurate but will significantly slow down
273 detection more accurate but will significantly slow down
268 computation on large projects. The option is only relevant
274 computation on large projects. The option is only relevant
269 if "convert.git.similarity" is greater than 0. The default
275 if "convert.git.similarity" is greater than 0. The default
270 is "400".
276 is "400".
271 convert.git.committeractions
277 convert.git.committeractions
272 list of actions to take when processing author and committer
278 list of actions to take when processing author and committer
273 values.
279 values.
274
280
275 Git commits have separate author (who wrote the commit) and committer
281 Git commits have separate author (who wrote the commit) and committer
276 (who applied the commit) fields. Not all destinations support separate
282 (who applied the commit) fields. Not all destinations support separate
277 author and committer fields (including Mercurial). This config option
283 author and committer fields (including Mercurial). This config option
278 controls what to do with these author and committer fields during
284 controls what to do with these author and committer fields during
279 conversion.
285 conversion.
280
286
281 A value of "messagedifferent" will append a "committer: ..." line to
287 A value of "messagedifferent" will append a "committer: ..." line to
282 the commit message if the Git committer is different from the author.
288 the commit message if the Git committer is different from the author.
283 The prefix of that line can be specified using the syntax
289 The prefix of that line can be specified using the syntax
284 "messagedifferent=<prefix>". e.g. "messagedifferent=git-committer:".
290 "messagedifferent=<prefix>". e.g. "messagedifferent=git-committer:".
285 When a prefix is specified, a space will always be inserted between
291 When a prefix is specified, a space will always be inserted between
286 the prefix and the value.
292 the prefix and the value.
287
293
288 "messagealways" behaves like "messagedifferent" except it will always
294 "messagealways" behaves like "messagedifferent" except it will always
289 result in a "committer: ..." line being appended to the commit
295 result in a "committer: ..." line being appended to the commit
290 message. This value is mutually exclusive with "messagedifferent".
296 message. This value is mutually exclusive with "messagedifferent".
291
297
292 "dropcommitter" will remove references to the committer. Only
298 "dropcommitter" will remove references to the committer. Only
293 references to the author will remain. Actions that add references to
299 references to the author will remain. Actions that add references to
294 the committer will have no effect when this is set.
300 the committer will have no effect when this is set.
295
301
296 "replaceauthor" will replace the value of the author field with the
302 "replaceauthor" will replace the value of the author field with the
297 committer. Other actions that add references to the committer will
303 committer. Other actions that add references to the committer will
298 still take effect when this is set.
304 still take effect when this is set.
299
305
300 The default is "messagedifferent".
306 The default is "messagedifferent".
301
307
302 convert.git.extrakeys
308 convert.git.extrakeys
303 list of extra keys from commit metadata to copy to the
309 list of extra keys from commit metadata to copy to the
304 destination. Some Git repositories store extra metadata in
310 destination. Some Git repositories store extra metadata in
305 commits. By default, this non-default metadata will be lost
311 commits. By default, this non-default metadata will be lost
306 during conversion. Setting this config option can retain
312 during conversion. Setting this config option can retain
307 that metadata. Some built-in keys such as "parent" and
313 that metadata. Some built-in keys such as "parent" and
308 "branch" are not allowed to be copied.
314 "branch" are not allowed to be copied.
309 convert.git.remoteprefix
315 convert.git.remoteprefix
310 remote refs are converted as bookmarks with
316 remote refs are converted as bookmarks with
311 "convert.git.remoteprefix" as a prefix followed by a /. The
317 "convert.git.remoteprefix" as a prefix followed by a /. The
312 default is 'remote'.
318 default is 'remote'.
313 convert.git.saverev
319 convert.git.saverev
314 whether to store the original Git commit ID in the metadata
320 whether to store the original Git commit ID in the metadata
315 of the destination commit. The default is True.
321 of the destination commit. The default is True.
316 convert.git.skipsubmodules
322 convert.git.skipsubmodules
317 does not convert root level .gitmodules files or files with
323 does not convert root level .gitmodules files or files with
318 160000 mode indicating a submodule. Default is False.
324 160000 mode indicating a submodule. Default is False.
319
325
320 Perforce Source
326 Perforce Source
321 ###############
327 ###############
322
328
323 The Perforce (P4) importer can be given a p4 depot path or a client
329 The Perforce (P4) importer can be given a p4 depot path or a client
324 specification as source. It will convert all files in the source to a flat
330 specification as source. It will convert all files in the source to a flat
325 Mercurial repository, ignoring labels, branches and integrations. Note
331 Mercurial repository, ignoring labels, branches and integrations. Note
326 that when a depot path is given you then usually should specify a target
332 that when a depot path is given you then usually should specify a target
327 directory, because otherwise the target may be named "...-hg".
333 directory, because otherwise the target may be named "...-hg".
328
334
329 The following options can be set with "--config":
335 The following options can be set with "--config":
330
336
331 convert.p4.encoding
337 convert.p4.encoding
332 specify the encoding to use when decoding standard output of
338 specify the encoding to use when decoding standard output of
333 the Perforce command line tool. The default is default
339 the Perforce command line tool. The default is default
334 system encoding.
340 system encoding.
335 convert.p4.startrev
341 convert.p4.startrev
336 specify initial Perforce revision (a Perforce changelist
342 specify initial Perforce revision (a Perforce changelist
337 number).
343 number).
338
344
339 Mercurial Destination
345 Mercurial Destination
340 #####################
346 #####################
341
347
342 The Mercurial destination will recognize Mercurial subrepositories in the
348 The Mercurial destination will recognize Mercurial subrepositories in the
343 destination directory, and update the .hgsubstate file automatically if
349 destination directory, and update the .hgsubstate file automatically if
344 the destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
350 the destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
345 Converting a repository with subrepositories requires converting a single
351 Converting a repository with subrepositories requires converting a single
346 repository at a time, from the bottom up.
352 repository at a time, from the bottom up.
347
353
348 The following options are supported:
354 The following options are supported:
349
355
350 convert.hg.clonebranches
356 convert.hg.clonebranches
351 dispatch source branches in separate clones. The default is
357 dispatch source branches in separate clones. The default is
352 False.
358 False.
353 convert.hg.tagsbranch
359 convert.hg.tagsbranch
354 branch name for tag revisions, defaults to "default".
360 branch name for tag revisions, defaults to "default".
355 convert.hg.usebranchnames
361 convert.hg.usebranchnames
356 preserve branch names. The default is True.
362 preserve branch names. The default is True.
357 convert.hg.sourcename
363 convert.hg.sourcename
358 records the given string as a 'convert_source' extra value
364 records the given string as a 'convert_source' extra value
359 on each commit made in the target repository. The default is
365 on each commit made in the target repository. The default is
360 None.
366 None.
361
367
362 All Destinations
368 All Destinations
363 ################
369 ################
364
370
365 All destination types accept the following options:
371 All destination types accept the following options:
366
372
367 convert.skiptags
373 convert.skiptags
368 does not convert tags from the source repo to the target
374 does not convert tags from the source repo to the target
369 repo. The default is False.
375 repo. The default is False.
370
376
371 options ([+] can be repeated):
377 options ([+] can be repeated):
372
378
373 -s --source-type TYPE source repository type
379 -s --source-type TYPE source repository type
374 -d --dest-type TYPE destination repository type
380 -d --dest-type TYPE destination repository type
375 -r --rev REV [+] import up to source revision REV
381 -r --rev REV [+] import up to source revision REV
376 -A --authormap FILE remap usernames using this file
382 -A --authormap FILE remap usernames using this file
377 --filemap FILE remap file names using contents of file
383 --filemap FILE remap file names using contents of file
378 --full apply filemap changes by converting all files again
384 --full apply filemap changes by converting all files again
379 --splicemap FILE splice synthesized history into place
385 --splicemap FILE splice synthesized history into place
380 --branchmap FILE change branch names while converting
386 --branchmap FILE change branch names while converting
381 --branchsort try to sort changesets by branches
387 --branchsort try to sort changesets by branches
382 --datesort try to sort changesets by date
388 --datesort try to sort changesets by date
383 --sourcesort preserve source changesets order
389 --sourcesort preserve source changesets order
384 --closesort try to reorder closed revisions
390 --closesort try to reorder closed revisions
385
391
386 (some details hidden, use --verbose to show complete help)
392 (some details hidden, use --verbose to show complete help)
387 $ hg init a
393 $ hg init a
388 $ cd a
394 $ cd a
389 $ echo a > a
395 $ echo a > a
390 $ hg ci -d'0 0' -Ama
396 $ hg ci -d'0 0' -Ama
391 adding a
397 adding a
392 $ hg cp a b
398 $ hg cp a b
393 $ hg ci -d'1 0' -mb
399 $ hg ci -d'1 0' -mb
394 $ hg rm a
400 $ hg rm a
395 $ hg ci -d'2 0' -mc
401 $ hg ci -d'2 0' -mc
396 $ hg mv b a
402 $ hg mv b a
397 $ hg ci -d'3 0' -md
403 $ hg ci -d'3 0' -md
398 $ echo a >> a
404 $ echo a >> a
399 $ hg ci -d'4 0' -me
405 $ hg ci -d'4 0' -me
400 $ cd ..
406 $ cd ..
401 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
407 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
402 assuming destination a-hg
408 assuming destination a-hg
403 initializing destination a-hg repository
409 initializing destination a-hg repository
404 scanning source...
410 scanning source...
405 sorting...
411 sorting...
406 converting...
412 converting...
407 4 a
413 4 a
408 3 b
414 3 b
409 2 c
415 2 c
410 1 d
416 1 d
411 0 e
417 0 e
412 $ hg --cwd a-hg pull ../a
418 $ hg --cwd a-hg pull ../a
413 pulling from ../a
419 pulling from ../a
414 searching for changes
420 searching for changes
415 no changes found
421 no changes found
416
422
417 conversion to existing file should fail
423 conversion to existing file should fail
418
424
419 $ touch bogusfile
425 $ touch bogusfile
420 $ hg convert a bogusfile
426 $ hg convert a bogusfile
421 initializing destination bogusfile repository
427 initializing destination bogusfile repository
422 abort: cannot create new bundle repository
428 abort: cannot create new bundle repository
423 [255]
429 [255]
424
430
425 #if unix-permissions no-root
431 #if unix-permissions no-root
426
432
427 conversion to dir without permissions should fail
433 conversion to dir without permissions should fail
428
434
429 $ mkdir bogusdir
435 $ mkdir bogusdir
430 $ chmod 000 bogusdir
436 $ chmod 000 bogusdir
431
437
432 $ hg convert a bogusdir
438 $ hg convert a bogusdir
433 abort: Permission denied: 'bogusdir'
439 abort: Permission denied: 'bogusdir'
434 [255]
440 [255]
435
441
436 user permissions should succeed
442 user permissions should succeed
437
443
438 $ chmod 700 bogusdir
444 $ chmod 700 bogusdir
439 $ hg convert a bogusdir
445 $ hg convert a bogusdir
440 initializing destination bogusdir repository
446 initializing destination bogusdir repository
441 scanning source...
447 scanning source...
442 sorting...
448 sorting...
443 converting...
449 converting...
444 4 a
450 4 a
445 3 b
451 3 b
446 2 c
452 2 c
447 1 d
453 1 d
448 0 e
454 0 e
449
455
450 #endif
456 #endif
451
457
452 test pre and post conversion actions
458 test pre and post conversion actions
453
459
454 $ echo 'include b' > filemap
460 $ echo 'include b' > filemap
455 $ hg convert --debug --filemap filemap a partialb | \
461 $ hg convert --debug --filemap filemap a partialb | \
456 > grep 'run hg'
462 > grep 'run hg'
457 run hg source pre-conversion action
463 run hg source pre-conversion action
458 run hg sink pre-conversion action
464 run hg sink pre-conversion action
459 run hg sink post-conversion action
465 run hg sink post-conversion action
460 run hg source post-conversion action
466 run hg source post-conversion action
461
467
462 converting empty dir should fail "nicely
468 converting empty dir should fail "nicely
463
469
464 $ mkdir emptydir
470 $ mkdir emptydir
465
471
466 override $PATH to ensure p4 not visible; use $PYTHON in case we're
472 override $PATH to ensure p4 not visible; use $PYTHON in case we're
467 running from a devel copy, not a temp installation
473 running from a devel copy, not a temp installation
468
474
469 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
475 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
470 assuming destination emptydir-hg
476 assuming destination emptydir-hg
471 initializing destination emptydir-hg repository
477 initializing destination emptydir-hg repository
472 emptydir does not look like a CVS checkout
478 emptydir does not look like a CVS checkout
473 $TESTTMP/emptydir does not look like a Git repository (glob)
479 $TESTTMP/emptydir does not look like a Git repository (glob)
474 emptydir does not look like a Subversion repository
480 emptydir does not look like a Subversion repository
475 emptydir is not a local Mercurial repository
481 emptydir is not a local Mercurial repository
476 emptydir does not look like a darcs repository
482 emptydir does not look like a darcs repository
477 emptydir does not look like a monotone repository
483 emptydir does not look like a monotone repository
478 emptydir does not look like a GNU Arch repository
484 emptydir does not look like a GNU Arch repository
479 emptydir does not look like a Bazaar repository
485 emptydir does not look like a Bazaar repository
480 cannot find required "p4" tool
486 cannot find required "p4" tool
481 abort: emptydir: missing or unsupported repository
487 abort: emptydir: missing or unsupported repository
482 [255]
488 [255]
483
489
484 convert with imaginary source type
490 convert with imaginary source type
485
491
486 $ hg convert --source-type foo a a-foo
492 $ hg convert --source-type foo a a-foo
487 initializing destination a-foo repository
493 initializing destination a-foo repository
488 abort: foo: invalid source repository type
494 abort: foo: invalid source repository type
489 [255]
495 [255]
490
496
491 convert with imaginary sink type
497 convert with imaginary sink type
492
498
493 $ hg convert --dest-type foo a a-foo
499 $ hg convert --dest-type foo a a-foo
494 abort: foo: invalid destination repository type
500 abort: foo: invalid destination repository type
495 [255]
501 [255]
496
502
497 testing: convert must not produce duplicate entries in fncache
503 testing: convert must not produce duplicate entries in fncache
498
504
499 $ hg convert a b
505 $ hg convert a b
500 initializing destination b repository
506 initializing destination b repository
501 scanning source...
507 scanning source...
502 sorting...
508 sorting...
503 converting...
509 converting...
504 4 a
510 4 a
505 3 b
511 3 b
506 2 c
512 2 c
507 1 d
513 1 d
508 0 e
514 0 e
509
515
510 contents of fncache file:
516 contents of fncache file:
511
517
512 $ cat b/.hg/store/fncache | sort
518 $ cat b/.hg/store/fncache | sort
513 data/a.i
519 data/a.i
514 data/b.i
520 data/b.i
515
521
516 test bogus URL
522 test bogus URL
517
523
518 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
524 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
519 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
525 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
520 [255]
526 [255]
521
527
522 test revset converted() lookup
528 test revset converted() lookup
523
529
524 $ hg --config convert.hg.saverev=True convert a c
530 $ hg --config convert.hg.saverev=True convert a c
525 initializing destination c repository
531 initializing destination c repository
526 scanning source...
532 scanning source...
527 sorting...
533 sorting...
528 converting...
534 converting...
529 4 a
535 4 a
530 3 b
536 3 b
531 2 c
537 2 c
532 1 d
538 1 d
533 0 e
539 0 e
534 $ echo f > c/f
540 $ echo f > c/f
535 $ hg -R c ci -d'0 0' -Amf
541 $ hg -R c ci -d'0 0' -Amf
536 adding f
542 adding f
537 created new head
543 created new head
538 $ hg -R c log -r "converted(09d945a62ce6)"
544 $ hg -R c log -r "converted(09d945a62ce6)"
539 changeset: 1:98c3dd46a874
545 changeset: 1:98c3dd46a874
540 user: test
546 user: test
541 date: Thu Jan 01 00:00:01 1970 +0000
547 date: Thu Jan 01 00:00:01 1970 +0000
542 summary: b
548 summary: b
543
549
544 $ hg -R c log -r "converted()"
550 $ hg -R c log -r "converted()"
545 changeset: 0:31ed57b2037c
551 changeset: 0:31ed57b2037c
546 user: test
552 user: test
547 date: Thu Jan 01 00:00:00 1970 +0000
553 date: Thu Jan 01 00:00:00 1970 +0000
548 summary: a
554 summary: a
549
555
550 changeset: 1:98c3dd46a874
556 changeset: 1:98c3dd46a874
551 user: test
557 user: test
552 date: Thu Jan 01 00:00:01 1970 +0000
558 date: Thu Jan 01 00:00:01 1970 +0000
553 summary: b
559 summary: b
554
560
555 changeset: 2:3b9ca06ef716
561 changeset: 2:3b9ca06ef716
556 user: test
562 user: test
557 date: Thu Jan 01 00:00:02 1970 +0000
563 date: Thu Jan 01 00:00:02 1970 +0000
558 summary: c
564 summary: c
559
565
560 changeset: 3:4e0debd37cf2
566 changeset: 3:4e0debd37cf2
561 user: test
567 user: test
562 date: Thu Jan 01 00:00:03 1970 +0000
568 date: Thu Jan 01 00:00:03 1970 +0000
563 summary: d
569 summary: d
564
570
565 changeset: 4:9de3bc9349c5
571 changeset: 4:9de3bc9349c5
566 user: test
572 user: test
567 date: Thu Jan 01 00:00:04 1970 +0000
573 date: Thu Jan 01 00:00:04 1970 +0000
568 summary: e
574 summary: e
569
575
570
576
571 test specifying a sourcename
577 test specifying a sourcename
572 $ echo g > a/g
578 $ echo g > a/g
573 $ hg -R a ci -d'0 0' -Amg
579 $ hg -R a ci -d'0 0' -Amg
574 adding g
580 adding g
575 $ hg --config convert.hg.sourcename=mysource --config convert.hg.saverev=True convert a c
581 $ hg --config convert.hg.sourcename=mysource --config convert.hg.saverev=True convert a c
576 scanning source...
582 scanning source...
577 sorting...
583 sorting...
578 converting...
584 converting...
579 0 g
585 0 g
580 $ hg -R c log -r tip --template '{extras % "{extra}\n"}'
586 $ hg -R c log -r tip --template '{extras % "{extra}\n"}'
581 branch=default
587 branch=default
582 convert_revision=a3bc6100aa8ec03e00aaf271f1f50046fb432072
588 convert_revision=a3bc6100aa8ec03e00aaf271f1f50046fb432072
583 convert_source=mysource
589 convert_source=mysource
584
590
585 $ cat > branchmap.txt << EOF
591 $ cat > branchmap.txt << EOF
586 > old branch new_branch
592 > old branch new_branch
587 > EOF
593 > EOF
588
594
589 $ hg -R a branch -q 'old branch'
595 $ hg -R a branch -q 'old branch'
590 $ echo gg > a/g
596 $ echo gg > a/g
591 $ hg -R a ci -m 'branch name with spaces'
597 $ hg -R a ci -m 'branch name with spaces'
592 $ hg convert --branchmap branchmap.txt a d
598 $ hg convert --branchmap branchmap.txt a d
593 initializing destination d repository
599 initializing destination d repository
594 scanning source...
600 scanning source...
595 sorting...
601 sorting...
596 converting...
602 converting...
597 6 a
603 6 a
598 5 b
604 5 b
599 4 c
605 4 c
600 3 d
606 3 d
601 2 e
607 2 e
602 1 g
608 1 g
603 0 branch name with spaces
609 0 branch name with spaces
604
610
605 $ hg -R a branches
611 $ hg -R a branches
606 old branch 6:a24a66ade009
612 old branch 6:a24a66ade009
607 default 5:a3bc6100aa8e (inactive)
613 default 5:a3bc6100aa8e (inactive)
608 $ hg -R d branches
614 $ hg -R d branches
609 new_branch 6:64ed208b732b
615 new_branch 6:64ed208b732b
610 default 5:a3bc6100aa8e (inactive)
616 default 5:a3bc6100aa8e (inactive)
General Comments 0
You need to be logged in to leave comments. Login now