##// END OF EJS Templates
convert: make mapfile handle LF and CRLF shamap (issue1846)
Patrick Mezard -
r9528:314fc589 default
parent child Browse files
Show More
@@ -1,391 +1,391 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, incorporated herein by reference.
7 7
8 8 import base64, errno
9 9 import os
10 10 import cPickle as pickle
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13
14 14 def encodeargs(args):
15 15 def encodearg(s):
16 16 lines = base64.encodestring(s)
17 17 lines = [l.splitlines()[0] for l in lines]
18 18 return ''.join(lines)
19 19
20 20 s = pickle.dumps(args)
21 21 return encodearg(s)
22 22
23 23 def decodeargs(s):
24 24 s = base64.decodestring(s)
25 25 return pickle.loads(s)
26 26
27 27 class MissingTool(Exception): pass
28 28
29 29 def checktool(exe, name=None, abort=True):
30 30 name = name or exe
31 31 if not util.find_exe(exe):
32 32 exc = abort and util.Abort or MissingTool
33 33 raise exc(_('cannot find required "%s" tool') % name)
34 34
35 35 class NoRepo(Exception): pass
36 36
37 37 SKIPREV = 'SKIP'
38 38
39 39 class commit(object):
40 40 def __init__(self, author, date, desc, parents, branch=None, rev=None,
41 41 extra={}, sortkey=None):
42 42 self.author = author or 'unknown'
43 43 self.date = date or '0 0'
44 44 self.desc = desc
45 45 self.parents = parents
46 46 self.branch = branch
47 47 self.rev = rev
48 48 self.extra = extra
49 49 self.sortkey = sortkey
50 50
51 51 class converter_source(object):
52 52 """Conversion source interface"""
53 53
54 54 def __init__(self, ui, path=None, rev=None):
55 55 """Initialize conversion source (or raise NoRepo("message")
56 56 exception if path is not a valid repository)"""
57 57 self.ui = ui
58 58 self.path = path
59 59 self.rev = rev
60 60
61 61 self.encoding = 'utf-8'
62 62
63 63 def before(self):
64 64 pass
65 65
66 66 def after(self):
67 67 pass
68 68
69 69 def setrevmap(self, revmap):
70 70 """set the map of already-converted revisions"""
71 71 pass
72 72
73 73 def getheads(self):
74 74 """Return a list of this repository's heads"""
75 75 raise NotImplementedError()
76 76
77 77 def getfile(self, name, rev):
78 78 """Return file contents as a string. rev is the identifier returned
79 79 by a previous call to getchanges(). Raise IOError to indicate that
80 80 name was deleted in rev.
81 81 """
82 82 raise NotImplementedError()
83 83
84 84 def getmode(self, name, rev):
85 85 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
86 86 returned by a previous call to getchanges().
87 87 """
88 88 raise NotImplementedError()
89 89
90 90 def getchanges(self, version):
91 91 """Returns a tuple of (files, copies).
92 92
93 93 files is a sorted list of (filename, id) tuples for all files
94 94 changed between version and its first parent returned by
95 95 getcommit(). id is the source revision id of the file.
96 96
97 97 copies is a dictionary of dest: source
98 98 """
99 99 raise NotImplementedError()
100 100
101 101 def getcommit(self, version):
102 102 """Return the commit object for version"""
103 103 raise NotImplementedError()
104 104
105 105 def gettags(self):
106 106 """Return the tags as a dictionary of name: revision
107 107
108 108 Tag names must be UTF-8 strings.
109 109 """
110 110 raise NotImplementedError()
111 111
112 112 def recode(self, s, encoding=None):
113 113 if not encoding:
114 114 encoding = self.encoding or 'utf-8'
115 115
116 116 if isinstance(s, unicode):
117 117 return s.encode("utf-8")
118 118 try:
119 119 return s.decode(encoding).encode("utf-8")
120 120 except:
121 121 try:
122 122 return s.decode("latin-1").encode("utf-8")
123 123 except:
124 124 return s.decode(encoding, "replace").encode("utf-8")
125 125
126 126 def getchangedfiles(self, rev, i):
127 127 """Return the files changed by rev compared to parent[i].
128 128
129 129 i is an index selecting one of the parents of rev. The return
130 130 value should be the list of files that are different in rev and
131 131 this parent.
132 132
133 133 If rev has no parents, i is None.
134 134
135 135 This function is only needed to support --filemap
136 136 """
137 137 raise NotImplementedError()
138 138
139 139 def converted(self, rev, sinkrev):
140 140 '''Notify the source that a revision has been converted.'''
141 141 pass
142 142
143 143 def hasnativeorder(self):
144 144 """Return true if this source has a meaningful, native revision
145 145 order. For instance, Mercurial revisions are store sequentially
146 146 while there is no such global ordering with Darcs.
147 147 """
148 148 return False
149 149
150 150 def lookuprev(self, rev):
151 151 """If rev is a meaningful revision reference in source, return
152 152 the referenced identifier in the same format used by getcommit().
153 153 return None otherwise.
154 154 """
155 155 return None
156 156
157 157 class converter_sink(object):
158 158 """Conversion sink (target) interface"""
159 159
160 160 def __init__(self, ui, path):
161 161 """Initialize conversion sink (or raise NoRepo("message")
162 162 exception if path is not a valid repository)
163 163
164 164 created is a list of paths to remove if a fatal error occurs
165 165 later"""
166 166 self.ui = ui
167 167 self.path = path
168 168 self.created = []
169 169
170 170 def getheads(self):
171 171 """Return a list of this repository's heads"""
172 172 raise NotImplementedError()
173 173
174 174 def revmapfile(self):
175 175 """Path to a file that will contain lines
176 176 source_rev_id sink_rev_id
177 177 mapping equivalent revision identifiers for each system."""
178 178 raise NotImplementedError()
179 179
180 180 def authorfile(self):
181 181 """Path to a file that will contain lines
182 182 srcauthor=dstauthor
183 183 mapping equivalent authors identifiers for each system."""
184 184 return None
185 185
186 186 def putcommit(self, files, copies, parents, commit, source, revmap):
187 187 """Create a revision with all changed files listed in 'files'
188 188 and having listed parents. 'commit' is a commit object
189 189 containing at a minimum the author, date, and message for this
190 190 changeset. 'files' is a list of (path, version) tuples,
191 191 'copies' is a dictionary mapping destinations to sources,
192 192 'source' is the source repository, and 'revmap' is a mapfile
193 193 of source revisions to converted revisions. Only getfile(),
194 194 getmode(), and lookuprev() should be called on 'source'.
195 195
196 196 Note that the sink repository is not told to update itself to
197 197 a particular revision (or even what that revision would be)
198 198 before it receives the file data.
199 199 """
200 200 raise NotImplementedError()
201 201
202 202 def puttags(self, tags):
203 203 """Put tags into sink.
204 204
205 205 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
206 206 Return a pair (tag_revision, tag_parent_revision), or (None, None)
207 207 if nothing was changed.
208 208 """
209 209 raise NotImplementedError()
210 210
211 211 def setbranch(self, branch, pbranches):
212 212 """Set the current branch name. Called before the first putcommit
213 213 on the branch.
214 214 branch: branch name for subsequent commits
215 215 pbranches: (converted parent revision, parent branch) tuples"""
216 216 pass
217 217
218 218 def setfilemapmode(self, active):
219 219 """Tell the destination that we're using a filemap
220 220
221 221 Some converter_sources (svn in particular) can claim that a file
222 222 was changed in a revision, even if there was no change. This method
223 223 tells the destination that we're using a filemap and that it should
224 224 filter empty revisions.
225 225 """
226 226 pass
227 227
228 228 def before(self):
229 229 pass
230 230
231 231 def after(self):
232 232 pass
233 233
234 234
235 235 class commandline(object):
236 236 def __init__(self, ui, command):
237 237 self.ui = ui
238 238 self.command = command
239 239
240 240 def prerun(self):
241 241 pass
242 242
243 243 def postrun(self):
244 244 pass
245 245
246 246 def _cmdline(self, cmd, *args, **kwargs):
247 247 cmdline = [self.command, cmd] + list(args)
248 248 for k, v in kwargs.iteritems():
249 249 if len(k) == 1:
250 250 cmdline.append('-' + k)
251 251 else:
252 252 cmdline.append('--' + k.replace('_', '-'))
253 253 try:
254 254 if len(k) == 1:
255 255 cmdline.append('' + v)
256 256 else:
257 257 cmdline[-1] += '=' + v
258 258 except TypeError:
259 259 pass
260 260 cmdline = [util.shellquote(arg) for arg in cmdline]
261 261 if not self.ui.debugflag:
262 262 cmdline += ['2>', util.nulldev]
263 263 cmdline += ['<', util.nulldev]
264 264 cmdline = ' '.join(cmdline)
265 265 return cmdline
266 266
267 267 def _run(self, cmd, *args, **kwargs):
268 268 cmdline = self._cmdline(cmd, *args, **kwargs)
269 269 self.ui.debug(_('running: %s\n') % (cmdline,))
270 270 self.prerun()
271 271 try:
272 272 return util.popen(cmdline)
273 273 finally:
274 274 self.postrun()
275 275
276 276 def run(self, cmd, *args, **kwargs):
277 277 fp = self._run(cmd, *args, **kwargs)
278 278 output = fp.read()
279 279 self.ui.debug(output)
280 280 return output, fp.close()
281 281
282 282 def runlines(self, cmd, *args, **kwargs):
283 283 fp = self._run(cmd, *args, **kwargs)
284 284 output = fp.readlines()
285 285 self.ui.debug(''.join(output))
286 286 return output, fp.close()
287 287
288 288 def checkexit(self, status, output=''):
289 289 if status:
290 290 if output:
291 291 self.ui.warn(_('%s error:\n') % self.command)
292 292 self.ui.warn(output)
293 293 msg = util.explain_exit(status)[0]
294 294 raise util.Abort('%s %s' % (self.command, msg))
295 295
296 296 def run0(self, cmd, *args, **kwargs):
297 297 output, status = self.run(cmd, *args, **kwargs)
298 298 self.checkexit(status, output)
299 299 return output
300 300
301 301 def runlines0(self, cmd, *args, **kwargs):
302 302 output, status = self.runlines(cmd, *args, **kwargs)
303 303 self.checkexit(status, ''.join(output))
304 304 return output
305 305
306 306 def getargmax(self):
307 307 if '_argmax' in self.__dict__:
308 308 return self._argmax
309 309
310 310 # POSIX requires at least 4096 bytes for ARG_MAX
311 311 self._argmax = 4096
312 312 try:
313 313 self._argmax = os.sysconf("SC_ARG_MAX")
314 314 except:
315 315 pass
316 316
317 317 # Windows shells impose their own limits on command line length,
318 318 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
319 319 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
320 320 # details about cmd.exe limitations.
321 321
322 322 # Since ARG_MAX is for command line _and_ environment, lower our limit
323 323 # (and make happy Windows shells while doing this).
324 324
325 325 self._argmax = self._argmax/2 - 1
326 326 return self._argmax
327 327
328 328 def limit_arglist(self, arglist, cmd, *args, **kwargs):
329 329 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
330 330 bytes = 0
331 331 fl = []
332 332 for fn in arglist:
333 333 b = len(fn) + 3
334 334 if bytes + b < limit or len(fl) == 0:
335 335 fl.append(fn)
336 336 bytes += b
337 337 else:
338 338 yield fl
339 339 fl = [fn]
340 340 bytes = b
341 341 if fl:
342 342 yield fl
343 343
344 344 def xargs(self, arglist, cmd, *args, **kwargs):
345 345 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
346 346 self.run0(cmd, *(list(args) + l), **kwargs)
347 347
348 348 class mapfile(dict):
349 349 def __init__(self, ui, path):
350 350 super(mapfile, self).__init__()
351 351 self.ui = ui
352 352 self.path = path
353 353 self.fp = None
354 354 self.order = []
355 355 self._read()
356 356
357 357 def _read(self):
358 358 if not self.path:
359 359 return
360 360 try:
361 361 fp = open(self.path, 'r')
362 362 except IOError, err:
363 363 if err.errno != errno.ENOENT:
364 364 raise
365 365 return
366 366 for i, line in enumerate(fp):
367 367 try:
368 key, value = line[:-1].rsplit(' ', 1)
368 key, value = line.splitlines()[0].rsplit(' ', 1)
369 369 except ValueError:
370 370 raise util.Abort(_('syntax error in %s(%d): key/value pair expected')
371 371 % (self.path, i+1))
372 372 if key not in self:
373 373 self.order.append(key)
374 374 super(mapfile, self).__setitem__(key, value)
375 375 fp.close()
376 376
377 377 def __setitem__(self, key, value):
378 378 if self.fp is None:
379 379 try:
380 380 self.fp = open(self.path, 'a')
381 381 except IOError, err:
382 382 raise util.Abort(_('could not open map file %r: %s') %
383 383 (self.path, err.strerror))
384 384 self.fp.write('%s %s\n' % (key, value))
385 385 self.fp.flush()
386 386 super(mapfile, self).__setitem__(key, value)
387 387
388 388 def close(self):
389 389 if self.fp:
390 390 self.fp.close()
391 391 self.fp = None
@@ -1,69 +1,88 b''
1 1 #!/bin/sh
2 2
3 3 cat >> $HGRCPATH <<EOF
4 4 [extensions]
5 5 convert=
6 6 [convert]
7 7 hg.saverev=False
8 8 EOF
9 9
10 10 hg init orig
11 11 cd orig
12 12
13 13 echo foo > foo
14 14 echo bar > bar
15 15 hg ci -qAm 'add foo bar' -d '0 0'
16 16
17 17 echo >> foo
18 18 hg ci -m 'change foo' -d '1 0'
19 19
20 20 hg up -qC 0
21 21 hg copy --after --force foo bar
22 22 hg copy foo baz
23 23 hg ci -m 'make bar and baz copies of foo' -d '2 0'
24 24
25 25 hg merge
26 26 hg ci -m 'merge local copy' -d '3 0'
27 27
28 28 hg up -C 1
29 29 hg merge 2
30 30 hg ci -m 'merge remote copy' -d '4 0'
31 31
32 32 chmod +x baz
33 33 hg ci -m 'mark baz executable' -d '5 0'
34 34
35 35 cd ..
36 36 hg convert --datesort orig new 2>&1 | grep -v 'subversion python bindings could not be loaded'
37 37 cd new
38 38 hg out ../orig
39 39 cd ..
40 40
41 echo '% check shamap LF and CRLF handling'
42 cat > rewrite.py <<EOF
43 import sys
44 # Interlace LF and CRLF
45 lines = [(l.rstrip() + ((i % 2) and '\n' or '\r\n'))
46 for i, l in enumerate(file(sys.argv[1]))]
47 file(sys.argv[1], 'wb').write(''.join(lines))
48 EOF
49 python rewrite.py new/.hg/shamap
50 cd orig
51 hg up -qC 1
52 echo foo >> foo
53 hg ci -qm 'change foo again'
54 hg up -qC 2
55 echo foo >> foo
56 hg ci -qm 'change foo again again'
57 cd ..
58 hg convert --datesort orig new 2>&1 | grep -v 'subversion python bindings could not be loaded'
59
41 60 echo % init broken repository
42 61 hg init broken
43 62 cd broken
44 63 echo a >> a
45 64 echo b >> b
46 65 hg ci -qAm init
47 66 echo a >> a
48 67 echo b >> b
49 68 hg copy b c
50 69 hg ci -qAm changeall
51 70 hg up -qC 0
52 71 echo bc >> b
53 72 hg ci -m changebagain
54 73 HGMERGE=internal:local hg -q merge
55 74 hg ci -m merge
56 75 hg mv b d
57 76 hg ci -m moveb
58 77 echo % break it
59 78 rm .hg/store/data/b.*
60 79 cd ..
61 80
62 81 hg --config convert.hg.ignoreerrors=True convert broken fixed
63 82 hg -R fixed verify
64 83 echo '% manifest -r 0'
65 84 hg -R fixed manifest -r 0
66 85 echo '% manifest -r tip'
67 86 hg -R fixed manifest -r tip
68 87
69 88 true
@@ -1,46 +1,52 b''
1 1 created new head
2 2 merging baz and foo to baz
3 3 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
4 4 (branch merge, don't forget to commit)
5 5 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
6 6 merging foo and baz to baz
7 7 1 files updated, 1 files merged, 0 files removed, 0 files unresolved
8 8 (branch merge, don't forget to commit)
9 9 created new head
10 10 initializing destination new repository
11 11 scanning source...
12 12 sorting...
13 13 converting...
14 14 5 add foo bar
15 15 4 change foo
16 16 3 make bar and baz copies of foo
17 17 2 merge local copy
18 18 1 merge remote copy
19 19 0 mark baz executable
20 20 comparing with ../orig
21 21 searching for changes
22 22 no changes found
23 % check shamap LF and CRLF handling
24 scanning source...
25 sorting...
26 converting...
27 1 change foo again again
28 0 change foo again
23 29 % init broken repository
24 30 created new head
25 31 % break it
26 32 initializing destination fixed repository
27 33 scanning source...
28 34 sorting...
29 35 converting...
30 36 4 init
31 37 ignoring: data/b.i@1e88685f5dde: no match found
32 38 3 changeall
33 39 2 changebagain
34 40 1 merge
35 41 0 moveb
36 42 checking changesets
37 43 checking manifests
38 44 crosschecking files in changesets and manifests
39 45 checking files
40 46 3 files, 5 changesets, 5 total revisions
41 47 % manifest -r 0
42 48 a
43 49 % manifest -r tip
44 50 a
45 51 c
46 52 d
General Comments 0
You need to be logged in to leave comments. Login now