##// END OF EJS Templates
convert: better mapfile parsing errors (issue1581/1)
Patrick Mezard -
r8047:04c62275 default
parent child Browse files
Show More
@@ -0,0 +1,51
1 #!/bin/sh
2
3 echo "[extensions]" >> $HGRCPATH
4 echo "convert=" >> $HGRCPATH
5 echo 'hgext.graphlog =' >> $HGRCPATH
6
7 glog()
8 {
9 hg glog --template '#rev# "#desc|firstline#" files: #files#\n' "$@"
10 }
11
12 hg init repo1
13 cd repo1
14 echo a > a
15 hg ci -Am adda
16 echo b > b
17 echo a >> a
18 hg ci -Am addb
19 PARENTID1=`hg id --debug -i`
20 echo c > c
21 hg ci -Am addc
22 PARENTID2=`hg id --debug -i`
23 cd ..
24
25 hg init repo2
26 cd repo2
27 echo b > a
28 echo d > d
29 hg ci -Am addaandd
30 CHILDID1=`hg id --debug -i`
31 echo d >> d
32 hg ci -Am changed
33 CHILDID2=`hg id --debug -i`
34 echo e > e
35 hg ci -Am adde
36 cd ..
37
38 echo '% test invalid splicemap'
39 cat > splicemap <<EOF
40 $CHILDID2
41 EOF
42 hg convert --splicemap splicemap repo2 repo1
43
44 echo '% splice repo2 on repo1'
45 cat > splicemap <<EOF
46 $CHILDID1 $PARENTID1
47 $CHILDID2 $PARENTID2,$CHILDID1
48 EOF
49 hg clone repo1 target1
50 hg convert --splicemap splicemap repo2 target1
51 glog -R target1
@@ -0,0 +1,31
1 adding a
2 adding b
3 adding c
4 adding a
5 adding d
6 adding e
7 % test invalid splicemap
8 abort: syntax error in splicemap(1): key/value pair expected
9 % splice repo2 on repo1
10 updating working directory
11 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
12 scanning source...
13 sorting...
14 converting...
15 2 addaandd
16 spliced in ['6d4c2037ddc2cb2627ac3a244ecce35283268f8e'] as parents of 527cdedf31fbd5ea708aa14eeecf53d4676f38db
17 1 changed
18 spliced in ['e55c719b85b60e5102fac26110ba626e7cb6b7dc', '527cdedf31fbd5ea708aa14eeecf53d4676f38db'] as parents of e4ea00df91897da3079a10fab658c1eddba6617b
19 0 adde
20 o 5 "adde" files: e
21 |
22 o 4 "changed" files: d
23 |\
24 | o 3 "addaandd" files: a d
25 | |
26 @ | 2 "addc" files: c
27 |/
28 o 1 "addb" files: a b
29 |
30 o 0 "adda" files: a
31
@@ -1,357 +1,361
1 # common code for the convert extension
1 # common code for the convert extension
2 import base64, errno
2 import base64, errno
3 import os
3 import os
4 import cPickle as pickle
4 import cPickle as pickle
5 from mercurial import util, strutil
5 from mercurial import util, strutil
6 from mercurial.i18n import _
6 from mercurial.i18n import _
7
7
8 def encodeargs(args):
8 def encodeargs(args):
9 def encodearg(s):
9 def encodearg(s):
10 lines = base64.encodestring(s)
10 lines = base64.encodestring(s)
11 lines = [l.splitlines()[0] for l in lines]
11 lines = [l.splitlines()[0] for l in lines]
12 return ''.join(lines)
12 return ''.join(lines)
13
13
14 s = pickle.dumps(args)
14 s = pickle.dumps(args)
15 return encodearg(s)
15 return encodearg(s)
16
16
17 def decodeargs(s):
17 def decodeargs(s):
18 s = base64.decodestring(s)
18 s = base64.decodestring(s)
19 return pickle.loads(s)
19 return pickle.loads(s)
20
20
21 class MissingTool(Exception): pass
21 class MissingTool(Exception): pass
22
22
23 def checktool(exe, name=None, abort=True):
23 def checktool(exe, name=None, abort=True):
24 name = name or exe
24 name = name or exe
25 if not util.find_exe(exe):
25 if not util.find_exe(exe):
26 exc = abort and util.Abort or MissingTool
26 exc = abort and util.Abort or MissingTool
27 raise exc(_('cannot find required "%s" tool') % name)
27 raise exc(_('cannot find required "%s" tool') % name)
28
28
29 class NoRepo(Exception): pass
29 class NoRepo(Exception): pass
30
30
31 SKIPREV = 'SKIP'
31 SKIPREV = 'SKIP'
32
32
33 class commit(object):
33 class commit(object):
34 def __init__(self, author, date, desc, parents, branch=None, rev=None,
34 def __init__(self, author, date, desc, parents, branch=None, rev=None,
35 extra={}):
35 extra={}):
36 self.author = author or 'unknown'
36 self.author = author or 'unknown'
37 self.date = date or '0 0'
37 self.date = date or '0 0'
38 self.desc = desc
38 self.desc = desc
39 self.parents = parents
39 self.parents = parents
40 self.branch = branch
40 self.branch = branch
41 self.rev = rev
41 self.rev = rev
42 self.extra = extra
42 self.extra = extra
43
43
44 class converter_source(object):
44 class converter_source(object):
45 """Conversion source interface"""
45 """Conversion source interface"""
46
46
47 def __init__(self, ui, path=None, rev=None):
47 def __init__(self, ui, path=None, rev=None):
48 """Initialize conversion source (or raise NoRepo("message")
48 """Initialize conversion source (or raise NoRepo("message")
49 exception if path is not a valid repository)"""
49 exception if path is not a valid repository)"""
50 self.ui = ui
50 self.ui = ui
51 self.path = path
51 self.path = path
52 self.rev = rev
52 self.rev = rev
53
53
54 self.encoding = 'utf-8'
54 self.encoding = 'utf-8'
55
55
56 def before(self):
56 def before(self):
57 pass
57 pass
58
58
59 def after(self):
59 def after(self):
60 pass
60 pass
61
61
62 def setrevmap(self, revmap):
62 def setrevmap(self, revmap):
63 """set the map of already-converted revisions"""
63 """set the map of already-converted revisions"""
64 pass
64 pass
65
65
66 def getheads(self):
66 def getheads(self):
67 """Return a list of this repository's heads"""
67 """Return a list of this repository's heads"""
68 raise NotImplementedError()
68 raise NotImplementedError()
69
69
70 def getfile(self, name, rev):
70 def getfile(self, name, rev):
71 """Return file contents as a string. rev is the identifier returned
71 """Return file contents as a string. rev is the identifier returned
72 by a previous call to getchanges().
72 by a previous call to getchanges().
73 """
73 """
74 raise NotImplementedError()
74 raise NotImplementedError()
75
75
76 def getmode(self, name, rev):
76 def getmode(self, name, rev):
77 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
77 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
78 returned by a previous call to getchanges().
78 returned by a previous call to getchanges().
79 """
79 """
80 raise NotImplementedError()
80 raise NotImplementedError()
81
81
82 def getchanges(self, version):
82 def getchanges(self, version):
83 """Returns a tuple of (files, copies).
83 """Returns a tuple of (files, copies).
84
84
85 files is a sorted list of (filename, id) tuples for all files
85 files is a sorted list of (filename, id) tuples for all files
86 changed between version and it's first parent returned by
86 changed between version and it's first parent returned by
87 getcommit(). id is the source revision id of the file.
87 getcommit(). id is the source revision id of the file.
88
88
89 copies is a dictionary of dest: source
89 copies is a dictionary of dest: source
90 """
90 """
91 raise NotImplementedError()
91 raise NotImplementedError()
92
92
93 def getcommit(self, version):
93 def getcommit(self, version):
94 """Return the commit object for version"""
94 """Return the commit object for version"""
95 raise NotImplementedError()
95 raise NotImplementedError()
96
96
97 def gettags(self):
97 def gettags(self):
98 """Return the tags as a dictionary of name: revision"""
98 """Return the tags as a dictionary of name: revision"""
99 raise NotImplementedError()
99 raise NotImplementedError()
100
100
101 def recode(self, s, encoding=None):
101 def recode(self, s, encoding=None):
102 if not encoding:
102 if not encoding:
103 encoding = self.encoding or 'utf-8'
103 encoding = self.encoding or 'utf-8'
104
104
105 if isinstance(s, unicode):
105 if isinstance(s, unicode):
106 return s.encode("utf-8")
106 return s.encode("utf-8")
107 try:
107 try:
108 return s.decode(encoding).encode("utf-8")
108 return s.decode(encoding).encode("utf-8")
109 except:
109 except:
110 try:
110 try:
111 return s.decode("latin-1").encode("utf-8")
111 return s.decode("latin-1").encode("utf-8")
112 except:
112 except:
113 return s.decode(encoding, "replace").encode("utf-8")
113 return s.decode(encoding, "replace").encode("utf-8")
114
114
115 def getchangedfiles(self, rev, i):
115 def getchangedfiles(self, rev, i):
116 """Return the files changed by rev compared to parent[i].
116 """Return the files changed by rev compared to parent[i].
117
117
118 i is an index selecting one of the parents of rev. The return
118 i is an index selecting one of the parents of rev. The return
119 value should be the list of files that are different in rev and
119 value should be the list of files that are different in rev and
120 this parent.
120 this parent.
121
121
122 If rev has no parents, i is None.
122 If rev has no parents, i is None.
123
123
124 This function is only needed to support --filemap
124 This function is only needed to support --filemap
125 """
125 """
126 raise NotImplementedError()
126 raise NotImplementedError()
127
127
128 def converted(self, rev, sinkrev):
128 def converted(self, rev, sinkrev):
129 '''Notify the source that a revision has been converted.'''
129 '''Notify the source that a revision has been converted.'''
130 pass
130 pass
131
131
132
132
133 class converter_sink(object):
133 class converter_sink(object):
134 """Conversion sink (target) interface"""
134 """Conversion sink (target) interface"""
135
135
136 def __init__(self, ui, path):
136 def __init__(self, ui, path):
137 """Initialize conversion sink (or raise NoRepo("message")
137 """Initialize conversion sink (or raise NoRepo("message")
138 exception if path is not a valid repository)
138 exception if path is not a valid repository)
139
139
140 created is a list of paths to remove if a fatal error occurs
140 created is a list of paths to remove if a fatal error occurs
141 later"""
141 later"""
142 self.ui = ui
142 self.ui = ui
143 self.path = path
143 self.path = path
144 self.created = []
144 self.created = []
145
145
146 def getheads(self):
146 def getheads(self):
147 """Return a list of this repository's heads"""
147 """Return a list of this repository's heads"""
148 raise NotImplementedError()
148 raise NotImplementedError()
149
149
150 def revmapfile(self):
150 def revmapfile(self):
151 """Path to a file that will contain lines
151 """Path to a file that will contain lines
152 source_rev_id sink_rev_id
152 source_rev_id sink_rev_id
153 mapping equivalent revision identifiers for each system."""
153 mapping equivalent revision identifiers for each system."""
154 raise NotImplementedError()
154 raise NotImplementedError()
155
155
156 def authorfile(self):
156 def authorfile(self):
157 """Path to a file that will contain lines
157 """Path to a file that will contain lines
158 srcauthor=dstauthor
158 srcauthor=dstauthor
159 mapping equivalent authors identifiers for each system."""
159 mapping equivalent authors identifiers for each system."""
160 return None
160 return None
161
161
162 def putcommit(self, files, copies, parents, commit, source):
162 def putcommit(self, files, copies, parents, commit, source):
163 """Create a revision with all changed files listed in 'files'
163 """Create a revision with all changed files listed in 'files'
164 and having listed parents. 'commit' is a commit object containing
164 and having listed parents. 'commit' is a commit object containing
165 at a minimum the author, date, and message for this changeset.
165 at a minimum the author, date, and message for this changeset.
166 'files' is a list of (path, version) tuples, 'copies'is a dictionary
166 'files' is a list of (path, version) tuples, 'copies'is a dictionary
167 mapping destinations to sources, and 'source' is the source repository.
167 mapping destinations to sources, and 'source' is the source repository.
168 Only getfile() and getmode() should be called on 'source'.
168 Only getfile() and getmode() should be called on 'source'.
169
169
170 Note that the sink repository is not told to update itself to
170 Note that the sink repository is not told to update itself to
171 a particular revision (or even what that revision would be)
171 a particular revision (or even what that revision would be)
172 before it receives the file data.
172 before it receives the file data.
173 """
173 """
174 raise NotImplementedError()
174 raise NotImplementedError()
175
175
176 def puttags(self, tags):
176 def puttags(self, tags):
177 """Put tags into sink.
177 """Put tags into sink.
178 tags: {tagname: sink_rev_id, ...}"""
178 tags: {tagname: sink_rev_id, ...}"""
179 raise NotImplementedError()
179 raise NotImplementedError()
180
180
181 def setbranch(self, branch, pbranches):
181 def setbranch(self, branch, pbranches):
182 """Set the current branch name. Called before the first putcommit
182 """Set the current branch name. Called before the first putcommit
183 on the branch.
183 on the branch.
184 branch: branch name for subsequent commits
184 branch: branch name for subsequent commits
185 pbranches: (converted parent revision, parent branch) tuples"""
185 pbranches: (converted parent revision, parent branch) tuples"""
186 pass
186 pass
187
187
188 def setfilemapmode(self, active):
188 def setfilemapmode(self, active):
189 """Tell the destination that we're using a filemap
189 """Tell the destination that we're using a filemap
190
190
191 Some converter_sources (svn in particular) can claim that a file
191 Some converter_sources (svn in particular) can claim that a file
192 was changed in a revision, even if there was no change. This method
192 was changed in a revision, even if there was no change. This method
193 tells the destination that we're using a filemap and that it should
193 tells the destination that we're using a filemap and that it should
194 filter empty revisions.
194 filter empty revisions.
195 """
195 """
196 pass
196 pass
197
197
198 def before(self):
198 def before(self):
199 pass
199 pass
200
200
201 def after(self):
201 def after(self):
202 pass
202 pass
203
203
204
204
205 class commandline(object):
205 class commandline(object):
206 def __init__(self, ui, command):
206 def __init__(self, ui, command):
207 self.ui = ui
207 self.ui = ui
208 self.command = command
208 self.command = command
209
209
210 def prerun(self):
210 def prerun(self):
211 pass
211 pass
212
212
213 def postrun(self):
213 def postrun(self):
214 pass
214 pass
215
215
216 def _cmdline(self, cmd, *args, **kwargs):
216 def _cmdline(self, cmd, *args, **kwargs):
217 cmdline = [self.command, cmd] + list(args)
217 cmdline = [self.command, cmd] + list(args)
218 for k, v in kwargs.iteritems():
218 for k, v in kwargs.iteritems():
219 if len(k) == 1:
219 if len(k) == 1:
220 cmdline.append('-' + k)
220 cmdline.append('-' + k)
221 else:
221 else:
222 cmdline.append('--' + k.replace('_', '-'))
222 cmdline.append('--' + k.replace('_', '-'))
223 try:
223 try:
224 if len(k) == 1:
224 if len(k) == 1:
225 cmdline.append('' + v)
225 cmdline.append('' + v)
226 else:
226 else:
227 cmdline[-1] += '=' + v
227 cmdline[-1] += '=' + v
228 except TypeError:
228 except TypeError:
229 pass
229 pass
230 cmdline = [util.shellquote(arg) for arg in cmdline]
230 cmdline = [util.shellquote(arg) for arg in cmdline]
231 if not self.ui.debugflag:
231 if not self.ui.debugflag:
232 cmdline += ['2>', util.nulldev]
232 cmdline += ['2>', util.nulldev]
233 cmdline += ['<', util.nulldev]
233 cmdline += ['<', util.nulldev]
234 cmdline = ' '.join(cmdline)
234 cmdline = ' '.join(cmdline)
235 return cmdline
235 return cmdline
236
236
237 def _run(self, cmd, *args, **kwargs):
237 def _run(self, cmd, *args, **kwargs):
238 cmdline = self._cmdline(cmd, *args, **kwargs)
238 cmdline = self._cmdline(cmd, *args, **kwargs)
239 self.ui.debug(_('running: %s\n') % (cmdline,))
239 self.ui.debug(_('running: %s\n') % (cmdline,))
240 self.prerun()
240 self.prerun()
241 try:
241 try:
242 return util.popen(cmdline)
242 return util.popen(cmdline)
243 finally:
243 finally:
244 self.postrun()
244 self.postrun()
245
245
246 def run(self, cmd, *args, **kwargs):
246 def run(self, cmd, *args, **kwargs):
247 fp = self._run(cmd, *args, **kwargs)
247 fp = self._run(cmd, *args, **kwargs)
248 output = fp.read()
248 output = fp.read()
249 self.ui.debug(output)
249 self.ui.debug(output)
250 return output, fp.close()
250 return output, fp.close()
251
251
252 def runlines(self, cmd, *args, **kwargs):
252 def runlines(self, cmd, *args, **kwargs):
253 fp = self._run(cmd, *args, **kwargs)
253 fp = self._run(cmd, *args, **kwargs)
254 output = fp.readlines()
254 output = fp.readlines()
255 self.ui.debug(''.join(output))
255 self.ui.debug(''.join(output))
256 return output, fp.close()
256 return output, fp.close()
257
257
258 def checkexit(self, status, output=''):
258 def checkexit(self, status, output=''):
259 if status:
259 if status:
260 if output:
260 if output:
261 self.ui.warn(_('%s error:\n') % self.command)
261 self.ui.warn(_('%s error:\n') % self.command)
262 self.ui.warn(output)
262 self.ui.warn(output)
263 msg = util.explain_exit(status)[0]
263 msg = util.explain_exit(status)[0]
264 raise util.Abort(_('%s %s') % (self.command, msg))
264 raise util.Abort(_('%s %s') % (self.command, msg))
265
265
266 def run0(self, cmd, *args, **kwargs):
266 def run0(self, cmd, *args, **kwargs):
267 output, status = self.run(cmd, *args, **kwargs)
267 output, status = self.run(cmd, *args, **kwargs)
268 self.checkexit(status, output)
268 self.checkexit(status, output)
269 return output
269 return output
270
270
271 def runlines0(self, cmd, *args, **kwargs):
271 def runlines0(self, cmd, *args, **kwargs):
272 output, status = self.runlines(cmd, *args, **kwargs)
272 output, status = self.runlines(cmd, *args, **kwargs)
273 self.checkexit(status, ''.join(output))
273 self.checkexit(status, ''.join(output))
274 return output
274 return output
275
275
276 def getargmax(self):
276 def getargmax(self):
277 if '_argmax' in self.__dict__:
277 if '_argmax' in self.__dict__:
278 return self._argmax
278 return self._argmax
279
279
280 # POSIX requires at least 4096 bytes for ARG_MAX
280 # POSIX requires at least 4096 bytes for ARG_MAX
281 self._argmax = 4096
281 self._argmax = 4096
282 try:
282 try:
283 self._argmax = os.sysconf("SC_ARG_MAX")
283 self._argmax = os.sysconf("SC_ARG_MAX")
284 except:
284 except:
285 pass
285 pass
286
286
287 # Windows shells impose their own limits on command line length,
287 # Windows shells impose their own limits on command line length,
288 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
288 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
289 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
289 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
290 # details about cmd.exe limitations.
290 # details about cmd.exe limitations.
291
291
292 # Since ARG_MAX is for command line _and_ environment, lower our limit
292 # Since ARG_MAX is for command line _and_ environment, lower our limit
293 # (and make happy Windows shells while doing this).
293 # (and make happy Windows shells while doing this).
294
294
295 self._argmax = self._argmax/2 - 1
295 self._argmax = self._argmax/2 - 1
296 return self._argmax
296 return self._argmax
297
297
298 def limit_arglist(self, arglist, cmd, *args, **kwargs):
298 def limit_arglist(self, arglist, cmd, *args, **kwargs):
299 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
299 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
300 bytes = 0
300 bytes = 0
301 fl = []
301 fl = []
302 for fn in arglist:
302 for fn in arglist:
303 b = len(fn) + 3
303 b = len(fn) + 3
304 if bytes + b < limit or len(fl) == 0:
304 if bytes + b < limit or len(fl) == 0:
305 fl.append(fn)
305 fl.append(fn)
306 bytes += b
306 bytes += b
307 else:
307 else:
308 yield fl
308 yield fl
309 fl = [fn]
309 fl = [fn]
310 bytes = b
310 bytes = b
311 if fl:
311 if fl:
312 yield fl
312 yield fl
313
313
314 def xargs(self, arglist, cmd, *args, **kwargs):
314 def xargs(self, arglist, cmd, *args, **kwargs):
315 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
315 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
316 self.run0(cmd, *(list(args) + l), **kwargs)
316 self.run0(cmd, *(list(args) + l), **kwargs)
317
317
318 class mapfile(dict):
318 class mapfile(dict):
319 def __init__(self, ui, path):
319 def __init__(self, ui, path):
320 super(mapfile, self).__init__()
320 super(mapfile, self).__init__()
321 self.ui = ui
321 self.ui = ui
322 self.path = path
322 self.path = path
323 self.fp = None
323 self.fp = None
324 self.order = []
324 self.order = []
325 self._read()
325 self._read()
326
326
327 def _read(self):
327 def _read(self):
328 if not self.path:
328 if not self.path:
329 return
329 return
330 try:
330 try:
331 fp = open(self.path, 'r')
331 fp = open(self.path, 'r')
332 except IOError, err:
332 except IOError, err:
333 if err.errno != errno.ENOENT:
333 if err.errno != errno.ENOENT:
334 raise
334 raise
335 return
335 return
336 for line in fp:
336 for i, line in enumerate(fp):
337 try:
337 key, value = strutil.rsplit(line[:-1], ' ', 1)
338 key, value = strutil.rsplit(line[:-1], ' ', 1)
339 except ValueError:
340 raise util.Abort(_('syntax error in %s(%d): key/value pair expected')
341 % (self.path, i+1))
338 if key not in self:
342 if key not in self:
339 self.order.append(key)
343 self.order.append(key)
340 super(mapfile, self).__setitem__(key, value)
344 super(mapfile, self).__setitem__(key, value)
341 fp.close()
345 fp.close()
342
346
343 def __setitem__(self, key, value):
347 def __setitem__(self, key, value):
344 if self.fp is None:
348 if self.fp is None:
345 try:
349 try:
346 self.fp = open(self.path, 'a')
350 self.fp = open(self.path, 'a')
347 except IOError, err:
351 except IOError, err:
348 raise util.Abort(_('could not open map file %r: %s') %
352 raise util.Abort(_('could not open map file %r: %s') %
349 (self.path, err.strerror))
353 (self.path, err.strerror))
350 self.fp.write('%s %s\n' % (key, value))
354 self.fp.write('%s %s\n' % (key, value))
351 self.fp.flush()
355 self.fp.flush()
352 super(mapfile, self).__setitem__(key, value)
356 super(mapfile, self).__setitem__(key, value)
353
357
354 def close(self):
358 def close(self):
355 if self.fp:
359 if self.fp:
356 self.fp.close()
360 self.fp.close()
357 self.fp = None
361 self.fp = None
General Comments 0
You need to be logged in to leave comments. Login now