##// END OF EJS Templates
convert: add support for specifying multiple revs...
Durham Goode -
r25748:baea47ca default
parent child Browse files
Show More
@@ -1,434 +1,434
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 import convcmd
10 import convcmd
11 import cvsps
11 import cvsps
12 import subversion
12 import subversion
13 from mercurial import cmdutil, templatekw
13 from mercurial import cmdutil, templatekw
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15
15
16 cmdtable = {}
16 cmdtable = {}
17 command = cmdutil.command(cmdtable)
17 command = cmdutil.command(cmdtable)
18 # Note for extension authors: ONLY specify testedwith = 'internal' for
18 # Note for extension authors: ONLY specify testedwith = 'internal' for
19 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
19 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
20 # be specifying the version(s) of Mercurial they are tested with, or
20 # be specifying the version(s) of Mercurial they are tested with, or
21 # leave the attribute unspecified.
21 # leave the attribute unspecified.
22 testedwith = 'internal'
22 testedwith = 'internal'
23
23
24 # Commands definition was moved elsewhere to ease demandload job.
24 # Commands definition was moved elsewhere to ease demandload job.
25
25
26 @command('convert',
26 @command('convert',
27 [('', 'authors', '',
27 [('', 'authors', '',
28 _('username mapping filename (DEPRECATED, use --authormap instead)'),
28 _('username mapping filename (DEPRECATED, use --authormap instead)'),
29 _('FILE')),
29 _('FILE')),
30 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
30 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
31 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
31 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
32 ('r', 'rev', '', _('import up to source revision REV'), _('REV')),
32 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
33 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
33 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
34 ('', 'filemap', '', _('remap file names using contents of file'),
34 ('', 'filemap', '', _('remap file names using contents of file'),
35 _('FILE')),
35 _('FILE')),
36 ('', 'full', None,
36 ('', 'full', None,
37 _('apply filemap changes by converting all files again')),
37 _('apply filemap changes by converting all files again')),
38 ('', 'splicemap', '', _('splice synthesized history into place'),
38 ('', 'splicemap', '', _('splice synthesized history into place'),
39 _('FILE')),
39 _('FILE')),
40 ('', 'branchmap', '', _('change branch names while converting'),
40 ('', 'branchmap', '', _('change branch names while converting'),
41 _('FILE')),
41 _('FILE')),
42 ('', 'branchsort', None, _('try to sort changesets by branches')),
42 ('', 'branchsort', None, _('try to sort changesets by branches')),
43 ('', 'datesort', None, _('try to sort changesets by date')),
43 ('', 'datesort', None, _('try to sort changesets by date')),
44 ('', 'sourcesort', None, _('preserve source changesets order')),
44 ('', 'sourcesort', None, _('preserve source changesets order')),
45 ('', 'closesort', None, _('try to reorder closed revisions'))],
45 ('', 'closesort', None, _('try to reorder closed revisions'))],
46 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
46 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
47 norepo=True)
47 norepo=True)
48 def convert(ui, src, dest=None, revmapfile=None, **opts):
48 def convert(ui, src, dest=None, revmapfile=None, **opts):
49 """convert a foreign SCM repository to a Mercurial one.
49 """convert a foreign SCM repository to a Mercurial one.
50
50
51 Accepted source formats [identifiers]:
51 Accepted source formats [identifiers]:
52
52
53 - Mercurial [hg]
53 - Mercurial [hg]
54 - CVS [cvs]
54 - CVS [cvs]
55 - Darcs [darcs]
55 - Darcs [darcs]
56 - git [git]
56 - git [git]
57 - Subversion [svn]
57 - Subversion [svn]
58 - Monotone [mtn]
58 - Monotone [mtn]
59 - GNU Arch [gnuarch]
59 - GNU Arch [gnuarch]
60 - Bazaar [bzr]
60 - Bazaar [bzr]
61 - Perforce [p4]
61 - Perforce [p4]
62
62
63 Accepted destination formats [identifiers]:
63 Accepted destination formats [identifiers]:
64
64
65 - Mercurial [hg]
65 - Mercurial [hg]
66 - Subversion [svn] (history on branches is not preserved)
66 - Subversion [svn] (history on branches is not preserved)
67
67
68 If no revision is given, all revisions will be converted.
68 If no revision is given, all revisions will be converted.
69 Otherwise, convert will only import up to the named revision
69 Otherwise, convert will only import up to the named revision
70 (given in a format understood by the source).
70 (given in a format understood by the source).
71
71
72 If no destination directory name is specified, it defaults to the
72 If no destination directory name is specified, it defaults to the
73 basename of the source with ``-hg`` appended. If the destination
73 basename of the source with ``-hg`` appended. If the destination
74 repository doesn't exist, it will be created.
74 repository doesn't exist, it will be created.
75
75
76 By default, all sources except Mercurial will use --branchsort.
76 By default, all sources except Mercurial will use --branchsort.
77 Mercurial uses --sourcesort to preserve original revision numbers
77 Mercurial uses --sourcesort to preserve original revision numbers
78 order. Sort modes have the following effects:
78 order. Sort modes have the following effects:
79
79
80 --branchsort convert from parent to child revision when possible,
80 --branchsort convert from parent to child revision when possible,
81 which means branches are usually converted one after
81 which means branches are usually converted one after
82 the other. It generates more compact repositories.
82 the other. It generates more compact repositories.
83
83
84 --datesort sort revisions by date. Converted repositories have
84 --datesort sort revisions by date. Converted repositories have
85 good-looking changelogs but are often an order of
85 good-looking changelogs but are often an order of
86 magnitude larger than the same ones generated by
86 magnitude larger than the same ones generated by
87 --branchsort.
87 --branchsort.
88
88
89 --sourcesort try to preserve source revisions order, only
89 --sourcesort try to preserve source revisions order, only
90 supported by Mercurial sources.
90 supported by Mercurial sources.
91
91
92 --closesort try to move closed revisions as close as possible
92 --closesort try to move closed revisions as close as possible
93 to parent branches, only supported by Mercurial
93 to parent branches, only supported by Mercurial
94 sources.
94 sources.
95
95
96 If ``REVMAP`` isn't given, it will be put in a default location
96 If ``REVMAP`` isn't given, it will be put in a default location
97 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
97 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
98 text file that maps each source commit ID to the destination ID
98 text file that maps each source commit ID to the destination ID
99 for that revision, like so::
99 for that revision, like so::
100
100
101 <source ID> <destination ID>
101 <source ID> <destination ID>
102
102
103 If the file doesn't exist, it's automatically created. It's
103 If the file doesn't exist, it's automatically created. It's
104 updated on each commit copied, so :hg:`convert` can be interrupted
104 updated on each commit copied, so :hg:`convert` can be interrupted
105 and can be run repeatedly to copy new commits.
105 and can be run repeatedly to copy new commits.
106
106
107 The authormap is a simple text file that maps each source commit
107 The authormap is a simple text file that maps each source commit
108 author to a destination commit author. It is handy for source SCMs
108 author to a destination commit author. It is handy for source SCMs
109 that use unix logins to identify authors (e.g.: CVS). One line per
109 that use unix logins to identify authors (e.g.: CVS). One line per
110 author mapping and the line format is::
110 author mapping and the line format is::
111
111
112 source author = destination author
112 source author = destination author
113
113
114 Empty lines and lines starting with a ``#`` are ignored.
114 Empty lines and lines starting with a ``#`` are ignored.
115
115
116 The filemap is a file that allows filtering and remapping of files
116 The filemap is a file that allows filtering and remapping of files
117 and directories. Each line can contain one of the following
117 and directories. Each line can contain one of the following
118 directives::
118 directives::
119
119
120 include path/to/file-or-dir
120 include path/to/file-or-dir
121
121
122 exclude path/to/file-or-dir
122 exclude path/to/file-or-dir
123
123
124 rename path/to/source path/to/destination
124 rename path/to/source path/to/destination
125
125
126 Comment lines start with ``#``. A specified path matches if it
126 Comment lines start with ``#``. A specified path matches if it
127 equals the full relative name of a file or one of its parent
127 equals the full relative name of a file or one of its parent
128 directories. The ``include`` or ``exclude`` directive with the
128 directories. The ``include`` or ``exclude`` directive with the
129 longest matching path applies, so line order does not matter.
129 longest matching path applies, so line order does not matter.
130
130
131 The ``include`` directive causes a file, or all files under a
131 The ``include`` directive causes a file, or all files under a
132 directory, to be included in the destination repository. The default
132 directory, to be included in the destination repository. The default
133 if there are no ``include`` statements is to include everything.
133 if there are no ``include`` statements is to include everything.
134 If there are any ``include`` statements, nothing else is included.
134 If there are any ``include`` statements, nothing else is included.
135 The ``exclude`` directive causes files or directories to
135 The ``exclude`` directive causes files or directories to
136 be omitted. The ``rename`` directive renames a file or directory if
136 be omitted. The ``rename`` directive renames a file or directory if
137 it is converted. To rename from a subdirectory into the root of
137 it is converted. To rename from a subdirectory into the root of
138 the repository, use ``.`` as the path to rename to.
138 the repository, use ``.`` as the path to rename to.
139
139
140 ``--full`` will make sure the converted changesets contain exactly
140 ``--full`` will make sure the converted changesets contain exactly
141 the right files with the right content. It will make a full
141 the right files with the right content. It will make a full
142 conversion of all files, not just the ones that have
142 conversion of all files, not just the ones that have
143 changed. Files that already are correct will not be changed. This
143 changed. Files that already are correct will not be changed. This
144 can be used to apply filemap changes when converting
144 can be used to apply filemap changes when converting
145 incrementally. This is currently only supported for Mercurial and
145 incrementally. This is currently only supported for Mercurial and
146 Subversion.
146 Subversion.
147
147
148 The splicemap is a file that allows insertion of synthetic
148 The splicemap is a file that allows insertion of synthetic
149 history, letting you specify the parents of a revision. This is
149 history, letting you specify the parents of a revision. This is
150 useful if you want to e.g. give a Subversion merge two parents, or
150 useful if you want to e.g. give a Subversion merge two parents, or
151 graft two disconnected series of history together. Each entry
151 graft two disconnected series of history together. Each entry
152 contains a key, followed by a space, followed by one or two
152 contains a key, followed by a space, followed by one or two
153 comma-separated values::
153 comma-separated values::
154
154
155 key parent1, parent2
155 key parent1, parent2
156
156
157 The key is the revision ID in the source
157 The key is the revision ID in the source
158 revision control system whose parents should be modified (same
158 revision control system whose parents should be modified (same
159 format as a key in .hg/shamap). The values are the revision IDs
159 format as a key in .hg/shamap). The values are the revision IDs
160 (in either the source or destination revision control system) that
160 (in either the source or destination revision control system) that
161 should be used as the new parents for that node. For example, if
161 should be used as the new parents for that node. For example, if
162 you have merged "release-1.0" into "trunk", then you should
162 you have merged "release-1.0" into "trunk", then you should
163 specify the revision on "trunk" as the first parent and the one on
163 specify the revision on "trunk" as the first parent and the one on
164 the "release-1.0" branch as the second.
164 the "release-1.0" branch as the second.
165
165
166 The branchmap is a file that allows you to rename a branch when it is
166 The branchmap is a file that allows you to rename a branch when it is
167 being brought in from whatever external repository. When used in
167 being brought in from whatever external repository. When used in
168 conjunction with a splicemap, it allows for a powerful combination
168 conjunction with a splicemap, it allows for a powerful combination
169 to help fix even the most badly mismanaged repositories and turn them
169 to help fix even the most badly mismanaged repositories and turn them
170 into nicely structured Mercurial repositories. The branchmap contains
170 into nicely structured Mercurial repositories. The branchmap contains
171 lines of the form::
171 lines of the form::
172
172
173 original_branch_name new_branch_name
173 original_branch_name new_branch_name
174
174
175 where "original_branch_name" is the name of the branch in the
175 where "original_branch_name" is the name of the branch in the
176 source repository, and "new_branch_name" is the name of the branch
176 source repository, and "new_branch_name" is the name of the branch
177 is the destination repository. No whitespace is allowed in the
177 is the destination repository. No whitespace is allowed in the
178 branch names. This can be used to (for instance) move code in one
178 branch names. This can be used to (for instance) move code in one
179 repository from "default" to a named branch.
179 repository from "default" to a named branch.
180
180
181 Mercurial Source
181 Mercurial Source
182 ################
182 ################
183
183
184 The Mercurial source recognizes the following configuration
184 The Mercurial source recognizes the following configuration
185 options, which you can set on the command line with ``--config``:
185 options, which you can set on the command line with ``--config``:
186
186
187 :convert.hg.ignoreerrors: ignore integrity errors when reading.
187 :convert.hg.ignoreerrors: ignore integrity errors when reading.
188 Use it to fix Mercurial repositories with missing revlogs, by
188 Use it to fix Mercurial repositories with missing revlogs, by
189 converting from and to Mercurial. Default is False.
189 converting from and to Mercurial. Default is False.
190
190
191 :convert.hg.saverev: store original revision ID in changeset
191 :convert.hg.saverev: store original revision ID in changeset
192 (forces target IDs to change). It takes a boolean argument and
192 (forces target IDs to change). It takes a boolean argument and
193 defaults to False.
193 defaults to False.
194
194
195 :convert.hg.revs: revset specifying the source revisions to convert.
195 :convert.hg.revs: revset specifying the source revisions to convert.
196
196
197 CVS Source
197 CVS Source
198 ##########
198 ##########
199
199
200 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
200 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
201 to indicate the starting point of what will be converted. Direct
201 to indicate the starting point of what will be converted. Direct
202 access to the repository files is not needed, unless of course the
202 access to the repository files is not needed, unless of course the
203 repository is ``:local:``. The conversion uses the top level
203 repository is ``:local:``. The conversion uses the top level
204 directory in the sandbox to find the CVS repository, and then uses
204 directory in the sandbox to find the CVS repository, and then uses
205 CVS rlog commands to find files to convert. This means that unless
205 CVS rlog commands to find files to convert. This means that unless
206 a filemap is given, all files under the starting directory will be
206 a filemap is given, all files under the starting directory will be
207 converted, and that any directory reorganization in the CVS
207 converted, and that any directory reorganization in the CVS
208 sandbox is ignored.
208 sandbox is ignored.
209
209
210 The following options can be used with ``--config``:
210 The following options can be used with ``--config``:
211
211
212 :convert.cvsps.cache: Set to False to disable remote log caching,
212 :convert.cvsps.cache: Set to False to disable remote log caching,
213 for testing and debugging purposes. Default is True.
213 for testing and debugging purposes. Default is True.
214
214
215 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
215 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
216 allowed between commits with identical user and log message in
216 allowed between commits with identical user and log message in
217 a single changeset. When very large files were checked in as
217 a single changeset. When very large files were checked in as
218 part of a changeset then the default may not be long enough.
218 part of a changeset then the default may not be long enough.
219 The default is 60.
219 The default is 60.
220
220
221 :convert.cvsps.mergeto: Specify a regular expression to which
221 :convert.cvsps.mergeto: Specify a regular expression to which
222 commit log messages are matched. If a match occurs, then the
222 commit log messages are matched. If a match occurs, then the
223 conversion process will insert a dummy revision merging the
223 conversion process will insert a dummy revision merging the
224 branch on which this log message occurs to the branch
224 branch on which this log message occurs to the branch
225 indicated in the regex. Default is ``{{mergetobranch
225 indicated in the regex. Default is ``{{mergetobranch
226 ([-\\w]+)}}``
226 ([-\\w]+)}}``
227
227
228 :convert.cvsps.mergefrom: Specify a regular expression to which
228 :convert.cvsps.mergefrom: Specify a regular expression to which
229 commit log messages are matched. If a match occurs, then the
229 commit log messages are matched. If a match occurs, then the
230 conversion process will add the most recent revision on the
230 conversion process will add the most recent revision on the
231 branch indicated in the regex as the second parent of the
231 branch indicated in the regex as the second parent of the
232 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
232 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
233
233
234 :convert.localtimezone: use local time (as determined by the TZ
234 :convert.localtimezone: use local time (as determined by the TZ
235 environment variable) for changeset date/times. The default
235 environment variable) for changeset date/times. The default
236 is False (use UTC).
236 is False (use UTC).
237
237
238 :hooks.cvslog: Specify a Python function to be called at the end of
238 :hooks.cvslog: Specify a Python function to be called at the end of
239 gathering the CVS log. The function is passed a list with the
239 gathering the CVS log. The function is passed a list with the
240 log entries, and can modify the entries in-place, or add or
240 log entries, and can modify the entries in-place, or add or
241 delete them.
241 delete them.
242
242
243 :hooks.cvschangesets: Specify a Python function to be called after
243 :hooks.cvschangesets: Specify a Python function to be called after
244 the changesets are calculated from the CVS log. The
244 the changesets are calculated from the CVS log. The
245 function is passed a list with the changeset entries, and can
245 function is passed a list with the changeset entries, and can
246 modify the changesets in-place, or add or delete them.
246 modify the changesets in-place, or add or delete them.
247
247
248 An additional "debugcvsps" Mercurial command allows the builtin
248 An additional "debugcvsps" Mercurial command allows the builtin
249 changeset merging code to be run without doing a conversion. Its
249 changeset merging code to be run without doing a conversion. Its
250 parameters and output are similar to that of cvsps 2.1. Please see
250 parameters and output are similar to that of cvsps 2.1. Please see
251 the command help for more details.
251 the command help for more details.
252
252
253 Subversion Source
253 Subversion Source
254 #################
254 #################
255
255
256 Subversion source detects classical trunk/branches/tags layouts.
256 Subversion source detects classical trunk/branches/tags layouts.
257 By default, the supplied ``svn://repo/path/`` source URL is
257 By default, the supplied ``svn://repo/path/`` source URL is
258 converted as a single branch. If ``svn://repo/path/trunk`` exists
258 converted as a single branch. If ``svn://repo/path/trunk`` exists
259 it replaces the default branch. If ``svn://repo/path/branches``
259 it replaces the default branch. If ``svn://repo/path/branches``
260 exists, its subdirectories are listed as possible branches. If
260 exists, its subdirectories are listed as possible branches. If
261 ``svn://repo/path/tags`` exists, it is looked for tags referencing
261 ``svn://repo/path/tags`` exists, it is looked for tags referencing
262 converted branches. Default ``trunk``, ``branches`` and ``tags``
262 converted branches. Default ``trunk``, ``branches`` and ``tags``
263 values can be overridden with following options. Set them to paths
263 values can be overridden with following options. Set them to paths
264 relative to the source URL, or leave them blank to disable auto
264 relative to the source URL, or leave them blank to disable auto
265 detection.
265 detection.
266
266
267 The following options can be set with ``--config``:
267 The following options can be set with ``--config``:
268
268
269 :convert.svn.branches: specify the directory containing branches.
269 :convert.svn.branches: specify the directory containing branches.
270 The default is ``branches``.
270 The default is ``branches``.
271
271
272 :convert.svn.tags: specify the directory containing tags. The
272 :convert.svn.tags: specify the directory containing tags. The
273 default is ``tags``.
273 default is ``tags``.
274
274
275 :convert.svn.trunk: specify the name of the trunk branch. The
275 :convert.svn.trunk: specify the name of the trunk branch. The
276 default is ``trunk``.
276 default is ``trunk``.
277
277
278 :convert.localtimezone: use local time (as determined by the TZ
278 :convert.localtimezone: use local time (as determined by the TZ
279 environment variable) for changeset date/times. The default
279 environment variable) for changeset date/times. The default
280 is False (use UTC).
280 is False (use UTC).
281
281
282 Source history can be retrieved starting at a specific revision,
282 Source history can be retrieved starting at a specific revision,
283 instead of being integrally converted. Only single branch
283 instead of being integrally converted. Only single branch
284 conversions are supported.
284 conversions are supported.
285
285
286 :convert.svn.startrev: specify start Subversion revision number.
286 :convert.svn.startrev: specify start Subversion revision number.
287 The default is 0.
287 The default is 0.
288
288
289 Git Source
289 Git Source
290 ##########
290 ##########
291
291
292 The Git importer converts commits from all reachable branches (refs
292 The Git importer converts commits from all reachable branches (refs
293 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
293 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
294 Branches are converted to bookmarks with the same name, with the
294 Branches are converted to bookmarks with the same name, with the
295 leading 'refs/heads' stripped. Git submodules are converted to Git
295 leading 'refs/heads' stripped. Git submodules are converted to Git
296 subrepos in Mercurial.
296 subrepos in Mercurial.
297
297
298 The following options can be set with ``--config``:
298 The following options can be set with ``--config``:
299
299
300 :convert.git.similarity: specify how similar files modified in a
300 :convert.git.similarity: specify how similar files modified in a
301 commit must be to be imported as renames or copies, as a
301 commit must be to be imported as renames or copies, as a
302 percentage between ``0`` (disabled) and ``100`` (files must be
302 percentage between ``0`` (disabled) and ``100`` (files must be
303 identical). For example, ``90`` means that a delete/add pair will
303 identical). For example, ``90`` means that a delete/add pair will
304 be imported as a rename if more than 90% of the file hasn't
304 be imported as a rename if more than 90% of the file hasn't
305 changed. The default is ``50``.
305 changed. The default is ``50``.
306
306
307 :convert.git.findcopiesharder: while detecting copies, look at all
307 :convert.git.findcopiesharder: while detecting copies, look at all
308 files in the working copy instead of just changed ones. This
308 files in the working copy instead of just changed ones. This
309 is very expensive for large projects, and is only effective when
309 is very expensive for large projects, and is only effective when
310 ``convert.git.similarity`` is greater than 0. The default is False.
310 ``convert.git.similarity`` is greater than 0. The default is False.
311
311
312 Perforce Source
312 Perforce Source
313 ###############
313 ###############
314
314
315 The Perforce (P4) importer can be given a p4 depot path or a
315 The Perforce (P4) importer can be given a p4 depot path or a
316 client specification as source. It will convert all files in the
316 client specification as source. It will convert all files in the
317 source to a flat Mercurial repository, ignoring labels, branches
317 source to a flat Mercurial repository, ignoring labels, branches
318 and integrations. Note that when a depot path is given you then
318 and integrations. Note that when a depot path is given you then
319 usually should specify a target directory, because otherwise the
319 usually should specify a target directory, because otherwise the
320 target may be named ``...-hg``.
320 target may be named ``...-hg``.
321
321
322 It is possible to limit the amount of source history to be
322 It is possible to limit the amount of source history to be
323 converted by specifying an initial Perforce revision:
323 converted by specifying an initial Perforce revision:
324
324
325 :convert.p4.startrev: specify initial Perforce revision (a
325 :convert.p4.startrev: specify initial Perforce revision (a
326 Perforce changelist number).
326 Perforce changelist number).
327
327
328 Mercurial Destination
328 Mercurial Destination
329 #####################
329 #####################
330
330
331 The Mercurial destination will recognize Mercurial subrepositories in the
331 The Mercurial destination will recognize Mercurial subrepositories in the
332 destination directory, and update the .hgsubstate file automatically if the
332 destination directory, and update the .hgsubstate file automatically if the
333 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
333 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
334 Converting a repository with subrepositories requires converting a single
334 Converting a repository with subrepositories requires converting a single
335 repository at a time, from the bottom up.
335 repository at a time, from the bottom up.
336
336
337 .. container:: verbose
337 .. container:: verbose
338
338
339 An example showing how to convert a repository with subrepositories::
339 An example showing how to convert a repository with subrepositories::
340
340
341 # so convert knows the type when it sees a non empty destination
341 # so convert knows the type when it sees a non empty destination
342 $ hg init converted
342 $ hg init converted
343
343
344 $ hg convert orig/sub1 converted/sub1
344 $ hg convert orig/sub1 converted/sub1
345 $ hg convert orig/sub2 converted/sub2
345 $ hg convert orig/sub2 converted/sub2
346 $ hg convert orig converted
346 $ hg convert orig converted
347
347
348 The following options are supported:
348 The following options are supported:
349
349
350 :convert.hg.clonebranches: dispatch source branches in separate
350 :convert.hg.clonebranches: dispatch source branches in separate
351 clones. The default is False.
351 clones. The default is False.
352
352
353 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
353 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
354 ``default``.
354 ``default``.
355
355
356 :convert.hg.usebranchnames: preserve branch names. The default is
356 :convert.hg.usebranchnames: preserve branch names. The default is
357 True.
357 True.
358
358
359 All Destinations
359 All Destinations
360 ################
360 ################
361
361
362 All destination types accept the following options:
362 All destination types accept the following options:
363
363
364 :convert.skiptags: does not convert tags from the source repo to the target
364 :convert.skiptags: does not convert tags from the source repo to the target
365 repo. The default is False.
365 repo. The default is False.
366 """
366 """
367 return convcmd.convert(ui, src, dest, revmapfile, **opts)
367 return convcmd.convert(ui, src, dest, revmapfile, **opts)
368
368
369 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
369 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
370 def debugsvnlog(ui, **opts):
370 def debugsvnlog(ui, **opts):
371 return subversion.debugsvnlog(ui, **opts)
371 return subversion.debugsvnlog(ui, **opts)
372
372
373 @command('debugcvsps',
373 @command('debugcvsps',
374 [
374 [
375 # Main options shared with cvsps-2.1
375 # Main options shared with cvsps-2.1
376 ('b', 'branches', [], _('only return changes on specified branches')),
376 ('b', 'branches', [], _('only return changes on specified branches')),
377 ('p', 'prefix', '', _('prefix to remove from file names')),
377 ('p', 'prefix', '', _('prefix to remove from file names')),
378 ('r', 'revisions', [],
378 ('r', 'revisions', [],
379 _('only return changes after or between specified tags')),
379 _('only return changes after or between specified tags')),
380 ('u', 'update-cache', None, _("update cvs log cache")),
380 ('u', 'update-cache', None, _("update cvs log cache")),
381 ('x', 'new-cache', None, _("create new cvs log cache")),
381 ('x', 'new-cache', None, _("create new cvs log cache")),
382 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
382 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
383 ('', 'root', '', _('specify cvsroot')),
383 ('', 'root', '', _('specify cvsroot')),
384 # Options specific to builtin cvsps
384 # Options specific to builtin cvsps
385 ('', 'parents', '', _('show parent changesets')),
385 ('', 'parents', '', _('show parent changesets')),
386 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
386 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
387 # Options that are ignored for compatibility with cvsps-2.1
387 # Options that are ignored for compatibility with cvsps-2.1
388 ('A', 'cvs-direct', None, _('ignored for compatibility')),
388 ('A', 'cvs-direct', None, _('ignored for compatibility')),
389 ],
389 ],
390 _('hg debugcvsps [OPTION]... [PATH]...'),
390 _('hg debugcvsps [OPTION]... [PATH]...'),
391 norepo=True)
391 norepo=True)
392 def debugcvsps(ui, *args, **opts):
392 def debugcvsps(ui, *args, **opts):
393 '''create changeset information from CVS
393 '''create changeset information from CVS
394
394
395 This command is intended as a debugging tool for the CVS to
395 This command is intended as a debugging tool for the CVS to
396 Mercurial converter, and can be used as a direct replacement for
396 Mercurial converter, and can be used as a direct replacement for
397 cvsps.
397 cvsps.
398
398
399 Hg debugcvsps reads the CVS rlog for current directory (or any
399 Hg debugcvsps reads the CVS rlog for current directory (or any
400 named directory) in the CVS repository, and converts the log to a
400 named directory) in the CVS repository, and converts the log to a
401 series of changesets based on matching commit log entries and
401 series of changesets based on matching commit log entries and
402 dates.'''
402 dates.'''
403 return cvsps.debugcvsps(ui, *args, **opts)
403 return cvsps.debugcvsps(ui, *args, **opts)
404
404
405 def kwconverted(ctx, name):
405 def kwconverted(ctx, name):
406 rev = ctx.extra().get('convert_revision', '')
406 rev = ctx.extra().get('convert_revision', '')
407 if rev.startswith('svn:'):
407 if rev.startswith('svn:'):
408 if name == 'svnrev':
408 if name == 'svnrev':
409 return str(subversion.revsplit(rev)[2])
409 return str(subversion.revsplit(rev)[2])
410 elif name == 'svnpath':
410 elif name == 'svnpath':
411 return subversion.revsplit(rev)[1]
411 return subversion.revsplit(rev)[1]
412 elif name == 'svnuuid':
412 elif name == 'svnuuid':
413 return subversion.revsplit(rev)[0]
413 return subversion.revsplit(rev)[0]
414 return rev
414 return rev
415
415
416 def kwsvnrev(repo, ctx, **args):
416 def kwsvnrev(repo, ctx, **args):
417 """:svnrev: String. Converted subversion revision number."""
417 """:svnrev: String. Converted subversion revision number."""
418 return kwconverted(ctx, 'svnrev')
418 return kwconverted(ctx, 'svnrev')
419
419
420 def kwsvnpath(repo, ctx, **args):
420 def kwsvnpath(repo, ctx, **args):
421 """:svnpath: String. Converted subversion revision project path."""
421 """:svnpath: String. Converted subversion revision project path."""
422 return kwconverted(ctx, 'svnpath')
422 return kwconverted(ctx, 'svnpath')
423
423
424 def kwsvnuuid(repo, ctx, **args):
424 def kwsvnuuid(repo, ctx, **args):
425 """:svnuuid: String. Converted subversion revision repository identifier."""
425 """:svnuuid: String. Converted subversion revision repository identifier."""
426 return kwconverted(ctx, 'svnuuid')
426 return kwconverted(ctx, 'svnuuid')
427
427
428 def extsetup(ui):
428 def extsetup(ui):
429 templatekw.keywords['svnrev'] = kwsvnrev
429 templatekw.keywords['svnrev'] = kwsvnrev
430 templatekw.keywords['svnpath'] = kwsvnpath
430 templatekw.keywords['svnpath'] = kwsvnpath
431 templatekw.keywords['svnuuid'] = kwsvnuuid
431 templatekw.keywords['svnuuid'] = kwsvnuuid
432
432
433 # tell hggettext to extract docstrings from these functions:
433 # tell hggettext to extract docstrings from these functions:
434 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
434 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,286 +1,286
1 # bzr.py - bzr support for the convert extension
1 # bzr.py - bzr support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
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 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
9 # it cannot access 'bar' repositories, but they were never used very much
9 # it cannot access 'bar' repositories, but they were never used very much
10
10
11 import os
11 import os
12 from mercurial import demandimport
12 from mercurial import demandimport
13 # these do not work with demandimport, blacklist
13 # these do not work with demandimport, blacklist
14 demandimport.ignore.extend([
14 demandimport.ignore.extend([
15 'bzrlib.transactions',
15 'bzrlib.transactions',
16 'bzrlib.urlutils',
16 'bzrlib.urlutils',
17 'ElementPath',
17 'ElementPath',
18 ])
18 ])
19
19
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21 from mercurial import util
21 from mercurial import util
22 from common import NoRepo, commit, converter_source
22 from common import NoRepo, commit, converter_source
23
23
24 try:
24 try:
25 # bazaar imports
25 # bazaar imports
26 from bzrlib import bzrdir, revision, errors
26 from bzrlib import bzrdir, revision, errors
27 from bzrlib.revisionspec import RevisionSpec
27 from bzrlib.revisionspec import RevisionSpec
28 except ImportError:
28 except ImportError:
29 pass
29 pass
30
30
31 supportedkinds = ('file', 'symlink')
31 supportedkinds = ('file', 'symlink')
32
32
33 class bzr_source(converter_source):
33 class bzr_source(converter_source):
34 """Reads Bazaar repositories by using the Bazaar Python libraries"""
34 """Reads Bazaar repositories by using the Bazaar Python libraries"""
35
35
36 def __init__(self, ui, path, rev=None):
36 def __init__(self, ui, path, revs=None):
37 super(bzr_source, self).__init__(ui, path, rev=rev)
37 super(bzr_source, self).__init__(ui, path, revs=revs)
38
38
39 if not os.path.exists(os.path.join(path, '.bzr')):
39 if not os.path.exists(os.path.join(path, '.bzr')):
40 raise NoRepo(_('%s does not look like a Bazaar repository')
40 raise NoRepo(_('%s does not look like a Bazaar repository')
41 % path)
41 % path)
42
42
43 try:
43 try:
44 # access bzrlib stuff
44 # access bzrlib stuff
45 bzrdir
45 bzrdir
46 except NameError:
46 except NameError:
47 raise NoRepo(_('Bazaar modules could not be loaded'))
47 raise NoRepo(_('Bazaar modules could not be loaded'))
48
48
49 path = os.path.abspath(path)
49 path = os.path.abspath(path)
50 self._checkrepotype(path)
50 self._checkrepotype(path)
51 try:
51 try:
52 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
52 self.sourcerepo = bzrdir.BzrDir.open(path).open_repository()
53 except errors.NoRepositoryPresent:
53 except errors.NoRepositoryPresent:
54 raise NoRepo(_('%s does not look like a Bazaar repository')
54 raise NoRepo(_('%s does not look like a Bazaar repository')
55 % path)
55 % path)
56 self._parentids = {}
56 self._parentids = {}
57
57
58 def _checkrepotype(self, path):
58 def _checkrepotype(self, path):
59 # Lightweight checkouts detection is informational but probably
59 # Lightweight checkouts detection is informational but probably
60 # fragile at API level. It should not terminate the conversion.
60 # fragile at API level. It should not terminate the conversion.
61 try:
61 try:
62 from bzrlib import bzrdir
62 from bzrlib import bzrdir
63 dir = bzrdir.BzrDir.open_containing(path)[0]
63 dir = bzrdir.BzrDir.open_containing(path)[0]
64 try:
64 try:
65 tree = dir.open_workingtree(recommend_upgrade=False)
65 tree = dir.open_workingtree(recommend_upgrade=False)
66 branch = tree.branch
66 branch = tree.branch
67 except (errors.NoWorkingTree, errors.NotLocalUrl):
67 except (errors.NoWorkingTree, errors.NotLocalUrl):
68 tree = None
68 tree = None
69 branch = dir.open_branch()
69 branch = dir.open_branch()
70 if (tree is not None and tree.bzrdir.root_transport.base !=
70 if (tree is not None and tree.bzrdir.root_transport.base !=
71 branch.bzrdir.root_transport.base):
71 branch.bzrdir.root_transport.base):
72 self.ui.warn(_('warning: lightweight checkouts may cause '
72 self.ui.warn(_('warning: lightweight checkouts may cause '
73 'conversion failures, try with a regular '
73 'conversion failures, try with a regular '
74 'branch instead.\n'))
74 'branch instead.\n'))
75 except Exception:
75 except Exception:
76 self.ui.note(_('bzr source type could not be determined\n'))
76 self.ui.note(_('bzr source type could not be determined\n'))
77
77
78 def before(self):
78 def before(self):
79 """Before the conversion begins, acquire a read lock
79 """Before the conversion begins, acquire a read lock
80 for all the operations that might need it. Fortunately
80 for all the operations that might need it. Fortunately
81 read locks don't block other reads or writes to the
81 read locks don't block other reads or writes to the
82 repository, so this shouldn't have any impact on the usage of
82 repository, so this shouldn't have any impact on the usage of
83 the source repository.
83 the source repository.
84
84
85 The alternative would be locking on every operation that
85 The alternative would be locking on every operation that
86 needs locks (there are currently two: getting the file and
86 needs locks (there are currently two: getting the file and
87 getting the parent map) and releasing immediately after,
87 getting the parent map) and releasing immediately after,
88 but this approach can take even 40% longer."""
88 but this approach can take even 40% longer."""
89 self.sourcerepo.lock_read()
89 self.sourcerepo.lock_read()
90
90
91 def after(self):
91 def after(self):
92 self.sourcerepo.unlock()
92 self.sourcerepo.unlock()
93
93
94 def _bzrbranches(self):
94 def _bzrbranches(self):
95 return self.sourcerepo.find_branches(using=True)
95 return self.sourcerepo.find_branches(using=True)
96
96
97 def getheads(self):
97 def getheads(self):
98 if not self.rev:
98 if not self.revs:
99 # Set using=True to avoid nested repositories (see issue3254)
99 # Set using=True to avoid nested repositories (see issue3254)
100 heads = sorted([b.last_revision() for b in self._bzrbranches()])
100 heads = sorted([b.last_revision() for b in self._bzrbranches()])
101 else:
101 else:
102 revid = None
102 revid = None
103 for branch in self._bzrbranches():
103 for branch in self._bzrbranches():
104 try:
104 try:
105 r = RevisionSpec.from_string(self.rev)
105 r = RevisionSpec.from_string(self.revs[0])
106 info = r.in_history(branch)
106 info = r.in_history(branch)
107 except errors.BzrError:
107 except errors.BzrError:
108 pass
108 pass
109 revid = info.rev_id
109 revid = info.rev_id
110 if revid is None:
110 if revid is None:
111 raise util.Abort(_('%s is not a valid revision') % self.rev)
111 raise util.Abort(_('%s is not a valid revision') % self.revs[0])
112 heads = [revid]
112 heads = [revid]
113 # Empty repositories return 'null:', which cannot be retrieved
113 # Empty repositories return 'null:', which cannot be retrieved
114 heads = [h for h in heads if h != 'null:']
114 heads = [h for h in heads if h != 'null:']
115 return heads
115 return heads
116
116
117 def getfile(self, name, rev):
117 def getfile(self, name, rev):
118 revtree = self.sourcerepo.revision_tree(rev)
118 revtree = self.sourcerepo.revision_tree(rev)
119 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
119 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
120 kind = None
120 kind = None
121 if fileid is not None:
121 if fileid is not None:
122 kind = revtree.kind(fileid)
122 kind = revtree.kind(fileid)
123 if kind not in supportedkinds:
123 if kind not in supportedkinds:
124 # the file is not available anymore - was deleted
124 # the file is not available anymore - was deleted
125 return None, None
125 return None, None
126 mode = self._modecache[(name, rev)]
126 mode = self._modecache[(name, rev)]
127 if kind == 'symlink':
127 if kind == 'symlink':
128 target = revtree.get_symlink_target(fileid)
128 target = revtree.get_symlink_target(fileid)
129 if target is None:
129 if target is None:
130 raise util.Abort(_('%s.%s symlink has no target')
130 raise util.Abort(_('%s.%s symlink has no target')
131 % (name, rev))
131 % (name, rev))
132 return target, mode
132 return target, mode
133 else:
133 else:
134 sio = revtree.get_file(fileid)
134 sio = revtree.get_file(fileid)
135 return sio.read(), mode
135 return sio.read(), mode
136
136
137 def getchanges(self, version, full):
137 def getchanges(self, version, full):
138 if full:
138 if full:
139 raise util.Abort(_("convert from cvs do not support --full"))
139 raise util.Abort(_("convert from cvs do not support --full"))
140 self._modecache = {}
140 self._modecache = {}
141 self._revtree = self.sourcerepo.revision_tree(version)
141 self._revtree = self.sourcerepo.revision_tree(version)
142 # get the parentids from the cache
142 # get the parentids from the cache
143 parentids = self._parentids.pop(version)
143 parentids = self._parentids.pop(version)
144 # only diff against first parent id
144 # only diff against first parent id
145 prevtree = self.sourcerepo.revision_tree(parentids[0])
145 prevtree = self.sourcerepo.revision_tree(parentids[0])
146 files, changes = self._gettreechanges(self._revtree, prevtree)
146 files, changes = self._gettreechanges(self._revtree, prevtree)
147 return files, changes, set()
147 return files, changes, set()
148
148
149 def getcommit(self, version):
149 def getcommit(self, version):
150 rev = self.sourcerepo.get_revision(version)
150 rev = self.sourcerepo.get_revision(version)
151 # populate parent id cache
151 # populate parent id cache
152 if not rev.parent_ids:
152 if not rev.parent_ids:
153 parents = []
153 parents = []
154 self._parentids[version] = (revision.NULL_REVISION,)
154 self._parentids[version] = (revision.NULL_REVISION,)
155 else:
155 else:
156 parents = self._filterghosts(rev.parent_ids)
156 parents = self._filterghosts(rev.parent_ids)
157 self._parentids[version] = parents
157 self._parentids[version] = parents
158
158
159 branch = self.recode(rev.properties.get('branch-nick', u'default'))
159 branch = self.recode(rev.properties.get('branch-nick', u'default'))
160 if branch == 'trunk':
160 if branch == 'trunk':
161 branch = 'default'
161 branch = 'default'
162 return commit(parents=parents,
162 return commit(parents=parents,
163 date='%d %d' % (rev.timestamp, -rev.timezone),
163 date='%d %d' % (rev.timestamp, -rev.timezone),
164 author=self.recode(rev.committer),
164 author=self.recode(rev.committer),
165 desc=self.recode(rev.message),
165 desc=self.recode(rev.message),
166 branch=branch,
166 branch=branch,
167 rev=version)
167 rev=version)
168
168
169 def gettags(self):
169 def gettags(self):
170 bytetags = {}
170 bytetags = {}
171 for branch in self._bzrbranches():
171 for branch in self._bzrbranches():
172 if not branch.supports_tags():
172 if not branch.supports_tags():
173 return {}
173 return {}
174 tagdict = branch.tags.get_tag_dict()
174 tagdict = branch.tags.get_tag_dict()
175 for name, rev in tagdict.iteritems():
175 for name, rev in tagdict.iteritems():
176 bytetags[self.recode(name)] = rev
176 bytetags[self.recode(name)] = rev
177 return bytetags
177 return bytetags
178
178
179 def getchangedfiles(self, rev, i):
179 def getchangedfiles(self, rev, i):
180 self._modecache = {}
180 self._modecache = {}
181 curtree = self.sourcerepo.revision_tree(rev)
181 curtree = self.sourcerepo.revision_tree(rev)
182 if i is not None:
182 if i is not None:
183 parentid = self._parentids[rev][i]
183 parentid = self._parentids[rev][i]
184 else:
184 else:
185 # no parent id, get the empty revision
185 # no parent id, get the empty revision
186 parentid = revision.NULL_REVISION
186 parentid = revision.NULL_REVISION
187
187
188 prevtree = self.sourcerepo.revision_tree(parentid)
188 prevtree = self.sourcerepo.revision_tree(parentid)
189 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
189 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
190 return changes
190 return changes
191
191
192 def _gettreechanges(self, current, origin):
192 def _gettreechanges(self, current, origin):
193 revid = current._revision_id
193 revid = current._revision_id
194 changes = []
194 changes = []
195 renames = {}
195 renames = {}
196 seen = set()
196 seen = set()
197 # Process the entries by reverse lexicographic name order to
197 # Process the entries by reverse lexicographic name order to
198 # handle nested renames correctly, most specific first.
198 # handle nested renames correctly, most specific first.
199 curchanges = sorted(current.iter_changes(origin),
199 curchanges = sorted(current.iter_changes(origin),
200 key=lambda c: c[1][0] or c[1][1],
200 key=lambda c: c[1][0] or c[1][1],
201 reverse=True)
201 reverse=True)
202 for (fileid, paths, changed_content, versioned, parent, name,
202 for (fileid, paths, changed_content, versioned, parent, name,
203 kind, executable) in curchanges:
203 kind, executable) in curchanges:
204
204
205 if paths[0] == u'' or paths[1] == u'':
205 if paths[0] == u'' or paths[1] == u'':
206 # ignore changes to tree root
206 # ignore changes to tree root
207 continue
207 continue
208
208
209 # bazaar tracks directories, mercurial does not, so
209 # bazaar tracks directories, mercurial does not, so
210 # we have to rename the directory contents
210 # we have to rename the directory contents
211 if kind[1] == 'directory':
211 if kind[1] == 'directory':
212 if kind[0] not in (None, 'directory'):
212 if kind[0] not in (None, 'directory'):
213 # Replacing 'something' with a directory, record it
213 # Replacing 'something' with a directory, record it
214 # so it can be removed.
214 # so it can be removed.
215 changes.append((self.recode(paths[0]), revid))
215 changes.append((self.recode(paths[0]), revid))
216
216
217 if kind[0] == 'directory' and None not in paths:
217 if kind[0] == 'directory' and None not in paths:
218 renaming = paths[0] != paths[1]
218 renaming = paths[0] != paths[1]
219 # neither an add nor an delete - a move
219 # neither an add nor an delete - a move
220 # rename all directory contents manually
220 # rename all directory contents manually
221 subdir = origin.inventory.path2id(paths[0])
221 subdir = origin.inventory.path2id(paths[0])
222 # get all child-entries of the directory
222 # get all child-entries of the directory
223 for name, entry in origin.inventory.iter_entries(subdir):
223 for name, entry in origin.inventory.iter_entries(subdir):
224 # hg does not track directory renames
224 # hg does not track directory renames
225 if entry.kind == 'directory':
225 if entry.kind == 'directory':
226 continue
226 continue
227 frompath = self.recode(paths[0] + '/' + name)
227 frompath = self.recode(paths[0] + '/' + name)
228 if frompath in seen:
228 if frompath in seen:
229 # Already handled by a more specific change entry
229 # Already handled by a more specific change entry
230 # This is important when you have:
230 # This is important when you have:
231 # a => b
231 # a => b
232 # a/c => a/c
232 # a/c => a/c
233 # Here a/c must not be renamed into b/c
233 # Here a/c must not be renamed into b/c
234 continue
234 continue
235 seen.add(frompath)
235 seen.add(frompath)
236 if not renaming:
236 if not renaming:
237 continue
237 continue
238 topath = self.recode(paths[1] + '/' + name)
238 topath = self.recode(paths[1] + '/' + name)
239 # register the files as changed
239 # register the files as changed
240 changes.append((frompath, revid))
240 changes.append((frompath, revid))
241 changes.append((topath, revid))
241 changes.append((topath, revid))
242 # add to mode cache
242 # add to mode cache
243 mode = ((entry.executable and 'x')
243 mode = ((entry.executable and 'x')
244 or (entry.kind == 'symlink' and 's')
244 or (entry.kind == 'symlink' and 's')
245 or '')
245 or '')
246 self._modecache[(topath, revid)] = mode
246 self._modecache[(topath, revid)] = mode
247 # register the change as move
247 # register the change as move
248 renames[topath] = frompath
248 renames[topath] = frompath
249
249
250 # no further changes, go to the next change
250 # no further changes, go to the next change
251 continue
251 continue
252
252
253 # we got unicode paths, need to convert them
253 # we got unicode paths, need to convert them
254 path, topath = paths
254 path, topath = paths
255 if path is not None:
255 if path is not None:
256 path = self.recode(path)
256 path = self.recode(path)
257 if topath is not None:
257 if topath is not None:
258 topath = self.recode(topath)
258 topath = self.recode(topath)
259 seen.add(path or topath)
259 seen.add(path or topath)
260
260
261 if topath is None:
261 if topath is None:
262 # file deleted
262 # file deleted
263 changes.append((path, revid))
263 changes.append((path, revid))
264 continue
264 continue
265
265
266 # renamed
266 # renamed
267 if path and path != topath:
267 if path and path != topath:
268 renames[topath] = path
268 renames[topath] = path
269 changes.append((path, revid))
269 changes.append((path, revid))
270
270
271 # populate the mode cache
271 # populate the mode cache
272 kind, executable = [e[1] for e in (kind, executable)]
272 kind, executable = [e[1] for e in (kind, executable)]
273 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
273 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
274 or '')
274 or '')
275 self._modecache[(topath, revid)] = mode
275 self._modecache[(topath, revid)] = mode
276 changes.append((topath, revid))
276 changes.append((topath, revid))
277
277
278 return changes, renames
278 return changes, renames
279
279
280 def _filterghosts(self, ids):
280 def _filterghosts(self, ids):
281 """Filters out ghost revisions which hg does not support, see
281 """Filters out ghost revisions which hg does not support, see
282 <http://bazaar-vcs.org/GhostRevision>
282 <http://bazaar-vcs.org/GhostRevision>
283 """
283 """
284 parentmap = self.sourcerepo.get_parent_map(ids)
284 parentmap = self.sourcerepo.get_parent_map(ids)
285 parents = tuple([parent for parent in ids if parent in parentmap])
285 parents = tuple([parent for parent in ids if parent in parentmap])
286 return parents
286 return parents
@@ -1,471 +1,471
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 base64, errno, subprocess, os, datetime, re
8 import base64, errno, subprocess, os, datetime, re
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import phases, util
10 from mercurial import phases, util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 if abort:
34 if abort:
35 exc = util.Abort
35 exc = util.Abort
36 else:
36 else:
37 exc = MissingTool
37 exc = MissingTool
38 raise exc(_('cannot find required "%s" tool') % name)
38 raise exc(_('cannot find required "%s" tool') % name)
39
39
40 class NoRepo(Exception):
40 class NoRepo(Exception):
41 pass
41 pass
42
42
43 SKIPREV = 'SKIP'
43 SKIPREV = 'SKIP'
44
44
45 class commit(object):
45 class commit(object):
46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
48 self.author = author or 'unknown'
48 self.author = author or 'unknown'
49 self.date = date or '0 0'
49 self.date = date or '0 0'
50 self.desc = desc
50 self.desc = desc
51 self.parents = parents
51 self.parents = parents
52 self.branch = branch
52 self.branch = branch
53 self.rev = rev
53 self.rev = rev
54 self.extra = extra
54 self.extra = extra
55 self.sortkey = sortkey
55 self.sortkey = sortkey
56 self.saverev = saverev
56 self.saverev = saverev
57 self.phase = phase
57 self.phase = phase
58
58
59 class converter_source(object):
59 class converter_source(object):
60 """Conversion source interface"""
60 """Conversion source interface"""
61
61
62 def __init__(self, ui, path=None, rev=None):
62 def __init__(self, ui, path=None, revs=None):
63 """Initialize conversion source (or raise NoRepo("message")
63 """Initialize conversion source (or raise NoRepo("message")
64 exception if path is not a valid repository)"""
64 exception if path is not a valid repository)"""
65 self.ui = ui
65 self.ui = ui
66 self.path = path
66 self.path = path
67 self.rev = rev
67 self.revs = revs
68
68
69 self.encoding = 'utf-8'
69 self.encoding = 'utf-8'
70
70
71 def checkhexformat(self, revstr, mapname='splicemap'):
71 def checkhexformat(self, revstr, mapname='splicemap'):
72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
73 such format for their revision numbering
73 such format for their revision numbering
74 """
74 """
75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
76 raise util.Abort(_('%s entry %s is not a valid revision'
76 raise util.Abort(_('%s entry %s is not a valid revision'
77 ' identifier') % (mapname, revstr))
77 ' identifier') % (mapname, revstr))
78
78
79 def before(self):
79 def before(self):
80 pass
80 pass
81
81
82 def after(self):
82 def after(self):
83 pass
83 pass
84
84
85 def setrevmap(self, revmap):
85 def setrevmap(self, revmap):
86 """set the map of already-converted revisions"""
86 """set the map of already-converted revisions"""
87 pass
87 pass
88
88
89 def getheads(self):
89 def getheads(self):
90 """Return a list of this repository's heads"""
90 """Return a list of this repository's heads"""
91 raise NotImplementedError
91 raise NotImplementedError
92
92
93 def getfile(self, name, rev):
93 def getfile(self, name, rev):
94 """Return a pair (data, mode) where data is the file content
94 """Return a pair (data, mode) where data is the file content
95 as a string and mode one of '', 'x' or 'l'. rev is the
95 as a string and mode one of '', 'x' or 'l'. rev is the
96 identifier returned by a previous call to getchanges().
96 identifier returned by a previous call to getchanges().
97 Data is None if file is missing/deleted in rev.
97 Data is None if file is missing/deleted in rev.
98 """
98 """
99 raise NotImplementedError
99 raise NotImplementedError
100
100
101 def getchanges(self, version, full):
101 def getchanges(self, version, full):
102 """Returns a tuple of (files, copies, cleanp2).
102 """Returns a tuple of (files, copies, cleanp2).
103
103
104 files is a sorted list of (filename, id) tuples for all files
104 files is a sorted list of (filename, id) tuples for all files
105 changed between version and its first parent returned by
105 changed between version and its first parent returned by
106 getcommit(). If full, all files in that revision is returned.
106 getcommit(). If full, all files in that revision is returned.
107 id is the source revision id of the file.
107 id is the source revision id of the file.
108
108
109 copies is a dictionary of dest: source
109 copies is a dictionary of dest: source
110
110
111 cleanp2 is the set of files filenames that are clean against p2.
111 cleanp2 is the set of files filenames that are clean against p2.
112 (Files that are clean against p1 are already not in files (unless
112 (Files that are clean against p1 are already not in files (unless
113 full). This makes it possible to handle p2 clean files similarly.)
113 full). This makes it possible to handle p2 clean files similarly.)
114 """
114 """
115 raise NotImplementedError
115 raise NotImplementedError
116
116
117 def getcommit(self, version):
117 def getcommit(self, version):
118 """Return the commit object for version"""
118 """Return the commit object for version"""
119 raise NotImplementedError
119 raise NotImplementedError
120
120
121 def numcommits(self):
121 def numcommits(self):
122 """Return the number of commits in this source.
122 """Return the number of commits in this source.
123
123
124 If unknown, return None.
124 If unknown, return None.
125 """
125 """
126 return None
126 return None
127
127
128 def gettags(self):
128 def gettags(self):
129 """Return the tags as a dictionary of name: revision
129 """Return the tags as a dictionary of name: revision
130
130
131 Tag names must be UTF-8 strings.
131 Tag names must be UTF-8 strings.
132 """
132 """
133 raise NotImplementedError
133 raise NotImplementedError
134
134
135 def recode(self, s, encoding=None):
135 def recode(self, s, encoding=None):
136 if not encoding:
136 if not encoding:
137 encoding = self.encoding or 'utf-8'
137 encoding = self.encoding or 'utf-8'
138
138
139 if isinstance(s, unicode):
139 if isinstance(s, unicode):
140 return s.encode("utf-8")
140 return s.encode("utf-8")
141 try:
141 try:
142 return s.decode(encoding).encode("utf-8")
142 return s.decode(encoding).encode("utf-8")
143 except UnicodeError:
143 except UnicodeError:
144 try:
144 try:
145 return s.decode("latin-1").encode("utf-8")
145 return s.decode("latin-1").encode("utf-8")
146 except UnicodeError:
146 except UnicodeError:
147 return s.decode(encoding, "replace").encode("utf-8")
147 return s.decode(encoding, "replace").encode("utf-8")
148
148
149 def getchangedfiles(self, rev, i):
149 def getchangedfiles(self, rev, i):
150 """Return the files changed by rev compared to parent[i].
150 """Return the files changed by rev compared to parent[i].
151
151
152 i is an index selecting one of the parents of rev. The return
152 i is an index selecting one of the parents of rev. The return
153 value should be the list of files that are different in rev and
153 value should be the list of files that are different in rev and
154 this parent.
154 this parent.
155
155
156 If rev has no parents, i is None.
156 If rev has no parents, i is None.
157
157
158 This function is only needed to support --filemap
158 This function is only needed to support --filemap
159 """
159 """
160 raise NotImplementedError
160 raise NotImplementedError
161
161
162 def converted(self, rev, sinkrev):
162 def converted(self, rev, sinkrev):
163 '''Notify the source that a revision has been converted.'''
163 '''Notify the source that a revision has been converted.'''
164 pass
164 pass
165
165
166 def hasnativeorder(self):
166 def hasnativeorder(self):
167 """Return true if this source has a meaningful, native revision
167 """Return true if this source has a meaningful, native revision
168 order. For instance, Mercurial revisions are store sequentially
168 order. For instance, Mercurial revisions are store sequentially
169 while there is no such global ordering with Darcs.
169 while there is no such global ordering with Darcs.
170 """
170 """
171 return False
171 return False
172
172
173 def hasnativeclose(self):
173 def hasnativeclose(self):
174 """Return true if this source has ability to close branch.
174 """Return true if this source has ability to close branch.
175 """
175 """
176 return False
176 return False
177
177
178 def lookuprev(self, rev):
178 def lookuprev(self, rev):
179 """If rev is a meaningful revision reference in source, return
179 """If rev is a meaningful revision reference in source, return
180 the referenced identifier in the same format used by getcommit().
180 the referenced identifier in the same format used by getcommit().
181 return None otherwise.
181 return None otherwise.
182 """
182 """
183 return None
183 return None
184
184
185 def getbookmarks(self):
185 def getbookmarks(self):
186 """Return the bookmarks as a dictionary of name: revision
186 """Return the bookmarks as a dictionary of name: revision
187
187
188 Bookmark names are to be UTF-8 strings.
188 Bookmark names are to be UTF-8 strings.
189 """
189 """
190 return {}
190 return {}
191
191
192 def checkrevformat(self, revstr, mapname='splicemap'):
192 def checkrevformat(self, revstr, mapname='splicemap'):
193 """revstr is a string that describes a revision in the given
193 """revstr is a string that describes a revision in the given
194 source control system. Return true if revstr has correct
194 source control system. Return true if revstr has correct
195 format.
195 format.
196 """
196 """
197 return True
197 return True
198
198
199 class converter_sink(object):
199 class converter_sink(object):
200 """Conversion sink (target) interface"""
200 """Conversion sink (target) interface"""
201
201
202 def __init__(self, ui, path):
202 def __init__(self, ui, path):
203 """Initialize conversion sink (or raise NoRepo("message")
203 """Initialize conversion sink (or raise NoRepo("message")
204 exception if path is not a valid repository)
204 exception if path is not a valid repository)
205
205
206 created is a list of paths to remove if a fatal error occurs
206 created is a list of paths to remove if a fatal error occurs
207 later"""
207 later"""
208 self.ui = ui
208 self.ui = ui
209 self.path = path
209 self.path = path
210 self.created = []
210 self.created = []
211
211
212 def revmapfile(self):
212 def revmapfile(self):
213 """Path to a file that will contain lines
213 """Path to a file that will contain lines
214 source_rev_id sink_rev_id
214 source_rev_id sink_rev_id
215 mapping equivalent revision identifiers for each system."""
215 mapping equivalent revision identifiers for each system."""
216 raise NotImplementedError
216 raise NotImplementedError
217
217
218 def authorfile(self):
218 def authorfile(self):
219 """Path to a file that will contain lines
219 """Path to a file that will contain lines
220 srcauthor=dstauthor
220 srcauthor=dstauthor
221 mapping equivalent authors identifiers for each system."""
221 mapping equivalent authors identifiers for each system."""
222 return None
222 return None
223
223
224 def putcommit(self, files, copies, parents, commit, source, revmap, full,
224 def putcommit(self, files, copies, parents, commit, source, revmap, full,
225 cleanp2):
225 cleanp2):
226 """Create a revision with all changed files listed in 'files'
226 """Create a revision with all changed files listed in 'files'
227 and having listed parents. 'commit' is a commit object
227 and having listed parents. 'commit' is a commit object
228 containing at a minimum the author, date, and message for this
228 containing at a minimum the author, date, and message for this
229 changeset. 'files' is a list of (path, version) tuples,
229 changeset. 'files' is a list of (path, version) tuples,
230 'copies' is a dictionary mapping destinations to sources,
230 'copies' is a dictionary mapping destinations to sources,
231 'source' is the source repository, and 'revmap' is a mapfile
231 'source' is the source repository, and 'revmap' is a mapfile
232 of source revisions to converted revisions. Only getfile() and
232 of source revisions to converted revisions. Only getfile() and
233 lookuprev() should be called on 'source'. 'full' means that 'files'
233 lookuprev() should be called on 'source'. 'full' means that 'files'
234 is complete and all other files should be removed.
234 is complete and all other files should be removed.
235 'cleanp2' is a set of the filenames that are unchanged from p2
235 'cleanp2' is a set of the filenames that are unchanged from p2
236 (only in the common merge case where there two parents).
236 (only in the common merge case where there two parents).
237
237
238 Note that the sink repository is not told to update itself to
238 Note that the sink repository is not told to update itself to
239 a particular revision (or even what that revision would be)
239 a particular revision (or even what that revision would be)
240 before it receives the file data.
240 before it receives the file data.
241 """
241 """
242 raise NotImplementedError
242 raise NotImplementedError
243
243
244 def puttags(self, tags):
244 def puttags(self, tags):
245 """Put tags into sink.
245 """Put tags into sink.
246
246
247 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
247 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
248 Return a pair (tag_revision, tag_parent_revision), or (None, None)
248 Return a pair (tag_revision, tag_parent_revision), or (None, None)
249 if nothing was changed.
249 if nothing was changed.
250 """
250 """
251 raise NotImplementedError
251 raise NotImplementedError
252
252
253 def setbranch(self, branch, pbranches):
253 def setbranch(self, branch, pbranches):
254 """Set the current branch name. Called before the first putcommit
254 """Set the current branch name. Called before the first putcommit
255 on the branch.
255 on the branch.
256 branch: branch name for subsequent commits
256 branch: branch name for subsequent commits
257 pbranches: (converted parent revision, parent branch) tuples"""
257 pbranches: (converted parent revision, parent branch) tuples"""
258 pass
258 pass
259
259
260 def setfilemapmode(self, active):
260 def setfilemapmode(self, active):
261 """Tell the destination that we're using a filemap
261 """Tell the destination that we're using a filemap
262
262
263 Some converter_sources (svn in particular) can claim that a file
263 Some converter_sources (svn in particular) can claim that a file
264 was changed in a revision, even if there was no change. This method
264 was changed in a revision, even if there was no change. This method
265 tells the destination that we're using a filemap and that it should
265 tells the destination that we're using a filemap and that it should
266 filter empty revisions.
266 filter empty revisions.
267 """
267 """
268 pass
268 pass
269
269
270 def before(self):
270 def before(self):
271 pass
271 pass
272
272
273 def after(self):
273 def after(self):
274 pass
274 pass
275
275
276 def putbookmarks(self, bookmarks):
276 def putbookmarks(self, bookmarks):
277 """Put bookmarks into sink.
277 """Put bookmarks into sink.
278
278
279 bookmarks: {bookmarkname: sink_rev_id, ...}
279 bookmarks: {bookmarkname: sink_rev_id, ...}
280 where bookmarkname is an UTF-8 string.
280 where bookmarkname is an UTF-8 string.
281 """
281 """
282 pass
282 pass
283
283
284 def hascommitfrommap(self, rev):
284 def hascommitfrommap(self, rev):
285 """Return False if a rev mentioned in a filemap is known to not be
285 """Return False if a rev mentioned in a filemap is known to not be
286 present."""
286 present."""
287 raise NotImplementedError
287 raise NotImplementedError
288
288
289 def hascommitforsplicemap(self, rev):
289 def hascommitforsplicemap(self, rev):
290 """This method is for the special needs for splicemap handling and not
290 """This method is for the special needs for splicemap handling and not
291 for general use. Returns True if the sink contains rev, aborts on some
291 for general use. Returns True if the sink contains rev, aborts on some
292 special cases."""
292 special cases."""
293 raise NotImplementedError
293 raise NotImplementedError
294
294
295 class commandline(object):
295 class commandline(object):
296 def __init__(self, ui, command):
296 def __init__(self, ui, command):
297 self.ui = ui
297 self.ui = ui
298 self.command = command
298 self.command = command
299
299
300 def prerun(self):
300 def prerun(self):
301 pass
301 pass
302
302
303 def postrun(self):
303 def postrun(self):
304 pass
304 pass
305
305
306 def _cmdline(self, cmd, *args, **kwargs):
306 def _cmdline(self, cmd, *args, **kwargs):
307 cmdline = [self.command, cmd] + list(args)
307 cmdline = [self.command, cmd] + list(args)
308 for k, v in kwargs.iteritems():
308 for k, v in kwargs.iteritems():
309 if len(k) == 1:
309 if len(k) == 1:
310 cmdline.append('-' + k)
310 cmdline.append('-' + k)
311 else:
311 else:
312 cmdline.append('--' + k.replace('_', '-'))
312 cmdline.append('--' + k.replace('_', '-'))
313 try:
313 try:
314 if len(k) == 1:
314 if len(k) == 1:
315 cmdline.append('' + v)
315 cmdline.append('' + v)
316 else:
316 else:
317 cmdline[-1] += '=' + v
317 cmdline[-1] += '=' + v
318 except TypeError:
318 except TypeError:
319 pass
319 pass
320 cmdline = [util.shellquote(arg) for arg in cmdline]
320 cmdline = [util.shellquote(arg) for arg in cmdline]
321 if not self.ui.debugflag:
321 if not self.ui.debugflag:
322 cmdline += ['2>', os.devnull]
322 cmdline += ['2>', os.devnull]
323 cmdline = ' '.join(cmdline)
323 cmdline = ' '.join(cmdline)
324 return cmdline
324 return cmdline
325
325
326 def _run(self, cmd, *args, **kwargs):
326 def _run(self, cmd, *args, **kwargs):
327 def popen(cmdline):
327 def popen(cmdline):
328 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
328 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
329 close_fds=util.closefds,
329 close_fds=util.closefds,
330 stdout=subprocess.PIPE)
330 stdout=subprocess.PIPE)
331 return p
331 return p
332 return self._dorun(popen, cmd, *args, **kwargs)
332 return self._dorun(popen, cmd, *args, **kwargs)
333
333
334 def _run2(self, cmd, *args, **kwargs):
334 def _run2(self, cmd, *args, **kwargs):
335 return self._dorun(util.popen2, cmd, *args, **kwargs)
335 return self._dorun(util.popen2, cmd, *args, **kwargs)
336
336
337 def _dorun(self, openfunc, cmd, *args, **kwargs):
337 def _dorun(self, openfunc, cmd, *args, **kwargs):
338 cmdline = self._cmdline(cmd, *args, **kwargs)
338 cmdline = self._cmdline(cmd, *args, **kwargs)
339 self.ui.debug('running: %s\n' % (cmdline,))
339 self.ui.debug('running: %s\n' % (cmdline,))
340 self.prerun()
340 self.prerun()
341 try:
341 try:
342 return openfunc(cmdline)
342 return openfunc(cmdline)
343 finally:
343 finally:
344 self.postrun()
344 self.postrun()
345
345
346 def run(self, cmd, *args, **kwargs):
346 def run(self, cmd, *args, **kwargs):
347 p = self._run(cmd, *args, **kwargs)
347 p = self._run(cmd, *args, **kwargs)
348 output = p.communicate()[0]
348 output = p.communicate()[0]
349 self.ui.debug(output)
349 self.ui.debug(output)
350 return output, p.returncode
350 return output, p.returncode
351
351
352 def runlines(self, cmd, *args, **kwargs):
352 def runlines(self, cmd, *args, **kwargs):
353 p = self._run(cmd, *args, **kwargs)
353 p = self._run(cmd, *args, **kwargs)
354 output = p.stdout.readlines()
354 output = p.stdout.readlines()
355 p.wait()
355 p.wait()
356 self.ui.debug(''.join(output))
356 self.ui.debug(''.join(output))
357 return output, p.returncode
357 return output, p.returncode
358
358
359 def checkexit(self, status, output=''):
359 def checkexit(self, status, output=''):
360 if status:
360 if status:
361 if output:
361 if output:
362 self.ui.warn(_('%s error:\n') % self.command)
362 self.ui.warn(_('%s error:\n') % self.command)
363 self.ui.warn(output)
363 self.ui.warn(output)
364 msg = util.explainexit(status)[0]
364 msg = util.explainexit(status)[0]
365 raise util.Abort('%s %s' % (self.command, msg))
365 raise util.Abort('%s %s' % (self.command, msg))
366
366
367 def run0(self, cmd, *args, **kwargs):
367 def run0(self, cmd, *args, **kwargs):
368 output, status = self.run(cmd, *args, **kwargs)
368 output, status = self.run(cmd, *args, **kwargs)
369 self.checkexit(status, output)
369 self.checkexit(status, output)
370 return output
370 return output
371
371
372 def runlines0(self, cmd, *args, **kwargs):
372 def runlines0(self, cmd, *args, **kwargs):
373 output, status = self.runlines(cmd, *args, **kwargs)
373 output, status = self.runlines(cmd, *args, **kwargs)
374 self.checkexit(status, ''.join(output))
374 self.checkexit(status, ''.join(output))
375 return output
375 return output
376
376
377 @propertycache
377 @propertycache
378 def argmax(self):
378 def argmax(self):
379 # POSIX requires at least 4096 bytes for ARG_MAX
379 # POSIX requires at least 4096 bytes for ARG_MAX
380 argmax = 4096
380 argmax = 4096
381 try:
381 try:
382 argmax = os.sysconf("SC_ARG_MAX")
382 argmax = os.sysconf("SC_ARG_MAX")
383 except (AttributeError, ValueError):
383 except (AttributeError, ValueError):
384 pass
384 pass
385
385
386 # Windows shells impose their own limits on command line length,
386 # Windows shells impose their own limits on command line length,
387 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
387 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
388 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
388 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
389 # details about cmd.exe limitations.
389 # details about cmd.exe limitations.
390
390
391 # Since ARG_MAX is for command line _and_ environment, lower our limit
391 # Since ARG_MAX is for command line _and_ environment, lower our limit
392 # (and make happy Windows shells while doing this).
392 # (and make happy Windows shells while doing this).
393 return argmax // 2 - 1
393 return argmax // 2 - 1
394
394
395 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
395 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
396 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
396 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
397 limit = self.argmax - cmdlen
397 limit = self.argmax - cmdlen
398 bytes = 0
398 bytes = 0
399 fl = []
399 fl = []
400 for fn in arglist:
400 for fn in arglist:
401 b = len(fn) + 3
401 b = len(fn) + 3
402 if bytes + b < limit or len(fl) == 0:
402 if bytes + b < limit or len(fl) == 0:
403 fl.append(fn)
403 fl.append(fn)
404 bytes += b
404 bytes += b
405 else:
405 else:
406 yield fl
406 yield fl
407 fl = [fn]
407 fl = [fn]
408 bytes = b
408 bytes = b
409 if fl:
409 if fl:
410 yield fl
410 yield fl
411
411
412 def xargs(self, arglist, cmd, *args, **kwargs):
412 def xargs(self, arglist, cmd, *args, **kwargs):
413 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
413 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
414 self.run0(cmd, *(list(args) + l), **kwargs)
414 self.run0(cmd, *(list(args) + l), **kwargs)
415
415
416 class mapfile(dict):
416 class mapfile(dict):
417 def __init__(self, ui, path):
417 def __init__(self, ui, path):
418 super(mapfile, self).__init__()
418 super(mapfile, self).__init__()
419 self.ui = ui
419 self.ui = ui
420 self.path = path
420 self.path = path
421 self.fp = None
421 self.fp = None
422 self.order = []
422 self.order = []
423 self._read()
423 self._read()
424
424
425 def _read(self):
425 def _read(self):
426 if not self.path:
426 if not self.path:
427 return
427 return
428 try:
428 try:
429 fp = open(self.path, 'r')
429 fp = open(self.path, 'r')
430 except IOError as err:
430 except IOError as err:
431 if err.errno != errno.ENOENT:
431 if err.errno != errno.ENOENT:
432 raise
432 raise
433 return
433 return
434 for i, line in enumerate(fp):
434 for i, line in enumerate(fp):
435 line = line.splitlines()[0].rstrip()
435 line = line.splitlines()[0].rstrip()
436 if not line:
436 if not line:
437 # Ignore blank lines
437 # Ignore blank lines
438 continue
438 continue
439 try:
439 try:
440 key, value = line.rsplit(' ', 1)
440 key, value = line.rsplit(' ', 1)
441 except ValueError:
441 except ValueError:
442 raise util.Abort(
442 raise util.Abort(
443 _('syntax error in %s(%d): key/value pair expected')
443 _('syntax error in %s(%d): key/value pair expected')
444 % (self.path, i + 1))
444 % (self.path, i + 1))
445 if key not in self:
445 if key not in self:
446 self.order.append(key)
446 self.order.append(key)
447 super(mapfile, self).__setitem__(key, value)
447 super(mapfile, self).__setitem__(key, value)
448 fp.close()
448 fp.close()
449
449
450 def __setitem__(self, key, value):
450 def __setitem__(self, key, value):
451 if self.fp is None:
451 if self.fp is None:
452 try:
452 try:
453 self.fp = open(self.path, 'a')
453 self.fp = open(self.path, 'a')
454 except IOError as err:
454 except IOError as err:
455 raise util.Abort(_('could not open map file %r: %s') %
455 raise util.Abort(_('could not open map file %r: %s') %
456 (self.path, err.strerror))
456 (self.path, err.strerror))
457 self.fp.write('%s %s\n' % (key, value))
457 self.fp.write('%s %s\n' % (key, value))
458 self.fp.flush()
458 self.fp.flush()
459 super(mapfile, self).__setitem__(key, value)
459 super(mapfile, self).__setitem__(key, value)
460
460
461 def close(self):
461 def close(self):
462 if self.fp:
462 if self.fp:
463 self.fp.close()
463 self.fp.close()
464 self.fp = None
464 self.fp = None
465
465
466 def makedatetimestamp(t):
466 def makedatetimestamp(t):
467 """Like util.makedate() but for time t instead of current time"""
467 """Like util.makedate() but for time t instead of current time"""
468 delta = (datetime.datetime.utcfromtimestamp(t) -
468 delta = (datetime.datetime.utcfromtimestamp(t) -
469 datetime.datetime.fromtimestamp(t))
469 datetime.datetime.fromtimestamp(t))
470 tz = delta.days * 86400 + delta.seconds
470 tz = delta.days * 86400 + delta.seconds
471 return t, tz
471 return t, tz
@@ -1,548 +1,548
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
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 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import svn_source, svn_sink
13 from subversion import svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 from bzr import bzr_source
16 from bzr import bzr_source
17 from p4 import p4_source
17 from p4 import p4_source
18 import filemap
18 import filemap
19
19
20 import os, shutil, shlex
20 import os, shutil, shlex
21 from mercurial import hg, util, encoding
21 from mercurial import hg, util, encoding
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23
23
24 orig_encoding = 'ascii'
24 orig_encoding = 'ascii'
25
25
26 def recode(s):
26 def recode(s):
27 if isinstance(s, unicode):
27 if isinstance(s, unicode):
28 return s.encode(orig_encoding, 'replace')
28 return s.encode(orig_encoding, 'replace')
29 else:
29 else:
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31
31
32 source_converters = [
32 source_converters = [
33 ('cvs', convert_cvs, 'branchsort'),
33 ('cvs', convert_cvs, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
34 ('git', convert_git, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
35 ('svn', svn_source, 'branchsort'),
36 ('hg', mercurial_source, 'sourcesort'),
36 ('hg', mercurial_source, 'sourcesort'),
37 ('darcs', darcs_source, 'branchsort'),
37 ('darcs', darcs_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
38 ('mtn', monotone_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
39 ('gnuarch', gnuarch_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
40 ('bzr', bzr_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
41 ('p4', p4_source, 'branchsort'),
42 ]
42 ]
43
43
44 sink_converters = [
44 sink_converters = [
45 ('hg', mercurial_sink),
45 ('hg', mercurial_sink),
46 ('svn', svn_sink),
46 ('svn', svn_sink),
47 ]
47 ]
48
48
49 def convertsource(ui, path, type, rev):
49 def convertsource(ui, path, type, revs):
50 exceptions = []
50 exceptions = []
51 if type and type not in [s[0] for s in source_converters]:
51 if type and type not in [s[0] for s in source_converters]:
52 raise util.Abort(_('%s: invalid source repository type') % type)
52 raise util.Abort(_('%s: invalid source repository type') % type)
53 for name, source, sortmode in source_converters:
53 for name, source, sortmode in source_converters:
54 try:
54 try:
55 if not type or name == type:
55 if not type or name == type:
56 return source(ui, path, rev), sortmode
56 return source(ui, path, revs), sortmode
57 except (NoRepo, MissingTool) as inst:
57 except (NoRepo, MissingTool) as inst:
58 exceptions.append(inst)
58 exceptions.append(inst)
59 if not ui.quiet:
59 if not ui.quiet:
60 for inst in exceptions:
60 for inst in exceptions:
61 ui.write("%s\n" % inst)
61 ui.write("%s\n" % inst)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63
63
64 def convertsink(ui, path, type):
64 def convertsink(ui, path, type):
65 if type and type not in [s[0] for s in sink_converters]:
65 if type and type not in [s[0] for s in sink_converters]:
66 raise util.Abort(_('%s: invalid destination repository type') % type)
66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 for name, sink in sink_converters:
67 for name, sink in sink_converters:
68 try:
68 try:
69 if not type or name == type:
69 if not type or name == type:
70 return sink(ui, path)
70 return sink(ui, path)
71 except NoRepo as inst:
71 except NoRepo as inst:
72 ui.note(_("convert: %s\n") % inst)
72 ui.note(_("convert: %s\n") % inst)
73 except MissingTool as inst:
73 except MissingTool as inst:
74 raise util.Abort('%s\n' % inst)
74 raise util.Abort('%s\n' % inst)
75 raise util.Abort(_('%s: unknown repository type') % path)
75 raise util.Abort(_('%s: unknown repository type') % path)
76
76
77 class progresssource(object):
77 class progresssource(object):
78 def __init__(self, ui, source, filecount):
78 def __init__(self, ui, source, filecount):
79 self.ui = ui
79 self.ui = ui
80 self.source = source
80 self.source = source
81 self.filecount = filecount
81 self.filecount = filecount
82 self.retrieved = 0
82 self.retrieved = 0
83
83
84 def getfile(self, file, rev):
84 def getfile(self, file, rev):
85 self.retrieved += 1
85 self.retrieved += 1
86 self.ui.progress(_('getting files'), self.retrieved,
86 self.ui.progress(_('getting files'), self.retrieved,
87 item=file, total=self.filecount)
87 item=file, total=self.filecount)
88 return self.source.getfile(file, rev)
88 return self.source.getfile(file, rev)
89
89
90 def lookuprev(self, rev):
90 def lookuprev(self, rev):
91 return self.source.lookuprev(rev)
91 return self.source.lookuprev(rev)
92
92
93 def close(self):
93 def close(self):
94 self.ui.progress(_('getting files'), None)
94 self.ui.progress(_('getting files'), None)
95
95
96 class converter(object):
96 class converter(object):
97 def __init__(self, ui, source, dest, revmapfile, opts):
97 def __init__(self, ui, source, dest, revmapfile, opts):
98
98
99 self.source = source
99 self.source = source
100 self.dest = dest
100 self.dest = dest
101 self.ui = ui
101 self.ui = ui
102 self.opts = opts
102 self.opts = opts
103 self.commitcache = {}
103 self.commitcache = {}
104 self.authors = {}
104 self.authors = {}
105 self.authorfile = None
105 self.authorfile = None
106
106
107 # Record converted revisions persistently: maps source revision
107 # Record converted revisions persistently: maps source revision
108 # ID to target revision ID (both strings). (This is how
108 # ID to target revision ID (both strings). (This is how
109 # incremental conversions work.)
109 # incremental conversions work.)
110 self.map = mapfile(ui, revmapfile)
110 self.map = mapfile(ui, revmapfile)
111
111
112 # Read first the dst author map if any
112 # Read first the dst author map if any
113 authorfile = self.dest.authorfile()
113 authorfile = self.dest.authorfile()
114 if authorfile and os.path.exists(authorfile):
114 if authorfile and os.path.exists(authorfile):
115 self.readauthormap(authorfile)
115 self.readauthormap(authorfile)
116 # Extend/Override with new author map if necessary
116 # Extend/Override with new author map if necessary
117 if opts.get('authormap'):
117 if opts.get('authormap'):
118 self.readauthormap(opts.get('authormap'))
118 self.readauthormap(opts.get('authormap'))
119 self.authorfile = self.dest.authorfile()
119 self.authorfile = self.dest.authorfile()
120
120
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123
123
124 def parsesplicemap(self, path):
124 def parsesplicemap(self, path):
125 """ check and validate the splicemap format and
125 """ check and validate the splicemap format and
126 return a child/parents dictionary.
126 return a child/parents dictionary.
127 Format checking has two parts.
127 Format checking has two parts.
128 1. generic format which is same across all source types
128 1. generic format which is same across all source types
129 2. specific format checking which may be different for
129 2. specific format checking which may be different for
130 different source type. This logic is implemented in
130 different source type. This logic is implemented in
131 checkrevformat function in source files like
131 checkrevformat function in source files like
132 hg.py, subversion.py etc.
132 hg.py, subversion.py etc.
133 """
133 """
134
134
135 if not path:
135 if not path:
136 return {}
136 return {}
137 m = {}
137 m = {}
138 try:
138 try:
139 fp = open(path, 'r')
139 fp = open(path, 'r')
140 for i, line in enumerate(fp):
140 for i, line in enumerate(fp):
141 line = line.splitlines()[0].rstrip()
141 line = line.splitlines()[0].rstrip()
142 if not line:
142 if not line:
143 # Ignore blank lines
143 # Ignore blank lines
144 continue
144 continue
145 # split line
145 # split line
146 lex = shlex.shlex(line, posix=True)
146 lex = shlex.shlex(line, posix=True)
147 lex.whitespace_split = True
147 lex.whitespace_split = True
148 lex.whitespace += ','
148 lex.whitespace += ','
149 line = list(lex)
149 line = list(lex)
150 # check number of parents
150 # check number of parents
151 if not (2 <= len(line) <= 3):
151 if not (2 <= len(line) <= 3):
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 '[,parent2] expected') % (path, i + 1))
153 '[,parent2] expected') % (path, i + 1))
154 for part in line:
154 for part in line:
155 self.source.checkrevformat(part)
155 self.source.checkrevformat(part)
156 child, p1, p2 = line[0], line[1:2], line[2:]
156 child, p1, p2 = line[0], line[1:2], line[2:]
157 if p1 == p2:
157 if p1 == p2:
158 m[child] = p1
158 m[child] = p1
159 else:
159 else:
160 m[child] = p1 + p2
160 m[child] = p1 + p2
161 # if file does not exist or error reading, exit
161 # if file does not exist or error reading, exit
162 except IOError:
162 except IOError:
163 raise util.Abort(_('splicemap file not found or error reading %s:')
163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 % path)
164 % path)
165 return m
165 return m
166
166
167
167
168 def walktree(self, heads):
168 def walktree(self, heads):
169 '''Return a mapping that identifies the uncommitted parents of every
169 '''Return a mapping that identifies the uncommitted parents of every
170 uncommitted changeset.'''
170 uncommitted changeset.'''
171 visit = heads
171 visit = heads
172 known = set()
172 known = set()
173 parents = {}
173 parents = {}
174 numcommits = self.source.numcommits()
174 numcommits = self.source.numcommits()
175 while visit:
175 while visit:
176 n = visit.pop(0)
176 n = visit.pop(0)
177 if n in known:
177 if n in known:
178 continue
178 continue
179 if n in self.map:
179 if n in self.map:
180 m = self.map[n]
180 m = self.map[n]
181 if m == SKIPREV or self.dest.hascommitfrommap(m):
181 if m == SKIPREV or self.dest.hascommitfrommap(m):
182 continue
182 continue
183 known.add(n)
183 known.add(n)
184 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
184 self.ui.progress(_('scanning'), len(known), unit=_('revisions'),
185 total=numcommits)
185 total=numcommits)
186 commit = self.cachecommit(n)
186 commit = self.cachecommit(n)
187 parents[n] = []
187 parents[n] = []
188 for p in commit.parents:
188 for p in commit.parents:
189 parents[n].append(p)
189 parents[n].append(p)
190 visit.append(p)
190 visit.append(p)
191 self.ui.progress(_('scanning'), None)
191 self.ui.progress(_('scanning'), None)
192
192
193 return parents
193 return parents
194
194
195 def mergesplicemap(self, parents, splicemap):
195 def mergesplicemap(self, parents, splicemap):
196 """A splicemap redefines child/parent relationships. Check the
196 """A splicemap redefines child/parent relationships. Check the
197 map contains valid revision identifiers and merge the new
197 map contains valid revision identifiers and merge the new
198 links in the source graph.
198 links in the source graph.
199 """
199 """
200 for c in sorted(splicemap):
200 for c in sorted(splicemap):
201 if c not in parents:
201 if c not in parents:
202 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
202 if not self.dest.hascommitforsplicemap(self.map.get(c, c)):
203 # Could be in source but not converted during this run
203 # Could be in source but not converted during this run
204 self.ui.warn(_('splice map revision %s is not being '
204 self.ui.warn(_('splice map revision %s is not being '
205 'converted, ignoring\n') % c)
205 'converted, ignoring\n') % c)
206 continue
206 continue
207 pc = []
207 pc = []
208 for p in splicemap[c]:
208 for p in splicemap[c]:
209 # We do not have to wait for nodes already in dest.
209 # We do not have to wait for nodes already in dest.
210 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
210 if self.dest.hascommitforsplicemap(self.map.get(p, p)):
211 continue
211 continue
212 # Parent is not in dest and not being converted, not good
212 # Parent is not in dest and not being converted, not good
213 if p not in parents:
213 if p not in parents:
214 raise util.Abort(_('unknown splice map parent: %s') % p)
214 raise util.Abort(_('unknown splice map parent: %s') % p)
215 pc.append(p)
215 pc.append(p)
216 parents[c] = pc
216 parents[c] = pc
217
217
218 def toposort(self, parents, sortmode):
218 def toposort(self, parents, sortmode):
219 '''Return an ordering such that every uncommitted changeset is
219 '''Return an ordering such that every uncommitted changeset is
220 preceded by all its uncommitted ancestors.'''
220 preceded by all its uncommitted ancestors.'''
221
221
222 def mapchildren(parents):
222 def mapchildren(parents):
223 """Return a (children, roots) tuple where 'children' maps parent
223 """Return a (children, roots) tuple where 'children' maps parent
224 revision identifiers to children ones, and 'roots' is the list of
224 revision identifiers to children ones, and 'roots' is the list of
225 revisions without parents. 'parents' must be a mapping of revision
225 revisions without parents. 'parents' must be a mapping of revision
226 identifier to its parents ones.
226 identifier to its parents ones.
227 """
227 """
228 visit = sorted(parents)
228 visit = sorted(parents)
229 seen = set()
229 seen = set()
230 children = {}
230 children = {}
231 roots = []
231 roots = []
232
232
233 while visit:
233 while visit:
234 n = visit.pop(0)
234 n = visit.pop(0)
235 if n in seen:
235 if n in seen:
236 continue
236 continue
237 seen.add(n)
237 seen.add(n)
238 # Ensure that nodes without parents are present in the
238 # Ensure that nodes without parents are present in the
239 # 'children' mapping.
239 # 'children' mapping.
240 children.setdefault(n, [])
240 children.setdefault(n, [])
241 hasparent = False
241 hasparent = False
242 for p in parents[n]:
242 for p in parents[n]:
243 if p not in self.map:
243 if p not in self.map:
244 visit.append(p)
244 visit.append(p)
245 hasparent = True
245 hasparent = True
246 children.setdefault(p, []).append(n)
246 children.setdefault(p, []).append(n)
247 if not hasparent:
247 if not hasparent:
248 roots.append(n)
248 roots.append(n)
249
249
250 return children, roots
250 return children, roots
251
251
252 # Sort functions are supposed to take a list of revisions which
252 # Sort functions are supposed to take a list of revisions which
253 # can be converted immediately and pick one
253 # can be converted immediately and pick one
254
254
255 def makebranchsorter():
255 def makebranchsorter():
256 """If the previously converted revision has a child in the
256 """If the previously converted revision has a child in the
257 eligible revisions list, pick it. Return the list head
257 eligible revisions list, pick it. Return the list head
258 otherwise. Branch sort attempts to minimize branch
258 otherwise. Branch sort attempts to minimize branch
259 switching, which is harmful for Mercurial backend
259 switching, which is harmful for Mercurial backend
260 compression.
260 compression.
261 """
261 """
262 prev = [None]
262 prev = [None]
263 def picknext(nodes):
263 def picknext(nodes):
264 next = nodes[0]
264 next = nodes[0]
265 for n in nodes:
265 for n in nodes:
266 if prev[0] in parents[n]:
266 if prev[0] in parents[n]:
267 next = n
267 next = n
268 break
268 break
269 prev[0] = next
269 prev[0] = next
270 return next
270 return next
271 return picknext
271 return picknext
272
272
273 def makesourcesorter():
273 def makesourcesorter():
274 """Source specific sort."""
274 """Source specific sort."""
275 keyfn = lambda n: self.commitcache[n].sortkey
275 keyfn = lambda n: self.commitcache[n].sortkey
276 def picknext(nodes):
276 def picknext(nodes):
277 return sorted(nodes, key=keyfn)[0]
277 return sorted(nodes, key=keyfn)[0]
278 return picknext
278 return picknext
279
279
280 def makeclosesorter():
280 def makeclosesorter():
281 """Close order sort."""
281 """Close order sort."""
282 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
282 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
283 self.commitcache[n].sortkey)
283 self.commitcache[n].sortkey)
284 def picknext(nodes):
284 def picknext(nodes):
285 return sorted(nodes, key=keyfn)[0]
285 return sorted(nodes, key=keyfn)[0]
286 return picknext
286 return picknext
287
287
288 def makedatesorter():
288 def makedatesorter():
289 """Sort revisions by date."""
289 """Sort revisions by date."""
290 dates = {}
290 dates = {}
291 def getdate(n):
291 def getdate(n):
292 if n not in dates:
292 if n not in dates:
293 dates[n] = util.parsedate(self.commitcache[n].date)
293 dates[n] = util.parsedate(self.commitcache[n].date)
294 return dates[n]
294 return dates[n]
295
295
296 def picknext(nodes):
296 def picknext(nodes):
297 return min([(getdate(n), n) for n in nodes])[1]
297 return min([(getdate(n), n) for n in nodes])[1]
298
298
299 return picknext
299 return picknext
300
300
301 if sortmode == 'branchsort':
301 if sortmode == 'branchsort':
302 picknext = makebranchsorter()
302 picknext = makebranchsorter()
303 elif sortmode == 'datesort':
303 elif sortmode == 'datesort':
304 picknext = makedatesorter()
304 picknext = makedatesorter()
305 elif sortmode == 'sourcesort':
305 elif sortmode == 'sourcesort':
306 picknext = makesourcesorter()
306 picknext = makesourcesorter()
307 elif sortmode == 'closesort':
307 elif sortmode == 'closesort':
308 picknext = makeclosesorter()
308 picknext = makeclosesorter()
309 else:
309 else:
310 raise util.Abort(_('unknown sort mode: %s') % sortmode)
310 raise util.Abort(_('unknown sort mode: %s') % sortmode)
311
311
312 children, actives = mapchildren(parents)
312 children, actives = mapchildren(parents)
313
313
314 s = []
314 s = []
315 pendings = {}
315 pendings = {}
316 while actives:
316 while actives:
317 n = picknext(actives)
317 n = picknext(actives)
318 actives.remove(n)
318 actives.remove(n)
319 s.append(n)
319 s.append(n)
320
320
321 # Update dependents list
321 # Update dependents list
322 for c in children.get(n, []):
322 for c in children.get(n, []):
323 if c not in pendings:
323 if c not in pendings:
324 pendings[c] = [p for p in parents[c] if p not in self.map]
324 pendings[c] = [p for p in parents[c] if p not in self.map]
325 try:
325 try:
326 pendings[c].remove(n)
326 pendings[c].remove(n)
327 except ValueError:
327 except ValueError:
328 raise util.Abort(_('cycle detected between %s and %s')
328 raise util.Abort(_('cycle detected between %s and %s')
329 % (recode(c), recode(n)))
329 % (recode(c), recode(n)))
330 if not pendings[c]:
330 if not pendings[c]:
331 # Parents are converted, node is eligible
331 # Parents are converted, node is eligible
332 actives.insert(0, c)
332 actives.insert(0, c)
333 pendings[c] = None
333 pendings[c] = None
334
334
335 if len(s) != len(parents):
335 if len(s) != len(parents):
336 raise util.Abort(_("not all revisions were sorted"))
336 raise util.Abort(_("not all revisions were sorted"))
337
337
338 return s
338 return s
339
339
340 def writeauthormap(self):
340 def writeauthormap(self):
341 authorfile = self.authorfile
341 authorfile = self.authorfile
342 if authorfile:
342 if authorfile:
343 self.ui.status(_('writing author map file %s\n') % authorfile)
343 self.ui.status(_('writing author map file %s\n') % authorfile)
344 ofile = open(authorfile, 'w+')
344 ofile = open(authorfile, 'w+')
345 for author in self.authors:
345 for author in self.authors:
346 ofile.write("%s=%s\n" % (author, self.authors[author]))
346 ofile.write("%s=%s\n" % (author, self.authors[author]))
347 ofile.close()
347 ofile.close()
348
348
349 def readauthormap(self, authorfile):
349 def readauthormap(self, authorfile):
350 afile = open(authorfile, 'r')
350 afile = open(authorfile, 'r')
351 for line in afile:
351 for line in afile:
352
352
353 line = line.strip()
353 line = line.strip()
354 if not line or line.startswith('#'):
354 if not line or line.startswith('#'):
355 continue
355 continue
356
356
357 try:
357 try:
358 srcauthor, dstauthor = line.split('=', 1)
358 srcauthor, dstauthor = line.split('=', 1)
359 except ValueError:
359 except ValueError:
360 msg = _('ignoring bad line in author map file %s: %s\n')
360 msg = _('ignoring bad line in author map file %s: %s\n')
361 self.ui.warn(msg % (authorfile, line.rstrip()))
361 self.ui.warn(msg % (authorfile, line.rstrip()))
362 continue
362 continue
363
363
364 srcauthor = srcauthor.strip()
364 srcauthor = srcauthor.strip()
365 dstauthor = dstauthor.strip()
365 dstauthor = dstauthor.strip()
366 if self.authors.get(srcauthor) in (None, dstauthor):
366 if self.authors.get(srcauthor) in (None, dstauthor):
367 msg = _('mapping author %s to %s\n')
367 msg = _('mapping author %s to %s\n')
368 self.ui.debug(msg % (srcauthor, dstauthor))
368 self.ui.debug(msg % (srcauthor, dstauthor))
369 self.authors[srcauthor] = dstauthor
369 self.authors[srcauthor] = dstauthor
370 continue
370 continue
371
371
372 m = _('overriding mapping for author %s, was %s, will be %s\n')
372 m = _('overriding mapping for author %s, was %s, will be %s\n')
373 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
373 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
374
374
375 afile.close()
375 afile.close()
376
376
377 def cachecommit(self, rev):
377 def cachecommit(self, rev):
378 commit = self.source.getcommit(rev)
378 commit = self.source.getcommit(rev)
379 commit.author = self.authors.get(commit.author, commit.author)
379 commit.author = self.authors.get(commit.author, commit.author)
380 # If commit.branch is None, this commit is coming from the source
380 # If commit.branch is None, this commit is coming from the source
381 # repository's default branch and destined for the default branch in the
381 # repository's default branch and destined for the default branch in the
382 # destination repository. For such commits, passing a literal "None"
382 # destination repository. For such commits, passing a literal "None"
383 # string to branchmap.get() below allows the user to map "None" to an
383 # string to branchmap.get() below allows the user to map "None" to an
384 # alternate default branch in the destination repository.
384 # alternate default branch in the destination repository.
385 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
385 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
386 self.commitcache[rev] = commit
386 self.commitcache[rev] = commit
387 return commit
387 return commit
388
388
389 def copy(self, rev):
389 def copy(self, rev):
390 commit = self.commitcache[rev]
390 commit = self.commitcache[rev]
391 full = self.opts.get('full')
391 full = self.opts.get('full')
392 changes = self.source.getchanges(rev, full)
392 changes = self.source.getchanges(rev, full)
393 if isinstance(changes, basestring):
393 if isinstance(changes, basestring):
394 if changes == SKIPREV:
394 if changes == SKIPREV:
395 dest = SKIPREV
395 dest = SKIPREV
396 else:
396 else:
397 dest = self.map[changes]
397 dest = self.map[changes]
398 self.map[rev] = dest
398 self.map[rev] = dest
399 return
399 return
400 files, copies, cleanp2 = changes
400 files, copies, cleanp2 = changes
401 pbranches = []
401 pbranches = []
402 if commit.parents:
402 if commit.parents:
403 for prev in commit.parents:
403 for prev in commit.parents:
404 if prev not in self.commitcache:
404 if prev not in self.commitcache:
405 self.cachecommit(prev)
405 self.cachecommit(prev)
406 pbranches.append((self.map[prev],
406 pbranches.append((self.map[prev],
407 self.commitcache[prev].branch))
407 self.commitcache[prev].branch))
408 self.dest.setbranch(commit.branch, pbranches)
408 self.dest.setbranch(commit.branch, pbranches)
409 try:
409 try:
410 parents = self.splicemap[rev]
410 parents = self.splicemap[rev]
411 self.ui.status(_('spliced in %s as parents of %s\n') %
411 self.ui.status(_('spliced in %s as parents of %s\n') %
412 (parents, rev))
412 (parents, rev))
413 parents = [self.map.get(p, p) for p in parents]
413 parents = [self.map.get(p, p) for p in parents]
414 except KeyError:
414 except KeyError:
415 parents = [b[0] for b in pbranches]
415 parents = [b[0] for b in pbranches]
416 if len(pbranches) != 2:
416 if len(pbranches) != 2:
417 cleanp2 = set()
417 cleanp2 = set()
418 if len(parents) < 3:
418 if len(parents) < 3:
419 source = progresssource(self.ui, self.source, len(files))
419 source = progresssource(self.ui, self.source, len(files))
420 else:
420 else:
421 # For an octopus merge, we end up traversing the list of
421 # For an octopus merge, we end up traversing the list of
422 # changed files N-1 times. This tweak to the number of
422 # changed files N-1 times. This tweak to the number of
423 # files makes it so the progress bar doesn't overflow
423 # files makes it so the progress bar doesn't overflow
424 # itself.
424 # itself.
425 source = progresssource(self.ui, self.source,
425 source = progresssource(self.ui, self.source,
426 len(files) * (len(parents) - 1))
426 len(files) * (len(parents) - 1))
427 newnode = self.dest.putcommit(files, copies, parents, commit,
427 newnode = self.dest.putcommit(files, copies, parents, commit,
428 source, self.map, full, cleanp2)
428 source, self.map, full, cleanp2)
429 source.close()
429 source.close()
430 self.source.converted(rev, newnode)
430 self.source.converted(rev, newnode)
431 self.map[rev] = newnode
431 self.map[rev] = newnode
432
432
433 def convert(self, sortmode):
433 def convert(self, sortmode):
434 try:
434 try:
435 self.source.before()
435 self.source.before()
436 self.dest.before()
436 self.dest.before()
437 self.source.setrevmap(self.map)
437 self.source.setrevmap(self.map)
438 self.ui.status(_("scanning source...\n"))
438 self.ui.status(_("scanning source...\n"))
439 heads = self.source.getheads()
439 heads = self.source.getheads()
440 parents = self.walktree(heads)
440 parents = self.walktree(heads)
441 self.mergesplicemap(parents, self.splicemap)
441 self.mergesplicemap(parents, self.splicemap)
442 self.ui.status(_("sorting...\n"))
442 self.ui.status(_("sorting...\n"))
443 t = self.toposort(parents, sortmode)
443 t = self.toposort(parents, sortmode)
444 num = len(t)
444 num = len(t)
445 c = None
445 c = None
446
446
447 self.ui.status(_("converting...\n"))
447 self.ui.status(_("converting...\n"))
448 for i, c in enumerate(t):
448 for i, c in enumerate(t):
449 num -= 1
449 num -= 1
450 desc = self.commitcache[c].desc
450 desc = self.commitcache[c].desc
451 if "\n" in desc:
451 if "\n" in desc:
452 desc = desc.splitlines()[0]
452 desc = desc.splitlines()[0]
453 # convert log message to local encoding without using
453 # convert log message to local encoding without using
454 # tolocal() because the encoding.encoding convert()
454 # tolocal() because the encoding.encoding convert()
455 # uses is 'utf-8'
455 # uses is 'utf-8'
456 self.ui.status("%d %s\n" % (num, recode(desc)))
456 self.ui.status("%d %s\n" % (num, recode(desc)))
457 self.ui.note(_("source: %s\n") % recode(c))
457 self.ui.note(_("source: %s\n") % recode(c))
458 self.ui.progress(_('converting'), i, unit=_('revisions'),
458 self.ui.progress(_('converting'), i, unit=_('revisions'),
459 total=len(t))
459 total=len(t))
460 self.copy(c)
460 self.copy(c)
461 self.ui.progress(_('converting'), None)
461 self.ui.progress(_('converting'), None)
462
462
463 if not self.ui.configbool('convert', 'skiptags'):
463 if not self.ui.configbool('convert', 'skiptags'):
464 tags = self.source.gettags()
464 tags = self.source.gettags()
465 ctags = {}
465 ctags = {}
466 for k in tags:
466 for k in tags:
467 v = tags[k]
467 v = tags[k]
468 if self.map.get(v, SKIPREV) != SKIPREV:
468 if self.map.get(v, SKIPREV) != SKIPREV:
469 ctags[k] = self.map[v]
469 ctags[k] = self.map[v]
470
470
471 if c and ctags:
471 if c and ctags:
472 nrev, tagsparent = self.dest.puttags(ctags)
472 nrev, tagsparent = self.dest.puttags(ctags)
473 if nrev and tagsparent:
473 if nrev and tagsparent:
474 # write another hash correspondence to override the
474 # write another hash correspondence to override the
475 # previous one so we don't end up with extra tag heads
475 # previous one so we don't end up with extra tag heads
476 tagsparents = [e for e in self.map.iteritems()
476 tagsparents = [e for e in self.map.iteritems()
477 if e[1] == tagsparent]
477 if e[1] == tagsparent]
478 if tagsparents:
478 if tagsparents:
479 self.map[tagsparents[0][0]] = nrev
479 self.map[tagsparents[0][0]] = nrev
480
480
481 bookmarks = self.source.getbookmarks()
481 bookmarks = self.source.getbookmarks()
482 cbookmarks = {}
482 cbookmarks = {}
483 for k in bookmarks:
483 for k in bookmarks:
484 v = bookmarks[k]
484 v = bookmarks[k]
485 if self.map.get(v, SKIPREV) != SKIPREV:
485 if self.map.get(v, SKIPREV) != SKIPREV:
486 cbookmarks[k] = self.map[v]
486 cbookmarks[k] = self.map[v]
487
487
488 if c and cbookmarks:
488 if c and cbookmarks:
489 self.dest.putbookmarks(cbookmarks)
489 self.dest.putbookmarks(cbookmarks)
490
490
491 self.writeauthormap()
491 self.writeauthormap()
492 finally:
492 finally:
493 self.cleanup()
493 self.cleanup()
494
494
495 def cleanup(self):
495 def cleanup(self):
496 try:
496 try:
497 self.dest.after()
497 self.dest.after()
498 finally:
498 finally:
499 self.source.after()
499 self.source.after()
500 self.map.close()
500 self.map.close()
501
501
502 def convert(ui, src, dest=None, revmapfile=None, **opts):
502 def convert(ui, src, dest=None, revmapfile=None, **opts):
503 global orig_encoding
503 global orig_encoding
504 orig_encoding = encoding.encoding
504 orig_encoding = encoding.encoding
505 encoding.encoding = 'UTF-8'
505 encoding.encoding = 'UTF-8'
506
506
507 # support --authors as an alias for --authormap
507 # support --authors as an alias for --authormap
508 if not opts.get('authormap'):
508 if not opts.get('authormap'):
509 opts['authormap'] = opts.get('authors')
509 opts['authormap'] = opts.get('authors')
510
510
511 if not dest:
511 if not dest:
512 dest = hg.defaultdest(src) + "-hg"
512 dest = hg.defaultdest(src) + "-hg"
513 ui.status(_("assuming destination %s\n") % dest)
513 ui.status(_("assuming destination %s\n") % dest)
514
514
515 destc = convertsink(ui, dest, opts.get('dest_type'))
515 destc = convertsink(ui, dest, opts.get('dest_type'))
516
516
517 try:
517 try:
518 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
518 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
519 opts.get('rev'))
519 opts.get('rev'))
520 except Exception:
520 except Exception:
521 for path in destc.created:
521 for path in destc.created:
522 shutil.rmtree(path, True)
522 shutil.rmtree(path, True)
523 raise
523 raise
524
524
525 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
525 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
526 sortmode = [m for m in sortmodes if opts.get(m)]
526 sortmode = [m for m in sortmodes if opts.get(m)]
527 if len(sortmode) > 1:
527 if len(sortmode) > 1:
528 raise util.Abort(_('more than one sort mode specified'))
528 raise util.Abort(_('more than one sort mode specified'))
529 if sortmode:
529 if sortmode:
530 sortmode = sortmode[0]
530 sortmode = sortmode[0]
531 else:
531 else:
532 sortmode = defaultsort
532 sortmode = defaultsort
533
533
534 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
534 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
535 raise util.Abort(_('--sourcesort is not supported by this data source'))
535 raise util.Abort(_('--sourcesort is not supported by this data source'))
536 if sortmode == 'closesort' and not srcc.hasnativeclose():
536 if sortmode == 'closesort' and not srcc.hasnativeclose():
537 raise util.Abort(_('--closesort is not supported by this data source'))
537 raise util.Abort(_('--closesort is not supported by this data source'))
538
538
539 fmap = opts.get('filemap')
539 fmap = opts.get('filemap')
540 if fmap:
540 if fmap:
541 srcc = filemap.filemap_source(ui, srcc, fmap)
541 srcc = filemap.filemap_source(ui, srcc, fmap)
542 destc.setfilemapmode(True)
542 destc.setfilemapmode(True)
543
543
544 if not revmapfile:
544 if not revmapfile:
545 revmapfile = destc.revmapfile()
545 revmapfile = destc.revmapfile()
546
546
547 c = converter(ui, srcc, destc, revmapfile, opts)
547 c = converter(ui, srcc, destc, revmapfile, opts)
548 c.convert(sortmode)
548 c.convert(sortmode)
@@ -1,277 +1,280
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 os, re, socket, errno
8 import os, re, socket, errno
9 from cStringIO import StringIO
9 from cStringIO import StringIO
10 from mercurial import encoding, util
10 from mercurial import encoding, util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 from common import NoRepo, commit, converter_source, checktool
13 from common import NoRepo, commit, converter_source, checktool
14 from common import makedatetimestamp
14 from common import makedatetimestamp
15 import cvsps
15 import cvsps
16
16
17 class convert_cvs(converter_source):
17 class convert_cvs(converter_source):
18 def __init__(self, ui, path, rev=None):
18 def __init__(self, ui, path, revs=None):
19 super(convert_cvs, self).__init__(ui, path, rev=rev)
19 super(convert_cvs, self).__init__(ui, path, revs=revs)
20
20
21 cvs = os.path.join(path, "CVS")
21 cvs = os.path.join(path, "CVS")
22 if not os.path.exists(cvs):
22 if not os.path.exists(cvs):
23 raise NoRepo(_("%s does not look like a CVS checkout") % path)
23 raise NoRepo(_("%s does not look like a CVS checkout") % path)
24
24
25 checktool('cvs')
25 checktool('cvs')
26
26
27 self.changeset = None
27 self.changeset = None
28 self.files = {}
28 self.files = {}
29 self.tags = {}
29 self.tags = {}
30 self.lastbranch = {}
30 self.lastbranch = {}
31 self.socket = None
31 self.socket = None
32 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
33 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
34 self.encoding = encoding.encoding
34 self.encoding = encoding.encoding
35
35
36 self._connect()
36 self._connect()
37
37
38 def _parse(self):
38 def _parse(self):
39 if self.changeset is not None:
39 if self.changeset is not None:
40 return
40 return
41 self.changeset = {}
41 self.changeset = {}
42
42
43 maxrev = 0
43 maxrev = 0
44 if self.rev:
44 if self.revs:
45 if len(self.revs) > 1:
46 raise util.Abort(_('cvs source does not support specifying '
47 'multiple revs'))
45 # TODO: handle tags
48 # TODO: handle tags
46 try:
49 try:
47 # patchset number?
50 # patchset number?
48 maxrev = int(self.rev)
51 maxrev = int(self.revs[0])
49 except ValueError:
52 except ValueError:
50 raise util.Abort(_('revision %s is not a patchset number')
53 raise util.Abort(_('revision %s is not a patchset number')
51 % self.rev)
54 % self.revs[0])
52
55
53 d = os.getcwd()
56 d = os.getcwd()
54 try:
57 try:
55 os.chdir(self.path)
58 os.chdir(self.path)
56 id = None
59 id = None
57
60
58 cache = 'update'
61 cache = 'update'
59 if not self.ui.configbool('convert', 'cvsps.cache', True):
62 if not self.ui.configbool('convert', 'cvsps.cache', True):
60 cache = None
63 cache = None
61 db = cvsps.createlog(self.ui, cache=cache)
64 db = cvsps.createlog(self.ui, cache=cache)
62 db = cvsps.createchangeset(self.ui, db,
65 db = cvsps.createchangeset(self.ui, db,
63 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
66 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
64 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
67 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
65 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
68 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
66
69
67 for cs in db:
70 for cs in db:
68 if maxrev and cs.id > maxrev:
71 if maxrev and cs.id > maxrev:
69 break
72 break
70 id = str(cs.id)
73 id = str(cs.id)
71 cs.author = self.recode(cs.author)
74 cs.author = self.recode(cs.author)
72 self.lastbranch[cs.branch] = id
75 self.lastbranch[cs.branch] = id
73 cs.comment = self.recode(cs.comment)
76 cs.comment = self.recode(cs.comment)
74 if self.ui.configbool('convert', 'localtimezone'):
77 if self.ui.configbool('convert', 'localtimezone'):
75 cs.date = makedatetimestamp(cs.date[0])
78 cs.date = makedatetimestamp(cs.date[0])
76 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
79 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
77 self.tags.update(dict.fromkeys(cs.tags, id))
80 self.tags.update(dict.fromkeys(cs.tags, id))
78
81
79 files = {}
82 files = {}
80 for f in cs.entries:
83 for f in cs.entries:
81 files[f.file] = "%s%s" % ('.'.join([str(x)
84 files[f.file] = "%s%s" % ('.'.join([str(x)
82 for x in f.revision]),
85 for x in f.revision]),
83 ['', '(DEAD)'][f.dead])
86 ['', '(DEAD)'][f.dead])
84
87
85 # add current commit to set
88 # add current commit to set
86 c = commit(author=cs.author, date=date,
89 c = commit(author=cs.author, date=date,
87 parents=[str(p.id) for p in cs.parents],
90 parents=[str(p.id) for p in cs.parents],
88 desc=cs.comment, branch=cs.branch or '')
91 desc=cs.comment, branch=cs.branch or '')
89 self.changeset[id] = c
92 self.changeset[id] = c
90 self.files[id] = files
93 self.files[id] = files
91
94
92 self.heads = self.lastbranch.values()
95 self.heads = self.lastbranch.values()
93 finally:
96 finally:
94 os.chdir(d)
97 os.chdir(d)
95
98
96 def _connect(self):
99 def _connect(self):
97 root = self.cvsroot
100 root = self.cvsroot
98 conntype = None
101 conntype = None
99 user, host = None, None
102 user, host = None, None
100 cmd = ['cvs', 'server']
103 cmd = ['cvs', 'server']
101
104
102 self.ui.status(_("connecting to %s\n") % root)
105 self.ui.status(_("connecting to %s\n") % root)
103
106
104 if root.startswith(":pserver:"):
107 if root.startswith(":pserver:"):
105 root = root[9:]
108 root = root[9:]
106 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
109 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
107 root)
110 root)
108 if m:
111 if m:
109 conntype = "pserver"
112 conntype = "pserver"
110 user, passw, serv, port, root = m.groups()
113 user, passw, serv, port, root = m.groups()
111 if not user:
114 if not user:
112 user = "anonymous"
115 user = "anonymous"
113 if not port:
116 if not port:
114 port = 2401
117 port = 2401
115 else:
118 else:
116 port = int(port)
119 port = int(port)
117 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
120 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
118 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
121 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
119
122
120 if not passw:
123 if not passw:
121 passw = "A"
124 passw = "A"
122 cvspass = os.path.expanduser("~/.cvspass")
125 cvspass = os.path.expanduser("~/.cvspass")
123 try:
126 try:
124 pf = open(cvspass)
127 pf = open(cvspass)
125 for line in pf.read().splitlines():
128 for line in pf.read().splitlines():
126 part1, part2 = line.split(' ', 1)
129 part1, part2 = line.split(' ', 1)
127 # /1 :pserver:user@example.com:2401/cvsroot/foo
130 # /1 :pserver:user@example.com:2401/cvsroot/foo
128 # Ah<Z
131 # Ah<Z
129 if part1 == '/1':
132 if part1 == '/1':
130 part1, part2 = part2.split(' ', 1)
133 part1, part2 = part2.split(' ', 1)
131 format = format1
134 format = format1
132 # :pserver:user@example.com:/cvsroot/foo Ah<Z
135 # :pserver:user@example.com:/cvsroot/foo Ah<Z
133 else:
136 else:
134 format = format0
137 format = format0
135 if part1 == format:
138 if part1 == format:
136 passw = part2
139 passw = part2
137 break
140 break
138 pf.close()
141 pf.close()
139 except IOError as inst:
142 except IOError as inst:
140 if inst.errno != errno.ENOENT:
143 if inst.errno != errno.ENOENT:
141 if not getattr(inst, 'filename', None):
144 if not getattr(inst, 'filename', None):
142 inst.filename = cvspass
145 inst.filename = cvspass
143 raise
146 raise
144
147
145 sck = socket.socket()
148 sck = socket.socket()
146 sck.connect((serv, port))
149 sck.connect((serv, port))
147 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
150 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
148 "END AUTH REQUEST", ""]))
151 "END AUTH REQUEST", ""]))
149 if sck.recv(128) != "I LOVE YOU\n":
152 if sck.recv(128) != "I LOVE YOU\n":
150 raise util.Abort(_("CVS pserver authentication failed"))
153 raise util.Abort(_("CVS pserver authentication failed"))
151
154
152 self.writep = self.readp = sck.makefile('r+')
155 self.writep = self.readp = sck.makefile('r+')
153
156
154 if not conntype and root.startswith(":local:"):
157 if not conntype and root.startswith(":local:"):
155 conntype = "local"
158 conntype = "local"
156 root = root[7:]
159 root = root[7:]
157
160
158 if not conntype:
161 if not conntype:
159 # :ext:user@host/home/user/path/to/cvsroot
162 # :ext:user@host/home/user/path/to/cvsroot
160 if root.startswith(":ext:"):
163 if root.startswith(":ext:"):
161 root = root[5:]
164 root = root[5:]
162 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
165 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
163 # Do not take Windows path "c:\foo\bar" for a connection strings
166 # Do not take Windows path "c:\foo\bar" for a connection strings
164 if os.path.isdir(root) or not m:
167 if os.path.isdir(root) or not m:
165 conntype = "local"
168 conntype = "local"
166 else:
169 else:
167 conntype = "rsh"
170 conntype = "rsh"
168 user, host, root = m.group(1), m.group(2), m.group(3)
171 user, host, root = m.group(1), m.group(2), m.group(3)
169
172
170 if conntype != "pserver":
173 if conntype != "pserver":
171 if conntype == "rsh":
174 if conntype == "rsh":
172 rsh = os.environ.get("CVS_RSH") or "ssh"
175 rsh = os.environ.get("CVS_RSH") or "ssh"
173 if user:
176 if user:
174 cmd = [rsh, '-l', user, host] + cmd
177 cmd = [rsh, '-l', user, host] + cmd
175 else:
178 else:
176 cmd = [rsh, host] + cmd
179 cmd = [rsh, host] + cmd
177
180
178 # popen2 does not support argument lists under Windows
181 # popen2 does not support argument lists under Windows
179 cmd = [util.shellquote(arg) for arg in cmd]
182 cmd = [util.shellquote(arg) for arg in cmd]
180 cmd = util.quotecommand(' '.join(cmd))
183 cmd = util.quotecommand(' '.join(cmd))
181 self.writep, self.readp = util.popen2(cmd)
184 self.writep, self.readp = util.popen2(cmd)
182
185
183 self.realroot = root
186 self.realroot = root
184
187
185 self.writep.write("Root %s\n" % root)
188 self.writep.write("Root %s\n" % root)
186 self.writep.write("Valid-responses ok error Valid-requests Mode"
189 self.writep.write("Valid-responses ok error Valid-requests Mode"
187 " M Mbinary E Checked-in Created Updated"
190 " M Mbinary E Checked-in Created Updated"
188 " Merged Removed\n")
191 " Merged Removed\n")
189 self.writep.write("valid-requests\n")
192 self.writep.write("valid-requests\n")
190 self.writep.flush()
193 self.writep.flush()
191 r = self.readp.readline()
194 r = self.readp.readline()
192 if not r.startswith("Valid-requests"):
195 if not r.startswith("Valid-requests"):
193 raise util.Abort(_('unexpected response from CVS server '
196 raise util.Abort(_('unexpected response from CVS server '
194 '(expected "Valid-requests", but got %r)')
197 '(expected "Valid-requests", but got %r)')
195 % r)
198 % r)
196 if "UseUnchanged" in r:
199 if "UseUnchanged" in r:
197 self.writep.write("UseUnchanged\n")
200 self.writep.write("UseUnchanged\n")
198 self.writep.flush()
201 self.writep.flush()
199 r = self.readp.readline()
202 r = self.readp.readline()
200
203
201 def getheads(self):
204 def getheads(self):
202 self._parse()
205 self._parse()
203 return self.heads
206 return self.heads
204
207
205 def getfile(self, name, rev):
208 def getfile(self, name, rev):
206
209
207 def chunkedread(fp, count):
210 def chunkedread(fp, count):
208 # file-objects returned by socket.makefile() do not handle
211 # file-objects returned by socket.makefile() do not handle
209 # large read() requests very well.
212 # large read() requests very well.
210 chunksize = 65536
213 chunksize = 65536
211 output = StringIO()
214 output = StringIO()
212 while count > 0:
215 while count > 0:
213 data = fp.read(min(count, chunksize))
216 data = fp.read(min(count, chunksize))
214 if not data:
217 if not data:
215 raise util.Abort(_("%d bytes missing from remote file")
218 raise util.Abort(_("%d bytes missing from remote file")
216 % count)
219 % count)
217 count -= len(data)
220 count -= len(data)
218 output.write(data)
221 output.write(data)
219 return output.getvalue()
222 return output.getvalue()
220
223
221 self._parse()
224 self._parse()
222 if rev.endswith("(DEAD)"):
225 if rev.endswith("(DEAD)"):
223 return None, None
226 return None, None
224
227
225 args = ("-N -P -kk -r %s --" % rev).split()
228 args = ("-N -P -kk -r %s --" % rev).split()
226 args.append(self.cvsrepo + '/' + name)
229 args.append(self.cvsrepo + '/' + name)
227 for x in args:
230 for x in args:
228 self.writep.write("Argument %s\n" % x)
231 self.writep.write("Argument %s\n" % x)
229 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
232 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
230 self.writep.flush()
233 self.writep.flush()
231
234
232 data = ""
235 data = ""
233 mode = None
236 mode = None
234 while True:
237 while True:
235 line = self.readp.readline()
238 line = self.readp.readline()
236 if line.startswith("Created ") or line.startswith("Updated "):
239 if line.startswith("Created ") or line.startswith("Updated "):
237 self.readp.readline() # path
240 self.readp.readline() # path
238 self.readp.readline() # entries
241 self.readp.readline() # entries
239 mode = self.readp.readline()[:-1]
242 mode = self.readp.readline()[:-1]
240 count = int(self.readp.readline()[:-1])
243 count = int(self.readp.readline()[:-1])
241 data = chunkedread(self.readp, count)
244 data = chunkedread(self.readp, count)
242 elif line.startswith(" "):
245 elif line.startswith(" "):
243 data += line[1:]
246 data += line[1:]
244 elif line.startswith("M "):
247 elif line.startswith("M "):
245 pass
248 pass
246 elif line.startswith("Mbinary "):
249 elif line.startswith("Mbinary "):
247 count = int(self.readp.readline()[:-1])
250 count = int(self.readp.readline()[:-1])
248 data = chunkedread(self.readp, count)
251 data = chunkedread(self.readp, count)
249 else:
252 else:
250 if line == "ok\n":
253 if line == "ok\n":
251 if mode is None:
254 if mode is None:
252 raise util.Abort(_('malformed response from CVS'))
255 raise util.Abort(_('malformed response from CVS'))
253 return (data, "x" in mode and "x" or "")
256 return (data, "x" in mode and "x" or "")
254 elif line.startswith("E "):
257 elif line.startswith("E "):
255 self.ui.warn(_("cvs server: %s\n") % line[2:])
258 self.ui.warn(_("cvs server: %s\n") % line[2:])
256 elif line.startswith("Remove"):
259 elif line.startswith("Remove"):
257 self.readp.readline()
260 self.readp.readline()
258 else:
261 else:
259 raise util.Abort(_("unknown CVS response: %s") % line)
262 raise util.Abort(_("unknown CVS response: %s") % line)
260
263
261 def getchanges(self, rev, full):
264 def getchanges(self, rev, full):
262 if full:
265 if full:
263 raise util.Abort(_("convert from cvs do not support --full"))
266 raise util.Abort(_("convert from cvs do not support --full"))
264 self._parse()
267 self._parse()
265 return sorted(self.files[rev].iteritems()), {}, set()
268 return sorted(self.files[rev].iteritems()), {}, set()
266
269
267 def getcommit(self, rev):
270 def getcommit(self, rev):
268 self._parse()
271 self._parse()
269 return self.changeset[rev]
272 return self.changeset[rev]
270
273
271 def gettags(self):
274 def gettags(self):
272 self._parse()
275 self._parse()
273 return self.tags
276 return self.tags
274
277
275 def getchangedfiles(self, rev, i):
278 def getchangedfiles(self, rev, i):
276 self._parse()
279 self._parse()
277 return sorted(self.files[rev])
280 return sorted(self.files[rev])
@@ -1,208 +1,208
1 # darcs.py - darcs support for the convert extension
1 # darcs.py - darcs support for the convert extension
2 #
2 #
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
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 from common import NoRepo, checktool, commandline, commit, converter_source
8 from common import NoRepo, checktool, commandline, commit, converter_source
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import util
10 from mercurial import util
11 import os, shutil, tempfile, re, errno
11 import os, shutil, tempfile, re, errno
12
12
13 # The naming drift of ElementTree is fun!
13 # The naming drift of ElementTree is fun!
14
14
15 try:
15 try:
16 from xml.etree.cElementTree import ElementTree, XMLParser
16 from xml.etree.cElementTree import ElementTree, XMLParser
17 except ImportError:
17 except ImportError:
18 try:
18 try:
19 from xml.etree.ElementTree import ElementTree, XMLParser
19 from xml.etree.ElementTree import ElementTree, XMLParser
20 except ImportError:
20 except ImportError:
21 try:
21 try:
22 from elementtree.cElementTree import ElementTree, XMLParser
22 from elementtree.cElementTree import ElementTree, XMLParser
23 except ImportError:
23 except ImportError:
24 try:
24 try:
25 from elementtree.ElementTree import ElementTree, XMLParser
25 from elementtree.ElementTree import ElementTree, XMLParser
26 except ImportError:
26 except ImportError:
27 pass
27 pass
28
28
29 class darcs_source(converter_source, commandline):
29 class darcs_source(converter_source, commandline):
30 def __init__(self, ui, path, rev=None):
30 def __init__(self, ui, path, revs=None):
31 converter_source.__init__(self, ui, path, rev=rev)
31 converter_source.__init__(self, ui, path, revs=revs)
32 commandline.__init__(self, ui, 'darcs')
32 commandline.__init__(self, ui, 'darcs')
33
33
34 # check for _darcs, ElementTree so that we can easily skip
34 # check for _darcs, ElementTree so that we can easily skip
35 # test-convert-darcs if ElementTree is not around
35 # test-convert-darcs if ElementTree is not around
36 if not os.path.exists(os.path.join(path, '_darcs')):
36 if not os.path.exists(os.path.join(path, '_darcs')):
37 raise NoRepo(_("%s does not look like a darcs repository") % path)
37 raise NoRepo(_("%s does not look like a darcs repository") % path)
38
38
39 checktool('darcs')
39 checktool('darcs')
40 version = self.run0('--version').splitlines()[0].strip()
40 version = self.run0('--version').splitlines()[0].strip()
41 if version < '2.1':
41 if version < '2.1':
42 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
42 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
43 version)
43 version)
44
44
45 if "ElementTree" not in globals():
45 if "ElementTree" not in globals():
46 raise util.Abort(_("Python ElementTree module is not available"))
46 raise util.Abort(_("Python ElementTree module is not available"))
47
47
48 self.path = os.path.realpath(path)
48 self.path = os.path.realpath(path)
49
49
50 self.lastrev = None
50 self.lastrev = None
51 self.changes = {}
51 self.changes = {}
52 self.parents = {}
52 self.parents = {}
53 self.tags = {}
53 self.tags = {}
54
54
55 # Check darcs repository format
55 # Check darcs repository format
56 format = self.format()
56 format = self.format()
57 if format:
57 if format:
58 if format in ('darcs-1.0', 'hashed'):
58 if format in ('darcs-1.0', 'hashed'):
59 raise NoRepo(_("%s repository format is unsupported, "
59 raise NoRepo(_("%s repository format is unsupported, "
60 "please upgrade") % format)
60 "please upgrade") % format)
61 else:
61 else:
62 self.ui.warn(_('failed to detect repository format!'))
62 self.ui.warn(_('failed to detect repository format!'))
63
63
64 def before(self):
64 def before(self):
65 self.tmppath = tempfile.mkdtemp(
65 self.tmppath = tempfile.mkdtemp(
66 prefix='convert-' + os.path.basename(self.path) + '-')
66 prefix='convert-' + os.path.basename(self.path) + '-')
67 output, status = self.run('init', repodir=self.tmppath)
67 output, status = self.run('init', repodir=self.tmppath)
68 self.checkexit(status)
68 self.checkexit(status)
69
69
70 tree = self.xml('changes', xml_output=True, summary=True,
70 tree = self.xml('changes', xml_output=True, summary=True,
71 repodir=self.path)
71 repodir=self.path)
72 tagname = None
72 tagname = None
73 child = None
73 child = None
74 for elt in tree.findall('patch'):
74 for elt in tree.findall('patch'):
75 node = elt.get('hash')
75 node = elt.get('hash')
76 name = elt.findtext('name', '')
76 name = elt.findtext('name', '')
77 if name.startswith('TAG '):
77 if name.startswith('TAG '):
78 tagname = name[4:].strip()
78 tagname = name[4:].strip()
79 elif tagname is not None:
79 elif tagname is not None:
80 self.tags[tagname] = node
80 self.tags[tagname] = node
81 tagname = None
81 tagname = None
82 self.changes[node] = elt
82 self.changes[node] = elt
83 self.parents[child] = [node]
83 self.parents[child] = [node]
84 child = node
84 child = node
85 self.parents[child] = []
85 self.parents[child] = []
86
86
87 def after(self):
87 def after(self):
88 self.ui.debug('cleaning up %s\n' % self.tmppath)
88 self.ui.debug('cleaning up %s\n' % self.tmppath)
89 shutil.rmtree(self.tmppath, ignore_errors=True)
89 shutil.rmtree(self.tmppath, ignore_errors=True)
90
90
91 def recode(self, s, encoding=None):
91 def recode(self, s, encoding=None):
92 if isinstance(s, unicode):
92 if isinstance(s, unicode):
93 # XMLParser returns unicode objects for anything it can't
93 # XMLParser returns unicode objects for anything it can't
94 # encode into ASCII. We convert them back to str to get
94 # encode into ASCII. We convert them back to str to get
95 # recode's normal conversion behavior.
95 # recode's normal conversion behavior.
96 s = s.encode('latin-1')
96 s = s.encode('latin-1')
97 return super(darcs_source, self).recode(s, encoding)
97 return super(darcs_source, self).recode(s, encoding)
98
98
99 def xml(self, cmd, **kwargs):
99 def xml(self, cmd, **kwargs):
100 # NOTE: darcs is currently encoding agnostic and will print
100 # NOTE: darcs is currently encoding agnostic and will print
101 # patch metadata byte-for-byte, even in the XML changelog.
101 # patch metadata byte-for-byte, even in the XML changelog.
102 etree = ElementTree()
102 etree = ElementTree()
103 # While we are decoding the XML as latin-1 to be as liberal as
103 # While we are decoding the XML as latin-1 to be as liberal as
104 # possible, etree will still raise an exception if any
104 # possible, etree will still raise an exception if any
105 # non-printable characters are in the XML changelog.
105 # non-printable characters are in the XML changelog.
106 parser = XMLParser(encoding='latin-1')
106 parser = XMLParser(encoding='latin-1')
107 p = self._run(cmd, **kwargs)
107 p = self._run(cmd, **kwargs)
108 etree.parse(p.stdout, parser=parser)
108 etree.parse(p.stdout, parser=parser)
109 p.wait()
109 p.wait()
110 self.checkexit(p.returncode)
110 self.checkexit(p.returncode)
111 return etree.getroot()
111 return etree.getroot()
112
112
113 def format(self):
113 def format(self):
114 output, status = self.run('show', 'repo', no_files=True,
114 output, status = self.run('show', 'repo', no_files=True,
115 repodir=self.path)
115 repodir=self.path)
116 self.checkexit(status)
116 self.checkexit(status)
117 m = re.search(r'^\s*Format:\s*(.*)$', output, re.MULTILINE)
117 m = re.search(r'^\s*Format:\s*(.*)$', output, re.MULTILINE)
118 if not m:
118 if not m:
119 return None
119 return None
120 return ','.join(sorted(f.strip() for f in m.group(1).split(',')))
120 return ','.join(sorted(f.strip() for f in m.group(1).split(',')))
121
121
122 def manifest(self):
122 def manifest(self):
123 man = []
123 man = []
124 output, status = self.run('show', 'files', no_directories=True,
124 output, status = self.run('show', 'files', no_directories=True,
125 repodir=self.tmppath)
125 repodir=self.tmppath)
126 self.checkexit(status)
126 self.checkexit(status)
127 for line in output.split('\n'):
127 for line in output.split('\n'):
128 path = line[2:]
128 path = line[2:]
129 if path:
129 if path:
130 man.append(path)
130 man.append(path)
131 return man
131 return man
132
132
133 def getheads(self):
133 def getheads(self):
134 return self.parents[None]
134 return self.parents[None]
135
135
136 def getcommit(self, rev):
136 def getcommit(self, rev):
137 elt = self.changes[rev]
137 elt = self.changes[rev]
138 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
138 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
139 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
139 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
140 # etree can return unicode objects for name, comment, and author,
140 # etree can return unicode objects for name, comment, and author,
141 # so recode() is used to ensure str objects are emitted.
141 # so recode() is used to ensure str objects are emitted.
142 return commit(author=self.recode(elt.get('author')),
142 return commit(author=self.recode(elt.get('author')),
143 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
143 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
144 desc=self.recode(desc).strip(),
144 desc=self.recode(desc).strip(),
145 parents=self.parents[rev])
145 parents=self.parents[rev])
146
146
147 def pull(self, rev):
147 def pull(self, rev):
148 output, status = self.run('pull', self.path, all=True,
148 output, status = self.run('pull', self.path, all=True,
149 match='hash %s' % rev,
149 match='hash %s' % rev,
150 no_test=True, no_posthook=True,
150 no_test=True, no_posthook=True,
151 external_merge='/bin/false',
151 external_merge='/bin/false',
152 repodir=self.tmppath)
152 repodir=self.tmppath)
153 if status:
153 if status:
154 if output.find('We have conflicts in') == -1:
154 if output.find('We have conflicts in') == -1:
155 self.checkexit(status, output)
155 self.checkexit(status, output)
156 output, status = self.run('revert', all=True, repodir=self.tmppath)
156 output, status = self.run('revert', all=True, repodir=self.tmppath)
157 self.checkexit(status, output)
157 self.checkexit(status, output)
158
158
159 def getchanges(self, rev, full):
159 def getchanges(self, rev, full):
160 if full:
160 if full:
161 raise util.Abort(_("convert from darcs do not support --full"))
161 raise util.Abort(_("convert from darcs do not support --full"))
162 copies = {}
162 copies = {}
163 changes = []
163 changes = []
164 man = None
164 man = None
165 for elt in self.changes[rev].find('summary').getchildren():
165 for elt in self.changes[rev].find('summary').getchildren():
166 if elt.tag in ('add_directory', 'remove_directory'):
166 if elt.tag in ('add_directory', 'remove_directory'):
167 continue
167 continue
168 if elt.tag == 'move':
168 if elt.tag == 'move':
169 if man is None:
169 if man is None:
170 man = self.manifest()
170 man = self.manifest()
171 source, dest = elt.get('from'), elt.get('to')
171 source, dest = elt.get('from'), elt.get('to')
172 if source in man:
172 if source in man:
173 # File move
173 # File move
174 changes.append((source, rev))
174 changes.append((source, rev))
175 changes.append((dest, rev))
175 changes.append((dest, rev))
176 copies[dest] = source
176 copies[dest] = source
177 else:
177 else:
178 # Directory move, deduce file moves from manifest
178 # Directory move, deduce file moves from manifest
179 source = source + '/'
179 source = source + '/'
180 for f in man:
180 for f in man:
181 if not f.startswith(source):
181 if not f.startswith(source):
182 continue
182 continue
183 fdest = dest + '/' + f[len(source):]
183 fdest = dest + '/' + f[len(source):]
184 changes.append((f, rev))
184 changes.append((f, rev))
185 changes.append((fdest, rev))
185 changes.append((fdest, rev))
186 copies[fdest] = f
186 copies[fdest] = f
187 else:
187 else:
188 changes.append((elt.text.strip(), rev))
188 changes.append((elt.text.strip(), rev))
189 self.pull(rev)
189 self.pull(rev)
190 self.lastrev = rev
190 self.lastrev = rev
191 return sorted(changes), copies, set()
191 return sorted(changes), copies, set()
192
192
193 def getfile(self, name, rev):
193 def getfile(self, name, rev):
194 if rev != self.lastrev:
194 if rev != self.lastrev:
195 raise util.Abort(_('internal calling inconsistency'))
195 raise util.Abort(_('internal calling inconsistency'))
196 path = os.path.join(self.tmppath, name)
196 path = os.path.join(self.tmppath, name)
197 try:
197 try:
198 data = util.readfile(path)
198 data = util.readfile(path)
199 mode = os.lstat(path).st_mode
199 mode = os.lstat(path).st_mode
200 except IOError as inst:
200 except IOError as inst:
201 if inst.errno == errno.ENOENT:
201 if inst.errno == errno.ENOENT:
202 return None, None
202 return None, None
203 raise
203 raise
204 mode = (mode & 0o111) and 'x' or ''
204 mode = (mode & 0o111) and 'x' or ''
205 return data, mode
205 return data, mode
206
206
207 def gettags(self):
207 def gettags(self):
208 return self.tags
208 return self.tags
@@ -1,396 +1,401
1 # git.py - git support for the convert extension
1 # git.py - git support for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 os
8 import os
9 import subprocess
9 import subprocess
10 from mercurial import util, config, error
10 from mercurial import util, config, error
11 from mercurial.node import hex, nullid
11 from mercurial.node import hex, nullid
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13
13
14 from common import NoRepo, commit, converter_source, checktool
14 from common import NoRepo, commit, converter_source, checktool
15
15
16 class submodule(object):
16 class submodule(object):
17 def __init__(self, path, node, url):
17 def __init__(self, path, node, url):
18 self.path = path
18 self.path = path
19 self.node = node
19 self.node = node
20 self.url = url
20 self.url = url
21
21
22 def hgsub(self):
22 def hgsub(self):
23 return "%s = [git]%s" % (self.path, self.url)
23 return "%s = [git]%s" % (self.path, self.url)
24
24
25 def hgsubstate(self):
25 def hgsubstate(self):
26 return "%s %s" % (self.node, self.path)
26 return "%s %s" % (self.node, self.path)
27
27
28 class convert_git(converter_source):
28 class convert_git(converter_source):
29 # Windows does not support GIT_DIR= construct while other systems
29 # Windows does not support GIT_DIR= construct while other systems
30 # cannot remove environment variable. Just assume none have
30 # cannot remove environment variable. Just assume none have
31 # both issues.
31 # both issues.
32 if util.safehasattr(os, 'unsetenv'):
32 if util.safehasattr(os, 'unsetenv'):
33 def gitopen(self, s, err=None):
33 def gitopen(self, s, err=None):
34 prevgitdir = os.environ.get('GIT_DIR')
34 prevgitdir = os.environ.get('GIT_DIR')
35 os.environ['GIT_DIR'] = self.path
35 os.environ['GIT_DIR'] = self.path
36 try:
36 try:
37 if err == subprocess.PIPE:
37 if err == subprocess.PIPE:
38 (stdin, stdout, stderr) = util.popen3(s)
38 (stdin, stdout, stderr) = util.popen3(s)
39 return stdout
39 return stdout
40 elif err == subprocess.STDOUT:
40 elif err == subprocess.STDOUT:
41 return self.popen_with_stderr(s)
41 return self.popen_with_stderr(s)
42 else:
42 else:
43 return util.popen(s, 'rb')
43 return util.popen(s, 'rb')
44 finally:
44 finally:
45 if prevgitdir is None:
45 if prevgitdir is None:
46 del os.environ['GIT_DIR']
46 del os.environ['GIT_DIR']
47 else:
47 else:
48 os.environ['GIT_DIR'] = prevgitdir
48 os.environ['GIT_DIR'] = prevgitdir
49
49
50 def gitpipe(self, s):
50 def gitpipe(self, s):
51 prevgitdir = os.environ.get('GIT_DIR')
51 prevgitdir = os.environ.get('GIT_DIR')
52 os.environ['GIT_DIR'] = self.path
52 os.environ['GIT_DIR'] = self.path
53 try:
53 try:
54 return util.popen3(s)
54 return util.popen3(s)
55 finally:
55 finally:
56 if prevgitdir is None:
56 if prevgitdir is None:
57 del os.environ['GIT_DIR']
57 del os.environ['GIT_DIR']
58 else:
58 else:
59 os.environ['GIT_DIR'] = prevgitdir
59 os.environ['GIT_DIR'] = prevgitdir
60
60
61 else:
61 else:
62 def gitopen(self, s, err=None):
62 def gitopen(self, s, err=None):
63 if err == subprocess.PIPE:
63 if err == subprocess.PIPE:
64 (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s))
64 (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s))
65 return so
65 return so
66 elif err == subprocess.STDOUT:
66 elif err == subprocess.STDOUT:
67 return self.popen_with_stderr(s)
67 return self.popen_with_stderr(s)
68 else:
68 else:
69 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
69 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
70
70
71 def gitpipe(self, s):
71 def gitpipe(self, s):
72 return util.popen3('GIT_DIR=%s %s' % (self.path, s))
72 return util.popen3('GIT_DIR=%s %s' % (self.path, s))
73
73
74 def popen_with_stderr(self, s):
74 def popen_with_stderr(self, s):
75 p = subprocess.Popen(s, shell=True, bufsize=-1,
75 p = subprocess.Popen(s, shell=True, bufsize=-1,
76 close_fds=util.closefds,
76 close_fds=util.closefds,
77 stdin=subprocess.PIPE,
77 stdin=subprocess.PIPE,
78 stdout=subprocess.PIPE,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.STDOUT,
79 stderr=subprocess.STDOUT,
80 universal_newlines=False,
80 universal_newlines=False,
81 env=None)
81 env=None)
82 return p.stdout
82 return p.stdout
83
83
84 def gitread(self, s):
84 def gitread(self, s):
85 fh = self.gitopen(s)
85 fh = self.gitopen(s)
86 data = fh.read()
86 data = fh.read()
87 return data, fh.close()
87 return data, fh.close()
88
88
89 def __init__(self, ui, path, rev=None):
89 def __init__(self, ui, path, revs=None):
90 super(convert_git, self).__init__(ui, path, rev=rev)
90 super(convert_git, self).__init__(ui, path, revs=revs)
91
92 if revs and len(revs) > 1:
93 raise util.Abort(_("git source does not support specifying "
94 "multiple revs"))
91
95
92 if os.path.isdir(path + "/.git"):
96 if os.path.isdir(path + "/.git"):
93 path += "/.git"
97 path += "/.git"
94 if not os.path.exists(path + "/objects"):
98 if not os.path.exists(path + "/objects"):
95 raise NoRepo(_("%s does not look like a Git repository") % path)
99 raise NoRepo(_("%s does not look like a Git repository") % path)
96
100
97 # The default value (50) is based on the default for 'git diff'.
101 # The default value (50) is based on the default for 'git diff'.
98 similarity = ui.configint('convert', 'git.similarity', default=50)
102 similarity = ui.configint('convert', 'git.similarity', default=50)
99 if similarity < 0 or similarity > 100:
103 if similarity < 0 or similarity > 100:
100 raise util.Abort(_('similarity must be between 0 and 100'))
104 raise util.Abort(_('similarity must be between 0 and 100'))
101 if similarity > 0:
105 if similarity > 0:
102 self.simopt = '-C%d%%' % similarity
106 self.simopt = '-C%d%%' % similarity
103 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
107 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
104 False)
108 False)
105 if findcopiesharder:
109 if findcopiesharder:
106 self.simopt += ' --find-copies-harder'
110 self.simopt += ' --find-copies-harder'
107 else:
111 else:
108 self.simopt = ''
112 self.simopt = ''
109
113
110 checktool('git', 'git')
114 checktool('git', 'git')
111
115
112 self.path = path
116 self.path = path
113 self.submodules = []
117 self.submodules = []
114
118
115 self.catfilepipe = self.gitpipe('git cat-file --batch')
119 self.catfilepipe = self.gitpipe('git cat-file --batch')
116
120
117 def after(self):
121 def after(self):
118 for f in self.catfilepipe:
122 for f in self.catfilepipe:
119 f.close()
123 f.close()
120
124
121 def getheads(self):
125 def getheads(self):
122 if not self.rev:
126 if not self.revs:
123 heads, ret = self.gitread('git rev-parse --branches --remotes')
127 heads, ret = self.gitread('git rev-parse --branches --remotes')
124 heads = heads.splitlines()
128 heads = heads.splitlines()
125 else:
129 else:
126 heads, ret = self.gitread("git rev-parse --verify %s" % self.rev)
130 heads, ret = self.gitread("git rev-parse --verify %s" %
131 self.revs[0])
127 heads = [heads[:-1]]
132 heads = [heads[:-1]]
128 if ret:
133 if ret:
129 raise util.Abort(_('cannot retrieve git heads'))
134 raise util.Abort(_('cannot retrieve git heads'))
130 return heads
135 return heads
131
136
132 def catfile(self, rev, type):
137 def catfile(self, rev, type):
133 if rev == hex(nullid):
138 if rev == hex(nullid):
134 raise IOError
139 raise IOError
135 self.catfilepipe[0].write(rev+'\n')
140 self.catfilepipe[0].write(rev+'\n')
136 self.catfilepipe[0].flush()
141 self.catfilepipe[0].flush()
137 info = self.catfilepipe[1].readline().split()
142 info = self.catfilepipe[1].readline().split()
138 if info[1] != type:
143 if info[1] != type:
139 raise util.Abort(_('cannot read %r object at %s') % (type, rev))
144 raise util.Abort(_('cannot read %r object at %s') % (type, rev))
140 size = int(info[2])
145 size = int(info[2])
141 data = self.catfilepipe[1].read(size)
146 data = self.catfilepipe[1].read(size)
142 if len(data) < size:
147 if len(data) < size:
143 raise util.Abort(_('cannot read %r object at %s: unexpected size')
148 raise util.Abort(_('cannot read %r object at %s: unexpected size')
144 % (type, rev))
149 % (type, rev))
145 # read the trailing newline
150 # read the trailing newline
146 self.catfilepipe[1].read(1)
151 self.catfilepipe[1].read(1)
147 return data
152 return data
148
153
149 def getfile(self, name, rev):
154 def getfile(self, name, rev):
150 if rev == hex(nullid):
155 if rev == hex(nullid):
151 return None, None
156 return None, None
152 if name == '.hgsub':
157 if name == '.hgsub':
153 data = '\n'.join([m.hgsub() for m in self.submoditer()])
158 data = '\n'.join([m.hgsub() for m in self.submoditer()])
154 mode = ''
159 mode = ''
155 elif name == '.hgsubstate':
160 elif name == '.hgsubstate':
156 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
161 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
157 mode = ''
162 mode = ''
158 else:
163 else:
159 data = self.catfile(rev, "blob")
164 data = self.catfile(rev, "blob")
160 mode = self.modecache[(name, rev)]
165 mode = self.modecache[(name, rev)]
161 return data, mode
166 return data, mode
162
167
163 def submoditer(self):
168 def submoditer(self):
164 null = hex(nullid)
169 null = hex(nullid)
165 for m in sorted(self.submodules, key=lambda p: p.path):
170 for m in sorted(self.submodules, key=lambda p: p.path):
166 if m.node != null:
171 if m.node != null:
167 yield m
172 yield m
168
173
169 def parsegitmodules(self, content):
174 def parsegitmodules(self, content):
170 """Parse the formatted .gitmodules file, example file format:
175 """Parse the formatted .gitmodules file, example file format:
171 [submodule "sub"]\n
176 [submodule "sub"]\n
172 \tpath = sub\n
177 \tpath = sub\n
173 \turl = git://giturl\n
178 \turl = git://giturl\n
174 """
179 """
175 self.submodules = []
180 self.submodules = []
176 c = config.config()
181 c = config.config()
177 # Each item in .gitmodules starts with whitespace that cant be parsed
182 # Each item in .gitmodules starts with whitespace that cant be parsed
178 c.parse('.gitmodules', '\n'.join(line.strip() for line in
183 c.parse('.gitmodules', '\n'.join(line.strip() for line in
179 content.split('\n')))
184 content.split('\n')))
180 for sec in c.sections():
185 for sec in c.sections():
181 s = c[sec]
186 s = c[sec]
182 if 'url' in s and 'path' in s:
187 if 'url' in s and 'path' in s:
183 self.submodules.append(submodule(s['path'], '', s['url']))
188 self.submodules.append(submodule(s['path'], '', s['url']))
184
189
185 def retrievegitmodules(self, version):
190 def retrievegitmodules(self, version):
186 modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules'))
191 modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules'))
187 if ret:
192 if ret:
188 # This can happen if a file is in the repo that has permissions
193 # This can happen if a file is in the repo that has permissions
189 # 160000, but there is no .gitmodules file.
194 # 160000, but there is no .gitmodules file.
190 self.ui.warn(_("warning: cannot read submodules config file in "
195 self.ui.warn(_("warning: cannot read submodules config file in "
191 "%s\n") % version)
196 "%s\n") % version)
192 return
197 return
193
198
194 try:
199 try:
195 self.parsegitmodules(modules)
200 self.parsegitmodules(modules)
196 except error.ParseError:
201 except error.ParseError:
197 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
202 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
198 % version)
203 % version)
199 return
204 return
200
205
201 for m in self.submodules:
206 for m in self.submodules:
202 node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path))
207 node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path))
203 if ret:
208 if ret:
204 continue
209 continue
205 m.node = node.strip()
210 m.node = node.strip()
206
211
207 def getchanges(self, version, full):
212 def getchanges(self, version, full):
208 if full:
213 if full:
209 raise util.Abort(_("convert from git do not support --full"))
214 raise util.Abort(_("convert from git do not support --full"))
210 self.modecache = {}
215 self.modecache = {}
211 fh = self.gitopen("git diff-tree -z --root -m -r %s %s" % (
216 fh = self.gitopen("git diff-tree -z --root -m -r %s %s" % (
212 self.simopt, version))
217 self.simopt, version))
213 changes = []
218 changes = []
214 copies = {}
219 copies = {}
215 seen = set()
220 seen = set()
216 entry = None
221 entry = None
217 subexists = [False]
222 subexists = [False]
218 subdeleted = [False]
223 subdeleted = [False]
219 difftree = fh.read().split('\x00')
224 difftree = fh.read().split('\x00')
220 lcount = len(difftree)
225 lcount = len(difftree)
221 i = 0
226 i = 0
222
227
223 def add(entry, f, isdest):
228 def add(entry, f, isdest):
224 seen.add(f)
229 seen.add(f)
225 h = entry[3]
230 h = entry[3]
226 p = (entry[1] == "100755")
231 p = (entry[1] == "100755")
227 s = (entry[1] == "120000")
232 s = (entry[1] == "120000")
228 renamesource = (not isdest and entry[4][0] == 'R')
233 renamesource = (not isdest and entry[4][0] == 'R')
229
234
230 if f == '.gitmodules':
235 if f == '.gitmodules':
231 subexists[0] = True
236 subexists[0] = True
232 if entry[4] == 'D' or renamesource:
237 if entry[4] == 'D' or renamesource:
233 subdeleted[0] = True
238 subdeleted[0] = True
234 changes.append(('.hgsub', hex(nullid)))
239 changes.append(('.hgsub', hex(nullid)))
235 else:
240 else:
236 changes.append(('.hgsub', ''))
241 changes.append(('.hgsub', ''))
237 elif entry[1] == '160000' or entry[0] == ':160000':
242 elif entry[1] == '160000' or entry[0] == ':160000':
238 subexists[0] = True
243 subexists[0] = True
239 else:
244 else:
240 if renamesource:
245 if renamesource:
241 h = hex(nullid)
246 h = hex(nullid)
242 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
247 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
243 changes.append((f, h))
248 changes.append((f, h))
244
249
245 while i < lcount:
250 while i < lcount:
246 l = difftree[i]
251 l = difftree[i]
247 i += 1
252 i += 1
248 if not entry:
253 if not entry:
249 if not l.startswith(':'):
254 if not l.startswith(':'):
250 continue
255 continue
251 entry = l.split()
256 entry = l.split()
252 continue
257 continue
253 f = l
258 f = l
254 if f not in seen:
259 if f not in seen:
255 add(entry, f, False)
260 add(entry, f, False)
256 # A file can be copied multiple times, or modified and copied
261 # A file can be copied multiple times, or modified and copied
257 # simultaneously. So f can be repeated even if fdest isn't.
262 # simultaneously. So f can be repeated even if fdest isn't.
258 if entry[4][0] in 'RC':
263 if entry[4][0] in 'RC':
259 # rename or copy: next line is the destination
264 # rename or copy: next line is the destination
260 fdest = difftree[i]
265 fdest = difftree[i]
261 i += 1
266 i += 1
262 if fdest not in seen:
267 if fdest not in seen:
263 add(entry, fdest, True)
268 add(entry, fdest, True)
264 # .gitmodules isn't imported at all, so it being copied to
269 # .gitmodules isn't imported at all, so it being copied to
265 # and fro doesn't really make sense
270 # and fro doesn't really make sense
266 if f != '.gitmodules' and fdest != '.gitmodules':
271 if f != '.gitmodules' and fdest != '.gitmodules':
267 copies[fdest] = f
272 copies[fdest] = f
268 entry = None
273 entry = None
269 if fh.close():
274 if fh.close():
270 raise util.Abort(_('cannot read changes in %s') % version)
275 raise util.Abort(_('cannot read changes in %s') % version)
271
276
272 if subexists[0]:
277 if subexists[0]:
273 if subdeleted[0]:
278 if subdeleted[0]:
274 changes.append(('.hgsubstate', hex(nullid)))
279 changes.append(('.hgsubstate', hex(nullid)))
275 else:
280 else:
276 self.retrievegitmodules(version)
281 self.retrievegitmodules(version)
277 changes.append(('.hgsubstate', ''))
282 changes.append(('.hgsubstate', ''))
278 return (changes, copies, set())
283 return (changes, copies, set())
279
284
280 def getcommit(self, version):
285 def getcommit(self, version):
281 c = self.catfile(version, "commit") # read the commit hash
286 c = self.catfile(version, "commit") # read the commit hash
282 end = c.find("\n\n")
287 end = c.find("\n\n")
283 message = c[end + 2:]
288 message = c[end + 2:]
284 message = self.recode(message)
289 message = self.recode(message)
285 l = c[:end].splitlines()
290 l = c[:end].splitlines()
286 parents = []
291 parents = []
287 author = committer = None
292 author = committer = None
288 for e in l[1:]:
293 for e in l[1:]:
289 n, v = e.split(" ", 1)
294 n, v = e.split(" ", 1)
290 if n == "author":
295 if n == "author":
291 p = v.split()
296 p = v.split()
292 tm, tz = p[-2:]
297 tm, tz = p[-2:]
293 author = " ".join(p[:-2])
298 author = " ".join(p[:-2])
294 if author[0] == "<": author = author[1:-1]
299 if author[0] == "<": author = author[1:-1]
295 author = self.recode(author)
300 author = self.recode(author)
296 if n == "committer":
301 if n == "committer":
297 p = v.split()
302 p = v.split()
298 tm, tz = p[-2:]
303 tm, tz = p[-2:]
299 committer = " ".join(p[:-2])
304 committer = " ".join(p[:-2])
300 if committer[0] == "<": committer = committer[1:-1]
305 if committer[0] == "<": committer = committer[1:-1]
301 committer = self.recode(committer)
306 committer = self.recode(committer)
302 if n == "parent":
307 if n == "parent":
303 parents.append(v)
308 parents.append(v)
304
309
305 if committer and committer != author:
310 if committer and committer != author:
306 message += "\ncommitter: %s\n" % committer
311 message += "\ncommitter: %s\n" % committer
307 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
312 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
308 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
313 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
309 date = tm + " " + str(tz)
314 date = tm + " " + str(tz)
310
315
311 c = commit(parents=parents, date=date, author=author, desc=message,
316 c = commit(parents=parents, date=date, author=author, desc=message,
312 rev=version)
317 rev=version)
313 return c
318 return c
314
319
315 def numcommits(self):
320 def numcommits(self):
316 return len([None for _ in self.gitopen('git rev-list --all')])
321 return len([None for _ in self.gitopen('git rev-list --all')])
317
322
318 def gettags(self):
323 def gettags(self):
319 tags = {}
324 tags = {}
320 alltags = {}
325 alltags = {}
321 fh = self.gitopen('git ls-remote --tags "%s"' % self.path,
326 fh = self.gitopen('git ls-remote --tags "%s"' % self.path,
322 err=subprocess.STDOUT)
327 err=subprocess.STDOUT)
323 prefix = 'refs/tags/'
328 prefix = 'refs/tags/'
324
329
325 # Build complete list of tags, both annotated and bare ones
330 # Build complete list of tags, both annotated and bare ones
326 for line in fh:
331 for line in fh:
327 line = line.strip()
332 line = line.strip()
328 if line.startswith("error:") or line.startswith("fatal:"):
333 if line.startswith("error:") or line.startswith("fatal:"):
329 raise util.Abort(_('cannot read tags from %s') % self.path)
334 raise util.Abort(_('cannot read tags from %s') % self.path)
330 node, tag = line.split(None, 1)
335 node, tag = line.split(None, 1)
331 if not tag.startswith(prefix):
336 if not tag.startswith(prefix):
332 continue
337 continue
333 alltags[tag[len(prefix):]] = node
338 alltags[tag[len(prefix):]] = node
334 if fh.close():
339 if fh.close():
335 raise util.Abort(_('cannot read tags from %s') % self.path)
340 raise util.Abort(_('cannot read tags from %s') % self.path)
336
341
337 # Filter out tag objects for annotated tag refs
342 # Filter out tag objects for annotated tag refs
338 for tag in alltags:
343 for tag in alltags:
339 if tag.endswith('^{}'):
344 if tag.endswith('^{}'):
340 tags[tag[:-3]] = alltags[tag]
345 tags[tag[:-3]] = alltags[tag]
341 else:
346 else:
342 if tag + '^{}' in alltags:
347 if tag + '^{}' in alltags:
343 continue
348 continue
344 else:
349 else:
345 tags[tag] = alltags[tag]
350 tags[tag] = alltags[tag]
346
351
347 return tags
352 return tags
348
353
349 def getchangedfiles(self, version, i):
354 def getchangedfiles(self, version, i):
350 changes = []
355 changes = []
351 if i is None:
356 if i is None:
352 fh = self.gitopen("git diff-tree --root -m -r %s" % version)
357 fh = self.gitopen("git diff-tree --root -m -r %s" % version)
353 for l in fh:
358 for l in fh:
354 if "\t" not in l:
359 if "\t" not in l:
355 continue
360 continue
356 m, f = l[:-1].split("\t")
361 m, f = l[:-1].split("\t")
357 changes.append(f)
362 changes.append(f)
358 else:
363 else:
359 fh = self.gitopen('git diff-tree --name-only --root -r %s '
364 fh = self.gitopen('git diff-tree --name-only --root -r %s '
360 '"%s^%s" --' % (version, version, i + 1))
365 '"%s^%s" --' % (version, version, i + 1))
361 changes = [f.rstrip('\n') for f in fh]
366 changes = [f.rstrip('\n') for f in fh]
362 if fh.close():
367 if fh.close():
363 raise util.Abort(_('cannot read changes in %s') % version)
368 raise util.Abort(_('cannot read changes in %s') % version)
364
369
365 return changes
370 return changes
366
371
367 def getbookmarks(self):
372 def getbookmarks(self):
368 bookmarks = {}
373 bookmarks = {}
369
374
370 # Interesting references in git are prefixed
375 # Interesting references in git are prefixed
371 prefix = 'refs/heads/'
376 prefix = 'refs/heads/'
372 prefixlen = len(prefix)
377 prefixlen = len(prefix)
373
378
374 # factor two commands
379 # factor two commands
375 gitcmd = { 'remote/': 'git ls-remote --heads origin',
380 gitcmd = { 'remote/': 'git ls-remote --heads origin',
376 '': 'git show-ref'}
381 '': 'git show-ref'}
377
382
378 # Origin heads
383 # Origin heads
379 for reftype in gitcmd:
384 for reftype in gitcmd:
380 try:
385 try:
381 fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE)
386 fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE)
382 for line in fh:
387 for line in fh:
383 line = line.strip()
388 line = line.strip()
384 rev, name = line.split(None, 1)
389 rev, name = line.split(None, 1)
385 if not name.startswith(prefix):
390 if not name.startswith(prefix):
386 continue
391 continue
387 name = '%s%s' % (reftype, name[prefixlen:])
392 name = '%s%s' % (reftype, name[prefixlen:])
388 bookmarks[name] = rev
393 bookmarks[name] = rev
389 except Exception:
394 except Exception:
390 pass
395 pass
391
396
392 return bookmarks
397 return bookmarks
393
398
394 def checkrevformat(self, revstr, mapname='splicemap'):
399 def checkrevformat(self, revstr, mapname='splicemap'):
395 """ git revision string is a 40 byte hex """
400 """ git revision string is a 40 byte hex """
396 self.checkhexformat(revstr, mapname)
401 self.checkhexformat(revstr, mapname)
@@ -1,342 +1,342
1 # gnuarch.py - GNU Arch support for the convert extension
1 # gnuarch.py - GNU Arch support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 # and others
4 # and others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from common import NoRepo, commandline, commit, converter_source
9 from common import NoRepo, commandline, commit, converter_source
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11 from mercurial import encoding, util
11 from mercurial import encoding, util
12 import os, shutil, tempfile, stat
12 import os, shutil, tempfile, stat
13 from email.Parser import Parser
13 from email.Parser import Parser
14
14
15 class gnuarch_source(converter_source, commandline):
15 class gnuarch_source(converter_source, commandline):
16
16
17 class gnuarch_rev(object):
17 class gnuarch_rev(object):
18 def __init__(self, rev):
18 def __init__(self, rev):
19 self.rev = rev
19 self.rev = rev
20 self.summary = ''
20 self.summary = ''
21 self.date = None
21 self.date = None
22 self.author = ''
22 self.author = ''
23 self.continuationof = None
23 self.continuationof = None
24 self.add_files = []
24 self.add_files = []
25 self.mod_files = []
25 self.mod_files = []
26 self.del_files = []
26 self.del_files = []
27 self.ren_files = {}
27 self.ren_files = {}
28 self.ren_dirs = {}
28 self.ren_dirs = {}
29
29
30 def __init__(self, ui, path, rev=None):
30 def __init__(self, ui, path, revs=None):
31 super(gnuarch_source, self).__init__(ui, path, rev=rev)
31 super(gnuarch_source, self).__init__(ui, path, revs=revs)
32
32
33 if not os.path.exists(os.path.join(path, '{arch}')):
33 if not os.path.exists(os.path.join(path, '{arch}')):
34 raise NoRepo(_("%s does not look like a GNU Arch repository")
34 raise NoRepo(_("%s does not look like a GNU Arch repository")
35 % path)
35 % path)
36
36
37 # Could use checktool, but we want to check for baz or tla.
37 # Could use checktool, but we want to check for baz or tla.
38 self.execmd = None
38 self.execmd = None
39 if util.findexe('baz'):
39 if util.findexe('baz'):
40 self.execmd = 'baz'
40 self.execmd = 'baz'
41 else:
41 else:
42 if util.findexe('tla'):
42 if util.findexe('tla'):
43 self.execmd = 'tla'
43 self.execmd = 'tla'
44 else:
44 else:
45 raise util.Abort(_('cannot find a GNU Arch tool'))
45 raise util.Abort(_('cannot find a GNU Arch tool'))
46
46
47 commandline.__init__(self, ui, self.execmd)
47 commandline.__init__(self, ui, self.execmd)
48
48
49 self.path = os.path.realpath(path)
49 self.path = os.path.realpath(path)
50 self.tmppath = None
50 self.tmppath = None
51
51
52 self.treeversion = None
52 self.treeversion = None
53 self.lastrev = None
53 self.lastrev = None
54 self.changes = {}
54 self.changes = {}
55 self.parents = {}
55 self.parents = {}
56 self.tags = {}
56 self.tags = {}
57 self.catlogparser = Parser()
57 self.catlogparser = Parser()
58 self.encoding = encoding.encoding
58 self.encoding = encoding.encoding
59 self.archives = []
59 self.archives = []
60
60
61 def before(self):
61 def before(self):
62 # Get registered archives
62 # Get registered archives
63 self.archives = [i.rstrip('\n')
63 self.archives = [i.rstrip('\n')
64 for i in self.runlines0('archives', '-n')]
64 for i in self.runlines0('archives', '-n')]
65
65
66 if self.execmd == 'tla':
66 if self.execmd == 'tla':
67 output = self.run0('tree-version', self.path)
67 output = self.run0('tree-version', self.path)
68 else:
68 else:
69 output = self.run0('tree-version', '-d', self.path)
69 output = self.run0('tree-version', '-d', self.path)
70 self.treeversion = output.strip()
70 self.treeversion = output.strip()
71
71
72 # Get name of temporary directory
72 # Get name of temporary directory
73 version = self.treeversion.split('/')
73 version = self.treeversion.split('/')
74 self.tmppath = os.path.join(tempfile.gettempdir(),
74 self.tmppath = os.path.join(tempfile.gettempdir(),
75 'hg-%s' % version[1])
75 'hg-%s' % version[1])
76
76
77 # Generate parents dictionary
77 # Generate parents dictionary
78 self.parents[None] = []
78 self.parents[None] = []
79 treeversion = self.treeversion
79 treeversion = self.treeversion
80 child = None
80 child = None
81 while treeversion:
81 while treeversion:
82 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
82 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
83
83
84 archive = treeversion.split('/')[0]
84 archive = treeversion.split('/')[0]
85 if archive not in self.archives:
85 if archive not in self.archives:
86 self.ui.status(_('tree analysis stopped because it points to '
86 self.ui.status(_('tree analysis stopped because it points to '
87 'an unregistered archive %s...\n') % archive)
87 'an unregistered archive %s...\n') % archive)
88 break
88 break
89
89
90 # Get the complete list of revisions for that tree version
90 # Get the complete list of revisions for that tree version
91 output, status = self.runlines('revisions', '-r', '-f', treeversion)
91 output, status = self.runlines('revisions', '-r', '-f', treeversion)
92 self.checkexit(status, 'failed retrieving revisions for %s'
92 self.checkexit(status, 'failed retrieving revisions for %s'
93 % treeversion)
93 % treeversion)
94
94
95 # No new iteration unless a revision has a continuation-of header
95 # No new iteration unless a revision has a continuation-of header
96 treeversion = None
96 treeversion = None
97
97
98 for l in output:
98 for l in output:
99 rev = l.strip()
99 rev = l.strip()
100 self.changes[rev] = self.gnuarch_rev(rev)
100 self.changes[rev] = self.gnuarch_rev(rev)
101 self.parents[rev] = []
101 self.parents[rev] = []
102
102
103 # Read author, date and summary
103 # Read author, date and summary
104 catlog, status = self.run('cat-log', '-d', self.path, rev)
104 catlog, status = self.run('cat-log', '-d', self.path, rev)
105 if status:
105 if status:
106 catlog = self.run0('cat-archive-log', rev)
106 catlog = self.run0('cat-archive-log', rev)
107 self._parsecatlog(catlog, rev)
107 self._parsecatlog(catlog, rev)
108
108
109 # Populate the parents map
109 # Populate the parents map
110 self.parents[child].append(rev)
110 self.parents[child].append(rev)
111
111
112 # Keep track of the current revision as the child of the next
112 # Keep track of the current revision as the child of the next
113 # revision scanned
113 # revision scanned
114 child = rev
114 child = rev
115
115
116 # Check if we have to follow the usual incremental history
116 # Check if we have to follow the usual incremental history
117 # or if we have to 'jump' to a different treeversion given
117 # or if we have to 'jump' to a different treeversion given
118 # by the continuation-of header.
118 # by the continuation-of header.
119 if self.changes[rev].continuationof:
119 if self.changes[rev].continuationof:
120 treeversion = '--'.join(
120 treeversion = '--'.join(
121 self.changes[rev].continuationof.split('--')[:-1])
121 self.changes[rev].continuationof.split('--')[:-1])
122 break
122 break
123
123
124 # If we reached a base-0 revision w/o any continuation-of
124 # If we reached a base-0 revision w/o any continuation-of
125 # header, it means the tree history ends here.
125 # header, it means the tree history ends here.
126 if rev[-6:] == 'base-0':
126 if rev[-6:] == 'base-0':
127 break
127 break
128
128
129 def after(self):
129 def after(self):
130 self.ui.debug('cleaning up %s\n' % self.tmppath)
130 self.ui.debug('cleaning up %s\n' % self.tmppath)
131 shutil.rmtree(self.tmppath, ignore_errors=True)
131 shutil.rmtree(self.tmppath, ignore_errors=True)
132
132
133 def getheads(self):
133 def getheads(self):
134 return self.parents[None]
134 return self.parents[None]
135
135
136 def getfile(self, name, rev):
136 def getfile(self, name, rev):
137 if rev != self.lastrev:
137 if rev != self.lastrev:
138 raise util.Abort(_('internal calling inconsistency'))
138 raise util.Abort(_('internal calling inconsistency'))
139
139
140 if not os.path.lexists(os.path.join(self.tmppath, name)):
140 if not os.path.lexists(os.path.join(self.tmppath, name)):
141 return None, None
141 return None, None
142
142
143 return self._getfile(name, rev)
143 return self._getfile(name, rev)
144
144
145 def getchanges(self, rev, full):
145 def getchanges(self, rev, full):
146 if full:
146 if full:
147 raise util.Abort(_("convert from arch do not support --full"))
147 raise util.Abort(_("convert from arch do not support --full"))
148 self._update(rev)
148 self._update(rev)
149 changes = []
149 changes = []
150 copies = {}
150 copies = {}
151
151
152 for f in self.changes[rev].add_files:
152 for f in self.changes[rev].add_files:
153 changes.append((f, rev))
153 changes.append((f, rev))
154
154
155 for f in self.changes[rev].mod_files:
155 for f in self.changes[rev].mod_files:
156 changes.append((f, rev))
156 changes.append((f, rev))
157
157
158 for f in self.changes[rev].del_files:
158 for f in self.changes[rev].del_files:
159 changes.append((f, rev))
159 changes.append((f, rev))
160
160
161 for src in self.changes[rev].ren_files:
161 for src in self.changes[rev].ren_files:
162 to = self.changes[rev].ren_files[src]
162 to = self.changes[rev].ren_files[src]
163 changes.append((src, rev))
163 changes.append((src, rev))
164 changes.append((to, rev))
164 changes.append((to, rev))
165 copies[to] = src
165 copies[to] = src
166
166
167 for src in self.changes[rev].ren_dirs:
167 for src in self.changes[rev].ren_dirs:
168 to = self.changes[rev].ren_dirs[src]
168 to = self.changes[rev].ren_dirs[src]
169 chgs, cps = self._rendirchanges(src, to)
169 chgs, cps = self._rendirchanges(src, to)
170 changes += [(f, rev) for f in chgs]
170 changes += [(f, rev) for f in chgs]
171 copies.update(cps)
171 copies.update(cps)
172
172
173 self.lastrev = rev
173 self.lastrev = rev
174 return sorted(set(changes)), copies, set()
174 return sorted(set(changes)), copies, set()
175
175
176 def getcommit(self, rev):
176 def getcommit(self, rev):
177 changes = self.changes[rev]
177 changes = self.changes[rev]
178 return commit(author=changes.author, date=changes.date,
178 return commit(author=changes.author, date=changes.date,
179 desc=changes.summary, parents=self.parents[rev], rev=rev)
179 desc=changes.summary, parents=self.parents[rev], rev=rev)
180
180
181 def gettags(self):
181 def gettags(self):
182 return self.tags
182 return self.tags
183
183
184 def _execute(self, cmd, *args, **kwargs):
184 def _execute(self, cmd, *args, **kwargs):
185 cmdline = [self.execmd, cmd]
185 cmdline = [self.execmd, cmd]
186 cmdline += args
186 cmdline += args
187 cmdline = [util.shellquote(arg) for arg in cmdline]
187 cmdline = [util.shellquote(arg) for arg in cmdline]
188 cmdline += ['>', os.devnull, '2>', os.devnull]
188 cmdline += ['>', os.devnull, '2>', os.devnull]
189 cmdline = util.quotecommand(' '.join(cmdline))
189 cmdline = util.quotecommand(' '.join(cmdline))
190 self.ui.debug(cmdline, '\n')
190 self.ui.debug(cmdline, '\n')
191 return os.system(cmdline)
191 return os.system(cmdline)
192
192
193 def _update(self, rev):
193 def _update(self, rev):
194 self.ui.debug('applying revision %s...\n' % rev)
194 self.ui.debug('applying revision %s...\n' % rev)
195 changeset, status = self.runlines('replay', '-d', self.tmppath,
195 changeset, status = self.runlines('replay', '-d', self.tmppath,
196 rev)
196 rev)
197 if status:
197 if status:
198 # Something went wrong while merging (baz or tla
198 # Something went wrong while merging (baz or tla
199 # issue?), get latest revision and try from there
199 # issue?), get latest revision and try from there
200 shutil.rmtree(self.tmppath, ignore_errors=True)
200 shutil.rmtree(self.tmppath, ignore_errors=True)
201 self._obtainrevision(rev)
201 self._obtainrevision(rev)
202 else:
202 else:
203 old_rev = self.parents[rev][0]
203 old_rev = self.parents[rev][0]
204 self.ui.debug('computing changeset between %s and %s...\n'
204 self.ui.debug('computing changeset between %s and %s...\n'
205 % (old_rev, rev))
205 % (old_rev, rev))
206 self._parsechangeset(changeset, rev)
206 self._parsechangeset(changeset, rev)
207
207
208 def _getfile(self, name, rev):
208 def _getfile(self, name, rev):
209 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
209 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
210 if stat.S_ISLNK(mode):
210 if stat.S_ISLNK(mode):
211 data = os.readlink(os.path.join(self.tmppath, name))
211 data = os.readlink(os.path.join(self.tmppath, name))
212 if mode:
212 if mode:
213 mode = 'l'
213 mode = 'l'
214 else:
214 else:
215 mode = ''
215 mode = ''
216 else:
216 else:
217 data = open(os.path.join(self.tmppath, name), 'rb').read()
217 data = open(os.path.join(self.tmppath, name), 'rb').read()
218 mode = (mode & 0o111) and 'x' or ''
218 mode = (mode & 0o111) and 'x' or ''
219 return data, mode
219 return data, mode
220
220
221 def _exclude(self, name):
221 def _exclude(self, name):
222 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
222 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
223 for exc in exclude:
223 for exc in exclude:
224 if name.find(exc) != -1:
224 if name.find(exc) != -1:
225 return True
225 return True
226 return False
226 return False
227
227
228 def _readcontents(self, path):
228 def _readcontents(self, path):
229 files = []
229 files = []
230 contents = os.listdir(path)
230 contents = os.listdir(path)
231 while len(contents) > 0:
231 while len(contents) > 0:
232 c = contents.pop()
232 c = contents.pop()
233 p = os.path.join(path, c)
233 p = os.path.join(path, c)
234 # os.walk could be used, but here we avoid internal GNU
234 # os.walk could be used, but here we avoid internal GNU
235 # Arch files and directories, thus saving a lot time.
235 # Arch files and directories, thus saving a lot time.
236 if not self._exclude(p):
236 if not self._exclude(p):
237 if os.path.isdir(p):
237 if os.path.isdir(p):
238 contents += [os.path.join(c, f) for f in os.listdir(p)]
238 contents += [os.path.join(c, f) for f in os.listdir(p)]
239 else:
239 else:
240 files.append(c)
240 files.append(c)
241 return files
241 return files
242
242
243 def _rendirchanges(self, src, dest):
243 def _rendirchanges(self, src, dest):
244 changes = []
244 changes = []
245 copies = {}
245 copies = {}
246 files = self._readcontents(os.path.join(self.tmppath, dest))
246 files = self._readcontents(os.path.join(self.tmppath, dest))
247 for f in files:
247 for f in files:
248 s = os.path.join(src, f)
248 s = os.path.join(src, f)
249 d = os.path.join(dest, f)
249 d = os.path.join(dest, f)
250 changes.append(s)
250 changes.append(s)
251 changes.append(d)
251 changes.append(d)
252 copies[d] = s
252 copies[d] = s
253 return changes, copies
253 return changes, copies
254
254
255 def _obtainrevision(self, rev):
255 def _obtainrevision(self, rev):
256 self.ui.debug('obtaining revision %s...\n' % rev)
256 self.ui.debug('obtaining revision %s...\n' % rev)
257 output = self._execute('get', rev, self.tmppath)
257 output = self._execute('get', rev, self.tmppath)
258 self.checkexit(output)
258 self.checkexit(output)
259 self.ui.debug('analyzing revision %s...\n' % rev)
259 self.ui.debug('analyzing revision %s...\n' % rev)
260 files = self._readcontents(self.tmppath)
260 files = self._readcontents(self.tmppath)
261 self.changes[rev].add_files += files
261 self.changes[rev].add_files += files
262
262
263 def _stripbasepath(self, path):
263 def _stripbasepath(self, path):
264 if path.startswith('./'):
264 if path.startswith('./'):
265 return path[2:]
265 return path[2:]
266 return path
266 return path
267
267
268 def _parsecatlog(self, data, rev):
268 def _parsecatlog(self, data, rev):
269 try:
269 try:
270 catlog = self.catlogparser.parsestr(data)
270 catlog = self.catlogparser.parsestr(data)
271
271
272 # Commit date
272 # Commit date
273 self.changes[rev].date = util.datestr(
273 self.changes[rev].date = util.datestr(
274 util.strdate(catlog['Standard-date'],
274 util.strdate(catlog['Standard-date'],
275 '%Y-%m-%d %H:%M:%S'))
275 '%Y-%m-%d %H:%M:%S'))
276
276
277 # Commit author
277 # Commit author
278 self.changes[rev].author = self.recode(catlog['Creator'])
278 self.changes[rev].author = self.recode(catlog['Creator'])
279
279
280 # Commit description
280 # Commit description
281 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
281 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
282 catlog.get_payload()))
282 catlog.get_payload()))
283 self.changes[rev].summary = self.recode(self.changes[rev].summary)
283 self.changes[rev].summary = self.recode(self.changes[rev].summary)
284
284
285 # Commit revision origin when dealing with a branch or tag
285 # Commit revision origin when dealing with a branch or tag
286 if 'Continuation-of' in catlog:
286 if 'Continuation-of' in catlog:
287 self.changes[rev].continuationof = self.recode(
287 self.changes[rev].continuationof = self.recode(
288 catlog['Continuation-of'])
288 catlog['Continuation-of'])
289 except Exception:
289 except Exception:
290 raise util.Abort(_('could not parse cat-log of %s') % rev)
290 raise util.Abort(_('could not parse cat-log of %s') % rev)
291
291
292 def _parsechangeset(self, data, rev):
292 def _parsechangeset(self, data, rev):
293 for l in data:
293 for l in data:
294 l = l.strip()
294 l = l.strip()
295 # Added file (ignore added directory)
295 # Added file (ignore added directory)
296 if l.startswith('A') and not l.startswith('A/'):
296 if l.startswith('A') and not l.startswith('A/'):
297 file = self._stripbasepath(l[1:].strip())
297 file = self._stripbasepath(l[1:].strip())
298 if not self._exclude(file):
298 if not self._exclude(file):
299 self.changes[rev].add_files.append(file)
299 self.changes[rev].add_files.append(file)
300 # Deleted file (ignore deleted directory)
300 # Deleted file (ignore deleted directory)
301 elif l.startswith('D') and not l.startswith('D/'):
301 elif l.startswith('D') and not l.startswith('D/'):
302 file = self._stripbasepath(l[1:].strip())
302 file = self._stripbasepath(l[1:].strip())
303 if not self._exclude(file):
303 if not self._exclude(file):
304 self.changes[rev].del_files.append(file)
304 self.changes[rev].del_files.append(file)
305 # Modified binary file
305 # Modified binary file
306 elif l.startswith('Mb'):
306 elif l.startswith('Mb'):
307 file = self._stripbasepath(l[2:].strip())
307 file = self._stripbasepath(l[2:].strip())
308 if not self._exclude(file):
308 if not self._exclude(file):
309 self.changes[rev].mod_files.append(file)
309 self.changes[rev].mod_files.append(file)
310 # Modified link
310 # Modified link
311 elif l.startswith('M->'):
311 elif l.startswith('M->'):
312 file = self._stripbasepath(l[3:].strip())
312 file = self._stripbasepath(l[3:].strip())
313 if not self._exclude(file):
313 if not self._exclude(file):
314 self.changes[rev].mod_files.append(file)
314 self.changes[rev].mod_files.append(file)
315 # Modified file
315 # Modified file
316 elif l.startswith('M'):
316 elif l.startswith('M'):
317 file = self._stripbasepath(l[1:].strip())
317 file = self._stripbasepath(l[1:].strip())
318 if not self._exclude(file):
318 if not self._exclude(file):
319 self.changes[rev].mod_files.append(file)
319 self.changes[rev].mod_files.append(file)
320 # Renamed file (or link)
320 # Renamed file (or link)
321 elif l.startswith('=>'):
321 elif l.startswith('=>'):
322 files = l[2:].strip().split(' ')
322 files = l[2:].strip().split(' ')
323 if len(files) == 1:
323 if len(files) == 1:
324 files = l[2:].strip().split('\t')
324 files = l[2:].strip().split('\t')
325 src = self._stripbasepath(files[0])
325 src = self._stripbasepath(files[0])
326 dst = self._stripbasepath(files[1])
326 dst = self._stripbasepath(files[1])
327 if not self._exclude(src) and not self._exclude(dst):
327 if not self._exclude(src) and not self._exclude(dst):
328 self.changes[rev].ren_files[src] = dst
328 self.changes[rev].ren_files[src] = dst
329 # Conversion from file to link or from link to file (modified)
329 # Conversion from file to link or from link to file (modified)
330 elif l.startswith('ch'):
330 elif l.startswith('ch'):
331 file = self._stripbasepath(l[2:].strip())
331 file = self._stripbasepath(l[2:].strip())
332 if not self._exclude(file):
332 if not self._exclude(file):
333 self.changes[rev].mod_files.append(file)
333 self.changes[rev].mod_files.append(file)
334 # Renamed directory
334 # Renamed directory
335 elif l.startswith('/>'):
335 elif l.startswith('/>'):
336 dirs = l[2:].strip().split(' ')
336 dirs = l[2:].strip().split(' ')
337 if len(dirs) == 1:
337 if len(dirs) == 1:
338 dirs = l[2:].strip().split('\t')
338 dirs = l[2:].strip().split('\t')
339 src = self._stripbasepath(dirs[0])
339 src = self._stripbasepath(dirs[0])
340 dst = self._stripbasepath(dirs[1])
340 dst = self._stripbasepath(dirs[1])
341 if not self._exclude(src) and not self._exclude(dst):
341 if not self._exclude(src) and not self._exclude(dst):
342 self.changes[rev].ren_dirs[src] = dst
342 self.changes[rev].ren_dirs[src] = dst
@@ -1,561 +1,564
1 # hg.py - hg backend for convert extension
1 # hg.py - hg backend for convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 # Notes for hg->hg conversion:
8 # Notes for hg->hg conversion:
9 #
9 #
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 # of commit messages, but new versions do. Changesets created by
11 # of commit messages, but new versions do. Changesets created by
12 # those older versions, then converted, may thus have different
12 # those older versions, then converted, may thus have different
13 # hashes for changesets that are otherwise identical.
13 # hashes for changesets that are otherwise identical.
14 #
14 #
15 # * Using "--config convert.hg.saverev=true" will make the source
15 # * Using "--config convert.hg.saverev=true" will make the source
16 # identifier to be stored in the converted revision. This will cause
16 # identifier to be stored in the converted revision. This will cause
17 # the converted revision to have a different identity than the
17 # the converted revision to have a different identity than the
18 # source.
18 # source.
19
19
20
20
21 import os, time, cStringIO
21 import os, time, cStringIO
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 from mercurial.node import bin, hex, nullid
23 from mercurial.node import bin, hex, nullid
24 from mercurial import hg, util, context, bookmarks, error, scmutil, exchange
24 from mercurial import hg, util, context, bookmarks, error, scmutil, exchange
25 from mercurial import phases
25 from mercurial import phases
26
26
27 from common import NoRepo, commit, converter_source, converter_sink, mapfile
27 from common import NoRepo, commit, converter_source, converter_sink, mapfile
28
28
29 import re
29 import re
30 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
30 sha1re = re.compile(r'\b[0-9a-f]{12,40}\b')
31
31
32 class mercurial_sink(converter_sink):
32 class mercurial_sink(converter_sink):
33 def __init__(self, ui, path):
33 def __init__(self, ui, path):
34 converter_sink.__init__(self, ui, path)
34 converter_sink.__init__(self, ui, path)
35 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
35 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
36 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
36 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
37 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
37 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
38 self.lastbranch = None
38 self.lastbranch = None
39 if os.path.isdir(path) and len(os.listdir(path)) > 0:
39 if os.path.isdir(path) and len(os.listdir(path)) > 0:
40 try:
40 try:
41 self.repo = hg.repository(self.ui, path)
41 self.repo = hg.repository(self.ui, path)
42 if not self.repo.local():
42 if not self.repo.local():
43 raise NoRepo(_('%s is not a local Mercurial repository')
43 raise NoRepo(_('%s is not a local Mercurial repository')
44 % path)
44 % path)
45 except error.RepoError as err:
45 except error.RepoError as err:
46 ui.traceback()
46 ui.traceback()
47 raise NoRepo(err.args[0])
47 raise NoRepo(err.args[0])
48 else:
48 else:
49 try:
49 try:
50 ui.status(_('initializing destination %s repository\n') % path)
50 ui.status(_('initializing destination %s repository\n') % path)
51 self.repo = hg.repository(self.ui, path, create=True)
51 self.repo = hg.repository(self.ui, path, create=True)
52 if not self.repo.local():
52 if not self.repo.local():
53 raise NoRepo(_('%s is not a local Mercurial repository')
53 raise NoRepo(_('%s is not a local Mercurial repository')
54 % path)
54 % path)
55 self.created.append(path)
55 self.created.append(path)
56 except error.RepoError:
56 except error.RepoError:
57 ui.traceback()
57 ui.traceback()
58 raise NoRepo(_("could not create hg repository %s as sink")
58 raise NoRepo(_("could not create hg repository %s as sink")
59 % path)
59 % path)
60 self.lock = None
60 self.lock = None
61 self.wlock = None
61 self.wlock = None
62 self.filemapmode = False
62 self.filemapmode = False
63 self.subrevmaps = {}
63 self.subrevmaps = {}
64
64
65 def before(self):
65 def before(self):
66 self.ui.debug('run hg sink pre-conversion action\n')
66 self.ui.debug('run hg sink pre-conversion action\n')
67 self.wlock = self.repo.wlock()
67 self.wlock = self.repo.wlock()
68 self.lock = self.repo.lock()
68 self.lock = self.repo.lock()
69
69
70 def after(self):
70 def after(self):
71 self.ui.debug('run hg sink post-conversion action\n')
71 self.ui.debug('run hg sink post-conversion action\n')
72 if self.lock:
72 if self.lock:
73 self.lock.release()
73 self.lock.release()
74 if self.wlock:
74 if self.wlock:
75 self.wlock.release()
75 self.wlock.release()
76
76
77 def revmapfile(self):
77 def revmapfile(self):
78 return self.repo.join("shamap")
78 return self.repo.join("shamap")
79
79
80 def authorfile(self):
80 def authorfile(self):
81 return self.repo.join("authormap")
81 return self.repo.join("authormap")
82
82
83 def setbranch(self, branch, pbranches):
83 def setbranch(self, branch, pbranches):
84 if not self.clonebranches:
84 if not self.clonebranches:
85 return
85 return
86
86
87 setbranch = (branch != self.lastbranch)
87 setbranch = (branch != self.lastbranch)
88 self.lastbranch = branch
88 self.lastbranch = branch
89 if not branch:
89 if not branch:
90 branch = 'default'
90 branch = 'default'
91 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
91 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
92 if pbranches:
92 if pbranches:
93 pbranch = pbranches[0][1]
93 pbranch = pbranches[0][1]
94 else:
94 else:
95 pbranch = 'default'
95 pbranch = 'default'
96
96
97 branchpath = os.path.join(self.path, branch)
97 branchpath = os.path.join(self.path, branch)
98 if setbranch:
98 if setbranch:
99 self.after()
99 self.after()
100 try:
100 try:
101 self.repo = hg.repository(self.ui, branchpath)
101 self.repo = hg.repository(self.ui, branchpath)
102 except Exception:
102 except Exception:
103 self.repo = hg.repository(self.ui, branchpath, create=True)
103 self.repo = hg.repository(self.ui, branchpath, create=True)
104 self.before()
104 self.before()
105
105
106 # pbranches may bring revisions from other branches (merge parents)
106 # pbranches may bring revisions from other branches (merge parents)
107 # Make sure we have them, or pull them.
107 # Make sure we have them, or pull them.
108 missings = {}
108 missings = {}
109 for b in pbranches:
109 for b in pbranches:
110 try:
110 try:
111 self.repo.lookup(b[0])
111 self.repo.lookup(b[0])
112 except Exception:
112 except Exception:
113 missings.setdefault(b[1], []).append(b[0])
113 missings.setdefault(b[1], []).append(b[0])
114
114
115 if missings:
115 if missings:
116 self.after()
116 self.after()
117 for pbranch, heads in sorted(missings.iteritems()):
117 for pbranch, heads in sorted(missings.iteritems()):
118 pbranchpath = os.path.join(self.path, pbranch)
118 pbranchpath = os.path.join(self.path, pbranch)
119 prepo = hg.peer(self.ui, {}, pbranchpath)
119 prepo = hg.peer(self.ui, {}, pbranchpath)
120 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
120 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
121 exchange.pull(self.repo, prepo,
121 exchange.pull(self.repo, prepo,
122 [prepo.lookup(h) for h in heads])
122 [prepo.lookup(h) for h in heads])
123 self.before()
123 self.before()
124
124
125 def _rewritetags(self, source, revmap, data):
125 def _rewritetags(self, source, revmap, data):
126 fp = cStringIO.StringIO()
126 fp = cStringIO.StringIO()
127 for line in data.splitlines():
127 for line in data.splitlines():
128 s = line.split(' ', 1)
128 s = line.split(' ', 1)
129 if len(s) != 2:
129 if len(s) != 2:
130 continue
130 continue
131 revid = revmap.get(source.lookuprev(s[0]))
131 revid = revmap.get(source.lookuprev(s[0]))
132 if not revid:
132 if not revid:
133 if s[0] == hex(nullid):
133 if s[0] == hex(nullid):
134 revid = s[0]
134 revid = s[0]
135 else:
135 else:
136 continue
136 continue
137 fp.write('%s %s\n' % (revid, s[1]))
137 fp.write('%s %s\n' % (revid, s[1]))
138 return fp.getvalue()
138 return fp.getvalue()
139
139
140 def _rewritesubstate(self, source, data):
140 def _rewritesubstate(self, source, data):
141 fp = cStringIO.StringIO()
141 fp = cStringIO.StringIO()
142 for line in data.splitlines():
142 for line in data.splitlines():
143 s = line.split(' ', 1)
143 s = line.split(' ', 1)
144 if len(s) != 2:
144 if len(s) != 2:
145 continue
145 continue
146
146
147 revid = s[0]
147 revid = s[0]
148 subpath = s[1]
148 subpath = s[1]
149 if revid != hex(nullid):
149 if revid != hex(nullid):
150 revmap = self.subrevmaps.get(subpath)
150 revmap = self.subrevmaps.get(subpath)
151 if revmap is None:
151 if revmap is None:
152 revmap = mapfile(self.ui,
152 revmap = mapfile(self.ui,
153 self.repo.wjoin(subpath, '.hg/shamap'))
153 self.repo.wjoin(subpath, '.hg/shamap'))
154 self.subrevmaps[subpath] = revmap
154 self.subrevmaps[subpath] = revmap
155
155
156 # It is reasonable that one or more of the subrepos don't
156 # It is reasonable that one or more of the subrepos don't
157 # need to be converted, in which case they can be cloned
157 # need to be converted, in which case they can be cloned
158 # into place instead of converted. Therefore, only warn
158 # into place instead of converted. Therefore, only warn
159 # once.
159 # once.
160 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
160 msg = _('no ".hgsubstate" updates will be made for "%s"\n')
161 if len(revmap) == 0:
161 if len(revmap) == 0:
162 sub = self.repo.wvfs.reljoin(subpath, '.hg')
162 sub = self.repo.wvfs.reljoin(subpath, '.hg')
163
163
164 if self.repo.wvfs.exists(sub):
164 if self.repo.wvfs.exists(sub):
165 self.ui.warn(msg % subpath)
165 self.ui.warn(msg % subpath)
166
166
167 newid = revmap.get(revid)
167 newid = revmap.get(revid)
168 if not newid:
168 if not newid:
169 if len(revmap) > 0:
169 if len(revmap) > 0:
170 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
170 self.ui.warn(_("%s is missing from %s/.hg/shamap\n") %
171 (revid, subpath))
171 (revid, subpath))
172 else:
172 else:
173 revid = newid
173 revid = newid
174
174
175 fp.write('%s %s\n' % (revid, subpath))
175 fp.write('%s %s\n' % (revid, subpath))
176
176
177 return fp.getvalue()
177 return fp.getvalue()
178
178
179 def putcommit(self, files, copies, parents, commit, source, revmap, full,
179 def putcommit(self, files, copies, parents, commit, source, revmap, full,
180 cleanp2):
180 cleanp2):
181 files = dict(files)
181 files = dict(files)
182
182
183 def getfilectx(repo, memctx, f):
183 def getfilectx(repo, memctx, f):
184 if p2ctx and f in cleanp2 and f not in copies:
184 if p2ctx and f in cleanp2 and f not in copies:
185 self.ui.debug('reusing %s from p2\n' % f)
185 self.ui.debug('reusing %s from p2\n' % f)
186 return p2ctx[f]
186 return p2ctx[f]
187 try:
187 try:
188 v = files[f]
188 v = files[f]
189 except KeyError:
189 except KeyError:
190 return None
190 return None
191 data, mode = source.getfile(f, v)
191 data, mode = source.getfile(f, v)
192 if data is None:
192 if data is None:
193 return None
193 return None
194 if f == '.hgtags':
194 if f == '.hgtags':
195 data = self._rewritetags(source, revmap, data)
195 data = self._rewritetags(source, revmap, data)
196 if f == '.hgsubstate':
196 if f == '.hgsubstate':
197 data = self._rewritesubstate(source, data)
197 data = self._rewritesubstate(source, data)
198 return context.memfilectx(self.repo, f, data, 'l' in mode,
198 return context.memfilectx(self.repo, f, data, 'l' in mode,
199 'x' in mode, copies.get(f))
199 'x' in mode, copies.get(f))
200
200
201 pl = []
201 pl = []
202 for p in parents:
202 for p in parents:
203 if p not in pl:
203 if p not in pl:
204 pl.append(p)
204 pl.append(p)
205 parents = pl
205 parents = pl
206 nparents = len(parents)
206 nparents = len(parents)
207 if self.filemapmode and nparents == 1:
207 if self.filemapmode and nparents == 1:
208 m1node = self.repo.changelog.read(bin(parents[0]))[0]
208 m1node = self.repo.changelog.read(bin(parents[0]))[0]
209 parent = parents[0]
209 parent = parents[0]
210
210
211 if len(parents) < 2:
211 if len(parents) < 2:
212 parents.append(nullid)
212 parents.append(nullid)
213 if len(parents) < 2:
213 if len(parents) < 2:
214 parents.append(nullid)
214 parents.append(nullid)
215 p2 = parents.pop(0)
215 p2 = parents.pop(0)
216
216
217 text = commit.desc
217 text = commit.desc
218
218
219 sha1s = re.findall(sha1re, text)
219 sha1s = re.findall(sha1re, text)
220 for sha1 in sha1s:
220 for sha1 in sha1s:
221 oldrev = source.lookuprev(sha1)
221 oldrev = source.lookuprev(sha1)
222 newrev = revmap.get(oldrev)
222 newrev = revmap.get(oldrev)
223 if newrev is not None:
223 if newrev is not None:
224 text = text.replace(sha1, newrev[:len(sha1)])
224 text = text.replace(sha1, newrev[:len(sha1)])
225
225
226 extra = commit.extra.copy()
226 extra = commit.extra.copy()
227
227
228 for label in ('source', 'transplant_source', 'rebase_source',
228 for label in ('source', 'transplant_source', 'rebase_source',
229 'intermediate-source'):
229 'intermediate-source'):
230 node = extra.get(label)
230 node = extra.get(label)
231
231
232 if node is None:
232 if node is None:
233 continue
233 continue
234
234
235 # Only transplant stores its reference in binary
235 # Only transplant stores its reference in binary
236 if label == 'transplant_source':
236 if label == 'transplant_source':
237 node = hex(node)
237 node = hex(node)
238
238
239 newrev = revmap.get(node)
239 newrev = revmap.get(node)
240 if newrev is not None:
240 if newrev is not None:
241 if label == 'transplant_source':
241 if label == 'transplant_source':
242 newrev = bin(newrev)
242 newrev = bin(newrev)
243
243
244 extra[label] = newrev
244 extra[label] = newrev
245
245
246 if self.branchnames and commit.branch:
246 if self.branchnames and commit.branch:
247 extra['branch'] = commit.branch
247 extra['branch'] = commit.branch
248 if commit.rev and commit.saverev:
248 if commit.rev and commit.saverev:
249 extra['convert_revision'] = commit.rev
249 extra['convert_revision'] = commit.rev
250
250
251 while parents:
251 while parents:
252 p1 = p2
252 p1 = p2
253 p2 = parents.pop(0)
253 p2 = parents.pop(0)
254 p2ctx = None
254 p2ctx = None
255 if p2 != nullid:
255 if p2 != nullid:
256 p2ctx = self.repo[p2]
256 p2ctx = self.repo[p2]
257 fileset = set(files)
257 fileset = set(files)
258 if full:
258 if full:
259 fileset.update(self.repo[p1])
259 fileset.update(self.repo[p1])
260 fileset.update(self.repo[p2])
260 fileset.update(self.repo[p2])
261 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
261 ctx = context.memctx(self.repo, (p1, p2), text, fileset,
262 getfilectx, commit.author, commit.date, extra)
262 getfilectx, commit.author, commit.date, extra)
263
263
264 # We won't know if the conversion changes the node until after the
264 # We won't know if the conversion changes the node until after the
265 # commit, so copy the source's phase for now.
265 # commit, so copy the source's phase for now.
266 self.repo.ui.setconfig('phases', 'new-commit',
266 self.repo.ui.setconfig('phases', 'new-commit',
267 phases.phasenames[commit.phase], 'convert')
267 phases.phasenames[commit.phase], 'convert')
268
268
269 tr = self.repo.transaction("convert")
269 tr = self.repo.transaction("convert")
270
270
271 try:
271 try:
272 node = hex(self.repo.commitctx(ctx))
272 node = hex(self.repo.commitctx(ctx))
273
273
274 # If the node value has changed, but the phase is lower than
274 # If the node value has changed, but the phase is lower than
275 # draft, set it back to draft since it hasn't been exposed
275 # draft, set it back to draft since it hasn't been exposed
276 # anywhere.
276 # anywhere.
277 if commit.rev != node:
277 if commit.rev != node:
278 ctx = self.repo[node]
278 ctx = self.repo[node]
279 if ctx.phase() < phases.draft:
279 if ctx.phase() < phases.draft:
280 phases.retractboundary(self.repo, tr, phases.draft,
280 phases.retractboundary(self.repo, tr, phases.draft,
281 [ctx.node()])
281 [ctx.node()])
282 tr.close()
282 tr.close()
283 finally:
283 finally:
284 tr.release()
284 tr.release()
285
285
286 text = "(octopus merge fixup)\n"
286 text = "(octopus merge fixup)\n"
287 p2 = node
287 p2 = node
288
288
289 if self.filemapmode and nparents == 1:
289 if self.filemapmode and nparents == 1:
290 man = self.repo.manifest
290 man = self.repo.manifest
291 mnode = self.repo.changelog.read(bin(p2))[0]
291 mnode = self.repo.changelog.read(bin(p2))[0]
292 closed = 'close' in commit.extra
292 closed = 'close' in commit.extra
293 if not closed and not man.cmp(m1node, man.revision(mnode)):
293 if not closed and not man.cmp(m1node, man.revision(mnode)):
294 self.ui.status(_("filtering out empty revision\n"))
294 self.ui.status(_("filtering out empty revision\n"))
295 self.repo.rollback(force=True)
295 self.repo.rollback(force=True)
296 return parent
296 return parent
297 return p2
297 return p2
298
298
299 def puttags(self, tags):
299 def puttags(self, tags):
300 try:
300 try:
301 parentctx = self.repo[self.tagsbranch]
301 parentctx = self.repo[self.tagsbranch]
302 tagparent = parentctx.node()
302 tagparent = parentctx.node()
303 except error.RepoError:
303 except error.RepoError:
304 parentctx = None
304 parentctx = None
305 tagparent = nullid
305 tagparent = nullid
306
306
307 oldlines = set()
307 oldlines = set()
308 for branch, heads in self.repo.branchmap().iteritems():
308 for branch, heads in self.repo.branchmap().iteritems():
309 for h in heads:
309 for h in heads:
310 if '.hgtags' in self.repo[h]:
310 if '.hgtags' in self.repo[h]:
311 oldlines.update(
311 oldlines.update(
312 set(self.repo[h]['.hgtags'].data().splitlines(True)))
312 set(self.repo[h]['.hgtags'].data().splitlines(True)))
313 oldlines = sorted(list(oldlines))
313 oldlines = sorted(list(oldlines))
314
314
315 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
315 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
316 if newlines == oldlines:
316 if newlines == oldlines:
317 return None, None
317 return None, None
318
318
319 # if the old and new tags match, then there is nothing to update
319 # if the old and new tags match, then there is nothing to update
320 oldtags = set()
320 oldtags = set()
321 newtags = set()
321 newtags = set()
322 for line in oldlines:
322 for line in oldlines:
323 s = line.strip().split(' ', 1)
323 s = line.strip().split(' ', 1)
324 if len(s) != 2:
324 if len(s) != 2:
325 continue
325 continue
326 oldtags.add(s[1])
326 oldtags.add(s[1])
327 for line in newlines:
327 for line in newlines:
328 s = line.strip().split(' ', 1)
328 s = line.strip().split(' ', 1)
329 if len(s) != 2:
329 if len(s) != 2:
330 continue
330 continue
331 if s[1] not in oldtags:
331 if s[1] not in oldtags:
332 newtags.add(s[1].strip())
332 newtags.add(s[1].strip())
333
333
334 if not newtags:
334 if not newtags:
335 return None, None
335 return None, None
336
336
337 data = "".join(newlines)
337 data = "".join(newlines)
338 def getfilectx(repo, memctx, f):
338 def getfilectx(repo, memctx, f):
339 return context.memfilectx(repo, f, data, False, False, None)
339 return context.memfilectx(repo, f, data, False, False, None)
340
340
341 self.ui.status(_("updating tags\n"))
341 self.ui.status(_("updating tags\n"))
342 date = "%s 0" % int(time.mktime(time.gmtime()))
342 date = "%s 0" % int(time.mktime(time.gmtime()))
343 extra = {'branch': self.tagsbranch}
343 extra = {'branch': self.tagsbranch}
344 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
344 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
345 [".hgtags"], getfilectx, "convert-repo", date,
345 [".hgtags"], getfilectx, "convert-repo", date,
346 extra)
346 extra)
347 node = self.repo.commitctx(ctx)
347 node = self.repo.commitctx(ctx)
348 return hex(node), hex(tagparent)
348 return hex(node), hex(tagparent)
349
349
350 def setfilemapmode(self, active):
350 def setfilemapmode(self, active):
351 self.filemapmode = active
351 self.filemapmode = active
352
352
353 def putbookmarks(self, updatedbookmark):
353 def putbookmarks(self, updatedbookmark):
354 if not len(updatedbookmark):
354 if not len(updatedbookmark):
355 return
355 return
356
356
357 self.ui.status(_("updating bookmarks\n"))
357 self.ui.status(_("updating bookmarks\n"))
358 destmarks = self.repo._bookmarks
358 destmarks = self.repo._bookmarks
359 for bookmark in updatedbookmark:
359 for bookmark in updatedbookmark:
360 destmarks[bookmark] = bin(updatedbookmark[bookmark])
360 destmarks[bookmark] = bin(updatedbookmark[bookmark])
361 destmarks.write()
361 destmarks.write()
362
362
363 def hascommitfrommap(self, rev):
363 def hascommitfrommap(self, rev):
364 # the exact semantics of clonebranches is unclear so we can't say no
364 # the exact semantics of clonebranches is unclear so we can't say no
365 return rev in self.repo or self.clonebranches
365 return rev in self.repo or self.clonebranches
366
366
367 def hascommitforsplicemap(self, rev):
367 def hascommitforsplicemap(self, rev):
368 if rev not in self.repo and self.clonebranches:
368 if rev not in self.repo and self.clonebranches:
369 raise util.Abort(_('revision %s not found in destination '
369 raise util.Abort(_('revision %s not found in destination '
370 'repository (lookups with clonebranches=true '
370 'repository (lookups with clonebranches=true '
371 'are not implemented)') % rev)
371 'are not implemented)') % rev)
372 return rev in self.repo
372 return rev in self.repo
373
373
374 class mercurial_source(converter_source):
374 class mercurial_source(converter_source):
375 def __init__(self, ui, path, rev=None):
375 def __init__(self, ui, path, revs=None):
376 converter_source.__init__(self, ui, path, rev)
376 converter_source.__init__(self, ui, path, revs)
377 if revs and len(revs) > 1:
378 raise util.Abort(_("mercurial source does not support specifying "
379 "multiple revisions"))
377 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
380 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
378 self.ignored = set()
381 self.ignored = set()
379 self.saverev = ui.configbool('convert', 'hg.saverev', False)
382 self.saverev = ui.configbool('convert', 'hg.saverev', False)
380 try:
383 try:
381 self.repo = hg.repository(self.ui, path)
384 self.repo = hg.repository(self.ui, path)
382 # try to provoke an exception if this isn't really a hg
385 # try to provoke an exception if this isn't really a hg
383 # repo, but some other bogus compatible-looking url
386 # repo, but some other bogus compatible-looking url
384 if not self.repo.local():
387 if not self.repo.local():
385 raise error.RepoError
388 raise error.RepoError
386 except error.RepoError:
389 except error.RepoError:
387 ui.traceback()
390 ui.traceback()
388 raise NoRepo(_("%s is not a local Mercurial repository") % path)
391 raise NoRepo(_("%s is not a local Mercurial repository") % path)
389 self.lastrev = None
392 self.lastrev = None
390 self.lastctx = None
393 self.lastctx = None
391 self._changescache = None, None
394 self._changescache = None, None
392 self.convertfp = None
395 self.convertfp = None
393 # Restrict converted revisions to startrev descendants
396 # Restrict converted revisions to startrev descendants
394 startnode = ui.config('convert', 'hg.startrev')
397 startnode = ui.config('convert', 'hg.startrev')
395 hgrevs = ui.config('convert', 'hg.revs')
398 hgrevs = ui.config('convert', 'hg.revs')
396 if hgrevs is None:
399 if hgrevs is None:
397 if startnode is not None:
400 if startnode is not None:
398 try:
401 try:
399 startnode = self.repo.lookup(startnode)
402 startnode = self.repo.lookup(startnode)
400 except error.RepoError:
403 except error.RepoError:
401 raise util.Abort(_('%s is not a valid start revision')
404 raise util.Abort(_('%s is not a valid start revision')
402 % startnode)
405 % startnode)
403 startrev = self.repo.changelog.rev(startnode)
406 startrev = self.repo.changelog.rev(startnode)
404 children = {startnode: 1}
407 children = {startnode: 1}
405 for r in self.repo.changelog.descendants([startrev]):
408 for r in self.repo.changelog.descendants([startrev]):
406 children[self.repo.changelog.node(r)] = 1
409 children[self.repo.changelog.node(r)] = 1
407 self.keep = children.__contains__
410 self.keep = children.__contains__
408 else:
411 else:
409 self.keep = util.always
412 self.keep = util.always
410 if rev:
413 if revs:
411 self._heads = [self.repo[rev].node()]
414 self._heads = [self.repo[revs[0]].node()]
412 else:
415 else:
413 self._heads = self.repo.heads()
416 self._heads = self.repo.heads()
414 else:
417 else:
415 if rev or startnode is not None:
418 if revs or startnode is not None:
416 raise util.Abort(_('hg.revs cannot be combined with '
419 raise util.Abort(_('hg.revs cannot be combined with '
417 'hg.startrev or --rev'))
420 'hg.startrev or --rev'))
418 nodes = set()
421 nodes = set()
419 parents = set()
422 parents = set()
420 for r in scmutil.revrange(self.repo, [hgrevs]):
423 for r in scmutil.revrange(self.repo, [hgrevs]):
421 ctx = self.repo[r]
424 ctx = self.repo[r]
422 nodes.add(ctx.node())
425 nodes.add(ctx.node())
423 parents.update(p.node() for p in ctx.parents())
426 parents.update(p.node() for p in ctx.parents())
424 self.keep = nodes.__contains__
427 self.keep = nodes.__contains__
425 self._heads = nodes - parents
428 self._heads = nodes - parents
426
429
427 def changectx(self, rev):
430 def changectx(self, rev):
428 if self.lastrev != rev:
431 if self.lastrev != rev:
429 self.lastctx = self.repo[rev]
432 self.lastctx = self.repo[rev]
430 self.lastrev = rev
433 self.lastrev = rev
431 return self.lastctx
434 return self.lastctx
432
435
433 def parents(self, ctx):
436 def parents(self, ctx):
434 return [p for p in ctx.parents() if p and self.keep(p.node())]
437 return [p for p in ctx.parents() if p and self.keep(p.node())]
435
438
436 def getheads(self):
439 def getheads(self):
437 return [hex(h) for h in self._heads if self.keep(h)]
440 return [hex(h) for h in self._heads if self.keep(h)]
438
441
439 def getfile(self, name, rev):
442 def getfile(self, name, rev):
440 try:
443 try:
441 fctx = self.changectx(rev)[name]
444 fctx = self.changectx(rev)[name]
442 return fctx.data(), fctx.flags()
445 return fctx.data(), fctx.flags()
443 except error.LookupError:
446 except error.LookupError:
444 return None, None
447 return None, None
445
448
446 def getchanges(self, rev, full):
449 def getchanges(self, rev, full):
447 ctx = self.changectx(rev)
450 ctx = self.changectx(rev)
448 parents = self.parents(ctx)
451 parents = self.parents(ctx)
449 if full or not parents:
452 if full or not parents:
450 files = copyfiles = ctx.manifest()
453 files = copyfiles = ctx.manifest()
451 if parents:
454 if parents:
452 if self._changescache[0] == rev:
455 if self._changescache[0] == rev:
453 m, a, r = self._changescache[1]
456 m, a, r = self._changescache[1]
454 else:
457 else:
455 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
458 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
456 if not full:
459 if not full:
457 files = m + a + r
460 files = m + a + r
458 copyfiles = m + a
461 copyfiles = m + a
459 # getcopies() is also run for roots and before filtering so missing
462 # getcopies() is also run for roots and before filtering so missing
460 # revlogs are detected early
463 # revlogs are detected early
461 copies = self.getcopies(ctx, parents, copyfiles)
464 copies = self.getcopies(ctx, parents, copyfiles)
462 cleanp2 = set()
465 cleanp2 = set()
463 if len(parents) == 2:
466 if len(parents) == 2:
464 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
467 cleanp2.update(self.repo.status(parents[1].node(), ctx.node(),
465 clean=True).clean)
468 clean=True).clean)
466 changes = [(f, rev) for f in files if f not in self.ignored]
469 changes = [(f, rev) for f in files if f not in self.ignored]
467 changes.sort()
470 changes.sort()
468 return changes, copies, cleanp2
471 return changes, copies, cleanp2
469
472
470 def getcopies(self, ctx, parents, files):
473 def getcopies(self, ctx, parents, files):
471 copies = {}
474 copies = {}
472 for name in files:
475 for name in files:
473 if name in self.ignored:
476 if name in self.ignored:
474 continue
477 continue
475 try:
478 try:
476 copysource, _copynode = ctx.filectx(name).renamed()
479 copysource, _copynode = ctx.filectx(name).renamed()
477 if copysource in self.ignored:
480 if copysource in self.ignored:
478 continue
481 continue
479 # Ignore copy sources not in parent revisions
482 # Ignore copy sources not in parent revisions
480 found = False
483 found = False
481 for p in parents:
484 for p in parents:
482 if copysource in p:
485 if copysource in p:
483 found = True
486 found = True
484 break
487 break
485 if not found:
488 if not found:
486 continue
489 continue
487 copies[name] = copysource
490 copies[name] = copysource
488 except TypeError:
491 except TypeError:
489 pass
492 pass
490 except error.LookupError as e:
493 except error.LookupError as e:
491 if not self.ignoreerrors:
494 if not self.ignoreerrors:
492 raise
495 raise
493 self.ignored.add(name)
496 self.ignored.add(name)
494 self.ui.warn(_('ignoring: %s\n') % e)
497 self.ui.warn(_('ignoring: %s\n') % e)
495 return copies
498 return copies
496
499
497 def getcommit(self, rev):
500 def getcommit(self, rev):
498 ctx = self.changectx(rev)
501 ctx = self.changectx(rev)
499 parents = [p.hex() for p in self.parents(ctx)]
502 parents = [p.hex() for p in self.parents(ctx)]
500 crev = rev
503 crev = rev
501
504
502 return commit(author=ctx.user(),
505 return commit(author=ctx.user(),
503 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
506 date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'),
504 desc=ctx.description(), rev=crev, parents=parents,
507 desc=ctx.description(), rev=crev, parents=parents,
505 branch=ctx.branch(), extra=ctx.extra(),
508 branch=ctx.branch(), extra=ctx.extra(),
506 sortkey=ctx.rev(), saverev=self.saverev,
509 sortkey=ctx.rev(), saverev=self.saverev,
507 phase=ctx.phase())
510 phase=ctx.phase())
508
511
509 def gettags(self):
512 def gettags(self):
510 # This will get written to .hgtags, filter non global tags out.
513 # This will get written to .hgtags, filter non global tags out.
511 tags = [t for t in self.repo.tagslist()
514 tags = [t for t in self.repo.tagslist()
512 if self.repo.tagtype(t[0]) == 'global']
515 if self.repo.tagtype(t[0]) == 'global']
513 return dict([(name, hex(node)) for name, node in tags
516 return dict([(name, hex(node)) for name, node in tags
514 if self.keep(node)])
517 if self.keep(node)])
515
518
516 def getchangedfiles(self, rev, i):
519 def getchangedfiles(self, rev, i):
517 ctx = self.changectx(rev)
520 ctx = self.changectx(rev)
518 parents = self.parents(ctx)
521 parents = self.parents(ctx)
519 if not parents and i is None:
522 if not parents and i is None:
520 i = 0
523 i = 0
521 changes = [], ctx.manifest().keys(), []
524 changes = [], ctx.manifest().keys(), []
522 else:
525 else:
523 i = i or 0
526 i = i or 0
524 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
527 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
525 changes = [[f for f in l if f not in self.ignored] for l in changes]
528 changes = [[f for f in l if f not in self.ignored] for l in changes]
526
529
527 if i == 0:
530 if i == 0:
528 self._changescache = (rev, changes)
531 self._changescache = (rev, changes)
529
532
530 return changes[0] + changes[1] + changes[2]
533 return changes[0] + changes[1] + changes[2]
531
534
532 def converted(self, rev, destrev):
535 def converted(self, rev, destrev):
533 if self.convertfp is None:
536 if self.convertfp is None:
534 self.convertfp = open(self.repo.join('shamap'), 'a')
537 self.convertfp = open(self.repo.join('shamap'), 'a')
535 self.convertfp.write('%s %s\n' % (destrev, rev))
538 self.convertfp.write('%s %s\n' % (destrev, rev))
536 self.convertfp.flush()
539 self.convertfp.flush()
537
540
538 def before(self):
541 def before(self):
539 self.ui.debug('run hg source pre-conversion action\n')
542 self.ui.debug('run hg source pre-conversion action\n')
540
543
541 def after(self):
544 def after(self):
542 self.ui.debug('run hg source post-conversion action\n')
545 self.ui.debug('run hg source post-conversion action\n')
543
546
544 def hasnativeorder(self):
547 def hasnativeorder(self):
545 return True
548 return True
546
549
547 def hasnativeclose(self):
550 def hasnativeclose(self):
548 return True
551 return True
549
552
550 def lookuprev(self, rev):
553 def lookuprev(self, rev):
551 try:
554 try:
552 return hex(self.repo.lookup(rev))
555 return hex(self.repo.lookup(rev))
553 except (error.RepoError, error.LookupError):
556 except (error.RepoError, error.LookupError):
554 return None
557 return None
555
558
556 def getbookmarks(self):
559 def getbookmarks(self):
557 return bookmarks.listbookmarks(self.repo)
560 return bookmarks.listbookmarks(self.repo)
558
561
559 def checkrevformat(self, revstr, mapname='splicemap'):
562 def checkrevformat(self, revstr, mapname='splicemap'):
560 """ Mercurial, revision string is a 40 byte hex """
563 """ Mercurial, revision string is a 40 byte hex """
561 self.checkhexformat(revstr, mapname)
564 self.checkhexformat(revstr, mapname)
@@ -1,361 +1,364
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import os, re
9 import os, re
10 from mercurial import util
10 from mercurial import util
11 from common import NoRepo, commit, converter_source, checktool
11 from common import NoRepo, commit, converter_source, checktool
12 from common import commandline
12 from common import commandline
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14
14
15 class monotone_source(converter_source, commandline):
15 class monotone_source(converter_source, commandline):
16 def __init__(self, ui, path=None, rev=None):
16 def __init__(self, ui, path=None, revs=None):
17 converter_source.__init__(self, ui, path, rev)
17 converter_source.__init__(self, ui, path, revs)
18 if revs and len(revs) > 1:
19 raise util.Abort(_('monotone source does not support specifying '
20 'multiple revs'))
18 commandline.__init__(self, ui, 'mtn')
21 commandline.__init__(self, ui, 'mtn')
19
22
20 self.ui = ui
23 self.ui = ui
21 self.path = path
24 self.path = path
22 self.automatestdio = False
25 self.automatestdio = False
23 self.rev = rev
26 self.revs = revs
24
27
25 norepo = NoRepo(_("%s does not look like a monotone repository")
28 norepo = NoRepo(_("%s does not look like a monotone repository")
26 % path)
29 % path)
27 if not os.path.exists(os.path.join(path, '_MTN')):
30 if not os.path.exists(os.path.join(path, '_MTN')):
28 # Could be a monotone repository (SQLite db file)
31 # Could be a monotone repository (SQLite db file)
29 try:
32 try:
30 f = file(path, 'rb')
33 f = file(path, 'rb')
31 header = f.read(16)
34 header = f.read(16)
32 f.close()
35 f.close()
33 except IOError:
36 except IOError:
34 header = ''
37 header = ''
35 if header != 'SQLite format 3\x00':
38 if header != 'SQLite format 3\x00':
36 raise norepo
39 raise norepo
37
40
38 # regular expressions for parsing monotone output
41 # regular expressions for parsing monotone output
39 space = r'\s*'
42 space = r'\s*'
40 name = r'\s+"((?:\\"|[^"])*)"\s*'
43 name = r'\s+"((?:\\"|[^"])*)"\s*'
41 value = name
44 value = name
42 revision = r'\s+\[(\w+)\]\s*'
45 revision = r'\s+\[(\w+)\]\s*'
43 lines = r'(?:.|\n)+'
46 lines = r'(?:.|\n)+'
44
47
45 self.dir_re = re.compile(space + "dir" + name)
48 self.dir_re = re.compile(space + "dir" + name)
46 self.file_re = re.compile(space + "file" + name +
49 self.file_re = re.compile(space + "file" + name +
47 "content" + revision)
50 "content" + revision)
48 self.add_file_re = re.compile(space + "add_file" + name +
51 self.add_file_re = re.compile(space + "add_file" + name +
49 "content" + revision)
52 "content" + revision)
50 self.patch_re = re.compile(space + "patch" + name +
53 self.patch_re = re.compile(space + "patch" + name +
51 "from" + revision + "to" + revision)
54 "from" + revision + "to" + revision)
52 self.rename_re = re.compile(space + "rename" + name + "to" + name)
55 self.rename_re = re.compile(space + "rename" + name + "to" + name)
53 self.delete_re = re.compile(space + "delete" + name)
56 self.delete_re = re.compile(space + "delete" + name)
54 self.tag_re = re.compile(space + "tag" + name + "revision" +
57 self.tag_re = re.compile(space + "tag" + name + "revision" +
55 revision)
58 revision)
56 self.cert_re = re.compile(lines + space + "name" + name +
59 self.cert_re = re.compile(lines + space + "name" + name +
57 "value" + value)
60 "value" + value)
58
61
59 attr = space + "file" + lines + space + "attr" + space
62 attr = space + "file" + lines + space + "attr" + space
60 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
63 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
61 space + '"true"')
64 space + '"true"')
62
65
63 # cached data
66 # cached data
64 self.manifest_rev = None
67 self.manifest_rev = None
65 self.manifest = None
68 self.manifest = None
66 self.files = None
69 self.files = None
67 self.dirs = None
70 self.dirs = None
68
71
69 checktool('mtn', abort=False)
72 checktool('mtn', abort=False)
70
73
71 def mtnrun(self, *args, **kwargs):
74 def mtnrun(self, *args, **kwargs):
72 if self.automatestdio:
75 if self.automatestdio:
73 return self.mtnrunstdio(*args, **kwargs)
76 return self.mtnrunstdio(*args, **kwargs)
74 else:
77 else:
75 return self.mtnrunsingle(*args, **kwargs)
78 return self.mtnrunsingle(*args, **kwargs)
76
79
77 def mtnrunsingle(self, *args, **kwargs):
80 def mtnrunsingle(self, *args, **kwargs):
78 kwargs['d'] = self.path
81 kwargs['d'] = self.path
79 return self.run0('automate', *args, **kwargs)
82 return self.run0('automate', *args, **kwargs)
80
83
81 def mtnrunstdio(self, *args, **kwargs):
84 def mtnrunstdio(self, *args, **kwargs):
82 # Prepare the command in automate stdio format
85 # Prepare the command in automate stdio format
83 command = []
86 command = []
84 for k, v in kwargs.iteritems():
87 for k, v in kwargs.iteritems():
85 command.append("%s:%s" % (len(k), k))
88 command.append("%s:%s" % (len(k), k))
86 if v:
89 if v:
87 command.append("%s:%s" % (len(v), v))
90 command.append("%s:%s" % (len(v), v))
88 if command:
91 if command:
89 command.insert(0, 'o')
92 command.insert(0, 'o')
90 command.append('e')
93 command.append('e')
91
94
92 command.append('l')
95 command.append('l')
93 for arg in args:
96 for arg in args:
94 command += "%s:%s" % (len(arg), arg)
97 command += "%s:%s" % (len(arg), arg)
95 command.append('e')
98 command.append('e')
96 command = ''.join(command)
99 command = ''.join(command)
97
100
98 self.ui.debug("mtn: sending '%s'\n" % command)
101 self.ui.debug("mtn: sending '%s'\n" % command)
99 self.mtnwritefp.write(command)
102 self.mtnwritefp.write(command)
100 self.mtnwritefp.flush()
103 self.mtnwritefp.flush()
101
104
102 return self.mtnstdioreadcommandoutput(command)
105 return self.mtnstdioreadcommandoutput(command)
103
106
104 def mtnstdioreadpacket(self):
107 def mtnstdioreadpacket(self):
105 read = None
108 read = None
106 commandnbr = ''
109 commandnbr = ''
107 while read != ':':
110 while read != ':':
108 read = self.mtnreadfp.read(1)
111 read = self.mtnreadfp.read(1)
109 if not read:
112 if not read:
110 raise util.Abort(_('bad mtn packet - no end of commandnbr'))
113 raise util.Abort(_('bad mtn packet - no end of commandnbr'))
111 commandnbr += read
114 commandnbr += read
112 commandnbr = commandnbr[:-1]
115 commandnbr = commandnbr[:-1]
113
116
114 stream = self.mtnreadfp.read(1)
117 stream = self.mtnreadfp.read(1)
115 if stream not in 'mewptl':
118 if stream not in 'mewptl':
116 raise util.Abort(_('bad mtn packet - bad stream type %s') % stream)
119 raise util.Abort(_('bad mtn packet - bad stream type %s') % stream)
117
120
118 read = self.mtnreadfp.read(1)
121 read = self.mtnreadfp.read(1)
119 if read != ':':
122 if read != ':':
120 raise util.Abort(_('bad mtn packet - no divider before size'))
123 raise util.Abort(_('bad mtn packet - no divider before size'))
121
124
122 read = None
125 read = None
123 lengthstr = ''
126 lengthstr = ''
124 while read != ':':
127 while read != ':':
125 read = self.mtnreadfp.read(1)
128 read = self.mtnreadfp.read(1)
126 if not read:
129 if not read:
127 raise util.Abort(_('bad mtn packet - no end of packet size'))
130 raise util.Abort(_('bad mtn packet - no end of packet size'))
128 lengthstr += read
131 lengthstr += read
129 try:
132 try:
130 length = long(lengthstr[:-1])
133 length = long(lengthstr[:-1])
131 except TypeError:
134 except TypeError:
132 raise util.Abort(_('bad mtn packet - bad packet size %s')
135 raise util.Abort(_('bad mtn packet - bad packet size %s')
133 % lengthstr)
136 % lengthstr)
134
137
135 read = self.mtnreadfp.read(length)
138 read = self.mtnreadfp.read(length)
136 if len(read) != length:
139 if len(read) != length:
137 raise util.Abort(_("bad mtn packet - unable to read full packet "
140 raise util.Abort(_("bad mtn packet - unable to read full packet "
138 "read %s of %s") % (len(read), length))
141 "read %s of %s") % (len(read), length))
139
142
140 return (commandnbr, stream, length, read)
143 return (commandnbr, stream, length, read)
141
144
142 def mtnstdioreadcommandoutput(self, command):
145 def mtnstdioreadcommandoutput(self, command):
143 retval = []
146 retval = []
144 while True:
147 while True:
145 commandnbr, stream, length, output = self.mtnstdioreadpacket()
148 commandnbr, stream, length, output = self.mtnstdioreadpacket()
146 self.ui.debug('mtn: read packet %s:%s:%s\n' %
149 self.ui.debug('mtn: read packet %s:%s:%s\n' %
147 (commandnbr, stream, length))
150 (commandnbr, stream, length))
148
151
149 if stream == 'l':
152 if stream == 'l':
150 # End of command
153 # End of command
151 if output != '0':
154 if output != '0':
152 raise util.Abort(_("mtn command '%s' returned %s") %
155 raise util.Abort(_("mtn command '%s' returned %s") %
153 (command, output))
156 (command, output))
154 break
157 break
155 elif stream in 'ew':
158 elif stream in 'ew':
156 # Error, warning output
159 # Error, warning output
157 self.ui.warn(_('%s error:\n') % self.command)
160 self.ui.warn(_('%s error:\n') % self.command)
158 self.ui.warn(output)
161 self.ui.warn(output)
159 elif stream == 'p':
162 elif stream == 'p':
160 # Progress messages
163 # Progress messages
161 self.ui.debug('mtn: ' + output)
164 self.ui.debug('mtn: ' + output)
162 elif stream == 'm':
165 elif stream == 'm':
163 # Main stream - command output
166 # Main stream - command output
164 retval.append(output)
167 retval.append(output)
165
168
166 return ''.join(retval)
169 return ''.join(retval)
167
170
168 def mtnloadmanifest(self, rev):
171 def mtnloadmanifest(self, rev):
169 if self.manifest_rev == rev:
172 if self.manifest_rev == rev:
170 return
173 return
171 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
174 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
172 self.manifest_rev = rev
175 self.manifest_rev = rev
173 self.files = {}
176 self.files = {}
174 self.dirs = {}
177 self.dirs = {}
175
178
176 for e in self.manifest:
179 for e in self.manifest:
177 m = self.file_re.match(e)
180 m = self.file_re.match(e)
178 if m:
181 if m:
179 attr = ""
182 attr = ""
180 name = m.group(1)
183 name = m.group(1)
181 node = m.group(2)
184 node = m.group(2)
182 if self.attr_execute_re.match(e):
185 if self.attr_execute_re.match(e):
183 attr += "x"
186 attr += "x"
184 self.files[name] = (node, attr)
187 self.files[name] = (node, attr)
185 m = self.dir_re.match(e)
188 m = self.dir_re.match(e)
186 if m:
189 if m:
187 self.dirs[m.group(1)] = True
190 self.dirs[m.group(1)] = True
188
191
189 def mtnisfile(self, name, rev):
192 def mtnisfile(self, name, rev):
190 # a non-file could be a directory or a deleted or renamed file
193 # a non-file could be a directory or a deleted or renamed file
191 self.mtnloadmanifest(rev)
194 self.mtnloadmanifest(rev)
192 return name in self.files
195 return name in self.files
193
196
194 def mtnisdir(self, name, rev):
197 def mtnisdir(self, name, rev):
195 self.mtnloadmanifest(rev)
198 self.mtnloadmanifest(rev)
196 return name in self.dirs
199 return name in self.dirs
197
200
198 def mtngetcerts(self, rev):
201 def mtngetcerts(self, rev):
199 certs = {"author":"<missing>", "date":"<missing>",
202 certs = {"author":"<missing>", "date":"<missing>",
200 "changelog":"<missing>", "branch":"<missing>"}
203 "changelog":"<missing>", "branch":"<missing>"}
201 certlist = self.mtnrun("certs", rev)
204 certlist = self.mtnrun("certs", rev)
202 # mtn < 0.45:
205 # mtn < 0.45:
203 # key "test@selenic.com"
206 # key "test@selenic.com"
204 # mtn >= 0.45:
207 # mtn >= 0.45:
205 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
208 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
206 certlist = re.split('\n\n key ["\[]', certlist)
209 certlist = re.split('\n\n key ["\[]', certlist)
207 for e in certlist:
210 for e in certlist:
208 m = self.cert_re.match(e)
211 m = self.cert_re.match(e)
209 if m:
212 if m:
210 name, value = m.groups()
213 name, value = m.groups()
211 value = value.replace(r'\"', '"')
214 value = value.replace(r'\"', '"')
212 value = value.replace(r'\\', '\\')
215 value = value.replace(r'\\', '\\')
213 certs[name] = value
216 certs[name] = value
214 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
217 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
215 # and all times are stored in UTC
218 # and all times are stored in UTC
216 certs["date"] = certs["date"].split('.')[0] + " UTC"
219 certs["date"] = certs["date"].split('.')[0] + " UTC"
217 return certs
220 return certs
218
221
219 # implement the converter_source interface:
222 # implement the converter_source interface:
220
223
221 def getheads(self):
224 def getheads(self):
222 if not self.rev:
225 if not self.revs:
223 return self.mtnrun("leaves").splitlines()
226 return self.mtnrun("leaves").splitlines()
224 else:
227 else:
225 return [self.rev]
228 return self.revs
226
229
227 def getchanges(self, rev, full):
230 def getchanges(self, rev, full):
228 if full:
231 if full:
229 raise util.Abort(_("convert from monotone do not support --full"))
232 raise util.Abort(_("convert from monotone do not support --full"))
230 revision = self.mtnrun("get_revision", rev).split("\n\n")
233 revision = self.mtnrun("get_revision", rev).split("\n\n")
231 files = {}
234 files = {}
232 ignoremove = {}
235 ignoremove = {}
233 renameddirs = []
236 renameddirs = []
234 copies = {}
237 copies = {}
235 for e in revision:
238 for e in revision:
236 m = self.add_file_re.match(e)
239 m = self.add_file_re.match(e)
237 if m:
240 if m:
238 files[m.group(1)] = rev
241 files[m.group(1)] = rev
239 ignoremove[m.group(1)] = rev
242 ignoremove[m.group(1)] = rev
240 m = self.patch_re.match(e)
243 m = self.patch_re.match(e)
241 if m:
244 if m:
242 files[m.group(1)] = rev
245 files[m.group(1)] = rev
243 # Delete/rename is handled later when the convert engine
246 # Delete/rename is handled later when the convert engine
244 # discovers an IOError exception from getfile,
247 # discovers an IOError exception from getfile,
245 # but only if we add the "from" file to the list of changes.
248 # but only if we add the "from" file to the list of changes.
246 m = self.delete_re.match(e)
249 m = self.delete_re.match(e)
247 if m:
250 if m:
248 files[m.group(1)] = rev
251 files[m.group(1)] = rev
249 m = self.rename_re.match(e)
252 m = self.rename_re.match(e)
250 if m:
253 if m:
251 toname = m.group(2)
254 toname = m.group(2)
252 fromname = m.group(1)
255 fromname = m.group(1)
253 if self.mtnisfile(toname, rev):
256 if self.mtnisfile(toname, rev):
254 ignoremove[toname] = 1
257 ignoremove[toname] = 1
255 copies[toname] = fromname
258 copies[toname] = fromname
256 files[toname] = rev
259 files[toname] = rev
257 files[fromname] = rev
260 files[fromname] = rev
258 elif self.mtnisdir(toname, rev):
261 elif self.mtnisdir(toname, rev):
259 renameddirs.append((fromname, toname))
262 renameddirs.append((fromname, toname))
260
263
261 # Directory renames can be handled only once we have recorded
264 # Directory renames can be handled only once we have recorded
262 # all new files
265 # all new files
263 for fromdir, todir in renameddirs:
266 for fromdir, todir in renameddirs:
264 renamed = {}
267 renamed = {}
265 for tofile in self.files:
268 for tofile in self.files:
266 if tofile in ignoremove:
269 if tofile in ignoremove:
267 continue
270 continue
268 if tofile.startswith(todir + '/'):
271 if tofile.startswith(todir + '/'):
269 renamed[tofile] = fromdir + tofile[len(todir):]
272 renamed[tofile] = fromdir + tofile[len(todir):]
270 # Avoid chained moves like:
273 # Avoid chained moves like:
271 # d1(/a) => d3/d1(/a)
274 # d1(/a) => d3/d1(/a)
272 # d2 => d3
275 # d2 => d3
273 ignoremove[tofile] = 1
276 ignoremove[tofile] = 1
274 for tofile, fromfile in renamed.items():
277 for tofile, fromfile in renamed.items():
275 self.ui.debug (_("copying file in renamed directory "
278 self.ui.debug (_("copying file in renamed directory "
276 "from '%s' to '%s'")
279 "from '%s' to '%s'")
277 % (fromfile, tofile), '\n')
280 % (fromfile, tofile), '\n')
278 files[tofile] = rev
281 files[tofile] = rev
279 copies[tofile] = fromfile
282 copies[tofile] = fromfile
280 for fromfile in renamed.values():
283 for fromfile in renamed.values():
281 files[fromfile] = rev
284 files[fromfile] = rev
282
285
283 return (files.items(), copies, set())
286 return (files.items(), copies, set())
284
287
285 def getfile(self, name, rev):
288 def getfile(self, name, rev):
286 if not self.mtnisfile(name, rev):
289 if not self.mtnisfile(name, rev):
287 return None, None
290 return None, None
288 try:
291 try:
289 data = self.mtnrun("get_file_of", name, r=rev)
292 data = self.mtnrun("get_file_of", name, r=rev)
290 except Exception:
293 except Exception:
291 return None, None
294 return None, None
292 self.mtnloadmanifest(rev)
295 self.mtnloadmanifest(rev)
293 node, attr = self.files.get(name, (None, ""))
296 node, attr = self.files.get(name, (None, ""))
294 return data, attr
297 return data, attr
295
298
296 def getcommit(self, rev):
299 def getcommit(self, rev):
297 extra = {}
300 extra = {}
298 certs = self.mtngetcerts(rev)
301 certs = self.mtngetcerts(rev)
299 if certs.get('suspend') == certs["branch"]:
302 if certs.get('suspend') == certs["branch"]:
300 extra['close'] = 1
303 extra['close'] = 1
301 return commit(
304 return commit(
302 author=certs["author"],
305 author=certs["author"],
303 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
306 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
304 desc=certs["changelog"],
307 desc=certs["changelog"],
305 rev=rev,
308 rev=rev,
306 parents=self.mtnrun("parents", rev).splitlines(),
309 parents=self.mtnrun("parents", rev).splitlines(),
307 branch=certs["branch"],
310 branch=certs["branch"],
308 extra=extra)
311 extra=extra)
309
312
310 def gettags(self):
313 def gettags(self):
311 tags = {}
314 tags = {}
312 for e in self.mtnrun("tags").split("\n\n"):
315 for e in self.mtnrun("tags").split("\n\n"):
313 m = self.tag_re.match(e)
316 m = self.tag_re.match(e)
314 if m:
317 if m:
315 tags[m.group(1)] = m.group(2)
318 tags[m.group(1)] = m.group(2)
316 return tags
319 return tags
317
320
318 def getchangedfiles(self, rev, i):
321 def getchangedfiles(self, rev, i):
319 # This function is only needed to support --filemap
322 # This function is only needed to support --filemap
320 # ... and we don't support that
323 # ... and we don't support that
321 raise NotImplementedError
324 raise NotImplementedError
322
325
323 def before(self):
326 def before(self):
324 # Check if we have a new enough version to use automate stdio
327 # Check if we have a new enough version to use automate stdio
325 version = 0.0
328 version = 0.0
326 try:
329 try:
327 versionstr = self.mtnrunsingle("interface_version")
330 versionstr = self.mtnrunsingle("interface_version")
328 version = float(versionstr)
331 version = float(versionstr)
329 except Exception:
332 except Exception:
330 raise util.Abort(_("unable to determine mtn automate interface "
333 raise util.Abort(_("unable to determine mtn automate interface "
331 "version"))
334 "version"))
332
335
333 if version >= 12.0:
336 if version >= 12.0:
334 self.automatestdio = True
337 self.automatestdio = True
335 self.ui.debug("mtn automate version %s - using automate stdio\n" %
338 self.ui.debug("mtn automate version %s - using automate stdio\n" %
336 version)
339 version)
337
340
338 # launch the long-running automate stdio process
341 # launch the long-running automate stdio process
339 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
342 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
340 '-d', self.path)
343 '-d', self.path)
341 # read the headers
344 # read the headers
342 read = self.mtnreadfp.readline()
345 read = self.mtnreadfp.readline()
343 if read != 'format-version: 2\n':
346 if read != 'format-version: 2\n':
344 raise util.Abort(_('mtn automate stdio header unexpected: %s')
347 raise util.Abort(_('mtn automate stdio header unexpected: %s')
345 % read)
348 % read)
346 while read != '\n':
349 while read != '\n':
347 read = self.mtnreadfp.readline()
350 read = self.mtnreadfp.readline()
348 if not read:
351 if not read:
349 raise util.Abort(_("failed to reach end of mtn automate "
352 raise util.Abort(_("failed to reach end of mtn automate "
350 "stdio headers"))
353 "stdio headers"))
351 else:
354 else:
352 self.ui.debug("mtn automate version %s - not using automate stdio "
355 self.ui.debug("mtn automate version %s - not using automate stdio "
353 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
356 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
354
357
355 def after(self):
358 def after(self):
356 if self.automatestdio:
359 if self.automatestdio:
357 self.mtnwritefp.close()
360 self.mtnwritefp.close()
358 self.mtnwritefp = None
361 self.mtnwritefp = None
359 self.mtnreadfp.close()
362 self.mtnreadfp.close()
360 self.mtnreadfp = None
363 self.mtnreadfp = None
361
364
@@ -1,207 +1,210
1 # Perforce source for convert extension.
1 # Perforce source for convert extension.
2 #
2 #
3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
3 # Copyright 2009, 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
7
8 from mercurial import util
8 from mercurial import util
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10
10
11 from common import commit, converter_source, checktool, NoRepo
11 from common import commit, converter_source, checktool, NoRepo
12 import marshal
12 import marshal
13 import re
13 import re
14
14
15 def loaditer(f):
15 def loaditer(f):
16 "Yield the dictionary objects generated by p4"
16 "Yield the dictionary objects generated by p4"
17 try:
17 try:
18 while True:
18 while True:
19 d = marshal.load(f)
19 d = marshal.load(f)
20 if not d:
20 if not d:
21 break
21 break
22 yield d
22 yield d
23 except EOFError:
23 except EOFError:
24 pass
24 pass
25
25
26 class p4_source(converter_source):
26 class p4_source(converter_source):
27 def __init__(self, ui, path, rev=None):
27 def __init__(self, ui, path, revs=None):
28 super(p4_source, self).__init__(ui, path, rev=rev)
28 super(p4_source, self).__init__(ui, path, revs=revs)
29
29
30 if "/" in path and not path.startswith('//'):
30 if "/" in path and not path.startswith('//'):
31 raise NoRepo(_('%s does not look like a P4 repository') % path)
31 raise NoRepo(_('%s does not look like a P4 repository') % path)
32
32
33 checktool('p4', abort=False)
33 checktool('p4', abort=False)
34
34
35 self.p4changes = {}
35 self.p4changes = {}
36 self.heads = {}
36 self.heads = {}
37 self.changeset = {}
37 self.changeset = {}
38 self.files = {}
38 self.files = {}
39 self.tags = {}
39 self.tags = {}
40 self.lastbranch = {}
40 self.lastbranch = {}
41 self.parent = {}
41 self.parent = {}
42 self.encoding = "latin_1"
42 self.encoding = "latin_1"
43 self.depotname = {} # mapping from local name to depot name
43 self.depotname = {} # mapping from local name to depot name
44 self.re_type = re.compile(
44 self.re_type = re.compile(
45 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
45 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
46 "(\+\w+)?$")
46 "(\+\w+)?$")
47 self.re_keywords = re.compile(
47 self.re_keywords = re.compile(
48 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
48 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
49 r":[^$\n]*\$")
49 r":[^$\n]*\$")
50 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
50 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
51
51
52 if revs and len(revs) > 1:
53 raise util.Abort(_("p4 source does not support specifying "
54 "multiple revisions"))
52 self._parse(ui, path)
55 self._parse(ui, path)
53
56
54 def _parse_view(self, path):
57 def _parse_view(self, path):
55 "Read changes affecting the path"
58 "Read changes affecting the path"
56 cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
59 cmd = 'p4 -G changes -s submitted %s' % util.shellquote(path)
57 stdout = util.popen(cmd, mode='rb')
60 stdout = util.popen(cmd, mode='rb')
58 for d in loaditer(stdout):
61 for d in loaditer(stdout):
59 c = d.get("change", None)
62 c = d.get("change", None)
60 if c:
63 if c:
61 self.p4changes[c] = True
64 self.p4changes[c] = True
62
65
63 def _parse(self, ui, path):
66 def _parse(self, ui, path):
64 "Prepare list of P4 filenames and revisions to import"
67 "Prepare list of P4 filenames and revisions to import"
65 ui.status(_('reading p4 views\n'))
68 ui.status(_('reading p4 views\n'))
66
69
67 # read client spec or view
70 # read client spec or view
68 if "/" in path:
71 if "/" in path:
69 self._parse_view(path)
72 self._parse_view(path)
70 if path.startswith("//") and path.endswith("/..."):
73 if path.startswith("//") and path.endswith("/..."):
71 views = {path[:-3]:""}
74 views = {path[:-3]:""}
72 else:
75 else:
73 views = {"//": ""}
76 views = {"//": ""}
74 else:
77 else:
75 cmd = 'p4 -G client -o %s' % util.shellquote(path)
78 cmd = 'p4 -G client -o %s' % util.shellquote(path)
76 clientspec = marshal.load(util.popen(cmd, mode='rb'))
79 clientspec = marshal.load(util.popen(cmd, mode='rb'))
77
80
78 views = {}
81 views = {}
79 for client in clientspec:
82 for client in clientspec:
80 if client.startswith("View"):
83 if client.startswith("View"):
81 sview, cview = clientspec[client].split()
84 sview, cview = clientspec[client].split()
82 self._parse_view(sview)
85 self._parse_view(sview)
83 if sview.endswith("...") and cview.endswith("..."):
86 if sview.endswith("...") and cview.endswith("..."):
84 sview = sview[:-3]
87 sview = sview[:-3]
85 cview = cview[:-3]
88 cview = cview[:-3]
86 cview = cview[2:]
89 cview = cview[2:]
87 cview = cview[cview.find("/") + 1:]
90 cview = cview[cview.find("/") + 1:]
88 views[sview] = cview
91 views[sview] = cview
89
92
90 # list of changes that affect our source files
93 # list of changes that affect our source files
91 self.p4changes = self.p4changes.keys()
94 self.p4changes = self.p4changes.keys()
92 self.p4changes.sort(key=int)
95 self.p4changes.sort(key=int)
93
96
94 # list with depot pathnames, longest first
97 # list with depot pathnames, longest first
95 vieworder = views.keys()
98 vieworder = views.keys()
96 vieworder.sort(key=len, reverse=True)
99 vieworder.sort(key=len, reverse=True)
97
100
98 # handle revision limiting
101 # handle revision limiting
99 startrev = self.ui.config('convert', 'p4.startrev', default=0)
102 startrev = self.ui.config('convert', 'p4.startrev', default=0)
100 self.p4changes = [x for x in self.p4changes
103 self.p4changes = [x for x in self.p4changes
101 if ((not startrev or int(x) >= int(startrev)) and
104 if ((not startrev or int(x) >= int(startrev)) and
102 (not self.rev or int(x) <= int(self.rev)))]
105 (not self.revs or int(x) <= int(self.revs[0])))]
103
106
104 # now read the full changelists to get the list of file revisions
107 # now read the full changelists to get the list of file revisions
105 ui.status(_('collecting p4 changelists\n'))
108 ui.status(_('collecting p4 changelists\n'))
106 lastid = None
109 lastid = None
107 for change in self.p4changes:
110 for change in self.p4changes:
108 cmd = "p4 -G describe -s %s" % change
111 cmd = "p4 -G describe -s %s" % change
109 stdout = util.popen(cmd, mode='rb')
112 stdout = util.popen(cmd, mode='rb')
110 d = marshal.load(stdout)
113 d = marshal.load(stdout)
111 desc = self.recode(d.get("desc", ""))
114 desc = self.recode(d.get("desc", ""))
112 shortdesc = desc.split("\n", 1)[0]
115 shortdesc = desc.split("\n", 1)[0]
113 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
116 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
114 ui.status(util.ellipsis(t, 80) + '\n')
117 ui.status(util.ellipsis(t, 80) + '\n')
115
118
116 if lastid:
119 if lastid:
117 parents = [lastid]
120 parents = [lastid]
118 else:
121 else:
119 parents = []
122 parents = []
120
123
121 date = (int(d["time"]), 0) # timezone not set
124 date = (int(d["time"]), 0) # timezone not set
122 c = commit(author=self.recode(d["user"]),
125 c = commit(author=self.recode(d["user"]),
123 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
126 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
124 parents=parents, desc=desc, branch='',
127 parents=parents, desc=desc, branch='',
125 extra={"p4": change})
128 extra={"p4": change})
126
129
127 files = []
130 files = []
128 i = 0
131 i = 0
129 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
132 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
130 oldname = d["depotFile%d" % i]
133 oldname = d["depotFile%d" % i]
131 filename = None
134 filename = None
132 for v in vieworder:
135 for v in vieworder:
133 if oldname.startswith(v):
136 if oldname.startswith(v):
134 filename = views[v] + oldname[len(v):]
137 filename = views[v] + oldname[len(v):]
135 break
138 break
136 if filename:
139 if filename:
137 files.append((filename, d["rev%d" % i]))
140 files.append((filename, d["rev%d" % i]))
138 self.depotname[filename] = oldname
141 self.depotname[filename] = oldname
139 i += 1
142 i += 1
140 self.changeset[change] = c
143 self.changeset[change] = c
141 self.files[change] = files
144 self.files[change] = files
142 lastid = change
145 lastid = change
143
146
144 if lastid:
147 if lastid:
145 self.heads = [lastid]
148 self.heads = [lastid]
146
149
147 def getheads(self):
150 def getheads(self):
148 return self.heads
151 return self.heads
149
152
150 def getfile(self, name, rev):
153 def getfile(self, name, rev):
151 cmd = 'p4 -G print %s' \
154 cmd = 'p4 -G print %s' \
152 % util.shellquote("%s#%s" % (self.depotname[name], rev))
155 % util.shellquote("%s#%s" % (self.depotname[name], rev))
153 stdout = util.popen(cmd, mode='rb')
156 stdout = util.popen(cmd, mode='rb')
154
157
155 mode = None
158 mode = None
156 contents = ""
159 contents = ""
157 keywords = None
160 keywords = None
158
161
159 for d in loaditer(stdout):
162 for d in loaditer(stdout):
160 code = d["code"]
163 code = d["code"]
161 data = d.get("data")
164 data = d.get("data")
162
165
163 if code == "error":
166 if code == "error":
164 raise IOError(d["generic"], data)
167 raise IOError(d["generic"], data)
165
168
166 elif code == "stat":
169 elif code == "stat":
167 if d.get("action") == "purge":
170 if d.get("action") == "purge":
168 return None, None
171 return None, None
169 p4type = self.re_type.match(d["type"])
172 p4type = self.re_type.match(d["type"])
170 if p4type:
173 if p4type:
171 mode = ""
174 mode = ""
172 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
175 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
173 if "x" in flags:
176 if "x" in flags:
174 mode = "x"
177 mode = "x"
175 if p4type.group(2) == "symlink":
178 if p4type.group(2) == "symlink":
176 mode = "l"
179 mode = "l"
177 if "ko" in flags:
180 if "ko" in flags:
178 keywords = self.re_keywords_old
181 keywords = self.re_keywords_old
179 elif "k" in flags:
182 elif "k" in flags:
180 keywords = self.re_keywords
183 keywords = self.re_keywords
181
184
182 elif code == "text" or code == "binary":
185 elif code == "text" or code == "binary":
183 contents += data
186 contents += data
184
187
185 if mode is None:
188 if mode is None:
186 return None, None
189 return None, None
187
190
188 if keywords:
191 if keywords:
189 contents = keywords.sub("$\\1$", contents)
192 contents = keywords.sub("$\\1$", contents)
190 if mode == "l" and contents.endswith("\n"):
193 if mode == "l" and contents.endswith("\n"):
191 contents = contents[:-1]
194 contents = contents[:-1]
192
195
193 return contents, mode
196 return contents, mode
194
197
195 def getchanges(self, rev, full):
198 def getchanges(self, rev, full):
196 if full:
199 if full:
197 raise util.Abort(_("convert from p4 do not support --full"))
200 raise util.Abort(_("convert from p4 do not support --full"))
198 return self.files[rev], {}, set()
201 return self.files[rev], {}, set()
199
202
200 def getcommit(self, rev):
203 def getcommit(self, rev):
201 return self.changeset[rev]
204 return self.changeset[rev]
202
205
203 def gettags(self):
206 def gettags(self):
204 return self.tags
207 return self.tags
205
208
206 def getchangedfiles(self, rev, i):
209 def getchangedfiles(self, rev, i):
207 return sorted([x[0] for x in self.files[rev]])
210 return sorted([x[0] for x in self.files[rev]])
@@ -1,1330 +1,1334
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4
4
5 import os, re, sys, tempfile, urllib, urllib2
5 import os, re, sys, tempfile, urllib, urllib2
6 import xml.dom.minidom
6 import xml.dom.minidom
7 import cPickle as pickle
7 import cPickle as pickle
8
8
9 from mercurial import strutil, scmutil, util, encoding
9 from mercurial import strutil, scmutil, util, encoding
10 from mercurial.i18n import _
10 from mercurial.i18n import _
11
11
12 propertycache = util.propertycache
12 propertycache = util.propertycache
13
13
14 # Subversion stuff. Works best with very recent Python SVN bindings
14 # Subversion stuff. Works best with very recent Python SVN bindings
15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
16 # these bindings.
16 # these bindings.
17
17
18 from cStringIO import StringIO
18 from cStringIO import StringIO
19
19
20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
21 from common import commandline, converter_source, converter_sink, mapfile
21 from common import commandline, converter_source, converter_sink, mapfile
22 from common import makedatetimestamp
22 from common import makedatetimestamp
23
23
24 try:
24 try:
25 from svn.core import SubversionException, Pool
25 from svn.core import SubversionException, Pool
26 import svn
26 import svn
27 import svn.client
27 import svn.client
28 import svn.core
28 import svn.core
29 import svn.ra
29 import svn.ra
30 import svn.delta
30 import svn.delta
31 import transport
31 import transport
32 import warnings
32 import warnings
33 warnings.filterwarnings('ignore',
33 warnings.filterwarnings('ignore',
34 module='svn.core',
34 module='svn.core',
35 category=DeprecationWarning)
35 category=DeprecationWarning)
36
36
37 except ImportError:
37 except ImportError:
38 svn = None
38 svn = None
39
39
40 class SvnPathNotFound(Exception):
40 class SvnPathNotFound(Exception):
41 pass
41 pass
42
42
43 def revsplit(rev):
43 def revsplit(rev):
44 """Parse a revision string and return (uuid, path, revnum).
44 """Parse a revision string and return (uuid, path, revnum).
45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
45 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
46 ... '/proj%20B/mytrunk/mytrunk@1')
46 ... '/proj%20B/mytrunk/mytrunk@1')
47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
47 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
48 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
49 ('', '', 1)
49 ('', '', 1)
50 >>> revsplit('@7')
50 >>> revsplit('@7')
51 ('', '', 7)
51 ('', '', 7)
52 >>> revsplit('7')
52 >>> revsplit('7')
53 ('', '', 0)
53 ('', '', 0)
54 >>> revsplit('bad')
54 >>> revsplit('bad')
55 ('', '', 0)
55 ('', '', 0)
56 """
56 """
57 parts = rev.rsplit('@', 1)
57 parts = rev.rsplit('@', 1)
58 revnum = 0
58 revnum = 0
59 if len(parts) > 1:
59 if len(parts) > 1:
60 revnum = int(parts[1])
60 revnum = int(parts[1])
61 parts = parts[0].split('/', 1)
61 parts = parts[0].split('/', 1)
62 uuid = ''
62 uuid = ''
63 mod = ''
63 mod = ''
64 if len(parts) > 1 and parts[0].startswith('svn:'):
64 if len(parts) > 1 and parts[0].startswith('svn:'):
65 uuid = parts[0][4:]
65 uuid = parts[0][4:]
66 mod = '/' + parts[1]
66 mod = '/' + parts[1]
67 return uuid, mod, revnum
67 return uuid, mod, revnum
68
68
69 def quote(s):
69 def quote(s):
70 # As of svn 1.7, many svn calls expect "canonical" paths. In
70 # As of svn 1.7, many svn calls expect "canonical" paths. In
71 # theory, we should call svn.core.*canonicalize() on all paths
71 # theory, we should call svn.core.*canonicalize() on all paths
72 # before passing them to the API. Instead, we assume the base url
72 # before passing them to the API. Instead, we assume the base url
73 # is canonical and copy the behaviour of svn URL encoding function
73 # is canonical and copy the behaviour of svn URL encoding function
74 # so we can extend it safely with new components. The "safe"
74 # so we can extend it safely with new components. The "safe"
75 # characters were taken from the "svn_uri__char_validity" table in
75 # characters were taken from the "svn_uri__char_validity" table in
76 # libsvn_subr/path.c.
76 # libsvn_subr/path.c.
77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
77 return urllib.quote(s, "!$&'()*+,-./:=@_~")
78
78
79 def geturl(path):
79 def geturl(path):
80 try:
80 try:
81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
81 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
82 except SubversionException:
82 except SubversionException:
83 # svn.client.url_from_path() fails with local repositories
83 # svn.client.url_from_path() fails with local repositories
84 pass
84 pass
85 if os.path.isdir(path):
85 if os.path.isdir(path):
86 path = os.path.normpath(os.path.abspath(path))
86 path = os.path.normpath(os.path.abspath(path))
87 if os.name == 'nt':
87 if os.name == 'nt':
88 path = '/' + util.normpath(path)
88 path = '/' + util.normpath(path)
89 # Module URL is later compared with the repository URL returned
89 # Module URL is later compared with the repository URL returned
90 # by svn API, which is UTF-8.
90 # by svn API, which is UTF-8.
91 path = encoding.tolocal(path)
91 path = encoding.tolocal(path)
92 path = 'file://%s' % quote(path)
92 path = 'file://%s' % quote(path)
93 return svn.core.svn_path_canonicalize(path)
93 return svn.core.svn_path_canonicalize(path)
94
94
95 def optrev(number):
95 def optrev(number):
96 optrev = svn.core.svn_opt_revision_t()
96 optrev = svn.core.svn_opt_revision_t()
97 optrev.kind = svn.core.svn_opt_revision_number
97 optrev.kind = svn.core.svn_opt_revision_number
98 optrev.value.number = number
98 optrev.value.number = number
99 return optrev
99 return optrev
100
100
101 class changedpath(object):
101 class changedpath(object):
102 def __init__(self, p):
102 def __init__(self, p):
103 self.copyfrom_path = p.copyfrom_path
103 self.copyfrom_path = p.copyfrom_path
104 self.copyfrom_rev = p.copyfrom_rev
104 self.copyfrom_rev = p.copyfrom_rev
105 self.action = p.action
105 self.action = p.action
106
106
107 def get_log_child(fp, url, paths, start, end, limit=0,
107 def get_log_child(fp, url, paths, start, end, limit=0,
108 discover_changed_paths=True, strict_node_history=False):
108 discover_changed_paths=True, strict_node_history=False):
109 protocol = -1
109 protocol = -1
110 def receiver(orig_paths, revnum, author, date, message, pool):
110 def receiver(orig_paths, revnum, author, date, message, pool):
111 paths = {}
111 paths = {}
112 if orig_paths is not None:
112 if orig_paths is not None:
113 for k, v in orig_paths.iteritems():
113 for k, v in orig_paths.iteritems():
114 paths[k] = changedpath(v)
114 paths[k] = changedpath(v)
115 pickle.dump((paths, revnum, author, date, message),
115 pickle.dump((paths, revnum, author, date, message),
116 fp, protocol)
116 fp, protocol)
117
117
118 try:
118 try:
119 # Use an ra of our own so that our parent can consume
119 # Use an ra of our own so that our parent can consume
120 # our results without confusing the server.
120 # our results without confusing the server.
121 t = transport.SvnRaTransport(url=url)
121 t = transport.SvnRaTransport(url=url)
122 svn.ra.get_log(t.ra, paths, start, end, limit,
122 svn.ra.get_log(t.ra, paths, start, end, limit,
123 discover_changed_paths,
123 discover_changed_paths,
124 strict_node_history,
124 strict_node_history,
125 receiver)
125 receiver)
126 except IOError:
126 except IOError:
127 # Caller may interrupt the iteration
127 # Caller may interrupt the iteration
128 pickle.dump(None, fp, protocol)
128 pickle.dump(None, fp, protocol)
129 except Exception as inst:
129 except Exception as inst:
130 pickle.dump(str(inst), fp, protocol)
130 pickle.dump(str(inst), fp, protocol)
131 else:
131 else:
132 pickle.dump(None, fp, protocol)
132 pickle.dump(None, fp, protocol)
133 fp.close()
133 fp.close()
134 # With large history, cleanup process goes crazy and suddenly
134 # With large history, cleanup process goes crazy and suddenly
135 # consumes *huge* amount of memory. The output file being closed,
135 # consumes *huge* amount of memory. The output file being closed,
136 # there is no need for clean termination.
136 # there is no need for clean termination.
137 os._exit(0)
137 os._exit(0)
138
138
139 def debugsvnlog(ui, **opts):
139 def debugsvnlog(ui, **opts):
140 """Fetch SVN log in a subprocess and channel them back to parent to
140 """Fetch SVN log in a subprocess and channel them back to parent to
141 avoid memory collection issues.
141 avoid memory collection issues.
142 """
142 """
143 if svn is None:
143 if svn is None:
144 raise util.Abort(_('debugsvnlog could not load Subversion python '
144 raise util.Abort(_('debugsvnlog could not load Subversion python '
145 'bindings'))
145 'bindings'))
146
146
147 util.setbinary(sys.stdin)
147 util.setbinary(sys.stdin)
148 util.setbinary(sys.stdout)
148 util.setbinary(sys.stdout)
149 args = decodeargs(sys.stdin.read())
149 args = decodeargs(sys.stdin.read())
150 get_log_child(sys.stdout, *args)
150 get_log_child(sys.stdout, *args)
151
151
152 class logstream(object):
152 class logstream(object):
153 """Interruptible revision log iterator."""
153 """Interruptible revision log iterator."""
154 def __init__(self, stdout):
154 def __init__(self, stdout):
155 self._stdout = stdout
155 self._stdout = stdout
156
156
157 def __iter__(self):
157 def __iter__(self):
158 while True:
158 while True:
159 try:
159 try:
160 entry = pickle.load(self._stdout)
160 entry = pickle.load(self._stdout)
161 except EOFError:
161 except EOFError:
162 raise util.Abort(_('Mercurial failed to run itself, check'
162 raise util.Abort(_('Mercurial failed to run itself, check'
163 ' hg executable is in PATH'))
163 ' hg executable is in PATH'))
164 try:
164 try:
165 orig_paths, revnum, author, date, message = entry
165 orig_paths, revnum, author, date, message = entry
166 except (TypeError, ValueError):
166 except (TypeError, ValueError):
167 if entry is None:
167 if entry is None:
168 break
168 break
169 raise util.Abort(_("log stream exception '%s'") % entry)
169 raise util.Abort(_("log stream exception '%s'") % entry)
170 yield entry
170 yield entry
171
171
172 def close(self):
172 def close(self):
173 if self._stdout:
173 if self._stdout:
174 self._stdout.close()
174 self._stdout.close()
175 self._stdout = None
175 self._stdout = None
176
176
177 class directlogstream(list):
177 class directlogstream(list):
178 """Direct revision log iterator.
178 """Direct revision log iterator.
179 This can be used for debugging and development but it will probably leak
179 This can be used for debugging and development but it will probably leak
180 memory and is not suitable for real conversions."""
180 memory and is not suitable for real conversions."""
181 def __init__(self, url, paths, start, end, limit=0,
181 def __init__(self, url, paths, start, end, limit=0,
182 discover_changed_paths=True, strict_node_history=False):
182 discover_changed_paths=True, strict_node_history=False):
183
183
184 def receiver(orig_paths, revnum, author, date, message, pool):
184 def receiver(orig_paths, revnum, author, date, message, pool):
185 paths = {}
185 paths = {}
186 if orig_paths is not None:
186 if orig_paths is not None:
187 for k, v in orig_paths.iteritems():
187 for k, v in orig_paths.iteritems():
188 paths[k] = changedpath(v)
188 paths[k] = changedpath(v)
189 self.append((paths, revnum, author, date, message))
189 self.append((paths, revnum, author, date, message))
190
190
191 # Use an ra of our own so that our parent can consume
191 # Use an ra of our own so that our parent can consume
192 # our results without confusing the server.
192 # our results without confusing the server.
193 t = transport.SvnRaTransport(url=url)
193 t = transport.SvnRaTransport(url=url)
194 svn.ra.get_log(t.ra, paths, start, end, limit,
194 svn.ra.get_log(t.ra, paths, start, end, limit,
195 discover_changed_paths,
195 discover_changed_paths,
196 strict_node_history,
196 strict_node_history,
197 receiver)
197 receiver)
198
198
199 def close(self):
199 def close(self):
200 pass
200 pass
201
201
202 # Check to see if the given path is a local Subversion repo. Verify this by
202 # Check to see if the given path is a local Subversion repo. Verify this by
203 # looking for several svn-specific files and directories in the given
203 # looking for several svn-specific files and directories in the given
204 # directory.
204 # directory.
205 def filecheck(ui, path, proto):
205 def filecheck(ui, path, proto):
206 for x in ('locks', 'hooks', 'format', 'db'):
206 for x in ('locks', 'hooks', 'format', 'db'):
207 if not os.path.exists(os.path.join(path, x)):
207 if not os.path.exists(os.path.join(path, x)):
208 return False
208 return False
209 return True
209 return True
210
210
211 # Check to see if a given path is the root of an svn repo over http. We verify
211 # Check to see if a given path is the root of an svn repo over http. We verify
212 # this by requesting a version-controlled URL we know can't exist and looking
212 # this by requesting a version-controlled URL we know can't exist and looking
213 # for the svn-specific "not found" XML.
213 # for the svn-specific "not found" XML.
214 def httpcheck(ui, path, proto):
214 def httpcheck(ui, path, proto):
215 try:
215 try:
216 opener = urllib2.build_opener()
216 opener = urllib2.build_opener()
217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
217 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
218 data = rsp.read()
218 data = rsp.read()
219 except urllib2.HTTPError as inst:
219 except urllib2.HTTPError as inst:
220 if inst.code != 404:
220 if inst.code != 404:
221 # Except for 404 we cannot know for sure this is not an svn repo
221 # Except for 404 we cannot know for sure this is not an svn repo
222 ui.warn(_('svn: cannot probe remote repository, assume it could '
222 ui.warn(_('svn: cannot probe remote repository, assume it could '
223 'be a subversion repository. Use --source-type if you '
223 'be a subversion repository. Use --source-type if you '
224 'know better.\n'))
224 'know better.\n'))
225 return True
225 return True
226 data = inst.fp.read()
226 data = inst.fp.read()
227 except Exception:
227 except Exception:
228 # Could be urllib2.URLError if the URL is invalid or anything else.
228 # Could be urllib2.URLError if the URL is invalid or anything else.
229 return False
229 return False
230 return '<m:human-readable errcode="160013">' in data
230 return '<m:human-readable errcode="160013">' in data
231
231
232 protomap = {'http': httpcheck,
232 protomap = {'http': httpcheck,
233 'https': httpcheck,
233 'https': httpcheck,
234 'file': filecheck,
234 'file': filecheck,
235 }
235 }
236 def issvnurl(ui, url):
236 def issvnurl(ui, url):
237 try:
237 try:
238 proto, path = url.split('://', 1)
238 proto, path = url.split('://', 1)
239 if proto == 'file':
239 if proto == 'file':
240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
240 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
241 and path[2:6].lower() == '%3a/'):
241 and path[2:6].lower() == '%3a/'):
242 path = path[:2] + ':/' + path[6:]
242 path = path[:2] + ':/' + path[6:]
243 path = urllib.url2pathname(path)
243 path = urllib.url2pathname(path)
244 except ValueError:
244 except ValueError:
245 proto = 'file'
245 proto = 'file'
246 path = os.path.abspath(url)
246 path = os.path.abspath(url)
247 if proto == 'file':
247 if proto == 'file':
248 path = util.pconvert(path)
248 path = util.pconvert(path)
249 check = protomap.get(proto, lambda *args: False)
249 check = protomap.get(proto, lambda *args: False)
250 while '/' in path:
250 while '/' in path:
251 if check(ui, path, proto):
251 if check(ui, path, proto):
252 return True
252 return True
253 path = path.rsplit('/', 1)[0]
253 path = path.rsplit('/', 1)[0]
254 return False
254 return False
255
255
256 # SVN conversion code stolen from bzr-svn and tailor
256 # SVN conversion code stolen from bzr-svn and tailor
257 #
257 #
258 # Subversion looks like a versioned filesystem, branches structures
258 # Subversion looks like a versioned filesystem, branches structures
259 # are defined by conventions and not enforced by the tool. First,
259 # are defined by conventions and not enforced by the tool. First,
260 # we define the potential branches (modules) as "trunk" and "branches"
260 # we define the potential branches (modules) as "trunk" and "branches"
261 # children directories. Revisions are then identified by their
261 # children directories. Revisions are then identified by their
262 # module and revision number (and a repository identifier).
262 # module and revision number (and a repository identifier).
263 #
263 #
264 # The revision graph is really a tree (or a forest). By default, a
264 # The revision graph is really a tree (or a forest). By default, a
265 # revision parent is the previous revision in the same module. If the
265 # revision parent is the previous revision in the same module. If the
266 # module directory is copied/moved from another module then the
266 # module directory is copied/moved from another module then the
267 # revision is the module root and its parent the source revision in
267 # revision is the module root and its parent the source revision in
268 # the parent module. A revision has at most one parent.
268 # the parent module. A revision has at most one parent.
269 #
269 #
270 class svn_source(converter_source):
270 class svn_source(converter_source):
271 def __init__(self, ui, url, rev=None):
271 def __init__(self, ui, url, revs=None):
272 super(svn_source, self).__init__(ui, url, rev=rev)
272 super(svn_source, self).__init__(ui, url, revs=revs)
273
273
274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
274 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
275 (os.path.exists(url) and
275 (os.path.exists(url) and
276 os.path.exists(os.path.join(url, '.svn'))) or
276 os.path.exists(os.path.join(url, '.svn'))) or
277 issvnurl(ui, url)):
277 issvnurl(ui, url)):
278 raise NoRepo(_("%s does not look like a Subversion repository")
278 raise NoRepo(_("%s does not look like a Subversion repository")
279 % url)
279 % url)
280 if svn is None:
280 if svn is None:
281 raise MissingTool(_('could not load Subversion python bindings'))
281 raise MissingTool(_('could not load Subversion python bindings'))
282
282
283 try:
283 try:
284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
284 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
285 if version < (1, 4):
285 if version < (1, 4):
286 raise MissingTool(_('Subversion python bindings %d.%d found, '
286 raise MissingTool(_('Subversion python bindings %d.%d found, '
287 '1.4 or later required') % version)
287 '1.4 or later required') % version)
288 except AttributeError:
288 except AttributeError:
289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
289 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
290 'or later required'))
290 'or later required'))
291
291
292 self.lastrevs = {}
292 self.lastrevs = {}
293
293
294 latest = None
294 latest = None
295 try:
295 try:
296 # Support file://path@rev syntax. Useful e.g. to convert
296 # Support file://path@rev syntax. Useful e.g. to convert
297 # deleted branches.
297 # deleted branches.
298 at = url.rfind('@')
298 at = url.rfind('@')
299 if at >= 0:
299 if at >= 0:
300 latest = int(url[at + 1:])
300 latest = int(url[at + 1:])
301 url = url[:at]
301 url = url[:at]
302 except ValueError:
302 except ValueError:
303 pass
303 pass
304 self.url = geturl(url)
304 self.url = geturl(url)
305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
305 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
306 try:
306 try:
307 self.transport = transport.SvnRaTransport(url=self.url)
307 self.transport = transport.SvnRaTransport(url=self.url)
308 self.ra = self.transport.ra
308 self.ra = self.transport.ra
309 self.ctx = self.transport.client
309 self.ctx = self.transport.client
310 self.baseurl = svn.ra.get_repos_root(self.ra)
310 self.baseurl = svn.ra.get_repos_root(self.ra)
311 # Module is either empty or a repository path starting with
311 # Module is either empty or a repository path starting with
312 # a slash and not ending with a slash.
312 # a slash and not ending with a slash.
313 self.module = urllib.unquote(self.url[len(self.baseurl):])
313 self.module = urllib.unquote(self.url[len(self.baseurl):])
314 self.prevmodule = None
314 self.prevmodule = None
315 self.rootmodule = self.module
315 self.rootmodule = self.module
316 self.commits = {}
316 self.commits = {}
317 self.paths = {}
317 self.paths = {}
318 self.uuid = svn.ra.get_uuid(self.ra)
318 self.uuid = svn.ra.get_uuid(self.ra)
319 except SubversionException:
319 except SubversionException:
320 ui.traceback()
320 ui.traceback()
321 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
321 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
322 svn.core.SVN_VER_MINOR,
322 svn.core.SVN_VER_MINOR,
323 svn.core.SVN_VER_MICRO)
323 svn.core.SVN_VER_MICRO)
324 raise NoRepo(_("%s does not look like a Subversion repository "
324 raise NoRepo(_("%s does not look like a Subversion repository "
325 "to libsvn version %s")
325 "to libsvn version %s")
326 % (self.url, svnversion))
326 % (self.url, svnversion))
327
327
328 if rev:
328 if revs:
329 if len(revs) > 1:
330 raise util.Abort(_('subversion source does not support '
331 'specifying multiple revisions'))
329 try:
332 try:
330 latest = int(rev)
333 latest = int(revs[0])
331 except ValueError:
334 except ValueError:
332 raise util.Abort(_('svn: revision %s is not an integer') % rev)
335 raise util.Abort(_('svn: revision %s is not an integer') %
336 revs[0])
333
337
334 self.trunkname = self.ui.config('convert', 'svn.trunk',
338 self.trunkname = self.ui.config('convert', 'svn.trunk',
335 'trunk').strip('/')
339 'trunk').strip('/')
336 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
340 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
337 try:
341 try:
338 self.startrev = int(self.startrev)
342 self.startrev = int(self.startrev)
339 if self.startrev < 0:
343 if self.startrev < 0:
340 self.startrev = 0
344 self.startrev = 0
341 except ValueError:
345 except ValueError:
342 raise util.Abort(_('svn: start revision %s is not an integer')
346 raise util.Abort(_('svn: start revision %s is not an integer')
343 % self.startrev)
347 % self.startrev)
344
348
345 try:
349 try:
346 self.head = self.latest(self.module, latest)
350 self.head = self.latest(self.module, latest)
347 except SvnPathNotFound:
351 except SvnPathNotFound:
348 self.head = None
352 self.head = None
349 if not self.head:
353 if not self.head:
350 raise util.Abort(_('no revision found in module %s')
354 raise util.Abort(_('no revision found in module %s')
351 % self.module)
355 % self.module)
352 self.last_changed = self.revnum(self.head)
356 self.last_changed = self.revnum(self.head)
353
357
354 self._changescache = (None, None)
358 self._changescache = (None, None)
355
359
356 if os.path.exists(os.path.join(url, '.svn/entries')):
360 if os.path.exists(os.path.join(url, '.svn/entries')):
357 self.wc = url
361 self.wc = url
358 else:
362 else:
359 self.wc = None
363 self.wc = None
360 self.convertfp = None
364 self.convertfp = None
361
365
362 def setrevmap(self, revmap):
366 def setrevmap(self, revmap):
363 lastrevs = {}
367 lastrevs = {}
364 for revid in revmap.iterkeys():
368 for revid in revmap.iterkeys():
365 uuid, module, revnum = revsplit(revid)
369 uuid, module, revnum = revsplit(revid)
366 lastrevnum = lastrevs.setdefault(module, revnum)
370 lastrevnum = lastrevs.setdefault(module, revnum)
367 if revnum > lastrevnum:
371 if revnum > lastrevnum:
368 lastrevs[module] = revnum
372 lastrevs[module] = revnum
369 self.lastrevs = lastrevs
373 self.lastrevs = lastrevs
370
374
371 def exists(self, path, optrev):
375 def exists(self, path, optrev):
372 try:
376 try:
373 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
377 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
374 optrev, False, self.ctx)
378 optrev, False, self.ctx)
375 return True
379 return True
376 except SubversionException:
380 except SubversionException:
377 return False
381 return False
378
382
379 def getheads(self):
383 def getheads(self):
380
384
381 def isdir(path, revnum):
385 def isdir(path, revnum):
382 kind = self._checkpath(path, revnum)
386 kind = self._checkpath(path, revnum)
383 return kind == svn.core.svn_node_dir
387 return kind == svn.core.svn_node_dir
384
388
385 def getcfgpath(name, rev):
389 def getcfgpath(name, rev):
386 cfgpath = self.ui.config('convert', 'svn.' + name)
390 cfgpath = self.ui.config('convert', 'svn.' + name)
387 if cfgpath is not None and cfgpath.strip() == '':
391 if cfgpath is not None and cfgpath.strip() == '':
388 return None
392 return None
389 path = (cfgpath or name).strip('/')
393 path = (cfgpath or name).strip('/')
390 if not self.exists(path, rev):
394 if not self.exists(path, rev):
391 if self.module.endswith(path) and name == 'trunk':
395 if self.module.endswith(path) and name == 'trunk':
392 # we are converting from inside this directory
396 # we are converting from inside this directory
393 return None
397 return None
394 if cfgpath:
398 if cfgpath:
395 raise util.Abort(_('expected %s to be at %r, but not found')
399 raise util.Abort(_('expected %s to be at %r, but not found')
396 % (name, path))
400 % (name, path))
397 return None
401 return None
398 self.ui.note(_('found %s at %r\n') % (name, path))
402 self.ui.note(_('found %s at %r\n') % (name, path))
399 return path
403 return path
400
404
401 rev = optrev(self.last_changed)
405 rev = optrev(self.last_changed)
402 oldmodule = ''
406 oldmodule = ''
403 trunk = getcfgpath('trunk', rev)
407 trunk = getcfgpath('trunk', rev)
404 self.tags = getcfgpath('tags', rev)
408 self.tags = getcfgpath('tags', rev)
405 branches = getcfgpath('branches', rev)
409 branches = getcfgpath('branches', rev)
406
410
407 # If the project has a trunk or branches, we will extract heads
411 # If the project has a trunk or branches, we will extract heads
408 # from them. We keep the project root otherwise.
412 # from them. We keep the project root otherwise.
409 if trunk:
413 if trunk:
410 oldmodule = self.module or ''
414 oldmodule = self.module or ''
411 self.module += '/' + trunk
415 self.module += '/' + trunk
412 self.head = self.latest(self.module, self.last_changed)
416 self.head = self.latest(self.module, self.last_changed)
413 if not self.head:
417 if not self.head:
414 raise util.Abort(_('no revision found in module %s')
418 raise util.Abort(_('no revision found in module %s')
415 % self.module)
419 % self.module)
416
420
417 # First head in the list is the module's head
421 # First head in the list is the module's head
418 self.heads = [self.head]
422 self.heads = [self.head]
419 if self.tags is not None:
423 if self.tags is not None:
420 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
424 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
421
425
422 # Check if branches bring a few more heads to the list
426 # Check if branches bring a few more heads to the list
423 if branches:
427 if branches:
424 rpath = self.url.strip('/')
428 rpath = self.url.strip('/')
425 branchnames = svn.client.ls(rpath + '/' + quote(branches),
429 branchnames = svn.client.ls(rpath + '/' + quote(branches),
426 rev, False, self.ctx)
430 rev, False, self.ctx)
427 for branch in sorted(branchnames):
431 for branch in sorted(branchnames):
428 module = '%s/%s/%s' % (oldmodule, branches, branch)
432 module = '%s/%s/%s' % (oldmodule, branches, branch)
429 if not isdir(module, self.last_changed):
433 if not isdir(module, self.last_changed):
430 continue
434 continue
431 brevid = self.latest(module, self.last_changed)
435 brevid = self.latest(module, self.last_changed)
432 if not brevid:
436 if not brevid:
433 self.ui.note(_('ignoring empty branch %s\n') % branch)
437 self.ui.note(_('ignoring empty branch %s\n') % branch)
434 continue
438 continue
435 self.ui.note(_('found branch %s at %d\n') %
439 self.ui.note(_('found branch %s at %d\n') %
436 (branch, self.revnum(brevid)))
440 (branch, self.revnum(brevid)))
437 self.heads.append(brevid)
441 self.heads.append(brevid)
438
442
439 if self.startrev and self.heads:
443 if self.startrev and self.heads:
440 if len(self.heads) > 1:
444 if len(self.heads) > 1:
441 raise util.Abort(_('svn: start revision is not supported '
445 raise util.Abort(_('svn: start revision is not supported '
442 'with more than one branch'))
446 'with more than one branch'))
443 revnum = self.revnum(self.heads[0])
447 revnum = self.revnum(self.heads[0])
444 if revnum < self.startrev:
448 if revnum < self.startrev:
445 raise util.Abort(
449 raise util.Abort(
446 _('svn: no revision found after start revision %d')
450 _('svn: no revision found after start revision %d')
447 % self.startrev)
451 % self.startrev)
448
452
449 return self.heads
453 return self.heads
450
454
451 def _getchanges(self, rev, full):
455 def _getchanges(self, rev, full):
452 (paths, parents) = self.paths[rev]
456 (paths, parents) = self.paths[rev]
453 copies = {}
457 copies = {}
454 if parents:
458 if parents:
455 files, self.removed, copies = self.expandpaths(rev, paths, parents)
459 files, self.removed, copies = self.expandpaths(rev, paths, parents)
456 if full or not parents:
460 if full or not parents:
457 # Perform a full checkout on roots
461 # Perform a full checkout on roots
458 uuid, module, revnum = revsplit(rev)
462 uuid, module, revnum = revsplit(rev)
459 entries = svn.client.ls(self.baseurl + quote(module),
463 entries = svn.client.ls(self.baseurl + quote(module),
460 optrev(revnum), True, self.ctx)
464 optrev(revnum), True, self.ctx)
461 files = [n for n, e in entries.iteritems()
465 files = [n for n, e in entries.iteritems()
462 if e.kind == svn.core.svn_node_file]
466 if e.kind == svn.core.svn_node_file]
463 self.removed = set()
467 self.removed = set()
464
468
465 files.sort()
469 files.sort()
466 files = zip(files, [rev] * len(files))
470 files = zip(files, [rev] * len(files))
467 return (files, copies)
471 return (files, copies)
468
472
469 def getchanges(self, rev, full):
473 def getchanges(self, rev, full):
470 # reuse cache from getchangedfiles
474 # reuse cache from getchangedfiles
471 if self._changescache[0] == rev and not full:
475 if self._changescache[0] == rev and not full:
472 (files, copies) = self._changescache[1]
476 (files, copies) = self._changescache[1]
473 else:
477 else:
474 (files, copies) = self._getchanges(rev, full)
478 (files, copies) = self._getchanges(rev, full)
475 # caller caches the result, so free it here to release memory
479 # caller caches the result, so free it here to release memory
476 del self.paths[rev]
480 del self.paths[rev]
477 return (files, copies, set())
481 return (files, copies, set())
478
482
479 def getchangedfiles(self, rev, i):
483 def getchangedfiles(self, rev, i):
480 # called from filemap - cache computed values for reuse in getchanges
484 # called from filemap - cache computed values for reuse in getchanges
481 (files, copies) = self._getchanges(rev, False)
485 (files, copies) = self._getchanges(rev, False)
482 self._changescache = (rev, (files, copies))
486 self._changescache = (rev, (files, copies))
483 return [f[0] for f in files]
487 return [f[0] for f in files]
484
488
485 def getcommit(self, rev):
489 def getcommit(self, rev):
486 if rev not in self.commits:
490 if rev not in self.commits:
487 uuid, module, revnum = revsplit(rev)
491 uuid, module, revnum = revsplit(rev)
488 self.module = module
492 self.module = module
489 self.reparent(module)
493 self.reparent(module)
490 # We assume that:
494 # We assume that:
491 # - requests for revisions after "stop" come from the
495 # - requests for revisions after "stop" come from the
492 # revision graph backward traversal. Cache all of them
496 # revision graph backward traversal. Cache all of them
493 # down to stop, they will be used eventually.
497 # down to stop, they will be used eventually.
494 # - requests for revisions before "stop" come to get
498 # - requests for revisions before "stop" come to get
495 # isolated branches parents. Just fetch what is needed.
499 # isolated branches parents. Just fetch what is needed.
496 stop = self.lastrevs.get(module, 0)
500 stop = self.lastrevs.get(module, 0)
497 if revnum < stop:
501 if revnum < stop:
498 stop = revnum + 1
502 stop = revnum + 1
499 self._fetch_revisions(revnum, stop)
503 self._fetch_revisions(revnum, stop)
500 if rev not in self.commits:
504 if rev not in self.commits:
501 raise util.Abort(_('svn: revision %s not found') % revnum)
505 raise util.Abort(_('svn: revision %s not found') % revnum)
502 revcommit = self.commits[rev]
506 revcommit = self.commits[rev]
503 # caller caches the result, so free it here to release memory
507 # caller caches the result, so free it here to release memory
504 del self.commits[rev]
508 del self.commits[rev]
505 return revcommit
509 return revcommit
506
510
507 def checkrevformat(self, revstr, mapname='splicemap'):
511 def checkrevformat(self, revstr, mapname='splicemap'):
508 """ fails if revision format does not match the correct format"""
512 """ fails if revision format does not match the correct format"""
509 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
513 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
510 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
514 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
511 '{12,12}(.*)\@[0-9]+$',revstr):
515 '{12,12}(.*)\@[0-9]+$',revstr):
512 raise util.Abort(_('%s entry %s is not a valid revision'
516 raise util.Abort(_('%s entry %s is not a valid revision'
513 ' identifier') % (mapname, revstr))
517 ' identifier') % (mapname, revstr))
514
518
515 def numcommits(self):
519 def numcommits(self):
516 return int(self.head.rsplit('@', 1)[1]) - self.startrev
520 return int(self.head.rsplit('@', 1)[1]) - self.startrev
517
521
518 def gettags(self):
522 def gettags(self):
519 tags = {}
523 tags = {}
520 if self.tags is None:
524 if self.tags is None:
521 return tags
525 return tags
522
526
523 # svn tags are just a convention, project branches left in a
527 # svn tags are just a convention, project branches left in a
524 # 'tags' directory. There is no other relationship than
528 # 'tags' directory. There is no other relationship than
525 # ancestry, which is expensive to discover and makes them hard
529 # ancestry, which is expensive to discover and makes them hard
526 # to update incrementally. Worse, past revisions may be
530 # to update incrementally. Worse, past revisions may be
527 # referenced by tags far away in the future, requiring a deep
531 # referenced by tags far away in the future, requiring a deep
528 # history traversal on every calculation. Current code
532 # history traversal on every calculation. Current code
529 # performs a single backward traversal, tracking moves within
533 # performs a single backward traversal, tracking moves within
530 # the tags directory (tag renaming) and recording a new tag
534 # the tags directory (tag renaming) and recording a new tag
531 # everytime a project is copied from outside the tags
535 # everytime a project is copied from outside the tags
532 # directory. It also lists deleted tags, this behaviour may
536 # directory. It also lists deleted tags, this behaviour may
533 # change in the future.
537 # change in the future.
534 pendings = []
538 pendings = []
535 tagspath = self.tags
539 tagspath = self.tags
536 start = svn.ra.get_latest_revnum(self.ra)
540 start = svn.ra.get_latest_revnum(self.ra)
537 stream = self._getlog([self.tags], start, self.startrev)
541 stream = self._getlog([self.tags], start, self.startrev)
538 try:
542 try:
539 for entry in stream:
543 for entry in stream:
540 origpaths, revnum, author, date, message = entry
544 origpaths, revnum, author, date, message = entry
541 if not origpaths:
545 if not origpaths:
542 origpaths = []
546 origpaths = []
543 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
547 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
544 in origpaths.iteritems() if e.copyfrom_path]
548 in origpaths.iteritems() if e.copyfrom_path]
545 # Apply moves/copies from more specific to general
549 # Apply moves/copies from more specific to general
546 copies.sort(reverse=True)
550 copies.sort(reverse=True)
547
551
548 srctagspath = tagspath
552 srctagspath = tagspath
549 if copies and copies[-1][2] == tagspath:
553 if copies and copies[-1][2] == tagspath:
550 # Track tags directory moves
554 # Track tags directory moves
551 srctagspath = copies.pop()[0]
555 srctagspath = copies.pop()[0]
552
556
553 for source, sourcerev, dest in copies:
557 for source, sourcerev, dest in copies:
554 if not dest.startswith(tagspath + '/'):
558 if not dest.startswith(tagspath + '/'):
555 continue
559 continue
556 for tag in pendings:
560 for tag in pendings:
557 if tag[0].startswith(dest):
561 if tag[0].startswith(dest):
558 tagpath = source + tag[0][len(dest):]
562 tagpath = source + tag[0][len(dest):]
559 tag[:2] = [tagpath, sourcerev]
563 tag[:2] = [tagpath, sourcerev]
560 break
564 break
561 else:
565 else:
562 pendings.append([source, sourcerev, dest])
566 pendings.append([source, sourcerev, dest])
563
567
564 # Filter out tags with children coming from different
568 # Filter out tags with children coming from different
565 # parts of the repository like:
569 # parts of the repository like:
566 # /tags/tag.1 (from /trunk:10)
570 # /tags/tag.1 (from /trunk:10)
567 # /tags/tag.1/foo (from /branches/foo:12)
571 # /tags/tag.1/foo (from /branches/foo:12)
568 # Here/tags/tag.1 discarded as well as its children.
572 # Here/tags/tag.1 discarded as well as its children.
569 # It happens with tools like cvs2svn. Such tags cannot
573 # It happens with tools like cvs2svn. Such tags cannot
570 # be represented in mercurial.
574 # be represented in mercurial.
571 addeds = dict((p, e.copyfrom_path) for p, e
575 addeds = dict((p, e.copyfrom_path) for p, e
572 in origpaths.iteritems()
576 in origpaths.iteritems()
573 if e.action == 'A' and e.copyfrom_path)
577 if e.action == 'A' and e.copyfrom_path)
574 badroots = set()
578 badroots = set()
575 for destroot in addeds:
579 for destroot in addeds:
576 for source, sourcerev, dest in pendings:
580 for source, sourcerev, dest in pendings:
577 if (not dest.startswith(destroot + '/')
581 if (not dest.startswith(destroot + '/')
578 or source.startswith(addeds[destroot] + '/')):
582 or source.startswith(addeds[destroot] + '/')):
579 continue
583 continue
580 badroots.add(destroot)
584 badroots.add(destroot)
581 break
585 break
582
586
583 for badroot in badroots:
587 for badroot in badroots:
584 pendings = [p for p in pendings if p[2] != badroot
588 pendings = [p for p in pendings if p[2] != badroot
585 and not p[2].startswith(badroot + '/')]
589 and not p[2].startswith(badroot + '/')]
586
590
587 # Tell tag renamings from tag creations
591 # Tell tag renamings from tag creations
588 renamings = []
592 renamings = []
589 for source, sourcerev, dest in pendings:
593 for source, sourcerev, dest in pendings:
590 tagname = dest.split('/')[-1]
594 tagname = dest.split('/')[-1]
591 if source.startswith(srctagspath):
595 if source.startswith(srctagspath):
592 renamings.append([source, sourcerev, tagname])
596 renamings.append([source, sourcerev, tagname])
593 continue
597 continue
594 if tagname in tags:
598 if tagname in tags:
595 # Keep the latest tag value
599 # Keep the latest tag value
596 continue
600 continue
597 # From revision may be fake, get one with changes
601 # From revision may be fake, get one with changes
598 try:
602 try:
599 tagid = self.latest(source, sourcerev)
603 tagid = self.latest(source, sourcerev)
600 if tagid and tagname not in tags:
604 if tagid and tagname not in tags:
601 tags[tagname] = tagid
605 tags[tagname] = tagid
602 except SvnPathNotFound:
606 except SvnPathNotFound:
603 # It happens when we are following directories
607 # It happens when we are following directories
604 # we assumed were copied with their parents
608 # we assumed were copied with their parents
605 # but were really created in the tag
609 # but were really created in the tag
606 # directory.
610 # directory.
607 pass
611 pass
608 pendings = renamings
612 pendings = renamings
609 tagspath = srctagspath
613 tagspath = srctagspath
610 finally:
614 finally:
611 stream.close()
615 stream.close()
612 return tags
616 return tags
613
617
614 def converted(self, rev, destrev):
618 def converted(self, rev, destrev):
615 if not self.wc:
619 if not self.wc:
616 return
620 return
617 if self.convertfp is None:
621 if self.convertfp is None:
618 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
622 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
619 'a')
623 'a')
620 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
624 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
621 self.convertfp.flush()
625 self.convertfp.flush()
622
626
623 def revid(self, revnum, module=None):
627 def revid(self, revnum, module=None):
624 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
628 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
625
629
626 def revnum(self, rev):
630 def revnum(self, rev):
627 return int(rev.split('@')[-1])
631 return int(rev.split('@')[-1])
628
632
629 def latest(self, path, stop=None):
633 def latest(self, path, stop=None):
630 """Find the latest revid affecting path, up to stop revision
634 """Find the latest revid affecting path, up to stop revision
631 number. If stop is None, default to repository latest
635 number. If stop is None, default to repository latest
632 revision. It may return a revision in a different module,
636 revision. It may return a revision in a different module,
633 since a branch may be moved without a change being
637 since a branch may be moved without a change being
634 reported. Return None if computed module does not belong to
638 reported. Return None if computed module does not belong to
635 rootmodule subtree.
639 rootmodule subtree.
636 """
640 """
637 def findchanges(path, start, stop=None):
641 def findchanges(path, start, stop=None):
638 stream = self._getlog([path], start, stop or 1)
642 stream = self._getlog([path], start, stop or 1)
639 try:
643 try:
640 for entry in stream:
644 for entry in stream:
641 paths, revnum, author, date, message = entry
645 paths, revnum, author, date, message = entry
642 if stop is None and paths:
646 if stop is None and paths:
643 # We do not know the latest changed revision,
647 # We do not know the latest changed revision,
644 # keep the first one with changed paths.
648 # keep the first one with changed paths.
645 break
649 break
646 if revnum <= stop:
650 if revnum <= stop:
647 break
651 break
648
652
649 for p in paths:
653 for p in paths:
650 if (not path.startswith(p) or
654 if (not path.startswith(p) or
651 not paths[p].copyfrom_path):
655 not paths[p].copyfrom_path):
652 continue
656 continue
653 newpath = paths[p].copyfrom_path + path[len(p):]
657 newpath = paths[p].copyfrom_path + path[len(p):]
654 self.ui.debug("branch renamed from %s to %s at %d\n" %
658 self.ui.debug("branch renamed from %s to %s at %d\n" %
655 (path, newpath, revnum))
659 (path, newpath, revnum))
656 path = newpath
660 path = newpath
657 break
661 break
658 if not paths:
662 if not paths:
659 revnum = None
663 revnum = None
660 return revnum, path
664 return revnum, path
661 finally:
665 finally:
662 stream.close()
666 stream.close()
663
667
664 if not path.startswith(self.rootmodule):
668 if not path.startswith(self.rootmodule):
665 # Requests on foreign branches may be forbidden at server level
669 # Requests on foreign branches may be forbidden at server level
666 self.ui.debug('ignoring foreign branch %r\n' % path)
670 self.ui.debug('ignoring foreign branch %r\n' % path)
667 return None
671 return None
668
672
669 if stop is None:
673 if stop is None:
670 stop = svn.ra.get_latest_revnum(self.ra)
674 stop = svn.ra.get_latest_revnum(self.ra)
671 try:
675 try:
672 prevmodule = self.reparent('')
676 prevmodule = self.reparent('')
673 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
677 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
674 self.reparent(prevmodule)
678 self.reparent(prevmodule)
675 except SubversionException:
679 except SubversionException:
676 dirent = None
680 dirent = None
677 if not dirent:
681 if not dirent:
678 raise SvnPathNotFound(_('%s not found up to revision %d')
682 raise SvnPathNotFound(_('%s not found up to revision %d')
679 % (path, stop))
683 % (path, stop))
680
684
681 # stat() gives us the previous revision on this line of
685 # stat() gives us the previous revision on this line of
682 # development, but it might be in *another module*. Fetch the
686 # development, but it might be in *another module*. Fetch the
683 # log and detect renames down to the latest revision.
687 # log and detect renames down to the latest revision.
684 revnum, realpath = findchanges(path, stop, dirent.created_rev)
688 revnum, realpath = findchanges(path, stop, dirent.created_rev)
685 if revnum is None:
689 if revnum is None:
686 # Tools like svnsync can create empty revision, when
690 # Tools like svnsync can create empty revision, when
687 # synchronizing only a subtree for instance. These empty
691 # synchronizing only a subtree for instance. These empty
688 # revisions created_rev still have their original values
692 # revisions created_rev still have their original values
689 # despite all changes having disappeared and can be
693 # despite all changes having disappeared and can be
690 # returned by ra.stat(), at least when stating the root
694 # returned by ra.stat(), at least when stating the root
691 # module. In that case, do not trust created_rev and scan
695 # module. In that case, do not trust created_rev and scan
692 # the whole history.
696 # the whole history.
693 revnum, realpath = findchanges(path, stop)
697 revnum, realpath = findchanges(path, stop)
694 if revnum is None:
698 if revnum is None:
695 self.ui.debug('ignoring empty branch %r\n' % realpath)
699 self.ui.debug('ignoring empty branch %r\n' % realpath)
696 return None
700 return None
697
701
698 if not realpath.startswith(self.rootmodule):
702 if not realpath.startswith(self.rootmodule):
699 self.ui.debug('ignoring foreign branch %r\n' % realpath)
703 self.ui.debug('ignoring foreign branch %r\n' % realpath)
700 return None
704 return None
701 return self.revid(revnum, realpath)
705 return self.revid(revnum, realpath)
702
706
703 def reparent(self, module):
707 def reparent(self, module):
704 """Reparent the svn transport and return the previous parent."""
708 """Reparent the svn transport and return the previous parent."""
705 if self.prevmodule == module:
709 if self.prevmodule == module:
706 return module
710 return module
707 svnurl = self.baseurl + quote(module)
711 svnurl = self.baseurl + quote(module)
708 prevmodule = self.prevmodule
712 prevmodule = self.prevmodule
709 if prevmodule is None:
713 if prevmodule is None:
710 prevmodule = ''
714 prevmodule = ''
711 self.ui.debug("reparent to %s\n" % svnurl)
715 self.ui.debug("reparent to %s\n" % svnurl)
712 svn.ra.reparent(self.ra, svnurl)
716 svn.ra.reparent(self.ra, svnurl)
713 self.prevmodule = module
717 self.prevmodule = module
714 return prevmodule
718 return prevmodule
715
719
716 def expandpaths(self, rev, paths, parents):
720 def expandpaths(self, rev, paths, parents):
717 changed, removed = set(), set()
721 changed, removed = set(), set()
718 copies = {}
722 copies = {}
719
723
720 new_module, revnum = revsplit(rev)[1:]
724 new_module, revnum = revsplit(rev)[1:]
721 if new_module != self.module:
725 if new_module != self.module:
722 self.module = new_module
726 self.module = new_module
723 self.reparent(self.module)
727 self.reparent(self.module)
724
728
725 for i, (path, ent) in enumerate(paths):
729 for i, (path, ent) in enumerate(paths):
726 self.ui.progress(_('scanning paths'), i, item=path,
730 self.ui.progress(_('scanning paths'), i, item=path,
727 total=len(paths))
731 total=len(paths))
728 entrypath = self.getrelpath(path)
732 entrypath = self.getrelpath(path)
729
733
730 kind = self._checkpath(entrypath, revnum)
734 kind = self._checkpath(entrypath, revnum)
731 if kind == svn.core.svn_node_file:
735 if kind == svn.core.svn_node_file:
732 changed.add(self.recode(entrypath))
736 changed.add(self.recode(entrypath))
733 if not ent.copyfrom_path or not parents:
737 if not ent.copyfrom_path or not parents:
734 continue
738 continue
735 # Copy sources not in parent revisions cannot be
739 # Copy sources not in parent revisions cannot be
736 # represented, ignore their origin for now
740 # represented, ignore their origin for now
737 pmodule, prevnum = revsplit(parents[0])[1:]
741 pmodule, prevnum = revsplit(parents[0])[1:]
738 if ent.copyfrom_rev < prevnum:
742 if ent.copyfrom_rev < prevnum:
739 continue
743 continue
740 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
744 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
741 if not copyfrom_path:
745 if not copyfrom_path:
742 continue
746 continue
743 self.ui.debug("copied to %s from %s@%s\n" %
747 self.ui.debug("copied to %s from %s@%s\n" %
744 (entrypath, copyfrom_path, ent.copyfrom_rev))
748 (entrypath, copyfrom_path, ent.copyfrom_rev))
745 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
749 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
746 elif kind == 0: # gone, but had better be a deleted *file*
750 elif kind == 0: # gone, but had better be a deleted *file*
747 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
751 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
748 pmodule, prevnum = revsplit(parents[0])[1:]
752 pmodule, prevnum = revsplit(parents[0])[1:]
749 parentpath = pmodule + "/" + entrypath
753 parentpath = pmodule + "/" + entrypath
750 fromkind = self._checkpath(entrypath, prevnum, pmodule)
754 fromkind = self._checkpath(entrypath, prevnum, pmodule)
751
755
752 if fromkind == svn.core.svn_node_file:
756 if fromkind == svn.core.svn_node_file:
753 removed.add(self.recode(entrypath))
757 removed.add(self.recode(entrypath))
754 elif fromkind == svn.core.svn_node_dir:
758 elif fromkind == svn.core.svn_node_dir:
755 oroot = parentpath.strip('/')
759 oroot = parentpath.strip('/')
756 nroot = path.strip('/')
760 nroot = path.strip('/')
757 children = self._iterfiles(oroot, prevnum)
761 children = self._iterfiles(oroot, prevnum)
758 for childpath in children:
762 for childpath in children:
759 childpath = childpath.replace(oroot, nroot)
763 childpath = childpath.replace(oroot, nroot)
760 childpath = self.getrelpath("/" + childpath, pmodule)
764 childpath = self.getrelpath("/" + childpath, pmodule)
761 if childpath:
765 if childpath:
762 removed.add(self.recode(childpath))
766 removed.add(self.recode(childpath))
763 else:
767 else:
764 self.ui.debug('unknown path in revision %d: %s\n' % \
768 self.ui.debug('unknown path in revision %d: %s\n' % \
765 (revnum, path))
769 (revnum, path))
766 elif kind == svn.core.svn_node_dir:
770 elif kind == svn.core.svn_node_dir:
767 if ent.action == 'M':
771 if ent.action == 'M':
768 # If the directory just had a prop change,
772 # If the directory just had a prop change,
769 # then we shouldn't need to look for its children.
773 # then we shouldn't need to look for its children.
770 continue
774 continue
771 if ent.action == 'R' and parents:
775 if ent.action == 'R' and parents:
772 # If a directory is replacing a file, mark the previous
776 # If a directory is replacing a file, mark the previous
773 # file as deleted
777 # file as deleted
774 pmodule, prevnum = revsplit(parents[0])[1:]
778 pmodule, prevnum = revsplit(parents[0])[1:]
775 pkind = self._checkpath(entrypath, prevnum, pmodule)
779 pkind = self._checkpath(entrypath, prevnum, pmodule)
776 if pkind == svn.core.svn_node_file:
780 if pkind == svn.core.svn_node_file:
777 removed.add(self.recode(entrypath))
781 removed.add(self.recode(entrypath))
778 elif pkind == svn.core.svn_node_dir:
782 elif pkind == svn.core.svn_node_dir:
779 # We do not know what files were kept or removed,
783 # We do not know what files were kept or removed,
780 # mark them all as changed.
784 # mark them all as changed.
781 for childpath in self._iterfiles(pmodule, prevnum):
785 for childpath in self._iterfiles(pmodule, prevnum):
782 childpath = self.getrelpath("/" + childpath)
786 childpath = self.getrelpath("/" + childpath)
783 if childpath:
787 if childpath:
784 changed.add(self.recode(childpath))
788 changed.add(self.recode(childpath))
785
789
786 for childpath in self._iterfiles(path, revnum):
790 for childpath in self._iterfiles(path, revnum):
787 childpath = self.getrelpath("/" + childpath)
791 childpath = self.getrelpath("/" + childpath)
788 if childpath:
792 if childpath:
789 changed.add(self.recode(childpath))
793 changed.add(self.recode(childpath))
790
794
791 # Handle directory copies
795 # Handle directory copies
792 if not ent.copyfrom_path or not parents:
796 if not ent.copyfrom_path or not parents:
793 continue
797 continue
794 # Copy sources not in parent revisions cannot be
798 # Copy sources not in parent revisions cannot be
795 # represented, ignore their origin for now
799 # represented, ignore their origin for now
796 pmodule, prevnum = revsplit(parents[0])[1:]
800 pmodule, prevnum = revsplit(parents[0])[1:]
797 if ent.copyfrom_rev < prevnum:
801 if ent.copyfrom_rev < prevnum:
798 continue
802 continue
799 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
803 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
800 if not copyfrompath:
804 if not copyfrompath:
801 continue
805 continue
802 self.ui.debug("mark %s came from %s:%d\n"
806 self.ui.debug("mark %s came from %s:%d\n"
803 % (path, copyfrompath, ent.copyfrom_rev))
807 % (path, copyfrompath, ent.copyfrom_rev))
804 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
808 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
805 for childpath in children:
809 for childpath in children:
806 childpath = self.getrelpath("/" + childpath, pmodule)
810 childpath = self.getrelpath("/" + childpath, pmodule)
807 if not childpath:
811 if not childpath:
808 continue
812 continue
809 copytopath = path + childpath[len(copyfrompath):]
813 copytopath = path + childpath[len(copyfrompath):]
810 copytopath = self.getrelpath(copytopath)
814 copytopath = self.getrelpath(copytopath)
811 copies[self.recode(copytopath)] = self.recode(childpath)
815 copies[self.recode(copytopath)] = self.recode(childpath)
812
816
813 self.ui.progress(_('scanning paths'), None)
817 self.ui.progress(_('scanning paths'), None)
814 changed.update(removed)
818 changed.update(removed)
815 return (list(changed), removed, copies)
819 return (list(changed), removed, copies)
816
820
817 def _fetch_revisions(self, from_revnum, to_revnum):
821 def _fetch_revisions(self, from_revnum, to_revnum):
818 if from_revnum < to_revnum:
822 if from_revnum < to_revnum:
819 from_revnum, to_revnum = to_revnum, from_revnum
823 from_revnum, to_revnum = to_revnum, from_revnum
820
824
821 self.child_cset = None
825 self.child_cset = None
822
826
823 def parselogentry(orig_paths, revnum, author, date, message):
827 def parselogentry(orig_paths, revnum, author, date, message):
824 """Return the parsed commit object or None, and True if
828 """Return the parsed commit object or None, and True if
825 the revision is a branch root.
829 the revision is a branch root.
826 """
830 """
827 self.ui.debug("parsing revision %d (%d changes)\n" %
831 self.ui.debug("parsing revision %d (%d changes)\n" %
828 (revnum, len(orig_paths)))
832 (revnum, len(orig_paths)))
829
833
830 branched = False
834 branched = False
831 rev = self.revid(revnum)
835 rev = self.revid(revnum)
832 # branch log might return entries for a parent we already have
836 # branch log might return entries for a parent we already have
833
837
834 if rev in self.commits or revnum < to_revnum:
838 if rev in self.commits or revnum < to_revnum:
835 return None, branched
839 return None, branched
836
840
837 parents = []
841 parents = []
838 # check whether this revision is the start of a branch or part
842 # check whether this revision is the start of a branch or part
839 # of a branch renaming
843 # of a branch renaming
840 orig_paths = sorted(orig_paths.iteritems())
844 orig_paths = sorted(orig_paths.iteritems())
841 root_paths = [(p, e) for p, e in orig_paths
845 root_paths = [(p, e) for p, e in orig_paths
842 if self.module.startswith(p)]
846 if self.module.startswith(p)]
843 if root_paths:
847 if root_paths:
844 path, ent = root_paths[-1]
848 path, ent = root_paths[-1]
845 if ent.copyfrom_path:
849 if ent.copyfrom_path:
846 branched = True
850 branched = True
847 newpath = ent.copyfrom_path + self.module[len(path):]
851 newpath = ent.copyfrom_path + self.module[len(path):]
848 # ent.copyfrom_rev may not be the actual last revision
852 # ent.copyfrom_rev may not be the actual last revision
849 previd = self.latest(newpath, ent.copyfrom_rev)
853 previd = self.latest(newpath, ent.copyfrom_rev)
850 if previd is not None:
854 if previd is not None:
851 prevmodule, prevnum = revsplit(previd)[1:]
855 prevmodule, prevnum = revsplit(previd)[1:]
852 if prevnum >= self.startrev:
856 if prevnum >= self.startrev:
853 parents = [previd]
857 parents = [previd]
854 self.ui.note(
858 self.ui.note(
855 _('found parent of branch %s at %d: %s\n') %
859 _('found parent of branch %s at %d: %s\n') %
856 (self.module, prevnum, prevmodule))
860 (self.module, prevnum, prevmodule))
857 else:
861 else:
858 self.ui.debug("no copyfrom path, don't know what to do.\n")
862 self.ui.debug("no copyfrom path, don't know what to do.\n")
859
863
860 paths = []
864 paths = []
861 # filter out unrelated paths
865 # filter out unrelated paths
862 for path, ent in orig_paths:
866 for path, ent in orig_paths:
863 if self.getrelpath(path) is None:
867 if self.getrelpath(path) is None:
864 continue
868 continue
865 paths.append((path, ent))
869 paths.append((path, ent))
866
870
867 # Example SVN datetime. Includes microseconds.
871 # Example SVN datetime. Includes microseconds.
868 # ISO-8601 conformant
872 # ISO-8601 conformant
869 # '2007-01-04T17:35:00.902377Z'
873 # '2007-01-04T17:35:00.902377Z'
870 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
874 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
871 if self.ui.configbool('convert', 'localtimezone'):
875 if self.ui.configbool('convert', 'localtimezone'):
872 date = makedatetimestamp(date[0])
876 date = makedatetimestamp(date[0])
873
877
874 if message:
878 if message:
875 log = self.recode(message)
879 log = self.recode(message)
876 else:
880 else:
877 log = ''
881 log = ''
878
882
879 if author:
883 if author:
880 author = self.recode(author)
884 author = self.recode(author)
881 else:
885 else:
882 author = ''
886 author = ''
883
887
884 try:
888 try:
885 branch = self.module.split("/")[-1]
889 branch = self.module.split("/")[-1]
886 if branch == self.trunkname:
890 if branch == self.trunkname:
887 branch = None
891 branch = None
888 except IndexError:
892 except IndexError:
889 branch = None
893 branch = None
890
894
891 cset = commit(author=author,
895 cset = commit(author=author,
892 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
896 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
893 desc=log,
897 desc=log,
894 parents=parents,
898 parents=parents,
895 branch=branch,
899 branch=branch,
896 rev=rev)
900 rev=rev)
897
901
898 self.commits[rev] = cset
902 self.commits[rev] = cset
899 # The parents list is *shared* among self.paths and the
903 # The parents list is *shared* among self.paths and the
900 # commit object. Both will be updated below.
904 # commit object. Both will be updated below.
901 self.paths[rev] = (paths, cset.parents)
905 self.paths[rev] = (paths, cset.parents)
902 if self.child_cset and not self.child_cset.parents:
906 if self.child_cset and not self.child_cset.parents:
903 self.child_cset.parents[:] = [rev]
907 self.child_cset.parents[:] = [rev]
904 self.child_cset = cset
908 self.child_cset = cset
905 return cset, branched
909 return cset, branched
906
910
907 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
911 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
908 (self.module, from_revnum, to_revnum))
912 (self.module, from_revnum, to_revnum))
909
913
910 try:
914 try:
911 firstcset = None
915 firstcset = None
912 lastonbranch = False
916 lastonbranch = False
913 stream = self._getlog([self.module], from_revnum, to_revnum)
917 stream = self._getlog([self.module], from_revnum, to_revnum)
914 try:
918 try:
915 for entry in stream:
919 for entry in stream:
916 paths, revnum, author, date, message = entry
920 paths, revnum, author, date, message = entry
917 if revnum < self.startrev:
921 if revnum < self.startrev:
918 lastonbranch = True
922 lastonbranch = True
919 break
923 break
920 if not paths:
924 if not paths:
921 self.ui.debug('revision %d has no entries\n' % revnum)
925 self.ui.debug('revision %d has no entries\n' % revnum)
922 # If we ever leave the loop on an empty
926 # If we ever leave the loop on an empty
923 # revision, do not try to get a parent branch
927 # revision, do not try to get a parent branch
924 lastonbranch = lastonbranch or revnum == 0
928 lastonbranch = lastonbranch or revnum == 0
925 continue
929 continue
926 cset, lastonbranch = parselogentry(paths, revnum, author,
930 cset, lastonbranch = parselogentry(paths, revnum, author,
927 date, message)
931 date, message)
928 if cset:
932 if cset:
929 firstcset = cset
933 firstcset = cset
930 if lastonbranch:
934 if lastonbranch:
931 break
935 break
932 finally:
936 finally:
933 stream.close()
937 stream.close()
934
938
935 if not lastonbranch and firstcset and not firstcset.parents:
939 if not lastonbranch and firstcset and not firstcset.parents:
936 # The first revision of the sequence (the last fetched one)
940 # The first revision of the sequence (the last fetched one)
937 # has invalid parents if not a branch root. Find the parent
941 # has invalid parents if not a branch root. Find the parent
938 # revision now, if any.
942 # revision now, if any.
939 try:
943 try:
940 firstrevnum = self.revnum(firstcset.rev)
944 firstrevnum = self.revnum(firstcset.rev)
941 if firstrevnum > 1:
945 if firstrevnum > 1:
942 latest = self.latest(self.module, firstrevnum - 1)
946 latest = self.latest(self.module, firstrevnum - 1)
943 if latest:
947 if latest:
944 firstcset.parents.append(latest)
948 firstcset.parents.append(latest)
945 except SvnPathNotFound:
949 except SvnPathNotFound:
946 pass
950 pass
947 except SubversionException as xxx_todo_changeme:
951 except SubversionException as xxx_todo_changeme:
948 (inst, num) = xxx_todo_changeme.args
952 (inst, num) = xxx_todo_changeme.args
949 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
953 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
950 raise util.Abort(_('svn: branch has no revision %s')
954 raise util.Abort(_('svn: branch has no revision %s')
951 % to_revnum)
955 % to_revnum)
952 raise
956 raise
953
957
954 def getfile(self, file, rev):
958 def getfile(self, file, rev):
955 # TODO: ra.get_file transmits the whole file instead of diffs.
959 # TODO: ra.get_file transmits the whole file instead of diffs.
956 if file in self.removed:
960 if file in self.removed:
957 return None, None
961 return None, None
958 mode = ''
962 mode = ''
959 try:
963 try:
960 new_module, revnum = revsplit(rev)[1:]
964 new_module, revnum = revsplit(rev)[1:]
961 if self.module != new_module:
965 if self.module != new_module:
962 self.module = new_module
966 self.module = new_module
963 self.reparent(self.module)
967 self.reparent(self.module)
964 io = StringIO()
968 io = StringIO()
965 info = svn.ra.get_file(self.ra, file, revnum, io)
969 info = svn.ra.get_file(self.ra, file, revnum, io)
966 data = io.getvalue()
970 data = io.getvalue()
967 # ra.get_file() seems to keep a reference on the input buffer
971 # ra.get_file() seems to keep a reference on the input buffer
968 # preventing collection. Release it explicitly.
972 # preventing collection. Release it explicitly.
969 io.close()
973 io.close()
970 if isinstance(info, list):
974 if isinstance(info, list):
971 info = info[-1]
975 info = info[-1]
972 mode = ("svn:executable" in info) and 'x' or ''
976 mode = ("svn:executable" in info) and 'x' or ''
973 mode = ("svn:special" in info) and 'l' or mode
977 mode = ("svn:special" in info) and 'l' or mode
974 except SubversionException as e:
978 except SubversionException as e:
975 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
979 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
976 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
980 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
977 if e.apr_err in notfound: # File not found
981 if e.apr_err in notfound: # File not found
978 return None, None
982 return None, None
979 raise
983 raise
980 if mode == 'l':
984 if mode == 'l':
981 link_prefix = "link "
985 link_prefix = "link "
982 if data.startswith(link_prefix):
986 if data.startswith(link_prefix):
983 data = data[len(link_prefix):]
987 data = data[len(link_prefix):]
984 return data, mode
988 return data, mode
985
989
986 def _iterfiles(self, path, revnum):
990 def _iterfiles(self, path, revnum):
987 """Enumerate all files in path at revnum, recursively."""
991 """Enumerate all files in path at revnum, recursively."""
988 path = path.strip('/')
992 path = path.strip('/')
989 pool = Pool()
993 pool = Pool()
990 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
994 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
991 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
995 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
992 if path:
996 if path:
993 path += '/'
997 path += '/'
994 return ((path + p) for p, e in entries.iteritems()
998 return ((path + p) for p, e in entries.iteritems()
995 if e.kind == svn.core.svn_node_file)
999 if e.kind == svn.core.svn_node_file)
996
1000
997 def getrelpath(self, path, module=None):
1001 def getrelpath(self, path, module=None):
998 if module is None:
1002 if module is None:
999 module = self.module
1003 module = self.module
1000 # Given the repository url of this wc, say
1004 # Given the repository url of this wc, say
1001 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1005 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1002 # extract the "entry" portion (a relative path) from what
1006 # extract the "entry" portion (a relative path) from what
1003 # svn log --xml says, i.e.
1007 # svn log --xml says, i.e.
1004 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1008 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1005 # that is to say "tests/PloneTestCase.py"
1009 # that is to say "tests/PloneTestCase.py"
1006 if path.startswith(module):
1010 if path.startswith(module):
1007 relative = path.rstrip('/')[len(module):]
1011 relative = path.rstrip('/')[len(module):]
1008 if relative.startswith('/'):
1012 if relative.startswith('/'):
1009 return relative[1:]
1013 return relative[1:]
1010 elif relative == '':
1014 elif relative == '':
1011 return relative
1015 return relative
1012
1016
1013 # The path is outside our tracked tree...
1017 # The path is outside our tracked tree...
1014 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1018 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1015 return None
1019 return None
1016
1020
1017 def _checkpath(self, path, revnum, module=None):
1021 def _checkpath(self, path, revnum, module=None):
1018 if module is not None:
1022 if module is not None:
1019 prevmodule = self.reparent('')
1023 prevmodule = self.reparent('')
1020 path = module + '/' + path
1024 path = module + '/' + path
1021 try:
1025 try:
1022 # ra.check_path does not like leading slashes very much, it leads
1026 # ra.check_path does not like leading slashes very much, it leads
1023 # to PROPFIND subversion errors
1027 # to PROPFIND subversion errors
1024 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1028 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1025 finally:
1029 finally:
1026 if module is not None:
1030 if module is not None:
1027 self.reparent(prevmodule)
1031 self.reparent(prevmodule)
1028
1032
1029 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1033 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1030 strict_node_history=False):
1034 strict_node_history=False):
1031 # Normalize path names, svn >= 1.5 only wants paths relative to
1035 # Normalize path names, svn >= 1.5 only wants paths relative to
1032 # supplied URL
1036 # supplied URL
1033 relpaths = []
1037 relpaths = []
1034 for p in paths:
1038 for p in paths:
1035 if not p.startswith('/'):
1039 if not p.startswith('/'):
1036 p = self.module + '/' + p
1040 p = self.module + '/' + p
1037 relpaths.append(p.strip('/'))
1041 relpaths.append(p.strip('/'))
1038 args = [self.baseurl, relpaths, start, end, limit,
1042 args = [self.baseurl, relpaths, start, end, limit,
1039 discover_changed_paths, strict_node_history]
1043 discover_changed_paths, strict_node_history]
1040 # undocumented feature: debugsvnlog can be disabled
1044 # undocumented feature: debugsvnlog can be disabled
1041 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1045 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1042 return directlogstream(*args)
1046 return directlogstream(*args)
1043 arg = encodeargs(args)
1047 arg = encodeargs(args)
1044 hgexe = util.hgexecutable()
1048 hgexe = util.hgexecutable()
1045 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1049 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1046 stdin, stdout = util.popen2(util.quotecommand(cmd))
1050 stdin, stdout = util.popen2(util.quotecommand(cmd))
1047 stdin.write(arg)
1051 stdin.write(arg)
1048 try:
1052 try:
1049 stdin.close()
1053 stdin.close()
1050 except IOError:
1054 except IOError:
1051 raise util.Abort(_('Mercurial failed to run itself, check'
1055 raise util.Abort(_('Mercurial failed to run itself, check'
1052 ' hg executable is in PATH'))
1056 ' hg executable is in PATH'))
1053 return logstream(stdout)
1057 return logstream(stdout)
1054
1058
1055 pre_revprop_change = '''#!/bin/sh
1059 pre_revprop_change = '''#!/bin/sh
1056
1060
1057 REPOS="$1"
1061 REPOS="$1"
1058 REV="$2"
1062 REV="$2"
1059 USER="$3"
1063 USER="$3"
1060 PROPNAME="$4"
1064 PROPNAME="$4"
1061 ACTION="$5"
1065 ACTION="$5"
1062
1066
1063 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1067 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1064 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1068 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1065 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1069 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1066
1070
1067 echo "Changing prohibited revision property" >&2
1071 echo "Changing prohibited revision property" >&2
1068 exit 1
1072 exit 1
1069 '''
1073 '''
1070
1074
1071 class svn_sink(converter_sink, commandline):
1075 class svn_sink(converter_sink, commandline):
1072 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1076 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1073 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1077 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1074
1078
1075 def prerun(self):
1079 def prerun(self):
1076 if self.wc:
1080 if self.wc:
1077 os.chdir(self.wc)
1081 os.chdir(self.wc)
1078
1082
1079 def postrun(self):
1083 def postrun(self):
1080 if self.wc:
1084 if self.wc:
1081 os.chdir(self.cwd)
1085 os.chdir(self.cwd)
1082
1086
1083 def join(self, name):
1087 def join(self, name):
1084 return os.path.join(self.wc, '.svn', name)
1088 return os.path.join(self.wc, '.svn', name)
1085
1089
1086 def revmapfile(self):
1090 def revmapfile(self):
1087 return self.join('hg-shamap')
1091 return self.join('hg-shamap')
1088
1092
1089 def authorfile(self):
1093 def authorfile(self):
1090 return self.join('hg-authormap')
1094 return self.join('hg-authormap')
1091
1095
1092 def __init__(self, ui, path):
1096 def __init__(self, ui, path):
1093
1097
1094 converter_sink.__init__(self, ui, path)
1098 converter_sink.__init__(self, ui, path)
1095 commandline.__init__(self, ui, 'svn')
1099 commandline.__init__(self, ui, 'svn')
1096 self.delete = []
1100 self.delete = []
1097 self.setexec = []
1101 self.setexec = []
1098 self.delexec = []
1102 self.delexec = []
1099 self.copies = []
1103 self.copies = []
1100 self.wc = None
1104 self.wc = None
1101 self.cwd = os.getcwd()
1105 self.cwd = os.getcwd()
1102
1106
1103 created = False
1107 created = False
1104 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1108 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1105 self.wc = os.path.realpath(path)
1109 self.wc = os.path.realpath(path)
1106 self.run0('update')
1110 self.run0('update')
1107 else:
1111 else:
1108 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1112 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1109 path = os.path.realpath(path)
1113 path = os.path.realpath(path)
1110 if os.path.isdir(os.path.dirname(path)):
1114 if os.path.isdir(os.path.dirname(path)):
1111 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1115 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1112 ui.status(_('initializing svn repository %r\n') %
1116 ui.status(_('initializing svn repository %r\n') %
1113 os.path.basename(path))
1117 os.path.basename(path))
1114 commandline(ui, 'svnadmin').run0('create', path)
1118 commandline(ui, 'svnadmin').run0('create', path)
1115 created = path
1119 created = path
1116 path = util.normpath(path)
1120 path = util.normpath(path)
1117 if not path.startswith('/'):
1121 if not path.startswith('/'):
1118 path = '/' + path
1122 path = '/' + path
1119 path = 'file://' + path
1123 path = 'file://' + path
1120
1124
1121 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1125 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1122 ui.status(_('initializing svn working copy %r\n')
1126 ui.status(_('initializing svn working copy %r\n')
1123 % os.path.basename(wcpath))
1127 % os.path.basename(wcpath))
1124 self.run0('checkout', path, wcpath)
1128 self.run0('checkout', path, wcpath)
1125
1129
1126 self.wc = wcpath
1130 self.wc = wcpath
1127 self.opener = scmutil.opener(self.wc)
1131 self.opener = scmutil.opener(self.wc)
1128 self.wopener = scmutil.opener(self.wc)
1132 self.wopener = scmutil.opener(self.wc)
1129 self.childmap = mapfile(ui, self.join('hg-childmap'))
1133 self.childmap = mapfile(ui, self.join('hg-childmap'))
1130 if util.checkexec(self.wc):
1134 if util.checkexec(self.wc):
1131 self.is_exec = util.isexec
1135 self.is_exec = util.isexec
1132 else:
1136 else:
1133 self.is_exec = None
1137 self.is_exec = None
1134
1138
1135 if created:
1139 if created:
1136 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1140 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1137 fp = open(hook, 'w')
1141 fp = open(hook, 'w')
1138 fp.write(pre_revprop_change)
1142 fp.write(pre_revprop_change)
1139 fp.close()
1143 fp.close()
1140 util.setflags(hook, False, True)
1144 util.setflags(hook, False, True)
1141
1145
1142 output = self.run0('info')
1146 output = self.run0('info')
1143 self.uuid = self.uuid_re.search(output).group(1).strip()
1147 self.uuid = self.uuid_re.search(output).group(1).strip()
1144
1148
1145 def wjoin(self, *names):
1149 def wjoin(self, *names):
1146 return os.path.join(self.wc, *names)
1150 return os.path.join(self.wc, *names)
1147
1151
1148 @propertycache
1152 @propertycache
1149 def manifest(self):
1153 def manifest(self):
1150 # As of svn 1.7, the "add" command fails when receiving
1154 # As of svn 1.7, the "add" command fails when receiving
1151 # already tracked entries, so we have to track and filter them
1155 # already tracked entries, so we have to track and filter them
1152 # ourselves.
1156 # ourselves.
1153 m = set()
1157 m = set()
1154 output = self.run0('ls', recursive=True, xml=True)
1158 output = self.run0('ls', recursive=True, xml=True)
1155 doc = xml.dom.minidom.parseString(output)
1159 doc = xml.dom.minidom.parseString(output)
1156 for e in doc.getElementsByTagName('entry'):
1160 for e in doc.getElementsByTagName('entry'):
1157 for n in e.childNodes:
1161 for n in e.childNodes:
1158 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1162 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1159 continue
1163 continue
1160 name = ''.join(c.data for c in n.childNodes
1164 name = ''.join(c.data for c in n.childNodes
1161 if c.nodeType == c.TEXT_NODE)
1165 if c.nodeType == c.TEXT_NODE)
1162 # Entries are compared with names coming from
1166 # Entries are compared with names coming from
1163 # mercurial, so bytes with undefined encoding. Our
1167 # mercurial, so bytes with undefined encoding. Our
1164 # best bet is to assume they are in local
1168 # best bet is to assume they are in local
1165 # encoding. They will be passed to command line calls
1169 # encoding. They will be passed to command line calls
1166 # later anyway, so they better be.
1170 # later anyway, so they better be.
1167 m.add(encoding.tolocal(name.encode('utf-8')))
1171 m.add(encoding.tolocal(name.encode('utf-8')))
1168 break
1172 break
1169 return m
1173 return m
1170
1174
1171 def putfile(self, filename, flags, data):
1175 def putfile(self, filename, flags, data):
1172 if 'l' in flags:
1176 if 'l' in flags:
1173 self.wopener.symlink(data, filename)
1177 self.wopener.symlink(data, filename)
1174 else:
1178 else:
1175 try:
1179 try:
1176 if os.path.islink(self.wjoin(filename)):
1180 if os.path.islink(self.wjoin(filename)):
1177 os.unlink(filename)
1181 os.unlink(filename)
1178 except OSError:
1182 except OSError:
1179 pass
1183 pass
1180 self.wopener.write(filename, data)
1184 self.wopener.write(filename, data)
1181
1185
1182 if self.is_exec:
1186 if self.is_exec:
1183 if self.is_exec(self.wjoin(filename)):
1187 if self.is_exec(self.wjoin(filename)):
1184 if 'x' not in flags:
1188 if 'x' not in flags:
1185 self.delexec.append(filename)
1189 self.delexec.append(filename)
1186 else:
1190 else:
1187 if 'x' in flags:
1191 if 'x' in flags:
1188 self.setexec.append(filename)
1192 self.setexec.append(filename)
1189 util.setflags(self.wjoin(filename), False, 'x' in flags)
1193 util.setflags(self.wjoin(filename), False, 'x' in flags)
1190
1194
1191 def _copyfile(self, source, dest):
1195 def _copyfile(self, source, dest):
1192 # SVN's copy command pukes if the destination file exists, but
1196 # SVN's copy command pukes if the destination file exists, but
1193 # our copyfile method expects to record a copy that has
1197 # our copyfile method expects to record a copy that has
1194 # already occurred. Cross the semantic gap.
1198 # already occurred. Cross the semantic gap.
1195 wdest = self.wjoin(dest)
1199 wdest = self.wjoin(dest)
1196 exists = os.path.lexists(wdest)
1200 exists = os.path.lexists(wdest)
1197 if exists:
1201 if exists:
1198 fd, tempname = tempfile.mkstemp(
1202 fd, tempname = tempfile.mkstemp(
1199 prefix='hg-copy-', dir=os.path.dirname(wdest))
1203 prefix='hg-copy-', dir=os.path.dirname(wdest))
1200 os.close(fd)
1204 os.close(fd)
1201 os.unlink(tempname)
1205 os.unlink(tempname)
1202 os.rename(wdest, tempname)
1206 os.rename(wdest, tempname)
1203 try:
1207 try:
1204 self.run0('copy', source, dest)
1208 self.run0('copy', source, dest)
1205 finally:
1209 finally:
1206 self.manifest.add(dest)
1210 self.manifest.add(dest)
1207 if exists:
1211 if exists:
1208 try:
1212 try:
1209 os.unlink(wdest)
1213 os.unlink(wdest)
1210 except OSError:
1214 except OSError:
1211 pass
1215 pass
1212 os.rename(tempname, wdest)
1216 os.rename(tempname, wdest)
1213
1217
1214 def dirs_of(self, files):
1218 def dirs_of(self, files):
1215 dirs = set()
1219 dirs = set()
1216 for f in files:
1220 for f in files:
1217 if os.path.isdir(self.wjoin(f)):
1221 if os.path.isdir(self.wjoin(f)):
1218 dirs.add(f)
1222 dirs.add(f)
1219 for i in strutil.rfindall(f, '/'):
1223 for i in strutil.rfindall(f, '/'):
1220 dirs.add(f[:i])
1224 dirs.add(f[:i])
1221 return dirs
1225 return dirs
1222
1226
1223 def add_dirs(self, files):
1227 def add_dirs(self, files):
1224 add_dirs = [d for d in sorted(self.dirs_of(files))
1228 add_dirs = [d for d in sorted(self.dirs_of(files))
1225 if d not in self.manifest]
1229 if d not in self.manifest]
1226 if add_dirs:
1230 if add_dirs:
1227 self.manifest.update(add_dirs)
1231 self.manifest.update(add_dirs)
1228 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1232 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1229 return add_dirs
1233 return add_dirs
1230
1234
1231 def add_files(self, files):
1235 def add_files(self, files):
1232 files = [f for f in files if f not in self.manifest]
1236 files = [f for f in files if f not in self.manifest]
1233 if files:
1237 if files:
1234 self.manifest.update(files)
1238 self.manifest.update(files)
1235 self.xargs(files, 'add', quiet=True)
1239 self.xargs(files, 'add', quiet=True)
1236 return files
1240 return files
1237
1241
1238 def addchild(self, parent, child):
1242 def addchild(self, parent, child):
1239 self.childmap[parent] = child
1243 self.childmap[parent] = child
1240
1244
1241 def revid(self, rev):
1245 def revid(self, rev):
1242 return u"svn:%s@%s" % (self.uuid, rev)
1246 return u"svn:%s@%s" % (self.uuid, rev)
1243
1247
1244 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1248 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1245 cleanp2):
1249 cleanp2):
1246 for parent in parents:
1250 for parent in parents:
1247 try:
1251 try:
1248 return self.revid(self.childmap[parent])
1252 return self.revid(self.childmap[parent])
1249 except KeyError:
1253 except KeyError:
1250 pass
1254 pass
1251
1255
1252 # Apply changes to working copy
1256 # Apply changes to working copy
1253 for f, v in files:
1257 for f, v in files:
1254 data, mode = source.getfile(f, v)
1258 data, mode = source.getfile(f, v)
1255 if data is None:
1259 if data is None:
1256 self.delete.append(f)
1260 self.delete.append(f)
1257 else:
1261 else:
1258 self.putfile(f, mode, data)
1262 self.putfile(f, mode, data)
1259 if f in copies:
1263 if f in copies:
1260 self.copies.append([copies[f], f])
1264 self.copies.append([copies[f], f])
1261 if full:
1265 if full:
1262 self.delete.extend(sorted(self.manifest.difference(files)))
1266 self.delete.extend(sorted(self.manifest.difference(files)))
1263 files = [f[0] for f in files]
1267 files = [f[0] for f in files]
1264
1268
1265 entries = set(self.delete)
1269 entries = set(self.delete)
1266 files = frozenset(files)
1270 files = frozenset(files)
1267 entries.update(self.add_dirs(files.difference(entries)))
1271 entries.update(self.add_dirs(files.difference(entries)))
1268 if self.copies:
1272 if self.copies:
1269 for s, d in self.copies:
1273 for s, d in self.copies:
1270 self._copyfile(s, d)
1274 self._copyfile(s, d)
1271 self.copies = []
1275 self.copies = []
1272 if self.delete:
1276 if self.delete:
1273 self.xargs(self.delete, 'delete')
1277 self.xargs(self.delete, 'delete')
1274 for f in self.delete:
1278 for f in self.delete:
1275 self.manifest.remove(f)
1279 self.manifest.remove(f)
1276 self.delete = []
1280 self.delete = []
1277 entries.update(self.add_files(files.difference(entries)))
1281 entries.update(self.add_files(files.difference(entries)))
1278 if self.delexec:
1282 if self.delexec:
1279 self.xargs(self.delexec, 'propdel', 'svn:executable')
1283 self.xargs(self.delexec, 'propdel', 'svn:executable')
1280 self.delexec = []
1284 self.delexec = []
1281 if self.setexec:
1285 if self.setexec:
1282 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1286 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1283 self.setexec = []
1287 self.setexec = []
1284
1288
1285 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1289 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1286 fp = os.fdopen(fd, 'w')
1290 fp = os.fdopen(fd, 'w')
1287 fp.write(commit.desc)
1291 fp.write(commit.desc)
1288 fp.close()
1292 fp.close()
1289 try:
1293 try:
1290 output = self.run0('commit',
1294 output = self.run0('commit',
1291 username=util.shortuser(commit.author),
1295 username=util.shortuser(commit.author),
1292 file=messagefile,
1296 file=messagefile,
1293 encoding='utf-8')
1297 encoding='utf-8')
1294 try:
1298 try:
1295 rev = self.commit_re.search(output).group(1)
1299 rev = self.commit_re.search(output).group(1)
1296 except AttributeError:
1300 except AttributeError:
1297 if parents and not files:
1301 if parents and not files:
1298 return parents[0]
1302 return parents[0]
1299 self.ui.warn(_('unexpected svn output:\n'))
1303 self.ui.warn(_('unexpected svn output:\n'))
1300 self.ui.warn(output)
1304 self.ui.warn(output)
1301 raise util.Abort(_('unable to cope with svn output'))
1305 raise util.Abort(_('unable to cope with svn output'))
1302 if commit.rev:
1306 if commit.rev:
1303 self.run('propset', 'hg:convert-rev', commit.rev,
1307 self.run('propset', 'hg:convert-rev', commit.rev,
1304 revprop=True, revision=rev)
1308 revprop=True, revision=rev)
1305 if commit.branch and commit.branch != 'default':
1309 if commit.branch and commit.branch != 'default':
1306 self.run('propset', 'hg:convert-branch', commit.branch,
1310 self.run('propset', 'hg:convert-branch', commit.branch,
1307 revprop=True, revision=rev)
1311 revprop=True, revision=rev)
1308 for parent in parents:
1312 for parent in parents:
1309 self.addchild(parent, rev)
1313 self.addchild(parent, rev)
1310 return self.revid(rev)
1314 return self.revid(rev)
1311 finally:
1315 finally:
1312 os.unlink(messagefile)
1316 os.unlink(messagefile)
1313
1317
1314 def puttags(self, tags):
1318 def puttags(self, tags):
1315 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1319 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1316 return None, None
1320 return None, None
1317
1321
1318 def hascommitfrommap(self, rev):
1322 def hascommitfrommap(self, rev):
1319 # We trust that revisions referenced in a map still is present
1323 # We trust that revisions referenced in a map still is present
1320 # TODO: implement something better if necessary and feasible
1324 # TODO: implement something better if necessary and feasible
1321 return True
1325 return True
1322
1326
1323 def hascommitforsplicemap(self, rev):
1327 def hascommitforsplicemap(self, rev):
1324 # This is not correct as one can convert to an existing subversion
1328 # This is not correct as one can convert to an existing subversion
1325 # repository and childmap would not list all revisions. Too bad.
1329 # repository and childmap would not list all revisions. Too bad.
1326 if rev in self.childmap:
1330 if rev in self.childmap:
1327 return True
1331 return True
1328 raise util.Abort(_('splice map revision %s not found in subversion '
1332 raise util.Abort(_('splice map revision %s not found in subversion '
1329 'child map (revision lookups are not implemented)')
1333 'child map (revision lookups are not implemented)')
1330 % rev)
1334 % rev)
@@ -1,505 +1,505
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 branch names. This
128 destination repository. No whitespace is allowed in the branch names. This
129 can be used to (for instance) move code in one repository from "default"
129 can be used to (for instance) move code in one repository from "default"
130 to a named branch.
130 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.revs
146 convert.hg.revs
147 revset specifying the source revisions to convert.
147 revset specifying the source revisions to convert.
148
148
149 CVS Source
149 CVS Source
150 ##########
150 ##########
151
151
152 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
152 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
153 indicate the starting point of what will be converted. Direct access to
153 indicate the starting point of what will be converted. Direct access to
154 the repository files is not needed, unless of course the repository is
154 the repository files is not needed, unless of course the repository is
155 ":local:". The conversion uses the top level directory in the sandbox to
155 ":local:". The conversion uses the top level directory in the sandbox to
156 find the CVS repository, and then uses CVS rlog commands to find files to
156 find the CVS repository, and then uses CVS rlog commands to find files to
157 convert. This means that unless a filemap is given, all files under the
157 convert. This means that unless a filemap is given, all files under the
158 starting directory will be converted, and that any directory
158 starting directory will be converted, and that any directory
159 reorganization in the CVS sandbox is ignored.
159 reorganization in the CVS sandbox is ignored.
160
160
161 The following options can be used with "--config":
161 The following options can be used with "--config":
162
162
163 convert.cvsps.cache
163 convert.cvsps.cache
164 Set to False to disable remote log caching, for testing and
164 Set to False to disable remote log caching, for testing and
165 debugging purposes. Default is True.
165 debugging purposes. Default is True.
166 convert.cvsps.fuzz
166 convert.cvsps.fuzz
167 Specify the maximum time (in seconds) that is allowed
167 Specify the maximum time (in seconds) that is allowed
168 between commits with identical user and log message in a
168 between commits with identical user and log message in a
169 single changeset. When very large files were checked in as
169 single changeset. When very large files were checked in as
170 part of a changeset then the default may not be long enough.
170 part of a changeset then the default may not be long enough.
171 The default is 60.
171 The default is 60.
172 convert.cvsps.mergeto
172 convert.cvsps.mergeto
173 Specify a regular expression to which commit log messages
173 Specify a regular expression to which commit log messages
174 are matched. If a match occurs, then the conversion process
174 are matched. If a match occurs, then the conversion process
175 will insert a dummy revision merging the branch on which
175 will insert a dummy revision merging the branch on which
176 this log message occurs to the branch indicated in the
176 this log message occurs to the branch indicated in the
177 regex. Default is "{{mergetobranch ([-\w]+)}}"
177 regex. Default is "{{mergetobranch ([-\w]+)}}"
178 convert.cvsps.mergefrom
178 convert.cvsps.mergefrom
179 Specify a regular expression to which commit log messages
179 Specify a regular expression to which commit log messages
180 are matched. If a match occurs, then the conversion process
180 are matched. If a match occurs, then the conversion process
181 will add the most recent revision on the branch indicated in
181 will add the most recent revision on the branch indicated in
182 the regex as the second parent of the changeset. Default is
182 the regex as the second parent of the changeset. Default is
183 "{{mergefrombranch ([-\w]+)}}"
183 "{{mergefrombranch ([-\w]+)}}"
184 convert.localtimezone
184 convert.localtimezone
185 use local time (as determined by the TZ environment
185 use local time (as determined by the TZ environment
186 variable) for changeset date/times. The default is False
186 variable) for changeset date/times. The default is False
187 (use UTC).
187 (use UTC).
188 hooks.cvslog Specify a Python function to be called at the end of
188 hooks.cvslog Specify a Python function to be called at the end of
189 gathering the CVS log. The function is passed a list with
189 gathering the CVS log. The function is passed a list with
190 the log entries, and can modify the entries in-place, or add
190 the log entries, and can modify the entries in-place, or add
191 or delete them.
191 or delete them.
192 hooks.cvschangesets
192 hooks.cvschangesets
193 Specify a Python function to be called after the changesets
193 Specify a Python function to be called after the changesets
194 are calculated from the CVS log. The function is passed a
194 are calculated from the CVS log. The function is passed a
195 list with the changeset entries, and can modify the
195 list with the changeset entries, and can modify the
196 changesets in-place, or add or delete them.
196 changesets in-place, or add or delete them.
197
197
198 An additional "debugcvsps" Mercurial command allows the builtin changeset
198 An additional "debugcvsps" Mercurial command allows the builtin changeset
199 merging code to be run without doing a conversion. Its parameters and
199 merging code to be run without doing a conversion. Its parameters and
200 output are similar to that of cvsps 2.1. Please see the command help for
200 output are similar to that of cvsps 2.1. Please see the command help for
201 more details.
201 more details.
202
202
203 Subversion Source
203 Subversion Source
204 #################
204 #################
205
205
206 Subversion source detects classical trunk/branches/tags layouts. By
206 Subversion source detects classical trunk/branches/tags layouts. By
207 default, the supplied "svn://repo/path/" source URL is converted as a
207 default, the supplied "svn://repo/path/" source URL is converted as a
208 single branch. If "svn://repo/path/trunk" exists it replaces the default
208 single branch. If "svn://repo/path/trunk" exists it replaces the default
209 branch. If "svn://repo/path/branches" exists, its subdirectories are
209 branch. If "svn://repo/path/branches" exists, its subdirectories are
210 listed as possible branches. If "svn://repo/path/tags" exists, it is
210 listed as possible branches. If "svn://repo/path/tags" exists, it is
211 looked for tags referencing converted branches. Default "trunk",
211 looked for tags referencing converted branches. Default "trunk",
212 "branches" and "tags" values can be overridden with following options. Set
212 "branches" and "tags" values can be overridden with following options. Set
213 them to paths relative to the source URL, or leave them blank to disable
213 them to paths relative to the source URL, or leave them blank to disable
214 auto detection.
214 auto detection.
215
215
216 The following options can be set with "--config":
216 The following options can be set with "--config":
217
217
218 convert.svn.branches
218 convert.svn.branches
219 specify the directory containing branches. The default is
219 specify the directory containing branches. The default is
220 "branches".
220 "branches".
221 convert.svn.tags
221 convert.svn.tags
222 specify the directory containing tags. The default is
222 specify the directory containing tags. The default is
223 "tags".
223 "tags".
224 convert.svn.trunk
224 convert.svn.trunk
225 specify the name of the trunk branch. The default is
225 specify the name of the trunk branch. The default is
226 "trunk".
226 "trunk".
227 convert.localtimezone
227 convert.localtimezone
228 use local time (as determined by the TZ environment
228 use local time (as determined by the TZ environment
229 variable) for changeset date/times. The default is False
229 variable) for changeset date/times. The default is False
230 (use UTC).
230 (use UTC).
231
231
232 Source history can be retrieved starting at a specific revision, instead
232 Source history can be retrieved starting at a specific revision, instead
233 of being integrally converted. Only single branch conversions are
233 of being integrally converted. Only single branch conversions are
234 supported.
234 supported.
235
235
236 convert.svn.startrev
236 convert.svn.startrev
237 specify start Subversion revision number. The default is 0.
237 specify start Subversion revision number. The default is 0.
238
238
239 Git Source
239 Git Source
240 ##########
240 ##########
241
241
242 The Git importer converts commits from all reachable branches (refs in
242 The Git importer converts commits from all reachable branches (refs in
243 refs/heads) and remotes (refs in refs/remotes) to Mercurial. Branches are
243 refs/heads) and remotes (refs in refs/remotes) to Mercurial. Branches are
244 converted to bookmarks with the same name, with the leading 'refs/heads'
244 converted to bookmarks with the same name, with the leading 'refs/heads'
245 stripped. Git submodules are converted to Git subrepos in Mercurial.
245 stripped. Git submodules are converted to Git subrepos in Mercurial.
246
246
247 The following options can be set with "--config":
247 The following options can be set with "--config":
248
248
249 convert.git.similarity
249 convert.git.similarity
250 specify how similar files modified in a commit must be to be
250 specify how similar files modified in a commit must be to be
251 imported as renames or copies, as a percentage between "0"
251 imported as renames or copies, as a percentage between "0"
252 (disabled) and "100" (files must be identical). For example,
252 (disabled) and "100" (files must be identical). For example,
253 "90" means that a delete/add pair will be imported as a
253 "90" means that a delete/add pair will be imported as a
254 rename if more than 90% of the file hasn't changed. The
254 rename if more than 90% of the file hasn't changed. The
255 default is "50".
255 default is "50".
256 convert.git.findcopiesharder
256 convert.git.findcopiesharder
257 while detecting copies, look at all files in the working
257 while detecting copies, look at all files in the working
258 copy instead of just changed ones. This is very expensive
258 copy instead of just changed ones. This is very expensive
259 for large projects, and is only effective when
259 for large projects, and is only effective when
260 "convert.git.similarity" is greater than 0. The default is
260 "convert.git.similarity" is greater than 0. The default is
261 False.
261 False.
262
262
263 Perforce Source
263 Perforce Source
264 ###############
264 ###############
265
265
266 The Perforce (P4) importer can be given a p4 depot path or a client
266 The Perforce (P4) importer can be given a p4 depot path or a client
267 specification as source. It will convert all files in the source to a flat
267 specification as source. It will convert all files in the source to a flat
268 Mercurial repository, ignoring labels, branches and integrations. Note
268 Mercurial repository, ignoring labels, branches and integrations. Note
269 that when a depot path is given you then usually should specify a target
269 that when a depot path is given you then usually should specify a target
270 directory, because otherwise the target may be named "...-hg".
270 directory, because otherwise the target may be named "...-hg".
271
271
272 It is possible to limit the amount of source history to be converted by
272 It is possible to limit the amount of source history to be converted by
273 specifying an initial Perforce revision:
273 specifying an initial Perforce revision:
274
274
275 convert.p4.startrev
275 convert.p4.startrev
276 specify initial Perforce revision (a Perforce changelist
276 specify initial Perforce revision (a Perforce changelist
277 number).
277 number).
278
278
279 Mercurial Destination
279 Mercurial Destination
280 #####################
280 #####################
281
281
282 The Mercurial destination will recognize Mercurial subrepositories in the
282 The Mercurial destination will recognize Mercurial subrepositories in the
283 destination directory, and update the .hgsubstate file automatically if
283 destination directory, and update the .hgsubstate file automatically if
284 the destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
284 the destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
285 Converting a repository with subrepositories requires converting a single
285 Converting a repository with subrepositories requires converting a single
286 repository at a time, from the bottom up.
286 repository at a time, from the bottom up.
287
287
288 The following options are supported:
288 The following options are supported:
289
289
290 convert.hg.clonebranches
290 convert.hg.clonebranches
291 dispatch source branches in separate clones. The default is
291 dispatch source branches in separate clones. The default is
292 False.
292 False.
293 convert.hg.tagsbranch
293 convert.hg.tagsbranch
294 branch name for tag revisions, defaults to "default".
294 branch name for tag revisions, defaults to "default".
295 convert.hg.usebranchnames
295 convert.hg.usebranchnames
296 preserve branch names. The default is True.
296 preserve branch names. The default is True.
297
297
298 All Destinations
298 All Destinations
299 ################
299 ################
300
300
301 All destination types accept the following options:
301 All destination types accept the following options:
302
302
303 convert.skiptags
303 convert.skiptags
304 does not convert tags from the source repo to the target
304 does not convert tags from the source repo to the target
305 repo. The default is False.
305 repo. The default is False.
306
306
307 options:
307 options ([+] can be repeated):
308
308
309 -s --source-type TYPE source repository type
309 -s --source-type TYPE source repository type
310 -d --dest-type TYPE destination repository type
310 -d --dest-type TYPE destination repository type
311 -r --rev REV import up to source revision REV
311 -r --rev REV [+] import up to source revision REV
312 -A --authormap FILE remap usernames using this file
312 -A --authormap FILE remap usernames using this file
313 --filemap FILE remap file names using contents of file
313 --filemap FILE remap file names using contents of file
314 --full apply filemap changes by converting all files again
314 --full apply filemap changes by converting all files again
315 --splicemap FILE splice synthesized history into place
315 --splicemap FILE splice synthesized history into place
316 --branchmap FILE change branch names while converting
316 --branchmap FILE change branch names while converting
317 --branchsort try to sort changesets by branches
317 --branchsort try to sort changesets by branches
318 --datesort try to sort changesets by date
318 --datesort try to sort changesets by date
319 --sourcesort preserve source changesets order
319 --sourcesort preserve source changesets order
320 --closesort try to reorder closed revisions
320 --closesort try to reorder closed revisions
321
321
322 (some details hidden, use --verbose to show complete help)
322 (some details hidden, use --verbose to show complete help)
323 $ hg init a
323 $ hg init a
324 $ cd a
324 $ cd a
325 $ echo a > a
325 $ echo a > a
326 $ hg ci -d'0 0' -Ama
326 $ hg ci -d'0 0' -Ama
327 adding a
327 adding a
328 $ hg cp a b
328 $ hg cp a b
329 $ hg ci -d'1 0' -mb
329 $ hg ci -d'1 0' -mb
330 $ hg rm a
330 $ hg rm a
331 $ hg ci -d'2 0' -mc
331 $ hg ci -d'2 0' -mc
332 $ hg mv b a
332 $ hg mv b a
333 $ hg ci -d'3 0' -md
333 $ hg ci -d'3 0' -md
334 $ echo a >> a
334 $ echo a >> a
335 $ hg ci -d'4 0' -me
335 $ hg ci -d'4 0' -me
336 $ cd ..
336 $ cd ..
337 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
337 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
338 assuming destination a-hg
338 assuming destination a-hg
339 initializing destination a-hg repository
339 initializing destination a-hg repository
340 scanning source...
340 scanning source...
341 sorting...
341 sorting...
342 converting...
342 converting...
343 4 a
343 4 a
344 3 b
344 3 b
345 2 c
345 2 c
346 1 d
346 1 d
347 0 e
347 0 e
348 $ hg --cwd a-hg pull ../a
348 $ hg --cwd a-hg pull ../a
349 pulling from ../a
349 pulling from ../a
350 searching for changes
350 searching for changes
351 no changes found
351 no changes found
352
352
353 conversion to existing file should fail
353 conversion to existing file should fail
354
354
355 $ touch bogusfile
355 $ touch bogusfile
356 $ hg convert a bogusfile
356 $ hg convert a bogusfile
357 initializing destination bogusfile repository
357 initializing destination bogusfile repository
358 abort: cannot create new bundle repository
358 abort: cannot create new bundle repository
359 [255]
359 [255]
360
360
361 #if unix-permissions no-root
361 #if unix-permissions no-root
362
362
363 conversion to dir without permissions should fail
363 conversion to dir without permissions should fail
364
364
365 $ mkdir bogusdir
365 $ mkdir bogusdir
366 $ chmod 000 bogusdir
366 $ chmod 000 bogusdir
367
367
368 $ hg convert a bogusdir
368 $ hg convert a bogusdir
369 abort: Permission denied: 'bogusdir'
369 abort: Permission denied: 'bogusdir'
370 [255]
370 [255]
371
371
372 user permissions should succeed
372 user permissions should succeed
373
373
374 $ chmod 700 bogusdir
374 $ chmod 700 bogusdir
375 $ hg convert a bogusdir
375 $ hg convert a bogusdir
376 initializing destination bogusdir repository
376 initializing destination bogusdir repository
377 scanning source...
377 scanning source...
378 sorting...
378 sorting...
379 converting...
379 converting...
380 4 a
380 4 a
381 3 b
381 3 b
382 2 c
382 2 c
383 1 d
383 1 d
384 0 e
384 0 e
385
385
386 #endif
386 #endif
387
387
388 test pre and post conversion actions
388 test pre and post conversion actions
389
389
390 $ echo 'include b' > filemap
390 $ echo 'include b' > filemap
391 $ hg convert --debug --filemap filemap a partialb | \
391 $ hg convert --debug --filemap filemap a partialb | \
392 > grep 'run hg'
392 > grep 'run hg'
393 run hg source pre-conversion action
393 run hg source pre-conversion action
394 run hg sink pre-conversion action
394 run hg sink pre-conversion action
395 run hg sink post-conversion action
395 run hg sink post-conversion action
396 run hg source post-conversion action
396 run hg source post-conversion action
397
397
398 converting empty dir should fail "nicely
398 converting empty dir should fail "nicely
399
399
400 $ mkdir emptydir
400 $ mkdir emptydir
401
401
402 override $PATH to ensure p4 not visible; use $PYTHON in case we're
402 override $PATH to ensure p4 not visible; use $PYTHON in case we're
403 running from a devel copy, not a temp installation
403 running from a devel copy, not a temp installation
404
404
405 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
405 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
406 assuming destination emptydir-hg
406 assuming destination emptydir-hg
407 initializing destination emptydir-hg repository
407 initializing destination emptydir-hg repository
408 emptydir does not look like a CVS checkout
408 emptydir does not look like a CVS checkout
409 emptydir does not look like a Git repository
409 emptydir does not look like a Git repository
410 emptydir does not look like a Subversion repository
410 emptydir does not look like a Subversion repository
411 emptydir is not a local Mercurial repository
411 emptydir is not a local Mercurial repository
412 emptydir does not look like a darcs repository
412 emptydir does not look like a darcs repository
413 emptydir does not look like a monotone repository
413 emptydir does not look like a monotone repository
414 emptydir does not look like a GNU Arch repository
414 emptydir does not look like a GNU Arch repository
415 emptydir does not look like a Bazaar repository
415 emptydir does not look like a Bazaar repository
416 cannot find required "p4" tool
416 cannot find required "p4" tool
417 abort: emptydir: missing or unsupported repository
417 abort: emptydir: missing or unsupported repository
418 [255]
418 [255]
419
419
420 convert with imaginary source type
420 convert with imaginary source type
421
421
422 $ hg convert --source-type foo a a-foo
422 $ hg convert --source-type foo a a-foo
423 initializing destination a-foo repository
423 initializing destination a-foo repository
424 abort: foo: invalid source repository type
424 abort: foo: invalid source repository type
425 [255]
425 [255]
426
426
427 convert with imaginary sink type
427 convert with imaginary sink type
428
428
429 $ hg convert --dest-type foo a a-foo
429 $ hg convert --dest-type foo a a-foo
430 abort: foo: invalid destination repository type
430 abort: foo: invalid destination repository type
431 [255]
431 [255]
432
432
433 testing: convert must not produce duplicate entries in fncache
433 testing: convert must not produce duplicate entries in fncache
434
434
435 $ hg convert a b
435 $ hg convert a b
436 initializing destination b repository
436 initializing destination b repository
437 scanning source...
437 scanning source...
438 sorting...
438 sorting...
439 converting...
439 converting...
440 4 a
440 4 a
441 3 b
441 3 b
442 2 c
442 2 c
443 1 d
443 1 d
444 0 e
444 0 e
445
445
446 contents of fncache file:
446 contents of fncache file:
447
447
448 $ cat b/.hg/store/fncache | sort
448 $ cat b/.hg/store/fncache | sort
449 data/a.i
449 data/a.i
450 data/b.i
450 data/b.i
451
451
452 test bogus URL
452 test bogus URL
453
453
454 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
454 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
455 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
455 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
456 [255]
456 [255]
457
457
458 test revset converted() lookup
458 test revset converted() lookup
459
459
460 $ hg --config convert.hg.saverev=True convert a c
460 $ hg --config convert.hg.saverev=True convert a c
461 initializing destination c repository
461 initializing destination c repository
462 scanning source...
462 scanning source...
463 sorting...
463 sorting...
464 converting...
464 converting...
465 4 a
465 4 a
466 3 b
466 3 b
467 2 c
467 2 c
468 1 d
468 1 d
469 0 e
469 0 e
470 $ echo f > c/f
470 $ echo f > c/f
471 $ hg -R c ci -d'0 0' -Amf
471 $ hg -R c ci -d'0 0' -Amf
472 adding f
472 adding f
473 created new head
473 created new head
474 $ hg -R c log -r "converted(09d945a62ce6)"
474 $ hg -R c log -r "converted(09d945a62ce6)"
475 changeset: 1:98c3dd46a874
475 changeset: 1:98c3dd46a874
476 user: test
476 user: test
477 date: Thu Jan 01 00:00:01 1970 +0000
477 date: Thu Jan 01 00:00:01 1970 +0000
478 summary: b
478 summary: b
479
479
480 $ hg -R c log -r "converted()"
480 $ hg -R c log -r "converted()"
481 changeset: 0:31ed57b2037c
481 changeset: 0:31ed57b2037c
482 user: test
482 user: test
483 date: Thu Jan 01 00:00:00 1970 +0000
483 date: Thu Jan 01 00:00:00 1970 +0000
484 summary: a
484 summary: a
485
485
486 changeset: 1:98c3dd46a874
486 changeset: 1:98c3dd46a874
487 user: test
487 user: test
488 date: Thu Jan 01 00:00:01 1970 +0000
488 date: Thu Jan 01 00:00:01 1970 +0000
489 summary: b
489 summary: b
490
490
491 changeset: 2:3b9ca06ef716
491 changeset: 2:3b9ca06ef716
492 user: test
492 user: test
493 date: Thu Jan 01 00:00:02 1970 +0000
493 date: Thu Jan 01 00:00:02 1970 +0000
494 summary: c
494 summary: c
495
495
496 changeset: 3:4e0debd37cf2
496 changeset: 3:4e0debd37cf2
497 user: test
497 user: test
498 date: Thu Jan 01 00:00:03 1970 +0000
498 date: Thu Jan 01 00:00:03 1970 +0000
499 summary: d
499 summary: d
500
500
501 changeset: 4:9de3bc9349c5
501 changeset: 4:9de3bc9349c5
502 user: test
502 user: test
503 date: Thu Jan 01 00:00:04 1970 +0000
503 date: Thu Jan 01 00:00:04 1970 +0000
504 summary: e
504 summary: e
505
505
General Comments 0
You need to be logged in to leave comments. Login now