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