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