##// END OF EJS Templates
convert: merge sources getmode() into getfile()
Patrick Mezard -
r11134:33010ff1 default
parent child Browse files
Show More
@@ -1,262 +1,260 b''
1 1 # bzr.py - bzr support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Marek Kubica <marek@xivilization.net> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # This module is for handling 'bzr', that was formerly known as Bazaar-NG;
9 9 # it cannot access 'bar' repositories, but they were never used very much
10 10
11 11 import os
12 12 from mercurial import demandimport
13 13 # these do not work with demandimport, blacklist
14 14 demandimport.ignore.extend([
15 15 'bzrlib.transactions',
16 16 'bzrlib.urlutils',
17 17 'ElementPath',
18 18 ])
19 19
20 20 from mercurial.i18n import _
21 21 from mercurial import util
22 22 from common import NoRepo, commit, converter_source
23 23
24 24 try:
25 25 # bazaar imports
26 26 from bzrlib import branch, revision, errors
27 27 from bzrlib.revisionspec import RevisionSpec
28 28 except ImportError:
29 29 pass
30 30
31 31 supportedkinds = ('file', 'symlink')
32 32
33 33 class bzr_source(converter_source):
34 34 """Reads Bazaar repositories by using the Bazaar Python libraries"""
35 35
36 36 def __init__(self, ui, path, rev=None):
37 37 super(bzr_source, self).__init__(ui, path, rev=rev)
38 38
39 39 if not os.path.exists(os.path.join(path, '.bzr')):
40 40 raise NoRepo(_('%s does not look like a Bazaar repository')
41 41 % path)
42 42
43 43 try:
44 44 # access bzrlib stuff
45 45 branch
46 46 except NameError:
47 47 raise NoRepo(_('Bazaar modules could not be loaded'))
48 48
49 49 path = os.path.abspath(path)
50 50 self._checkrepotype(path)
51 51 self.branch = branch.Branch.open(path)
52 52 self.sourcerepo = self.branch.repository
53 53 self._parentids = {}
54 54
55 55 def _checkrepotype(self, path):
56 56 # Lightweight checkouts detection is informational but probably
57 57 # fragile at API level. It should not terminate the conversion.
58 58 try:
59 59 from bzrlib import bzrdir
60 60 dir = bzrdir.BzrDir.open_containing(path)[0]
61 61 try:
62 62 tree = dir.open_workingtree(recommend_upgrade=False)
63 63 branch = tree.branch
64 64 except (errors.NoWorkingTree, errors.NotLocalUrl), e:
65 65 tree = None
66 66 branch = dir.open_branch()
67 67 if (tree is not None and tree.bzrdir.root_transport.base !=
68 68 branch.bzrdir.root_transport.base):
69 69 self.ui.warn(_('warning: lightweight checkouts may cause '
70 70 'conversion failures, try with a regular '
71 71 'branch instead.\n'))
72 72 except:
73 73 self.ui.note(_('bzr source type could not be determined\n'))
74 74
75 75 def before(self):
76 76 """Before the conversion begins, acquire a read lock
77 77 for all the operations that might need it. Fortunately
78 78 read locks don't block other reads or writes to the
79 79 repository, so this shouldn't have any impact on the usage of
80 80 the source repository.
81 81
82 82 The alternative would be locking on every operation that
83 83 needs locks (there are currently two: getting the file and
84 84 getting the parent map) and releasing immediately after,
85 85 but this approach can take even 40% longer."""
86 86 self.sourcerepo.lock_read()
87 87
88 88 def after(self):
89 89 self.sourcerepo.unlock()
90 90
91 91 def getheads(self):
92 92 if not self.rev:
93 93 return [self.branch.last_revision()]
94 94 try:
95 95 r = RevisionSpec.from_string(self.rev)
96 96 info = r.in_history(self.branch)
97 97 except errors.BzrError:
98 98 raise util.Abort(_('%s is not a valid revision in current branch')
99 99 % self.rev)
100 100 return [info.rev_id]
101 101
102 102 def getfile(self, name, rev):
103 103 revtree = self.sourcerepo.revision_tree(rev)
104 104 fileid = revtree.path2id(name.decode(self.encoding or 'utf-8'))
105 105 kind = None
106 106 if fileid is not None:
107 107 kind = revtree.kind(fileid)
108 108 if kind not in supportedkinds:
109 109 # the file is not available anymore - was deleted
110 110 raise IOError(_('%s is not available in %s anymore') %
111 111 (name, rev))
112 mode = self._modecache[(name, rev)]
112 113 if kind == 'symlink':
113 114 target = revtree.get_symlink_target(fileid)
114 115 if target is None:
115 116 raise util.Abort(_('%s.%s symlink has no target')
116 117 % (name, rev))
117 return target
118 return target, mode
118 119 else:
119 120 sio = revtree.get_file(fileid)
120 return sio.read()
121
122 def getmode(self, name, rev):
123 return self._modecache[(name, rev)]
121 return sio.read(), mode
124 122
125 123 def getchanges(self, version):
126 124 # set up caches: modecache and revtree
127 125 self._modecache = {}
128 126 self._revtree = self.sourcerepo.revision_tree(version)
129 127 # get the parentids from the cache
130 128 parentids = self._parentids.pop(version)
131 129 # only diff against first parent id
132 130 prevtree = self.sourcerepo.revision_tree(parentids[0])
133 131 return self._gettreechanges(self._revtree, prevtree)
134 132
135 133 def getcommit(self, version):
136 134 rev = self.sourcerepo.get_revision(version)
137 135 # populate parent id cache
138 136 if not rev.parent_ids:
139 137 parents = []
140 138 self._parentids[version] = (revision.NULL_REVISION,)
141 139 else:
142 140 parents = self._filterghosts(rev.parent_ids)
143 141 self._parentids[version] = parents
144 142
145 143 return commit(parents=parents,
146 144 date='%d %d' % (rev.timestamp, -rev.timezone),
147 145 author=self.recode(rev.committer),
148 146 # bzr returns bytestrings or unicode, depending on the content
149 147 desc=self.recode(rev.message),
150 148 rev=version)
151 149
152 150 def gettags(self):
153 151 if not self.branch.supports_tags():
154 152 return {}
155 153 tagdict = self.branch.tags.get_tag_dict()
156 154 bytetags = {}
157 155 for name, rev in tagdict.iteritems():
158 156 bytetags[self.recode(name)] = rev
159 157 return bytetags
160 158
161 159 def getchangedfiles(self, rev, i):
162 160 self._modecache = {}
163 161 curtree = self.sourcerepo.revision_tree(rev)
164 162 if i is not None:
165 163 parentid = self._parentids[rev][i]
166 164 else:
167 165 # no parent id, get the empty revision
168 166 parentid = revision.NULL_REVISION
169 167
170 168 prevtree = self.sourcerepo.revision_tree(parentid)
171 169 changes = [e[0] for e in self._gettreechanges(curtree, prevtree)[0]]
172 170 return changes
173 171
174 172 def _gettreechanges(self, current, origin):
175 173 revid = current._revision_id
176 174 changes = []
177 175 renames = {}
178 176 for (fileid, paths, changed_content, versioned, parent, name,
179 177 kind, executable) in current.iter_changes(origin):
180 178
181 179 if paths[0] == u'' or paths[1] == u'':
182 180 # ignore changes to tree root
183 181 continue
184 182
185 183 # bazaar tracks directories, mercurial does not, so
186 184 # we have to rename the directory contents
187 185 if kind[1] == 'directory':
188 186 if kind[0] not in (None, 'directory'):
189 187 # Replacing 'something' with a directory, record it
190 188 # so it can be removed.
191 189 changes.append((self.recode(paths[0]), revid))
192 190
193 191 if None not in paths and paths[0] != paths[1]:
194 192 # neither an add nor an delete - a move
195 193 # rename all directory contents manually
196 194 subdir = origin.inventory.path2id(paths[0])
197 195 # get all child-entries of the directory
198 196 for name, entry in origin.inventory.iter_entries(subdir):
199 197 # hg does not track directory renames
200 198 if entry.kind == 'directory':
201 199 continue
202 200 frompath = self.recode(paths[0] + '/' + name)
203 201 topath = self.recode(paths[1] + '/' + name)
204 202 # register the files as changed
205 203 changes.append((frompath, revid))
206 204 changes.append((topath, revid))
207 205 # add to mode cache
208 206 mode = ((entry.executable and 'x')
209 207 or (entry.kind == 'symlink' and 's')
210 208 or '')
211 209 self._modecache[(topath, revid)] = mode
212 210 # register the change as move
213 211 renames[topath] = frompath
214 212
215 213 # no futher changes, go to the next change
216 214 continue
217 215
218 216 # we got unicode paths, need to convert them
219 217 path, topath = [self.recode(part) for part in paths]
220 218
221 219 if topath is None:
222 220 # file deleted
223 221 changes.append((path, revid))
224 222 continue
225 223
226 224 # renamed
227 225 if path and path != topath:
228 226 renames[topath] = path
229 227 changes.append((path, revid))
230 228
231 229 # populate the mode cache
232 230 kind, executable = [e[1] for e in (kind, executable)]
233 231 mode = ((executable and 'x') or (kind == 'symlink' and 'l')
234 232 or '')
235 233 self._modecache[(topath, revid)] = mode
236 234 changes.append((topath, revid))
237 235
238 236 return changes, renames
239 237
240 238 def _filterghosts(self, ids):
241 239 """Filters out ghost revisions which hg does not support, see
242 240 <http://bazaar-vcs.org/GhostRevision>
243 241 """
244 242 parentmap = self.sourcerepo.get_parent_map(ids)
245 243 parents = tuple([parent for parent in ids if parent in parentmap])
246 244 return parents
247 245
248 246 def recode(self, s, encoding=None):
249 247 """This version of recode tries to encode unicode to bytecode,
250 248 and preferably using the UTF-8 codec.
251 249 Other types than Unicode are silently returned, this is by
252 250 intention, e.g. the None-type is not going to be encoded but instead
253 251 just passed through
254 252 """
255 253 if not encoding:
256 254 encoding = self.encoding or 'utf-8'
257 255
258 256 if isinstance(s, unicode):
259 257 return s.encode(encoding)
260 258 else:
261 259 # leave it alone
262 260 return s
@@ -1,394 +1,389 b''
1 1 # common.py - common code for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import base64, errno
9 9 import os
10 10 import cPickle as pickle
11 11 from mercurial import util
12 12 from mercurial.i18n import _
13 13
14 14 def encodeargs(args):
15 15 def encodearg(s):
16 16 lines = base64.encodestring(s)
17 17 lines = [l.splitlines()[0] for l in lines]
18 18 return ''.join(lines)
19 19
20 20 s = pickle.dumps(args)
21 21 return encodearg(s)
22 22
23 23 def decodeargs(s):
24 24 s = base64.decodestring(s)
25 25 return pickle.loads(s)
26 26
27 27 class MissingTool(Exception):
28 28 pass
29 29
30 30 def checktool(exe, name=None, abort=True):
31 31 name = name or exe
32 32 if not util.find_exe(exe):
33 33 exc = abort and util.Abort or MissingTool
34 34 raise exc(_('cannot find required "%s" tool') % name)
35 35
36 36 class NoRepo(Exception):
37 37 pass
38 38
39 39 SKIPREV = 'SKIP'
40 40
41 41 class commit(object):
42 42 def __init__(self, author, date, desc, parents, branch=None, rev=None,
43 43 extra={}, sortkey=None):
44 44 self.author = author or 'unknown'
45 45 self.date = date or '0 0'
46 46 self.desc = desc
47 47 self.parents = parents
48 48 self.branch = branch
49 49 self.rev = rev
50 50 self.extra = extra
51 51 self.sortkey = sortkey
52 52
53 53 class converter_source(object):
54 54 """Conversion source interface"""
55 55
56 56 def __init__(self, ui, path=None, rev=None):
57 57 """Initialize conversion source (or raise NoRepo("message")
58 58 exception if path is not a valid repository)"""
59 59 self.ui = ui
60 60 self.path = path
61 61 self.rev = rev
62 62
63 63 self.encoding = 'utf-8'
64 64
65 65 def before(self):
66 66 pass
67 67
68 68 def after(self):
69 69 pass
70 70
71 71 def setrevmap(self, revmap):
72 72 """set the map of already-converted revisions"""
73 73 pass
74 74
75 75 def getheads(self):
76 76 """Return a list of this repository's heads"""
77 77 raise NotImplementedError()
78 78
79 79 def getfile(self, name, rev):
80 """Return file contents as a string. rev is the identifier returned
81 by a previous call to getchanges(). Raise IOError to indicate that
82 name was deleted in rev.
83 """
84 raise NotImplementedError()
85
86 def getmode(self, name, rev):
87 """Return file mode, eg. '', 'x', or 'l'. rev is the identifier
88 returned by a previous call to getchanges().
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.
89 84 """
90 85 raise NotImplementedError()
91 86
92 87 def getchanges(self, version):
93 88 """Returns a tuple of (files, copies).
94 89
95 90 files is a sorted list of (filename, id) tuples for all files
96 91 changed between version and its first parent returned by
97 92 getcommit(). id is the source revision id of the file.
98 93
99 94 copies is a dictionary of dest: source
100 95 """
101 96 raise NotImplementedError()
102 97
103 98 def getcommit(self, version):
104 99 """Return the commit object for version"""
105 100 raise NotImplementedError()
106 101
107 102 def gettags(self):
108 103 """Return the tags as a dictionary of name: revision
109 104
110 105 Tag names must be UTF-8 strings.
111 106 """
112 107 raise NotImplementedError()
113 108
114 109 def recode(self, s, encoding=None):
115 110 if not encoding:
116 111 encoding = self.encoding or 'utf-8'
117 112
118 113 if isinstance(s, unicode):
119 114 return s.encode("utf-8")
120 115 try:
121 116 return s.decode(encoding).encode("utf-8")
122 117 except:
123 118 try:
124 119 return s.decode("latin-1").encode("utf-8")
125 120 except:
126 121 return s.decode(encoding, "replace").encode("utf-8")
127 122
128 123 def getchangedfiles(self, rev, i):
129 124 """Return the files changed by rev compared to parent[i].
130 125
131 126 i is an index selecting one of the parents of rev. The return
132 127 value should be the list of files that are different in rev and
133 128 this parent.
134 129
135 130 If rev has no parents, i is None.
136 131
137 132 This function is only needed to support --filemap
138 133 """
139 134 raise NotImplementedError()
140 135
141 136 def converted(self, rev, sinkrev):
142 137 '''Notify the source that a revision has been converted.'''
143 138 pass
144 139
145 140 def hasnativeorder(self):
146 141 """Return true if this source has a meaningful, native revision
147 142 order. For instance, Mercurial revisions are store sequentially
148 143 while there is no such global ordering with Darcs.
149 144 """
150 145 return False
151 146
152 147 def lookuprev(self, rev):
153 148 """If rev is a meaningful revision reference in source, return
154 149 the referenced identifier in the same format used by getcommit().
155 150 return None otherwise.
156 151 """
157 152 return None
158 153
159 154 class converter_sink(object):
160 155 """Conversion sink (target) interface"""
161 156
162 157 def __init__(self, ui, path):
163 158 """Initialize conversion sink (or raise NoRepo("message")
164 159 exception if path is not a valid repository)
165 160
166 161 created is a list of paths to remove if a fatal error occurs
167 162 later"""
168 163 self.ui = ui
169 164 self.path = path
170 165 self.created = []
171 166
172 167 def getheads(self):
173 168 """Return a list of this repository's heads"""
174 169 raise NotImplementedError()
175 170
176 171 def revmapfile(self):
177 172 """Path to a file that will contain lines
178 173 source_rev_id sink_rev_id
179 174 mapping equivalent revision identifiers for each system."""
180 175 raise NotImplementedError()
181 176
182 177 def authorfile(self):
183 178 """Path to a file that will contain lines
184 179 srcauthor=dstauthor
185 180 mapping equivalent authors identifiers for each system."""
186 181 return None
187 182
188 183 def putcommit(self, files, copies, parents, commit, source, revmap):
189 184 """Create a revision with all changed files listed in 'files'
190 185 and having listed parents. 'commit' is a commit object
191 186 containing at a minimum the author, date, and message for this
192 187 changeset. 'files' is a list of (path, version) tuples,
193 188 'copies' is a dictionary mapping destinations to sources,
194 189 'source' is the source repository, and 'revmap' is a mapfile
195 of source revisions to converted revisions. Only getfile(),
196 getmode(), and lookuprev() should be called on 'source'.
190 of source revisions to converted revisions. Only getfile() and
191 lookuprev() should be called on 'source'.
197 192
198 193 Note that the sink repository is not told to update itself to
199 194 a particular revision (or even what that revision would be)
200 195 before it receives the file data.
201 196 """
202 197 raise NotImplementedError()
203 198
204 199 def puttags(self, tags):
205 200 """Put tags into sink.
206 201
207 202 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
208 203 Return a pair (tag_revision, tag_parent_revision), or (None, None)
209 204 if nothing was changed.
210 205 """
211 206 raise NotImplementedError()
212 207
213 208 def setbranch(self, branch, pbranches):
214 209 """Set the current branch name. Called before the first putcommit
215 210 on the branch.
216 211 branch: branch name for subsequent commits
217 212 pbranches: (converted parent revision, parent branch) tuples"""
218 213 pass
219 214
220 215 def setfilemapmode(self, active):
221 216 """Tell the destination that we're using a filemap
222 217
223 218 Some converter_sources (svn in particular) can claim that a file
224 219 was changed in a revision, even if there was no change. This method
225 220 tells the destination that we're using a filemap and that it should
226 221 filter empty revisions.
227 222 """
228 223 pass
229 224
230 225 def before(self):
231 226 pass
232 227
233 228 def after(self):
234 229 pass
235 230
236 231
237 232 class commandline(object):
238 233 def __init__(self, ui, command):
239 234 self.ui = ui
240 235 self.command = command
241 236
242 237 def prerun(self):
243 238 pass
244 239
245 240 def postrun(self):
246 241 pass
247 242
248 243 def _cmdline(self, cmd, *args, **kwargs):
249 244 cmdline = [self.command, cmd] + list(args)
250 245 for k, v in kwargs.iteritems():
251 246 if len(k) == 1:
252 247 cmdline.append('-' + k)
253 248 else:
254 249 cmdline.append('--' + k.replace('_', '-'))
255 250 try:
256 251 if len(k) == 1:
257 252 cmdline.append('' + v)
258 253 else:
259 254 cmdline[-1] += '=' + v
260 255 except TypeError:
261 256 pass
262 257 cmdline = [util.shellquote(arg) for arg in cmdline]
263 258 if not self.ui.debugflag:
264 259 cmdline += ['2>', util.nulldev]
265 260 cmdline += ['<', util.nulldev]
266 261 cmdline = ' '.join(cmdline)
267 262 return cmdline
268 263
269 264 def _run(self, cmd, *args, **kwargs):
270 265 cmdline = self._cmdline(cmd, *args, **kwargs)
271 266 self.ui.debug('running: %s\n' % (cmdline,))
272 267 self.prerun()
273 268 try:
274 269 return util.popen(cmdline)
275 270 finally:
276 271 self.postrun()
277 272
278 273 def run(self, cmd, *args, **kwargs):
279 274 fp = self._run(cmd, *args, **kwargs)
280 275 output = fp.read()
281 276 self.ui.debug(output)
282 277 return output, fp.close()
283 278
284 279 def runlines(self, cmd, *args, **kwargs):
285 280 fp = self._run(cmd, *args, **kwargs)
286 281 output = fp.readlines()
287 282 self.ui.debug(''.join(output))
288 283 return output, fp.close()
289 284
290 285 def checkexit(self, status, output=''):
291 286 if status:
292 287 if output:
293 288 self.ui.warn(_('%s error:\n') % self.command)
294 289 self.ui.warn(output)
295 290 msg = util.explain_exit(status)[0]
296 291 raise util.Abort('%s %s' % (self.command, msg))
297 292
298 293 def run0(self, cmd, *args, **kwargs):
299 294 output, status = self.run(cmd, *args, **kwargs)
300 295 self.checkexit(status, output)
301 296 return output
302 297
303 298 def runlines0(self, cmd, *args, **kwargs):
304 299 output, status = self.runlines(cmd, *args, **kwargs)
305 300 self.checkexit(status, ''.join(output))
306 301 return output
307 302
308 303 def getargmax(self):
309 304 if '_argmax' in self.__dict__:
310 305 return self._argmax
311 306
312 307 # POSIX requires at least 4096 bytes for ARG_MAX
313 308 self._argmax = 4096
314 309 try:
315 310 self._argmax = os.sysconf("SC_ARG_MAX")
316 311 except:
317 312 pass
318 313
319 314 # Windows shells impose their own limits on command line length,
320 315 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
321 316 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
322 317 # details about cmd.exe limitations.
323 318
324 319 # Since ARG_MAX is for command line _and_ environment, lower our limit
325 320 # (and make happy Windows shells while doing this).
326 321
327 322 self._argmax = self._argmax / 2 - 1
328 323 return self._argmax
329 324
330 325 def limit_arglist(self, arglist, cmd, *args, **kwargs):
331 326 limit = self.getargmax() - len(self._cmdline(cmd, *args, **kwargs))
332 327 bytes = 0
333 328 fl = []
334 329 for fn in arglist:
335 330 b = len(fn) + 3
336 331 if bytes + b < limit or len(fl) == 0:
337 332 fl.append(fn)
338 333 bytes += b
339 334 else:
340 335 yield fl
341 336 fl = [fn]
342 337 bytes = b
343 338 if fl:
344 339 yield fl
345 340
346 341 def xargs(self, arglist, cmd, *args, **kwargs):
347 342 for l in self.limit_arglist(arglist, cmd, *args, **kwargs):
348 343 self.run0(cmd, *(list(args) + l), **kwargs)
349 344
350 345 class mapfile(dict):
351 346 def __init__(self, ui, path):
352 347 super(mapfile, self).__init__()
353 348 self.ui = ui
354 349 self.path = path
355 350 self.fp = None
356 351 self.order = []
357 352 self._read()
358 353
359 354 def _read(self):
360 355 if not self.path:
361 356 return
362 357 try:
363 358 fp = open(self.path, 'r')
364 359 except IOError, err:
365 360 if err.errno != errno.ENOENT:
366 361 raise
367 362 return
368 363 for i, line in enumerate(fp):
369 364 try:
370 365 key, value = line.splitlines()[0].rsplit(' ', 1)
371 366 except ValueError:
372 367 raise util.Abort(
373 368 _('syntax error in %s(%d): key/value pair expected')
374 369 % (self.path, i + 1))
375 370 if key not in self:
376 371 self.order.append(key)
377 372 super(mapfile, self).__setitem__(key, value)
378 373 fp.close()
379 374
380 375 def __setitem__(self, key, value):
381 376 if self.fp is None:
382 377 try:
383 378 self.fp = open(self.path, 'a')
384 379 except IOError, err:
385 380 raise util.Abort(_('could not open map file %r: %s') %
386 381 (self.path, err.strerror))
387 382 self.fp.write('%s %s\n' % (key, value))
388 383 self.fp.flush()
389 384 super(mapfile, self).__setitem__(key, value)
390 385
391 386 def close(self):
392 387 if self.fp:
393 388 self.fp.close()
394 389 self.fp = None
@@ -1,282 +1,273 b''
1 1 # cvs.py: CVS conversion code inspired by hg-cvs-import and git-cvsimport
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os, locale, re, socket, errno
9 9 from cStringIO import StringIO
10 10 from mercurial import util
11 11 from mercurial.i18n import _
12 12
13 13 from common import NoRepo, commit, converter_source, checktool
14 14 import cvsps
15 15
16 16 class convert_cvs(converter_source):
17 17 def __init__(self, ui, path, rev=None):
18 18 super(convert_cvs, self).__init__(ui, path, rev=rev)
19 19
20 20 cvs = os.path.join(path, "CVS")
21 21 if not os.path.exists(cvs):
22 22 raise NoRepo(_("%s does not look like a CVS checkout") % path)
23 23
24 24 checktool('cvs')
25 25
26 26 self.changeset = None
27 27 self.files = {}
28 28 self.tags = {}
29 29 self.lastbranch = {}
30 30 self.socket = None
31 31 self.cvsroot = open(os.path.join(cvs, "Root")).read()[:-1]
32 32 self.cvsrepo = open(os.path.join(cvs, "Repository")).read()[:-1]
33 33 self.encoding = locale.getpreferredencoding()
34 34
35 35 self._connect()
36 36
37 37 def _parse(self):
38 38 if self.changeset is not None:
39 39 return
40 40 self.changeset = {}
41 41
42 42 maxrev = 0
43 43 if self.rev:
44 44 # TODO: handle tags
45 45 try:
46 46 # patchset number?
47 47 maxrev = int(self.rev)
48 48 except ValueError:
49 49 raise util.Abort(_('revision %s is not a patchset number')
50 50 % self.rev)
51 51
52 52 d = os.getcwd()
53 53 try:
54 54 os.chdir(self.path)
55 55 id = None
56 56 state = 0
57 57 filerevids = {}
58 58
59 59 cache = 'update'
60 60 if not self.ui.configbool('convert', 'cvsps.cache', True):
61 61 cache = None
62 62 db = cvsps.createlog(self.ui, cache=cache)
63 63 db = cvsps.createchangeset(self.ui, db,
64 64 fuzz=int(self.ui.config('convert', 'cvsps.fuzz', 60)),
65 65 mergeto=self.ui.config('convert', 'cvsps.mergeto', None),
66 66 mergefrom=self.ui.config('convert', 'cvsps.mergefrom', None))
67 67
68 68 for cs in db:
69 69 if maxrev and cs.id > maxrev:
70 70 break
71 71 id = str(cs.id)
72 72 cs.author = self.recode(cs.author)
73 73 self.lastbranch[cs.branch] = id
74 74 cs.comment = self.recode(cs.comment)
75 75 date = util.datestr(cs.date)
76 76 self.tags.update(dict.fromkeys(cs.tags, id))
77 77
78 78 files = {}
79 79 for f in cs.entries:
80 80 files[f.file] = "%s%s" % ('.'.join([str(x)
81 81 for x in f.revision]),
82 82 ['', '(DEAD)'][f.dead])
83 83
84 84 # add current commit to set
85 85 c = commit(author=cs.author, date=date,
86 86 parents=[str(p.id) for p in cs.parents],
87 87 desc=cs.comment, branch=cs.branch or '')
88 88 self.changeset[id] = c
89 89 self.files[id] = files
90 90
91 91 self.heads = self.lastbranch.values()
92 92 finally:
93 93 os.chdir(d)
94 94
95 95 def _connect(self):
96 96 root = self.cvsroot
97 97 conntype = None
98 98 user, host = None, None
99 99 cmd = ['cvs', 'server']
100 100
101 101 self.ui.status(_("connecting to %s\n") % root)
102 102
103 103 if root.startswith(":pserver:"):
104 104 root = root[9:]
105 105 m = re.match(r'(?:(.*?)(?::(.*?))?@)?([^:\/]*)(?::(\d*))?(.*)',
106 106 root)
107 107 if m:
108 108 conntype = "pserver"
109 109 user, passw, serv, port, root = m.groups()
110 110 if not user:
111 111 user = "anonymous"
112 112 if not port:
113 113 port = 2401
114 114 else:
115 115 port = int(port)
116 116 format0 = ":pserver:%s@%s:%s" % (user, serv, root)
117 117 format1 = ":pserver:%s@%s:%d%s" % (user, serv, port, root)
118 118
119 119 if not passw:
120 120 passw = "A"
121 121 cvspass = os.path.expanduser("~/.cvspass")
122 122 try:
123 123 pf = open(cvspass)
124 124 for line in pf.read().splitlines():
125 125 part1, part2 = line.split(' ', 1)
126 126 if part1 == '/1':
127 127 # /1 :pserver:user@example.com:2401/cvsroot/foo Ah<Z
128 128 part1, part2 = part2.split(' ', 1)
129 129 format = format1
130 130 else:
131 131 # :pserver:user@example.com:/cvsroot/foo Ah<Z
132 132 format = format0
133 133 if part1 == format:
134 134 passw = part2
135 135 break
136 136 pf.close()
137 137 except IOError, inst:
138 138 if inst.errno != errno.ENOENT:
139 139 if not getattr(inst, 'filename', None):
140 140 inst.filename = cvspass
141 141 raise
142 142
143 143 sck = socket.socket()
144 144 sck.connect((serv, port))
145 145 sck.send("\n".join(["BEGIN AUTH REQUEST", root, user, passw,
146 146 "END AUTH REQUEST", ""]))
147 147 if sck.recv(128) != "I LOVE YOU\n":
148 148 raise util.Abort(_("CVS pserver authentication failed"))
149 149
150 150 self.writep = self.readp = sck.makefile('r+')
151 151
152 152 if not conntype and root.startswith(":local:"):
153 153 conntype = "local"
154 154 root = root[7:]
155 155
156 156 if not conntype:
157 157 # :ext:user@host/home/user/path/to/cvsroot
158 158 if root.startswith(":ext:"):
159 159 root = root[5:]
160 160 m = re.match(r'(?:([^@:/]+)@)?([^:/]+):?(.*)', root)
161 161 # Do not take Windows path "c:\foo\bar" for a connection strings
162 162 if os.path.isdir(root) or not m:
163 163 conntype = "local"
164 164 else:
165 165 conntype = "rsh"
166 166 user, host, root = m.group(1), m.group(2), m.group(3)
167 167
168 168 if conntype != "pserver":
169 169 if conntype == "rsh":
170 170 rsh = os.environ.get("CVS_RSH") or "ssh"
171 171 if user:
172 172 cmd = [rsh, '-l', user, host] + cmd
173 173 else:
174 174 cmd = [rsh, host] + cmd
175 175
176 176 # popen2 does not support argument lists under Windows
177 177 cmd = [util.shellquote(arg) for arg in cmd]
178 178 cmd = util.quotecommand(' '.join(cmd))
179 179 self.writep, self.readp = util.popen2(cmd)
180 180
181 181 self.realroot = root
182 182
183 183 self.writep.write("Root %s\n" % root)
184 184 self.writep.write("Valid-responses ok error Valid-requests Mode"
185 185 " M Mbinary E Checked-in Created Updated"
186 186 " Merged Removed\n")
187 187 self.writep.write("valid-requests\n")
188 188 self.writep.flush()
189 189 r = self.readp.readline()
190 190 if not r.startswith("Valid-requests"):
191 191 raise util.Abort(_('unexpected response from CVS server '
192 192 '(expected "Valid-requests", but got %r)')
193 193 % r)
194 194 if "UseUnchanged" in r:
195 195 self.writep.write("UseUnchanged\n")
196 196 self.writep.flush()
197 197 r = self.readp.readline()
198 198
199 199 def getheads(self):
200 200 self._parse()
201 201 return self.heads
202 202
203 def _getfile(self, name, rev):
203 def getfile(self, name, rev):
204 204
205 205 def chunkedread(fp, count):
206 206 # file-objects returned by socked.makefile() do not handle
207 207 # large read() requests very well.
208 208 chunksize = 65536
209 209 output = StringIO()
210 210 while count > 0:
211 211 data = fp.read(min(count, chunksize))
212 212 if not data:
213 213 raise util.Abort(_("%d bytes missing from remote file")
214 214 % count)
215 215 count -= len(data)
216 216 output.write(data)
217 217 return output.getvalue()
218 218
219 self._parse()
219 220 if rev.endswith("(DEAD)"):
220 221 raise IOError
221 222
222 223 args = ("-N -P -kk -r %s --" % rev).split()
223 224 args.append(self.cvsrepo + '/' + name)
224 225 for x in args:
225 226 self.writep.write("Argument %s\n" % x)
226 227 self.writep.write("Directory .\n%s\nco\n" % self.realroot)
227 228 self.writep.flush()
228 229
229 230 data = ""
230 231 mode = None
231 232 while 1:
232 233 line = self.readp.readline()
233 234 if line.startswith("Created ") or line.startswith("Updated "):
234 235 self.readp.readline() # path
235 236 self.readp.readline() # entries
236 237 mode = self.readp.readline()[:-1]
237 238 count = int(self.readp.readline()[:-1])
238 239 data = chunkedread(self.readp, count)
239 240 elif line.startswith(" "):
240 241 data += line[1:]
241 242 elif line.startswith("M "):
242 243 pass
243 244 elif line.startswith("Mbinary "):
244 245 count = int(self.readp.readline()[:-1])
245 246 data = chunkedread(self.readp, count)
246 247 else:
247 248 if line == "ok\n":
248 249 if mode is None:
249 250 raise util.Abort(_('malformed response from CVS'))
250 251 return (data, "x" in mode and "x" or "")
251 252 elif line.startswith("E "):
252 253 self.ui.warn(_("cvs server: %s\n") % line[2:])
253 254 elif line.startswith("Remove"):
254 255 self.readp.readline()
255 256 else:
256 257 raise util.Abort(_("unknown CVS response: %s") % line)
257 258
258 def getfile(self, file, rev):
259 self._parse()
260 data, mode = self._getfile(file, rev)
261 self.modecache[(file, rev)] = mode
262 return data
263
264 def getmode(self, file, rev):
265 return self.modecache[(file, rev)]
266
267 259 def getchanges(self, rev):
268 260 self._parse()
269 self.modecache = {}
270 261 return sorted(self.files[rev].iteritems()), {}
271 262
272 263 def getcommit(self, rev):
273 264 self._parse()
274 265 return self.changeset[rev]
275 266
276 267 def gettags(self):
277 268 self._parse()
278 269 return self.tags
279 270
280 271 def getchangedfiles(self, rev, i):
281 272 self._parse()
282 273 return sorted(self.files[rev])
@@ -1,167 +1,167 b''
1 1 # darcs.py - darcs support for the convert extension
2 2 #
3 3 # Copyright 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from common import NoRepo, checktool, commandline, commit, converter_source
9 9 from mercurial.i18n import _
10 10 from mercurial import util
11 11 import os, shutil, tempfile
12 12
13 13 # The naming drift of ElementTree is fun!
14 14
15 15 try:
16 16 from xml.etree.cElementTree import ElementTree
17 17 except ImportError:
18 18 try:
19 19 from xml.etree.ElementTree import ElementTree
20 20 except ImportError:
21 21 try:
22 22 from elementtree.cElementTree import ElementTree
23 23 except ImportError:
24 24 try:
25 25 from elementtree.ElementTree import ElementTree
26 26 except ImportError:
27 27 ElementTree = None
28 28
29 29 class darcs_source(converter_source, commandline):
30 30 def __init__(self, ui, path, rev=None):
31 31 converter_source.__init__(self, ui, path, rev=rev)
32 32 commandline.__init__(self, ui, 'darcs')
33 33
34 34 # check for _darcs, ElementTree, _darcs/inventory so that we can
35 35 # easily skip test-convert-darcs if ElementTree is not around
36 36 if not os.path.exists(os.path.join(path, '_darcs', 'inventories')):
37 37 raise NoRepo(_("%s does not look like a darcs repository") % path)
38 38
39 39 if not os.path.exists(os.path.join(path, '_darcs')):
40 40 raise NoRepo(_("%s does not look like a darcs repository") % path)
41 41
42 42 checktool('darcs')
43 43 version = self.run0('--version').splitlines()[0].strip()
44 44 if version < '2.1':
45 45 raise util.Abort(_('darcs version 2.1 or newer needed (found %r)') %
46 46 version)
47 47
48 48 if ElementTree is None:
49 49 raise util.Abort(_("Python ElementTree module is not available"))
50 50
51 51 self.path = os.path.realpath(path)
52 52
53 53 self.lastrev = None
54 54 self.changes = {}
55 55 self.parents = {}
56 56 self.tags = {}
57 57
58 58 def before(self):
59 59 self.tmppath = tempfile.mkdtemp(
60 60 prefix='convert-' + os.path.basename(self.path) + '-')
61 61 output, status = self.run('init', repodir=self.tmppath)
62 62 self.checkexit(status)
63 63
64 64 tree = self.xml('changes', xml_output=True, summary=True,
65 65 repodir=self.path)
66 66 tagname = None
67 67 child = None
68 68 for elt in tree.findall('patch'):
69 69 node = elt.get('hash')
70 70 name = elt.findtext('name', '')
71 71 if name.startswith('TAG '):
72 72 tagname = name[4:].strip()
73 73 elif tagname is not None:
74 74 self.tags[tagname] = node
75 75 tagname = None
76 76 self.changes[node] = elt
77 77 self.parents[child] = [node]
78 78 child = node
79 79 self.parents[child] = []
80 80
81 81 def after(self):
82 82 self.ui.debug('cleaning up %s\n' % self.tmppath)
83 83 shutil.rmtree(self.tmppath, ignore_errors=True)
84 84
85 85 def xml(self, cmd, **kwargs):
86 86 etree = ElementTree()
87 87 fp = self._run(cmd, **kwargs)
88 88 etree.parse(fp)
89 89 self.checkexit(fp.close())
90 90 return etree.getroot()
91 91
92 92 def manifest(self):
93 93 man = []
94 94 output, status = self.run('show', 'files', no_directories=True,
95 95 repodir=self.tmppath)
96 96 self.checkexit(status)
97 97 for line in output.split('\n'):
98 98 path = line[2:]
99 99 if path:
100 100 man.append(path)
101 101 return man
102 102
103 103 def getheads(self):
104 104 return self.parents[None]
105 105
106 106 def getcommit(self, rev):
107 107 elt = self.changes[rev]
108 108 date = util.strdate(elt.get('local_date'), '%a %b %d %H:%M:%S %Z %Y')
109 109 desc = elt.findtext('name') + '\n' + elt.findtext('comment', '')
110 110 return commit(author=elt.get('author'), date=util.datestr(date),
111 111 desc=desc.strip(), parents=self.parents[rev])
112 112
113 113 def pull(self, rev):
114 114 output, status = self.run('pull', self.path, all=True,
115 115 match='hash %s' % rev,
116 116 no_test=True, no_posthook=True,
117 117 external_merge='/bin/false',
118 118 repodir=self.tmppath)
119 119 if status:
120 120 if output.find('We have conflicts in') == -1:
121 121 self.checkexit(status, output)
122 122 output, status = self.run('revert', all=True, repodir=self.tmppath)
123 123 self.checkexit(status, output)
124 124
125 125 def getchanges(self, rev):
126 126 copies = {}
127 127 changes = []
128 128 man = None
129 129 for elt in self.changes[rev].find('summary').getchildren():
130 130 if elt.tag in ('add_directory', 'remove_directory'):
131 131 continue
132 132 if elt.tag == 'move':
133 133 if man is None:
134 134 man = self.manifest()
135 135 source, dest = elt.get('from'), elt.get('to')
136 136 if source in man:
137 137 # File move
138 138 changes.append((source, rev))
139 139 changes.append((dest, rev))
140 140 copies[dest] = source
141 141 else:
142 142 # Directory move, deduce file moves from manifest
143 143 source = source + '/'
144 144 for f in man:
145 145 if not f.startswith(source):
146 146 continue
147 147 fdest = dest + '/' + f[len(source):]
148 148 changes.append((f, rev))
149 149 changes.append((fdest, rev))
150 150 copies[fdest] = f
151 151 else:
152 152 changes.append((elt.text.strip(), rev))
153 153 self.pull(rev)
154 154 self.lastrev = rev
155 155 return sorted(changes), copies
156 156
157 157 def getfile(self, name, rev):
158 158 if rev != self.lastrev:
159 159 raise util.Abort(_('internal calling inconsistency'))
160 return open(os.path.join(self.tmppath, name), 'rb').read()
161
162 def getmode(self, name, rev):
163 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
164 return (mode & 0111) and 'x' or ''
160 path = os.path.join(self.tmppath, name)
161 data = open(path, 'rb').read()
162 mode = os.lstat(path).st_mode
163 mode = (mode & 0111) and 'x' or ''
164 return data, mode
165 165
166 166 def gettags(self):
167 167 return self.tags
@@ -1,359 +1,353 b''
1 1 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
2 2 # Copyright 2007 Alexis S. L. Carvalho <alexis@cecm.usp.br>
3 3 #
4 4 # This software may be used and distributed according to the terms of the
5 5 # GNU General Public License version 2 or any later version.
6 6
7 7 import shlex
8 8 from mercurial.i18n import _
9 9 from mercurial import util
10 10 from common import SKIPREV, converter_source
11 11
12 12 def rpairs(name):
13 13 e = len(name)
14 14 while e != -1:
15 15 yield name[:e], name[e + 1:]
16 16 e = name.rfind('/', 0, e)
17 17 yield '.', name
18 18
19 19 class filemapper(object):
20 20 '''Map and filter filenames when importing.
21 21 A name can be mapped to itself, a new name, or None (omit from new
22 22 repository).'''
23 23
24 24 def __init__(self, ui, path=None):
25 25 self.ui = ui
26 26 self.include = {}
27 27 self.exclude = {}
28 28 self.rename = {}
29 29 if path:
30 30 if self.parse(path):
31 31 raise util.Abort(_('errors in filemap'))
32 32
33 33 def parse(self, path):
34 34 errs = 0
35 35 def check(name, mapping, listname):
36 36 if name in mapping:
37 37 self.ui.warn(_('%s:%d: %r already in %s list\n') %
38 38 (lex.infile, lex.lineno, name, listname))
39 39 return 1
40 40 return 0
41 41 lex = shlex.shlex(open(path), path, True)
42 42 lex.wordchars += '!@#$%^&*()-=+[]{}|;:,./<>?'
43 43 cmd = lex.get_token()
44 44 while cmd:
45 45 if cmd == 'include':
46 46 name = lex.get_token()
47 47 errs += check(name, self.exclude, 'exclude')
48 48 self.include[name] = name
49 49 elif cmd == 'exclude':
50 50 name = lex.get_token()
51 51 errs += check(name, self.include, 'include')
52 52 errs += check(name, self.rename, 'rename')
53 53 self.exclude[name] = name
54 54 elif cmd == 'rename':
55 55 src = lex.get_token()
56 56 dest = lex.get_token()
57 57 errs += check(src, self.exclude, 'exclude')
58 58 self.rename[src] = dest
59 59 elif cmd == 'source':
60 60 errs += self.parse(lex.get_token())
61 61 else:
62 62 self.ui.warn(_('%s:%d: unknown directive %r\n') %
63 63 (lex.infile, lex.lineno, cmd))
64 64 errs += 1
65 65 cmd = lex.get_token()
66 66 return errs
67 67
68 68 def lookup(self, name, mapping):
69 69 for pre, suf in rpairs(name):
70 70 try:
71 71 return mapping[pre], pre, suf
72 72 except KeyError:
73 73 pass
74 74 return '', name, ''
75 75
76 76 def __call__(self, name):
77 77 if self.include:
78 78 inc = self.lookup(name, self.include)[0]
79 79 else:
80 80 inc = name
81 81 if self.exclude:
82 82 exc = self.lookup(name, self.exclude)[0]
83 83 else:
84 84 exc = ''
85 85 if (not self.include and exc) or (len(inc) <= len(exc)):
86 86 return None
87 87 newpre, pre, suf = self.lookup(name, self.rename)
88 88 if newpre:
89 89 if newpre == '.':
90 90 return suf
91 91 if suf:
92 92 return newpre + '/' + suf
93 93 return newpre
94 94 return name
95 95
96 96 def active(self):
97 97 return bool(self.include or self.exclude or self.rename)
98 98
99 99 # This class does two additional things compared to a regular source:
100 100 #
101 101 # - Filter and rename files. This is mostly wrapped by the filemapper
102 102 # class above. We hide the original filename in the revision that is
103 # returned by getchanges to be able to find things later in getfile
104 # and getmode.
103 # returned by getchanges to be able to find things later in getfile.
105 104 #
106 105 # - Return only revisions that matter for the files we're interested in.
107 106 # This involves rewriting the parents of the original revision to
108 107 # create a graph that is restricted to those revisions.
109 108 #
110 109 # This set of revisions includes not only revisions that directly
111 110 # touch files we're interested in, but also merges that merge two
112 111 # or more interesting revisions.
113 112
114 113 class filemap_source(converter_source):
115 114 def __init__(self, ui, baseconverter, filemap):
116 115 super(filemap_source, self).__init__(ui)
117 116 self.base = baseconverter
118 117 self.filemapper = filemapper(ui, filemap)
119 118 self.commits = {}
120 119 # if a revision rev has parent p in the original revision graph, then
121 120 # rev will have parent self.parentmap[p] in the restricted graph.
122 121 self.parentmap = {}
123 122 # self.wantedancestors[rev] is the set of all ancestors of rev that
124 123 # are in the restricted graph.
125 124 self.wantedancestors = {}
126 125 self.convertedorder = None
127 126 self._rebuilt = False
128 127 self.origparents = {}
129 128 self.children = {}
130 129 self.seenchildren = {}
131 130
132 131 def before(self):
133 132 self.base.before()
134 133
135 134 def after(self):
136 135 self.base.after()
137 136
138 137 def setrevmap(self, revmap):
139 138 # rebuild our state to make things restartable
140 139 #
141 140 # To avoid calling getcommit for every revision that has already
142 141 # been converted, we rebuild only the parentmap, delaying the
143 142 # rebuild of wantedancestors until we need it (i.e. until a
144 143 # merge).
145 144 #
146 145 # We assume the order argument lists the revisions in
147 146 # topological order, so that we can infer which revisions were
148 147 # wanted by previous runs.
149 148 self._rebuilt = not revmap
150 149 seen = {SKIPREV: SKIPREV}
151 150 dummyset = set()
152 151 converted = []
153 152 for rev in revmap.order:
154 153 mapped = revmap[rev]
155 154 wanted = mapped not in seen
156 155 if wanted:
157 156 seen[mapped] = rev
158 157 self.parentmap[rev] = rev
159 158 else:
160 159 self.parentmap[rev] = seen[mapped]
161 160 self.wantedancestors[rev] = dummyset
162 161 arg = seen[mapped]
163 162 if arg == SKIPREV:
164 163 arg = None
165 164 converted.append((rev, wanted, arg))
166 165 self.convertedorder = converted
167 166 return self.base.setrevmap(revmap)
168 167
169 168 def rebuild(self):
170 169 if self._rebuilt:
171 170 return True
172 171 self._rebuilt = True
173 172 self.parentmap.clear()
174 173 self.wantedancestors.clear()
175 174 self.seenchildren.clear()
176 175 for rev, wanted, arg in self.convertedorder:
177 176 if rev not in self.origparents:
178 177 self.origparents[rev] = self.getcommit(rev).parents
179 178 if arg is not None:
180 179 self.children[arg] = self.children.get(arg, 0) + 1
181 180
182 181 for rev, wanted, arg in self.convertedorder:
183 182 parents = self.origparents[rev]
184 183 if wanted:
185 184 self.mark_wanted(rev, parents)
186 185 else:
187 186 self.mark_not_wanted(rev, arg)
188 187 self._discard(arg, *parents)
189 188
190 189 return True
191 190
192 191 def getheads(self):
193 192 return self.base.getheads()
194 193
195 194 def getcommit(self, rev):
196 195 # We want to save a reference to the commit objects to be able
197 196 # to rewrite their parents later on.
198 197 c = self.commits[rev] = self.base.getcommit(rev)
199 198 for p in c.parents:
200 199 self.children[p] = self.children.get(p, 0) + 1
201 200 return c
202 201
203 202 def _discard(self, *revs):
204 203 for r in revs:
205 204 if r is None:
206 205 continue
207 206 self.seenchildren[r] = self.seenchildren.get(r, 0) + 1
208 207 if self.seenchildren[r] == self.children[r]:
209 208 del self.wantedancestors[r]
210 209 del self.parentmap[r]
211 210 del self.seenchildren[r]
212 211 if self._rebuilt:
213 212 del self.children[r]
214 213
215 214 def wanted(self, rev, i):
216 215 # Return True if we're directly interested in rev.
217 216 #
218 217 # i is an index selecting one of the parents of rev (if rev
219 218 # has no parents, i is None). getchangedfiles will give us
220 219 # the list of files that are different in rev and in the parent
221 220 # indicated by i. If we're interested in any of these files,
222 221 # we're interested in rev.
223 222 try:
224 223 files = self.base.getchangedfiles(rev, i)
225 224 except NotImplementedError:
226 225 raise util.Abort(_("source repository doesn't support --filemap"))
227 226 for f in files:
228 227 if self.filemapper(f):
229 228 return True
230 229 return False
231 230
232 231 def mark_not_wanted(self, rev, p):
233 232 # Mark rev as not interesting and update data structures.
234 233
235 234 if p is None:
236 235 # A root revision. Use SKIPREV to indicate that it doesn't
237 236 # map to any revision in the restricted graph. Put SKIPREV
238 237 # in the set of wanted ancestors to simplify code elsewhere
239 238 self.parentmap[rev] = SKIPREV
240 239 self.wantedancestors[rev] = set((SKIPREV,))
241 240 return
242 241
243 242 # Reuse the data from our parent.
244 243 self.parentmap[rev] = self.parentmap[p]
245 244 self.wantedancestors[rev] = self.wantedancestors[p]
246 245
247 246 def mark_wanted(self, rev, parents):
248 247 # Mark rev ss wanted and update data structures.
249 248
250 249 # rev will be in the restricted graph, so children of rev in
251 250 # the original graph should still have rev as a parent in the
252 251 # restricted graph.
253 252 self.parentmap[rev] = rev
254 253
255 254 # The set of wanted ancestors of rev is the union of the sets
256 255 # of wanted ancestors of its parents. Plus rev itself.
257 256 wrev = set()
258 257 for p in parents:
259 258 wrev.update(self.wantedancestors[p])
260 259 wrev.add(rev)
261 260 self.wantedancestors[rev] = wrev
262 261
263 262 def getchanges(self, rev):
264 263 parents = self.commits[rev].parents
265 264 if len(parents) > 1:
266 265 self.rebuild()
267 266
268 267 # To decide whether we're interested in rev we:
269 268 #
270 269 # - calculate what parents rev will have if it turns out we're
271 270 # interested in it. If it's going to have more than 1 parent,
272 271 # we're interested in it.
273 272 #
274 273 # - otherwise, we'll compare it with the single parent we found.
275 274 # If any of the files we're interested in is different in the
276 275 # the two revisions, we're interested in rev.
277 276
278 277 # A parent p is interesting if its mapped version (self.parentmap[p]):
279 278 # - is not SKIPREV
280 279 # - is still not in the list of parents (we don't want duplicates)
281 280 # - is not an ancestor of the mapped versions of the other parents
282 281 mparents = []
283 282 wp = None
284 283 for i, p1 in enumerate(parents):
285 284 mp1 = self.parentmap[p1]
286 285 if mp1 == SKIPREV or mp1 in mparents:
287 286 continue
288 287 for p2 in parents:
289 288 if p1 == p2 or mp1 == self.parentmap[p2]:
290 289 continue
291 290 if mp1 in self.wantedancestors[p2]:
292 291 break
293 292 else:
294 293 mparents.append(mp1)
295 294 wp = i
296 295
297 296 if wp is None and parents:
298 297 wp = 0
299 298
300 299 self.origparents[rev] = parents
301 300
302 301 if len(mparents) < 2 and not self.wanted(rev, wp):
303 302 # We don't want this revision.
304 303 # Update our state and tell the convert process to map this
305 304 # revision to the same revision its parent as mapped to.
306 305 p = None
307 306 if parents:
308 307 p = parents[wp]
309 308 self.mark_not_wanted(rev, p)
310 309 self.convertedorder.append((rev, False, p))
311 310 self._discard(*parents)
312 311 return self.parentmap[rev]
313 312
314 313 # We want this revision.
315 314 # Rewrite the parents of the commit object
316 315 self.commits[rev].parents = mparents
317 316 self.mark_wanted(rev, parents)
318 317 self.convertedorder.append((rev, True, None))
319 318 self._discard(*parents)
320 319
321 # Get the real changes and do the filtering/mapping.
322 # To be able to get the files later on in getfile and getmode,
323 # we hide the original filename in the rev part of the return
324 # value.
320 # Get the real changes and do the filtering/mapping. To be
321 # able to get the files later on in getfile, we hide the
322 # original filename in the rev part of the return value.
325 323 changes, copies = self.base.getchanges(rev)
326 324 newnames = {}
327 325 files = []
328 326 for f, r in changes:
329 327 newf = self.filemapper(f)
330 328 if newf:
331 329 files.append((newf, (f, r)))
332 330 newnames[f] = newf
333 331
334 332 ncopies = {}
335 333 for c in copies:
336 334 newc = self.filemapper(c)
337 335 if newc:
338 336 newsource = self.filemapper(copies[c])
339 337 if newsource:
340 338 ncopies[newc] = newsource
341 339
342 340 return files, ncopies
343 341
344 342 def getfile(self, name, rev):
345 343 realname, realrev = rev
346 344 return self.base.getfile(realname, realrev)
347 345
348 def getmode(self, name, rev):
349 realname, realrev = rev
350 return self.base.getmode(realname, realrev)
351
352 346 def gettags(self):
353 347 return self.base.gettags()
354 348
355 349 def hasnativeorder(self):
356 350 return self.base.hasnativeorder()
357 351
358 352 def lookuprev(self, rev):
359 353 return self.base.lookuprev(rev)
@@ -1,170 +1,169 b''
1 1 # git.py - git support for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import os
9 9 from mercurial import util
10 10 from mercurial.i18n import _
11 11
12 12 from common import NoRepo, commit, converter_source, checktool
13 13
14 14 class convert_git(converter_source):
15 15 # Windows does not support GIT_DIR= construct while other systems
16 16 # cannot remove environment variable. Just assume none have
17 17 # both issues.
18 18 if hasattr(os, 'unsetenv'):
19 19 def gitopen(self, s):
20 20 prevgitdir = os.environ.get('GIT_DIR')
21 21 os.environ['GIT_DIR'] = self.path
22 22 try:
23 23 return util.popen(s, 'rb')
24 24 finally:
25 25 if prevgitdir is None:
26 26 del os.environ['GIT_DIR']
27 27 else:
28 28 os.environ['GIT_DIR'] = prevgitdir
29 29 else:
30 30 def gitopen(self, s):
31 31 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
32 32
33 33 def gitread(self, s):
34 34 fh = self.gitopen(s)
35 35 data = fh.read()
36 36 return data, fh.close()
37 37
38 38 def __init__(self, ui, path, rev=None):
39 39 super(convert_git, self).__init__(ui, path, rev=rev)
40 40
41 41 if os.path.isdir(path + "/.git"):
42 42 path += "/.git"
43 43 if not os.path.exists(path + "/objects"):
44 44 raise NoRepo(_("%s does not look like a Git repository") % path)
45 45
46 46 checktool('git', 'git')
47 47
48 48 self.path = path
49 49
50 50 def getheads(self):
51 51 if not self.rev:
52 52 heads, ret = self.gitread('git rev-parse --branches --remotes')
53 53 heads = heads.splitlines()
54 54 else:
55 55 heads, ret = self.gitread("git rev-parse --verify %s" % self.rev)
56 56 heads = [heads[:-1]]
57 57 if ret:
58 58 raise util.Abort(_('cannot retrieve git heads'))
59 59 return heads
60 60
61 61 def catfile(self, rev, type):
62 62 if rev == "0" * 40:
63 63 raise IOError()
64 64 data, ret = self.gitread("git cat-file %s %s" % (type, rev))
65 65 if ret:
66 66 raise util.Abort(_('cannot read %r object at %s') % (type, rev))
67 67 return data
68 68
69 69 def getfile(self, name, rev):
70 return self.catfile(rev, "blob")
71
72 def getmode(self, name, rev):
73 return self.modecache[(name, rev)]
70 data = self.catfile(rev, "blob")
71 mode = self.modecache[(name, rev)]
72 return data, mode
74 73
75 74 def getchanges(self, version):
76 75 self.modecache = {}
77 76 fh = self.gitopen("git diff-tree -z --root -m -r %s" % version)
78 77 changes = []
79 78 seen = set()
80 79 entry = None
81 80 for l in fh.read().split('\x00'):
82 81 if not entry:
83 82 if not l.startswith(':'):
84 83 continue
85 84 entry = l
86 85 continue
87 86 f = l
88 87 if f not in seen:
89 88 seen.add(f)
90 89 entry = entry.split()
91 90 h = entry[3]
92 91 p = (entry[1] == "100755")
93 92 s = (entry[1] == "120000")
94 93 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
95 94 changes.append((f, h))
96 95 entry = None
97 96 if fh.close():
98 97 raise util.Abort(_('cannot read changes in %s') % version)
99 98 return (changes, {})
100 99
101 100 def getcommit(self, version):
102 101 c = self.catfile(version, "commit") # read the commit hash
103 102 end = c.find("\n\n")
104 103 message = c[end + 2:]
105 104 message = self.recode(message)
106 105 l = c[:end].splitlines()
107 106 parents = []
108 107 author = committer = None
109 108 for e in l[1:]:
110 109 n, v = e.split(" ", 1)
111 110 if n == "author":
112 111 p = v.split()
113 112 tm, tz = p[-2:]
114 113 author = " ".join(p[:-2])
115 114 if author[0] == "<": author = author[1:-1]
116 115 author = self.recode(author)
117 116 if n == "committer":
118 117 p = v.split()
119 118 tm, tz = p[-2:]
120 119 committer = " ".join(p[:-2])
121 120 if committer[0] == "<": committer = committer[1:-1]
122 121 committer = self.recode(committer)
123 122 if n == "parent":
124 123 parents.append(v)
125 124
126 125 if committer and committer != author:
127 126 message += "\ncommitter: %s\n" % committer
128 127 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
129 128 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
130 129 date = tm + " " + str(tz)
131 130
132 131 c = commit(parents=parents, date=date, author=author, desc=message,
133 132 rev=version)
134 133 return c
135 134
136 135 def gettags(self):
137 136 tags = {}
138 137 fh = self.gitopen('git ls-remote --tags "%s"' % self.path)
139 138 prefix = 'refs/tags/'
140 139 for line in fh:
141 140 line = line.strip()
142 141 if not line.endswith("^{}"):
143 142 continue
144 143 node, tag = line.split(None, 1)
145 144 if not tag.startswith(prefix):
146 145 continue
147 146 tag = tag[len(prefix):-3]
148 147 tags[tag] = node
149 148 if fh.close():
150 149 raise util.Abort(_('cannot read tags from %s') % self.path)
151 150
152 151 return tags
153 152
154 153 def getchangedfiles(self, version, i):
155 154 changes = []
156 155 if i is None:
157 156 fh = self.gitopen("git diff-tree --root -m -r %s" % version)
158 157 for l in fh:
159 158 if "\t" not in l:
160 159 continue
161 160 m, f = l[:-1].split("\t")
162 161 changes.append(f)
163 162 else:
164 163 fh = self.gitopen('git diff-tree --name-only --root -r %s "%s^%s" --'
165 164 % (version, version, i + 1))
166 165 changes = [f.rstrip('\n') for f in fh]
167 166 if fh.close():
168 167 raise util.Abort(_('cannot read changes in %s') % version)
169 168
170 169 return changes
@@ -1,346 +1,338 b''
1 1 # gnuarch.py - GNU Arch support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 4 # and others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from common import NoRepo, commandline, commit, converter_source
10 10 from mercurial.i18n import _
11 11 from mercurial import util
12 12 import os, shutil, tempfile, stat, locale
13 13 from email.Parser import Parser
14 14
15 15 class gnuarch_source(converter_source, commandline):
16 16
17 17 class gnuarch_rev(object):
18 18 def __init__(self, rev):
19 19 self.rev = rev
20 20 self.summary = ''
21 21 self.date = None
22 22 self.author = ''
23 23 self.continuationof = None
24 24 self.add_files = []
25 25 self.mod_files = []
26 26 self.del_files = []
27 27 self.ren_files = {}
28 28 self.ren_dirs = {}
29 29
30 30 def __init__(self, ui, path, rev=None):
31 31 super(gnuarch_source, self).__init__(ui, path, rev=rev)
32 32
33 33 if not os.path.exists(os.path.join(path, '{arch}')):
34 34 raise NoRepo(_("%s does not look like a GNU Arch repository")
35 35 % path)
36 36
37 37 # Could use checktool, but we want to check for baz or tla.
38 38 self.execmd = None
39 39 if util.find_exe('baz'):
40 40 self.execmd = 'baz'
41 41 else:
42 42 if util.find_exe('tla'):
43 43 self.execmd = 'tla'
44 44 else:
45 45 raise util.Abort(_('cannot find a GNU Arch tool'))
46 46
47 47 commandline.__init__(self, ui, self.execmd)
48 48
49 49 self.path = os.path.realpath(path)
50 50 self.tmppath = None
51 51
52 52 self.treeversion = None
53 53 self.lastrev = None
54 54 self.changes = {}
55 55 self.parents = {}
56 56 self.tags = {}
57 self.modecache = {}
58 57 self.catlogparser = Parser()
59 58 self.locale = locale.getpreferredencoding()
60 59 self.archives = []
61 60
62 61 def before(self):
63 62 # Get registered archives
64 63 self.archives = [i.rstrip('\n')
65 64 for i in self.runlines0('archives', '-n')]
66 65
67 66 if self.execmd == 'tla':
68 67 output = self.run0('tree-version', self.path)
69 68 else:
70 69 output = self.run0('tree-version', '-d', self.path)
71 70 self.treeversion = output.strip()
72 71
73 72 # Get name of temporary directory
74 73 version = self.treeversion.split('/')
75 74 self.tmppath = os.path.join(tempfile.gettempdir(),
76 75 'hg-%s' % version[1])
77 76
78 77 # Generate parents dictionary
79 78 self.parents[None] = []
80 79 treeversion = self.treeversion
81 80 child = None
82 81 while treeversion:
83 82 self.ui.status(_('analyzing tree version %s...\n') % treeversion)
84 83
85 84 archive = treeversion.split('/')[0]
86 85 if archive not in self.archives:
87 86 self.ui.status(_('tree analysis stopped because it points to '
88 87 'an unregistered archive %s...\n') % archive)
89 88 break
90 89
91 90 # Get the complete list of revisions for that tree version
92 91 output, status = self.runlines('revisions', '-r', '-f', treeversion)
93 92 self.checkexit(status, 'failed retrieveing revisions for %s'
94 93 % treeversion)
95 94
96 95 # No new iteration unless a revision has a continuation-of header
97 96 treeversion = None
98 97
99 98 for l in output:
100 99 rev = l.strip()
101 100 self.changes[rev] = self.gnuarch_rev(rev)
102 101 self.parents[rev] = []
103 102
104 103 # Read author, date and summary
105 104 catlog, status = self.run('cat-log', '-d', self.path, rev)
106 105 if status:
107 106 catlog = self.run0('cat-archive-log', rev)
108 107 self._parsecatlog(catlog, rev)
109 108
110 109 # Populate the parents map
111 110 self.parents[child].append(rev)
112 111
113 112 # Keep track of the current revision as the child of the next
114 113 # revision scanned
115 114 child = rev
116 115
117 116 # Check if we have to follow the usual incremental history
118 117 # or if we have to 'jump' to a different treeversion given
119 118 # by the continuation-of header.
120 119 if self.changes[rev].continuationof:
121 120 treeversion = '--'.join(
122 121 self.changes[rev].continuationof.split('--')[:-1])
123 122 break
124 123
125 124 # If we reached a base-0 revision w/o any continuation-of
126 125 # header, it means the tree history ends here.
127 126 if rev[-6:] == 'base-0':
128 127 break
129 128
130 129 def after(self):
131 130 self.ui.debug('cleaning up %s\n' % self.tmppath)
132 131 shutil.rmtree(self.tmppath, ignore_errors=True)
133 132
134 133 def getheads(self):
135 134 return self.parents[None]
136 135
137 136 def getfile(self, name, rev):
138 137 if rev != self.lastrev:
139 138 raise util.Abort(_('internal calling inconsistency'))
140 139
141 140 # Raise IOError if necessary (i.e. deleted files).
142 141 if not os.path.exists(os.path.join(self.tmppath, name)):
143 142 raise IOError
144 143
145 data, mode = self._getfile(name, rev)
146 self.modecache[(name, rev)] = mode
147
148 return data
149
150 def getmode(self, name, rev):
151 return self.modecache[(name, rev)]
144 return self._getfile(name, rev)
152 145
153 146 def getchanges(self, rev):
154 self.modecache = {}
155 147 self._update(rev)
156 148 changes = []
157 149 copies = {}
158 150
159 151 for f in self.changes[rev].add_files:
160 152 changes.append((f, rev))
161 153
162 154 for f in self.changes[rev].mod_files:
163 155 changes.append((f, rev))
164 156
165 157 for f in self.changes[rev].del_files:
166 158 changes.append((f, rev))
167 159
168 160 for src in self.changes[rev].ren_files:
169 161 to = self.changes[rev].ren_files[src]
170 162 changes.append((src, rev))
171 163 changes.append((to, rev))
172 164 copies[to] = src
173 165
174 166 for src in self.changes[rev].ren_dirs:
175 167 to = self.changes[rev].ren_dirs[src]
176 168 chgs, cps = self._rendirchanges(src, to)
177 169 changes += [(f, rev) for f in chgs]
178 170 copies.update(cps)
179 171
180 172 self.lastrev = rev
181 173 return sorted(set(changes)), copies
182 174
183 175 def getcommit(self, rev):
184 176 changes = self.changes[rev]
185 177 return commit(author=changes.author, date=changes.date,
186 178 desc=changes.summary, parents=self.parents[rev], rev=rev)
187 179
188 180 def gettags(self):
189 181 return self.tags
190 182
191 183 def _execute(self, cmd, *args, **kwargs):
192 184 cmdline = [self.execmd, cmd]
193 185 cmdline += args
194 186 cmdline = [util.shellquote(arg) for arg in cmdline]
195 187 cmdline += ['>', util.nulldev, '2>', util.nulldev]
196 188 cmdline = util.quotecommand(' '.join(cmdline))
197 189 self.ui.debug(cmdline, '\n')
198 190 return os.system(cmdline)
199 191
200 192 def _update(self, rev):
201 193 self.ui.debug('applying revision %s...\n' % rev)
202 194 changeset, status = self.runlines('replay', '-d', self.tmppath,
203 195 rev)
204 196 if status:
205 197 # Something went wrong while merging (baz or tla
206 198 # issue?), get latest revision and try from there
207 199 shutil.rmtree(self.tmppath, ignore_errors=True)
208 200 self._obtainrevision(rev)
209 201 else:
210 202 old_rev = self.parents[rev][0]
211 203 self.ui.debug('computing changeset between %s and %s...\n'
212 204 % (old_rev, rev))
213 205 self._parsechangeset(changeset, rev)
214 206
215 207 def _getfile(self, name, rev):
216 208 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
217 209 if stat.S_ISLNK(mode):
218 210 data = os.readlink(os.path.join(self.tmppath, name))
219 211 mode = mode and 'l' or ''
220 212 else:
221 213 data = open(os.path.join(self.tmppath, name), 'rb').read()
222 214 mode = (mode & 0111) and 'x' or ''
223 215 return data, mode
224 216
225 217 def _exclude(self, name):
226 218 exclude = ['{arch}', '.arch-ids', '.arch-inventory']
227 219 for exc in exclude:
228 220 if name.find(exc) != -1:
229 221 return True
230 222 return False
231 223
232 224 def _readcontents(self, path):
233 225 files = []
234 226 contents = os.listdir(path)
235 227 while len(contents) > 0:
236 228 c = contents.pop()
237 229 p = os.path.join(path, c)
238 230 # os.walk could be used, but here we avoid internal GNU
239 231 # Arch files and directories, thus saving a lot time.
240 232 if not self._exclude(p):
241 233 if os.path.isdir(p):
242 234 contents += [os.path.join(c, f) for f in os.listdir(p)]
243 235 else:
244 236 files.append(c)
245 237 return files
246 238
247 239 def _rendirchanges(self, src, dest):
248 240 changes = []
249 241 copies = {}
250 242 files = self._readcontents(os.path.join(self.tmppath, dest))
251 243 for f in files:
252 244 s = os.path.join(src, f)
253 245 d = os.path.join(dest, f)
254 246 changes.append(s)
255 247 changes.append(d)
256 248 copies[d] = s
257 249 return changes, copies
258 250
259 251 def _obtainrevision(self, rev):
260 252 self.ui.debug('obtaining revision %s...\n' % rev)
261 253 output = self._execute('get', rev, self.tmppath)
262 254 self.checkexit(output)
263 255 self.ui.debug('analyzing revision %s...\n' % rev)
264 256 files = self._readcontents(self.tmppath)
265 257 self.changes[rev].add_files += files
266 258
267 259 def _stripbasepath(self, path):
268 260 if path.startswith('./'):
269 261 return path[2:]
270 262 return path
271 263
272 264 def _parsecatlog(self, data, rev):
273 265 try:
274 266 catlog = self.catlogparser.parsestr(data)
275 267
276 268 # Commit date
277 269 self.changes[rev].date = util.datestr(
278 270 util.strdate(catlog['Standard-date'],
279 271 '%Y-%m-%d %H:%M:%S'))
280 272
281 273 # Commit author
282 274 self.changes[rev].author = self.recode(catlog['Creator'])
283 275
284 276 # Commit description
285 277 self.changes[rev].summary = '\n\n'.join((catlog['Summary'],
286 278 catlog.get_payload()))
287 279 self.changes[rev].summary = self.recode(self.changes[rev].summary)
288 280
289 281 # Commit revision origin when dealing with a branch or tag
290 282 if 'Continuation-of' in catlog:
291 283 self.changes[rev].continuationof = self.recode(
292 284 catlog['Continuation-of'])
293 285 except Exception:
294 286 raise util.Abort(_('could not parse cat-log of %s') % rev)
295 287
296 288 def _parsechangeset(self, data, rev):
297 289 for l in data:
298 290 l = l.strip()
299 291 # Added file (ignore added directory)
300 292 if l.startswith('A') and not l.startswith('A/'):
301 293 file = self._stripbasepath(l[1:].strip())
302 294 if not self._exclude(file):
303 295 self.changes[rev].add_files.append(file)
304 296 # Deleted file (ignore deleted directory)
305 297 elif l.startswith('D') and not l.startswith('D/'):
306 298 file = self._stripbasepath(l[1:].strip())
307 299 if not self._exclude(file):
308 300 self.changes[rev].del_files.append(file)
309 301 # Modified binary file
310 302 elif l.startswith('Mb'):
311 303 file = self._stripbasepath(l[2:].strip())
312 304 if not self._exclude(file):
313 305 self.changes[rev].mod_files.append(file)
314 306 # Modified link
315 307 elif l.startswith('M->'):
316 308 file = self._stripbasepath(l[3:].strip())
317 309 if not self._exclude(file):
318 310 self.changes[rev].mod_files.append(file)
319 311 # Modified file
320 312 elif l.startswith('M'):
321 313 file = self._stripbasepath(l[1:].strip())
322 314 if not self._exclude(file):
323 315 self.changes[rev].mod_files.append(file)
324 316 # Renamed file (or link)
325 317 elif l.startswith('=>'):
326 318 files = l[2:].strip().split(' ')
327 319 if len(files) == 1:
328 320 files = l[2:].strip().split('\t')
329 321 src = self._stripbasepath(files[0])
330 322 dst = self._stripbasepath(files[1])
331 323 if not self._exclude(src) and not self._exclude(dst):
332 324 self.changes[rev].ren_files[src] = dst
333 325 # Conversion from file to link or from link to file (modified)
334 326 elif l.startswith('ch'):
335 327 file = self._stripbasepath(l[2:].strip())
336 328 if not self._exclude(file):
337 329 self.changes[rev].mod_files.append(file)
338 330 # Renamed directory
339 331 elif l.startswith('/>'):
340 332 dirs = l[2:].strip().split(' ')
341 333 if len(dirs) == 1:
342 334 dirs = l[2:].strip().split('\t')
343 335 src = self._stripbasepath(dirs[0])
344 336 dst = self._stripbasepath(dirs[1])
345 337 if not self._exclude(src) and not self._exclude(dst):
346 338 self.changes[rev].ren_dirs[src] = dst
@@ -1,377 +1,375 b''
1 1 # hg.py - hg backend for convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # Notes for hg->hg conversion:
9 9 #
10 10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 11 # of commit messages, but new versions do. Changesets created by
12 12 # those older versions, then converted, may thus have different
13 13 # hashes for changesets that are otherwise identical.
14 14 #
15 15 # * Using "--config convert.hg.saverev=true" will make the source
16 16 # identifier to be stored in the converted revision. This will cause
17 17 # the converted revision to have a different identity than the
18 18 # source.
19 19
20 20
21 21 import os, time, cStringIO
22 22 from mercurial.i18n import _
23 23 from mercurial.node import bin, hex, nullid
24 24 from mercurial import hg, util, context, error
25 25
26 26 from common import NoRepo, commit, converter_source, converter_sink
27 27
28 28 class mercurial_sink(converter_sink):
29 29 def __init__(self, ui, path):
30 30 converter_sink.__init__(self, ui, path)
31 31 self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True)
32 32 self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False)
33 33 self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default')
34 34 self.lastbranch = None
35 35 if os.path.isdir(path) and len(os.listdir(path)) > 0:
36 36 try:
37 37 self.repo = hg.repository(self.ui, path)
38 38 if not self.repo.local():
39 39 raise NoRepo(_('%s is not a local Mercurial repository')
40 40 % path)
41 41 except error.RepoError, err:
42 42 ui.traceback()
43 43 raise NoRepo(err.args[0])
44 44 else:
45 45 try:
46 46 ui.status(_('initializing destination %s repository\n') % path)
47 47 self.repo = hg.repository(self.ui, path, create=True)
48 48 if not self.repo.local():
49 49 raise NoRepo(_('%s is not a local Mercurial repository')
50 50 % path)
51 51 self.created.append(path)
52 52 except error.RepoError:
53 53 ui.traceback()
54 54 raise NoRepo(_("could not create hg repository %s as sink")
55 55 % path)
56 56 self.lock = None
57 57 self.wlock = None
58 58 self.filemapmode = False
59 59
60 60 def before(self):
61 61 self.ui.debug('run hg sink pre-conversion action\n')
62 62 self.wlock = self.repo.wlock()
63 63 self.lock = self.repo.lock()
64 64
65 65 def after(self):
66 66 self.ui.debug('run hg sink post-conversion action\n')
67 67 if self.lock:
68 68 self.lock.release()
69 69 if self.wlock:
70 70 self.wlock.release()
71 71
72 72 def revmapfile(self):
73 73 return os.path.join(self.path, ".hg", "shamap")
74 74
75 75 def authorfile(self):
76 76 return os.path.join(self.path, ".hg", "authormap")
77 77
78 78 def getheads(self):
79 79 h = self.repo.changelog.heads()
80 80 return [hex(x) for x in h]
81 81
82 82 def setbranch(self, branch, pbranches):
83 83 if not self.clonebranches:
84 84 return
85 85
86 86 setbranch = (branch != self.lastbranch)
87 87 self.lastbranch = branch
88 88 if not branch:
89 89 branch = 'default'
90 90 pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches]
91 91 pbranch = pbranches and pbranches[0][1] or 'default'
92 92
93 93 branchpath = os.path.join(self.path, branch)
94 94 if setbranch:
95 95 self.after()
96 96 try:
97 97 self.repo = hg.repository(self.ui, branchpath)
98 98 except:
99 99 self.repo = hg.repository(self.ui, branchpath, create=True)
100 100 self.before()
101 101
102 102 # pbranches may bring revisions from other branches (merge parents)
103 103 # Make sure we have them, or pull them.
104 104 missings = {}
105 105 for b in pbranches:
106 106 try:
107 107 self.repo.lookup(b[0])
108 108 except:
109 109 missings.setdefault(b[1], []).append(b[0])
110 110
111 111 if missings:
112 112 self.after()
113 113 for pbranch, heads in missings.iteritems():
114 114 pbranchpath = os.path.join(self.path, pbranch)
115 115 prepo = hg.repository(self.ui, pbranchpath)
116 116 self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch))
117 117 self.repo.pull(prepo, [prepo.lookup(h) for h in heads])
118 118 self.before()
119 119
120 120 def _rewritetags(self, source, revmap, data):
121 121 fp = cStringIO.StringIO()
122 122 for line in data.splitlines():
123 123 s = line.split(' ', 1)
124 124 if len(s) != 2:
125 125 continue
126 126 revid = revmap.get(source.lookuprev(s[0]))
127 127 if not revid:
128 128 continue
129 129 fp.write('%s %s\n' % (revid, s[1]))
130 130 return fp.getvalue()
131 131
132 132 def putcommit(self, files, copies, parents, commit, source, revmap):
133 133
134 134 files = dict(files)
135 135 def getfilectx(repo, memctx, f):
136 136 v = files[f]
137 data = source.getfile(f, v)
138 e = source.getmode(f, v)
137 data, mode = source.getfile(f, v)
139 138 if f == '.hgtags':
140 139 data = self._rewritetags(source, revmap, data)
141 return context.memfilectx(f, data, 'l' in e, 'x' in e, copies.get(f))
140 return context.memfilectx(f, data, 'l' in mode, 'x' in mode,
141 copies.get(f))
142 142
143 143 pl = []
144 144 for p in parents:
145 145 if p not in pl:
146 146 pl.append(p)
147 147 parents = pl
148 148 nparents = len(parents)
149 149 if self.filemapmode and nparents == 1:
150 150 m1node = self.repo.changelog.read(bin(parents[0]))[0]
151 151 parent = parents[0]
152 152
153 153 if len(parents) < 2:
154 154 parents.append(nullid)
155 155 if len(parents) < 2:
156 156 parents.append(nullid)
157 157 p2 = parents.pop(0)
158 158
159 159 text = commit.desc
160 160 extra = commit.extra.copy()
161 161 if self.branchnames and commit.branch:
162 162 extra['branch'] = commit.branch
163 163 if commit.rev:
164 164 extra['convert_revision'] = commit.rev
165 165
166 166 while parents:
167 167 p1 = p2
168 168 p2 = parents.pop(0)
169 169 ctx = context.memctx(self.repo, (p1, p2), text, files.keys(),
170 170 getfilectx, commit.author, commit.date, extra)
171 171 self.repo.commitctx(ctx)
172 172 text = "(octopus merge fixup)\n"
173 173 p2 = hex(self.repo.changelog.tip())
174 174
175 175 if self.filemapmode and nparents == 1:
176 176 man = self.repo.manifest
177 177 mnode = self.repo.changelog.read(bin(p2))[0]
178 178 if not man.cmp(m1node, man.revision(mnode)):
179 179 self.ui.status(_("filtering out empty revision\n"))
180 180 self.repo.rollback()
181 181 return parent
182 182 return p2
183 183
184 184 def puttags(self, tags):
185 185 try:
186 186 parentctx = self.repo[self.tagsbranch]
187 187 tagparent = parentctx.node()
188 188 except error.RepoError:
189 189 parentctx = None
190 190 tagparent = nullid
191 191
192 192 try:
193 193 oldlines = sorted(parentctx['.hgtags'].data().splitlines(True))
194 194 except:
195 195 oldlines = []
196 196
197 197 newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags])
198 198 if newlines == oldlines:
199 199 return None, None
200 200 data = "".join(newlines)
201 201 def getfilectx(repo, memctx, f):
202 202 return context.memfilectx(f, data, False, False, None)
203 203
204 204 self.ui.status(_("updating tags\n"))
205 205 date = "%s 0" % int(time.mktime(time.gmtime()))
206 206 extra = {'branch': self.tagsbranch}
207 207 ctx = context.memctx(self.repo, (tagparent, None), "update tags",
208 208 [".hgtags"], getfilectx, "convert-repo", date,
209 209 extra)
210 210 self.repo.commitctx(ctx)
211 211 return hex(self.repo.changelog.tip()), hex(tagparent)
212 212
213 213 def setfilemapmode(self, active):
214 214 self.filemapmode = active
215 215
216 216 class mercurial_source(converter_source):
217 217 def __init__(self, ui, path, rev=None):
218 218 converter_source.__init__(self, ui, path, rev)
219 219 self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False)
220 220 self.ignored = set()
221 221 self.saverev = ui.configbool('convert', 'hg.saverev', False)
222 222 try:
223 223 self.repo = hg.repository(self.ui, path)
224 224 # try to provoke an exception if this isn't really a hg
225 225 # repo, but some other bogus compatible-looking url
226 226 if not self.repo.local():
227 227 raise error.RepoError()
228 228 except error.RepoError:
229 229 ui.traceback()
230 230 raise NoRepo(_("%s is not a local Mercurial repository") % path)
231 231 self.lastrev = None
232 232 self.lastctx = None
233 233 self._changescache = None
234 234 self.convertfp = None
235 235 # Restrict converted revisions to startrev descendants
236 236 startnode = ui.config('convert', 'hg.startrev')
237 237 if startnode is not None:
238 238 try:
239 239 startnode = self.repo.lookup(startnode)
240 240 except error.RepoError:
241 241 raise util.Abort(_('%s is not a valid start revision')
242 242 % startnode)
243 243 startrev = self.repo.changelog.rev(startnode)
244 244 children = {startnode: 1}
245 245 for rev in self.repo.changelog.descendants(startrev):
246 246 children[self.repo.changelog.node(rev)] = 1
247 247 self.keep = children.__contains__
248 248 else:
249 249 self.keep = util.always
250 250
251 251 def changectx(self, rev):
252 252 if self.lastrev != rev:
253 253 self.lastctx = self.repo[rev]
254 254 self.lastrev = rev
255 255 return self.lastctx
256 256
257 257 def parents(self, ctx):
258 258 return [p for p in ctx.parents() if p and self.keep(p.node())]
259 259
260 260 def getheads(self):
261 261 if self.rev:
262 262 heads = [self.repo[self.rev].node()]
263 263 else:
264 264 heads = self.repo.heads()
265 265 return [hex(h) for h in heads if self.keep(h)]
266 266
267 267 def getfile(self, name, rev):
268 268 try:
269 return self.changectx(rev)[name].data()
269 fctx = self.changectx(rev)[name]
270 return fctx.data(), fctx.flags()
270 271 except error.LookupError, err:
271 272 raise IOError(err)
272 273
273 def getmode(self, name, rev):
274 return self.changectx(rev).manifest().flags(name)
275
276 274 def getchanges(self, rev):
277 275 ctx = self.changectx(rev)
278 276 parents = self.parents(ctx)
279 277 if not parents:
280 278 files = sorted(ctx.manifest())
281 279 if self.ignoreerrors:
282 280 # calling getcopies() is a simple way to detect missing
283 281 # revlogs and populate self.ignored
284 282 self.getcopies(ctx, parents, files)
285 283 return [(f, rev) for f in files if f not in self.ignored], {}
286 284 if self._changescache and self._changescache[0] == rev:
287 285 m, a, r = self._changescache[1]
288 286 else:
289 287 m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3]
290 288 # getcopies() detects missing revlogs early, run it before
291 289 # filtering the changes.
292 290 copies = self.getcopies(ctx, parents, m + a)
293 291 changes = [(name, rev) for name in m + a + r
294 292 if name not in self.ignored]
295 293 return sorted(changes), copies
296 294
297 295 def getcopies(self, ctx, parents, files):
298 296 copies = {}
299 297 for name in files:
300 298 if name in self.ignored:
301 299 continue
302 300 try:
303 301 copysource, copynode = ctx.filectx(name).renamed()
304 302 if copysource in self.ignored or not self.keep(copynode):
305 303 continue
306 304 # Ignore copy sources not in parent revisions
307 305 found = False
308 306 for p in parents:
309 307 if copysource in p:
310 308 found = True
311 309 break
312 310 if not found:
313 311 continue
314 312 copies[name] = copysource
315 313 except TypeError:
316 314 pass
317 315 except error.LookupError, e:
318 316 if not self.ignoreerrors:
319 317 raise
320 318 self.ignored.add(name)
321 319 self.ui.warn(_('ignoring: %s\n') % e)
322 320 return copies
323 321
324 322 def getcommit(self, rev):
325 323 ctx = self.changectx(rev)
326 324 parents = [p.hex() for p in self.parents(ctx)]
327 325 if self.saverev:
328 326 crev = rev
329 327 else:
330 328 crev = None
331 329 return commit(author=ctx.user(), date=util.datestr(ctx.date()),
332 330 desc=ctx.description(), rev=crev, parents=parents,
333 331 branch=ctx.branch(), extra=ctx.extra(),
334 332 sortkey=ctx.rev())
335 333
336 334 def gettags(self):
337 335 tags = [t for t in self.repo.tagslist() if t[0] != 'tip']
338 336 return dict([(name, hex(node)) for name, node in tags
339 337 if self.keep(node)])
340 338
341 339 def getchangedfiles(self, rev, i):
342 340 ctx = self.changectx(rev)
343 341 parents = self.parents(ctx)
344 342 if not parents and i is None:
345 343 i = 0
346 344 changes = [], ctx.manifest().keys(), []
347 345 else:
348 346 i = i or 0
349 347 changes = self.repo.status(parents[i].node(), ctx.node())[:3]
350 348 changes = [[f for f in l if f not in self.ignored] for l in changes]
351 349
352 350 if i == 0:
353 351 self._changescache = (rev, changes)
354 352
355 353 return changes[0] + changes[1] + changes[2]
356 354
357 355 def converted(self, rev, destrev):
358 356 if self.convertfp is None:
359 357 self.convertfp = open(os.path.join(self.path, '.hg', 'shamap'),
360 358 'a')
361 359 self.convertfp.write('%s %s\n' % (destrev, rev))
362 360 self.convertfp.flush()
363 361
364 362 def before(self):
365 363 self.ui.debug('run hg source pre-conversion action\n')
366 364
367 365 def after(self):
368 366 self.ui.debug('run hg source post-conversion action\n')
369 367
370 368 def hasnativeorder(self):
371 369 return True
372 370
373 371 def lookuprev(self, rev):
374 372 try:
375 373 return hex(self.repo.lookup(rev))
376 374 except error.RepoError:
377 375 return None
@@ -1,229 +1,227 b''
1 1 # monotone.py - monotone support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 4 # others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 import os, re
10 10 from mercurial import util
11 11 from common import NoRepo, commit, converter_source, checktool
12 12 from common import commandline
13 13 from mercurial.i18n import _
14 14
15 15 class monotone_source(converter_source, commandline):
16 16 def __init__(self, ui, path=None, rev=None):
17 17 converter_source.__init__(self, ui, path, rev)
18 18 commandline.__init__(self, ui, 'mtn')
19 19
20 20 self.ui = ui
21 21 self.path = path
22 22
23 23 norepo = NoRepo(_("%s does not look like a monotone repository")
24 24 % path)
25 25 if not os.path.exists(os.path.join(path, '_MTN')):
26 26 # Could be a monotone repository (SQLite db file)
27 27 try:
28 28 header = file(path, 'rb').read(16)
29 29 except:
30 30 header = ''
31 31 if header != 'SQLite format 3\x00':
32 32 raise norepo
33 33
34 34 # regular expressions for parsing monotone output
35 35 space = r'\s*'
36 36 name = r'\s+"((?:\\"|[^"])*)"\s*'
37 37 value = name
38 38 revision = r'\s+\[(\w+)\]\s*'
39 39 lines = r'(?:.|\n)+'
40 40
41 41 self.dir_re = re.compile(space + "dir" + name)
42 42 self.file_re = re.compile(space + "file" + name +
43 43 "content" + revision)
44 44 self.add_file_re = re.compile(space + "add_file" + name +
45 45 "content" + revision)
46 46 self.patch_re = re.compile(space + "patch" + name +
47 47 "from" + revision + "to" + revision)
48 48 self.rename_re = re.compile(space + "rename" + name + "to" + name)
49 49 self.delete_re = re.compile(space + "delete" + name)
50 50 self.tag_re = re.compile(space + "tag" + name + "revision" +
51 51 revision)
52 52 self.cert_re = re.compile(lines + space + "name" + name +
53 53 "value" + value)
54 54
55 55 attr = space + "file" + lines + space + "attr" + space
56 56 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
57 57 space + '"true"')
58 58
59 59 # cached data
60 60 self.manifest_rev = None
61 61 self.manifest = None
62 62 self.files = None
63 63 self.dirs = None
64 64
65 65 checktool('mtn', abort=False)
66 66
67 67 # test if there are any revisions
68 68 self.rev = None
69 69 try:
70 70 self.getheads()
71 71 except:
72 72 raise norepo
73 73 self.rev = rev
74 74
75 75 def mtnrun(self, *args, **kwargs):
76 76 kwargs['d'] = self.path
77 77 return self.run0('automate', *args, **kwargs)
78 78
79 79 def mtnloadmanifest(self, rev):
80 80 if self.manifest_rev == rev:
81 81 return
82 82 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
83 83 self.manifest_rev = rev
84 84 self.files = {}
85 85 self.dirs = {}
86 86
87 87 for e in self.manifest:
88 88 m = self.file_re.match(e)
89 89 if m:
90 90 attr = ""
91 91 name = m.group(1)
92 92 node = m.group(2)
93 93 if self.attr_execute_re.match(e):
94 94 attr += "x"
95 95 self.files[name] = (node, attr)
96 96 m = self.dir_re.match(e)
97 97 if m:
98 98 self.dirs[m.group(1)] = True
99 99
100 100 def mtnisfile(self, name, rev):
101 101 # a non-file could be a directory or a deleted or renamed file
102 102 self.mtnloadmanifest(rev)
103 103 return name in self.files
104 104
105 105 def mtnisdir(self, name, rev):
106 106 self.mtnloadmanifest(rev)
107 107 return name in self.dirs
108 108
109 109 def mtngetcerts(self, rev):
110 110 certs = {"author":"<missing>", "date":"<missing>",
111 111 "changelog":"<missing>", "branch":"<missing>"}
112 112 certlist = self.mtnrun("certs", rev)
113 113 # mtn < 0.45:
114 114 # key "test@selenic.com"
115 115 # mtn >= 0.45:
116 116 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
117 117 certlist = re.split('\n\n key ["\[]', certlist)
118 118 for e in certlist:
119 119 m = self.cert_re.match(e)
120 120 if m:
121 121 name, value = m.groups()
122 122 value = value.replace(r'\"', '"')
123 123 value = value.replace(r'\\', '\\')
124 124 certs[name] = value
125 125 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
126 126 # and all times are stored in UTC
127 127 certs["date"] = certs["date"].split('.')[0] + " UTC"
128 128 return certs
129 129
130 130 # implement the converter_source interface:
131 131
132 132 def getheads(self):
133 133 if not self.rev:
134 134 return self.mtnrun("leaves").splitlines()
135 135 else:
136 136 return [self.rev]
137 137
138 138 def getchanges(self, rev):
139 139 #revision = self.mtncmd("get_revision %s" % rev).split("\n\n")
140 140 revision = self.mtnrun("get_revision", rev).split("\n\n")
141 141 files = {}
142 142 ignoremove = {}
143 143 renameddirs = []
144 144 copies = {}
145 145 for e in revision:
146 146 m = self.add_file_re.match(e)
147 147 if m:
148 148 files[m.group(1)] = rev
149 149 ignoremove[m.group(1)] = rev
150 150 m = self.patch_re.match(e)
151 151 if m:
152 152 files[m.group(1)] = rev
153 153 # Delete/rename is handled later when the convert engine
154 154 # discovers an IOError exception from getfile,
155 155 # but only if we add the "from" file to the list of changes.
156 156 m = self.delete_re.match(e)
157 157 if m:
158 158 files[m.group(1)] = rev
159 159 m = self.rename_re.match(e)
160 160 if m:
161 161 toname = m.group(2)
162 162 fromname = m.group(1)
163 163 if self.mtnisfile(toname, rev):
164 164 ignoremove[toname] = 1
165 165 copies[toname] = fromname
166 166 files[toname] = rev
167 167 files[fromname] = rev
168 168 elif self.mtnisdir(toname, rev):
169 169 renameddirs.append((fromname, toname))
170 170
171 171 # Directory renames can be handled only once we have recorded
172 172 # all new files
173 173 for fromdir, todir in renameddirs:
174 174 renamed = {}
175 175 for tofile in self.files:
176 176 if tofile in ignoremove:
177 177 continue
178 178 if tofile.startswith(todir + '/'):
179 179 renamed[tofile] = fromdir + tofile[len(todir):]
180 180 # Avoid chained moves like:
181 181 # d1(/a) => d3/d1(/a)
182 182 # d2 => d3
183 183 ignoremove[tofile] = 1
184 184 for tofile, fromfile in renamed.items():
185 185 self.ui.debug (_("copying file in renamed directory "
186 186 "from '%s' to '%s'")
187 187 % (fromfile, tofile), '\n')
188 188 files[tofile] = rev
189 189 copies[tofile] = fromfile
190 190 for fromfile in renamed.values():
191 191 files[fromfile] = rev
192 192
193 193 return (files.items(), copies)
194 194
195 def getmode(self, name, rev):
196 self.mtnloadmanifest(rev)
197 node, attr = self.files.get(name, (None, ""))
198 return attr
199
200 195 def getfile(self, name, rev):
201 196 if not self.mtnisfile(name, rev):
202 197 raise IOError() # file was deleted or renamed
203 198 try:
204 return self.mtnrun("get_file_of", name, r=rev)
199 data = self.mtnrun("get_file_of", name, r=rev)
205 200 except:
206 201 raise IOError() # file was deleted or renamed
202 self.mtnloadmanifest(rev)
203 node, attr = self.files.get(name, (None, ""))
204 return data, attr
207 205
208 206 def getcommit(self, rev):
209 207 certs = self.mtngetcerts(rev)
210 208 return commit(
211 209 author=certs["author"],
212 210 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
213 211 desc=certs["changelog"],
214 212 rev=rev,
215 213 parents=self.mtnrun("parents", rev).splitlines(),
216 214 branch=certs["branch"])
217 215
218 216 def gettags(self):
219 217 tags = {}
220 218 for e in self.mtnrun("tags").split("\n\n"):
221 219 m = self.tag_re.match(e)
222 220 if m:
223 221 tags[m.group(1)] = m.group(2)
224 222 return tags
225 223
226 224 def getchangedfiles(self, rev, i):
227 225 # This function is only needed to support --filemap
228 226 # ... and we don't support that
229 227 raise NotImplementedError()
@@ -1,208 +1,202 b''
1 1 # Perforce source for convert extension.
2 2 #
3 3 # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from mercurial import util
9 9 from mercurial.i18n import _
10 10
11 11 from common import commit, converter_source, checktool, NoRepo
12 12 import marshal
13 13 import re
14 14
15 15 def loaditer(f):
16 16 "Yield the dictionary objects generated by p4"
17 17 try:
18 18 while True:
19 19 d = marshal.load(f)
20 20 if not d:
21 21 break
22 22 yield d
23 23 except EOFError:
24 24 pass
25 25
26 26 class p4_source(converter_source):
27 27 def __init__(self, ui, path, rev=None):
28 28 super(p4_source, self).__init__(ui, path, rev=rev)
29 29
30 30 if "/" in path and not path.startswith('//'):
31 31 raise NoRepo(_('%s does not look like a P4 repository') % path)
32 32
33 33 checktool('p4', abort=False)
34 34
35 35 self.p4changes = {}
36 36 self.heads = {}
37 37 self.changeset = {}
38 38 self.files = {}
39 39 self.tags = {}
40 40 self.lastbranch = {}
41 41 self.parent = {}
42 42 self.encoding = "latin_1"
43 43 self.depotname = {} # mapping from local name to depot name
44 self.modecache = {}
45 44 self.re_type = re.compile(
46 45 "([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)"
47 46 "(\+\w+)?$")
48 47 self.re_keywords = re.compile(
49 48 r"\$(Id|Header|Date|DateTime|Change|File|Revision|Author)"
50 49 r":[^$\n]*\$")
51 50 self.re_keywords_old = re.compile("\$(Id|Header):[^$\n]*\$")
52 51
53 52 self._parse(ui, path)
54 53
55 54 def _parse_view(self, path):
56 55 "Read changes affecting the path"
57 56 cmd = 'p4 -G changes -s submitted "%s"' % path
58 57 stdout = util.popen(cmd, mode='rb')
59 58 for d in loaditer(stdout):
60 59 c = d.get("change", None)
61 60 if c:
62 61 self.p4changes[c] = True
63 62
64 63 def _parse(self, ui, path):
65 64 "Prepare list of P4 filenames and revisions to import"
66 65 ui.status(_('reading p4 views\n'))
67 66
68 67 # read client spec or view
69 68 if "/" in path:
70 69 self._parse_view(path)
71 70 if path.startswith("//") and path.endswith("/..."):
72 71 views = {path[:-3]:""}
73 72 else:
74 73 views = {"//": ""}
75 74 else:
76 75 cmd = 'p4 -G client -o "%s"' % path
77 76 clientspec = marshal.load(util.popen(cmd, mode='rb'))
78 77
79 78 views = {}
80 79 for client in clientspec:
81 80 if client.startswith("View"):
82 81 sview, cview = clientspec[client].split()
83 82 self._parse_view(sview)
84 83 if sview.endswith("...") and cview.endswith("..."):
85 84 sview = sview[:-3]
86 85 cview = cview[:-3]
87 86 cview = cview[2:]
88 87 cview = cview[cview.find("/") + 1:]
89 88 views[sview] = cview
90 89
91 90 # list of changes that affect our source files
92 91 self.p4changes = self.p4changes.keys()
93 92 self.p4changes.sort(key=int)
94 93
95 94 # list with depot pathnames, longest first
96 95 vieworder = views.keys()
97 96 vieworder.sort(key=len, reverse=True)
98 97
99 98 # handle revision limiting
100 99 startrev = self.ui.config('convert', 'p4.startrev', default=0)
101 100 self.p4changes = [x for x in self.p4changes
102 101 if ((not startrev or int(x) >= int(startrev)) and
103 102 (not self.rev or int(x) <= int(self.rev)))]
104 103
105 104 # now read the full changelists to get the list of file revisions
106 105 ui.status(_('collecting p4 changelists\n'))
107 106 lastid = None
108 107 for change in self.p4changes:
109 108 cmd = "p4 -G describe %s" % change
110 109 stdout = util.popen(cmd, mode='rb')
111 110 d = marshal.load(stdout)
112 111
113 112 desc = self.recode(d["desc"])
114 113 shortdesc = desc.split("\n", 1)[0]
115 114 t = '%s %s' % (d["change"], repr(shortdesc)[1:-1])
116 115 ui.status(util.ellipsis(t, 80) + '\n')
117 116
118 117 if lastid:
119 118 parents = [lastid]
120 119 else:
121 120 parents = []
122 121
123 122 date = (int(d["time"]), 0) # timezone not set
124 123 c = commit(author=self.recode(d["user"]), date=util.datestr(date),
125 124 parents=parents, desc=desc, branch='',
126 125 extra={"p4": change})
127 126
128 127 files = []
129 128 i = 0
130 129 while ("depotFile%d" % i) in d and ("rev%d" % i) in d:
131 130 oldname = d["depotFile%d" % i]
132 131 filename = None
133 132 for v in vieworder:
134 133 if oldname.startswith(v):
135 134 filename = views[v] + oldname[len(v):]
136 135 break
137 136 if filename:
138 137 files.append((filename, d["rev%d" % i]))
139 138 self.depotname[filename] = oldname
140 139 i += 1
141 140 self.changeset[change] = c
142 141 self.files[change] = files
143 142 lastid = change
144 143
145 144 if lastid:
146 145 self.heads = [lastid]
147 146
148 147 def getheads(self):
149 148 return self.heads
150 149
151 150 def getfile(self, name, rev):
152 151 cmd = 'p4 -G print "%s#%s"' % (self.depotname[name], rev)
153 152 stdout = util.popen(cmd, mode='rb')
154 153
155 154 mode = None
156 155 contents = ""
157 156 keywords = None
158 157
159 158 for d in loaditer(stdout):
160 159 code = d["code"]
161 160 data = d.get("data")
162 161
163 162 if code == "error":
164 163 raise IOError(d["generic"], data)
165 164
166 165 elif code == "stat":
167 166 p4type = self.re_type.match(d["type"])
168 167 if p4type:
169 168 mode = ""
170 169 flags = (p4type.group(1) or "") + (p4type.group(3) or "")
171 170 if "x" in flags:
172 171 mode = "x"
173 172 if p4type.group(2) == "symlink":
174 173 mode = "l"
175 174 if "ko" in flags:
176 175 keywords = self.re_keywords_old
177 176 elif "k" in flags:
178 177 keywords = self.re_keywords
179 178
180 179 elif code == "text" or code == "binary":
181 180 contents += data
182 181
183 182 if mode is None:
184 183 raise IOError(0, "bad stat")
185 184
186 self.modecache[(name, rev)] = mode
187
188 185 if keywords:
189 186 contents = keywords.sub("$\\1$", contents)
190 187 if mode == "l" and contents.endswith("\n"):
191 188 contents = contents[:-1]
192 189
193 return contents
194
195 def getmode(self, name, rev):
196 return self.modecache[(name, rev)]
190 return contents, mode
197 191
198 192 def getchanges(self, rev):
199 193 return self.files[rev], {}
200 194
201 195 def getcommit(self, rev):
202 196 return self.changeset[rev]
203 197
204 198 def gettags(self):
205 199 return self.tags
206 200
207 201 def getchangedfiles(self, rev, i):
208 202 return sorted([x[0] for x in self.files[rev]])
@@ -1,1167 +1,1157 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 import os
6 6 import re
7 7 import sys
8 8 import cPickle as pickle
9 9 import tempfile
10 10 import urllib
11 11 import urllib2
12 12
13 13 from mercurial import strutil, util, encoding
14 14 from mercurial.i18n import _
15 15
16 16 # Subversion stuff. Works best with very recent Python SVN bindings
17 17 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
18 18 # these bindings.
19 19
20 20 from cStringIO import StringIO
21 21
22 22 from common import NoRepo, MissingTool, commit, encodeargs, decodeargs
23 23 from common import commandline, converter_source, converter_sink, mapfile
24 24
25 25 try:
26 26 from svn.core import SubversionException, Pool
27 27 import svn
28 28 import svn.client
29 29 import svn.core
30 30 import svn.ra
31 31 import svn.delta
32 32 import transport
33 33 import warnings
34 34 warnings.filterwarnings('ignore',
35 35 module='svn.core',
36 36 category=DeprecationWarning)
37 37
38 38 except ImportError:
39 39 pass
40 40
41 41 class SvnPathNotFound(Exception):
42 42 pass
43 43
44 44 def geturl(path):
45 45 try:
46 46 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
47 47 except SubversionException:
48 48 pass
49 49 if os.path.isdir(path):
50 50 path = os.path.normpath(os.path.abspath(path))
51 51 if os.name == 'nt':
52 52 path = '/' + util.normpath(path)
53 53 # Module URL is later compared with the repository URL returned
54 54 # by svn API, which is UTF-8.
55 55 path = encoding.tolocal(path)
56 56 return 'file://%s' % urllib.quote(path)
57 57 return path
58 58
59 59 def optrev(number):
60 60 optrev = svn.core.svn_opt_revision_t()
61 61 optrev.kind = svn.core.svn_opt_revision_number
62 62 optrev.value.number = number
63 63 return optrev
64 64
65 65 class changedpath(object):
66 66 def __init__(self, p):
67 67 self.copyfrom_path = p.copyfrom_path
68 68 self.copyfrom_rev = p.copyfrom_rev
69 69 self.action = p.action
70 70
71 71 def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,
72 72 strict_node_history=False):
73 73 protocol = -1
74 74 def receiver(orig_paths, revnum, author, date, message, pool):
75 75 if orig_paths is not None:
76 76 for k, v in orig_paths.iteritems():
77 77 orig_paths[k] = changedpath(v)
78 78 pickle.dump((orig_paths, revnum, author, date, message),
79 79 fp, protocol)
80 80
81 81 try:
82 82 # Use an ra of our own so that our parent can consume
83 83 # our results without confusing the server.
84 84 t = transport.SvnRaTransport(url=url)
85 85 svn.ra.get_log(t.ra, paths, start, end, limit,
86 86 discover_changed_paths,
87 87 strict_node_history,
88 88 receiver)
89 89 except SubversionException, (inst, num):
90 90 pickle.dump(num, fp, protocol)
91 91 except IOError:
92 92 # Caller may interrupt the iteration
93 93 pickle.dump(None, fp, protocol)
94 94 else:
95 95 pickle.dump(None, fp, protocol)
96 96 fp.close()
97 97 # With large history, cleanup process goes crazy and suddenly
98 98 # consumes *huge* amount of memory. The output file being closed,
99 99 # there is no need for clean termination.
100 100 os._exit(0)
101 101
102 102 def debugsvnlog(ui, **opts):
103 103 """Fetch SVN log in a subprocess and channel them back to parent to
104 104 avoid memory collection issues.
105 105 """
106 106 util.set_binary(sys.stdin)
107 107 util.set_binary(sys.stdout)
108 108 args = decodeargs(sys.stdin.read())
109 109 get_log_child(sys.stdout, *args)
110 110
111 111 class logstream(object):
112 112 """Interruptible revision log iterator."""
113 113 def __init__(self, stdout):
114 114 self._stdout = stdout
115 115
116 116 def __iter__(self):
117 117 while True:
118 118 try:
119 119 entry = pickle.load(self._stdout)
120 120 except EOFError:
121 121 raise util.Abort(_('Mercurial failed to run itself, check'
122 122 ' hg executable is in PATH'))
123 123 try:
124 124 orig_paths, revnum, author, date, message = entry
125 125 except:
126 126 if entry is None:
127 127 break
128 128 raise SubversionException("child raised exception", entry)
129 129 yield entry
130 130
131 131 def close(self):
132 132 if self._stdout:
133 133 self._stdout.close()
134 134 self._stdout = None
135 135
136 136
137 137 # Check to see if the given path is a local Subversion repo. Verify this by
138 138 # looking for several svn-specific files and directories in the given
139 139 # directory.
140 140 def filecheck(ui, path, proto):
141 141 for x in ('locks', 'hooks', 'format', 'db'):
142 142 if not os.path.exists(os.path.join(path, x)):
143 143 return False
144 144 return True
145 145
146 146 # Check to see if a given path is the root of an svn repo over http. We verify
147 147 # this by requesting a version-controlled URL we know can't exist and looking
148 148 # for the svn-specific "not found" XML.
149 149 def httpcheck(ui, path, proto):
150 150 try:
151 151 opener = urllib2.build_opener()
152 152 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
153 153 data = rsp.read()
154 154 except urllib2.HTTPError, inst:
155 155 if inst.code != 404:
156 156 # Except for 404 we cannot know for sure this is not an svn repo
157 157 ui.warn(_('svn: cannot probe remote repository, assume it could '
158 158 'be a subversion repository. Use --source-type if you '
159 159 'know better.\n'))
160 160 return True
161 161 data = inst.fp.read()
162 162 except:
163 163 # Could be urllib2.URLError if the URL is invalid or anything else.
164 164 return False
165 165 return '<m:human-readable errcode="160013">' in data
166 166
167 167 protomap = {'http': httpcheck,
168 168 'https': httpcheck,
169 169 'file': filecheck,
170 170 }
171 171 def issvnurl(ui, url):
172 172 try:
173 173 proto, path = url.split('://', 1)
174 174 if proto == 'file':
175 175 path = urllib.url2pathname(path)
176 176 except ValueError:
177 177 proto = 'file'
178 178 path = os.path.abspath(url)
179 179 if proto == 'file':
180 180 path = path.replace(os.sep, '/')
181 181 check = protomap.get(proto, lambda *args: False)
182 182 while '/' in path:
183 183 if check(ui, path, proto):
184 184 return True
185 185 path = path.rsplit('/', 1)[0]
186 186 return False
187 187
188 188 # SVN conversion code stolen from bzr-svn and tailor
189 189 #
190 190 # Subversion looks like a versioned filesystem, branches structures
191 191 # are defined by conventions and not enforced by the tool. First,
192 192 # we define the potential branches (modules) as "trunk" and "branches"
193 193 # children directories. Revisions are then identified by their
194 194 # module and revision number (and a repository identifier).
195 195 #
196 196 # The revision graph is really a tree (or a forest). By default, a
197 197 # revision parent is the previous revision in the same module. If the
198 198 # module directory is copied/moved from another module then the
199 199 # revision is the module root and its parent the source revision in
200 200 # the parent module. A revision has at most one parent.
201 201 #
202 202 class svn_source(converter_source):
203 203 def __init__(self, ui, url, rev=None):
204 204 super(svn_source, self).__init__(ui, url, rev=rev)
205 205
206 206 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
207 207 (os.path.exists(url) and
208 208 os.path.exists(os.path.join(url, '.svn'))) or
209 209 issvnurl(ui, url)):
210 210 raise NoRepo(_("%s does not look like a Subversion repository")
211 211 % url)
212 212
213 213 try:
214 214 SubversionException
215 215 except NameError:
216 216 raise MissingTool(_('Subversion python bindings could not be loaded'))
217 217
218 218 try:
219 219 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
220 220 if version < (1, 4):
221 221 raise MissingTool(_('Subversion python bindings %d.%d found, '
222 222 '1.4 or later required') % version)
223 223 except AttributeError:
224 224 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
225 225 'or later required'))
226 226
227 227 self.lastrevs = {}
228 228
229 229 latest = None
230 230 try:
231 231 # Support file://path@rev syntax. Useful e.g. to convert
232 232 # deleted branches.
233 233 at = url.rfind('@')
234 234 if at >= 0:
235 235 latest = int(url[at + 1:])
236 236 url = url[:at]
237 237 except ValueError:
238 238 pass
239 239 self.url = geturl(url)
240 240 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
241 241 try:
242 242 self.transport = transport.SvnRaTransport(url=self.url)
243 243 self.ra = self.transport.ra
244 244 self.ctx = self.transport.client
245 245 self.baseurl = svn.ra.get_repos_root(self.ra)
246 246 # Module is either empty or a repository path starting with
247 247 # a slash and not ending with a slash.
248 248 self.module = urllib.unquote(self.url[len(self.baseurl):])
249 249 self.prevmodule = None
250 250 self.rootmodule = self.module
251 251 self.commits = {}
252 252 self.paths = {}
253 253 self.uuid = svn.ra.get_uuid(self.ra)
254 254 except SubversionException:
255 255 ui.traceback()
256 256 raise NoRepo(_("%s does not look like a Subversion repository")
257 257 % self.url)
258 258
259 259 if rev:
260 260 try:
261 261 latest = int(rev)
262 262 except ValueError:
263 263 raise util.Abort(_('svn: revision %s is not an integer') % rev)
264 264
265 265 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
266 266 try:
267 267 self.startrev = int(self.startrev)
268 268 if self.startrev < 0:
269 269 self.startrev = 0
270 270 except ValueError:
271 271 raise util.Abort(_('svn: start revision %s is not an integer')
272 272 % self.startrev)
273 273
274 274 self.head = self.latest(self.module, latest)
275 275 if not self.head:
276 276 raise util.Abort(_('no revision found in module %s')
277 277 % self.module)
278 278 self.last_changed = self.revnum(self.head)
279 279
280 280 self._changescache = None
281 281
282 282 if os.path.exists(os.path.join(url, '.svn/entries')):
283 283 self.wc = url
284 284 else:
285 285 self.wc = None
286 286 self.convertfp = None
287 287
288 288 def setrevmap(self, revmap):
289 289 lastrevs = {}
290 290 for revid in revmap.iterkeys():
291 291 uuid, module, revnum = self.revsplit(revid)
292 292 lastrevnum = lastrevs.setdefault(module, revnum)
293 293 if revnum > lastrevnum:
294 294 lastrevs[module] = revnum
295 295 self.lastrevs = lastrevs
296 296
297 297 def exists(self, path, optrev):
298 298 try:
299 299 svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),
300 300 optrev, False, self.ctx)
301 301 return True
302 302 except SubversionException:
303 303 return False
304 304
305 305 def getheads(self):
306 306
307 307 def isdir(path, revnum):
308 308 kind = self._checkpath(path, revnum)
309 309 return kind == svn.core.svn_node_dir
310 310
311 311 def getcfgpath(name, rev):
312 312 cfgpath = self.ui.config('convert', 'svn.' + name)
313 313 if cfgpath is not None and cfgpath.strip() == '':
314 314 return None
315 315 path = (cfgpath or name).strip('/')
316 316 if not self.exists(path, rev):
317 317 if cfgpath:
318 318 raise util.Abort(_('expected %s to be at %r, but not found')
319 319 % (name, path))
320 320 return None
321 321 self.ui.note(_('found %s at %r\n') % (name, path))
322 322 return path
323 323
324 324 rev = optrev(self.last_changed)
325 325 oldmodule = ''
326 326 trunk = getcfgpath('trunk', rev)
327 327 self.tags = getcfgpath('tags', rev)
328 328 branches = getcfgpath('branches', rev)
329 329
330 330 # If the project has a trunk or branches, we will extract heads
331 331 # from them. We keep the project root otherwise.
332 332 if trunk:
333 333 oldmodule = self.module or ''
334 334 self.module += '/' + trunk
335 335 self.head = self.latest(self.module, self.last_changed)
336 336 if not self.head:
337 337 raise util.Abort(_('no revision found in module %s')
338 338 % self.module)
339 339
340 340 # First head in the list is the module's head
341 341 self.heads = [self.head]
342 342 if self.tags is not None:
343 343 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
344 344
345 345 # Check if branches bring a few more heads to the list
346 346 if branches:
347 347 rpath = self.url.strip('/')
348 348 branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),
349 349 rev, False, self.ctx)
350 350 for branch in branchnames.keys():
351 351 module = '%s/%s/%s' % (oldmodule, branches, branch)
352 352 if not isdir(module, self.last_changed):
353 353 continue
354 354 brevid = self.latest(module, self.last_changed)
355 355 if not brevid:
356 356 self.ui.note(_('ignoring empty branch %s\n') % branch)
357 357 continue
358 358 self.ui.note(_('found branch %s at %d\n') %
359 359 (branch, self.revnum(brevid)))
360 360 self.heads.append(brevid)
361 361
362 362 if self.startrev and self.heads:
363 363 if len(self.heads) > 1:
364 364 raise util.Abort(_('svn: start revision is not supported '
365 365 'with more than one branch'))
366 366 revnum = self.revnum(self.heads[0])
367 367 if revnum < self.startrev:
368 368 raise util.Abort(
369 369 _('svn: no revision found after start revision %d')
370 370 % self.startrev)
371 371
372 372 return self.heads
373 373
374 def getfile(self, file, rev):
375 data, mode = self._getfile(file, rev)
376 self.modecache[(file, rev)] = mode
377 return data
378
379 def getmode(self, file, rev):
380 return self.modecache[(file, rev)]
381
382 374 def getchanges(self, rev):
383 375 if self._changescache and self._changescache[0] == rev:
384 376 return self._changescache[1]
385 377 self._changescache = None
386 self.modecache = {}
387 378 (paths, parents) = self.paths[rev]
388 379 if parents:
389 380 files, self.removed, copies = self.expandpaths(rev, paths, parents)
390 381 else:
391 382 # Perform a full checkout on roots
392 383 uuid, module, revnum = self.revsplit(rev)
393 384 entries = svn.client.ls(self.baseurl + urllib.quote(module),
394 385 optrev(revnum), True, self.ctx)
395 386 files = [n for n, e in entries.iteritems()
396 387 if e.kind == svn.core.svn_node_file]
397 388 copies = {}
398 389 self.removed = set()
399 390
400 391 files.sort()
401 392 files = zip(files, [rev] * len(files))
402 393
403 394 # caller caches the result, so free it here to release memory
404 395 del self.paths[rev]
405 396 return (files, copies)
406 397
407 398 def getchangedfiles(self, rev, i):
408 399 changes = self.getchanges(rev)
409 400 self._changescache = (rev, changes)
410 401 return [f[0] for f in changes[0]]
411 402
412 403 def getcommit(self, rev):
413 404 if rev not in self.commits:
414 405 uuid, module, revnum = self.revsplit(rev)
415 406 self.module = module
416 407 self.reparent(module)
417 408 # We assume that:
418 409 # - requests for revisions after "stop" come from the
419 410 # revision graph backward traversal. Cache all of them
420 411 # down to stop, they will be used eventually.
421 412 # - requests for revisions before "stop" come to get
422 413 # isolated branches parents. Just fetch what is needed.
423 414 stop = self.lastrevs.get(module, 0)
424 415 if revnum < stop:
425 416 stop = revnum + 1
426 417 self._fetch_revisions(revnum, stop)
427 418 commit = self.commits[rev]
428 419 # caller caches the result, so free it here to release memory
429 420 del self.commits[rev]
430 421 return commit
431 422
432 423 def gettags(self):
433 424 tags = {}
434 425 if self.tags is None:
435 426 return tags
436 427
437 428 # svn tags are just a convention, project branches left in a
438 429 # 'tags' directory. There is no other relationship than
439 430 # ancestry, which is expensive to discover and makes them hard
440 431 # to update incrementally. Worse, past revisions may be
441 432 # referenced by tags far away in the future, requiring a deep
442 433 # history traversal on every calculation. Current code
443 434 # performs a single backward traversal, tracking moves within
444 435 # the tags directory (tag renaming) and recording a new tag
445 436 # everytime a project is copied from outside the tags
446 437 # directory. It also lists deleted tags, this behaviour may
447 438 # change in the future.
448 439 pendings = []
449 440 tagspath = self.tags
450 441 start = svn.ra.get_latest_revnum(self.ra)
451 442 try:
452 443 for entry in self._getlog([self.tags], start, self.startrev):
453 444 origpaths, revnum, author, date, message = entry
454 445 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
455 446 in origpaths.iteritems() if e.copyfrom_path]
456 447 # Apply moves/copies from more specific to general
457 448 copies.sort(reverse=True)
458 449
459 450 srctagspath = tagspath
460 451 if copies and copies[-1][2] == tagspath:
461 452 # Track tags directory moves
462 453 srctagspath = copies.pop()[0]
463 454
464 455 for source, sourcerev, dest in copies:
465 456 if not dest.startswith(tagspath + '/'):
466 457 continue
467 458 for tag in pendings:
468 459 if tag[0].startswith(dest):
469 460 tagpath = source + tag[0][len(dest):]
470 461 tag[:2] = [tagpath, sourcerev]
471 462 break
472 463 else:
473 464 pendings.append([source, sourcerev, dest])
474 465
475 466 # Filter out tags with children coming from different
476 467 # parts of the repository like:
477 468 # /tags/tag.1 (from /trunk:10)
478 469 # /tags/tag.1/foo (from /branches/foo:12)
479 470 # Here/tags/tag.1 discarded as well as its children.
480 471 # It happens with tools like cvs2svn. Such tags cannot
481 472 # be represented in mercurial.
482 473 addeds = dict((p, e.copyfrom_path) for p, e
483 474 in origpaths.iteritems()
484 475 if e.action == 'A' and e.copyfrom_path)
485 476 badroots = set()
486 477 for destroot in addeds:
487 478 for source, sourcerev, dest in pendings:
488 479 if (not dest.startswith(destroot + '/')
489 480 or source.startswith(addeds[destroot] + '/')):
490 481 continue
491 482 badroots.add(destroot)
492 483 break
493 484
494 485 for badroot in badroots:
495 486 pendings = [p for p in pendings if p[2] != badroot
496 487 and not p[2].startswith(badroot + '/')]
497 488
498 489 # Tell tag renamings from tag creations
499 490 remainings = []
500 491 for source, sourcerev, dest in pendings:
501 492 tagname = dest.split('/')[-1]
502 493 if source.startswith(srctagspath):
503 494 remainings.append([source, sourcerev, tagname])
504 495 continue
505 496 if tagname in tags:
506 497 # Keep the latest tag value
507 498 continue
508 499 # From revision may be fake, get one with changes
509 500 try:
510 501 tagid = self.latest(source, sourcerev)
511 502 if tagid and tagname not in tags:
512 503 tags[tagname] = tagid
513 504 except SvnPathNotFound:
514 505 # It happens when we are following directories
515 506 # we assumed were copied with their parents
516 507 # but were really created in the tag
517 508 # directory.
518 509 pass
519 510 pendings = remainings
520 511 tagspath = srctagspath
521 512
522 513 except SubversionException:
523 514 self.ui.note(_('no tags found at revision %d\n') % start)
524 515 return tags
525 516
526 517 def converted(self, rev, destrev):
527 518 if not self.wc:
528 519 return
529 520 if self.convertfp is None:
530 521 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
531 522 'a')
532 523 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
533 524 self.convertfp.flush()
534 525
535 526 def revid(self, revnum, module=None):
536 527 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
537 528
538 529 def revnum(self, rev):
539 530 return int(rev.split('@')[-1])
540 531
541 532 def revsplit(self, rev):
542 533 url, revnum = rev.rsplit('@', 1)
543 534 revnum = int(revnum)
544 535 parts = url.split('/', 1)
545 536 uuid = parts.pop(0)[4:]
546 537 mod = ''
547 538 if parts:
548 539 mod = '/' + parts[0]
549 540 return uuid, mod, revnum
550 541
551 542 def latest(self, path, stop=0):
552 543 """Find the latest revid affecting path, up to stop. It may return
553 544 a revision in a different module, since a branch may be moved without
554 545 a change being reported. Return None if computed module does not
555 546 belong to rootmodule subtree.
556 547 """
557 548 if not path.startswith(self.rootmodule):
558 549 # Requests on foreign branches may be forbidden at server level
559 550 self.ui.debug('ignoring foreign branch %r\n' % path)
560 551 return None
561 552
562 553 if not stop:
563 554 stop = svn.ra.get_latest_revnum(self.ra)
564 555 try:
565 556 prevmodule = self.reparent('')
566 557 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
567 558 self.reparent(prevmodule)
568 559 except SubversionException:
569 560 dirent = None
570 561 if not dirent:
571 562 raise SvnPathNotFound(_('%s not found up to revision %d')
572 563 % (path, stop))
573 564
574 565 # stat() gives us the previous revision on this line of
575 566 # development, but it might be in *another module*. Fetch the
576 567 # log and detect renames down to the latest revision.
577 568 stream = self._getlog([path], stop, dirent.created_rev)
578 569 try:
579 570 for entry in stream:
580 571 paths, revnum, author, date, message = entry
581 572 if revnum <= dirent.created_rev:
582 573 break
583 574
584 575 for p in paths:
585 576 if not path.startswith(p) or not paths[p].copyfrom_path:
586 577 continue
587 578 newpath = paths[p].copyfrom_path + path[len(p):]
588 579 self.ui.debug("branch renamed from %s to %s at %d\n" %
589 580 (path, newpath, revnum))
590 581 path = newpath
591 582 break
592 583 finally:
593 584 stream.close()
594 585
595 586 if not path.startswith(self.rootmodule):
596 587 self.ui.debug('ignoring foreign branch %r\n' % path)
597 588 return None
598 589 return self.revid(dirent.created_rev, path)
599 590
600 591 def reparent(self, module):
601 592 """Reparent the svn transport and return the previous parent."""
602 593 if self.prevmodule == module:
603 594 return module
604 595 svnurl = self.baseurl + urllib.quote(module)
605 596 prevmodule = self.prevmodule
606 597 if prevmodule is None:
607 598 prevmodule = ''
608 599 self.ui.debug("reparent to %s\n" % svnurl)
609 600 svn.ra.reparent(self.ra, svnurl)
610 601 self.prevmodule = module
611 602 return prevmodule
612 603
613 604 def expandpaths(self, rev, paths, parents):
614 605 changed, removed = set(), set()
615 606 copies = {}
616 607
617 608 new_module, revnum = self.revsplit(rev)[1:]
618 609 if new_module != self.module:
619 610 self.module = new_module
620 611 self.reparent(self.module)
621 612
622 613 for path, ent in paths:
623 614 entrypath = self.getrelpath(path)
624 615
625 616 kind = self._checkpath(entrypath, revnum)
626 617 if kind == svn.core.svn_node_file:
627 618 changed.add(self.recode(entrypath))
628 619 if not ent.copyfrom_path or not parents:
629 620 continue
630 621 # Copy sources not in parent revisions cannot be
631 622 # represented, ignore their origin for now
632 623 pmodule, prevnum = self.revsplit(parents[0])[1:]
633 624 if ent.copyfrom_rev < prevnum:
634 625 continue
635 626 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
636 627 if not copyfrom_path:
637 628 continue
638 629 self.ui.debug("copied to %s from %s@%s\n" %
639 630 (entrypath, copyfrom_path, ent.copyfrom_rev))
640 631 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
641 632 elif kind == 0: # gone, but had better be a deleted *file*
642 633 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
643 634 pmodule, prevnum = self.revsplit(parents[0])[1:]
644 635 parentpath = pmodule + "/" + entrypath
645 636 fromkind = self._checkpath(entrypath, prevnum, pmodule)
646 637
647 638 if fromkind == svn.core.svn_node_file:
648 639 removed.add(self.recode(entrypath))
649 640 elif fromkind == svn.core.svn_node_dir:
650 641 oroot = parentpath.strip('/')
651 642 nroot = path.strip('/')
652 643 children = self._iterfiles(oroot, prevnum)
653 644 for childpath in children:
654 645 childpath = childpath.replace(oroot, nroot)
655 646 childpath = self.getrelpath("/" + childpath, pmodule)
656 647 if childpath:
657 648 removed.add(self.recode(childpath))
658 649 else:
659 650 self.ui.debug('unknown path in revision %d: %s\n' % \
660 651 (revnum, path))
661 652 elif kind == svn.core.svn_node_dir:
662 653 if ent.action == 'M':
663 654 # If the directory just had a prop change,
664 655 # then we shouldn't need to look for its children.
665 656 continue
666 657 elif ent.action == 'R' and parents:
667 658 # If a directory is replacing a file, mark the previous
668 659 # file as deleted
669 660 pmodule, prevnum = self.revsplit(parents[0])[1:]
670 661 pkind = self._checkpath(entrypath, prevnum, pmodule)
671 662 if pkind == svn.core.svn_node_file:
672 663 removed.add(self.recode(entrypath))
673 664
674 665 for childpath in self._iterfiles(path, revnum):
675 666 childpath = self.getrelpath("/" + childpath)
676 667 if childpath:
677 668 changed.add(self.recode(childpath))
678 669
679 670 # Handle directory copies
680 671 if not ent.copyfrom_path or not parents:
681 672 continue
682 673 # Copy sources not in parent revisions cannot be
683 674 # represented, ignore their origin for now
684 675 pmodule, prevnum = self.revsplit(parents[0])[1:]
685 676 if ent.copyfrom_rev < prevnum:
686 677 continue
687 678 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
688 679 if not copyfrompath:
689 680 continue
690 681 self.ui.debug("mark %s came from %s:%d\n"
691 682 % (path, copyfrompath, ent.copyfrom_rev))
692 683 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
693 684 for childpath in children:
694 685 childpath = self.getrelpath("/" + childpath, pmodule)
695 686 if not childpath:
696 687 continue
697 688 copytopath = path + childpath[len(copyfrompath):]
698 689 copytopath = self.getrelpath(copytopath)
699 690 copies[self.recode(copytopath)] = self.recode(childpath)
700 691
701 692 changed.update(removed)
702 693 return (list(changed), removed, copies)
703 694
704 695 def _fetch_revisions(self, from_revnum, to_revnum):
705 696 if from_revnum < to_revnum:
706 697 from_revnum, to_revnum = to_revnum, from_revnum
707 698
708 699 self.child_cset = None
709 700
710 701 def parselogentry(orig_paths, revnum, author, date, message):
711 702 """Return the parsed commit object or None, and True if
712 703 the revision is a branch root.
713 704 """
714 705 self.ui.debug("parsing revision %d (%d changes)\n" %
715 706 (revnum, len(orig_paths)))
716 707
717 708 branched = False
718 709 rev = self.revid(revnum)
719 710 # branch log might return entries for a parent we already have
720 711
721 712 if rev in self.commits or revnum < to_revnum:
722 713 return None, branched
723 714
724 715 parents = []
725 716 # check whether this revision is the start of a branch or part
726 717 # of a branch renaming
727 718 orig_paths = sorted(orig_paths.iteritems())
728 719 root_paths = [(p, e) for p, e in orig_paths
729 720 if self.module.startswith(p)]
730 721 if root_paths:
731 722 path, ent = root_paths[-1]
732 723 if ent.copyfrom_path:
733 724 branched = True
734 725 newpath = ent.copyfrom_path + self.module[len(path):]
735 726 # ent.copyfrom_rev may not be the actual last revision
736 727 previd = self.latest(newpath, ent.copyfrom_rev)
737 728 if previd is not None:
738 729 prevmodule, prevnum = self.revsplit(previd)[1:]
739 730 if prevnum >= self.startrev:
740 731 parents = [previd]
741 732 self.ui.note(
742 733 _('found parent of branch %s at %d: %s\n') %
743 734 (self.module, prevnum, prevmodule))
744 735 else:
745 736 self.ui.debug("no copyfrom path, don't know what to do.\n")
746 737
747 738 paths = []
748 739 # filter out unrelated paths
749 740 for path, ent in orig_paths:
750 741 if self.getrelpath(path) is None:
751 742 continue
752 743 paths.append((path, ent))
753 744
754 745 # Example SVN datetime. Includes microseconds.
755 746 # ISO-8601 conformant
756 747 # '2007-01-04T17:35:00.902377Z'
757 748 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
758 749
759 750 log = message and self.recode(message) or ''
760 751 author = author and self.recode(author) or ''
761 752 try:
762 753 branch = self.module.split("/")[-1]
763 754 if branch == 'trunk':
764 755 branch = ''
765 756 except IndexError:
766 757 branch = None
767 758
768 759 cset = commit(author=author,
769 760 date=util.datestr(date),
770 761 desc=log,
771 762 parents=parents,
772 763 branch=branch,
773 764 rev=rev)
774 765
775 766 self.commits[rev] = cset
776 767 # The parents list is *shared* among self.paths and the
777 768 # commit object. Both will be updated below.
778 769 self.paths[rev] = (paths, cset.parents)
779 770 if self.child_cset and not self.child_cset.parents:
780 771 self.child_cset.parents[:] = [rev]
781 772 self.child_cset = cset
782 773 return cset, branched
783 774
784 775 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
785 776 (self.module, from_revnum, to_revnum))
786 777
787 778 try:
788 779 firstcset = None
789 780 lastonbranch = False
790 781 stream = self._getlog([self.module], from_revnum, to_revnum)
791 782 try:
792 783 for entry in stream:
793 784 paths, revnum, author, date, message = entry
794 785 if revnum < self.startrev:
795 786 lastonbranch = True
796 787 break
797 788 if not paths:
798 789 self.ui.debug('revision %d has no entries\n' % revnum)
799 790 # If we ever leave the loop on an empty
800 791 # revision, do not try to get a parent branch
801 792 lastonbranch = lastonbranch or revnum == 0
802 793 continue
803 794 cset, lastonbranch = parselogentry(paths, revnum, author,
804 795 date, message)
805 796 if cset:
806 797 firstcset = cset
807 798 if lastonbranch:
808 799 break
809 800 finally:
810 801 stream.close()
811 802
812 803 if not lastonbranch and firstcset and not firstcset.parents:
813 804 # The first revision of the sequence (the last fetched one)
814 805 # has invalid parents if not a branch root. Find the parent
815 806 # revision now, if any.
816 807 try:
817 808 firstrevnum = self.revnum(firstcset.rev)
818 809 if firstrevnum > 1:
819 810 latest = self.latest(self.module, firstrevnum - 1)
820 811 if latest:
821 812 firstcset.parents.append(latest)
822 813 except SvnPathNotFound:
823 814 pass
824 815 except SubversionException, (inst, num):
825 816 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
826 817 raise util.Abort(_('svn: branch has no revision %s') % to_revnum)
827 818 raise
828 819
829 def _getfile(self, file, rev):
820 def getfile(self, file, rev):
830 821 # TODO: ra.get_file transmits the whole file instead of diffs.
831 822 if file in self.removed:
832 raise IOError()
823 raise IOError()
833 824 mode = ''
834 825 try:
835 826 new_module, revnum = self.revsplit(rev)[1:]
836 827 if self.module != new_module:
837 828 self.module = new_module
838 829 self.reparent(self.module)
839 830 io = StringIO()
840 831 info = svn.ra.get_file(self.ra, file, revnum, io)
841 832 data = io.getvalue()
842 833 # ra.get_files() seems to keep a reference on the input buffer
843 834 # preventing collection. Release it explicitely.
844 835 io.close()
845 836 if isinstance(info, list):
846 837 info = info[-1]
847 838 mode = ("svn:executable" in info) and 'x' or ''
848 839 mode = ("svn:special" in info) and 'l' or mode
849 840 except SubversionException, e:
850 841 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
851 842 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
852 843 if e.apr_err in notfound: # File not found
853 844 raise IOError()
854 845 raise
855 846 if mode == 'l':
856 847 link_prefix = "link "
857 848 if data.startswith(link_prefix):
858 849 data = data[len(link_prefix):]
859 850 return data, mode
860 851
861 852 def _iterfiles(self, path, revnum):
862 853 """Enumerate all files in path at revnum, recursively."""
863 854 path = path.strip('/')
864 855 pool = Pool()
865 856 rpath = '/'.join([self.baseurl, urllib.quote(path)]).strip('/')
866 857 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
867 858 return ((path + '/' + p) for p, e in entries.iteritems()
868 859 if e.kind == svn.core.svn_node_file)
869 860
870 861 def getrelpath(self, path, module=None):
871 862 if module is None:
872 863 module = self.module
873 864 # Given the repository url of this wc, say
874 865 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
875 866 # extract the "entry" portion (a relative path) from what
876 867 # svn log --xml says, ie
877 868 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
878 869 # that is to say "tests/PloneTestCase.py"
879 870 if path.startswith(module):
880 871 relative = path.rstrip('/')[len(module):]
881 872 if relative.startswith('/'):
882 873 return relative[1:]
883 874 elif relative == '':
884 875 return relative
885 876
886 877 # The path is outside our tracked tree...
887 878 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
888 879 return None
889 880
890 881 def _checkpath(self, path, revnum, module=None):
891 882 if module is not None:
892 883 prevmodule = self.reparent('')
893 884 path = module + '/' + path
894 885 try:
895 886 # ra.check_path does not like leading slashes very much, it leads
896 887 # to PROPFIND subversion errors
897 888 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
898 889 finally:
899 890 if module is not None:
900 891 self.reparent(prevmodule)
901 892
902 893 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
903 894 strict_node_history=False):
904 895 # Normalize path names, svn >= 1.5 only wants paths relative to
905 896 # supplied URL
906 897 relpaths = []
907 898 for p in paths:
908 899 if not p.startswith('/'):
909 900 p = self.module + '/' + p
910 901 relpaths.append(p.strip('/'))
911 902 args = [self.baseurl, relpaths, start, end, limit, discover_changed_paths,
912 903 strict_node_history]
913 904 arg = encodeargs(args)
914 905 hgexe = util.hgexecutable()
915 906 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
916 907 stdin, stdout = util.popen2(cmd)
917 908 stdin.write(arg)
918 909 try:
919 910 stdin.close()
920 911 except IOError:
921 912 raise util.Abort(_('Mercurial failed to run itself, check'
922 913 ' hg executable is in PATH'))
923 914 return logstream(stdout)
924 915
925 916 pre_revprop_change = '''#!/bin/sh
926 917
927 918 REPOS="$1"
928 919 REV="$2"
929 920 USER="$3"
930 921 PROPNAME="$4"
931 922 ACTION="$5"
932 923
933 924 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
934 925 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
935 926 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
936 927
937 928 echo "Changing prohibited revision property" >&2
938 929 exit 1
939 930 '''
940 931
941 932 class svn_sink(converter_sink, commandline):
942 933 commit_re = re.compile(r'Committed revision (\d+).', re.M)
943 934
944 935 def prerun(self):
945 936 if self.wc:
946 937 os.chdir(self.wc)
947 938
948 939 def postrun(self):
949 940 if self.wc:
950 941 os.chdir(self.cwd)
951 942
952 943 def join(self, name):
953 944 return os.path.join(self.wc, '.svn', name)
954 945
955 946 def revmapfile(self):
956 947 return self.join('hg-shamap')
957 948
958 949 def authorfile(self):
959 950 return self.join('hg-authormap')
960 951
961 952 def __init__(self, ui, path):
962 953 converter_sink.__init__(self, ui, path)
963 954 commandline.__init__(self, ui, 'svn')
964 955 self.delete = []
965 956 self.setexec = []
966 957 self.delexec = []
967 958 self.copies = []
968 959 self.wc = None
969 960 self.cwd = os.getcwd()
970 961
971 962 path = os.path.realpath(path)
972 963
973 964 created = False
974 965 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
975 966 self.wc = path
976 967 self.run0('update')
977 968 else:
978 969 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
979 970
980 971 if os.path.isdir(os.path.dirname(path)):
981 972 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
982 973 ui.status(_('initializing svn repository %r\n') %
983 974 os.path.basename(path))
984 975 commandline(ui, 'svnadmin').run0('create', path)
985 976 created = path
986 977 path = util.normpath(path)
987 978 if not path.startswith('/'):
988 979 path = '/' + path
989 980 path = 'file://' + path
990 981
991 982 ui.status(_('initializing svn working copy %r\n')
992 983 % os.path.basename(wcpath))
993 984 self.run0('checkout', path, wcpath)
994 985
995 986 self.wc = wcpath
996 987 self.opener = util.opener(self.wc)
997 988 self.wopener = util.opener(self.wc)
998 989 self.childmap = mapfile(ui, self.join('hg-childmap'))
999 990 self.is_exec = util.checkexec(self.wc) and util.is_exec or None
1000 991
1001 992 if created:
1002 993 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1003 994 fp = open(hook, 'w')
1004 995 fp.write(pre_revprop_change)
1005 996 fp.close()
1006 997 util.set_flags(hook, False, True)
1007 998
1008 999 xport = transport.SvnRaTransport(url=geturl(path))
1009 1000 self.uuid = svn.ra.get_uuid(xport.ra)
1010 1001
1011 1002 def wjoin(self, *names):
1012 1003 return os.path.join(self.wc, *names)
1013 1004
1014 1005 def putfile(self, filename, flags, data):
1015 1006 if 'l' in flags:
1016 1007 self.wopener.symlink(data, filename)
1017 1008 else:
1018 1009 try:
1019 1010 if os.path.islink(self.wjoin(filename)):
1020 1011 os.unlink(filename)
1021 1012 except OSError:
1022 1013 pass
1023 1014 self.wopener(filename, 'w').write(data)
1024 1015
1025 1016 if self.is_exec:
1026 1017 was_exec = self.is_exec(self.wjoin(filename))
1027 1018 else:
1028 1019 # On filesystems not supporting execute-bit, there is no way
1029 1020 # to know if it is set but asking subversion. Setting it
1030 1021 # systematically is just as expensive and much simpler.
1031 1022 was_exec = 'x' not in flags
1032 1023
1033 1024 util.set_flags(self.wjoin(filename), False, 'x' in flags)
1034 1025 if was_exec:
1035 1026 if 'x' not in flags:
1036 1027 self.delexec.append(filename)
1037 1028 else:
1038 1029 if 'x' in flags:
1039 1030 self.setexec.append(filename)
1040 1031
1041 1032 def _copyfile(self, source, dest):
1042 1033 # SVN's copy command pukes if the destination file exists, but
1043 1034 # our copyfile method expects to record a copy that has
1044 1035 # already occurred. Cross the semantic gap.
1045 1036 wdest = self.wjoin(dest)
1046 1037 exists = os.path.exists(wdest)
1047 1038 if exists:
1048 1039 fd, tempname = tempfile.mkstemp(
1049 1040 prefix='hg-copy-', dir=os.path.dirname(wdest))
1050 1041 os.close(fd)
1051 1042 os.unlink(tempname)
1052 1043 os.rename(wdest, tempname)
1053 1044 try:
1054 1045 self.run0('copy', source, dest)
1055 1046 finally:
1056 1047 if exists:
1057 1048 try:
1058 1049 os.unlink(wdest)
1059 1050 except OSError:
1060 1051 pass
1061 1052 os.rename(tempname, wdest)
1062 1053
1063 1054 def dirs_of(self, files):
1064 1055 dirs = set()
1065 1056 for f in files:
1066 1057 if os.path.isdir(self.wjoin(f)):
1067 1058 dirs.add(f)
1068 1059 for i in strutil.rfindall(f, '/'):
1069 1060 dirs.add(f[:i])
1070 1061 return dirs
1071 1062
1072 1063 def add_dirs(self, files):
1073 1064 add_dirs = [d for d in sorted(self.dirs_of(files))
1074 1065 if not os.path.exists(self.wjoin(d, '.svn', 'entries'))]
1075 1066 if add_dirs:
1076 1067 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1077 1068 return add_dirs
1078 1069
1079 1070 def add_files(self, files):
1080 1071 if files:
1081 1072 self.xargs(files, 'add', quiet=True)
1082 1073 return files
1083 1074
1084 1075 def tidy_dirs(self, names):
1085 1076 deleted = []
1086 1077 for d in sorted(self.dirs_of(names), reverse=True):
1087 1078 wd = self.wjoin(d)
1088 1079 if os.listdir(wd) == '.svn':
1089 1080 self.run0('delete', d)
1090 1081 deleted.append(d)
1091 1082 return deleted
1092 1083
1093 1084 def addchild(self, parent, child):
1094 1085 self.childmap[parent] = child
1095 1086
1096 1087 def revid(self, rev):
1097 1088 return u"svn:%s@%s" % (self.uuid, rev)
1098 1089
1099 1090 def putcommit(self, files, copies, parents, commit, source, revmap):
1100 1091 # Apply changes to working copy
1101 1092 for f, v in files:
1102 1093 try:
1103 data = source.getfile(f, v)
1094 data, mode = source.getfile(f, v)
1104 1095 except IOError:
1105 1096 self.delete.append(f)
1106 1097 else:
1107 e = source.getmode(f, v)
1108 self.putfile(f, e, data)
1098 self.putfile(f, mode, data)
1109 1099 if f in copies:
1110 1100 self.copies.append([copies[f], f])
1111 1101 files = [f[0] for f in files]
1112 1102
1113 1103 for parent in parents:
1114 1104 try:
1115 1105 return self.revid(self.childmap[parent])
1116 1106 except KeyError:
1117 1107 pass
1118 1108 entries = set(self.delete)
1119 1109 files = frozenset(files)
1120 1110 entries.update(self.add_dirs(files.difference(entries)))
1121 1111 if self.copies:
1122 1112 for s, d in self.copies:
1123 1113 self._copyfile(s, d)
1124 1114 self.copies = []
1125 1115 if self.delete:
1126 1116 self.xargs(self.delete, 'delete')
1127 1117 self.delete = []
1128 1118 entries.update(self.add_files(files.difference(entries)))
1129 1119 entries.update(self.tidy_dirs(entries))
1130 1120 if self.delexec:
1131 1121 self.xargs(self.delexec, 'propdel', 'svn:executable')
1132 1122 self.delexec = []
1133 1123 if self.setexec:
1134 1124 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1135 1125 self.setexec = []
1136 1126
1137 1127 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1138 1128 fp = os.fdopen(fd, 'w')
1139 1129 fp.write(commit.desc)
1140 1130 fp.close()
1141 1131 try:
1142 1132 output = self.run0('commit',
1143 1133 username=util.shortuser(commit.author),
1144 1134 file=messagefile,
1145 1135 encoding='utf-8')
1146 1136 try:
1147 1137 rev = self.commit_re.search(output).group(1)
1148 1138 except AttributeError:
1149 1139 if not files:
1150 1140 return parents[0]
1151 1141 self.ui.warn(_('unexpected svn output:\n'))
1152 1142 self.ui.warn(output)
1153 1143 raise util.Abort(_('unable to cope with svn output'))
1154 1144 if commit.rev:
1155 1145 self.run('propset', 'hg:convert-rev', commit.rev,
1156 1146 revprop=True, revision=rev)
1157 1147 if commit.branch and commit.branch != 'default':
1158 1148 self.run('propset', 'hg:convert-branch', commit.branch,
1159 1149 revprop=True, revision=rev)
1160 1150 for parent in parents:
1161 1151 self.addchild(parent, rev)
1162 1152 return self.revid(rev)
1163 1153 finally:
1164 1154 os.unlink(messagefile)
1165 1155
1166 1156 def puttags(self, tags):
1167 1157 self.ui.warn(_('XXX TAGS NOT IMPLEMENTED YET\n'))
General Comments 0
You need to be logged in to leave comments. Login now