##// END OF EJS Templates
convert: allow missing tools not to stop source type detection
Patrick Mezard -
r6332:950e72fc default
parent child Browse files
Show More
@@ -1,354 +1,357
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
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 def checktool(exe, name=None):
21 class MissingTool(Exception): pass
22
23 def checktool(exe, name=None, abort=True):
22 24 name = name or exe
23 25 if not util.find_exe(exe):
24 raise util.Abort('cannot find required "%s" tool' % name)
26 exc = abort and util.Abort or MissingTool
27 raise exc(_('cannot find required "%s" tool') % name)
25 28
26 29 class NoRepo(Exception): pass
27 30
28 31 SKIPREV = 'SKIP'
29 32
30 33 class commit(object):
31 34 def __init__(self, author, date, desc, parents, branch=None, rev=None,
32 35 extra={}):
33 36 self.author = author or 'unknown'
34 37 self.date = date or '0 0'
35 38 self.desc = desc
36 39 self.parents = parents
37 40 self.branch = branch
38 41 self.rev = rev
39 42 self.extra = extra
40 43
41 44 class converter_source(object):
42 45 """Conversion source interface"""
43 46
44 47 def __init__(self, ui, path=None, rev=None):
45 48 """Initialize conversion source (or raise NoRepo("message")
46 49 exception if path is not a valid repository)"""
47 50 self.ui = ui
48 51 self.path = path
49 52 self.rev = rev
50 53
51 54 self.encoding = 'utf-8'
52 55
53 56 def before(self):
54 57 pass
55 58
56 59 def after(self):
57 60 pass
58 61
59 62 def setrevmap(self, revmap):
60 63 """set the map of already-converted revisions"""
61 64 pass
62 65
63 66 def getheads(self):
64 67 """Return a list of this repository's heads"""
65 68 raise NotImplementedError()
66 69
67 70 def getfile(self, name, rev):
68 71 """Return file contents as a string"""
69 72 raise NotImplementedError()
70 73
71 74 def getmode(self, name, rev):
72 75 """Return file mode, eg. '', 'x', or 'l'"""
73 76 raise NotImplementedError()
74 77
75 78 def getchanges(self, version):
76 79 """Returns a tuple of (files, copies)
77 80 Files is a sorted list of (filename, id) tuples for all files changed
78 81 in version, where id is the source revision id of the file.
79 82
80 83 copies is a dictionary of dest: source
81 84 """
82 85 raise NotImplementedError()
83 86
84 87 def getcommit(self, version):
85 88 """Return the commit object for version"""
86 89 raise NotImplementedError()
87 90
88 91 def gettags(self):
89 92 """Return the tags as a dictionary of name: revision"""
90 93 raise NotImplementedError()
91 94
92 95 def recode(self, s, encoding=None):
93 96 if not encoding:
94 97 encoding = self.encoding or 'utf-8'
95 98
96 99 if isinstance(s, unicode):
97 100 return s.encode("utf-8")
98 101 try:
99 102 return s.decode(encoding).encode("utf-8")
100 103 except:
101 104 try:
102 105 return s.decode("latin-1").encode("utf-8")
103 106 except:
104 107 return s.decode(encoding, "replace").encode("utf-8")
105 108
106 109 def getchangedfiles(self, rev, i):
107 110 """Return the files changed by rev compared to parent[i].
108 111
109 112 i is an index selecting one of the parents of rev. The return
110 113 value should be the list of files that are different in rev and
111 114 this parent.
112 115
113 116 If rev has no parents, i is None.
114 117
115 118 This function is only needed to support --filemap
116 119 """
117 120 raise NotImplementedError()
118 121
119 122 def converted(self, rev, sinkrev):
120 123 '''Notify the source that a revision has been converted.'''
121 124 pass
122 125
123 126
124 127 class converter_sink(object):
125 128 """Conversion sink (target) interface"""
126 129
127 130 def __init__(self, ui, path):
128 131 """Initialize conversion sink (or raise NoRepo("message")
129 132 exception if path is not a valid repository)
130 133
131 134 created is a list of paths to remove if a fatal error occurs
132 135 later"""
133 136 self.ui = ui
134 137 self.path = path
135 138 self.created = []
136 139
137 140 def getheads(self):
138 141 """Return a list of this repository's heads"""
139 142 raise NotImplementedError()
140 143
141 144 def revmapfile(self):
142 145 """Path to a file that will contain lines
143 146 source_rev_id sink_rev_id
144 147 mapping equivalent revision identifiers for each system."""
145 148 raise NotImplementedError()
146 149
147 150 def authorfile(self):
148 151 """Path to a file that will contain lines
149 152 srcauthor=dstauthor
150 153 mapping equivalent authors identifiers for each system."""
151 154 return None
152 155
153 156 def putfile(self, f, e, data):
154 157 """Put file for next putcommit().
155 158 f: path to file
156 159 e: '', 'x', or 'l' (regular file, executable, or symlink)
157 160 data: file contents"""
158 161 raise NotImplementedError()
159 162
160 163 def delfile(self, f):
161 164 """Delete file for next putcommit().
162 165 f: path to file"""
163 166 raise NotImplementedError()
164 167
165 168 def putcommit(self, files, parents, commit):
166 169 """Create a revision with all changed files listed in 'files'
167 170 and having listed parents. 'commit' is a commit object containing
168 171 at a minimum the author, date, and message for this changeset.
169 172 Called after putfile() and delfile() calls. Note that the sink
170 173 repository is not told to update itself to a particular revision
171 174 (or even what that revision would be) before it receives the
172 175 file data."""
173 176 raise NotImplementedError()
174 177
175 178 def puttags(self, tags):
176 179 """Put tags into sink.
177 180 tags: {tagname: sink_rev_id, ...}"""
178 181 raise NotImplementedError()
179 182
180 183 def setbranch(self, branch, pbranches):
181 184 """Set the current branch name. Called before the first putfile
182 185 on the branch.
183 186 branch: branch name for subsequent commits
184 187 pbranches: (converted parent revision, parent branch) tuples"""
185 188 pass
186 189
187 190 def setfilemapmode(self, active):
188 191 """Tell the destination that we're using a filemap
189 192
190 193 Some converter_sources (svn in particular) can claim that a file
191 194 was changed in a revision, even if there was no change. This method
192 195 tells the destination that we're using a filemap and that it should
193 196 filter empty revisions.
194 197 """
195 198 pass
196 199
197 200 def before(self):
198 201 pass
199 202
200 203 def after(self):
201 204 pass
202 205
203 206
204 207 class commandline(object):
205 208 def __init__(self, ui, command):
206 209 self.ui = ui
207 210 self.command = command
208 211
209 212 def prerun(self):
210 213 pass
211 214
212 215 def postrun(self):
213 216 pass
214 217
215 218 def _cmdline(self, cmd, *args, **kwargs):
216 219 cmdline = [self.command, cmd] + list(args)
217 220 for k, v in kwargs.iteritems():
218 221 if len(k) == 1:
219 222 cmdline.append('-' + k)
220 223 else:
221 224 cmdline.append('--' + k.replace('_', '-'))
222 225 try:
223 226 if len(k) == 1:
224 227 cmdline.append('' + v)
225 228 else:
226 229 cmdline[-1] += '=' + v
227 230 except TypeError:
228 231 pass
229 232 cmdline = [util.shellquote(arg) for arg in cmdline]
230 233 cmdline += ['2>', util.nulldev, '<', util.nulldev]
231 234 cmdline = ' '.join(cmdline)
232 235 self.ui.debug(cmdline, '\n')
233 236 return cmdline
234 237
235 238 def _run(self, cmd, *args, **kwargs):
236 239 cmdline = self._cmdline(cmd, *args, **kwargs)
237 240 self.prerun()
238 241 try:
239 242 return util.popen(cmdline)
240 243 finally:
241 244 self.postrun()
242 245
243 246 def run(self, cmd, *args, **kwargs):
244 247 fp = self._run(cmd, *args, **kwargs)
245 248 output = fp.read()
246 249 self.ui.debug(output)
247 250 return output, fp.close()
248 251
249 252 def runlines(self, cmd, *args, **kwargs):
250 253 fp = self._run(cmd, *args, **kwargs)
251 254 output = fp.readlines()
252 255 self.ui.debug(''.join(output))
253 256 return output, fp.close()
254 257
255 258 def checkexit(self, status, output=''):
256 259 if status:
257 260 if output:
258 261 self.ui.warn(_('%s error:\n') % self.command)
259 262 self.ui.warn(output)
260 263 msg = util.explain_exit(status)[0]
261 264 raise util.Abort(_('%s %s') % (self.command, msg))
262 265
263 266 def run0(self, cmd, *args, **kwargs):
264 267 output, status = self.run(cmd, *args, **kwargs)
265 268 self.checkexit(status, output)
266 269 return output
267 270
268 271 def runlines0(self, cmd, *args, **kwargs):
269 272 output, status = self.runlines(cmd, *args, **kwargs)
270 273 self.checkexit(status, ''.join(output))
271 274 return output
272 275
273 276 def getargmax(self):
274 277 if '_argmax' in self.__dict__:
275 278 return self._argmax
276 279
277 280 # POSIX requires at least 4096 bytes for ARG_MAX
278 281 self._argmax = 4096
279 282 try:
280 283 self._argmax = os.sysconf("SC_ARG_MAX")
281 284 except:
282 285 pass
283 286
284 287 # Windows shells impose their own limits on command line length,
285 288 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
286 289 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
287 290 # details about cmd.exe limitations.
288 291
289 292 # Since ARG_MAX is for command line _and_ environment, lower our limit
290 293 # (and make happy Windows shells while doing this).
291 294
292 295 self._argmax = self._argmax/2 - 1
293 296 return self._argmax
294 297
295 298 def limit_arglist(self, arglist, cmd, *args, **kwargs):
296 299 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
297 300 bytes = 0
298 301 fl = []
299 302 for fn in arglist:
300 303 b = len(fn) + 3
301 304 if bytes + b < limit or len(fl) == 0:
302 305 fl.append(fn)
303 306 bytes += b
304 307 else:
305 308 yield fl
306 309 fl = [fn]
307 310 bytes = b
308 311 if fl:
309 312 yield fl
310 313
311 314 def xargs(self, arglist, cmd, *args, **kwargs):
312 315 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
313 316 self.run0(cmd, *(list(args) + l), **kwargs)
314 317
315 318 class mapfile(dict):
316 319 def __init__(self, ui, path):
317 320 super(mapfile, self).__init__()
318 321 self.ui = ui
319 322 self.path = path
320 323 self.fp = None
321 324 self.order = []
322 325 self._read()
323 326
324 327 def _read(self):
325 328 if self.path is None:
326 329 return
327 330 try:
328 331 fp = open(self.path, 'r')
329 332 except IOError, err:
330 333 if err.errno != errno.ENOENT:
331 334 raise
332 335 return
333 336 for line in fp:
334 337 key, value = line[:-1].split(' ', 1)
335 338 if key not in self:
336 339 self.order.append(key)
337 340 super(mapfile, self).__setitem__(key, value)
338 341 fp.close()
339 342
340 343 def __setitem__(self, key, value):
341 344 if self.fp is None:
342 345 try:
343 346 self.fp = open(self.path, 'a')
344 347 except IOError, err:
345 348 raise util.Abort(_('could not open map file %r: %s') %
346 349 (self.path, err.strerror))
347 350 self.fp.write('%s %s\n' % (key, value))
348 351 self.fp.flush()
349 352 super(mapfile, self).__setitem__(key, value)
350 353
351 354 def close(self):
352 355 if self.fp:
353 356 self.fp.close()
354 357 self.fp = None
@@ -1,354 +1,354
1 1 # convcmd - convert extension commands definition
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
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 from common import NoRepo, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import debugsvnlog, svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 import filemap
17 17
18 18 import os, shutil
19 19 from mercurial import hg, util
20 20 from mercurial.i18n import _
21 21
22 22 orig_encoding = 'ascii'
23 23
24 24 def recode(s):
25 25 if isinstance(s, unicode):
26 26 return s.encode(orig_encoding, 'replace')
27 27 else:
28 28 return s.decode('utf-8').encode(orig_encoding, 'replace')
29 29
30 30 source_converters = [
31 31 ('cvs', convert_cvs),
32 32 ('git', convert_git),
33 33 ('svn', svn_source),
34 34 ('hg', mercurial_source),
35 35 ('darcs', darcs_source),
36 36 ('mtn', monotone_source),
37 37 ('gnuarch', gnuarch_source),
38 38 ]
39 39
40 40 sink_converters = [
41 41 ('hg', mercurial_sink),
42 42 ('svn', svn_sink),
43 43 ]
44 44
45 45 def convertsource(ui, path, type, rev):
46 46 exceptions = []
47 47 for name, source in source_converters:
48 48 try:
49 49 if not type or name == type:
50 50 return source(ui, path, rev)
51 except NoRepo, inst:
51 except (NoRepo, MissingTool), inst:
52 52 exceptions.append(inst)
53 53 if not ui.quiet:
54 54 for inst in exceptions:
55 55 ui.write(_("%s\n") % inst)
56 56 raise util.Abort('%s: unknown repository type' % path)
57 57
58 58 def convertsink(ui, path, type):
59 59 for name, sink in sink_converters:
60 60 try:
61 61 if not type or name == type:
62 62 return sink(ui, path)
63 63 except NoRepo, inst:
64 64 ui.note(_("convert: %s\n") % inst)
65 65 raise util.Abort('%s: unknown repository type' % path)
66 66
67 67 class converter(object):
68 68 def __init__(self, ui, source, dest, revmapfile, opts):
69 69
70 70 self.source = source
71 71 self.dest = dest
72 72 self.ui = ui
73 73 self.opts = opts
74 74 self.commitcache = {}
75 75 self.authors = {}
76 76 self.authorfile = None
77 77
78 78 self.map = mapfile(ui, revmapfile)
79 79
80 80 # Read first the dst author map if any
81 81 authorfile = self.dest.authorfile()
82 82 if authorfile and os.path.exists(authorfile):
83 83 self.readauthormap(authorfile)
84 84 # Extend/Override with new author map if necessary
85 85 if opts.get('authors'):
86 86 self.readauthormap(opts.get('authors'))
87 87 self.authorfile = self.dest.authorfile()
88 88
89 89 self.splicemap = mapfile(ui, opts.get('splicemap'))
90 90
91 91 def walktree(self, heads):
92 92 '''Return a mapping that identifies the uncommitted parents of every
93 93 uncommitted changeset.'''
94 94 visit = heads
95 95 known = {}
96 96 parents = {}
97 97 while visit:
98 98 n = visit.pop(0)
99 99 if n in known or n in self.map: continue
100 100 known[n] = 1
101 101 commit = self.cachecommit(n)
102 102 parents[n] = []
103 103 for p in commit.parents:
104 104 parents[n].append(p)
105 105 visit.append(p)
106 106
107 107 return parents
108 108
109 109 def toposort(self, parents):
110 110 '''Return an ordering such that every uncommitted changeset is
111 111 preceeded by all its uncommitted ancestors.'''
112 112 visit = parents.keys()
113 113 seen = {}
114 114 children = {}
115 115 actives = []
116 116
117 117 while visit:
118 118 n = visit.pop(0)
119 119 if n in seen: continue
120 120 seen[n] = 1
121 121 # Ensure that nodes without parents are present in the 'children'
122 122 # mapping.
123 123 children.setdefault(n, [])
124 124 hasparent = False
125 125 for p in parents[n]:
126 126 if not p in self.map:
127 127 visit.append(p)
128 128 hasparent = True
129 129 children.setdefault(p, []).append(n)
130 130 if not hasparent:
131 131 actives.append(n)
132 132
133 133 del seen
134 134 del visit
135 135
136 136 if self.opts.get('datesort'):
137 137 dates = {}
138 138 def getdate(n):
139 139 if n not in dates:
140 140 dates[n] = util.parsedate(self.commitcache[n].date)
141 141 return dates[n]
142 142
143 143 def picknext(nodes):
144 144 return min([(getdate(n), n) for n in nodes])[1]
145 145 else:
146 146 prev = [None]
147 147 def picknext(nodes):
148 148 # Return the first eligible child of the previously converted
149 149 # revision, or any of them.
150 150 next = nodes[0]
151 151 for n in nodes:
152 152 if prev[0] in parents[n]:
153 153 next = n
154 154 break
155 155 prev[0] = next
156 156 return next
157 157
158 158 s = []
159 159 pendings = {}
160 160 while actives:
161 161 n = picknext(actives)
162 162 actives.remove(n)
163 163 s.append(n)
164 164
165 165 # Update dependents list
166 166 for c in children.get(n, []):
167 167 if c not in pendings:
168 168 pendings[c] = [p for p in parents[c] if p not in self.map]
169 169 try:
170 170 pendings[c].remove(n)
171 171 except ValueError:
172 172 raise util.Abort(_('cycle detected between %s and %s')
173 173 % (recode(c), recode(n)))
174 174 if not pendings[c]:
175 175 # Parents are converted, node is eligible
176 176 actives.insert(0, c)
177 177 pendings[c] = None
178 178
179 179 if len(s) != len(parents):
180 180 raise util.Abort(_("not all revisions were sorted"))
181 181
182 182 return s
183 183
184 184 def writeauthormap(self):
185 185 authorfile = self.authorfile
186 186 if authorfile:
187 187 self.ui.status('Writing author map file %s\n' % authorfile)
188 188 ofile = open(authorfile, 'w+')
189 189 for author in self.authors:
190 190 ofile.write("%s=%s\n" % (author, self.authors[author]))
191 191 ofile.close()
192 192
193 193 def readauthormap(self, authorfile):
194 194 afile = open(authorfile, 'r')
195 195 for line in afile:
196 196 if line.strip() == '':
197 197 continue
198 198 try:
199 199 srcauthor, dstauthor = line.split('=', 1)
200 200 srcauthor = srcauthor.strip()
201 201 dstauthor = dstauthor.strip()
202 202 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
203 203 self.ui.status(
204 204 'Overriding mapping for author %s, was %s, will be %s\n'
205 205 % (srcauthor, self.authors[srcauthor], dstauthor))
206 206 else:
207 207 self.ui.debug('Mapping author %s to %s\n'
208 208 % (srcauthor, dstauthor))
209 209 self.authors[srcauthor] = dstauthor
210 210 except IndexError:
211 211 self.ui.warn(
212 212 'Ignoring bad line in author map file %s: %s\n'
213 213 % (authorfile, line.rstrip()))
214 214 afile.close()
215 215
216 216 def cachecommit(self, rev):
217 217 commit = self.source.getcommit(rev)
218 218 commit.author = self.authors.get(commit.author, commit.author)
219 219 self.commitcache[rev] = commit
220 220 return commit
221 221
222 222 def copy(self, rev):
223 223 commit = self.commitcache[rev]
224 224 do_copies = hasattr(self.dest, 'copyfile')
225 225 filenames = []
226 226
227 227 changes = self.source.getchanges(rev)
228 228 if isinstance(changes, basestring):
229 229 if changes == SKIPREV:
230 230 dest = SKIPREV
231 231 else:
232 232 dest = self.map[changes]
233 233 self.map[rev] = dest
234 234 return
235 235 files, copies = changes
236 236 pbranches = []
237 237 if commit.parents:
238 238 for prev in commit.parents:
239 239 if prev not in self.commitcache:
240 240 self.cachecommit(prev)
241 241 pbranches.append((self.map[prev],
242 242 self.commitcache[prev].branch))
243 243 self.dest.setbranch(commit.branch, pbranches)
244 244 for f, v in files:
245 245 filenames.append(f)
246 246 try:
247 247 data = self.source.getfile(f, v)
248 248 except IOError, inst:
249 249 self.dest.delfile(f)
250 250 else:
251 251 e = self.source.getmode(f, v)
252 252 self.dest.putfile(f, e, data)
253 253 if do_copies:
254 254 if f in copies:
255 255 copyf = copies[f]
256 256 # Merely marks that a copy happened.
257 257 self.dest.copyfile(copyf, f)
258 258
259 259 try:
260 260 parents = self.splicemap[rev].replace(',', ' ').split()
261 261 self.ui.status('spliced in %s as parents of %s\n' %
262 262 (parents, rev))
263 263 parents = [self.map.get(p, p) for p in parents]
264 264 except KeyError:
265 265 parents = [b[0] for b in pbranches]
266 266 newnode = self.dest.putcommit(filenames, parents, commit)
267 267 self.source.converted(rev, newnode)
268 268 self.map[rev] = newnode
269 269
270 270 def convert(self):
271 271
272 272 try:
273 273 self.source.before()
274 274 self.dest.before()
275 275 self.source.setrevmap(self.map)
276 276 self.ui.status("scanning source...\n")
277 277 heads = self.source.getheads()
278 278 parents = self.walktree(heads)
279 279 self.ui.status("sorting...\n")
280 280 t = self.toposort(parents)
281 281 num = len(t)
282 282 c = None
283 283
284 284 self.ui.status("converting...\n")
285 285 for c in t:
286 286 num -= 1
287 287 desc = self.commitcache[c].desc
288 288 if "\n" in desc:
289 289 desc = desc.splitlines()[0]
290 290 # convert log message to local encoding without using
291 291 # tolocal() because util._encoding conver() use it as
292 292 # 'utf-8'
293 293 self.ui.status("%d %s\n" % (num, recode(desc)))
294 294 self.ui.note(_("source: %s\n" % recode(c)))
295 295 self.copy(c)
296 296
297 297 tags = self.source.gettags()
298 298 ctags = {}
299 299 for k in tags:
300 300 v = tags[k]
301 301 if self.map.get(v, SKIPREV) != SKIPREV:
302 302 ctags[k] = self.map[v]
303 303
304 304 if c and ctags:
305 305 nrev = self.dest.puttags(ctags)
306 306 # write another hash correspondence to override the previous
307 307 # one so we don't end up with extra tag heads
308 308 if nrev:
309 309 self.map[c] = nrev
310 310
311 311 self.writeauthormap()
312 312 finally:
313 313 self.cleanup()
314 314
315 315 def cleanup(self):
316 316 try:
317 317 self.dest.after()
318 318 finally:
319 319 self.source.after()
320 320 self.map.close()
321 321
322 322 def convert(ui, src, dest=None, revmapfile=None, **opts):
323 323 global orig_encoding
324 324 orig_encoding = util._encoding
325 325 util._encoding = 'UTF-8'
326 326
327 327 if not dest:
328 328 dest = hg.defaultdest(src) + "-hg"
329 329 ui.status("assuming destination %s\n" % dest)
330 330
331 331 destc = convertsink(ui, dest, opts.get('dest_type'))
332 332
333 333 try:
334 334 srcc = convertsource(ui, src, opts.get('source_type'),
335 335 opts.get('rev'))
336 336 except Exception:
337 337 for path in destc.created:
338 338 shutil.rmtree(path, True)
339 339 raise
340 340
341 341 fmap = opts.get('filemap')
342 342 if fmap:
343 343 srcc = filemap.filemap_source(ui, srcc, fmap)
344 344 destc.setfilemapmode(True)
345 345
346 346 if not revmapfile:
347 347 try:
348 348 revmapfile = destc.revmapfile()
349 349 except:
350 350 revmapfile = os.path.join(destc, "map")
351 351
352 352 c = converter(ui, srcc, destc, revmapfile, opts)
353 353 c.convert()
354 354
@@ -1,193 +1,194
1 1 # monotone support for the convert extension
2 2
3 3 import os, re, time
4 4 from mercurial import util
5 from common import NoRepo, commit, converter_source, checktool, commandline
5 from common import NoRepo, MissingTool, commit, converter_source, checktool
6 from common import commandline
6 7 from mercurial.i18n import _
7 8
8 9 class monotone_source(converter_source, commandline):
9 10 def __init__(self, ui, path=None, rev=None):
10 11 converter_source.__init__(self, ui, path, rev)
11 12 commandline.__init__(self, ui, 'mtn')
12 13
13 14 self.ui = ui
14 15 self.path = path
15 16
16 17 # regular expressions for parsing monotone output
17 18 space = r'\s*'
18 19 name = r'\s+"((?:[^"]|\\")*)"\s*'
19 20 value = name
20 21 revision = r'\s+\[(\w+)\]\s*'
21 22 lines = r'(?:.|\n)+'
22 23
23 24 self.dir_re = re.compile(space + "dir" + name)
24 25 self.file_re = re.compile(space + "file" + name + "content" + revision)
25 26 self.add_file_re = re.compile(space + "add_file" + name + "content" + revision)
26 27 self.patch_re = re.compile(space + "patch" + name + "from" + revision + "to" + revision)
27 28 self.rename_re = re.compile(space + "rename" + name + "to" + name)
28 29 self.tag_re = re.compile(space + "tag" + name + "revision" + revision)
29 30 self.cert_re = re.compile(lines + space + "name" + name + "value" + value)
30 31
31 32 attr = space + "file" + lines + space + "attr" + space
32 33 self.attr_execute_re = re.compile(attr + '"mtn:execute"' + space + '"true"')
33 34
34 35 # cached data
35 36 self.manifest_rev = None
36 37 self.manifest = None
37 38 self.files = None
38 39 self.dirs = None
39 40
40 41 norepo = NoRepo (_("%s does not look like a monotone repo") % path)
41 42 if not os.path.exists(path):
42 43 raise norepo
43 44
44 checktool('mtn')
45 checktool('mtn', abort=False)
45 46
46 47 # test if there are any revisions
47 48 self.rev = None
48 49 try:
49 50 self.getheads()
50 51 except:
51 52 raise norepo
52 53 self.rev = rev
53 54
54 55 def mtnrun(self, *args, **kwargs):
55 56 kwargs['d'] = self.path
56 57 return self.run0('automate', *args, **kwargs)
57 58
58 59 def mtnloadmanifest(self, rev):
59 60 if self.manifest_rev == rev:
60 61 return
61 62 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
62 63 self.manifest_rev = rev
63 64 self.files = {}
64 65 self.dirs = {}
65 66
66 67 for e in self.manifest:
67 68 m = self.file_re.match(e)
68 69 if m:
69 70 attr = ""
70 71 name = m.group(1)
71 72 node = m.group(2)
72 73 if self.attr_execute_re.match(e):
73 74 attr += "x"
74 75 self.files[name] = (node, attr)
75 76 m = self.dir_re.match(e)
76 77 if m:
77 78 self.dirs[m.group(1)] = True
78 79
79 80 def mtnisfile(self, name, rev):
80 81 # a non-file could be a directory or a deleted or renamed file
81 82 self.mtnloadmanifest(rev)
82 83 try:
83 84 self.files[name]
84 85 return True
85 86 except KeyError:
86 87 return False
87 88
88 89 def mtnisdir(self, name, rev):
89 90 self.mtnloadmanifest(rev)
90 91 try:
91 92 self.dirs[name]
92 93 return True
93 94 except KeyError:
94 95 return False
95 96
96 97 def mtngetcerts(self, rev):
97 98 certs = {"author":"<missing>", "date":"<missing>",
98 99 "changelog":"<missing>", "branch":"<missing>"}
99 100 cert_list = self.mtnrun("certs", rev).split("\n\n")
100 101 for e in cert_list:
101 102 m = self.cert_re.match(e)
102 103 if m:
103 104 certs[m.group(1)] = m.group(2)
104 105 return certs
105 106
106 107 def mtnrenamefiles(self, files, fromdir, todir):
107 108 renamed = {}
108 109 for tofile in files:
109 110 suffix = tofile.lstrip(todir)
110 111 if todir + suffix == tofile:
111 112 renamed[tofile] = (fromdir + suffix).lstrip("/")
112 113 return renamed
113 114
114 115
115 116 # implement the converter_source interface:
116 117
117 118 def getheads(self):
118 119 if not self.rev:
119 120 return self.mtnrun("leaves").splitlines()
120 121 else:
121 122 return [self.rev]
122 123
123 124 def getchanges(self, rev):
124 125 #revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
125 126 revision = self.mtnrun("get_revision", rev).split("\n\n")
126 127 files = {}
127 128 copies = {}
128 129 for e in revision:
129 130 m = self.add_file_re.match(e)
130 131 if m:
131 132 files[m.group(1)] = rev
132 133 m = self.patch_re.match(e)
133 134 if m:
134 135 files[m.group(1)] = rev
135 136
136 137 # Delete/rename is handled later when the convert engine
137 138 # discovers an IOError exception from getfile,
138 139 # but only if we add the "from" file to the list of changes.
139 140 m = self.rename_re.match(e)
140 141 if m:
141 142 toname = m.group(2)
142 143 fromname = m.group(1)
143 144 if self.mtnisfile(toname, rev):
144 145 copies[toname] = fromname
145 146 files[toname] = rev
146 147 files[fromname] = rev
147 148 if self.mtnisdir(toname, rev):
148 149 renamed = self.mtnrenamefiles(self.files, fromname, toname)
149 150 for tofile, fromfile in renamed.items():
150 151 self.ui.debug (_("copying file in renamed dir from '%s' to '%s'") % (fromfile, tofile), '\n')
151 152 files[tofile] = rev
152 153 for fromfile in renamed.values():
153 154 files[fromfile] = rev
154 155 return (files.items(), copies)
155 156
156 157 def getmode(self, name, rev):
157 158 self.mtnloadmanifest(rev)
158 159 try:
159 160 node, attr = self.files[name]
160 161 return attr
161 162 except KeyError:
162 163 return ""
163 164
164 165 def getfile(self, name, rev):
165 166 if not self.mtnisfile(name, rev):
166 167 raise IOError() # file was deleted or renamed
167 168 try:
168 169 return self.mtnrun("get_file_of", name, r=rev)
169 170 except:
170 171 raise IOError() # file was deleted or renamed
171 172
172 173 def getcommit(self, rev):
173 174 certs = self.mtngetcerts(rev)
174 175 return commit(
175 176 author=certs["author"],
176 177 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
177 178 desc=certs["changelog"],
178 179 rev=rev,
179 180 parents=self.mtnrun("parents", rev).splitlines(),
180 181 branch=certs["branch"])
181 182
182 183 def gettags(self):
183 184 tags = {}
184 185 for e in self.mtnrun("tags").split("\n\n"):
185 186 m = self.tag_re.match(e)
186 187 if m:
187 188 tags[m.group(1)] = m.group(2)
188 189 return tags
189 190
190 191 def getchangedfiles(self, rev, i):
191 192 # This function is only needed to support --filemap
192 193 # ... and we don't support that
193 194 raise NotImplementedError()
General Comments 0
You need to be logged in to leave comments. Login now