##// END OF EJS Templates
convert: add config option to use the local time zone...
Julian Cowley -
r17974:337d728e default
parent child Browse files
Show More
@@ -1,370 +1,378 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''import revisions from foreign VCS repositories into Mercurial'''
9 9
10 10 import convcmd
11 11 import cvsps
12 12 import subversion
13 13 from mercurial import commands, templatekw
14 14 from mercurial.i18n import _
15 15
16 16 testedwith = 'internal'
17 17
18 18 # Commands definition was moved elsewhere to ease demandload job.
19 19
20 20 def convert(ui, src, dest=None, revmapfile=None, **opts):
21 21 """convert a foreign SCM repository to a Mercurial one.
22 22
23 23 Accepted source formats [identifiers]:
24 24
25 25 - Mercurial [hg]
26 26 - CVS [cvs]
27 27 - Darcs [darcs]
28 28 - git [git]
29 29 - Subversion [svn]
30 30 - Monotone [mtn]
31 31 - GNU Arch [gnuarch]
32 32 - Bazaar [bzr]
33 33 - Perforce [p4]
34 34
35 35 Accepted destination formats [identifiers]:
36 36
37 37 - Mercurial [hg]
38 38 - Subversion [svn] (history on branches is not preserved)
39 39
40 40 If no revision is given, all revisions will be converted.
41 41 Otherwise, convert will only import up to the named revision
42 42 (given in a format understood by the source).
43 43
44 44 If no destination directory name is specified, it defaults to the
45 45 basename of the source with ``-hg`` appended. If the destination
46 46 repository doesn't exist, it will be created.
47 47
48 48 By default, all sources except Mercurial will use --branchsort.
49 49 Mercurial uses --sourcesort to preserve original revision numbers
50 50 order. Sort modes have the following effects:
51 51
52 52 --branchsort convert from parent to child revision when possible,
53 53 which means branches are usually converted one after
54 54 the other. It generates more compact repositories.
55 55
56 56 --datesort sort revisions by date. Converted repositories have
57 57 good-looking changelogs but are often an order of
58 58 magnitude larger than the same ones generated by
59 59 --branchsort.
60 60
61 61 --sourcesort try to preserve source revisions order, only
62 62 supported by Mercurial sources.
63 63
64 64 If ``REVMAP`` isn't given, it will be put in a default location
65 65 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
66 66 text file that maps each source commit ID to the destination ID
67 67 for that revision, like so::
68 68
69 69 <source ID> <destination ID>
70 70
71 71 If the file doesn't exist, it's automatically created. It's
72 72 updated on each commit copied, so :hg:`convert` can be interrupted
73 73 and can be run repeatedly to copy new commits.
74 74
75 75 The authormap is a simple text file that maps each source commit
76 76 author to a destination commit author. It is handy for source SCMs
77 77 that use unix logins to identify authors (e.g.: CVS). One line per
78 78 author mapping and the line format is::
79 79
80 80 source author = destination author
81 81
82 82 Empty lines and lines starting with a ``#`` are ignored.
83 83
84 84 The filemap is a file that allows filtering and remapping of files
85 85 and directories. Each line can contain one of the following
86 86 directives::
87 87
88 88 include path/to/file-or-dir
89 89
90 90 exclude path/to/file-or-dir
91 91
92 92 rename path/to/source path/to/destination
93 93
94 94 Comment lines start with ``#``. A specified path matches if it
95 95 equals the full relative name of a file or one of its parent
96 96 directories. The ``include`` or ``exclude`` directive with the
97 97 longest matching path applies, so line order does not matter.
98 98
99 99 The ``include`` directive causes a file, or all files under a
100 100 directory, to be included in the destination repository, and the
101 101 exclusion of all other files and directories not explicitly
102 102 included. The ``exclude`` directive causes files or directories to
103 103 be omitted. The ``rename`` directive renames a file or directory if
104 104 it is converted. To rename from a subdirectory into the root of
105 105 the repository, use ``.`` as the path to rename to.
106 106
107 107 The splicemap is a file that allows insertion of synthetic
108 108 history, letting you specify the parents of a revision. This is
109 109 useful if you want to e.g. give a Subversion merge two parents, or
110 110 graft two disconnected series of history together. Each entry
111 111 contains a key, followed by a space, followed by one or two
112 112 comma-separated values::
113 113
114 114 key parent1, parent2
115 115
116 116 The key is the revision ID in the source
117 117 revision control system whose parents should be modified (same
118 118 format as a key in .hg/shamap). The values are the revision IDs
119 119 (in either the source or destination revision control system) that
120 120 should be used as the new parents for that node. For example, if
121 121 you have merged "release-1.0" into "trunk", then you should
122 122 specify the revision on "trunk" as the first parent and the one on
123 123 the "release-1.0" branch as the second.
124 124
125 125 The branchmap is a file that allows you to rename a branch when it is
126 126 being brought in from whatever external repository. When used in
127 127 conjunction with a splicemap, it allows for a powerful combination
128 128 to help fix even the most badly mismanaged repositories and turn them
129 129 into nicely structured Mercurial repositories. The branchmap contains
130 130 lines of the form::
131 131
132 132 original_branch_name new_branch_name
133 133
134 134 where "original_branch_name" is the name of the branch in the
135 135 source repository, and "new_branch_name" is the name of the branch
136 136 is the destination repository. No whitespace is allowed in the
137 137 branch names. This can be used to (for instance) move code in one
138 138 repository from "default" to a named branch.
139 139
140 140 Mercurial Source
141 141 ################
142 142
143 143 The Mercurial source recognizes the following configuration
144 144 options, which you can set on the command line with ``--config``:
145 145
146 146 :convert.hg.ignoreerrors: ignore integrity errors when reading.
147 147 Use it to fix Mercurial repositories with missing revlogs, by
148 148 converting from and to Mercurial. Default is False.
149 149
150 150 :convert.hg.saverev: store original revision ID in changeset
151 151 (forces target IDs to change). It takes a boolean argument and
152 152 defaults to False.
153 153
154 154 :convert.hg.startrev: convert start revision and its descendants.
155 155 It takes a hg revision identifier and defaults to 0.
156 156
157 157 CVS Source
158 158 ##########
159 159
160 160 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
161 161 to indicate the starting point of what will be converted. Direct
162 162 access to the repository files is not needed, unless of course the
163 163 repository is ``:local:``. The conversion uses the top level
164 164 directory in the sandbox to find the CVS repository, and then uses
165 165 CVS rlog commands to find files to convert. This means that unless
166 166 a filemap is given, all files under the starting directory will be
167 167 converted, and that any directory reorganization in the CVS
168 168 sandbox is ignored.
169 169
170 170 The following options can be used with ``--config``:
171 171
172 172 :convert.cvsps.cache: Set to False to disable remote log caching,
173 173 for testing and debugging purposes. Default is True.
174 174
175 175 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
176 176 allowed between commits with identical user and log message in
177 177 a single changeset. When very large files were checked in as
178 178 part of a changeset then the default may not be long enough.
179 179 The default is 60.
180 180
181 181 :convert.cvsps.mergeto: Specify a regular expression to which
182 182 commit log messages are matched. If a match occurs, then the
183 183 conversion process will insert a dummy revision merging the
184 184 branch on which this log message occurs to the branch
185 185 indicated in the regex. Default is ``{{mergetobranch
186 186 ([-\\w]+)}}``
187 187
188 188 :convert.cvsps.mergefrom: Specify a regular expression to which
189 189 commit log messages are matched. If a match occurs, then the
190 190 conversion process will add the most recent revision on the
191 191 branch indicated in the regex as the second parent of the
192 192 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
193 193
194 :convert.localtimezone: use local time (as determined by the TZ
195 environment variable) for changeset date/times. The default
196 is False (use UTC).
197
194 198 :hook.cvslog: Specify a Python function to be called at the end of
195 199 gathering the CVS log. The function is passed a list with the
196 200 log entries, and can modify the entries in-place, or add or
197 201 delete them.
198 202
199 203 :hook.cvschangesets: Specify a Python function to be called after
200 204 the changesets are calculated from the CVS log. The
201 205 function is passed a list with the changeset entries, and can
202 206 modify the changesets in-place, or add or delete them.
203 207
204 208 An additional "debugcvsps" Mercurial command allows the builtin
205 209 changeset merging code to be run without doing a conversion. Its
206 210 parameters and output are similar to that of cvsps 2.1. Please see
207 211 the command help for more details.
208 212
209 213 Subversion Source
210 214 #################
211 215
212 216 Subversion source detects classical trunk/branches/tags layouts.
213 217 By default, the supplied ``svn://repo/path/`` source URL is
214 218 converted as a single branch. If ``svn://repo/path/trunk`` exists
215 219 it replaces the default branch. If ``svn://repo/path/branches``
216 220 exists, its subdirectories are listed as possible branches. If
217 221 ``svn://repo/path/tags`` exists, it is looked for tags referencing
218 222 converted branches. Default ``trunk``, ``branches`` and ``tags``
219 223 values can be overridden with following options. Set them to paths
220 224 relative to the source URL, or leave them blank to disable auto
221 225 detection.
222 226
223 227 The following options can be set with ``--config``:
224 228
225 229 :convert.svn.branches: specify the directory containing branches.
226 230 The default is ``branches``.
227 231
228 232 :convert.svn.tags: specify the directory containing tags. The
229 233 default is ``tags``.
230 234
231 235 :convert.svn.trunk: specify the name of the trunk branch. The
232 236 default is ``trunk``.
233 237
238 :convert.localtimezone: use local time (as determined by the TZ
239 environment variable) for changeset date/times. The default
240 is False (use UTC).
241
234 242 Source history can be retrieved starting at a specific revision,
235 243 instead of being integrally converted. Only single branch
236 244 conversions are supported.
237 245
238 246 :convert.svn.startrev: specify start Subversion revision number.
239 247 The default is 0.
240 248
241 249 Perforce Source
242 250 ###############
243 251
244 252 The Perforce (P4) importer can be given a p4 depot path or a
245 253 client specification as source. It will convert all files in the
246 254 source to a flat Mercurial repository, ignoring labels, branches
247 255 and integrations. Note that when a depot path is given you then
248 256 usually should specify a target directory, because otherwise the
249 257 target may be named ``...-hg``.
250 258
251 259 It is possible to limit the amount of source history to be
252 260 converted by specifying an initial Perforce revision:
253 261
254 262 :convert.p4.startrev: specify initial Perforce revision (a
255 263 Perforce changelist number).
256 264
257 265 Mercurial Destination
258 266 #####################
259 267
260 268 The following options are supported:
261 269
262 270 :convert.hg.clonebranches: dispatch source branches in separate
263 271 clones. The default is False.
264 272
265 273 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
266 274 ``default``.
267 275
268 276 :convert.hg.usebranchnames: preserve branch names. The default is
269 277 True.
270 278 """
271 279 return convcmd.convert(ui, src, dest, revmapfile, **opts)
272 280
273 281 def debugsvnlog(ui, **opts):
274 282 return subversion.debugsvnlog(ui, **opts)
275 283
276 284 def debugcvsps(ui, *args, **opts):
277 285 '''create changeset information from CVS
278 286
279 287 This command is intended as a debugging tool for the CVS to
280 288 Mercurial converter, and can be used as a direct replacement for
281 289 cvsps.
282 290
283 291 Hg debugcvsps reads the CVS rlog for current directory (or any
284 292 named directory) in the CVS repository, and converts the log to a
285 293 series of changesets based on matching commit log entries and
286 294 dates.'''
287 295 return cvsps.debugcvsps(ui, *args, **opts)
288 296
289 297 commands.norepo += " convert debugsvnlog debugcvsps"
290 298
291 299 cmdtable = {
292 300 "convert":
293 301 (convert,
294 302 [('', 'authors', '',
295 303 _('username mapping filename (DEPRECATED, use --authormap instead)'),
296 304 _('FILE')),
297 305 ('s', 'source-type', '',
298 306 _('source repository type'), _('TYPE')),
299 307 ('d', 'dest-type', '',
300 308 _('destination repository type'), _('TYPE')),
301 309 ('r', 'rev', '',
302 310 _('import up to target revision REV'), _('REV')),
303 311 ('A', 'authormap', '',
304 312 _('remap usernames using this file'), _('FILE')),
305 313 ('', 'filemap', '',
306 314 _('remap file names using contents of file'), _('FILE')),
307 315 ('', 'splicemap', '',
308 316 _('splice synthesized history into place'), _('FILE')),
309 317 ('', 'branchmap', '',
310 318 _('change branch names while converting'), _('FILE')),
311 319 ('', 'branchsort', None, _('try to sort changesets by branches')),
312 320 ('', 'datesort', None, _('try to sort changesets by date')),
313 321 ('', 'sourcesort', None, _('preserve source changesets order'))],
314 322 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
315 323 "debugsvnlog":
316 324 (debugsvnlog,
317 325 [],
318 326 'hg debugsvnlog'),
319 327 "debugcvsps":
320 328 (debugcvsps,
321 329 [
322 330 # Main options shared with cvsps-2.1
323 331 ('b', 'branches', [], _('only return changes on specified branches')),
324 332 ('p', 'prefix', '', _('prefix to remove from file names')),
325 333 ('r', 'revisions', [],
326 334 _('only return changes after or between specified tags')),
327 335 ('u', 'update-cache', None, _("update cvs log cache")),
328 336 ('x', 'new-cache', None, _("create new cvs log cache")),
329 337 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
330 338 ('', 'root', '', _('specify cvsroot')),
331 339 # Options specific to builtin cvsps
332 340 ('', 'parents', '', _('show parent changesets')),
333 341 ('', 'ancestors', '',
334 342 _('show current changeset in ancestor branches')),
335 343 # Options that are ignored for compatibility with cvsps-2.1
336 344 ('A', 'cvs-direct', None, _('ignored for compatibility')),
337 345 ],
338 346 _('hg debugcvsps [OPTION]... [PATH]...')),
339 347 }
340 348
341 349 def kwconverted(ctx, name):
342 350 rev = ctx.extra().get('convert_revision', '')
343 351 if rev.startswith('svn:'):
344 352 if name == 'svnrev':
345 353 return str(subversion.revsplit(rev)[2])
346 354 elif name == 'svnpath':
347 355 return subversion.revsplit(rev)[1]
348 356 elif name == 'svnuuid':
349 357 return subversion.revsplit(rev)[0]
350 358 return rev
351 359
352 360 def kwsvnrev(repo, ctx, **args):
353 361 """:svnrev: String. Converted subversion revision number."""
354 362 return kwconverted(ctx, 'svnrev')
355 363
356 364 def kwsvnpath(repo, ctx, **args):
357 365 """:svnpath: String. Converted subversion revision project path."""
358 366 return kwconverted(ctx, 'svnpath')
359 367
360 368 def kwsvnuuid(repo, ctx, **args):
361 369 """:svnuuid: String. Converted subversion revision repository identifier."""
362 370 return kwconverted(ctx, 'svnuuid')
363 371
364 372 def extsetup(ui):
365 373 templatekw.keywords['svnrev'] = kwsvnrev
366 374 templatekw.keywords['svnpath'] = kwsvnpath
367 375 templatekw.keywords['svnuuid'] = kwsvnuuid
368 376
369 377 # tell hggettext to extract docstrings from these functions:
370 378 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,448 +1,455 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 import base64, errno, subprocess, os
8 import base64, errno, subprocess, os, datetime
9 9 import cPickle as pickle
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 propertycache = util.propertycache
14 14
15 15 def encodeargs(args):
16 16 def encodearg(s):
17 17 lines = base64.encodestring(s)
18 18 lines = [l.splitlines()[0] for l in lines]
19 19 return ''.join(lines)
20 20
21 21 s = pickle.dumps(args)
22 22 return encodearg(s)
23 23
24 24 def decodeargs(s):
25 25 s = base64.decodestring(s)
26 26 return pickle.loads(s)
27 27
28 28 class MissingTool(Exception):
29 29 pass
30 30
31 31 def checktool(exe, name=None, abort=True):
32 32 name = name or exe
33 33 if not util.findexe(exe):
34 34 exc = abort and util.Abort or MissingTool
35 35 raise exc(_('cannot find required "%s" tool') % name)
36 36
37 37 class NoRepo(Exception):
38 38 pass
39 39
40 40 SKIPREV = 'SKIP'
41 41
42 42 class commit(object):
43 43 def __init__(self, author, date, desc, parents, branch=None, rev=None,
44 44 extra={}, sortkey=None):
45 45 self.author = author or 'unknown'
46 46 self.date = date or '0 0'
47 47 self.desc = desc
48 48 self.parents = parents
49 49 self.branch = branch
50 50 self.rev = rev
51 51 self.extra = extra
52 52 self.sortkey = sortkey
53 53
54 54 class converter_source(object):
55 55 """Conversion source interface"""
56 56
57 57 def __init__(self, ui, path=None, rev=None):
58 58 """Initialize conversion source (or raise NoRepo("message")
59 59 exception if path is not a valid repository)"""
60 60 self.ui = ui
61 61 self.path = path
62 62 self.rev = rev
63 63
64 64 self.encoding = 'utf-8'
65 65
66 66 def before(self):
67 67 pass
68 68
69 69 def after(self):
70 70 pass
71 71
72 72 def setrevmap(self, revmap):
73 73 """set the map of already-converted revisions"""
74 74 pass
75 75
76 76 def getheads(self):
77 77 """Return a list of this repository's heads"""
78 78 raise NotImplementedError
79 79
80 80 def getfile(self, name, rev):
81 81 """Return a pair (data, mode) where data is the file content
82 82 as a string and mode one of '', 'x' or 'l'. rev is the
83 83 identifier returned by a previous call to getchanges(). Raise
84 84 IOError to indicate that name was deleted in rev.
85 85 """
86 86 raise NotImplementedError
87 87
88 88 def getchanges(self, version):
89 89 """Returns a tuple of (files, copies).
90 90
91 91 files is a sorted list of (filename, id) tuples for all files
92 92 changed between version and its first parent returned by
93 93 getcommit(). id is the source revision id of the file.
94 94
95 95 copies is a dictionary of dest: source
96 96 """
97 97 raise NotImplementedError
98 98
99 99 def getcommit(self, version):
100 100 """Return the commit object for version"""
101 101 raise NotImplementedError
102 102
103 103 def gettags(self):
104 104 """Return the tags as a dictionary of name: revision
105 105
106 106 Tag names must be UTF-8 strings.
107 107 """
108 108 raise NotImplementedError
109 109
110 110 def recode(self, s, encoding=None):
111 111 if not encoding:
112 112 encoding = self.encoding or 'utf-8'
113 113
114 114 if isinstance(s, unicode):
115 115 return s.encode("utf-8")
116 116 try:
117 117 return s.decode(encoding).encode("utf-8")
118 118 except UnicodeError:
119 119 try:
120 120 return s.decode("latin-1").encode("utf-8")
121 121 except UnicodeError:
122 122 return s.decode(encoding, "replace").encode("utf-8")
123 123
124 124 def getchangedfiles(self, rev, i):
125 125 """Return the files changed by rev compared to parent[i].
126 126
127 127 i is an index selecting one of the parents of rev. The return
128 128 value should be the list of files that are different in rev and
129 129 this parent.
130 130
131 131 If rev has no parents, i is None.
132 132
133 133 This function is only needed to support --filemap
134 134 """
135 135 raise NotImplementedError
136 136
137 137 def converted(self, rev, sinkrev):
138 138 '''Notify the source that a revision has been converted.'''
139 139 pass
140 140
141 141 def hasnativeorder(self):
142 142 """Return true if this source has a meaningful, native revision
143 143 order. For instance, Mercurial revisions are store sequentially
144 144 while there is no such global ordering with Darcs.
145 145 """
146 146 return False
147 147
148 148 def lookuprev(self, rev):
149 149 """If rev is a meaningful revision reference in source, return
150 150 the referenced identifier in the same format used by getcommit().
151 151 return None otherwise.
152 152 """
153 153 return None
154 154
155 155 def getbookmarks(self):
156 156 """Return the bookmarks as a dictionary of name: revision
157 157
158 158 Bookmark names are to be UTF-8 strings.
159 159 """
160 160 return {}
161 161
162 162 class converter_sink(object):
163 163 """Conversion sink (target) interface"""
164 164
165 165 def __init__(self, ui, path):
166 166 """Initialize conversion sink (or raise NoRepo("message")
167 167 exception if path is not a valid repository)
168 168
169 169 created is a list of paths to remove if a fatal error occurs
170 170 later"""
171 171 self.ui = ui
172 172 self.path = path
173 173 self.created = []
174 174
175 175 def getheads(self):
176 176 """Return a list of this repository's heads"""
177 177 raise NotImplementedError
178 178
179 179 def revmapfile(self):
180 180 """Path to a file that will contain lines
181 181 source_rev_id sink_rev_id
182 182 mapping equivalent revision identifiers for each system."""
183 183 raise NotImplementedError
184 184
185 185 def authorfile(self):
186 186 """Path to a file that will contain lines
187 187 srcauthor=dstauthor
188 188 mapping equivalent authors identifiers for each system."""
189 189 return None
190 190
191 191 def putcommit(self, files, copies, parents, commit, source, revmap):
192 192 """Create a revision with all changed files listed in 'files'
193 193 and having listed parents. 'commit' is a commit object
194 194 containing at a minimum the author, date, and message for this
195 195 changeset. 'files' is a list of (path, version) tuples,
196 196 'copies' is a dictionary mapping destinations to sources,
197 197 'source' is the source repository, and 'revmap' is a mapfile
198 198 of source revisions to converted revisions. Only getfile() and
199 199 lookuprev() should be called on 'source'.
200 200
201 201 Note that the sink repository is not told to update itself to
202 202 a particular revision (or even what that revision would be)
203 203 before it receives the file data.
204 204 """
205 205 raise NotImplementedError
206 206
207 207 def puttags(self, tags):
208 208 """Put tags into sink.
209 209
210 210 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
211 211 Return a pair (tag_revision, tag_parent_revision), or (None, None)
212 212 if nothing was changed.
213 213 """
214 214 raise NotImplementedError
215 215
216 216 def setbranch(self, branch, pbranches):
217 217 """Set the current branch name. Called before the first putcommit
218 218 on the branch.
219 219 branch: branch name for subsequent commits
220 220 pbranches: (converted parent revision, parent branch) tuples"""
221 221 pass
222 222
223 223 def setfilemapmode(self, active):
224 224 """Tell the destination that we're using a filemap
225 225
226 226 Some converter_sources (svn in particular) can claim that a file
227 227 was changed in a revision, even if there was no change. This method
228 228 tells the destination that we're using a filemap and that it should
229 229 filter empty revisions.
230 230 """
231 231 pass
232 232
233 233 def before(self):
234 234 pass
235 235
236 236 def after(self):
237 237 pass
238 238
239 239 def putbookmarks(self, bookmarks):
240 240 """Put bookmarks into sink.
241 241
242 242 bookmarks: {bookmarkname: sink_rev_id, ...}
243 243 where bookmarkname is an UTF-8 string.
244 244 """
245 245 pass
246 246
247 247 def hascommit(self, rev):
248 248 """Return True if the sink contains rev"""
249 249 raise NotImplementedError
250 250
251 251 class commandline(object):
252 252 def __init__(self, ui, command):
253 253 self.ui = ui
254 254 self.command = command
255 255
256 256 def prerun(self):
257 257 pass
258 258
259 259 def postrun(self):
260 260 pass
261 261
262 262 def _cmdline(self, cmd, *args, **kwargs):
263 263 cmdline = [self.command, cmd] + list(args)
264 264 for k, v in kwargs.iteritems():
265 265 if len(k) == 1:
266 266 cmdline.append('-' + k)
267 267 else:
268 268 cmdline.append('--' + k.replace('_', '-'))
269 269 try:
270 270 if len(k) == 1:
271 271 cmdline.append('' + v)
272 272 else:
273 273 cmdline[-1] += '=' + v
274 274 except TypeError:
275 275 pass
276 276 cmdline = [util.shellquote(arg) for arg in cmdline]
277 277 if not self.ui.debugflag:
278 278 cmdline += ['2>', os.devnull]
279 279 cmdline = ' '.join(cmdline)
280 280 return cmdline
281 281
282 282 def _run(self, cmd, *args, **kwargs):
283 283 def popen(cmdline):
284 284 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
285 285 close_fds=util.closefds,
286 286 stdout=subprocess.PIPE)
287 287 return p
288 288 return self._dorun(popen, cmd, *args, **kwargs)
289 289
290 290 def _run2(self, cmd, *args, **kwargs):
291 291 return self._dorun(util.popen2, cmd, *args, **kwargs)
292 292
293 293 def _dorun(self, openfunc, cmd, *args, **kwargs):
294 294 cmdline = self._cmdline(cmd, *args, **kwargs)
295 295 self.ui.debug('running: %s\n' % (cmdline,))
296 296 self.prerun()
297 297 try:
298 298 return openfunc(cmdline)
299 299 finally:
300 300 self.postrun()
301 301
302 302 def run(self, cmd, *args, **kwargs):
303 303 p = self._run(cmd, *args, **kwargs)
304 304 output = p.communicate()[0]
305 305 self.ui.debug(output)
306 306 return output, p.returncode
307 307
308 308 def runlines(self, cmd, *args, **kwargs):
309 309 p = self._run(cmd, *args, **kwargs)
310 310 output = p.stdout.readlines()
311 311 p.wait()
312 312 self.ui.debug(''.join(output))
313 313 return output, p.returncode
314 314
315 315 def checkexit(self, status, output=''):
316 316 if status:
317 317 if output:
318 318 self.ui.warn(_('%s error:\n') % self.command)
319 319 self.ui.warn(output)
320 320 msg = util.explainexit(status)[0]
321 321 raise util.Abort('%s %s' % (self.command, msg))
322 322
323 323 def run0(self, cmd, *args, **kwargs):
324 324 output, status = self.run(cmd, *args, **kwargs)
325 325 self.checkexit(status, output)
326 326 return output
327 327
328 328 def runlines0(self, cmd, *args, **kwargs):
329 329 output, status = self.runlines(cmd, *args, **kwargs)
330 330 self.checkexit(status, ''.join(output))
331 331 return output
332 332
333 333 @propertycache
334 334 def argmax(self):
335 335 # POSIX requires at least 4096 bytes for ARG_MAX
336 336 argmax = 4096
337 337 try:
338 338 argmax = os.sysconf("SC_ARG_MAX")
339 339 except (AttributeError, ValueError):
340 340 pass
341 341
342 342 # Windows shells impose their own limits on command line length,
343 343 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
344 344 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
345 345 # details about cmd.exe limitations.
346 346
347 347 # Since ARG_MAX is for command line _and_ environment, lower our limit
348 348 # (and make happy Windows shells while doing this).
349 349 return argmax // 2 - 1
350 350
351 351 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
352 352 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
353 353 limit = self.argmax - cmdlen
354 354 bytes = 0
355 355 fl = []
356 356 for fn in arglist:
357 357 b = len(fn) + 3
358 358 if bytes + b < limit or len(fl) == 0:
359 359 fl.append(fn)
360 360 bytes += b
361 361 else:
362 362 yield fl
363 363 fl = [fn]
364 364 bytes = b
365 365 if fl:
366 366 yield fl
367 367
368 368 def xargs(self, arglist, cmd, *args, **kwargs):
369 369 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
370 370 self.run0(cmd, *(list(args) + l), **kwargs)
371 371
372 372 class mapfile(dict):
373 373 def __init__(self, ui, path):
374 374 super(mapfile, self).__init__()
375 375 self.ui = ui
376 376 self.path = path
377 377 self.fp = None
378 378 self.order = []
379 379 self._read()
380 380
381 381 def _read(self):
382 382 if not self.path:
383 383 return
384 384 try:
385 385 fp = open(self.path, 'r')
386 386 except IOError, err:
387 387 if err.errno != errno.ENOENT:
388 388 raise
389 389 return
390 390 for i, line in enumerate(fp):
391 391 line = line.splitlines()[0].rstrip()
392 392 if not line:
393 393 # Ignore blank lines
394 394 continue
395 395 try:
396 396 key, value = line.rsplit(' ', 1)
397 397 except ValueError:
398 398 raise util.Abort(
399 399 _('syntax error in %s(%d): key/value pair expected')
400 400 % (self.path, i + 1))
401 401 if key not in self:
402 402 self.order.append(key)
403 403 super(mapfile, self).__setitem__(key, value)
404 404 fp.close()
405 405
406 406 def __setitem__(self, key, value):
407 407 if self.fp is None:
408 408 try:
409 409 self.fp = open(self.path, 'a')
410 410 except IOError, err:
411 411 raise util.Abort(_('could not open map file %r: %s') %
412 412 (self.path, err.strerror))
413 413 self.fp.write('%s %s\n' % (key, value))
414 414 self.fp.flush()
415 415 super(mapfile, self).__setitem__(key, value)
416 416
417 417 def close(self):
418 418 if self.fp:
419 419 self.fp.close()
420 420 self.fp = None
421 421
422 422 def parsesplicemap(path):
423 423 """Parse a splicemap, return a child/parents dictionary."""
424 424 if not path:
425 425 return {}
426 426 m = {}
427 427 try:
428 428 fp = open(path, 'r')
429 429 for i, line in enumerate(fp):
430 430 line = line.splitlines()[0].rstrip()
431 431 if not line:
432 432 # Ignore blank lines
433 433 continue
434 434 try:
435 435 child, parents = line.split(' ', 1)
436 436 parents = parents.replace(',', ' ').split()
437 437 except ValueError:
438 438 raise util.Abort(_('syntax error in %s(%d): child parent1'
439 439 '[,parent2] expected') % (path, i + 1))
440 440 pp = []
441 441 for p in parents:
442 442 if p not in pp:
443 443 pp.append(p)
444 444 m[child] = pp
445 445 except IOError, e:
446 446 if e.errno != errno.ENOENT:
447 447 raise
448 448 return m
449
450 def makedatetimestamp(t):
451 """Like util.makedate() but for time t instead of current time"""
452 delta = (datetime.datetime.utcfromtimestamp(t) -
453 datetime.datetime.fromtimestamp(t))
454 tz = delta.days * 86400 + delta.seconds
455 return t, tz
@@ -1,272 +1,275 b''
1 1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os, re, socket, errno
9 9 from cStringIO import StringIO
10 10 from mercurial import encoding, util
11 11 from mercurial.i18n import _
12 12
13 13 from common import NoRepo, commit, converter_source, checktool
14 from common import makedatetimestamp
14 15 import cvsps
15 16
16 17 class convert_cvs(converter_source):
17 18 def __init__(self, ui, path, rev=None):
18 19 super(convert_cvs, self).__init__(ui, path, rev=rev)
19 20
20 21 cvs = os.path.join(path, "CVS")
21 22 if not os.path.exists(cvs):
22 23 raise NoRepo(_("%s does not look like a CVS checkout") % path)
23 24
24 25 checktool('cvs')
25 26
26 27 self.changeset = None
27 28 self.files = {}
28 29 self.tags = {}
29 30 self.lastbranch = {}
30 31 self.socket = None
31 32 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 33 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 34 self.encoding = encoding.encoding
34 35
35 36 self._connect()
36 37
37 38 def _parse(self):
38 39 if self.changeset is not None:
39 40 return
40 41 self.changeset = {}
41 42
42 43 maxrev = 0
43 44 if self.rev:
44 45 # TODO: handle tags
45 46 try:
46 47 # patchset number?
47 48 maxrev = int(self.rev)
48 49 except ValueError:
49 50 raise util.Abort(_('revision %s is not a patchset number')
50 51 % self.rev)
51 52
52 53 d = os.getcwd()
53 54 try:
54 55 os.chdir(self.path)
55 56 id = None
56 57
57 58 cache = 'update'
58 59 if not self.ui.configbool('convert', 'cvsps.cache', True):
59 60 cache = None
60 61 db = cvsps.createlog(self.ui, cache=cache)
61 62 db = cvsps.createchangeset(self.ui, db,
62 63 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
63 64 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
64 65 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
65 66
66 67 for cs in db:
67 68 if maxrev and cs.id > maxrev:
68 69 break
69 70 id = str(cs.id)
70 71 cs.author = self.recode(cs.author)
71 72 self.lastbranch[cs.branch] = id
72 73 cs.comment = self.recode(cs.comment)
74 if self.ui.configbool('convert', 'localtimezone'):
75 cs.date = makedatetimestamp(cs.date[0])
73 76 date = util.datestr(cs.date, '%Y-%m-%d %H:%M:%S %1%2')
74 77 self.tags.update(dict.fromkeys(cs.tags, id))
75 78
76 79 files = {}
77 80 for f in cs.entries:
78 81 files[f.file] = "%s%s" % ('.'.join([str(x)
79 82 for x in f.revision]),
80 83 ['', '(DEAD)'][f.dead])
81 84
82 85 # add current commit to set
83 86 c = commit(author=cs.author, date=date,
84 87 parents=[str(p.id) for p in cs.parents],
85 88 desc=cs.comment, branch=cs.branch or '')
86 89 self.changeset[id] = c
87 90 self.files[id] = files
88 91
89 92 self.heads = self.lastbranch.values()
90 93 finally:
91 94 os.chdir(d)
92 95
93 96 def _connect(self):
94 97 root = self.cvsroot
95 98 conntype = None
96 99 user, host = None, None
97 100 cmd = ['cvs', 'server']
98 101
99 102 self.ui.status(_("connecting to %s\n") % root)
100 103
101 104 if root.startswith(":pserver:"):
102 105 root = root[9:]
103 106 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
104 107 root)
105 108 if m:
106 109 conntype = "pserver"
107 110 user, passw, serv, port, root = m.groups()
108 111 if not user:
109 112 user = "anonymous"
110 113 if not port:
111 114 port = 2401
112 115 else:
113 116 port = int(port)
114 117 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
115 118 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
116 119
117 120 if not passw:
118 121 passw = "A"
119 122 cvspass = os.path.expanduser("~/.cvspass")
120 123 try:
121 124 pf = open(cvspass)
122 125 for line in pf.read().splitlines():
123 126 part1, part2 = line.split(' ', 1)
124 127 # /1 :pserver:user@example.com:2401/cvsroot/foo
125 128 # Ah<Z
126 129 if part1 == '/1':
127 130 part1, part2 = part2.split(' ', 1)
128 131 format = format1
129 132 # :pserver:user@example.com:/cvsroot/foo Ah<Z
130 133 else:
131 134 format = format0
132 135 if part1 == format:
133 136 passw = part2
134 137 break
135 138 pf.close()
136 139 except IOError, inst:
137 140 if inst.errno != errno.ENOENT:
138 141 if not getattr(inst, 'filename', None):
139 142 inst.filename = cvspass
140 143 raise
141 144
142 145 sck = socket.socket()
143 146 sck.connect((serv, port))
144 147 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
145 148 "END AUTH REQUEST", ""]))
146 149 if sck.recv(128) != "I LOVE YOU\n":
147 150 raise util.Abort(_("CVS pserver authentication failed"))
148 151
149 152 self.writep = self.readp = sck.makefile('r+')
150 153
151 154 if not conntype and root.startswith(":local:"):
152 155 conntype = "local"
153 156 root = root[7:]
154 157
155 158 if not conntype:
156 159 # :ext:user@host/home/user/path/to/cvsroot
157 160 if root.startswith(":ext:"):
158 161 root = root[5:]
159 162 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
160 163 # Do not take Windows path "c:\foo\bar" for a connection strings
161 164 if os.path.isdir(root) or not m:
162 165 conntype = "local"
163 166 else:
164 167 conntype = "rsh"
165 168 user, host, root = m.group(1), m.group(2), m.group(3)
166 169
167 170 if conntype != "pserver":
168 171 if conntype == "rsh":
169 172 rsh = os.environ.get("CVS_RSH") or "ssh"
170 173 if user:
171 174 cmd = [rsh, '-l', user, host] + cmd
172 175 else:
173 176 cmd = [rsh, host] + cmd
174 177
175 178 # popen2 does not support argument lists under Windows
176 179 cmd = [util.shellquote(arg) for arg in cmd]
177 180 cmd = util.quotecommand(' '.join(cmd))
178 181 self.writep, self.readp = util.popen2(cmd)
179 182
180 183 self.realroot = root
181 184
182 185 self.writep.write("Root %s\n" % root)
183 186 self.writep.write("Valid-responses ok error Valid-requests Mode"
184 187 " M Mbinary E Checked-in Created Updated"
185 188 " Merged Removed\n")
186 189 self.writep.write("valid-requests\n")
187 190 self.writep.flush()
188 191 r = self.readp.readline()
189 192 if not r.startswith("Valid-requests"):
190 193 raise util.Abort(_('unexpected response from CVS server '
191 194 '(expected "Valid-requests", but got %r)')
192 195 % r)
193 196 if "UseUnchanged" in r:
194 197 self.writep.write("UseUnchanged\n")
195 198 self.writep.flush()
196 199 r = self.readp.readline()
197 200
198 201 def getheads(self):
199 202 self._parse()
200 203 return self.heads
201 204
202 205 def getfile(self, name, rev):
203 206
204 207 def chunkedread(fp, count):
205 208 # file-objects returned by socket.makefile() do not handle
206 209 # large read() requests very well.
207 210 chunksize = 65536
208 211 output = StringIO()
209 212 while count > 0:
210 213 data = fp.read(min(count, chunksize))
211 214 if not data:
212 215 raise util.Abort(_("%d bytes missing from remote file")
213 216 % count)
214 217 count -= len(data)
215 218 output.write(data)
216 219 return output.getvalue()
217 220
218 221 self._parse()
219 222 if rev.endswith("(DEAD)"):
220 223 raise IOError
221 224
222 225 args = ("-N -P -kk -r %s --" % rev).split()
223 226 args.append(self.cvsrepo + '/' + name)
224 227 for x in args:
225 228 self.writep.write("Argument %s\n" % x)
226 229 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
227 230 self.writep.flush()
228 231
229 232 data = ""
230 233 mode = None
231 234 while True:
232 235 line = self.readp.readline()
233 236 if line.startswith("Created ") or line.startswith("Updated "):
234 237 self.readp.readline() # path
235 238 self.readp.readline() # entries
236 239 mode = self.readp.readline()[:-1]
237 240 count = int(self.readp.readline()[:-1])
238 241 data = chunkedread(self.readp, count)
239 242 elif line.startswith(" "):
240 243 data += line[1:]
241 244 elif line.startswith("M "):
242 245 pass
243 246 elif line.startswith("Mbinary "):
244 247 count = int(self.readp.readline()[:-1])
245 248 data = chunkedread(self.readp, count)
246 249 else:
247 250 if line == "ok\n":
248 251 if mode is None:
249 252 raise util.Abort(_('malformed response from CVS'))
250 253 return (data, "x" in mode and "x" or "")
251 254 elif line.startswith("E "):
252 255 self.ui.warn(_("cvs server: %s\n") % line[2:])
253 256 elif line.startswith("Remove"):
254 257 self.readp.readline()
255 258 else:
256 259 raise util.Abort(_("unknown CVS response: %s") % line)
257 260
258 261 def getchanges(self, rev):
259 262 self._parse()
260 263 return sorted(self.files[rev].iteritems()), {}
261 264
262 265 def getcommit(self, rev):
263 266 self._parse()
264 267 return self.changeset[rev]
265 268
266 269 def gettags(self):
267 270 self._parse()
268 271 return self.tags
269 272
270 273 def getchangedfiles(self, rev, i):
271 274 self._parse()
272 275 return sorted(self.files[rev])
@@ -1,1251 +1,1254 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4
5 5 import os, re, sys, tempfile, urllib, urllib2, xml.dom.minidom
6 6 import cPickle as pickle
7 7
8 8 from mercurial import strutil, scmutil, util, encoding
9 9 from mercurial.i18n import _
10 10
11 11 propertycache = util.propertycache
12 12
13 13 # Subversion stuff. Works best with very recent Python SVN bindings
14 14 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
15 15 # these bindings.
16 16
17 17 from cStringIO import StringIO
18 18
19 19 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
20 20 from common import commandline, converter_source, converter_sink, mapfile
21 from common import makedatetimestamp
21 22
22 23 try:
23 24 from svn.core import SubversionException, Pool
24 25 import svn
25 26 import svn.client
26 27 import svn.core
27 28 import svn.ra
28 29 import svn.delta
29 30 import transport
30 31 import warnings
31 32 warnings.filterwarnings('ignore',
32 33 module='svn.core',
33 34 category=DeprecationWarning)
34 35
35 36 except ImportError:
36 37 svn = None
37 38
38 39 class SvnPathNotFound(Exception):
39 40 pass
40 41
41 42 def revsplit(rev):
42 43 """Parse a revision string and return (uuid, path, revnum)."""
43 44 url, revnum = rev.rsplit('@', 1)
44 45 parts = url.split('/', 1)
45 46 mod = ''
46 47 if len(parts) > 1:
47 48 mod = '/' + parts[1]
48 49 return parts[0][4:], mod, int(revnum)
49 50
50 51 def quote(s):
51 52 # As of svn 1.7, many svn calls expect "canonical" paths. In
52 53 # theory, we should call svn.core.*canonicalize() on all paths
53 54 # before passing them to the API. Instead, we assume the base url
54 55 # is canonical and copy the behaviour of svn URL encoding function
55 56 # so we can extend it safely with new components. The "safe"
56 57 # characters were taken from the "svn_uri__char_validity" table in
57 58 # libsvn_subr/path.c.
58 59 return urllib.quote(s, "!$&'()*+,-./:=@_~")
59 60
60 61 def geturl(path):
61 62 try:
62 63 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
63 64 except SubversionException:
64 65 # svn.client.url_from_path() fails with local repositories
65 66 pass
66 67 if os.path.isdir(path):
67 68 path = os.path.normpath(os.path.abspath(path))
68 69 if os.name == 'nt':
69 70 path = '/' + util.normpath(path)
70 71 # Module URL is later compared with the repository URL returned
71 72 # by svn API, which is UTF-8.
72 73 path = encoding.tolocal(path)
73 74 path = 'file://%s' % quote(path)
74 75 return svn.core.svn_path_canonicalize(path)
75 76
76 77 def optrev(number):
77 78 optrev = svn.core.svn_opt_revision_t()
78 79 optrev.kind = svn.core.svn_opt_revision_number
79 80 optrev.value.number = number
80 81 return optrev
81 82
82 83 class changedpath(object):
83 84 def __init__(self, p):
84 85 self.copyfrom_path = p.copyfrom_path
85 86 self.copyfrom_rev = p.copyfrom_rev
86 87 self.action = p.action
87 88
88 89 def get_log_child(fp, url, paths, start, end, limit=0,
89 90 discover_changed_paths=True, strict_node_history=False):
90 91 protocol = -1
91 92 def receiver(orig_paths, revnum, author, date, message, pool):
92 93 if orig_paths is not None:
93 94 for k, v in orig_paths.iteritems():
94 95 orig_paths[k] = changedpath(v)
95 96 pickle.dump((orig_paths, revnum, author, date, message),
96 97 fp, protocol)
97 98
98 99 try:
99 100 # Use an ra of our own so that our parent can consume
100 101 # our results without confusing the server.
101 102 t = transport.SvnRaTransport(url=url)
102 103 svn.ra.get_log(t.ra, paths, start, end, limit,
103 104 discover_changed_paths,
104 105 strict_node_history,
105 106 receiver)
106 107 except IOError:
107 108 # Caller may interrupt the iteration
108 109 pickle.dump(None, fp, protocol)
109 110 except Exception, inst:
110 111 pickle.dump(str(inst), fp, protocol)
111 112 else:
112 113 pickle.dump(None, fp, protocol)
113 114 fp.close()
114 115 # With large history, cleanup process goes crazy and suddenly
115 116 # consumes *huge* amount of memory. The output file being closed,
116 117 # there is no need for clean termination.
117 118 os._exit(0)
118 119
119 120 def debugsvnlog(ui, **opts):
120 121 """Fetch SVN log in a subprocess and channel them back to parent to
121 122 avoid memory collection issues.
122 123 """
123 124 if svn is None:
124 125 raise util.Abort(_('debugsvnlog could not load Subversion python '
125 126 'bindings'))
126 127
127 128 util.setbinary(sys.stdin)
128 129 util.setbinary(sys.stdout)
129 130 args = decodeargs(sys.stdin.read())
130 131 get_log_child(sys.stdout, *args)
131 132
132 133 class logstream(object):
133 134 """Interruptible revision log iterator."""
134 135 def __init__(self, stdout):
135 136 self._stdout = stdout
136 137
137 138 def __iter__(self):
138 139 while True:
139 140 try:
140 141 entry = pickle.load(self._stdout)
141 142 except EOFError:
142 143 raise util.Abort(_('Mercurial failed to run itself, check'
143 144 ' hg executable is in PATH'))
144 145 try:
145 146 orig_paths, revnum, author, date, message = entry
146 147 except (TypeError, ValueError):
147 148 if entry is None:
148 149 break
149 150 raise util.Abort(_("log stream exception '%s'") % entry)
150 151 yield entry
151 152
152 153 def close(self):
153 154 if self._stdout:
154 155 self._stdout.close()
155 156 self._stdout = None
156 157
157 158
158 159 # Check to see if the given path is a local Subversion repo. Verify this by
159 160 # looking for several svn-specific files and directories in the given
160 161 # directory.
161 162 def filecheck(ui, path, proto):
162 163 for x in ('locks', 'hooks', 'format', 'db'):
163 164 if not os.path.exists(os.path.join(path, x)):
164 165 return False
165 166 return True
166 167
167 168 # Check to see if a given path is the root of an svn repo over http. We verify
168 169 # this by requesting a version-controlled URL we know can't exist and looking
169 170 # for the svn-specific "not found" XML.
170 171 def httpcheck(ui, path, proto):
171 172 try:
172 173 opener = urllib2.build_opener()
173 174 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
174 175 data = rsp.read()
175 176 except urllib2.HTTPError, inst:
176 177 if inst.code != 404:
177 178 # Except for 404 we cannot know for sure this is not an svn repo
178 179 ui.warn(_('svn: cannot probe remote repository, assume it could '
179 180 'be a subversion repository. Use --source-type if you '
180 181 'know better.\n'))
181 182 return True
182 183 data = inst.fp.read()
183 184 except Exception:
184 185 # Could be urllib2.URLError if the URL is invalid or anything else.
185 186 return False
186 187 return '<m:human-readable errcode="160013">' in data
187 188
188 189 protomap = {'http': httpcheck,
189 190 'https': httpcheck,
190 191 'file': filecheck,
191 192 }
192 193 def issvnurl(ui, url):
193 194 try:
194 195 proto, path = url.split('://', 1)
195 196 if proto == 'file':
196 197 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
197 198 and path[2:6].lower() == '%3a/'):
198 199 path = path[:2] + ':/' + path[6:]
199 200 path = urllib.url2pathname(path)
200 201 except ValueError:
201 202 proto = 'file'
202 203 path = os.path.abspath(url)
203 204 if proto == 'file':
204 205 path = util.pconvert(path)
205 206 check = protomap.get(proto, lambda *args: False)
206 207 while '/' in path:
207 208 if check(ui, path, proto):
208 209 return True
209 210 path = path.rsplit('/', 1)[0]
210 211 return False
211 212
212 213 # SVN conversion code stolen from bzr-svn and tailor
213 214 #
214 215 # Subversion looks like a versioned filesystem, branches structures
215 216 # are defined by conventions and not enforced by the tool. First,
216 217 # we define the potential branches (modules) as "trunk" and "branches"
217 218 # children directories. Revisions are then identified by their
218 219 # module and revision number (and a repository identifier).
219 220 #
220 221 # The revision graph is really a tree (or a forest). By default, a
221 222 # revision parent is the previous revision in the same module. If the
222 223 # module directory is copied/moved from another module then the
223 224 # revision is the module root and its parent the source revision in
224 225 # the parent module. A revision has at most one parent.
225 226 #
226 227 class svn_source(converter_source):
227 228 def __init__(self, ui, url, rev=None):
228 229 super(svn_source, self).__init__(ui, url, rev=rev)
229 230
230 231 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
231 232 (os.path.exists(url) and
232 233 os.path.exists(os.path.join(url, '.svn'))) or
233 234 issvnurl(ui, url)):
234 235 raise NoRepo(_("%s does not look like a Subversion repository")
235 236 % url)
236 237 if svn is None:
237 238 raise MissingTool(_('could not load Subversion python bindings'))
238 239
239 240 try:
240 241 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
241 242 if version < (1, 4):
242 243 raise MissingTool(_('Subversion python bindings %d.%d found, '
243 244 '1.4 or later required') % version)
244 245 except AttributeError:
245 246 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
246 247 'or later required'))
247 248
248 249 self.lastrevs = {}
249 250
250 251 latest = None
251 252 try:
252 253 # Support file://path@rev syntax. Useful e.g. to convert
253 254 # deleted branches.
254 255 at = url.rfind('@')
255 256 if at >= 0:
256 257 latest = int(url[at + 1:])
257 258 url = url[:at]
258 259 except ValueError:
259 260 pass
260 261 self.url = geturl(url)
261 262 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
262 263 try:
263 264 self.transport = transport.SvnRaTransport(url=self.url)
264 265 self.ra = self.transport.ra
265 266 self.ctx = self.transport.client
266 267 self.baseurl = svn.ra.get_repos_root(self.ra)
267 268 # Module is either empty or a repository path starting with
268 269 # a slash and not ending with a slash.
269 270 self.module = urllib.unquote(self.url[len(self.baseurl):])
270 271 self.prevmodule = None
271 272 self.rootmodule = self.module
272 273 self.commits = {}
273 274 self.paths = {}
274 275 self.uuid = svn.ra.get_uuid(self.ra)
275 276 except SubversionException:
276 277 ui.traceback()
277 278 raise NoRepo(_("%s does not look like a Subversion repository")
278 279 % self.url)
279 280
280 281 if rev:
281 282 try:
282 283 latest = int(rev)
283 284 except ValueError:
284 285 raise util.Abort(_('svn: revision %s is not an integer') % rev)
285 286
286 287 self.trunkname = self.ui.config('convert', 'svn.trunk',
287 288 'trunk').strip('/')
288 289 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
289 290 try:
290 291 self.startrev = int(self.startrev)
291 292 if self.startrev < 0:
292 293 self.startrev = 0
293 294 except ValueError:
294 295 raise util.Abort(_('svn: start revision %s is not an integer')
295 296 % self.startrev)
296 297
297 298 try:
298 299 self.head = self.latest(self.module, latest)
299 300 except SvnPathNotFound:
300 301 self.head = None
301 302 if not self.head:
302 303 raise util.Abort(_('no revision found in module %s')
303 304 % self.module)
304 305 self.last_changed = self.revnum(self.head)
305 306
306 307 self._changescache = None
307 308
308 309 if os.path.exists(os.path.join(url, '.svn/entries')):
309 310 self.wc = url
310 311 else:
311 312 self.wc = None
312 313 self.convertfp = None
313 314
314 315 def setrevmap(self, revmap):
315 316 lastrevs = {}
316 317 for revid in revmap.iterkeys():
317 318 uuid, module, revnum = revsplit(revid)
318 319 lastrevnum = lastrevs.setdefault(module, revnum)
319 320 if revnum > lastrevnum:
320 321 lastrevs[module] = revnum
321 322 self.lastrevs = lastrevs
322 323
323 324 def exists(self, path, optrev):
324 325 try:
325 326 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
326 327 optrev, False, self.ctx)
327 328 return True
328 329 except SubversionException:
329 330 return False
330 331
331 332 def getheads(self):
332 333
333 334 def isdir(path, revnum):
334 335 kind = self._checkpath(path, revnum)
335 336 return kind == svn.core.svn_node_dir
336 337
337 338 def getcfgpath(name, rev):
338 339 cfgpath = self.ui.config('convert', 'svn.' + name)
339 340 if cfgpath is not None and cfgpath.strip() == '':
340 341 return None
341 342 path = (cfgpath or name).strip('/')
342 343 if not self.exists(path, rev):
343 344 if self.module.endswith(path) and name == 'trunk':
344 345 # we are converting from inside this directory
345 346 return None
346 347 if cfgpath:
347 348 raise util.Abort(_('expected %s to be at %r, but not found')
348 349 % (name, path))
349 350 return None
350 351 self.ui.note(_('found %s at %r\n') % (name, path))
351 352 return path
352 353
353 354 rev = optrev(self.last_changed)
354 355 oldmodule = ''
355 356 trunk = getcfgpath('trunk', rev)
356 357 self.tags = getcfgpath('tags', rev)
357 358 branches = getcfgpath('branches', rev)
358 359
359 360 # If the project has a trunk or branches, we will extract heads
360 361 # from them. We keep the project root otherwise.
361 362 if trunk:
362 363 oldmodule = self.module or ''
363 364 self.module += '/' + trunk
364 365 self.head = self.latest(self.module, self.last_changed)
365 366 if not self.head:
366 367 raise util.Abort(_('no revision found in module %s')
367 368 % self.module)
368 369
369 370 # First head in the list is the module's head
370 371 self.heads = [self.head]
371 372 if self.tags is not None:
372 373 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
373 374
374 375 # Check if branches bring a few more heads to the list
375 376 if branches:
376 377 rpath = self.url.strip('/')
377 378 branchnames = svn.client.ls(rpath + '/' + quote(branches),
378 379 rev, False, self.ctx)
379 380 for branch in branchnames.keys():
380 381 module = '%s/%s/%s' % (oldmodule, branches, branch)
381 382 if not isdir(module, self.last_changed):
382 383 continue
383 384 brevid = self.latest(module, self.last_changed)
384 385 if not brevid:
385 386 self.ui.note(_('ignoring empty branch %s\n') % branch)
386 387 continue
387 388 self.ui.note(_('found branch %s at %d\n') %
388 389 (branch, self.revnum(brevid)))
389 390 self.heads.append(brevid)
390 391
391 392 if self.startrev and self.heads:
392 393 if len(self.heads) > 1:
393 394 raise util.Abort(_('svn: start revision is not supported '
394 395 'with more than one branch'))
395 396 revnum = self.revnum(self.heads[0])
396 397 if revnum < self.startrev:
397 398 raise util.Abort(
398 399 _('svn: no revision found after start revision %d')
399 400 % self.startrev)
400 401
401 402 return self.heads
402 403
403 404 def getchanges(self, rev):
404 405 if self._changescache and self._changescache[0] == rev:
405 406 return self._changescache[1]
406 407 self._changescache = None
407 408 (paths, parents) = self.paths[rev]
408 409 if parents:
409 410 files, self.removed, copies = self.expandpaths(rev, paths, parents)
410 411 else:
411 412 # Perform a full checkout on roots
412 413 uuid, module, revnum = revsplit(rev)
413 414 entries = svn.client.ls(self.baseurl + quote(module),
414 415 optrev(revnum), True, self.ctx)
415 416 files = [n for n, e in entries.iteritems()
416 417 if e.kind == svn.core.svn_node_file]
417 418 copies = {}
418 419 self.removed = set()
419 420
420 421 files.sort()
421 422 files = zip(files, [rev] * len(files))
422 423
423 424 # caller caches the result, so free it here to release memory
424 425 del self.paths[rev]
425 426 return (files, copies)
426 427
427 428 def getchangedfiles(self, rev, i):
428 429 changes = self.getchanges(rev)
429 430 self._changescache = (rev, changes)
430 431 return [f[0] for f in changes[0]]
431 432
432 433 def getcommit(self, rev):
433 434 if rev not in self.commits:
434 435 uuid, module, revnum = revsplit(rev)
435 436 self.module = module
436 437 self.reparent(module)
437 438 # We assume that:
438 439 # - requests for revisions after "stop" come from the
439 440 # revision graph backward traversal. Cache all of them
440 441 # down to stop, they will be used eventually.
441 442 # - requests for revisions before "stop" come to get
442 443 # isolated branches parents. Just fetch what is needed.
443 444 stop = self.lastrevs.get(module, 0)
444 445 if revnum < stop:
445 446 stop = revnum + 1
446 447 self._fetch_revisions(revnum, stop)
447 448 if rev not in self.commits:
448 449 raise util.Abort(_('svn: revision %s not found') % revnum)
449 450 commit = self.commits[rev]
450 451 # caller caches the result, so free it here to release memory
451 452 del self.commits[rev]
452 453 return commit
453 454
454 455 def gettags(self):
455 456 tags = {}
456 457 if self.tags is None:
457 458 return tags
458 459
459 460 # svn tags are just a convention, project branches left in a
460 461 # 'tags' directory. There is no other relationship than
461 462 # ancestry, which is expensive to discover and makes them hard
462 463 # to update incrementally. Worse, past revisions may be
463 464 # referenced by tags far away in the future, requiring a deep
464 465 # history traversal on every calculation. Current code
465 466 # performs a single backward traversal, tracking moves within
466 467 # the tags directory (tag renaming) and recording a new tag
467 468 # everytime a project is copied from outside the tags
468 469 # directory. It also lists deleted tags, this behaviour may
469 470 # change in the future.
470 471 pendings = []
471 472 tagspath = self.tags
472 473 start = svn.ra.get_latest_revnum(self.ra)
473 474 stream = self._getlog([self.tags], start, self.startrev)
474 475 try:
475 476 for entry in stream:
476 477 origpaths, revnum, author, date, message = entry
477 478 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
478 479 in origpaths.iteritems() if e.copyfrom_path]
479 480 # Apply moves/copies from more specific to general
480 481 copies.sort(reverse=True)
481 482
482 483 srctagspath = tagspath
483 484 if copies and copies[-1][2] == tagspath:
484 485 # Track tags directory moves
485 486 srctagspath = copies.pop()[0]
486 487
487 488 for source, sourcerev, dest in copies:
488 489 if not dest.startswith(tagspath + '/'):
489 490 continue
490 491 for tag in pendings:
491 492 if tag[0].startswith(dest):
492 493 tagpath = source + tag[0][len(dest):]
493 494 tag[:2] = [tagpath, sourcerev]
494 495 break
495 496 else:
496 497 pendings.append([source, sourcerev, dest])
497 498
498 499 # Filter out tags with children coming from different
499 500 # parts of the repository like:
500 501 # /tags/tag.1 (from /trunk:10)
501 502 # /tags/tag.1/foo (from /branches/foo:12)
502 503 # Here/tags/tag.1 discarded as well as its children.
503 504 # It happens with tools like cvs2svn. Such tags cannot
504 505 # be represented in mercurial.
505 506 addeds = dict((p, e.copyfrom_path) for p, e
506 507 in origpaths.iteritems()
507 508 if e.action == 'A' and e.copyfrom_path)
508 509 badroots = set()
509 510 for destroot in addeds:
510 511 for source, sourcerev, dest in pendings:
511 512 if (not dest.startswith(destroot + '/')
512 513 or source.startswith(addeds[destroot] + '/')):
513 514 continue
514 515 badroots.add(destroot)
515 516 break
516 517
517 518 for badroot in badroots:
518 519 pendings = [p for p in pendings if p[2] != badroot
519 520 and not p[2].startswith(badroot + '/')]
520 521
521 522 # Tell tag renamings from tag creations
522 523 renamings = []
523 524 for source, sourcerev, dest in pendings:
524 525 tagname = dest.split('/')[-1]
525 526 if source.startswith(srctagspath):
526 527 renamings.append([source, sourcerev, tagname])
527 528 continue
528 529 if tagname in tags:
529 530 # Keep the latest tag value
530 531 continue
531 532 # From revision may be fake, get one with changes
532 533 try:
533 534 tagid = self.latest(source, sourcerev)
534 535 if tagid and tagname not in tags:
535 536 tags[tagname] = tagid
536 537 except SvnPathNotFound:
537 538 # It happens when we are following directories
538 539 # we assumed were copied with their parents
539 540 # but were really created in the tag
540 541 # directory.
541 542 pass
542 543 pendings = renamings
543 544 tagspath = srctagspath
544 545 finally:
545 546 stream.close()
546 547 return tags
547 548
548 549 def converted(self, rev, destrev):
549 550 if not self.wc:
550 551 return
551 552 if self.convertfp is None:
552 553 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
553 554 'a')
554 555 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
555 556 self.convertfp.flush()
556 557
557 558 def revid(self, revnum, module=None):
558 559 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
559 560
560 561 def revnum(self, rev):
561 562 return int(rev.split('@')[-1])
562 563
563 564 def latest(self, path, stop=None):
564 565 """Find the latest revid affecting path, up to stop revision
565 566 number. If stop is None, default to repository latest
566 567 revision. It may return a revision in a different module,
567 568 since a branch may be moved without a change being
568 569 reported. Return None if computed module does not belong to
569 570 rootmodule subtree.
570 571 """
571 572 def findchanges(path, start, stop=None):
572 573 stream = self._getlog([path], start, stop or 1)
573 574 try:
574 575 for entry in stream:
575 576 paths, revnum, author, date, message = entry
576 577 if stop is None and paths:
577 578 # We do not know the latest changed revision,
578 579 # keep the first one with changed paths.
579 580 break
580 581 if revnum <= stop:
581 582 break
582 583
583 584 for p in paths:
584 585 if (not path.startswith(p) or
585 586 not paths[p].copyfrom_path):
586 587 continue
587 588 newpath = paths[p].copyfrom_path + path[len(p):]
588 589 self.ui.debug("branch renamed from %s to %s at %d\n" %
589 590 (path, newpath, revnum))
590 591 path = newpath
591 592 break
592 593 if not paths:
593 594 revnum = None
594 595 return revnum, path
595 596 finally:
596 597 stream.close()
597 598
598 599 if not path.startswith(self.rootmodule):
599 600 # Requests on foreign branches may be forbidden at server level
600 601 self.ui.debug('ignoring foreign branch %r\n' % path)
601 602 return None
602 603
603 604 if stop is None:
604 605 stop = svn.ra.get_latest_revnum(self.ra)
605 606 try:
606 607 prevmodule = self.reparent('')
607 608 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
608 609 self.reparent(prevmodule)
609 610 except SubversionException:
610 611 dirent = None
611 612 if not dirent:
612 613 raise SvnPathNotFound(_('%s not found up to revision %d')
613 614 % (path, stop))
614 615
615 616 # stat() gives us the previous revision on this line of
616 617 # development, but it might be in *another module*. Fetch the
617 618 # log and detect renames down to the latest revision.
618 619 revnum, realpath = findchanges(path, stop, dirent.created_rev)
619 620 if revnum is None:
620 621 # Tools like svnsync can create empty revision, when
621 622 # synchronizing only a subtree for instance. These empty
622 623 # revisions created_rev still have their original values
623 624 # despite all changes having disappeared and can be
624 625 # returned by ra.stat(), at least when stating the root
625 626 # module. In that case, do not trust created_rev and scan
626 627 # the whole history.
627 628 revnum, realpath = findchanges(path, stop)
628 629 if revnum is None:
629 630 self.ui.debug('ignoring empty branch %r\n' % realpath)
630 631 return None
631 632
632 633 if not realpath.startswith(self.rootmodule):
633 634 self.ui.debug('ignoring foreign branch %r\n' % realpath)
634 635 return None
635 636 return self.revid(revnum, realpath)
636 637
637 638 def reparent(self, module):
638 639 """Reparent the svn transport and return the previous parent."""
639 640 if self.prevmodule == module:
640 641 return module
641 642 svnurl = self.baseurl + quote(module)
642 643 prevmodule = self.prevmodule
643 644 if prevmodule is None:
644 645 prevmodule = ''
645 646 self.ui.debug("reparent to %s\n" % svnurl)
646 647 svn.ra.reparent(self.ra, svnurl)
647 648 self.prevmodule = module
648 649 return prevmodule
649 650
650 651 def expandpaths(self, rev, paths, parents):
651 652 changed, removed = set(), set()
652 653 copies = {}
653 654
654 655 new_module, revnum = revsplit(rev)[1:]
655 656 if new_module != self.module:
656 657 self.module = new_module
657 658 self.reparent(self.module)
658 659
659 660 for i, (path, ent) in enumerate(paths):
660 661 self.ui.progress(_('scanning paths'), i, item=path,
661 662 total=len(paths))
662 663 entrypath = self.getrelpath(path)
663 664
664 665 kind = self._checkpath(entrypath, revnum)
665 666 if kind == svn.core.svn_node_file:
666 667 changed.add(self.recode(entrypath))
667 668 if not ent.copyfrom_path or not parents:
668 669 continue
669 670 # Copy sources not in parent revisions cannot be
670 671 # represented, ignore their origin for now
671 672 pmodule, prevnum = revsplit(parents[0])[1:]
672 673 if ent.copyfrom_rev < prevnum:
673 674 continue
674 675 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
675 676 if not copyfrom_path:
676 677 continue
677 678 self.ui.debug("copied to %s from %s@%s\n" %
678 679 (entrypath, copyfrom_path, ent.copyfrom_rev))
679 680 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
680 681 elif kind == 0: # gone, but had better be a deleted *file*
681 682 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
682 683 pmodule, prevnum = revsplit(parents[0])[1:]
683 684 parentpath = pmodule + "/" + entrypath
684 685 fromkind = self._checkpath(entrypath, prevnum, pmodule)
685 686
686 687 if fromkind == svn.core.svn_node_file:
687 688 removed.add(self.recode(entrypath))
688 689 elif fromkind == svn.core.svn_node_dir:
689 690 oroot = parentpath.strip('/')
690 691 nroot = path.strip('/')
691 692 children = self._iterfiles(oroot, prevnum)
692 693 for childpath in children:
693 694 childpath = childpath.replace(oroot, nroot)
694 695 childpath = self.getrelpath("/" + childpath, pmodule)
695 696 if childpath:
696 697 removed.add(self.recode(childpath))
697 698 else:
698 699 self.ui.debug('unknown path in revision %d: %s\n' % \
699 700 (revnum, path))
700 701 elif kind == svn.core.svn_node_dir:
701 702 if ent.action == 'M':
702 703 # If the directory just had a prop change,
703 704 # then we shouldn't need to look for its children.
704 705 continue
705 706 if ent.action == 'R' and parents:
706 707 # If a directory is replacing a file, mark the previous
707 708 # file as deleted
708 709 pmodule, prevnum = revsplit(parents[0])[1:]
709 710 pkind = self._checkpath(entrypath, prevnum, pmodule)
710 711 if pkind == svn.core.svn_node_file:
711 712 removed.add(self.recode(entrypath))
712 713 elif pkind == svn.core.svn_node_dir:
713 714 # We do not know what files were kept or removed,
714 715 # mark them all as changed.
715 716 for childpath in self._iterfiles(pmodule, prevnum):
716 717 childpath = self.getrelpath("/" + childpath)
717 718 if childpath:
718 719 changed.add(self.recode(childpath))
719 720
720 721 for childpath in self._iterfiles(path, revnum):
721 722 childpath = self.getrelpath("/" + childpath)
722 723 if childpath:
723 724 changed.add(self.recode(childpath))
724 725
725 726 # Handle directory copies
726 727 if not ent.copyfrom_path or not parents:
727 728 continue
728 729 # Copy sources not in parent revisions cannot be
729 730 # represented, ignore their origin for now
730 731 pmodule, prevnum = revsplit(parents[0])[1:]
731 732 if ent.copyfrom_rev < prevnum:
732 733 continue
733 734 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
734 735 if not copyfrompath:
735 736 continue
736 737 self.ui.debug("mark %s came from %s:%d\n"
737 738 % (path, copyfrompath, ent.copyfrom_rev))
738 739 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
739 740 for childpath in children:
740 741 childpath = self.getrelpath("/" + childpath, pmodule)
741 742 if not childpath:
742 743 continue
743 744 copytopath = path + childpath[len(copyfrompath):]
744 745 copytopath = self.getrelpath(copytopath)
745 746 copies[self.recode(copytopath)] = self.recode(childpath)
746 747
747 748 self.ui.progress(_('scanning paths'), None)
748 749 changed.update(removed)
749 750 return (list(changed), removed, copies)
750 751
751 752 def _fetch_revisions(self, from_revnum, to_revnum):
752 753 if from_revnum < to_revnum:
753 754 from_revnum, to_revnum = to_revnum, from_revnum
754 755
755 756 self.child_cset = None
756 757
757 758 def parselogentry(orig_paths, revnum, author, date, message):
758 759 """Return the parsed commit object or None, and True if
759 760 the revision is a branch root.
760 761 """
761 762 self.ui.debug("parsing revision %d (%d changes)\n" %
762 763 (revnum, len(orig_paths)))
763 764
764 765 branched = False
765 766 rev = self.revid(revnum)
766 767 # branch log might return entries for a parent we already have
767 768
768 769 if rev in self.commits or revnum < to_revnum:
769 770 return None, branched
770 771
771 772 parents = []
772 773 # check whether this revision is the start of a branch or part
773 774 # of a branch renaming
774 775 orig_paths = sorted(orig_paths.iteritems())
775 776 root_paths = [(p, e) for p, e in orig_paths
776 777 if self.module.startswith(p)]
777 778 if root_paths:
778 779 path, ent = root_paths[-1]
779 780 if ent.copyfrom_path:
780 781 branched = True
781 782 newpath = ent.copyfrom_path + self.module[len(path):]
782 783 # ent.copyfrom_rev may not be the actual last revision
783 784 previd = self.latest(newpath, ent.copyfrom_rev)
784 785 if previd is not None:
785 786 prevmodule, prevnum = revsplit(previd)[1:]
786 787 if prevnum >= self.startrev:
787 788 parents = [previd]
788 789 self.ui.note(
789 790 _('found parent of branch %s at %d: %s\n') %
790 791 (self.module, prevnum, prevmodule))
791 792 else:
792 793 self.ui.debug("no copyfrom path, don't know what to do.\n")
793 794
794 795 paths = []
795 796 # filter out unrelated paths
796 797 for path, ent in orig_paths:
797 798 if self.getrelpath(path) is None:
798 799 continue
799 800 paths.append((path, ent))
800 801
801 802 # Example SVN datetime. Includes microseconds.
802 803 # ISO-8601 conformant
803 804 # '2007-01-04T17:35:00.902377Z'
804 805 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
806 if self.ui.configbool('convert', 'localtimezone'):
807 date = makedatetimestamp(date[0])
805 808
806 809 log = message and self.recode(message) or ''
807 810 author = author and self.recode(author) or ''
808 811 try:
809 812 branch = self.module.split("/")[-1]
810 813 if branch == self.trunkname:
811 814 branch = None
812 815 except IndexError:
813 816 branch = None
814 817
815 818 cset = commit(author=author,
816 819 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
817 820 desc=log,
818 821 parents=parents,
819 822 branch=branch,
820 823 rev=rev)
821 824
822 825 self.commits[rev] = cset
823 826 # The parents list is *shared* among self.paths and the
824 827 # commit object. Both will be updated below.
825 828 self.paths[rev] = (paths, cset.parents)
826 829 if self.child_cset and not self.child_cset.parents:
827 830 self.child_cset.parents[:] = [rev]
828 831 self.child_cset = cset
829 832 return cset, branched
830 833
831 834 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
832 835 (self.module, from_revnum, to_revnum))
833 836
834 837 try:
835 838 firstcset = None
836 839 lastonbranch = False
837 840 stream = self._getlog([self.module], from_revnum, to_revnum)
838 841 try:
839 842 for entry in stream:
840 843 paths, revnum, author, date, message = entry
841 844 if revnum < self.startrev:
842 845 lastonbranch = True
843 846 break
844 847 if not paths:
845 848 self.ui.debug('revision %d has no entries\n' % revnum)
846 849 # If we ever leave the loop on an empty
847 850 # revision, do not try to get a parent branch
848 851 lastonbranch = lastonbranch or revnum == 0
849 852 continue
850 853 cset, lastonbranch = parselogentry(paths, revnum, author,
851 854 date, message)
852 855 if cset:
853 856 firstcset = cset
854 857 if lastonbranch:
855 858 break
856 859 finally:
857 860 stream.close()
858 861
859 862 if not lastonbranch and firstcset and not firstcset.parents:
860 863 # The first revision of the sequence (the last fetched one)
861 864 # has invalid parents if not a branch root. Find the parent
862 865 # revision now, if any.
863 866 try:
864 867 firstrevnum = self.revnum(firstcset.rev)
865 868 if firstrevnum > 1:
866 869 latest = self.latest(self.module, firstrevnum - 1)
867 870 if latest:
868 871 firstcset.parents.append(latest)
869 872 except SvnPathNotFound:
870 873 pass
871 874 except SubversionException, (inst, num):
872 875 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
873 876 raise util.Abort(_('svn: branch has no revision %s')
874 877 % to_revnum)
875 878 raise
876 879
877 880 def getfile(self, file, rev):
878 881 # TODO: ra.get_file transmits the whole file instead of diffs.
879 882 if file in self.removed:
880 883 raise IOError
881 884 mode = ''
882 885 try:
883 886 new_module, revnum = revsplit(rev)[1:]
884 887 if self.module != new_module:
885 888 self.module = new_module
886 889 self.reparent(self.module)
887 890 io = StringIO()
888 891 info = svn.ra.get_file(self.ra, file, revnum, io)
889 892 data = io.getvalue()
890 893 # ra.get_file() seems to keep a reference on the input buffer
891 894 # preventing collection. Release it explicitly.
892 895 io.close()
893 896 if isinstance(info, list):
894 897 info = info[-1]
895 898 mode = ("svn:executable" in info) and 'x' or ''
896 899 mode = ("svn:special" in info) and 'l' or mode
897 900 except SubversionException, e:
898 901 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
899 902 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
900 903 if e.apr_err in notfound: # File not found
901 904 raise IOError
902 905 raise
903 906 if mode == 'l':
904 907 link_prefix = "link "
905 908 if data.startswith(link_prefix):
906 909 data = data[len(link_prefix):]
907 910 return data, mode
908 911
909 912 def _iterfiles(self, path, revnum):
910 913 """Enumerate all files in path at revnum, recursively."""
911 914 path = path.strip('/')
912 915 pool = Pool()
913 916 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
914 917 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
915 918 if path:
916 919 path += '/'
917 920 return ((path + p) for p, e in entries.iteritems()
918 921 if e.kind == svn.core.svn_node_file)
919 922
920 923 def getrelpath(self, path, module=None):
921 924 if module is None:
922 925 module = self.module
923 926 # Given the repository url of this wc, say
924 927 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
925 928 # extract the "entry" portion (a relative path) from what
926 929 # svn log --xml says, i.e.
927 930 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
928 931 # that is to say "tests/PloneTestCase.py"
929 932 if path.startswith(module):
930 933 relative = path.rstrip('/')[len(module):]
931 934 if relative.startswith('/'):
932 935 return relative[1:]
933 936 elif relative == '':
934 937 return relative
935 938
936 939 # The path is outside our tracked tree...
937 940 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
938 941 return None
939 942
940 943 def _checkpath(self, path, revnum, module=None):
941 944 if module is not None:
942 945 prevmodule = self.reparent('')
943 946 path = module + '/' + path
944 947 try:
945 948 # ra.check_path does not like leading slashes very much, it leads
946 949 # to PROPFIND subversion errors
947 950 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
948 951 finally:
949 952 if module is not None:
950 953 self.reparent(prevmodule)
951 954
952 955 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
953 956 strict_node_history=False):
954 957 # Normalize path names, svn >= 1.5 only wants paths relative to
955 958 # supplied URL
956 959 relpaths = []
957 960 for p in paths:
958 961 if not p.startswith('/'):
959 962 p = self.module + '/' + p
960 963 relpaths.append(p.strip('/'))
961 964 args = [self.baseurl, relpaths, start, end, limit,
962 965 discover_changed_paths, strict_node_history]
963 966 arg = encodeargs(args)
964 967 hgexe = util.hgexecutable()
965 968 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
966 969 stdin, stdout = util.popen2(util.quotecommand(cmd))
967 970 stdin.write(arg)
968 971 try:
969 972 stdin.close()
970 973 except IOError:
971 974 raise util.Abort(_('Mercurial failed to run itself, check'
972 975 ' hg executable is in PATH'))
973 976 return logstream(stdout)
974 977
975 978 pre_revprop_change = '''#!/bin/sh
976 979
977 980 REPOS="$1"
978 981 REV="$2"
979 982 USER="$3"
980 983 PROPNAME="$4"
981 984 ACTION="$5"
982 985
983 986 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
984 987 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
985 988 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
986 989
987 990 echo "Changing prohibited revision property" >&2
988 991 exit 1
989 992 '''
990 993
991 994 class svn_sink(converter_sink, commandline):
992 995 commit_re = re.compile(r'Committed revision (\d+).', re.M)
993 996 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
994 997
995 998 def prerun(self):
996 999 if self.wc:
997 1000 os.chdir(self.wc)
998 1001
999 1002 def postrun(self):
1000 1003 if self.wc:
1001 1004 os.chdir(self.cwd)
1002 1005
1003 1006 def join(self, name):
1004 1007 return os.path.join(self.wc, '.svn', name)
1005 1008
1006 1009 def revmapfile(self):
1007 1010 return self.join('hg-shamap')
1008 1011
1009 1012 def authorfile(self):
1010 1013 return self.join('hg-authormap')
1011 1014
1012 1015 def __init__(self, ui, path):
1013 1016
1014 1017 converter_sink.__init__(self, ui, path)
1015 1018 commandline.__init__(self, ui, 'svn')
1016 1019 self.delete = []
1017 1020 self.setexec = []
1018 1021 self.delexec = []
1019 1022 self.copies = []
1020 1023 self.wc = None
1021 1024 self.cwd = os.getcwd()
1022 1025
1023 1026 created = False
1024 1027 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1025 1028 self.wc = os.path.realpath(path)
1026 1029 self.run0('update')
1027 1030 else:
1028 1031 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1029 1032 path = os.path.realpath(path)
1030 1033 if os.path.isdir(os.path.dirname(path)):
1031 1034 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1032 1035 ui.status(_('initializing svn repository %r\n') %
1033 1036 os.path.basename(path))
1034 1037 commandline(ui, 'svnadmin').run0('create', path)
1035 1038 created = path
1036 1039 path = util.normpath(path)
1037 1040 if not path.startswith('/'):
1038 1041 path = '/' + path
1039 1042 path = 'file://' + path
1040 1043
1041 1044 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1042 1045 ui.status(_('initializing svn working copy %r\n')
1043 1046 % os.path.basename(wcpath))
1044 1047 self.run0('checkout', path, wcpath)
1045 1048
1046 1049 self.wc = wcpath
1047 1050 self.opener = scmutil.opener(self.wc)
1048 1051 self.wopener = scmutil.opener(self.wc)
1049 1052 self.childmap = mapfile(ui, self.join('hg-childmap'))
1050 1053 self.is_exec = util.checkexec(self.wc) and util.isexec or None
1051 1054
1052 1055 if created:
1053 1056 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1054 1057 fp = open(hook, 'w')
1055 1058 fp.write(pre_revprop_change)
1056 1059 fp.close()
1057 1060 util.setflags(hook, False, True)
1058 1061
1059 1062 output = self.run0('info')
1060 1063 self.uuid = self.uuid_re.search(output).group(1).strip()
1061 1064
1062 1065 def wjoin(self, *names):
1063 1066 return os.path.join(self.wc, *names)
1064 1067
1065 1068 @propertycache
1066 1069 def manifest(self):
1067 1070 # As of svn 1.7, the "add" command fails when receiving
1068 1071 # already tracked entries, so we have to track and filter them
1069 1072 # ourselves.
1070 1073 m = set()
1071 1074 output = self.run0('ls', recursive=True, xml=True)
1072 1075 doc = xml.dom.minidom.parseString(output)
1073 1076 for e in doc.getElementsByTagName('entry'):
1074 1077 for n in e.childNodes:
1075 1078 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1076 1079 continue
1077 1080 name = ''.join(c.data for c in n.childNodes
1078 1081 if c.nodeType == c.TEXT_NODE)
1079 1082 # Entries are compared with names coming from
1080 1083 # mercurial, so bytes with undefined encoding. Our
1081 1084 # best bet is to assume they are in local
1082 1085 # encoding. They will be passed to command line calls
1083 1086 # later anyway, so they better be.
1084 1087 m.add(encoding.tolocal(name.encode('utf-8')))
1085 1088 break
1086 1089 return m
1087 1090
1088 1091 def putfile(self, filename, flags, data):
1089 1092 if 'l' in flags:
1090 1093 self.wopener.symlink(data, filename)
1091 1094 else:
1092 1095 try:
1093 1096 if os.path.islink(self.wjoin(filename)):
1094 1097 os.unlink(filename)
1095 1098 except OSError:
1096 1099 pass
1097 1100 self.wopener.write(filename, data)
1098 1101
1099 1102 if self.is_exec:
1100 1103 if self.is_exec(self.wjoin(filename)):
1101 1104 if 'x' not in flags:
1102 1105 self.delexec.append(filename)
1103 1106 else:
1104 1107 if 'x' in flags:
1105 1108 self.setexec.append(filename)
1106 1109 util.setflags(self.wjoin(filename), False, 'x' in flags)
1107 1110
1108 1111 def _copyfile(self, source, dest):
1109 1112 # SVN's copy command pukes if the destination file exists, but
1110 1113 # our copyfile method expects to record a copy that has
1111 1114 # already occurred. Cross the semantic gap.
1112 1115 wdest = self.wjoin(dest)
1113 1116 exists = os.path.lexists(wdest)
1114 1117 if exists:
1115 1118 fd, tempname = tempfile.mkstemp(
1116 1119 prefix='hg-copy-', dir=os.path.dirname(wdest))
1117 1120 os.close(fd)
1118 1121 os.unlink(tempname)
1119 1122 os.rename(wdest, tempname)
1120 1123 try:
1121 1124 self.run0('copy', source, dest)
1122 1125 finally:
1123 1126 self.manifest.add(dest)
1124 1127 if exists:
1125 1128 try:
1126 1129 os.unlink(wdest)
1127 1130 except OSError:
1128 1131 pass
1129 1132 os.rename(tempname, wdest)
1130 1133
1131 1134 def dirs_of(self, files):
1132 1135 dirs = set()
1133 1136 for f in files:
1134 1137 if os.path.isdir(self.wjoin(f)):
1135 1138 dirs.add(f)
1136 1139 for i in strutil.rfindall(f, '/'):
1137 1140 dirs.add(f[:i])
1138 1141 return dirs
1139 1142
1140 1143 def add_dirs(self, files):
1141 1144 add_dirs = [d for d in sorted(self.dirs_of(files))
1142 1145 if d not in self.manifest]
1143 1146 if add_dirs:
1144 1147 self.manifest.update(add_dirs)
1145 1148 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1146 1149 return add_dirs
1147 1150
1148 1151 def add_files(self, files):
1149 1152 files = [f for f in files if f not in self.manifest]
1150 1153 if files:
1151 1154 self.manifest.update(files)
1152 1155 self.xargs(files, 'add', quiet=True)
1153 1156 return files
1154 1157
1155 1158 def tidy_dirs(self, names):
1156 1159 deleted = []
1157 1160 for d in sorted(self.dirs_of(names), reverse=True):
1158 1161 wd = self.wjoin(d)
1159 1162 if os.listdir(wd) == '.svn':
1160 1163 self.run0('delete', d)
1161 1164 self.manifest.remove(d)
1162 1165 deleted.append(d)
1163 1166 return deleted
1164 1167
1165 1168 def addchild(self, parent, child):
1166 1169 self.childmap[parent] = child
1167 1170
1168 1171 def revid(self, rev):
1169 1172 return u"svn:%s@%s" % (self.uuid, rev)
1170 1173
1171 1174 def putcommit(self, files, copies, parents, commit, source, revmap):
1172 1175 for parent in parents:
1173 1176 try:
1174 1177 return self.revid(self.childmap[parent])
1175 1178 except KeyError:
1176 1179 pass
1177 1180
1178 1181 # Apply changes to working copy
1179 1182 for f, v in files:
1180 1183 try:
1181 1184 data, mode = source.getfile(f, v)
1182 1185 except IOError:
1183 1186 self.delete.append(f)
1184 1187 else:
1185 1188 self.putfile(f, mode, data)
1186 1189 if f in copies:
1187 1190 self.copies.append([copies[f], f])
1188 1191 files = [f[0] for f in files]
1189 1192
1190 1193 entries = set(self.delete)
1191 1194 files = frozenset(files)
1192 1195 entries.update(self.add_dirs(files.difference(entries)))
1193 1196 if self.copies:
1194 1197 for s, d in self.copies:
1195 1198 self._copyfile(s, d)
1196 1199 self.copies = []
1197 1200 if self.delete:
1198 1201 self.xargs(self.delete, 'delete')
1199 1202 for f in self.delete:
1200 1203 self.manifest.remove(f)
1201 1204 self.delete = []
1202 1205 entries.update(self.add_files(files.difference(entries)))
1203 1206 entries.update(self.tidy_dirs(entries))
1204 1207 if self.delexec:
1205 1208 self.xargs(self.delexec, 'propdel', 'svn:executable')
1206 1209 self.delexec = []
1207 1210 if self.setexec:
1208 1211 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1209 1212 self.setexec = []
1210 1213
1211 1214 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1212 1215 fp = os.fdopen(fd, 'w')
1213 1216 fp.write(commit.desc)
1214 1217 fp.close()
1215 1218 try:
1216 1219 output = self.run0('commit',
1217 1220 username=util.shortuser(commit.author),
1218 1221 file=messagefile,
1219 1222 encoding='utf-8')
1220 1223 try:
1221 1224 rev = self.commit_re.search(output).group(1)
1222 1225 except AttributeError:
1223 1226 if not files:
1224 1227 return parents[0]
1225 1228 self.ui.warn(_('unexpected svn output:\n'))
1226 1229 self.ui.warn(output)
1227 1230 raise util.Abort(_('unable to cope with svn output'))
1228 1231 if commit.rev:
1229 1232 self.run('propset', 'hg:convert-rev', commit.rev,
1230 1233 revprop=True, revision=rev)
1231 1234 if commit.branch and commit.branch != 'default':
1232 1235 self.run('propset', 'hg:convert-branch', commit.branch,
1233 1236 revprop=True, revision=rev)
1234 1237 for parent in parents:
1235 1238 self.addchild(parent, rev)
1236 1239 return self.revid(rev)
1237 1240 finally:
1238 1241 os.unlink(messagefile)
1239 1242
1240 1243 def puttags(self, tags):
1241 1244 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1242 1245 return None, None
1243 1246
1244 1247 def hascommit(self, rev):
1245 1248 # This is not correct as one can convert to an existing subversion
1246 1249 # repository and childmap would not list all revisions. Too bad.
1247 1250 if rev in self.childmap:
1248 1251 return True
1249 1252 raise util.Abort(_('splice map revision %s not found in subversion '
1250 1253 'child map (revision lookups are not implemented)')
1251 1254 % rev)
@@ -1,461 +1,468 b''
1 1
2 2 $ "$TESTDIR/hghave" cvs || exit 80
3 3 $ cvscall()
4 4 > {
5 5 > cvs -f "$@"
6 6 > }
7 7 $ hgcat()
8 8 > {
9 9 > hg --cwd src-hg cat -r tip "$1"
10 10 > }
11 11 $ echo "[extensions]" >> $HGRCPATH
12 12 $ echo "convert = " >> $HGRCPATH
13 13 $ echo "graphlog = " >> $HGRCPATH
14 14 $ cat > cvshooks.py <<EOF
15 15 > def cvslog(ui,repo,hooktype,log):
16 16 > print "%s hook: %d entries"%(hooktype,len(log))
17 17 >
18 18 > def cvschangesets(ui,repo,hooktype,changesets):
19 19 > print "%s hook: %d changesets"%(hooktype,len(changesets))
20 20 > EOF
21 21 $ hookpath=`pwd`
22 22 $ echo "[hooks]" >> $HGRCPATH
23 23 $ echo "cvslog=python:$hookpath/cvshooks.py:cvslog" >> $HGRCPATH
24 24 $ echo "cvschangesets=python:$hookpath/cvshooks.py:cvschangesets" >> $HGRCPATH
25 25
26 26 create cvs repository
27 27
28 28 $ mkdir cvsrepo
29 29 $ cd cvsrepo
30 30 $ CVSROOT=`pwd`
31 31 $ export CVSROOT
32 32 $ CVS_OPTIONS=-f
33 33 $ export CVS_OPTIONS
34 34 $ cd ..
35 35 $ cvscall -q -d "$CVSROOT" init
36 36
37 37 create source directory
38 38
39 39 $ mkdir src-temp
40 40 $ cd src-temp
41 41 $ echo a > a
42 42 $ mkdir b
43 43 $ cd b
44 44 $ echo c > c
45 45 $ cd ..
46 46
47 47 import source directory
48 48
49 49 $ cvscall -q import -m import src INITIAL start
50 50 N src/a
51 51 N src/b/c
52 52
53 53 No conflicts created by this import
54 54
55 55 $ cd ..
56 56
57 57 checkout source directory
58 58
59 59 $ cvscall -q checkout src
60 60 U src/a
61 61 U src/b/c
62 62
63 63 commit a new revision changing b/c
64 64
65 65 $ cd src
66 66 $ sleep 1
67 67 $ echo c >> b/c
68 68 $ cvscall -q commit -mci0 . | grep '<--'
69 69 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
70 70 $ cd ..
71 71
72 convert fresh repo
72 convert fresh repo and also check localtimezone option
73
74 NOTE: This doesn't check all time zones -- it merely determines that
75 the configuration option is taking effect.
73 76
74 $ hg convert src src-hg
77 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
78 since it does not use DST (unlike other U.S. time zones) and is always
79 a fixed difference from UTC.
80
81 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
75 82 initializing destination src-hg repository
76 83 connecting to $TESTTMP/cvsrepo
77 84 scanning source...
78 85 collecting CVS rlog
79 86 5 log entries
80 87 cvslog hook: 5 entries
81 88 creating changesets
82 89 3 changeset entries
83 90 cvschangesets hook: 3 changesets
84 91 sorting...
85 92 converting...
86 93 2 Initial revision
87 94 1 import
88 95 0 ci0
89 96 updating tags
90 97 $ hgcat a
91 98 a
92 99 $ hgcat b/c
93 100 c
94 101 c
95 102
96 103 convert fresh repo with --filemap
97 104
98 105 $ echo include b/c > filemap
99 106 $ hg convert --filemap filemap src src-filemap
100 107 initializing destination src-filemap repository
101 108 connecting to $TESTTMP/cvsrepo
102 109 scanning source...
103 110 collecting CVS rlog
104 111 5 log entries
105 112 cvslog hook: 5 entries
106 113 creating changesets
107 114 3 changeset entries
108 115 cvschangesets hook: 3 changesets
109 116 sorting...
110 117 converting...
111 118 2 Initial revision
112 119 1 import
113 120 filtering out empty revision
114 121 repository tip rolled back to revision 0 (undo commit)
115 122 0 ci0
116 123 updating tags
117 124 $ hgcat b/c
118 125 c
119 126 c
120 127 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
121 128 2 update tags files: .hgtags
122 129 1 ci0 files: b/c
123 130 0 Initial revision files: b/c
124 131
125 132 convert full repository (issue1649)
126 133
127 134 $ cvscall -q -d "$CVSROOT" checkout -d srcfull "." | grep -v CVSROOT
128 135 U srcfull/src/a
129 136 U srcfull/src/b/c
130 137 $ ls srcfull
131 138 CVS
132 139 CVSROOT
133 140 src
134 141 $ hg convert srcfull srcfull-hg \
135 142 > | grep -v 'log entries' | grep -v 'hook:' \
136 143 > | grep -v '^[0-3] .*' # filter instable changeset order
137 144 initializing destination srcfull-hg repository
138 145 connecting to $TESTTMP/cvsrepo
139 146 scanning source...
140 147 collecting CVS rlog
141 148 creating changesets
142 149 4 changeset entries
143 150 sorting...
144 151 converting...
145 152 updating tags
146 153 $ hg cat -r tip --cwd srcfull-hg src/a
147 154 a
148 155 $ hg cat -r tip --cwd srcfull-hg src/b/c
149 156 c
150 157 c
151 158
152 159 commit new file revisions
153 160
154 161 $ cd src
155 162 $ echo a >> a
156 163 $ echo c >> b/c
157 164 $ cvscall -q commit -mci1 . | grep '<--'
158 165 $TESTTMP/cvsrepo/src/a,v <-- a
159 166 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
160 167 $ cd ..
161 168
162 169 convert again
163 170
164 $ hg convert src src-hg
171 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
165 172 connecting to $TESTTMP/cvsrepo
166 173 scanning source...
167 174 collecting CVS rlog
168 175 7 log entries
169 176 cvslog hook: 7 entries
170 177 creating changesets
171 178 4 changeset entries
172 179 cvschangesets hook: 4 changesets
173 180 sorting...
174 181 converting...
175 182 0 ci1
176 183 $ hgcat a
177 184 a
178 185 a
179 186 $ hgcat b/c
180 187 c
181 188 c
182 189 c
183 190
184 191 convert again with --filemap
185 192
186 193 $ hg convert --filemap filemap src src-filemap
187 194 connecting to $TESTTMP/cvsrepo
188 195 scanning source...
189 196 collecting CVS rlog
190 197 7 log entries
191 198 cvslog hook: 7 entries
192 199 creating changesets
193 200 4 changeset entries
194 201 cvschangesets hook: 4 changesets
195 202 sorting...
196 203 converting...
197 204 0 ci1
198 205 $ hgcat b/c
199 206 c
200 207 c
201 208 c
202 209 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
203 210 3 ci1 files: b/c
204 211 2 update tags files: .hgtags
205 212 1 ci0 files: b/c
206 213 0 Initial revision files: b/c
207 214
208 215 commit branch
209 216
210 217 $ cd src
211 218 $ cvs -q update -r1.1 b/c
212 219 U b/c
213 220 $ cvs -q tag -b branch
214 221 T a
215 222 T b/c
216 223 $ cvs -q update -r branch > /dev/null
217 224 $ echo d >> b/c
218 225 $ cvs -q commit -mci2 . | grep '<--'
219 226 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
220 227 $ cd ..
221 228
222 229 convert again
223 230
224 $ hg convert src src-hg
231 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True src src-hg
225 232 connecting to $TESTTMP/cvsrepo
226 233 scanning source...
227 234 collecting CVS rlog
228 235 8 log entries
229 236 cvslog hook: 8 entries
230 237 creating changesets
231 238 5 changeset entries
232 239 cvschangesets hook: 5 changesets
233 240 sorting...
234 241 converting...
235 242 0 ci2
236 243 $ hgcat b/c
237 244 c
238 245 d
239 246
240 247 convert again with --filemap
241 248
242 $ hg convert --filemap filemap src src-filemap
249 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True --filemap filemap src src-filemap
243 250 connecting to $TESTTMP/cvsrepo
244 251 scanning source...
245 252 collecting CVS rlog
246 253 8 log entries
247 254 cvslog hook: 8 entries
248 255 creating changesets
249 256 5 changeset entries
250 257 cvschangesets hook: 5 changesets
251 258 sorting...
252 259 converting...
253 260 0 ci2
254 261 $ hgcat b/c
255 262 c
256 263 d
257 264 $ hg -R src-filemap log --template '{rev} {desc} files: {files}\n'
258 265 4 ci2 files: b/c
259 266 3 ci1 files: b/c
260 267 2 update tags files: .hgtags
261 268 1 ci0 files: b/c
262 269 0 Initial revision files: b/c
263 270
264 271 commit a new revision with funny log message
265 272
266 273 $ cd src
267 274 $ sleep 1
268 275 $ echo e >> a
269 276 $ cvscall -q commit -m'funny
270 277 > ----------------------------
271 278 > log message' . | grep '<--' |\
272 279 > sed -e 's:.*src/\(.*\),v.*:checking in src/\1,v:g'
273 280 checking in src/a,v
274 281
275 282 commit new file revisions with some fuzz
276 283
277 284 $ sleep 1
278 285 $ echo f >> a
279 286 $ cvscall -q commit -mfuzzy . | grep '<--'
280 287 $TESTTMP/cvsrepo/src/a,v <-- a
281 288 $ sleep 4 # the two changes will be split if fuzz < 4
282 289 $ echo g >> b/c
283 290 $ cvscall -q commit -mfuzzy . | grep '<--'
284 291 $TESTTMP/cvsrepo/src/b/c,v <-- *c (glob)
285 292 $ cd ..
286 293
287 294 convert again
288 295
289 $ hg convert --config convert.cvsps.fuzz=2 src src-hg
296 $ TZ=US/Hawaii hg convert --config convert.cvsps.fuzz=2 --config convert.localtimezone=True src src-hg
290 297 connecting to $TESTTMP/cvsrepo
291 298 scanning source...
292 299 collecting CVS rlog
293 300 11 log entries
294 301 cvslog hook: 11 entries
295 302 creating changesets
296 303 8 changeset entries
297 304 cvschangesets hook: 8 changesets
298 305 sorting...
299 306 converting...
300 307 2 funny
301 308 1 fuzzy
302 309 0 fuzzy
303 $ hg -R src-hg glog --template '{rev} ({branches}) {desc} files: {files}\n'
304 o 8 (branch) fuzzy files: b/c
310 $ hg -R src-hg glog --template '{rev} ({branches}) {desc} date: {date|date} files: {files}\n'
311 o 8 (branch) fuzzy date: * -1000 files: b/c (glob)
305 312 |
306 o 7 (branch) fuzzy files: a
313 o 7 (branch) fuzzy date: * -1000 files: a (glob)
307 314 |
308 315 o 6 (branch) funny
309 316 | ----------------------------
310 | log message files: a
311 o 5 (branch) ci2 files: b/c
317 | log message date: * -1000 files: a (glob)
318 o 5 (branch) ci2 date: * -1000 files: b/c (glob)
312 319
313 o 4 () ci1 files: a b/c
320 o 4 () ci1 date: * -1000 files: a b/c (glob)
314 321 |
315 o 3 () update tags files: .hgtags
322 o 3 () update tags date: * +0000 files: .hgtags (glob)
316 323 |
317 o 2 () ci0 files: b/c
324 o 2 () ci0 date: * -1000 files: b/c (glob)
318 325 |
319 | o 1 (INITIAL) import files:
326 | o 1 (INITIAL) import date: * -1000 files: (glob)
320 327 |/
321 o 0 () Initial revision files: a b/c
328 o 0 () Initial revision date: * -1000 files: a b/c (glob)
322 329
323 330
324 331 testing debugcvsps
325 332
326 333 $ cd src
327 334 $ hg debugcvsps --fuzz=2
328 335 collecting CVS rlog
329 336 11 log entries
330 337 cvslog hook: 11 entries
331 338 creating changesets
332 339 10 changeset entries
333 340 cvschangesets hook: 10 changesets
334 341 ---------------------
335 342 PatchSet 1
336 343 Date: * (glob)
337 344 Author: * (glob)
338 345 Branch: HEAD
339 346 Tag: (none)
340 347 Branchpoints: INITIAL
341 348 Log:
342 349 Initial revision
343 350
344 351 Members:
345 352 a:INITIAL->1.1
346 353
347 354 ---------------------
348 355 PatchSet 2
349 356 Date: * (glob)
350 357 Author: * (glob)
351 358 Branch: HEAD
352 359 Tag: (none)
353 360 Branchpoints: INITIAL, branch
354 361 Log:
355 362 Initial revision
356 363
357 364 Members:
358 365 b/c:INITIAL->1.1
359 366
360 367 ---------------------
361 368 PatchSet 3
362 369 Date: * (glob)
363 370 Author: * (glob)
364 371 Branch: INITIAL
365 372 Tag: start
366 373 Log:
367 374 import
368 375
369 376 Members:
370 377 a:1.1->1.1.1.1
371 378 b/c:1.1->1.1.1.1
372 379
373 380 ---------------------
374 381 PatchSet 4
375 382 Date: * (glob)
376 383 Author: * (glob)
377 384 Branch: HEAD
378 385 Tag: (none)
379 386 Log:
380 387 ci0
381 388
382 389 Members:
383 390 b/c:1.1->1.2
384 391
385 392 ---------------------
386 393 PatchSet 5
387 394 Date: * (glob)
388 395 Author: * (glob)
389 396 Branch: HEAD
390 397 Tag: (none)
391 398 Branchpoints: branch
392 399 Log:
393 400 ci1
394 401
395 402 Members:
396 403 a:1.1->1.2
397 404
398 405 ---------------------
399 406 PatchSet 6
400 407 Date: * (glob)
401 408 Author: * (glob)
402 409 Branch: HEAD
403 410 Tag: (none)
404 411 Log:
405 412 ci1
406 413
407 414 Members:
408 415 b/c:1.2->1.3
409 416
410 417 ---------------------
411 418 PatchSet 7
412 419 Date: * (glob)
413 420 Author: * (glob)
414 421 Branch: branch
415 422 Tag: (none)
416 423 Log:
417 424 ci2
418 425
419 426 Members:
420 427 b/c:1.1->1.1.2.1
421 428
422 429 ---------------------
423 430 PatchSet 8
424 431 Date: * (glob)
425 432 Author: * (glob)
426 433 Branch: branch
427 434 Tag: (none)
428 435 Log:
429 436 funny
430 437 ----------------------------
431 438 log message
432 439
433 440 Members:
434 441 a:1.2->1.2.2.1
435 442
436 443 ---------------------
437 444 PatchSet 9
438 445 Date: * (glob)
439 446 Author: * (glob)
440 447 Branch: branch
441 448 Tag: (none)
442 449 Log:
443 450 fuzzy
444 451
445 452 Members:
446 453 a:1.2.2.1->1.2.2.2
447 454
448 455 ---------------------
449 456 PatchSet 10
450 457 Date: * (glob)
451 458 Author: * (glob)
452 459 Branch: branch
453 460 Tag: (none)
454 461 Log:
455 462 fuzzy
456 463
457 464 Members:
458 465 b/c:1.1.2.1->1.1.2.2
459 466
460 467
461 468 $ cd ..
@@ -1,203 +1,210 b''
1 1
2 2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
3 3
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [extensions]
6 6 > convert =
7 7 > graphlog =
8 8 > [convert]
9 9 > svn.trunk = mytrunk
10 10 > EOF
11 11
12 12 $ svnadmin create svn-repo
13 13 $ SVNREPOPATH=`pwd`/svn-repo
14 14 #if windows
15 15 $ SVNREPOURL=file:///`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
16 16 #else
17 17 $ SVNREPOURL=file://`python -c "import urllib, sys; sys.stdout.write(urllib.quote(sys.argv[1]))" "$SVNREPOPATH"`
18 18 #endif
19 19
20 20 Now test that it works with trunk/tags layout, but no branches yet.
21 21
22 22 Initial svn import
23 23
24 24 $ mkdir projB
25 25 $ cd projB
26 26 $ mkdir mytrunk
27 27 $ mkdir tags
28 28 $ cd ..
29 29
30 30 $ svn import -m "init projB" projB "$SVNREPOURL/proj%20B" | sort
31 31
32 32 Adding projB/mytrunk (glob)
33 33 Adding projB/tags (glob)
34 34 Committed revision 1.
35 35
36 36 Update svn repository
37 37
38 38 $ svn co "$SVNREPOURL/proj%20B/mytrunk" B
39 39 Checked out revision 1.
40 40 $ cd B
41 41 $ echo hello > 'letter .txt'
42 42 $ svn add 'letter .txt'
43 43 A letter .txt
44 44 $ svn ci -m hello
45 45 Adding letter .txt
46 46 Transmitting file data .
47 47 Committed revision 2.
48 48
49 49 $ "$TESTDIR/svn-safe-append.py" world 'letter .txt'
50 50 $ svn ci -m world
51 51 Sending letter .txt
52 52 Transmitting file data .
53 53 Committed revision 3.
54 54
55 55 $ svn copy -m "tag v0.1" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.1"
56 56
57 57 Committed revision 4.
58 58
59 59 $ "$TESTDIR/svn-safe-append.py" 'nice day today!' 'letter .txt'
60 60 $ svn ci -m "nice day"
61 61 Sending letter .txt
62 62 Transmitting file data .
63 63 Committed revision 5.
64 64 $ cd ..
65 65
66 Convert to hg once
66 Convert to hg once and also test localtimezone option
67
68 NOTE: This doesn't check all time zones -- it merely determines that
69 the configuration option is taking effect.
67 70
68 $ hg convert "$SVNREPOURL/proj%20B" B-hg
71 An arbitrary (U.S.) time zone is used here. TZ=US/Hawaii is selected
72 since it does not use DST (unlike other U.S. time zones) and is always
73 a fixed difference from UTC.
74
75 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
69 76 initializing destination B-hg repository
70 77 scanning source...
71 78 sorting...
72 79 converting...
73 80 3 init projB
74 81 2 hello
75 82 1 world
76 83 0 nice day
77 84 updating tags
78 85
79 86 Update svn repository again
80 87
81 88 $ cd B
82 89 $ "$TESTDIR/svn-safe-append.py" "see second letter" 'letter .txt'
83 90 $ echo "nice to meet you" > letter2.txt
84 91 $ svn add letter2.txt
85 92 A letter2.txt
86 93 $ svn ci -m "second letter"
87 94 Sending letter .txt
88 95 Adding letter2.txt
89 96 Transmitting file data ..
90 97 Committed revision 6.
91 98
92 99 $ svn copy -m "tag v0.2" "$SVNREPOURL/proj%20B/mytrunk" "$SVNREPOURL/proj%20B/tags/v0.2"
93 100
94 101 Committed revision 7.
95 102
96 103 $ "$TESTDIR/svn-safe-append.py" "blah-blah-blah" letter2.txt
97 104 $ svn ci -m "work in progress"
98 105 Sending letter2.txt
99 106 Transmitting file data .
100 107 Committed revision 8.
101 108 $ cd ..
102 109
103 110 $ hg convert -s svn "$SVNREPOURL/proj%20B/non-existent-path" dest
104 111 initializing destination dest repository
105 112 abort: no revision found in module /proj B/non-existent-path
106 113 [255]
107 114
108 115 ########################################
109 116
110 117 Test incremental conversion
111 118
112 $ hg convert "$SVNREPOURL/proj%20B" B-hg
119 $ TZ=US/Hawaii hg convert --config convert.localtimezone=True "$SVNREPOURL/proj%20B" B-hg
113 120 scanning source...
114 121 sorting...
115 122 converting...
116 123 1 second letter
117 124 0 work in progress
118 125 updating tags
119 126
120 127 $ cd B-hg
121 $ hg glog --template '{rev} {desc|firstline} files: {files}\n'
122 o 7 update tags files: .hgtags
128 $ hg glog --template '{rev} {desc|firstline} date: {date|date} files: {files}\n'
129 o 7 update tags date: * +0000 files: .hgtags (glob)
123 130 |
124 o 6 work in progress files: letter2.txt
131 o 6 work in progress date: * -1000 files: letter2.txt (glob)
125 132 |
126 o 5 second letter files: letter .txt letter2.txt
133 o 5 second letter date: * -1000 files: letter .txt letter2.txt (glob)
127 134 |
128 o 4 update tags files: .hgtags
135 o 4 update tags date: * +0000 files: .hgtags (glob)
129 136 |
130 o 3 nice day files: letter .txt
137 o 3 nice day date: * -1000 files: letter .txt (glob)
131 138 |
132 o 2 world files: letter .txt
139 o 2 world date: * -1000 files: letter .txt (glob)
133 140 |
134 o 1 hello files: letter .txt
141 o 1 hello date: * -1000 files: letter .txt (glob)
135 142 |
136 o 0 init projB files:
143 o 0 init projB date: * -1000 files: (glob)
137 144
138 145 $ hg tags -q
139 146 tip
140 147 v0.2
141 148 v0.1
142 149 $ cd ..
143 150
144 151 Test filemap
145 152 $ echo 'include letter2.txt' > filemap
146 153 $ hg convert --filemap filemap "$SVNREPOURL/proj%20B/mytrunk" fmap
147 154 initializing destination fmap repository
148 155 scanning source...
149 156 sorting...
150 157 converting...
151 158 5 init projB
152 159 4 hello
153 160 3 world
154 161 2 nice day
155 162 1 second letter
156 163 0 work in progress
157 164 $ hg -R fmap branch -q
158 165 default
159 166 $ hg glog -R fmap --template '{rev} {desc|firstline} files: {files}\n'
160 167 o 1 work in progress files: letter2.txt
161 168 |
162 169 o 0 second letter files: letter2.txt
163 170
164 171
165 172 Test stop revision
166 173 $ hg convert --rev 1 "$SVNREPOURL/proj%20B/mytrunk" stoprev
167 174 initializing destination stoprev repository
168 175 scanning source...
169 176 sorting...
170 177 converting...
171 178 0 init projB
172 179 $ hg -R stoprev branch -q
173 180 default
174 181
175 182 Check convert_revision extra-records.
176 183 This is also the only place testing more than one extra field in a revision.
177 184
178 185 $ cd stoprev
179 186 $ hg tip --debug | grep extra
180 187 extra: branch=default
181 188 extra: convert_revision=svn:........-....-....-....-............/proj B/mytrunk@1 (re)
182 189 $ cd ..
183 190
184 191 Test converting empty heads (issue3347)
185 192
186 193 $ svnadmin create svn-empty
187 194 $ svnadmin load -q svn-empty < "$TESTDIR/svn/empty.svndump"
188 195 $ hg --config convert.svn.trunk= convert svn-empty
189 196 assuming destination svn-empty-hg
190 197 initializing destination svn-empty-hg repository
191 198 scanning source...
192 199 sorting...
193 200 converting...
194 201 1 init projA
195 202 0 adddir
196 203 $ hg --config convert.svn.trunk= convert "$SVNREPOURL/../svn-empty/trunk"
197 204 assuming destination trunk-hg
198 205 initializing destination trunk-hg repository
199 206 scanning source...
200 207 sorting...
201 208 converting...
202 209 1 init projA
203 210 0 adddir
@@ -1,447 +1,455 b''
1 1 $ cat >> $HGRCPATH <<EOF
2 2 > [extensions]
3 3 > convert=
4 4 > [convert]
5 5 > hg.saverev=False
6 6 > EOF
7 7 $ hg help convert
8 8 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
9 9
10 10 convert a foreign SCM repository to a Mercurial one.
11 11
12 12 Accepted source formats [identifiers]:
13 13
14 14 - Mercurial [hg]
15 15 - CVS [cvs]
16 16 - Darcs [darcs]
17 17 - git [git]
18 18 - Subversion [svn]
19 19 - Monotone [mtn]
20 20 - GNU Arch [gnuarch]
21 21 - Bazaar [bzr]
22 22 - Perforce [p4]
23 23
24 24 Accepted destination formats [identifiers]:
25 25
26 26 - Mercurial [hg]
27 27 - Subversion [svn] (history on branches is not preserved)
28 28
29 29 If no revision is given, all revisions will be converted. Otherwise,
30 30 convert will only import up to the named revision (given in a format
31 31 understood by the source).
32 32
33 33 If no destination directory name is specified, it defaults to the basename
34 34 of the source with "-hg" appended. If the destination repository doesn't
35 35 exist, it will be created.
36 36
37 37 By default, all sources except Mercurial will use --branchsort. Mercurial
38 38 uses --sourcesort to preserve original revision numbers order. Sort modes
39 39 have the following effects:
40 40
41 41 --branchsort convert from parent to child revision when possible, which
42 42 means branches are usually converted one after the other.
43 43 It generates more compact repositories.
44 44 --datesort sort revisions by date. Converted repositories have good-
45 45 looking changelogs but are often an order of magnitude
46 46 larger than the same ones generated by --branchsort.
47 47 --sourcesort try to preserve source revisions order, only supported by
48 48 Mercurial sources.
49 49
50 50 If "REVMAP" isn't given, it will be put in a default location
51 51 ("<dest>/.hg/shamap" by default). The "REVMAP" is a simple text file that
52 52 maps each source commit ID to the destination ID for that revision, like
53 53 so:
54 54
55 55 <source ID> <destination ID>
56 56
57 57 If the file doesn't exist, it's automatically created. It's updated on
58 58 each commit copied, so "hg convert" can be interrupted and can be run
59 59 repeatedly to copy new commits.
60 60
61 61 The authormap is a simple text file that maps each source commit author to
62 62 a destination commit author. It is handy for source SCMs that use unix
63 63 logins to identify authors (e.g.: CVS). One line per author mapping and
64 64 the line format is:
65 65
66 66 source author = destination author
67 67
68 68 Empty lines and lines starting with a "#" are ignored.
69 69
70 70 The filemap is a file that allows filtering and remapping of files and
71 71 directories. Each line can contain one of the following directives:
72 72
73 73 include path/to/file-or-dir
74 74
75 75 exclude path/to/file-or-dir
76 76
77 77 rename path/to/source path/to/destination
78 78
79 79 Comment lines start with "#". A specified path matches if it equals the
80 80 full relative name of a file or one of its parent directories. The
81 81 "include" or "exclude" directive with the longest matching path applies,
82 82 so line order does not matter.
83 83
84 84 The "include" directive causes a file, or all files under a directory, to
85 85 be included in the destination repository, and the exclusion of all other
86 86 files and directories not explicitly included. The "exclude" directive
87 87 causes files or directories to be omitted. The "rename" directive renames
88 88 a file or directory if it is converted. To rename from a subdirectory into
89 89 the root of the repository, use "." as the path to rename to.
90 90
91 91 The splicemap is a file that allows insertion of synthetic history,
92 92 letting you specify the parents of a revision. This is useful if you want
93 93 to e.g. give a Subversion merge two parents, or graft two disconnected
94 94 series of history together. Each entry contains a key, followed by a
95 95 space, followed by one or two comma-separated values:
96 96
97 97 key parent1, parent2
98 98
99 99 The key is the revision ID in the source revision control system whose
100 100 parents should be modified (same format as a key in .hg/shamap). The
101 101 values are the revision IDs (in either the source or destination revision
102 102 control system) that should be used as the new parents for that node. For
103 103 example, if you have merged "release-1.0" into "trunk", then you should
104 104 specify the revision on "trunk" as the first parent and the one on the
105 105 "release-1.0" branch as the second.
106 106
107 107 The branchmap is a file that allows you to rename a branch when it is
108 108 being brought in from whatever external repository. When used in
109 109 conjunction with a splicemap, it allows for a powerful combination to help
110 110 fix even the most badly mismanaged repositories and turn them into nicely
111 111 structured Mercurial repositories. The branchmap contains lines of the
112 112 form:
113 113
114 114 original_branch_name new_branch_name
115 115
116 116 where "original_branch_name" is the name of the branch in the source
117 117 repository, and "new_branch_name" is the name of the branch is the
118 118 destination repository. No whitespace is allowed in the branch names. This
119 119 can be used to (for instance) move code in one repository from "default"
120 120 to a named branch.
121 121
122 122 Mercurial Source
123 123 ################
124 124
125 125 The Mercurial source recognizes the following configuration options, which
126 126 you can set on the command line with "--config":
127 127
128 128 convert.hg.ignoreerrors
129 129 ignore integrity errors when reading. Use it to fix
130 130 Mercurial repositories with missing revlogs, by converting
131 131 from and to Mercurial. Default is False.
132 132 convert.hg.saverev
133 133 store original revision ID in changeset (forces target IDs
134 134 to change). It takes a boolean argument and defaults to
135 135 False.
136 136 convert.hg.startrev
137 137 convert start revision and its descendants. It takes a hg
138 138 revision identifier and defaults to 0.
139 139
140 140 CVS Source
141 141 ##########
142 142
143 143 CVS source will use a sandbox (i.e. a checked-out copy) from CVS to
144 144 indicate the starting point of what will be converted. Direct access to
145 145 the repository files is not needed, unless of course the repository is
146 146 ":local:". The conversion uses the top level directory in the sandbox to
147 147 find the CVS repository, and then uses CVS rlog commands to find files to
148 148 convert. This means that unless a filemap is given, all files under the
149 149 starting directory will be converted, and that any directory
150 150 reorganization in the CVS sandbox is ignored.
151 151
152 152 The following options can be used with "--config":
153 153
154 154 convert.cvsps.cache
155 155 Set to False to disable remote log caching, for testing and
156 156 debugging purposes. Default is True.
157 157 convert.cvsps.fuzz
158 158 Specify the maximum time (in seconds) that is allowed
159 159 between commits with identical user and log message in a
160 160 single changeset. When very large files were checked in as
161 161 part of a changeset then the default may not be long enough.
162 162 The default is 60.
163 163 convert.cvsps.mergeto
164 164 Specify a regular expression to which commit log messages
165 165 are matched. If a match occurs, then the conversion process
166 166 will insert a dummy revision merging the branch on which
167 167 this log message occurs to the branch indicated in the
168 168 regex. Default is "{{mergetobranch ([-\w]+)}}"
169 169 convert.cvsps.mergefrom
170 170 Specify a regular expression to which commit log messages
171 171 are matched. If a match occurs, then the conversion process
172 172 will add the most recent revision on the branch indicated in
173 173 the regex as the second parent of the changeset. Default is
174 174 "{{mergefrombranch ([-\w]+)}}"
175 convert.localtimezone
176 use local time (as determined by the TZ environment
177 variable) for changeset date/times. The default is False
178 (use UTC).
175 179 hook.cvslog Specify a Python function to be called at the end of
176 180 gathering the CVS log. The function is passed a list with
177 181 the log entries, and can modify the entries in-place, or add
178 182 or delete them.
179 183 hook.cvschangesets
180 184 Specify a Python function to be called after the changesets
181 185 are calculated from the CVS log. The function is passed a
182 186 list with the changeset entries, and can modify the
183 187 changesets in-place, or add or delete them.
184 188
185 189 An additional "debugcvsps" Mercurial command allows the builtin changeset
186 190 merging code to be run without doing a conversion. Its parameters and
187 191 output are similar to that of cvsps 2.1. Please see the command help for
188 192 more details.
189 193
190 194 Subversion Source
191 195 #################
192 196
193 197 Subversion source detects classical trunk/branches/tags layouts. By
194 198 default, the supplied "svn://repo/path/" source URL is converted as a
195 199 single branch. If "svn://repo/path/trunk" exists it replaces the default
196 200 branch. If "svn://repo/path/branches" exists, its subdirectories are
197 201 listed as possible branches. If "svn://repo/path/tags" exists, it is
198 202 looked for tags referencing converted branches. Default "trunk",
199 203 "branches" and "tags" values can be overridden with following options. Set
200 204 them to paths relative to the source URL, or leave them blank to disable
201 205 auto detection.
202 206
203 207 The following options can be set with "--config":
204 208
205 209 convert.svn.branches
206 210 specify the directory containing branches. The default is
207 211 "branches".
208 212 convert.svn.tags
209 213 specify the directory containing tags. The default is
210 214 "tags".
211 215 convert.svn.trunk
212 216 specify the name of the trunk branch. The default is
213 217 "trunk".
218 convert.localtimezone
219 use local time (as determined by the TZ environment
220 variable) for changeset date/times. The default is False
221 (use UTC).
214 222
215 223 Source history can be retrieved starting at a specific revision, instead
216 224 of being integrally converted. Only single branch conversions are
217 225 supported.
218 226
219 227 convert.svn.startrev
220 228 specify start Subversion revision number. The default is 0.
221 229
222 230 Perforce Source
223 231 ###############
224 232
225 233 The Perforce (P4) importer can be given a p4 depot path or a client
226 234 specification as source. It will convert all files in the source to a flat
227 235 Mercurial repository, ignoring labels, branches and integrations. Note
228 236 that when a depot path is given you then usually should specify a target
229 237 directory, because otherwise the target may be named "...-hg".
230 238
231 239 It is possible to limit the amount of source history to be converted by
232 240 specifying an initial Perforce revision:
233 241
234 242 convert.p4.startrev
235 243 specify initial Perforce revision (a Perforce changelist
236 244 number).
237 245
238 246 Mercurial Destination
239 247 #####################
240 248
241 249 The following options are supported:
242 250
243 251 convert.hg.clonebranches
244 252 dispatch source branches in separate clones. The default is
245 253 False.
246 254 convert.hg.tagsbranch
247 255 branch name for tag revisions, defaults to "default".
248 256 convert.hg.usebranchnames
249 257 preserve branch names. The default is True.
250 258
251 259 options:
252 260
253 261 -s --source-type TYPE source repository type
254 262 -d --dest-type TYPE destination repository type
255 263 -r --rev REV import up to target revision REV
256 264 -A --authormap FILE remap usernames using this file
257 265 --filemap FILE remap file names using contents of file
258 266 --splicemap FILE splice synthesized history into place
259 267 --branchmap FILE change branch names while converting
260 268 --branchsort try to sort changesets by branches
261 269 --datesort try to sort changesets by date
262 270 --sourcesort preserve source changesets order
263 271
264 272 use "hg -v help convert" to show the global options
265 273 $ hg init a
266 274 $ cd a
267 275 $ echo a > a
268 276 $ hg ci -d'0 0' -Ama
269 277 adding a
270 278 $ hg cp a b
271 279 $ hg ci -d'1 0' -mb
272 280 $ hg rm a
273 281 $ hg ci -d'2 0' -mc
274 282 $ hg mv b a
275 283 $ hg ci -d'3 0' -md
276 284 $ echo a >> a
277 285 $ hg ci -d'4 0' -me
278 286 $ cd ..
279 287 $ hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded'
280 288 assuming destination a-hg
281 289 initializing destination a-hg repository
282 290 scanning source...
283 291 sorting...
284 292 converting...
285 293 4 a
286 294 3 b
287 295 2 c
288 296 1 d
289 297 0 e
290 298 $ hg --cwd a-hg pull ../a
291 299 pulling from ../a
292 300 searching for changes
293 301 no changes found
294 302
295 303 conversion to existing file should fail
296 304
297 305 $ touch bogusfile
298 306 $ hg convert a bogusfile
299 307 initializing destination bogusfile repository
300 308 abort: cannot create new bundle repository
301 309 [255]
302 310
303 311 #if unix-permissions
304 312
305 313 conversion to dir without permissions should fail
306 314
307 315 $ mkdir bogusdir
308 316 $ chmod 000 bogusdir
309 317
310 318 $ hg convert a bogusdir
311 319 abort: Permission denied: bogusdir
312 320 [255]
313 321
314 322 user permissions should succeed
315 323
316 324 $ chmod 700 bogusdir
317 325 $ hg convert a bogusdir
318 326 initializing destination bogusdir repository
319 327 scanning source...
320 328 sorting...
321 329 converting...
322 330 4 a
323 331 3 b
324 332 2 c
325 333 1 d
326 334 0 e
327 335
328 336 #endif
329 337
330 338 test pre and post conversion actions
331 339
332 340 $ echo 'include b' > filemap
333 341 $ hg convert --debug --filemap filemap a partialb | \
334 342 > grep 'run hg'
335 343 run hg source pre-conversion action
336 344 run hg sink pre-conversion action
337 345 run hg sink post-conversion action
338 346 run hg source post-conversion action
339 347
340 348 converting empty dir should fail "nicely
341 349
342 350 $ mkdir emptydir
343 351
344 352 override $PATH to ensure p4 not visible; use $PYTHON in case we're
345 353 running from a devel copy, not a temp installation
346 354
347 355 $ PATH="$BINDIR" $PYTHON "$BINDIR"/hg convert emptydir
348 356 assuming destination emptydir-hg
349 357 initializing destination emptydir-hg repository
350 358 emptydir does not look like a CVS checkout
351 359 emptydir does not look like a Git repository
352 360 emptydir does not look like a Subversion repository
353 361 emptydir is not a local Mercurial repository
354 362 emptydir does not look like a darcs repository
355 363 emptydir does not look like a monotone repository
356 364 emptydir does not look like a GNU Arch repository
357 365 emptydir does not look like a Bazaar repository
358 366 cannot find required "p4" tool
359 367 abort: emptydir: missing or unsupported repository
360 368 [255]
361 369
362 370 convert with imaginary source type
363 371
364 372 $ hg convert --source-type foo a a-foo
365 373 initializing destination a-foo repository
366 374 abort: foo: invalid source repository type
367 375 [255]
368 376
369 377 convert with imaginary sink type
370 378
371 379 $ hg convert --dest-type foo a a-foo
372 380 abort: foo: invalid destination repository type
373 381 [255]
374 382
375 383 testing: convert must not produce duplicate entries in fncache
376 384
377 385 $ hg convert a b
378 386 initializing destination b repository
379 387 scanning source...
380 388 sorting...
381 389 converting...
382 390 4 a
383 391 3 b
384 392 2 c
385 393 1 d
386 394 0 e
387 395
388 396 contents of fncache file:
389 397
390 398 $ cat b/.hg/store/fncache | sort
391 399 data/a.i
392 400 data/b.i
393 401
394 402 test bogus URL
395 403
396 404 $ hg convert -q bzr+ssh://foobar@selenic.com/baz baz
397 405 abort: bzr+ssh://foobar@selenic.com/baz: missing or unsupported repository
398 406 [255]
399 407
400 408 test revset converted() lookup
401 409
402 410 $ hg --config convert.hg.saverev=True convert a c
403 411 initializing destination c repository
404 412 scanning source...
405 413 sorting...
406 414 converting...
407 415 4 a
408 416 3 b
409 417 2 c
410 418 1 d
411 419 0 e
412 420 $ echo f > c/f
413 421 $ hg -R c ci -d'0 0' -Amf
414 422 adding f
415 423 created new head
416 424 $ hg -R c log -r "converted(09d945a62ce6)"
417 425 changeset: 1:98c3dd46a874
418 426 user: test
419 427 date: Thu Jan 01 00:00:01 1970 +0000
420 428 summary: b
421 429
422 430 $ hg -R c log -r "converted()"
423 431 changeset: 0:31ed57b2037c
424 432 user: test
425 433 date: Thu Jan 01 00:00:00 1970 +0000
426 434 summary: a
427 435
428 436 changeset: 1:98c3dd46a874
429 437 user: test
430 438 date: Thu Jan 01 00:00:01 1970 +0000
431 439 summary: b
432 440
433 441 changeset: 2:3b9ca06ef716
434 442 user: test
435 443 date: Thu Jan 01 00:00:02 1970 +0000
436 444 summary: c
437 445
438 446 changeset: 3:4e0debd37cf2
439 447 user: test
440 448 date: Thu Jan 01 00:00:03 1970 +0000
441 449 summary: d
442 450
443 451 changeset: 4:9de3bc9349c5
444 452 user: test
445 453 date: Thu Jan 01 00:00:04 1970 +0000
446 454 summary: e
447 455
General Comments 0
You need to be logged in to leave comments. Login now