##// END OF EJS Templates
convert: reintegrate file retrieval code in sinks...
Patrick Mezard -
r6716:c9b8d256 default
parent child Browse files
Show More
@@ -1,357 +1,349 b''
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 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"""
72 72 raise NotImplementedError()
73 73
74 74 def getmode(self, name, rev):
75 75 """Return file mode, eg. '', 'x', or 'l'"""
76 76 raise NotImplementedError()
77 77
78 78 def getchanges(self, version):
79 79 """Returns a tuple of (files, copies)
80 80 Files is a sorted list of (filename, id) tuples for all files changed
81 81 in version, where id is the source revision id of the file.
82 82
83 83 copies is a dictionary of dest: source
84 84 """
85 85 raise NotImplementedError()
86 86
87 87 def getcommit(self, version):
88 88 """Return the commit object for version"""
89 89 raise NotImplementedError()
90 90
91 91 def gettags(self):
92 92 """Return the tags as a dictionary of name: revision"""
93 93 raise NotImplementedError()
94 94
95 95 def recode(self, s, encoding=None):
96 96 if not encoding:
97 97 encoding = self.encoding or 'utf-8'
98 98
99 99 if isinstance(s, unicode):
100 100 return s.encode("utf-8")
101 101 try:
102 102 return s.decode(encoding).encode("utf-8")
103 103 except:
104 104 try:
105 105 return s.decode("latin-1").encode("utf-8")
106 106 except:
107 107 return s.decode(encoding, "replace").encode("utf-8")
108 108
109 109 def getchangedfiles(self, rev, i):
110 110 """Return the files changed by rev compared to parent[i].
111 111
112 112 i is an index selecting one of the parents of rev. The return
113 113 value should be the list of files that are different in rev and
114 114 this parent.
115 115
116 116 If rev has no parents, i is None.
117 117
118 118 This function is only needed to support --filemap
119 119 """
120 120 raise NotImplementedError()
121 121
122 122 def converted(self, rev, sinkrev):
123 123 '''Notify the source that a revision has been converted.'''
124 124 pass
125 125
126 126
127 127 class converter_sink(object):
128 128 """Conversion sink (target) interface"""
129 129
130 130 def __init__(self, ui, path):
131 131 """Initialize conversion sink (or raise NoRepo("message")
132 132 exception if path is not a valid repository)
133 133
134 134 created is a list of paths to remove if a fatal error occurs
135 135 later"""
136 136 self.ui = ui
137 137 self.path = path
138 138 self.created = []
139 139
140 140 def getheads(self):
141 141 """Return a list of this repository's heads"""
142 142 raise NotImplementedError()
143 143
144 144 def revmapfile(self):
145 145 """Path to a file that will contain lines
146 146 source_rev_id sink_rev_id
147 147 mapping equivalent revision identifiers for each system."""
148 148 raise NotImplementedError()
149 149
150 150 def authorfile(self):
151 151 """Path to a file that will contain lines
152 152 srcauthor=dstauthor
153 153 mapping equivalent authors identifiers for each system."""
154 154 return None
155 155
156 def putfile(self, f, e, data):
157 """Put file for next putcommit().
158 f: path to file
159 e: '', 'x', or 'l' (regular file, executable, or symlink)
160 data: file contents"""
161 raise NotImplementedError()
162
163 def delfile(self, f):
164 """Delete file for next putcommit().
165 f: path to file"""
166 raise NotImplementedError()
167
168 def putcommit(self, files, parents, commit):
156 def putcommit(self, files, copies, parents, commit, source):
169 157 """Create a revision with all changed files listed in 'files'
170 158 and having listed parents. 'commit' is a commit object containing
171 159 at a minimum the author, date, and message for this changeset.
172 Called after putfile() and delfile() calls. Note that the sink
173 repository is not told to update itself to a particular revision
174 (or even what that revision would be) before it receives the
175 file data."""
160 'files' is a list of (path, version) tuples, 'copies'is a dictionary
161 mapping destinations to sources, and 'source' is the source repository.
162 Only getfile() and getmode() should be called on 'source'.
163
164 Note that the sink repository is not told to update itself to
165 a particular revision (or even what that revision would be)
166 before it receives the file data.
167 """
176 168 raise NotImplementedError()
177 169
178 170 def puttags(self, tags):
179 171 """Put tags into sink.
180 172 tags: {tagname: sink_rev_id, ...}"""
181 173 raise NotImplementedError()
182 174
183 175 def setbranch(self, branch, pbranches):
184 """Set the current branch name. Called before the first putfile
176 """Set the current branch name. Called before the first putcommit
185 177 on the branch.
186 178 branch: branch name for subsequent commits
187 179 pbranches: (converted parent revision, parent branch) tuples"""
188 180 pass
189 181
190 182 def setfilemapmode(self, active):
191 183 """Tell the destination that we're using a filemap
192 184
193 185 Some converter_sources (svn in particular) can claim that a file
194 186 was changed in a revision, even if there was no change. This method
195 187 tells the destination that we're using a filemap and that it should
196 188 filter empty revisions.
197 189 """
198 190 pass
199 191
200 192 def before(self):
201 193 pass
202 194
203 195 def after(self):
204 196 pass
205 197
206 198
207 199 class commandline(object):
208 200 def __init__(self, ui, command):
209 201 self.ui = ui
210 202 self.command = command
211 203
212 204 def prerun(self):
213 205 pass
214 206
215 207 def postrun(self):
216 208 pass
217 209
218 210 def _cmdline(self, cmd, *args, **kwargs):
219 211 cmdline = [self.command, cmd] + list(args)
220 212 for k, v in kwargs.iteritems():
221 213 if len(k) == 1:
222 214 cmdline.append('-' + k)
223 215 else:
224 216 cmdline.append('--' + k.replace('_', '-'))
225 217 try:
226 218 if len(k) == 1:
227 219 cmdline.append('' + v)
228 220 else:
229 221 cmdline[-1] += '=' + v
230 222 except TypeError:
231 223 pass
232 224 cmdline = [util.shellquote(arg) for arg in cmdline]
233 225 cmdline += ['2>', util.nulldev, '<', util.nulldev]
234 226 cmdline = ' '.join(cmdline)
235 227 self.ui.debug(cmdline, '\n')
236 228 return cmdline
237 229
238 230 def _run(self, cmd, *args, **kwargs):
239 231 cmdline = self._cmdline(cmd, *args, **kwargs)
240 232 self.prerun()
241 233 try:
242 234 return util.popen(cmdline)
243 235 finally:
244 236 self.postrun()
245 237
246 238 def run(self, cmd, *args, **kwargs):
247 239 fp = self._run(cmd, *args, **kwargs)
248 240 output = fp.read()
249 241 self.ui.debug(output)
250 242 return output, fp.close()
251 243
252 244 def runlines(self, cmd, *args, **kwargs):
253 245 fp = self._run(cmd, *args, **kwargs)
254 246 output = fp.readlines()
255 247 self.ui.debug(''.join(output))
256 248 return output, fp.close()
257 249
258 250 def checkexit(self, status, output=''):
259 251 if status:
260 252 if output:
261 253 self.ui.warn(_('%s error:\n') % self.command)
262 254 self.ui.warn(output)
263 255 msg = util.explain_exit(status)[0]
264 256 raise util.Abort(_('%s %s') % (self.command, msg))
265 257
266 258 def run0(self, cmd, *args, **kwargs):
267 259 output, status = self.run(cmd, *args, **kwargs)
268 260 self.checkexit(status, output)
269 261 return output
270 262
271 263 def runlines0(self, cmd, *args, **kwargs):
272 264 output, status = self.runlines(cmd, *args, **kwargs)
273 265 self.checkexit(status, ''.join(output))
274 266 return output
275 267
276 268 def getargmax(self):
277 269 if '_argmax' in self.__dict__:
278 270 return self._argmax
279 271
280 272 # POSIX requires at least 4096 bytes for ARG_MAX
281 273 self._argmax = 4096
282 274 try:
283 275 self._argmax = os.sysconf("SC_ARG_MAX")
284 276 except:
285 277 pass
286 278
287 279 # Windows shells impose their own limits on command line length,
288 280 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
289 281 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
290 282 # details about cmd.exe limitations.
291 283
292 284 # Since ARG_MAX is for command line _and_ environment, lower our limit
293 285 # (and make happy Windows shells while doing this).
294 286
295 287 self._argmax = self._argmax/2 - 1
296 288 return self._argmax
297 289
298 290 def limit_arglist(self, arglist, cmd, *args, **kwargs):
299 291 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
300 292 bytes = 0
301 293 fl = []
302 294 for fn in arglist:
303 295 b = len(fn) + 3
304 296 if bytes + b < limit or len(fl) == 0:
305 297 fl.append(fn)
306 298 bytes += b
307 299 else:
308 300 yield fl
309 301 fl = [fn]
310 302 bytes = b
311 303 if fl:
312 304 yield fl
313 305
314 306 def xargs(self, arglist, cmd, *args, **kwargs):
315 307 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
316 308 self.run0(cmd, *(list(args) + l), **kwargs)
317 309
318 310 class mapfile(dict):
319 311 def __init__(self, ui, path):
320 312 super(mapfile, self).__init__()
321 313 self.ui = ui
322 314 self.path = path
323 315 self.fp = None
324 316 self.order = []
325 317 self._read()
326 318
327 319 def _read(self):
328 320 if self.path is None:
329 321 return
330 322 try:
331 323 fp = open(self.path, 'r')
332 324 except IOError, err:
333 325 if err.errno != errno.ENOENT:
334 326 raise
335 327 return
336 328 for line in fp:
337 329 key, value = line[:-1].split(' ', 1)
338 330 if key not in self:
339 331 self.order.append(key)
340 332 super(mapfile, self).__setitem__(key, value)
341 333 fp.close()
342 334
343 335 def __setitem__(self, key, value):
344 336 if self.fp is None:
345 337 try:
346 338 self.fp = open(self.path, 'a')
347 339 except IOError, err:
348 340 raise util.Abort(_('could not open map file %r: %s') %
349 341 (self.path, err.strerror))
350 342 self.fp.write('%s %s\n' % (key, value))
351 343 self.fp.flush()
352 344 super(mapfile, self).__setitem__(key, value)
353 345
354 346 def close(self):
355 347 if self.fp:
356 348 self.fp.close()
357 349 self.fp = None
@@ -1,354 +1,337 b''
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 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 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 do_copies = hasattr(self.dest, 'copyfile')
225 filenames = []
226 224
227 225 changes = self.source.getchanges(rev)
228 226 if isinstance(changes, basestring):
229 227 if changes == SKIPREV:
230 228 dest = SKIPREV
231 229 else:
232 230 dest = self.map[changes]
233 231 self.map[rev] = dest
234 232 return
235 233 files, copies = changes
236 234 pbranches = []
237 235 if commit.parents:
238 236 for prev in commit.parents:
239 237 if prev not in self.commitcache:
240 238 self.cachecommit(prev)
241 239 pbranches.append((self.map[prev],
242 240 self.commitcache[prev].branch))
243 241 self.dest.setbranch(commit.branch, pbranches)
244 for f, v in files:
245 filenames.append(f)
246 try:
247 data = self.source.getfile(f, v)
248 except IOError, inst:
249 self.dest.delfile(f)
250 else:
251 e = self.source.getmode(f, v)
252 self.dest.putfile(f, e, data)
253 if do_copies:
254 if f in copies:
255 copyf = copies[f]
256 # Merely marks that a copy happened.
257 self.dest.copyfile(copyf, f)
258
259 242 try:
260 243 parents = self.splicemap[rev].replace(',', ' ').split()
261 244 self.ui.status('spliced in %s as parents of %s\n' %
262 245 (parents, rev))
263 246 parents = [self.map.get(p, p) for p in parents]
264 247 except KeyError:
265 248 parents = [b[0] for b in pbranches]
266 newnode = self.dest.putcommit(filenames, parents, commit)
249 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
267 250 self.source.converted(rev, newnode)
268 251 self.map[rev] = newnode
269 252
270 253 def convert(self):
271 254
272 255 try:
273 256 self.source.before()
274 257 self.dest.before()
275 258 self.source.setrevmap(self.map)
276 259 self.ui.status("scanning source...\n")
277 260 heads = self.source.getheads()
278 261 parents = self.walktree(heads)
279 262 self.ui.status("sorting...\n")
280 263 t = self.toposort(parents)
281 264 num = len(t)
282 265 c = None
283 266
284 267 self.ui.status("converting...\n")
285 268 for c in t:
286 269 num -= 1
287 270 desc = self.commitcache[c].desc
288 271 if "\n" in desc:
289 272 desc = desc.splitlines()[0]
290 273 # convert log message to local encoding without using
291 274 # tolocal() because util._encoding conver() use it as
292 275 # 'utf-8'
293 276 self.ui.status("%d %s\n" % (num, recode(desc)))
294 277 self.ui.note(_("source: %s\n" % recode(c)))
295 278 self.copy(c)
296 279
297 280 tags = self.source.gettags()
298 281 ctags = {}
299 282 for k in tags:
300 283 v = tags[k]
301 284 if self.map.get(v, SKIPREV) != SKIPREV:
302 285 ctags[k] = self.map[v]
303 286
304 287 if c and ctags:
305 288 nrev = self.dest.puttags(ctags)
306 289 # write another hash correspondence to override the previous
307 290 # one so we don't end up with extra tag heads
308 291 if nrev:
309 292 self.map[c] = nrev
310 293
311 294 self.writeauthormap()
312 295 finally:
313 296 self.cleanup()
314 297
315 298 def cleanup(self):
316 299 try:
317 300 self.dest.after()
318 301 finally:
319 302 self.source.after()
320 303 self.map.close()
321 304
322 305 def convert(ui, src, dest=None, revmapfile=None, **opts):
323 306 global orig_encoding
324 307 orig_encoding = util._encoding
325 308 util._encoding = 'UTF-8'
326 309
327 310 if not dest:
328 311 dest = hg.defaultdest(src) + "-hg"
329 312 ui.status("assuming destination %s\n" % dest)
330 313
331 314 destc = convertsink(ui, dest, opts.get('dest_type'))
332 315
333 316 try:
334 317 srcc = convertsource(ui, src, opts.get('source_type'),
335 318 opts.get('rev'))
336 319 except Exception:
337 320 for path in destc.created:
338 321 shutil.rmtree(path, True)
339 322 raise
340 323
341 324 fmap = opts.get('filemap')
342 325 if fmap:
343 326 srcc = filemap.filemap_source(ui, srcc, fmap)
344 327 destc.setfilemapmode(True)
345 328
346 329 if not revmapfile:
347 330 try:
348 331 revmapfile = destc.revmapfile()
349 332 except:
350 333 revmapfile = os.path.join(destc, "map")
351 334
352 335 c = converter(ui, srcc, destc, revmapfile, opts)
353 336 c.convert()
354 337
@@ -1,302 +1,305 b''
1 1 # hg backend for convert extension
2 2
3 3 # Notes for hg->hg conversion:
4 4 #
5 5 # * Old versions of Mercurial didn't trim the whitespace from the ends
6 6 # of commit messages, but new versions do. Changesets created by
7 7 # those older versions, then converted, may thus have different
8 8 # hashes for changesets that are otherwise identical.
9 9 #
10 10 # * By default, the source revision is stored in the converted
11 11 # revision. This will cause the converted revision to have a
12 12 # different identity than the source. To avoid this, use the
13 13 # following option: "--config convert.hg.saverev=false"
14 14
15 15
16 16 import os, time
17 17 from mercurial.i18n import _
18 18 from mercurial.repo import RepoError
19 19 from mercurial.node import bin, hex, nullid
20 20 from mercurial import hg, revlog, util
21 21
22 22 from common import NoRepo, commit, converter_source, converter_sink
23 23
24 24 class mercurial_sink(converter_sink):
25 25 def __init__(self, ui, path):
26 26 converter_sink.__init__(self, ui, path)
27 27 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
28 28 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
29 29 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
30 30 self.lastbranch = None
31 31 if os.path.isdir(path) and len(os.listdir(path)) > 0:
32 32 try:
33 33 self.repo = hg.repository(self.ui, path)
34 34 if not self.repo.local():
35 35 raise NoRepo(_('%s is not a local Mercurial repo') % path)
36 36 except RepoError, err:
37 37 ui.print_exc()
38 38 raise NoRepo(err.args[0])
39 39 else:
40 40 try:
41 41 ui.status(_('initializing destination %s repository\n') % path)
42 42 self.repo = hg.repository(self.ui, path, create=True)
43 43 if not self.repo.local():
44 44 raise NoRepo(_('%s is not a local Mercurial repo') % path)
45 45 self.created.append(path)
46 46 except RepoError, err:
47 47 ui.print_exc()
48 48 raise NoRepo("could not create hg repo %s as sink" % path)
49 49 self.lock = None
50 50 self.wlock = None
51 51 self.filemapmode = False
52 52
53 53 def before(self):
54 54 self.ui.debug(_('run hg sink pre-conversion action\n'))
55 55 self.wlock = self.repo.wlock()
56 56 self.lock = self.repo.lock()
57 57 self.repo.dirstate.clear()
58 58
59 59 def after(self):
60 60 self.ui.debug(_('run hg sink post-conversion action\n'))
61 61 self.repo.dirstate.invalidate()
62 62 self.lock = None
63 63 self.wlock = None
64 64
65 65 def revmapfile(self):
66 66 return os.path.join(self.path, ".hg", "shamap")
67 67
68 68 def authorfile(self):
69 69 return os.path.join(self.path, ".hg", "authormap")
70 70
71 71 def getheads(self):
72 72 h = self.repo.changelog.heads()
73 73 return [ hex(x) for x in h ]
74 74
75 def putfile(self, f, e, data):
76 self.repo.wwrite(f, data, e)
77 if f not in self.repo.dirstate:
78 self.repo.dirstate.normallookup(f)
79
80 def copyfile(self, source, dest):
81 self.repo.copy(source, dest)
82
83 def delfile(self, f):
84 try:
85 util.unlink(self.repo.wjoin(f))
86 #self.repo.remove([f])
87 except OSError:
88 pass
89
90 75 def setbranch(self, branch, pbranches):
91 76 if not self.clonebranches:
92 77 return
93 78
94 79 setbranch = (branch != self.lastbranch)
95 80 self.lastbranch = branch
96 81 if not branch:
97 82 branch = 'default'
98 83 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
99 84 pbranch = pbranches and pbranches[0][1] or 'default'
100 85
101 86 branchpath = os.path.join(self.path, branch)
102 87 if setbranch:
103 88 self.after()
104 89 try:
105 90 self.repo = hg.repository(self.ui, branchpath)
106 91 except:
107 92 self.repo = hg.repository(self.ui, branchpath, create=True)
108 93 self.before()
109 94
110 95 # pbranches may bring revisions from other branches (merge parents)
111 96 # Make sure we have them, or pull them.
112 97 missings = {}
113 98 for b in pbranches:
114 99 try:
115 100 self.repo.lookup(b[0])
116 101 except:
117 102 missings.setdefault(b[1], []).append(b[0])
118 103
119 104 if missings:
120 105 self.after()
121 106 for pbranch, heads in missings.iteritems():
122 107 pbranchpath = os.path.join(self.path, pbranch)
123 108 prepo = hg.repository(self.ui, pbranchpath)
124 109 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
125 110 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
126 111 self.before()
127 112
128 def putcommit(self, files, parents, commit):
113 def putcommit(self, files, copies, parents, commit, source):
114 # Apply changes to working copy
115 for f, v in files:
116 try:
117 data = source.getfile(f, v)
118 except IOError, inst:
119 try:
120 util.unlink(self.repo.wjoin(f))
121 except OSError:
122 pass
123 else:
124 e = source.getmode(f, v)
125 self.repo.wwrite(f, data, e)
126 if f not in self.repo.dirstate:
127 self.repo.dirstate.normallookup(f)
128 if f in copies:
129 self.repo.copy(copies[f], f)
130 files = [f[0] for f in files]
131
129 132 seen = {}
130 133 pl = []
131 134 for p in parents:
132 135 if p not in seen:
133 136 pl.append(p)
134 137 seen[p] = 1
135 138 parents = pl
136 139 nparents = len(parents)
137 140 if self.filemapmode and nparents == 1:
138 141 m1node = self.repo.changelog.read(bin(parents[0]))[0]
139 142 parent = parents[0]
140 143
141 144 if len(parents) < 2: parents.append("0" * 40)
142 145 if len(parents) < 2: parents.append("0" * 40)
143 146 p2 = parents.pop(0)
144 147
145 148 text = commit.desc
146 149 extra = commit.extra.copy()
147 150 if self.branchnames and commit.branch:
148 151 extra['branch'] = commit.branch
149 152 if commit.rev:
150 153 extra['convert_revision'] = commit.rev
151 154
152 155 while parents:
153 156 p1 = p2
154 157 p2 = parents.pop(0)
155 158 a = self.repo.rawcommit(files, text, commit.author, commit.date,
156 159 bin(p1), bin(p2), extra=extra)
157 160 self.repo.dirstate.clear()
158 161 text = "(octopus merge fixup)\n"
159 162 p2 = hex(self.repo.changelog.tip())
160 163
161 164 if self.filemapmode and nparents == 1:
162 165 man = self.repo.manifest
163 166 mnode = self.repo.changelog.read(bin(p2))[0]
164 167 if not man.cmp(m1node, man.revision(mnode)):
165 168 self.repo.rollback()
166 169 self.repo.dirstate.clear()
167 170 return parent
168 171 return p2
169 172
170 173 def puttags(self, tags):
171 174 try:
172 175 old = self.repo.wfile(".hgtags").read()
173 176 oldlines = old.splitlines(1)
174 177 oldlines.sort()
175 178 except:
176 179 oldlines = []
177 180
178 181 k = tags.keys()
179 182 k.sort()
180 183 newlines = []
181 184 for tag in k:
182 185 newlines.append("%s %s\n" % (tags[tag], tag))
183 186
184 187 newlines.sort()
185 188
186 189 if newlines != oldlines:
187 190 self.ui.status("updating tags\n")
188 191 f = self.repo.wfile(".hgtags", "w")
189 192 f.write("".join(newlines))
190 193 f.close()
191 194 if not oldlines: self.repo.add([".hgtags"])
192 195 date = "%s 0" % int(time.mktime(time.gmtime()))
193 196 extra = {}
194 197 if self.tagsbranch != 'default':
195 198 extra['branch'] = self.tagsbranch
196 199 try:
197 200 tagparent = self.repo.changectx(self.tagsbranch).node()
198 201 except RepoError, inst:
199 202 tagparent = nullid
200 203 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
201 204 date, tagparent, nullid, extra=extra)
202 205 return hex(self.repo.changelog.tip())
203 206
204 207 def setfilemapmode(self, active):
205 208 self.filemapmode = active
206 209
207 210 class mercurial_source(converter_source):
208 211 def __init__(self, ui, path, rev=None):
209 212 converter_source.__init__(self, ui, path, rev)
210 213 self.saverev = ui.configbool('convert', 'hg.saverev', True)
211 214 try:
212 215 self.repo = hg.repository(self.ui, path)
213 216 # try to provoke an exception if this isn't really a hg
214 217 # repo, but some other bogus compatible-looking url
215 218 if not self.repo.local():
216 219 raise RepoError()
217 220 except RepoError:
218 221 ui.print_exc()
219 222 raise NoRepo("%s is not a local Mercurial repo" % path)
220 223 self.lastrev = None
221 224 self.lastctx = None
222 225 self._changescache = None
223 226 self.convertfp = None
224 227
225 228 def changectx(self, rev):
226 229 if self.lastrev != rev:
227 230 self.lastctx = self.repo.changectx(rev)
228 231 self.lastrev = rev
229 232 return self.lastctx
230 233
231 234 def getheads(self):
232 235 if self.rev:
233 236 return [hex(self.repo.changectx(self.rev).node())]
234 237 else:
235 238 return [hex(node) for node in self.repo.heads()]
236 239
237 240 def getfile(self, name, rev):
238 241 try:
239 242 return self.changectx(rev).filectx(name).data()
240 243 except revlog.LookupError, err:
241 244 raise IOError(err)
242 245
243 246 def getmode(self, name, rev):
244 247 m = self.changectx(rev).manifest()
245 248 return (m.execf(name) and 'x' or '') + (m.linkf(name) and 'l' or '')
246 249
247 250 def getchanges(self, rev):
248 251 ctx = self.changectx(rev)
249 252 if self._changescache and self._changescache[0] == rev:
250 253 m, a, r = self._changescache[1]
251 254 else:
252 255 m, a, r = self.repo.status(ctx.parents()[0].node(), ctx.node())[:3]
253 256 changes = [(name, rev) for name in m + a + r]
254 257 changes.sort()
255 258 return (changes, self.getcopies(ctx, m + a))
256 259
257 260 def getcopies(self, ctx, files):
258 261 copies = {}
259 262 for name in files:
260 263 try:
261 264 copies[name] = ctx.filectx(name).renamed()[0]
262 265 except TypeError:
263 266 pass
264 267 return copies
265 268
266 269 def getcommit(self, rev):
267 270 ctx = self.changectx(rev)
268 271 parents = [hex(p.node()) for p in ctx.parents() if p.node() != nullid]
269 272 if self.saverev:
270 273 crev = rev
271 274 else:
272 275 crev = None
273 276 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
274 277 desc=ctx.description(), rev=crev, parents=parents,
275 278 branch=ctx.branch(), extra=ctx.extra())
276 279
277 280 def gettags(self):
278 281 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
279 282 return dict([(name, hex(node)) for name, node in tags])
280 283
281 284 def getchangedfiles(self, rev, i):
282 285 ctx = self.changectx(rev)
283 286 i = i or 0
284 287 changes = self.repo.status(ctx.parents()[i].node(), ctx.node())[:3]
285 288
286 289 if i == 0:
287 290 self._changescache = (rev, changes)
288 291
289 292 return changes[0] + changes[1] + changes[2]
290 293
291 294 def converted(self, rev, destrev):
292 295 if self.convertfp is None:
293 296 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
294 297 'a')
295 298 self.convertfp.write('%s %s\n' % (destrev, rev))
296 299 self.convertfp.flush()
297 300
298 301 def before(self):
299 302 self.ui.debug(_('run hg source pre-conversion action\n'))
300 303
301 304 def after(self):
302 305 self.ui.debug(_('run hg source post-conversion action\n'))
@@ -1,1137 +1,1144 b''
1 1 # Subversion 1.4/1.5 Python API backend
2 2 #
3 3 # Copyright(C) 2007 Daniel Holth et al
4 4 #
5 5 # Configuration options:
6 6 #
7 7 # convert.svn.trunk
8 8 # Relative path to the trunk (default: "trunk")
9 9 # convert.svn.branches
10 10 # Relative path to tree of branches (default: "branches")
11 11 # convert.svn.tags
12 12 # Relative path to tree of tags (default: "tags")
13 13 #
14 14 # Set these in a hgrc, or on the command line as follows:
15 15 #
16 16 # hg convert --config convert.svn.trunk=wackoname [...]
17 17
18 18 import locale
19 19 import os
20 20 import re
21 21 import sys
22 22 import cPickle as pickle
23 23 import tempfile
24 24
25 25 from mercurial import strutil, util
26 26 from mercurial.i18n import _
27 27
28 28 # Subversion stuff. Works best with very recent Python SVN bindings
29 29 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
30 30 # these bindings.
31 31
32 32 from cStringIO import StringIO
33 33
34 34 from common import NoRepo, commit, converter_source, encodeargs, decodeargs
35 35 from common import commandline, converter_sink, mapfile
36 36
37 37 try:
38 38 from svn.core import SubversionException, Pool
39 39 import svn
40 40 import svn.client
41 41 import svn.core
42 42 import svn.ra
43 43 import svn.delta
44 44 import transport
45 45 except ImportError:
46 46 pass
47 47
48 48 def geturl(path):
49 49 try:
50 50 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
51 51 except SubversionException:
52 52 pass
53 53 if os.path.isdir(path):
54 54 path = os.path.normpath(os.path.abspath(path))
55 55 if os.name == 'nt':
56 56 path = '/' + util.normpath(path)
57 57 return 'file://%s' % path
58 58 return path
59 59
60 60 def optrev(number):
61 61 optrev = svn.core.svn_opt_revision_t()
62 62 optrev.kind = svn.core.svn_opt_revision_number
63 63 optrev.value.number = number
64 64 return optrev
65 65
66 66 class changedpath(object):
67 67 def __init__(self, p):
68 68 self.copyfrom_path = p.copyfrom_path
69 69 self.copyfrom_rev = p.copyfrom_rev
70 70 self.action = p.action
71 71
72 72 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
73 73 strict_node_history=False):
74 74 protocol = -1
75 75 def receiver(orig_paths, revnum, author, date, message, pool):
76 76 if orig_paths is not None:
77 77 for k, v in orig_paths.iteritems():
78 78 orig_paths[k] = changedpath(v)
79 79 pickle.dump((orig_paths, revnum, author, date, message),
80 80 fp, protocol)
81 81
82 82 try:
83 83 # Use an ra of our own so that our parent can consume
84 84 # our results without confusing the server.
85 85 t = transport.SvnRaTransport(url=url)
86 86 svn.ra.get_log(t.ra, paths, start, end, limit,
87 87 discover_changed_paths,
88 88 strict_node_history,
89 89 receiver)
90 90 except SubversionException, (inst, num):
91 91 pickle.dump(num, fp, protocol)
92 92 except IOError:
93 93 # Caller may interrupt the iteration
94 94 pickle.dump(None, fp, protocol)
95 95 else:
96 96 pickle.dump(None, fp, protocol)
97 97 fp.close()
98 98 # With large history, cleanup process goes crazy and suddenly
99 99 # consumes *huge* amount of memory. The output file being closed,
100 100 # there is no need for clean termination.
101 101 os._exit(0)
102 102
103 103 def debugsvnlog(ui, **opts):
104 104 """Fetch SVN log in a subprocess and channel them back to parent to
105 105 avoid memory collection issues.
106 106 """
107 107 util.set_binary(sys.stdin)
108 108 util.set_binary(sys.stdout)
109 109 args = decodeargs(sys.stdin.read())
110 110 get_log_child(sys.stdout, *args)
111 111
112 112 class logstream:
113 113 """Interruptible revision log iterator."""
114 114 def __init__(self, stdout):
115 115 self._stdout = stdout
116 116
117 117 def __iter__(self):
118 118 while True:
119 119 entry = pickle.load(self._stdout)
120 120 try:
121 121 orig_paths, revnum, author, date, message = entry
122 122 except:
123 123 if entry is None:
124 124 break
125 125 raise SubversionException("child raised exception", entry)
126 126 yield entry
127 127
128 128 def close(self):
129 129 if self._stdout:
130 130 self._stdout.close()
131 131 self._stdout = None
132 132
133 133 def get_log(url, paths, start, end, limit=0, discover_changed_paths=True,
134 134 strict_node_history=False):
135 135 args = [url, paths, start, end, limit, discover_changed_paths,
136 136 strict_node_history]
137 137 arg = encodeargs(args)
138 138 hgexe = util.hgexecutable()
139 139 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
140 140 stdin, stdout = os.popen2(cmd, 'b')
141 141 stdin.write(arg)
142 142 stdin.close()
143 143 return logstream(stdout)
144 144
145 145 # SVN conversion code stolen from bzr-svn and tailor
146 146 #
147 147 # Subversion looks like a versioned filesystem, branches structures
148 148 # are defined by conventions and not enforced by the tool. First,
149 149 # we define the potential branches (modules) as "trunk" and "branches"
150 150 # children directories. Revisions are then identified by their
151 151 # module and revision number (and a repository identifier).
152 152 #
153 153 # The revision graph is really a tree (or a forest). By default, a
154 154 # revision parent is the previous revision in the same module. If the
155 155 # module directory is copied/moved from another module then the
156 156 # revision is the module root and its parent the source revision in
157 157 # the parent module. A revision has at most one parent.
158 158 #
159 159 class svn_source(converter_source):
160 160 def __init__(self, ui, url, rev=None):
161 161 super(svn_source, self).__init__(ui, url, rev=rev)
162 162
163 163 try:
164 164 SubversionException
165 165 except NameError:
166 166 raise NoRepo('Subversion python bindings could not be loaded')
167 167
168 168 self.encoding = locale.getpreferredencoding()
169 169 self.lastrevs = {}
170 170
171 171 latest = None
172 172 try:
173 173 # Support file://path@rev syntax. Useful e.g. to convert
174 174 # deleted branches.
175 175 at = url.rfind('@')
176 176 if at >= 0:
177 177 latest = int(url[at+1:])
178 178 url = url[:at]
179 179 except ValueError, e:
180 180 pass
181 181 self.url = geturl(url)
182 182 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
183 183 try:
184 184 self.transport = transport.SvnRaTransport(url=self.url)
185 185 self.ra = self.transport.ra
186 186 self.ctx = self.transport.client
187 187 self.base = svn.ra.get_repos_root(self.ra)
188 188 # Module is either empty or a repository path starting with
189 189 # a slash and not ending with a slash.
190 190 self.module = self.url[len(self.base):]
191 191 self.rootmodule = self.module
192 192 self.commits = {}
193 193 self.paths = {}
194 194 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
195 195 except SubversionException, e:
196 196 ui.print_exc()
197 197 raise NoRepo("%s does not look like a Subversion repo" % self.url)
198 198
199 199 if rev:
200 200 try:
201 201 latest = int(rev)
202 202 except ValueError:
203 203 raise util.Abort('svn: revision %s is not an integer' % rev)
204 204
205 205 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
206 206 try:
207 207 self.startrev = int(self.startrev)
208 208 if self.startrev < 0:
209 209 self.startrev = 0
210 210 except ValueError:
211 211 raise util.Abort(_('svn: start revision %s is not an integer')
212 212 % self.startrev)
213 213
214 214 try:
215 215 self.get_blacklist()
216 216 except IOError, e:
217 217 pass
218 218
219 219 self.head = self.latest(self.module, latest)
220 220 if not self.head:
221 221 raise util.Abort(_('no revision found in module %s') %
222 222 self.module.encode(self.encoding))
223 223 self.last_changed = self.revnum(self.head)
224 224
225 225 self._changescache = None
226 226
227 227 if os.path.exists(os.path.join(url, '.svn/entries')):
228 228 self.wc = url
229 229 else:
230 230 self.wc = None
231 231 self.convertfp = None
232 232
233 233 def setrevmap(self, revmap):
234 234 lastrevs = {}
235 235 for revid in revmap.iterkeys():
236 236 uuid, module, revnum = self.revsplit(revid)
237 237 lastrevnum = lastrevs.setdefault(module, revnum)
238 238 if revnum > lastrevnum:
239 239 lastrevs[module] = revnum
240 240 self.lastrevs = lastrevs
241 241
242 242 def exists(self, path, optrev):
243 243 try:
244 244 svn.client.ls(self.url.rstrip('/') + '/' + path,
245 245 optrev, False, self.ctx)
246 246 return True
247 247 except SubversionException, err:
248 248 return False
249 249
250 250 def getheads(self):
251 251
252 252 def isdir(path, revnum):
253 253 kind = svn.ra.check_path(self.ra, path, revnum)
254 254 return kind == svn.core.svn_node_dir
255 255
256 256 def getcfgpath(name, rev):
257 257 cfgpath = self.ui.config('convert', 'svn.' + name)
258 258 if cfgpath is not None and cfgpath.strip() == '':
259 259 return None
260 260 path = (cfgpath or name).strip('/')
261 261 if not self.exists(path, rev):
262 262 if cfgpath:
263 263 raise util.Abort(_('expected %s to be at %r, but not found')
264 264 % (name, path))
265 265 return None
266 266 self.ui.note(_('found %s at %r\n') % (name, path))
267 267 return path
268 268
269 269 rev = optrev(self.last_changed)
270 270 oldmodule = ''
271 271 trunk = getcfgpath('trunk', rev)
272 272 self.tags = getcfgpath('tags', rev)
273 273 branches = getcfgpath('branches', rev)
274 274
275 275 # If the project has a trunk or branches, we will extract heads
276 276 # from them. We keep the project root otherwise.
277 277 if trunk:
278 278 oldmodule = self.module or ''
279 279 self.module += '/' + trunk
280 280 self.head = self.latest(self.module, self.last_changed)
281 281 if not self.head:
282 282 raise util.Abort(_('no revision found in module %s') %
283 283 self.module.encode(self.encoding))
284 284
285 285 # First head in the list is the module's head
286 286 self.heads = [self.head]
287 287 if self.tags is not None:
288 288 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
289 289
290 290 # Check if branches bring a few more heads to the list
291 291 if branches:
292 292 rpath = self.url.strip('/')
293 293 branchnames = svn.client.ls(rpath + '/' + branches, rev, False,
294 294 self.ctx)
295 295 for branch in branchnames.keys():
296 296 module = '%s/%s/%s' % (oldmodule, branches, branch)
297 297 if not isdir(module, self.last_changed):
298 298 continue
299 299 brevid = self.latest(module, self.last_changed)
300 300 if not brevid:
301 301 self.ui.note(_('ignoring empty branch %s\n') %
302 302 branch.encode(self.encoding))
303 303 continue
304 304 self.ui.note('found branch %s at %d\n' %
305 305 (branch, self.revnum(brevid)))
306 306 self.heads.append(brevid)
307 307
308 308 if self.startrev and self.heads:
309 309 if len(self.heads) > 1:
310 310 raise util.Abort(_('svn: start revision is not supported with '
311 311 'with more than one branch'))
312 312 revnum = self.revnum(self.heads[0])
313 313 if revnum < self.startrev:
314 314 raise util.Abort(_('svn: no revision found after start revision %d')
315 315 % self.startrev)
316 316
317 317 return self.heads
318 318
319 319 def getfile(self, file, rev):
320 320 data, mode = self._getfile(file, rev)
321 321 self.modecache[(file, rev)] = mode
322 322 return data
323 323
324 324 def getmode(self, file, rev):
325 325 return self.modecache[(file, rev)]
326 326
327 327 def getchanges(self, rev):
328 328 if self._changescache and self._changescache[0] == rev:
329 329 return self._changescache[1]
330 330 self._changescache = None
331 331 self.modecache = {}
332 332 (paths, parents) = self.paths[rev]
333 333 if parents:
334 334 files, copies = self.expandpaths(rev, paths, parents)
335 335 else:
336 336 # Perform a full checkout on roots
337 337 uuid, module, revnum = self.revsplit(rev)
338 338 entries = svn.client.ls(self.base + module, optrev(revnum),
339 339 True, self.ctx)
340 340 files = [n for n,e in entries.iteritems()
341 341 if e.kind == svn.core.svn_node_file]
342 342 copies = {}
343 343
344 344 files.sort()
345 345 files = zip(files, [rev] * len(files))
346 346
347 347 # caller caches the result, so free it here to release memory
348 348 del self.paths[rev]
349 349 return (files, copies)
350 350
351 351 def getchangedfiles(self, rev, i):
352 352 changes = self.getchanges(rev)
353 353 self._changescache = (rev, changes)
354 354 return [f[0] for f in changes[0]]
355 355
356 356 def getcommit(self, rev):
357 357 if rev not in self.commits:
358 358 uuid, module, revnum = self.revsplit(rev)
359 359 self.module = module
360 360 self.reparent(module)
361 361 # We assume that:
362 362 # - requests for revisions after "stop" come from the
363 363 # revision graph backward traversal. Cache all of them
364 364 # down to stop, they will be used eventually.
365 365 # - requests for revisions before "stop" come to get
366 366 # isolated branches parents. Just fetch what is needed.
367 367 stop = self.lastrevs.get(module, 0)
368 368 if revnum < stop:
369 369 stop = revnum + 1
370 370 self._fetch_revisions(revnum, stop)
371 371 commit = self.commits[rev]
372 372 # caller caches the result, so free it here to release memory
373 373 del self.commits[rev]
374 374 return commit
375 375
376 376 def gettags(self):
377 377 tags = {}
378 378 if self.tags is None:
379 379 return tags
380 380
381 381 # svn tags are just a convention, project branches left in a
382 382 # 'tags' directory. There is no other relationship than
383 383 # ancestry, which is expensive to discover and makes them hard
384 384 # to update incrementally. Worse, past revisions may be
385 385 # referenced by tags far away in the future, requiring a deep
386 386 # history traversal on every calculation. Current code
387 387 # performs a single backward traversal, tracking moves within
388 388 # the tags directory (tag renaming) and recording a new tag
389 389 # everytime a project is copied from outside the tags
390 390 # directory. It also lists deleted tags, this behaviour may
391 391 # change in the future.
392 392 pendings = []
393 393 tagspath = self.tags
394 394 start = svn.ra.get_latest_revnum(self.ra)
395 395 try:
396 396 for entry in get_log(self.url, [self.tags], start, self.startrev):
397 397 origpaths, revnum, author, date, message = entry
398 398 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
399 399 in origpaths.iteritems() if e.copyfrom_path]
400 400 copies.sort()
401 401 # Apply moves/copies from more specific to general
402 402 copies.reverse()
403 403
404 404 srctagspath = tagspath
405 405 if copies and copies[-1][2] == tagspath:
406 406 # Track tags directory moves
407 407 srctagspath = copies.pop()[0]
408 408
409 409 for source, sourcerev, dest in copies:
410 410 if not dest.startswith(tagspath + '/'):
411 411 continue
412 412 for tag in pendings:
413 413 if tag[0].startswith(dest):
414 414 tagpath = source + tag[0][len(dest):]
415 415 tag[:2] = [tagpath, sourcerev]
416 416 break
417 417 else:
418 418 pendings.append([source, sourcerev, dest.split('/')[-1]])
419 419
420 420 # Tell tag renamings from tag creations
421 421 remainings = []
422 422 for source, sourcerev, tagname in pendings:
423 423 if source.startswith(srctagspath):
424 424 remainings.append([source, sourcerev, tagname])
425 425 continue
426 426 # From revision may be fake, get one with changes
427 427 tagid = self.latest(source, sourcerev)
428 428 if tagid:
429 429 tags[tagname] = tagid
430 430 pendings = remainings
431 431 tagspath = srctagspath
432 432
433 433 except SubversionException, (inst, num):
434 434 self.ui.note('no tags found at revision %d\n' % start)
435 435 return tags
436 436
437 437 def converted(self, rev, destrev):
438 438 if not self.wc:
439 439 return
440 440 if self.convertfp is None:
441 441 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
442 442 'a')
443 443 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
444 444 self.convertfp.flush()
445 445
446 446 # -- helper functions --
447 447
448 448 def revid(self, revnum, module=None):
449 449 if not module:
450 450 module = self.module
451 451 return u"svn:%s%s@%s" % (self.uuid, module.decode(self.encoding),
452 452 revnum)
453 453
454 454 def revnum(self, rev):
455 455 return int(rev.split('@')[-1])
456 456
457 457 def revsplit(self, rev):
458 458 url, revnum = rev.encode(self.encoding).split('@', 1)
459 459 revnum = int(revnum)
460 460 parts = url.split('/', 1)
461 461 uuid = parts.pop(0)[4:]
462 462 mod = ''
463 463 if parts:
464 464 mod = '/' + parts[0]
465 465 return uuid, mod, revnum
466 466
467 467 def latest(self, path, stop=0):
468 468 """Find the latest revid affecting path, up to stop. It may return
469 469 a revision in a different module, since a branch may be moved without
470 470 a change being reported. Return None if computed module does not
471 471 belong to rootmodule subtree.
472 472 """
473 473 if not path.startswith(self.rootmodule):
474 474 # Requests on foreign branches may be forbidden at server level
475 475 self.ui.debug(_('ignoring foreign branch %r\n') % path)
476 476 return None
477 477
478 478 if not stop:
479 479 stop = svn.ra.get_latest_revnum(self.ra)
480 480 try:
481 481 self.reparent('')
482 482 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
483 483 self.reparent(self.module)
484 484 except SubversionException:
485 485 dirent = None
486 486 if not dirent:
487 487 raise util.Abort('%s not found up to revision %d' % (path, stop))
488 488
489 489 # stat() gives us the previous revision on this line of development, but
490 490 # it might be in *another module*. Fetch the log and detect renames down
491 491 # to the latest revision.
492 492 stream = get_log(self.url, [path], stop, dirent.created_rev)
493 493 try:
494 494 for entry in stream:
495 495 paths, revnum, author, date, message = entry
496 496 if revnum <= dirent.created_rev:
497 497 break
498 498
499 499 for p in paths:
500 500 if not path.startswith(p) or not paths[p].copyfrom_path:
501 501 continue
502 502 newpath = paths[p].copyfrom_path + path[len(p):]
503 503 self.ui.debug("branch renamed from %s to %s at %d\n" %
504 504 (path, newpath, revnum))
505 505 path = newpath
506 506 break
507 507 finally:
508 508 stream.close()
509 509
510 510 if not path.startswith(self.rootmodule):
511 511 self.ui.debug(_('ignoring foreign branch %r\n') % path)
512 512 return None
513 513 return self.revid(dirent.created_rev, path)
514 514
515 515 def get_blacklist(self):
516 516 """Avoid certain revision numbers.
517 517 It is not uncommon for two nearby revisions to cancel each other
518 518 out, e.g. 'I copied trunk into a subdirectory of itself instead
519 519 of making a branch'. The converted repository is significantly
520 520 smaller if we ignore such revisions."""
521 521 self.blacklist = util.set()
522 522 blacklist = self.blacklist
523 523 for line in file("blacklist.txt", "r"):
524 524 if not line.startswith("#"):
525 525 try:
526 526 svn_rev = int(line.strip())
527 527 blacklist.add(svn_rev)
528 528 except ValueError, e:
529 529 pass # not an integer or a comment
530 530
531 531 def is_blacklisted(self, svn_rev):
532 532 return svn_rev in self.blacklist
533 533
534 534 def reparent(self, module):
535 535 svn_url = self.base + module
536 536 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
537 537 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
538 538
539 539 def expandpaths(self, rev, paths, parents):
540 540 entries = []
541 541 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
542 542 copies = {}
543 543
544 544 new_module, revnum = self.revsplit(rev)[1:]
545 545 if new_module != self.module:
546 546 self.module = new_module
547 547 self.reparent(self.module)
548 548
549 549 for path, ent in paths:
550 550 entrypath = self.getrelpath(path)
551 551 entry = entrypath.decode(self.encoding)
552 552
553 553 kind = svn.ra.check_path(self.ra, entrypath, revnum)
554 554 if kind == svn.core.svn_node_file:
555 555 entries.append(self.recode(entry))
556 556 if not ent.copyfrom_path or not parents:
557 557 continue
558 558 # Copy sources not in parent revisions cannot be represented,
559 559 # ignore their origin for now
560 560 pmodule, prevnum = self.revsplit(parents[0])[1:]
561 561 if ent.copyfrom_rev < prevnum:
562 562 continue
563 563 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
564 564 if not copyfrom_path:
565 565 continue
566 566 self.ui.debug("copied to %s from %s@%s\n" %
567 567 (entrypath, copyfrom_path, ent.copyfrom_rev))
568 568 copies[self.recode(entry)] = self.recode(copyfrom_path)
569 569 elif kind == 0: # gone, but had better be a deleted *file*
570 570 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
571 571
572 572 # if a branch is created but entries are removed in the same
573 573 # changeset, get the right fromrev
574 574 # parents cannot be empty here, you cannot remove things from
575 575 # a root revision.
576 576 uuid, old_module, fromrev = self.revsplit(parents[0])
577 577
578 578 basepath = old_module + "/" + self.getrelpath(path)
579 579 entrypath = basepath
580 580
581 581 def lookup_parts(p):
582 582 rc = None
583 583 parts = p.split("/")
584 584 for i in range(len(parts)):
585 585 part = "/".join(parts[:i])
586 586 info = part, copyfrom.get(part, None)
587 587 if info[1] is not None:
588 588 self.ui.debug("Found parent directory %s\n" % info[1])
589 589 rc = info
590 590 return rc
591 591
592 592 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
593 593
594 594 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
595 595
596 596 # need to remove fragment from lookup_parts and replace with copyfrom_path
597 597 if frompath is not None:
598 598 self.ui.debug("munge-o-matic\n")
599 599 self.ui.debug(entrypath + '\n')
600 600 self.ui.debug(entrypath[len(frompath):] + '\n')
601 601 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
602 602 fromrev = froment.copyfrom_rev
603 603 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
604 604
605 605 # We can avoid the reparent calls if the module has not changed
606 606 # but it probably does not worth the pain.
607 607 self.reparent('')
608 608 fromkind = svn.ra.check_path(self.ra, entrypath.strip('/'), fromrev)
609 609 self.reparent(self.module)
610 610
611 611 if fromkind == svn.core.svn_node_file: # a deleted file
612 612 entries.append(self.recode(entry))
613 613 elif fromkind == svn.core.svn_node_dir:
614 614 # print "Deleted/moved non-file:", revnum, path, ent
615 615 # children = self._find_children(path, revnum - 1)
616 616 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
617 617 # Sometimes this is tricky. For example: in
618 618 # The Subversion Repository revision 6940 a dir
619 619 # was copied and one of its files was deleted
620 620 # from the new location in the same commit. This
621 621 # code can't deal with that yet.
622 622 if ent.action == 'C':
623 623 children = self._find_children(path, fromrev)
624 624 else:
625 625 oroot = entrypath.strip('/')
626 626 nroot = path.strip('/')
627 627 children = self._find_children(oroot, fromrev)
628 628 children = [s.replace(oroot,nroot) for s in children]
629 629 # Mark all [files, not directories] as deleted.
630 630 for child in children:
631 631 # Can we move a child directory and its
632 632 # parent in the same commit? (probably can). Could
633 633 # cause problems if instead of revnum -1,
634 634 # we have to look in (copyfrom_path, revnum - 1)
635 635 entrypath = self.getrelpath("/" + child, module=old_module)
636 636 if entrypath:
637 637 entry = self.recode(entrypath.decode(self.encoding))
638 638 if entry in copies:
639 639 # deleted file within a copy
640 640 del copies[entry]
641 641 else:
642 642 entries.append(entry)
643 643 else:
644 644 self.ui.debug('unknown path in revision %d: %s\n' % \
645 645 (revnum, path))
646 646 elif kind == svn.core.svn_node_dir:
647 647 # Should probably synthesize normal file entries
648 648 # and handle as above to clean up copy/rename handling.
649 649
650 650 # If the directory just had a prop change,
651 651 # then we shouldn't need to look for its children.
652 652 if ent.action == 'M':
653 653 continue
654 654
655 655 # Also this could create duplicate entries. Not sure
656 656 # whether this will matter. Maybe should make entries a set.
657 657 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
658 658 # This will fail if a directory was copied
659 659 # from another branch and then some of its files
660 660 # were deleted in the same transaction.
661 661 children = self._find_children(path, revnum)
662 662 children.sort()
663 663 for child in children:
664 664 # Can we move a child directory and its
665 665 # parent in the same commit? (probably can). Could
666 666 # cause problems if instead of revnum -1,
667 667 # we have to look in (copyfrom_path, revnum - 1)
668 668 entrypath = self.getrelpath("/" + child)
669 669 # print child, self.module, entrypath
670 670 if entrypath:
671 671 # Need to filter out directories here...
672 672 kind = svn.ra.check_path(self.ra, entrypath, revnum)
673 673 if kind != svn.core.svn_node_dir:
674 674 entries.append(self.recode(entrypath))
675 675
676 676 # Copies here (must copy all from source)
677 677 # Probably not a real problem for us if
678 678 # source does not exist
679 679 if not ent.copyfrom_path or not parents:
680 680 continue
681 681 # Copy sources not in parent revisions cannot be represented,
682 682 # ignore their origin for now
683 683 pmodule, prevnum = self.revsplit(parents[0])[1:]
684 684 if ent.copyfrom_rev < prevnum:
685 685 continue
686 686 copyfrompath = ent.copyfrom_path.decode(self.encoding)
687 687 copyfrompath = self.getrelpath(copyfrompath, pmodule)
688 688 if not copyfrompath:
689 689 continue
690 690 copyfrom[path] = ent
691 691 self.ui.debug("mark %s came from %s:%d\n"
692 692 % (path, copyfrompath, ent.copyfrom_rev))
693 693 children = self._find_children(ent.copyfrom_path, ent.copyfrom_rev)
694 694 children.sort()
695 695 for child in children:
696 696 entrypath = self.getrelpath("/" + child, pmodule)
697 697 if not entrypath:
698 698 continue
699 699 entry = entrypath.decode(self.encoding)
700 700 copytopath = path + entry[len(copyfrompath):]
701 701 copytopath = self.getrelpath(copytopath)
702 702 copies[self.recode(copytopath)] = self.recode(entry, pmodule)
703 703
704 704 return (util.unique(entries), copies)
705 705
706 706 def _fetch_revisions(self, from_revnum, to_revnum):
707 707 if from_revnum < to_revnum:
708 708 from_revnum, to_revnum = to_revnum, from_revnum
709 709
710 710 self.child_cset = None
711 711
712 712 def isdescendantof(parent, child):
713 713 if not child or not parent or not child.startswith(parent):
714 714 return False
715 715 subpath = child[len(parent):]
716 716 return len(subpath) > 1 and subpath[0] == '/'
717 717
718 718 def parselogentry(orig_paths, revnum, author, date, message):
719 719 """Return the parsed commit object or None, and True if
720 720 the revision is a branch root.
721 721 """
722 722 self.ui.debug("parsing revision %d (%d changes)\n" %
723 723 (revnum, len(orig_paths)))
724 724
725 725 branched = False
726 726 rev = self.revid(revnum)
727 727 # branch log might return entries for a parent we already have
728 728
729 729 if (rev in self.commits or revnum < to_revnum):
730 730 return None, branched
731 731
732 732 parents = []
733 733 # check whether this revision is the start of a branch or part
734 734 # of a branch renaming
735 735 orig_paths = orig_paths.items()
736 736 orig_paths.sort()
737 737 root_paths = [(p,e) for p,e in orig_paths if self.module.startswith(p)]
738 738 if root_paths:
739 739 path, ent = root_paths[-1]
740 740 if ent.copyfrom_path:
741 741 # If dir was moved while one of its file was removed
742 742 # the log may look like:
743 743 # A /dir (from /dir:x)
744 744 # A /dir/a (from /dir/a:y)
745 745 # A /dir/b (from /dir/b:z)
746 746 # ...
747 747 # for all remaining children.
748 748 # Let's take the highest child element from rev as source.
749 749 copies = [(p,e) for p,e in orig_paths[:-1]
750 750 if isdescendantof(ent.copyfrom_path, e.copyfrom_path)]
751 751 fromrev = max([e.copyfrom_rev for p,e in copies] + [ent.copyfrom_rev])
752 752 branched = True
753 753 newpath = ent.copyfrom_path + self.module[len(path):]
754 754 # ent.copyfrom_rev may not be the actual last revision
755 755 previd = self.latest(newpath, fromrev)
756 756 if previd is not None:
757 757 prevmodule, prevnum = self.revsplit(previd)[1:]
758 758 if prevnum >= self.startrev:
759 759 parents = [previd]
760 760 self.ui.note('found parent of branch %s at %d: %s\n' %
761 761 (self.module, prevnum, prevmodule))
762 762 else:
763 763 self.ui.debug("No copyfrom path, don't know what to do.\n")
764 764
765 765 paths = []
766 766 # filter out unrelated paths
767 767 for path, ent in orig_paths:
768 768 if self.getrelpath(path) is None:
769 769 continue
770 770 paths.append((path, ent))
771 771
772 772 # Example SVN datetime. Includes microseconds.
773 773 # ISO-8601 conformant
774 774 # '2007-01-04T17:35:00.902377Z'
775 775 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
776 776
777 777 log = message and self.recode(message) or ''
778 778 author = author and self.recode(author) or ''
779 779 try:
780 780 branch = self.module.split("/")[-1]
781 781 if branch == 'trunk':
782 782 branch = ''
783 783 except IndexError:
784 784 branch = None
785 785
786 786 cset = commit(author=author,
787 787 date=util.datestr(date),
788 788 desc=log,
789 789 parents=parents,
790 790 branch=branch,
791 791 rev=rev.encode('utf-8'))
792 792
793 793 self.commits[rev] = cset
794 794 # The parents list is *shared* among self.paths and the
795 795 # commit object. Both will be updated below.
796 796 self.paths[rev] = (paths, cset.parents)
797 797 if self.child_cset and not self.child_cset.parents:
798 798 self.child_cset.parents[:] = [rev]
799 799 self.child_cset = cset
800 800 return cset, branched
801 801
802 802 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
803 803 (self.module, from_revnum, to_revnum))
804 804
805 805 try:
806 806 firstcset = None
807 807 lastonbranch = False
808 808 stream = get_log(self.url, [self.module], from_revnum, to_revnum)
809 809 try:
810 810 for entry in stream:
811 811 paths, revnum, author, date, message = entry
812 812 if revnum < self.startrev:
813 813 lastonbranch = True
814 814 break
815 815 if self.is_blacklisted(revnum):
816 816 self.ui.note('skipping blacklisted revision %d\n'
817 817 % revnum)
818 818 continue
819 819 if paths is None:
820 820 self.ui.debug('revision %d has no entries\n' % revnum)
821 821 continue
822 822 cset, lastonbranch = parselogentry(paths, revnum, author,
823 823 date, message)
824 824 if cset:
825 825 firstcset = cset
826 826 if lastonbranch:
827 827 break
828 828 finally:
829 829 stream.close()
830 830
831 831 if not lastonbranch and firstcset and not firstcset.parents:
832 832 # The first revision of the sequence (the last fetched one)
833 833 # has invalid parents if not a branch root. Find the parent
834 834 # revision now, if any.
835 835 try:
836 836 firstrevnum = self.revnum(firstcset.rev)
837 837 if firstrevnum > 1:
838 838 latest = self.latest(self.module, firstrevnum - 1)
839 839 if latest:
840 840 firstcset.parents.append(latest)
841 841 except util.Abort:
842 842 pass
843 843 except SubversionException, (inst, num):
844 844 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
845 845 raise util.Abort('svn: branch has no revision %s' % to_revnum)
846 846 raise
847 847
848 848 def _getfile(self, file, rev):
849 849 io = StringIO()
850 850 # TODO: ra.get_file transmits the whole file instead of diffs.
851 851 mode = ''
852 852 try:
853 853 new_module, revnum = self.revsplit(rev)[1:]
854 854 if self.module != new_module:
855 855 self.module = new_module
856 856 self.reparent(self.module)
857 857 info = svn.ra.get_file(self.ra, file, revnum, io)
858 858 if isinstance(info, list):
859 859 info = info[-1]
860 860 mode = ("svn:executable" in info) and 'x' or ''
861 861 mode = ("svn:special" in info) and 'l' or mode
862 862 except SubversionException, e:
863 863 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
864 864 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
865 865 if e.apr_err in notfound: # File not found
866 866 raise IOError()
867 867 raise
868 868 data = io.getvalue()
869 869 if mode == 'l':
870 870 link_prefix = "link "
871 871 if data.startswith(link_prefix):
872 872 data = data[len(link_prefix):]
873 873 return data, mode
874 874
875 875 def _find_children(self, path, revnum):
876 876 path = path.strip('/')
877 877 pool = Pool()
878 878 rpath = '/'.join([self.base, path]).strip('/')
879 879 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool).keys()]
880 880
881 881 def getrelpath(self, path, module=None):
882 882 if module is None:
883 883 module = self.module
884 884 # Given the repository url of this wc, say
885 885 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
886 886 # extract the "entry" portion (a relative path) from what
887 887 # svn log --xml says, ie
888 888 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
889 889 # that is to say "tests/PloneTestCase.py"
890 890 if path.startswith(module):
891 891 relative = path.rstrip('/')[len(module):]
892 892 if relative.startswith('/'):
893 893 return relative[1:]
894 894 elif relative == '':
895 895 return relative
896 896
897 897 # The path is outside our tracked tree...
898 898 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
899 899 return None
900 900
901 901 pre_revprop_change = '''#!/bin/sh
902 902
903 903 REPOS="$1"
904 904 REV="$2"
905 905 USER="$3"
906 906 PROPNAME="$4"
907 907 ACTION="$5"
908 908
909 909 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
910 910 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
911 911 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
912 912
913 913 echo "Changing prohibited revision property" >&2
914 914 exit 1
915 915 '''
916 916
917 917 class svn_sink(converter_sink, commandline):
918 918 commit_re = re.compile(r'Committed revision (\d+).', re.M)
919 919
920 920 def prerun(self):
921 921 if self.wc:
922 922 os.chdir(self.wc)
923 923
924 924 def postrun(self):
925 925 if self.wc:
926 926 os.chdir(self.cwd)
927 927
928 928 def join(self, name):
929 929 return os.path.join(self.wc, '.svn', name)
930 930
931 931 def revmapfile(self):
932 932 return self.join('hg-shamap')
933 933
934 934 def authorfile(self):
935 935 return self.join('hg-authormap')
936 936
937 937 def __init__(self, ui, path):
938 938 converter_sink.__init__(self, ui, path)
939 939 commandline.__init__(self, ui, 'svn')
940 940 self.delete = []
941 941 self.setexec = []
942 942 self.delexec = []
943 943 self.copies = []
944 944 self.wc = None
945 945 self.cwd = os.getcwd()
946 946
947 947 path = os.path.realpath(path)
948 948
949 949 created = False
950 950 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
951 951 self.wc = path
952 952 self.run0('update')
953 953 else:
954 954 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
955 955
956 956 if os.path.isdir(os.path.dirname(path)):
957 957 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
958 958 ui.status(_('initializing svn repo %r\n') %
959 959 os.path.basename(path))
960 960 commandline(ui, 'svnadmin').run0('create', path)
961 961 created = path
962 962 path = util.normpath(path)
963 963 if not path.startswith('/'):
964 964 path = '/' + path
965 965 path = 'file://' + path
966 966
967 967 ui.status(_('initializing svn wc %r\n') % os.path.basename(wcpath))
968 968 self.run0('checkout', path, wcpath)
969 969
970 970 self.wc = wcpath
971 971 self.opener = util.opener(self.wc)
972 972 self.wopener = util.opener(self.wc)
973 973 self.childmap = mapfile(ui, self.join('hg-childmap'))
974 974 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
975 975
976 976 if created:
977 977 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
978 978 fp = open(hook, 'w')
979 979 fp.write(pre_revprop_change)
980 980 fp.close()
981 981 util.set_flags(hook, "x")
982 982
983 983 xport = transport.SvnRaTransport(url=geturl(path))
984 984 self.uuid = svn.ra.get_uuid(xport.ra)
985 985
986 986 def wjoin(self, *names):
987 987 return os.path.join(self.wc, *names)
988 988
989 989 def putfile(self, filename, flags, data):
990 990 if 'l' in flags:
991 991 self.wopener.symlink(data, filename)
992 992 else:
993 993 try:
994 994 if os.path.islink(self.wjoin(filename)):
995 995 os.unlink(filename)
996 996 except OSError:
997 997 pass
998 998 self.wopener(filename, 'w').write(data)
999 999
1000 1000 if self.is_exec:
1001 1001 was_exec = self.is_exec(self.wjoin(filename))
1002 1002 else:
1003 1003 # On filesystems not supporting execute-bit, there is no way
1004 1004 # to know if it is set but asking subversion. Setting it
1005 1005 # systematically is just as expensive and much simpler.
1006 1006 was_exec = 'x' not in flags
1007 1007
1008 1008 util.set_flags(self.wjoin(filename), flags)
1009 1009 if was_exec:
1010 1010 if 'x' not in flags:
1011 1011 self.delexec.append(filename)
1012 1012 else:
1013 1013 if 'x' in flags:
1014 1014 self.setexec.append(filename)
1015 1015
1016 def delfile(self, name):
1017 self.delete.append(name)
1018
1019 def copyfile(self, source, dest):
1020 self.copies.append([source, dest])
1021
1022 1016 def _copyfile(self, source, dest):
1023 1017 # SVN's copy command pukes if the destination file exists, but
1024 1018 # our copyfile method expects to record a copy that has
1025 1019 # already occurred. Cross the semantic gap.
1026 1020 wdest = self.wjoin(dest)
1027 1021 exists = os.path.exists(wdest)
1028 1022 if exists:
1029 1023 fd, tempname = tempfile.mkstemp(
1030 1024 prefix='hg-copy-', dir=os.path.dirname(wdest))
1031 1025 os.close(fd)
1032 1026 os.unlink(tempname)
1033 1027 os.rename(wdest, tempname)
1034 1028 try:
1035 1029 self.run0('copy', source, dest)
1036 1030 finally:
1037 1031 if exists:
1038 1032 try:
1039 1033 os.unlink(wdest)
1040 1034 except OSError:
1041 1035 pass
1042 1036 os.rename(tempname, wdest)
1043 1037
1044 1038 def dirs_of(self, files):
1045 1039 dirs = util.set()
1046 1040 for f in files:
1047 1041 if os.path.isdir(self.wjoin(f)):
1048 1042 dirs.add(f)
1049 1043 for i in strutil.rfindall(f, '/'):
1050 1044 dirs.add(f[:i])
1051 1045 return dirs
1052 1046
1053 1047 def add_dirs(self, files):
1054 1048 add_dirs = [d for d in self.dirs_of(files)
1055 1049 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1056 1050 if add_dirs:
1057 1051 add_dirs.sort()
1058 1052 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1059 1053 return add_dirs
1060 1054
1061 1055 def add_files(self, files):
1062 1056 if files:
1063 1057 self.xargs(files, 'add', quiet=True)
1064 1058 return files
1065 1059
1066 1060 def tidy_dirs(self, names):
1067 1061 dirs = list(self.dirs_of(names))
1068 1062 dirs.sort()
1069 1063 dirs.reverse()
1070 1064 deleted = []
1071 1065 for d in dirs:
1072 1066 wd = self.wjoin(d)
1073 1067 if os.listdir(wd) == '.svn':
1074 1068 self.run0('delete', d)
1075 1069 deleted.append(d)
1076 1070 return deleted
1077 1071
1078 1072 def addchild(self, parent, child):
1079 1073 self.childmap[parent] = child
1080 1074
1081 1075 def revid(self, rev):
1082 1076 return u"svn:%s@%s" % (self.uuid, rev)
1083 1077
1084 def putcommit(self, files, parents, commit):
1078 def putcommit(self, files, copies, parents, commit, source):
1079 # Apply changes to working copy
1080 for f, v in files:
1081 try:
1082 data = source.getfile(f, v)
1083 except IOError, inst:
1084 self.delete.append(f)
1085 else:
1086 e = source.getmode(f, v)
1087 self.putfile(f, e, data)
1088 if f in copies:
1089 self.copies.append([copies[f], f])
1090 files = [f[0] for f in files]
1091
1085 1092 for parent in parents:
1086 1093 try:
1087 1094 return self.revid(self.childmap[parent])
1088 1095 except KeyError:
1089 1096 pass
1090 1097 entries = util.set(self.delete)
1091 1098 files = util.frozenset(files)
1092 1099 entries.update(self.add_dirs(files.difference(entries)))
1093 1100 if self.copies:
1094 1101 for s, d in self.copies:
1095 1102 self._copyfile(s, d)
1096 1103 self.copies = []
1097 1104 if self.delete:
1098 1105 self.xargs(self.delete, 'delete')
1099 1106 self.delete = []
1100 1107 entries.update(self.add_files(files.difference(entries)))
1101 1108 entries.update(self.tidy_dirs(entries))
1102 1109 if self.delexec:
1103 1110 self.xargs(self.delexec, 'propdel', 'svn:executable')
1104 1111 self.delexec = []
1105 1112 if self.setexec:
1106 1113 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1107 1114 self.setexec = []
1108 1115
1109 1116 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1110 1117 fp = os.fdopen(fd, 'w')
1111 1118 fp.write(commit.desc)
1112 1119 fp.close()
1113 1120 try:
1114 1121 output = self.run0('commit',
1115 1122 username=util.shortuser(commit.author),
1116 1123 file=messagefile,
1117 1124 encoding='utf-8')
1118 1125 try:
1119 1126 rev = self.commit_re.search(output).group(1)
1120 1127 except AttributeError:
1121 1128 self.ui.warn(_('unexpected svn output:\n'))
1122 1129 self.ui.warn(output)
1123 1130 raise util.Abort(_('unable to cope with svn output'))
1124 1131 if commit.rev:
1125 1132 self.run('propset', 'hg:convert-rev', commit.rev,
1126 1133 revprop=True, revision=rev)
1127 1134 if commit.branch and commit.branch != 'default':
1128 1135 self.run('propset', 'hg:convert-branch', commit.branch,
1129 1136 revprop=True, revision=rev)
1130 1137 for parent in parents:
1131 1138 self.addchild(parent, rev)
1132 1139 return self.revid(rev)
1133 1140 finally:
1134 1141 os.unlink(messagefile)
1135 1142
1136 1143 def puttags(self, tags):
1137 1144 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now