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