##// END OF EJS Templates
typing: suppress a couple of attribute-errors in convert...
Matt Harbison -
r50761:2ac60a71 default
parent child Browse files
Show More
@@ -1,731 +1,734 b''
1 # hg.py - hg backend for convert extension
1 # hg.py - hg backend for convert extension
2 #
2 #
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2005-2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 # Notes for hg->hg conversion:
8 # Notes for hg->hg conversion:
9 #
9 #
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
10 # * Old versions of Mercurial didn't trim the whitespace from the ends
11 # of commit messages, but new versions do. Changesets created by
11 # of commit messages, but new versions do. Changesets created by
12 # those older versions, then converted, may thus have different
12 # those older versions, then converted, may thus have different
13 # hashes for changesets that are otherwise identical.
13 # hashes for changesets that are otherwise identical.
14 #
14 #
15 # * Using "--config convert.hg.saverev=true" will make the source
15 # * Using "--config convert.hg.saverev=true" will make the source
16 # identifier to be stored in the converted revision. This will cause
16 # identifier to be stored in the converted revision. This will cause
17 # the converted revision to have a different identity than the
17 # the converted revision to have a different identity than the
18 # source.
18 # source.
19
19
20 import os
20 import os
21 import re
21 import re
22 import time
22 import time
23
23
24 from mercurial.i18n import _
24 from mercurial.i18n import _
25 from mercurial.pycompat import open
25 from mercurial.pycompat import open
26 from mercurial.node import (
26 from mercurial.node import (
27 bin,
27 bin,
28 hex,
28 hex,
29 sha1nodeconstants,
29 sha1nodeconstants,
30 )
30 )
31 from mercurial import (
31 from mercurial import (
32 bookmarks,
32 bookmarks,
33 context,
33 context,
34 error,
34 error,
35 exchange,
35 exchange,
36 hg,
36 hg,
37 lock as lockmod,
37 lock as lockmod,
38 logcmdutil,
38 logcmdutil,
39 merge as mergemod,
39 merge as mergemod,
40 mergestate,
40 mergestate,
41 phases,
41 phases,
42 util,
42 util,
43 )
43 )
44 from mercurial.utils import dateutil
44 from mercurial.utils import dateutil
45
45
46 stringio = util.stringio
46 stringio = util.stringio
47
47
48 from . import common
48 from . import common
49
49
50 mapfile = common.mapfile
50 mapfile = common.mapfile
51 NoRepo = common.NoRepo
51 NoRepo = common.NoRepo
52
52
53 sha1re = re.compile(br'\b[0-9a-f]{12,40}\b')
53 sha1re = re.compile(br'\b[0-9a-f]{12,40}\b')
54
54
55
55
56 class mercurial_sink(common.converter_sink):
56 class mercurial_sink(common.converter_sink):
57 def __init__(self, ui, repotype, path):
57 def __init__(self, ui, repotype, path):
58 common.converter_sink.__init__(self, ui, repotype, path)
58 common.converter_sink.__init__(self, ui, repotype, path)
59 self.branchnames = ui.configbool(b'convert', b'hg.usebranchnames')
59 self.branchnames = ui.configbool(b'convert', b'hg.usebranchnames')
60 self.clonebranches = ui.configbool(b'convert', b'hg.clonebranches')
60 self.clonebranches = ui.configbool(b'convert', b'hg.clonebranches')
61 self.tagsbranch = ui.config(b'convert', b'hg.tagsbranch')
61 self.tagsbranch = ui.config(b'convert', b'hg.tagsbranch')
62 self.lastbranch = None
62 self.lastbranch = None
63 if os.path.isdir(path) and len(os.listdir(path)) > 0:
63 if os.path.isdir(path) and len(os.listdir(path)) > 0:
64 try:
64 try:
65 self.repo = hg.repository(self.ui, path)
65 self.repo = hg.repository(self.ui, path)
66 if not self.repo.local():
66 if not self.repo.local():
67 raise NoRepo(
67 raise NoRepo(
68 _(b'%s is not a local Mercurial repository') % path
68 _(b'%s is not a local Mercurial repository') % path
69 )
69 )
70 except error.RepoError as err:
70 except error.RepoError as err:
71 ui.traceback()
71 ui.traceback()
72 raise NoRepo(err.args[0])
72 raise NoRepo(err.args[0])
73 else:
73 else:
74 try:
74 try:
75 ui.status(_(b'initializing destination %s repository\n') % path)
75 ui.status(_(b'initializing destination %s repository\n') % path)
76 self.repo = hg.repository(self.ui, path, create=True)
76 self.repo = hg.repository(self.ui, path, create=True)
77 if not self.repo.local():
77 if not self.repo.local():
78 raise NoRepo(
78 raise NoRepo(
79 _(b'%s is not a local Mercurial repository') % path
79 _(b'%s is not a local Mercurial repository') % path
80 )
80 )
81 self.created.append(path)
81 self.created.append(path)
82 except error.RepoError:
82 except error.RepoError:
83 ui.traceback()
83 ui.traceback()
84 raise NoRepo(
84 raise NoRepo(
85 _(b"could not create hg repository %s as sink") % path
85 _(b"could not create hg repository %s as sink") % path
86 )
86 )
87 self.lock = None
87 self.lock = None
88 self.wlock = None
88 self.wlock = None
89 self.filemapmode = False
89 self.filemapmode = False
90 self.subrevmaps = {}
90 self.subrevmaps = {}
91
91
92 def before(self):
92 def before(self):
93 self.ui.debug(b'run hg sink pre-conversion action\n')
93 self.ui.debug(b'run hg sink pre-conversion action\n')
94 self.wlock = self.repo.wlock()
94 self.wlock = self.repo.wlock()
95 self.lock = self.repo.lock()
95 self.lock = self.repo.lock()
96
96
97 def after(self):
97 def after(self):
98 self.ui.debug(b'run hg sink post-conversion action\n')
98 self.ui.debug(b'run hg sink post-conversion action\n')
99 if self.lock:
99 if self.lock:
100 self.lock.release()
100 self.lock.release()
101 if self.wlock:
101 if self.wlock:
102 self.wlock.release()
102 self.wlock.release()
103
103
104 def revmapfile(self):
104 def revmapfile(self):
105 return self.repo.vfs.join(b"shamap")
105 return self.repo.vfs.join(b"shamap")
106
106
107 def authorfile(self):
107 def authorfile(self):
108 return self.repo.vfs.join(b"authormap")
108 return self.repo.vfs.join(b"authormap")
109
109
110 def setbranch(self, branch, pbranches):
110 def setbranch(self, branch, pbranches):
111 if not self.clonebranches:
111 if not self.clonebranches:
112 return
112 return
113
113
114 setbranch = branch != self.lastbranch
114 setbranch = branch != self.lastbranch
115 self.lastbranch = branch
115 self.lastbranch = branch
116 if not branch:
116 if not branch:
117 branch = b'default'
117 branch = b'default'
118 pbranches = [(b[0], b[1] and b[1] or b'default') for b in pbranches]
118 pbranches = [(b[0], b[1] and b[1] or b'default') for b in pbranches]
119
119
120 branchpath = os.path.join(self.path, branch)
120 branchpath = os.path.join(self.path, branch)
121 if setbranch:
121 if setbranch:
122 self.after()
122 self.after()
123 try:
123 try:
124 self.repo = hg.repository(self.ui, branchpath)
124 self.repo = hg.repository(self.ui, branchpath)
125 except Exception:
125 except Exception:
126 self.repo = hg.repository(self.ui, branchpath, create=True)
126 self.repo = hg.repository(self.ui, branchpath, create=True)
127 self.before()
127 self.before()
128
128
129 # pbranches may bring revisions from other branches (merge parents)
129 # pbranches may bring revisions from other branches (merge parents)
130 # Make sure we have them, or pull them.
130 # Make sure we have them, or pull them.
131 missings = {}
131 missings = {}
132 for b in pbranches:
132 for b in pbranches:
133 try:
133 try:
134 self.repo.lookup(b[0])
134 self.repo.lookup(b[0])
135 except Exception:
135 except Exception:
136 missings.setdefault(b[1], []).append(b[0])
136 missings.setdefault(b[1], []).append(b[0])
137
137
138 if missings:
138 if missings:
139 self.after()
139 self.after()
140 for pbranch, heads in sorted(missings.items()):
140 for pbranch, heads in sorted(missings.items()):
141 pbranchpath = os.path.join(self.path, pbranch)
141 pbranchpath = os.path.join(self.path, pbranch)
142 prepo = hg.peer(self.ui, {}, pbranchpath)
142 prepo = hg.peer(self.ui, {}, pbranchpath)
143 self.ui.note(
143 self.ui.note(
144 _(b'pulling from %s into %s\n') % (pbranch, branch)
144 _(b'pulling from %s into %s\n') % (pbranch, branch)
145 )
145 )
146 exchange.pull(
146 exchange.pull(
147 self.repo, prepo, heads=[prepo.lookup(h) for h in heads]
147 self.repo, prepo, heads=[prepo.lookup(h) for h in heads]
148 )
148 )
149 self.before()
149 self.before()
150
150
151 def _rewritetags(self, source, revmap, data):
151 def _rewritetags(self, source, revmap, data):
152 fp = stringio()
152 fp = stringio()
153 for line in data.splitlines():
153 for line in data.splitlines():
154 s = line.split(b' ', 1)
154 s = line.split(b' ', 1)
155 if len(s) != 2:
155 if len(s) != 2:
156 self.ui.warn(_(b'invalid tag entry: "%s"\n') % line)
156 self.ui.warn(_(b'invalid tag entry: "%s"\n') % line)
157 fp.write(b'%s\n' % line) # Bogus, but keep for hash stability
157 fp.write(b'%s\n' % line) # Bogus, but keep for hash stability
158 continue
158 continue
159 revid = revmap.get(source.lookuprev(s[0]))
159 revid = revmap.get(source.lookuprev(s[0]))
160 if not revid:
160 if not revid:
161 if s[0] == sha1nodeconstants.nullhex:
161 if s[0] == sha1nodeconstants.nullhex:
162 revid = s[0]
162 revid = s[0]
163 else:
163 else:
164 # missing, but keep for hash stability
164 # missing, but keep for hash stability
165 self.ui.warn(_(b'missing tag entry: "%s"\n') % line)
165 self.ui.warn(_(b'missing tag entry: "%s"\n') % line)
166 fp.write(b'%s\n' % line)
166 fp.write(b'%s\n' % line)
167 continue
167 continue
168 fp.write(b'%s %s\n' % (revid, s[1]))
168 fp.write(b'%s %s\n' % (revid, s[1]))
169 return fp.getvalue()
169 return fp.getvalue()
170
170
171 def _rewritesubstate(self, source, data):
171 def _rewritesubstate(self, source, data):
172 fp = stringio()
172 fp = stringio()
173 for line in data.splitlines():
173 for line in data.splitlines():
174 s = line.split(b' ', 1)
174 s = line.split(b' ', 1)
175 if len(s) != 2:
175 if len(s) != 2:
176 continue
176 continue
177
177
178 revid = s[0]
178 revid = s[0]
179 subpath = s[1]
179 subpath = s[1]
180 if revid != sha1nodeconstants.nullhex:
180 if revid != sha1nodeconstants.nullhex:
181 revmap = self.subrevmaps.get(subpath)
181 revmap = self.subrevmaps.get(subpath)
182 if revmap is None:
182 if revmap is None:
183 revmap = mapfile(
183 revmap = mapfile(
184 self.ui, self.repo.wjoin(subpath, b'.hg/shamap')
184 self.ui, self.repo.wjoin(subpath, b'.hg/shamap')
185 )
185 )
186 self.subrevmaps[subpath] = revmap
186 self.subrevmaps[subpath] = revmap
187
187
188 # It is reasonable that one or more of the subrepos don't
188 # It is reasonable that one or more of the subrepos don't
189 # need to be converted, in which case they can be cloned
189 # need to be converted, in which case they can be cloned
190 # into place instead of converted. Therefore, only warn
190 # into place instead of converted. Therefore, only warn
191 # once.
191 # once.
192 msg = _(b'no ".hgsubstate" updates will be made for "%s"\n')
192 msg = _(b'no ".hgsubstate" updates will be made for "%s"\n')
193 if len(revmap) == 0:
193 if len(revmap) == 0:
194 sub = self.repo.wvfs.reljoin(subpath, b'.hg')
194 sub = self.repo.wvfs.reljoin(subpath, b'.hg')
195
195
196 if self.repo.wvfs.exists(sub):
196 if self.repo.wvfs.exists(sub):
197 self.ui.warn(msg % subpath)
197 self.ui.warn(msg % subpath)
198
198
199 newid = revmap.get(revid)
199 newid = revmap.get(revid)
200 if not newid:
200 if not newid:
201 if len(revmap) > 0:
201 if len(revmap) > 0:
202 self.ui.warn(
202 self.ui.warn(
203 _(b"%s is missing from %s/.hg/shamap\n")
203 _(b"%s is missing from %s/.hg/shamap\n")
204 % (revid, subpath)
204 % (revid, subpath)
205 )
205 )
206 else:
206 else:
207 revid = newid
207 revid = newid
208
208
209 fp.write(b'%s %s\n' % (revid, subpath))
209 fp.write(b'%s %s\n' % (revid, subpath))
210
210
211 return fp.getvalue()
211 return fp.getvalue()
212
212
213 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
213 def _calculatemergedfiles(self, source, p1ctx, p2ctx):
214 """Calculates the files from p2 that we need to pull in when merging p1
214 """Calculates the files from p2 that we need to pull in when merging p1
215 and p2, given that the merge is coming from the given source.
215 and p2, given that the merge is coming from the given source.
216
216
217 This prevents us from losing files that only exist in the target p2 and
217 This prevents us from losing files that only exist in the target p2 and
218 that don't come from the source repo (like if you're merging multiple
218 that don't come from the source repo (like if you're merging multiple
219 repositories together).
219 repositories together).
220 """
220 """
221 anc = [p1ctx.ancestor(p2ctx)]
221 anc = [p1ctx.ancestor(p2ctx)]
222 # Calculate what files are coming from p2
222 # Calculate what files are coming from p2
223 # TODO: mresult.commitinfo might be able to get that info
223 # TODO: mresult.commitinfo might be able to get that info
224 mresult = mergemod.calculateupdates(
224 mresult = mergemod.calculateupdates(
225 self.repo,
225 self.repo,
226 p1ctx,
226 p1ctx,
227 p2ctx,
227 p2ctx,
228 anc,
228 anc,
229 branchmerge=True,
229 branchmerge=True,
230 force=True,
230 force=True,
231 acceptremote=False,
231 acceptremote=False,
232 followcopies=False,
232 followcopies=False,
233 )
233 )
234
234
235 for file, (action, info, msg) in mresult.filemap():
235 for file, (action, info, msg) in mresult.filemap():
236 if source.targetfilebelongstosource(file):
236 if source.targetfilebelongstosource(file):
237 # If the file belongs to the source repo, ignore the p2
237 # If the file belongs to the source repo, ignore the p2
238 # since it will be covered by the existing fileset.
238 # since it will be covered by the existing fileset.
239 continue
239 continue
240
240
241 # If the file requires actual merging, abort. We don't have enough
241 # If the file requires actual merging, abort. We don't have enough
242 # context to resolve merges correctly.
242 # context to resolve merges correctly.
243 if action in mergestate.CONVERT_MERGE_ACTIONS:
243 if action in mergestate.CONVERT_MERGE_ACTIONS:
244 raise error.Abort(
244 raise error.Abort(
245 _(
245 _(
246 b"unable to convert merge commit "
246 b"unable to convert merge commit "
247 b"since target parents do not merge cleanly (file "
247 b"since target parents do not merge cleanly (file "
248 b"%s, parents %s and %s)"
248 b"%s, parents %s and %s)"
249 )
249 )
250 % (file, p1ctx, p2ctx)
250 % (file, p1ctx, p2ctx)
251 )
251 )
252 elif action == mergestate.ACTION_KEEP:
252 elif action == mergestate.ACTION_KEEP:
253 # 'keep' means nothing changed from p1
253 # 'keep' means nothing changed from p1
254 continue
254 continue
255 else:
255 else:
256 # Any other change means we want to take the p2 version
256 # Any other change means we want to take the p2 version
257 yield file
257 yield file
258
258
259 def putcommit(
259 def putcommit(
260 self, files, copies, parents, commit, source, revmap, full, cleanp2
260 self, files, copies, parents, commit, source, revmap, full, cleanp2
261 ):
261 ):
262 files = dict(files)
262 files = dict(files)
263
263
264 def getfilectx(repo, memctx, f):
264 def getfilectx(repo, memctx, f):
265 if p2ctx and f in p2files and f not in copies:
265 if p2ctx and f in p2files and f not in copies:
266 self.ui.debug(b'reusing %s from p2\n' % f)
266 self.ui.debug(b'reusing %s from p2\n' % f)
267 try:
267 try:
268 return p2ctx[f]
268 return p2ctx[f]
269 except error.ManifestLookupError:
269 except error.ManifestLookupError:
270 # If the file doesn't exist in p2, then we're syncing a
270 # If the file doesn't exist in p2, then we're syncing a
271 # delete, so just return None.
271 # delete, so just return None.
272 return None
272 return None
273 try:
273 try:
274 v = files[f]
274 v = files[f]
275 except KeyError:
275 except KeyError:
276 return None
276 return None
277 data, mode = source.getfile(f, v)
277 data, mode = source.getfile(f, v)
278 if data is None:
278 if data is None:
279 return None
279 return None
280 if f == b'.hgtags':
280 if f == b'.hgtags':
281 data = self._rewritetags(source, revmap, data)
281 data = self._rewritetags(source, revmap, data)
282 if f == b'.hgsubstate':
282 if f == b'.hgsubstate':
283 data = self._rewritesubstate(source, data)
283 data = self._rewritesubstate(source, data)
284 return context.memfilectx(
284 return context.memfilectx(
285 self.repo,
285 self.repo,
286 memctx,
286 memctx,
287 f,
287 f,
288 data,
288 data,
289 b'l' in mode,
289 b'l' in mode,
290 b'x' in mode,
290 b'x' in mode,
291 copies.get(f),
291 copies.get(f),
292 )
292 )
293
293
294 pl = []
294 pl = []
295 for p in parents:
295 for p in parents:
296 if p not in pl:
296 if p not in pl:
297 pl.append(p)
297 pl.append(p)
298 parents = pl
298 parents = pl
299 nparents = len(parents)
299 nparents = len(parents)
300 if self.filemapmode and nparents == 1:
300 if self.filemapmode and nparents == 1:
301 m1node = self.repo.changelog.read(bin(parents[0]))[0]
301 m1node = self.repo.changelog.read(bin(parents[0]))[0]
302 parent = parents[0]
302 parent = parents[0]
303
303
304 if len(parents) < 2:
304 if len(parents) < 2:
305 parents.append(self.repo.nullid)
305 parents.append(self.repo.nullid)
306 if len(parents) < 2:
306 if len(parents) < 2:
307 parents.append(self.repo.nullid)
307 parents.append(self.repo.nullid)
308 p2 = parents.pop(0)
308 p2 = parents.pop(0)
309
309
310 text = commit.desc
310 text = commit.desc
311
311
312 sha1s = re.findall(sha1re, text)
312 sha1s = re.findall(sha1re, text)
313 for sha1 in sha1s:
313 for sha1 in sha1s:
314 oldrev = source.lookuprev(sha1)
314 oldrev = source.lookuprev(sha1)
315 newrev = revmap.get(oldrev)
315 newrev = revmap.get(oldrev)
316 if newrev is not None:
316 if newrev is not None:
317 text = text.replace(sha1, newrev[: len(sha1)])
317 text = text.replace(sha1, newrev[: len(sha1)])
318
318
319 extra = commit.extra.copy()
319 extra = commit.extra.copy()
320
320
321 sourcename = self.repo.ui.config(b'convert', b'hg.sourcename')
321 sourcename = self.repo.ui.config(b'convert', b'hg.sourcename')
322 if sourcename:
322 if sourcename:
323 extra[b'convert_source'] = sourcename
323 extra[b'convert_source'] = sourcename
324
324
325 for label in (
325 for label in (
326 b'source',
326 b'source',
327 b'transplant_source',
327 b'transplant_source',
328 b'rebase_source',
328 b'rebase_source',
329 b'intermediate-source',
329 b'intermediate-source',
330 ):
330 ):
331 node = extra.get(label)
331 node = extra.get(label)
332
332
333 if node is None:
333 if node is None:
334 continue
334 continue
335
335
336 # Only transplant stores its reference in binary
336 # Only transplant stores its reference in binary
337 if label == b'transplant_source':
337 if label == b'transplant_source':
338 node = hex(node)
338 node = hex(node)
339
339
340 newrev = revmap.get(node)
340 newrev = revmap.get(node)
341 if newrev is not None:
341 if newrev is not None:
342 if label == b'transplant_source':
342 if label == b'transplant_source':
343 newrev = bin(newrev)
343 newrev = bin(newrev)
344
344
345 extra[label] = newrev
345 extra[label] = newrev
346
346
347 if self.branchnames and commit.branch:
347 if self.branchnames and commit.branch:
348 extra[b'branch'] = commit.branch
348 extra[b'branch'] = commit.branch
349 if commit.rev and commit.saverev:
349 if commit.rev and commit.saverev:
350 extra[b'convert_revision'] = commit.rev
350 extra[b'convert_revision'] = commit.rev
351
351
352 while parents:
352 while parents:
353 p1 = p2
353 p1 = p2
354 p2 = parents.pop(0)
354 p2 = parents.pop(0)
355 p1ctx = self.repo[p1]
355 p1ctx = self.repo[p1]
356 p2ctx = None
356 p2ctx = None
357 if p2 != self.repo.nullid:
357 if p2 != self.repo.nullid:
358 p2ctx = self.repo[p2]
358 p2ctx = self.repo[p2]
359 fileset = set(files)
359 fileset = set(files)
360 if full:
360 if full:
361 fileset.update(self.repo[p1])
361 fileset.update(self.repo[p1])
362 fileset.update(self.repo[p2])
362 fileset.update(self.repo[p2])
363
363
364 if p2ctx:
364 if p2ctx:
365 p2files = set(cleanp2)
365 p2files = set(cleanp2)
366 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
366 for file in self._calculatemergedfiles(source, p1ctx, p2ctx):
367 p2files.add(file)
367 p2files.add(file)
368 fileset.add(file)
368 fileset.add(file)
369
369
370 ctx = context.memctx(
370 ctx = context.memctx(
371 self.repo,
371 self.repo,
372 (p1, p2),
372 (p1, p2),
373 text,
373 text,
374 fileset,
374 fileset,
375 getfilectx,
375 getfilectx,
376 commit.author,
376 commit.author,
377 commit.date,
377 commit.date,
378 extra,
378 extra,
379 )
379 )
380
380
381 # We won't know if the conversion changes the node until after the
381 # We won't know if the conversion changes the node until after the
382 # commit, so copy the source's phase for now.
382 # commit, so copy the source's phase for now.
383 self.repo.ui.setconfig(
383 self.repo.ui.setconfig(
384 b'phases',
384 b'phases',
385 b'new-commit',
385 b'new-commit',
386 phases.phasenames[commit.phase],
386 phases.phasenames[commit.phase],
387 b'convert',
387 b'convert',
388 )
388 )
389
389
390 with self.repo.transaction(b"convert") as tr:
390 with self.repo.transaction(b"convert") as tr:
391 if self.repo.ui.config(b'convert', b'hg.preserve-hash'):
391 if self.repo.ui.config(b'convert', b'hg.preserve-hash'):
392 origctx = commit.ctx
392 origctx = commit.ctx
393 else:
393 else:
394 origctx = None
394 origctx = None
395 node = hex(self.repo.commitctx(ctx, origctx=origctx))
395 node = hex(self.repo.commitctx(ctx, origctx=origctx))
396
396
397 # If the node value has changed, but the phase is lower than
397 # If the node value has changed, but the phase is lower than
398 # draft, set it back to draft since it hasn't been exposed
398 # draft, set it back to draft since it hasn't been exposed
399 # anywhere.
399 # anywhere.
400 if commit.rev != node:
400 if commit.rev != node:
401 ctx = self.repo[node]
401 ctx = self.repo[node]
402 if ctx.phase() < phases.draft:
402 if ctx.phase() < phases.draft:
403 phases.registernew(
403 phases.registernew(
404 self.repo, tr, phases.draft, [ctx.rev()]
404 self.repo, tr, phases.draft, [ctx.rev()]
405 )
405 )
406
406
407 text = b"(octopus merge fixup)\n"
407 text = b"(octopus merge fixup)\n"
408 p2 = node
408 p2 = node
409
409
410 if self.filemapmode and nparents == 1:
410 if self.filemapmode and nparents == 1:
411 man = self.repo.manifestlog.getstorage(b'')
411 man = self.repo.manifestlog.getstorage(b'')
412 mnode = self.repo.changelog.read(bin(p2))[0]
412 mnode = self.repo.changelog.read(bin(p2))[0]
413 closed = b'close' in commit.extra
413 closed = b'close' in commit.extra
414 if not closed and not man.cmp(m1node, man.revision(mnode)):
414 if not closed and not man.cmp(m1node, man.revision(mnode)):
415 self.ui.status(_(b"filtering out empty revision\n"))
415 self.ui.status(_(b"filtering out empty revision\n"))
416 self.repo.rollback(force=True)
416 self.repo.rollback(force=True)
417 return parent
417 return parent
418 return p2
418 return p2
419
419
420 def puttags(self, tags):
420 def puttags(self, tags):
421 tagparent = self.repo.branchtip(self.tagsbranch, ignoremissing=True)
421 tagparent = self.repo.branchtip(self.tagsbranch, ignoremissing=True)
422 tagparent = tagparent or self.repo.nullid
422 tagparent = tagparent or self.repo.nullid
423
423
424 oldlines = set()
424 oldlines = set()
425 for branch, heads in self.repo.branchmap().items():
425 for branch, heads in self.repo.branchmap().items():
426 for h in heads:
426 for h in heads:
427 if b'.hgtags' in self.repo[h]:
427 if b'.hgtags' in self.repo[h]:
428 oldlines.update(
428 oldlines.update(
429 set(self.repo[h][b'.hgtags'].data().splitlines(True))
429 set(self.repo[h][b'.hgtags'].data().splitlines(True))
430 )
430 )
431 oldlines = sorted(list(oldlines))
431 oldlines = sorted(list(oldlines))
432
432
433 newlines = sorted([(b"%s %s\n" % (tags[tag], tag)) for tag in tags])
433 newlines = sorted([(b"%s %s\n" % (tags[tag], tag)) for tag in tags])
434 if newlines == oldlines:
434 if newlines == oldlines:
435 return None, None
435 return None, None
436
436
437 # if the old and new tags match, then there is nothing to update
437 # if the old and new tags match, then there is nothing to update
438 oldtags = set()
438 oldtags = set()
439 newtags = set()
439 newtags = set()
440 for line in oldlines:
440 for line in oldlines:
441 s = line.strip().split(b' ', 1)
441 s = line.strip().split(b' ', 1)
442 if len(s) != 2:
442 if len(s) != 2:
443 continue
443 continue
444 oldtags.add(s[1])
444 oldtags.add(s[1])
445 for line in newlines:
445 for line in newlines:
446 s = line.strip().split(b' ', 1)
446 s = line.strip().split(b' ', 1)
447 if len(s) != 2:
447 if len(s) != 2:
448 continue
448 continue
449 if s[1] not in oldtags:
449 if s[1] not in oldtags:
450 newtags.add(s[1].strip())
450 newtags.add(s[1].strip())
451
451
452 if not newtags:
452 if not newtags:
453 return None, None
453 return None, None
454
454
455 data = b"".join(newlines)
455 data = b"".join(newlines)
456
456
457 def getfilectx(repo, memctx, f):
457 def getfilectx(repo, memctx, f):
458 return context.memfilectx(repo, memctx, f, data, False, False, None)
458 return context.memfilectx(repo, memctx, f, data, False, False, None)
459
459
460 self.ui.status(_(b"updating tags\n"))
460 self.ui.status(_(b"updating tags\n"))
461 date = b"%d 0" % int(time.mktime(time.gmtime()))
461 date = b"%d 0" % int(time.mktime(time.gmtime()))
462 extra = {b'branch': self.tagsbranch}
462 extra = {b'branch': self.tagsbranch}
463 ctx = context.memctx(
463 ctx = context.memctx(
464 self.repo,
464 self.repo,
465 (tagparent, None),
465 (tagparent, None),
466 b"update tags",
466 b"update tags",
467 [b".hgtags"],
467 [b".hgtags"],
468 getfilectx,
468 getfilectx,
469 b"convert-repo",
469 b"convert-repo",
470 date,
470 date,
471 extra,
471 extra,
472 )
472 )
473 node = self.repo.commitctx(ctx)
473 node = self.repo.commitctx(ctx)
474 return hex(node), hex(tagparent)
474 return hex(node), hex(tagparent)
475
475
476 def setfilemapmode(self, active):
476 def setfilemapmode(self, active):
477 self.filemapmode = active
477 self.filemapmode = active
478
478
479 def putbookmarks(self, updatedbookmark):
479 def putbookmarks(self, updatedbookmark):
480 if not len(updatedbookmark):
480 if not len(updatedbookmark):
481 return
481 return
482 wlock = lock = tr = None
482 wlock = lock = tr = None
483 try:
483 try:
484 wlock = self.repo.wlock()
484 wlock = self.repo.wlock()
485 lock = self.repo.lock()
485 lock = self.repo.lock()
486 tr = self.repo.transaction(b'bookmark')
486 tr = self.repo.transaction(b'bookmark')
487 self.ui.status(_(b"updating bookmarks\n"))
487 self.ui.status(_(b"updating bookmarks\n"))
488 destmarks = self.repo._bookmarks
488 destmarks = self.repo._bookmarks
489 changes = [
489 changes = [
490 (bookmark, bin(updatedbookmark[bookmark]))
490 (bookmark, bin(updatedbookmark[bookmark]))
491 for bookmark in updatedbookmark
491 for bookmark in updatedbookmark
492 ]
492 ]
493 destmarks.applychanges(self.repo, tr, changes)
493 destmarks.applychanges(self.repo, tr, changes)
494 tr.close()
494 tr.close()
495 finally:
495 finally:
496 lockmod.release(lock, wlock, tr)
496 lockmod.release(lock, wlock, tr)
497
497
498 def hascommitfrommap(self, rev):
498 def hascommitfrommap(self, rev):
499 # the exact semantics of clonebranches is unclear so we can't say no
499 # the exact semantics of clonebranches is unclear so we can't say no
500 return rev in self.repo or self.clonebranches
500 return rev in self.repo or self.clonebranches
501
501
502 def hascommitforsplicemap(self, rev):
502 def hascommitforsplicemap(self, rev):
503 if rev not in self.repo and self.clonebranches:
503 if rev not in self.repo and self.clonebranches:
504 raise error.Abort(
504 raise error.Abort(
505 _(
505 _(
506 b'revision %s not found in destination '
506 b'revision %s not found in destination '
507 b'repository (lookups with clonebranches=true '
507 b'repository (lookups with clonebranches=true '
508 b'are not implemented)'
508 b'are not implemented)'
509 )
509 )
510 % rev
510 % rev
511 )
511 )
512 return rev in self.repo
512 return rev in self.repo
513
513
514
514
515 class mercurial_source(common.converter_source):
515 class mercurial_source(common.converter_source):
516 def __init__(self, ui, repotype, path, revs=None):
516 def __init__(self, ui, repotype, path, revs=None):
517 common.converter_source.__init__(self, ui, repotype, path, revs)
517 common.converter_source.__init__(self, ui, repotype, path, revs)
518 self.ignoreerrors = ui.configbool(b'convert', b'hg.ignoreerrors')
518 self.ignoreerrors = ui.configbool(b'convert', b'hg.ignoreerrors')
519 self.ignored = set()
519 self.ignored = set()
520 self.saverev = ui.configbool(b'convert', b'hg.saverev')
520 self.saverev = ui.configbool(b'convert', b'hg.saverev')
521 try:
521 try:
522 self.repo = hg.repository(self.ui, path)
522 self.repo = hg.repository(self.ui, path)
523 # try to provoke an exception if this isn't really a hg
523 # try to provoke an exception if this isn't really a hg
524 # repo, but some other bogus compatible-looking url
524 # repo, but some other bogus compatible-looking url
525 if not self.repo.local():
525 if not self.repo.local():
526 raise error.RepoError
526 raise error.RepoError
527 except error.RepoError:
527 except error.RepoError:
528 ui.traceback()
528 ui.traceback()
529 raise NoRepo(_(b"%s is not a local Mercurial repository") % path)
529 raise NoRepo(_(b"%s is not a local Mercurial repository") % path)
530 self.lastrev = None
530 self.lastrev = None
531 self.lastctx = None
531 self.lastctx = None
532 self._changescache = None, None
532 self._changescache = None, None
533 self.convertfp = None
533 self.convertfp = None
534 # Restrict converted revisions to startrev descendants
534 # Restrict converted revisions to startrev descendants
535 startnode = ui.config(b'convert', b'hg.startrev')
535 startnode = ui.config(b'convert', b'hg.startrev')
536 hgrevs = ui.config(b'convert', b'hg.revs')
536 hgrevs = ui.config(b'convert', b'hg.revs')
537 if hgrevs is None:
537 if hgrevs is None:
538 if startnode is not None:
538 if startnode is not None:
539 try:
539 try:
540 startnode = self.repo.lookup(startnode)
540 startnode = self.repo.lookup(startnode)
541 except error.RepoError:
541 except error.RepoError:
542 raise error.Abort(
542 raise error.Abort(
543 _(b'%s is not a valid start revision') % startnode
543 _(b'%s is not a valid start revision') % startnode
544 )
544 )
545 startrev = self.repo.changelog.rev(startnode)
545 startrev = self.repo.changelog.rev(startnode)
546 children = {startnode: 1}
546 children = {startnode: 1}
547 for r in self.repo.changelog.descendants([startrev]):
547 for r in self.repo.changelog.descendants([startrev]):
548 children[self.repo.changelog.node(r)] = 1
548 children[self.repo.changelog.node(r)] = 1
549 self.keep = children.__contains__
549 self.keep = children.__contains__
550 else:
550 else:
551 self.keep = util.always
551 self.keep = util.always
552 if revs:
552 if revs:
553 self._heads = [self.repo.lookup(r) for r in revs]
553 self._heads = [self.repo.lookup(r) for r in revs]
554 else:
554 else:
555 self._heads = self.repo.heads()
555 self._heads = self.repo.heads()
556 else:
556 else:
557 if revs or startnode is not None:
557 if revs or startnode is not None:
558 raise error.Abort(
558 raise error.Abort(
559 _(
559 _(
560 b'hg.revs cannot be combined with '
560 b'hg.revs cannot be combined with '
561 b'hg.startrev or --rev'
561 b'hg.startrev or --rev'
562 )
562 )
563 )
563 )
564 nodes = set()
564 nodes = set()
565 parents = set()
565 parents = set()
566 for r in logcmdutil.revrange(self.repo, [hgrevs]):
566 for r in logcmdutil.revrange(self.repo, [hgrevs]):
567 ctx = self.repo[r]
567 ctx = self.repo[r]
568 nodes.add(ctx.node())
568 nodes.add(ctx.node())
569 parents.update(p.node() for p in ctx.parents())
569 parents.update(p.node() for p in ctx.parents())
570 self.keep = nodes.__contains__
570 self.keep = nodes.__contains__
571 self._heads = nodes - parents
571 self._heads = nodes - parents
572
572
573 def _changectx(self, rev):
573 def _changectx(self, rev):
574 if self.lastrev != rev:
574 if self.lastrev != rev:
575 self.lastctx = self.repo[rev]
575 self.lastctx = self.repo[rev]
576 self.lastrev = rev
576 self.lastrev = rev
577 return self.lastctx
577 return self.lastctx
578
578
579 def _parents(self, ctx):
579 def _parents(self, ctx):
580 return [p for p in ctx.parents() if p and self.keep(p.node())]
580 return [p for p in ctx.parents() if p and self.keep(p.node())]
581
581
582 def getheads(self):
582 def getheads(self):
583 return [hex(h) for h in self._heads if self.keep(h)]
583 return [hex(h) for h in self._heads if self.keep(h)]
584
584
585 def getfile(self, name, rev):
585 def getfile(self, name, rev):
586 try:
586 try:
587 fctx = self._changectx(rev)[name]
587 fctx = self._changectx(rev)[name]
588 return fctx.data(), fctx.flags()
588 return fctx.data(), fctx.flags()
589 except error.LookupError:
589 except error.LookupError:
590 return None, None
590 return None, None
591
591
592 def _changedfiles(self, ctx1, ctx2):
592 def _changedfiles(self, ctx1, ctx2):
593 ma, r = [], []
593 ma, r = [], []
594 maappend = ma.append
594 maappend = ma.append
595 rappend = r.append
595 rappend = r.append
596 d = ctx1.manifest().diff(ctx2.manifest())
596 d = ctx1.manifest().diff(ctx2.manifest())
597 for f, ((node1, flag1), (node2, flag2)) in d.items():
597 for f, ((node1, flag1), (node2, flag2)) in d.items():
598 if node2 is None:
598 if node2 is None:
599 rappend(f)
599 rappend(f)
600 else:
600 else:
601 maappend(f)
601 maappend(f)
602 return ma, r
602 return ma, r
603
603
604 def getchanges(self, rev, full):
604 def getchanges(self, rev, full):
605 ctx = self._changectx(rev)
605 ctx = self._changectx(rev)
606 parents = self._parents(ctx)
606 parents = self._parents(ctx)
607 if full or not parents:
607 if full or not parents:
608 files = copyfiles = ctx.manifest()
608 files = copyfiles = ctx.manifest()
609 if parents:
609 if parents:
610 if self._changescache[0] == rev:
610 if self._changescache[0] == rev:
611 ma, r = self._changescache[1]
611 # TODO: add type hints to avoid this warning, instead of
612 # suppressing it:
613 # No attribute '__iter__' on None [attribute-error]
614 ma, r = self._changescache[1] # pytype: disable=attribute-error
612 else:
615 else:
613 ma, r = self._changedfiles(parents[0], ctx)
616 ma, r = self._changedfiles(parents[0], ctx)
614 if not full:
617 if not full:
615 files = ma + r
618 files = ma + r
616 copyfiles = ma
619 copyfiles = ma
617 # _getcopies() is also run for roots and before filtering so missing
620 # _getcopies() is also run for roots and before filtering so missing
618 # revlogs are detected early
621 # revlogs are detected early
619 copies = self._getcopies(ctx, parents, copyfiles)
622 copies = self._getcopies(ctx, parents, copyfiles)
620 cleanp2 = set()
623 cleanp2 = set()
621 if len(parents) == 2:
624 if len(parents) == 2:
622 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
625 d = parents[1].manifest().diff(ctx.manifest(), clean=True)
623 for f, value in d.items():
626 for f, value in d.items():
624 if value is None:
627 if value is None:
625 cleanp2.add(f)
628 cleanp2.add(f)
626 changes = [(f, rev) for f in files if f not in self.ignored]
629 changes = [(f, rev) for f in files if f not in self.ignored]
627 changes.sort()
630 changes.sort()
628 return changes, copies, cleanp2
631 return changes, copies, cleanp2
629
632
630 def _getcopies(self, ctx, parents, files):
633 def _getcopies(self, ctx, parents, files):
631 copies = {}
634 copies = {}
632 for name in files:
635 for name in files:
633 if name in self.ignored:
636 if name in self.ignored:
634 continue
637 continue
635 try:
638 try:
636 copysource = ctx.filectx(name).copysource()
639 copysource = ctx.filectx(name).copysource()
637 if copysource in self.ignored:
640 if copysource in self.ignored:
638 continue
641 continue
639 # Ignore copy sources not in parent revisions
642 # Ignore copy sources not in parent revisions
640 if not any(copysource in p for p in parents):
643 if not any(copysource in p for p in parents):
641 continue
644 continue
642 copies[name] = copysource
645 copies[name] = copysource
643 except TypeError:
646 except TypeError:
644 pass
647 pass
645 except error.LookupError as e:
648 except error.LookupError as e:
646 if not self.ignoreerrors:
649 if not self.ignoreerrors:
647 raise
650 raise
648 self.ignored.add(name)
651 self.ignored.add(name)
649 self.ui.warn(_(b'ignoring: %s\n') % e)
652 self.ui.warn(_(b'ignoring: %s\n') % e)
650 return copies
653 return copies
651
654
652 def getcommit(self, rev):
655 def getcommit(self, rev):
653 ctx = self._changectx(rev)
656 ctx = self._changectx(rev)
654 _parents = self._parents(ctx)
657 _parents = self._parents(ctx)
655 parents = [p.hex() for p in _parents]
658 parents = [p.hex() for p in _parents]
656 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
659 optparents = [p.hex() for p in ctx.parents() if p and p not in _parents]
657 crev = rev
660 crev = rev
658
661
659 return common.commit(
662 return common.commit(
660 author=ctx.user(),
663 author=ctx.user(),
661 date=dateutil.datestr(ctx.date(), b'%Y-%m-%d %H:%M:%S %1%2'),
664 date=dateutil.datestr(ctx.date(), b'%Y-%m-%d %H:%M:%S %1%2'),
662 desc=ctx.description(),
665 desc=ctx.description(),
663 rev=crev,
666 rev=crev,
664 parents=parents,
667 parents=parents,
665 optparents=optparents,
668 optparents=optparents,
666 branch=ctx.branch(),
669 branch=ctx.branch(),
667 extra=ctx.extra(),
670 extra=ctx.extra(),
668 sortkey=ctx.rev(),
671 sortkey=ctx.rev(),
669 saverev=self.saverev,
672 saverev=self.saverev,
670 phase=ctx.phase(),
673 phase=ctx.phase(),
671 ctx=ctx,
674 ctx=ctx,
672 )
675 )
673
676
674 def numcommits(self):
677 def numcommits(self):
675 return len(self.repo)
678 return len(self.repo)
676
679
677 def gettags(self):
680 def gettags(self):
678 # This will get written to .hgtags, filter non global tags out.
681 # This will get written to .hgtags, filter non global tags out.
679 tags = [
682 tags = [
680 t
683 t
681 for t in self.repo.tagslist()
684 for t in self.repo.tagslist()
682 if self.repo.tagtype(t[0]) == b'global'
685 if self.repo.tagtype(t[0]) == b'global'
683 ]
686 ]
684 return {name: hex(node) for name, node in tags if self.keep(node)}
687 return {name: hex(node) for name, node in tags if self.keep(node)}
685
688
686 def getchangedfiles(self, rev, i):
689 def getchangedfiles(self, rev, i):
687 ctx = self._changectx(rev)
690 ctx = self._changectx(rev)
688 parents = self._parents(ctx)
691 parents = self._parents(ctx)
689 if not parents and i is None:
692 if not parents and i is None:
690 i = 0
693 i = 0
691 ma, r = ctx.manifest().keys(), []
694 ma, r = ctx.manifest().keys(), []
692 else:
695 else:
693 i = i or 0
696 i = i or 0
694 ma, r = self._changedfiles(parents[i], ctx)
697 ma, r = self._changedfiles(parents[i], ctx)
695 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
698 ma, r = [[f for f in l if f not in self.ignored] for l in (ma, r)]
696
699
697 if i == 0:
700 if i == 0:
698 self._changescache = (rev, (ma, r))
701 self._changescache = (rev, (ma, r))
699
702
700 return ma + r
703 return ma + r
701
704
702 def converted(self, rev, destrev):
705 def converted(self, rev, destrev):
703 if self.convertfp is None:
706 if self.convertfp is None:
704 self.convertfp = open(self.repo.vfs.join(b'shamap'), b'ab')
707 self.convertfp = open(self.repo.vfs.join(b'shamap'), b'ab')
705 self.convertfp.write(util.tonativeeol(b'%s %s\n' % (destrev, rev)))
708 self.convertfp.write(util.tonativeeol(b'%s %s\n' % (destrev, rev)))
706 self.convertfp.flush()
709 self.convertfp.flush()
707
710
708 def before(self):
711 def before(self):
709 self.ui.debug(b'run hg source pre-conversion action\n')
712 self.ui.debug(b'run hg source pre-conversion action\n')
710
713
711 def after(self):
714 def after(self):
712 self.ui.debug(b'run hg source post-conversion action\n')
715 self.ui.debug(b'run hg source post-conversion action\n')
713
716
714 def hasnativeorder(self):
717 def hasnativeorder(self):
715 return True
718 return True
716
719
717 def hasnativeclose(self):
720 def hasnativeclose(self):
718 return True
721 return True
719
722
720 def lookuprev(self, rev):
723 def lookuprev(self, rev):
721 try:
724 try:
722 return hex(self.repo.lookup(rev))
725 return hex(self.repo.lookup(rev))
723 except (error.RepoError, error.LookupError):
726 except (error.RepoError, error.LookupError):
724 return None
727 return None
725
728
726 def getbookmarks(self):
729 def getbookmarks(self):
727 return bookmarks.listbookmarks(self.repo)
730 return bookmarks.listbookmarks(self.repo)
728
731
729 def checkrevformat(self, revstr, mapname=b'splicemap'):
732 def checkrevformat(self, revstr, mapname=b'splicemap'):
730 """Mercurial, revision string is a 40 byte hex"""
733 """Mercurial, revision string is a 40 byte hex"""
731 self.checkhexformat(revstr, mapname)
734 self.checkhexformat(revstr, mapname)
@@ -1,1724 +1,1730 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4
4
5 import codecs
5 import codecs
6 import locale
6 import locale
7 import os
7 import os
8 import pickle
8 import pickle
9 import re
9 import re
10 import xml.dom.minidom
10 import xml.dom.minidom
11
11
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13 from mercurial.pycompat import open
13 from mercurial.pycompat import open
14 from mercurial import (
14 from mercurial import (
15 encoding,
15 encoding,
16 error,
16 error,
17 pycompat,
17 pycompat,
18 util,
18 util,
19 vfs as vfsmod,
19 vfs as vfsmod,
20 )
20 )
21 from mercurial.utils import (
21 from mercurial.utils import (
22 dateutil,
22 dateutil,
23 procutil,
23 procutil,
24 stringutil,
24 stringutil,
25 )
25 )
26
26
27 from . import common
27 from . import common
28
28
29 stringio = util.stringio
29 stringio = util.stringio
30 propertycache = util.propertycache
30 propertycache = util.propertycache
31 urlerr = util.urlerr
31 urlerr = util.urlerr
32 urlreq = util.urlreq
32 urlreq = util.urlreq
33
33
34 commandline = common.commandline
34 commandline = common.commandline
35 commit = common.commit
35 commit = common.commit
36 converter_sink = common.converter_sink
36 converter_sink = common.converter_sink
37 converter_source = common.converter_source
37 converter_source = common.converter_source
38 decodeargs = common.decodeargs
38 decodeargs = common.decodeargs
39 encodeargs = common.encodeargs
39 encodeargs = common.encodeargs
40 makedatetimestamp = common.makedatetimestamp
40 makedatetimestamp = common.makedatetimestamp
41 mapfile = common.mapfile
41 mapfile = common.mapfile
42 MissingTool = common.MissingTool
42 MissingTool = common.MissingTool
43 NoRepo = common.NoRepo
43 NoRepo = common.NoRepo
44
44
45 # Subversion stuff. Works best with very recent Python SVN bindings
45 # Subversion stuff. Works best with very recent Python SVN bindings
46 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
46 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
47 # these bindings.
47 # these bindings.
48
48
49 try:
49 try:
50 # pytype: disable=import-error
50 # pytype: disable=import-error
51 import svn
51 import svn
52 import svn.client
52 import svn.client
53 import svn.core
53 import svn.core
54 import svn.ra
54 import svn.ra
55 import svn.delta
55 import svn.delta
56
56
57 # pytype: enable=import-error
57 # pytype: enable=import-error
58 from . import transport
58 from . import transport
59 import warnings
59 import warnings
60
60
61 warnings.filterwarnings(
61 warnings.filterwarnings(
62 'ignore', module='svn.core', category=DeprecationWarning
62 'ignore', module='svn.core', category=DeprecationWarning
63 )
63 )
64 svn.core.SubversionException # trigger import to catch error
64 svn.core.SubversionException # trigger import to catch error
65
65
66 except ImportError:
66 except ImportError:
67 svn = None
67 svn = None
68
68
69
69
70 # In Subversion, paths and URLs are Unicode (encoded as UTF-8), which
70 # In Subversion, paths and URLs are Unicode (encoded as UTF-8), which
71 # Subversion converts from / to native strings when interfacing with the OS.
71 # Subversion converts from / to native strings when interfacing with the OS.
72 # When passing paths and URLs to Subversion, we have to recode them such that
72 # When passing paths and URLs to Subversion, we have to recode them such that
73 # it roundstrips with what Subversion is doing.
73 # it roundstrips with what Subversion is doing.
74
74
75 fsencoding = None
75 fsencoding = None
76
76
77
77
78 def init_fsencoding():
78 def init_fsencoding():
79 global fsencoding, fsencoding_is_utf8
79 global fsencoding, fsencoding_is_utf8
80 if fsencoding is not None:
80 if fsencoding is not None:
81 return
81 return
82 if pycompat.iswindows:
82 if pycompat.iswindows:
83 # On Windows, filenames are Unicode, but we store them using the MBCS
83 # On Windows, filenames are Unicode, but we store them using the MBCS
84 # encoding.
84 # encoding.
85 fsencoding = 'mbcs'
85 fsencoding = 'mbcs'
86 else:
86 else:
87 # This is the encoding used to convert UTF-8 back to natively-encoded
87 # This is the encoding used to convert UTF-8 back to natively-encoded
88 # strings in Subversion 1.14.0 or earlier with APR 1.7.0 or earlier.
88 # strings in Subversion 1.14.0 or earlier with APR 1.7.0 or earlier.
89 with util.with_lc_ctype():
89 with util.with_lc_ctype():
90 fsencoding = locale.nl_langinfo(locale.CODESET) or 'ISO-8859-1'
90 fsencoding = locale.nl_langinfo(locale.CODESET) or 'ISO-8859-1'
91 fsencoding = codecs.lookup(fsencoding).name
91 fsencoding = codecs.lookup(fsencoding).name
92 fsencoding_is_utf8 = fsencoding == codecs.lookup('utf-8').name
92 fsencoding_is_utf8 = fsencoding == codecs.lookup('utf-8').name
93
93
94
94
95 def fs2svn(s):
95 def fs2svn(s):
96 if fsencoding_is_utf8:
96 if fsencoding_is_utf8:
97 return s
97 return s
98 else:
98 else:
99 return s.decode(fsencoding).encode('utf-8')
99 return s.decode(fsencoding).encode('utf-8')
100
100
101
101
102 def formatsvndate(date):
102 def formatsvndate(date):
103 return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
103 return dateutil.datestr(date, b'%Y-%m-%dT%H:%M:%S.000000Z')
104
104
105
105
106 def parsesvndate(s):
106 def parsesvndate(s):
107 # Example SVN datetime. Includes microseconds.
107 # Example SVN datetime. Includes microseconds.
108 # ISO-8601 conformant
108 # ISO-8601 conformant
109 # '2007-01-04T17:35:00.902377Z'
109 # '2007-01-04T17:35:00.902377Z'
110 return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
110 return dateutil.parsedate(s[:19] + b' UTC', [b'%Y-%m-%dT%H:%M:%S'])
111
111
112
112
113 class SvnPathNotFound(Exception):
113 class SvnPathNotFound(Exception):
114 pass
114 pass
115
115
116
116
117 def revsplit(rev):
117 def revsplit(rev):
118 """Parse a revision string and return (uuid, path, revnum).
118 """Parse a revision string and return (uuid, path, revnum).
119 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
119 >>> revsplit(b'svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
120 ... b'/proj%20B/mytrunk/mytrunk@1')
120 ... b'/proj%20B/mytrunk/mytrunk@1')
121 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
121 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
122 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
122 >>> revsplit(b'svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
123 ('', '', 1)
123 ('', '', 1)
124 >>> revsplit(b'@7')
124 >>> revsplit(b'@7')
125 ('', '', 7)
125 ('', '', 7)
126 >>> revsplit(b'7')
126 >>> revsplit(b'7')
127 ('', '', 0)
127 ('', '', 0)
128 >>> revsplit(b'bad')
128 >>> revsplit(b'bad')
129 ('', '', 0)
129 ('', '', 0)
130 """
130 """
131 parts = rev.rsplit(b'@', 1)
131 parts = rev.rsplit(b'@', 1)
132 revnum = 0
132 revnum = 0
133 if len(parts) > 1:
133 if len(parts) > 1:
134 revnum = int(parts[1])
134 revnum = int(parts[1])
135 parts = parts[0].split(b'/', 1)
135 parts = parts[0].split(b'/', 1)
136 uuid = b''
136 uuid = b''
137 mod = b''
137 mod = b''
138 if len(parts) > 1 and parts[0].startswith(b'svn:'):
138 if len(parts) > 1 and parts[0].startswith(b'svn:'):
139 uuid = parts[0][4:]
139 uuid = parts[0][4:]
140 mod = b'/' + parts[1]
140 mod = b'/' + parts[1]
141 return uuid, mod, revnum
141 return uuid, mod, revnum
142
142
143
143
144 def quote(s):
144 def quote(s):
145 # As of svn 1.7, many svn calls expect "canonical" paths. In
145 # As of svn 1.7, many svn calls expect "canonical" paths. In
146 # theory, we should call svn.core.*canonicalize() on all paths
146 # theory, we should call svn.core.*canonicalize() on all paths
147 # before passing them to the API. Instead, we assume the base url
147 # before passing them to the API. Instead, we assume the base url
148 # is canonical and copy the behaviour of svn URL encoding function
148 # is canonical and copy the behaviour of svn URL encoding function
149 # so we can extend it safely with new components. The "safe"
149 # so we can extend it safely with new components. The "safe"
150 # characters were taken from the "svn_uri__char_validity" table in
150 # characters were taken from the "svn_uri__char_validity" table in
151 # libsvn_subr/path.c.
151 # libsvn_subr/path.c.
152 return urlreq.quote(s, b"!$&'()*+,-./:=@_~")
152 return urlreq.quote(s, b"!$&'()*+,-./:=@_~")
153
153
154
154
155 def geturl(path):
155 def geturl(path):
156 """Convert path or URL to a SVN URL, encoded in UTF-8.
156 """Convert path or URL to a SVN URL, encoded in UTF-8.
157
157
158 This can raise UnicodeDecodeError if the path or URL can't be converted to
158 This can raise UnicodeDecodeError if the path or URL can't be converted to
159 unicode using `fsencoding`.
159 unicode using `fsencoding`.
160 """
160 """
161 try:
161 try:
162 return svn.client.url_from_path(
162 return svn.client.url_from_path(
163 svn.core.svn_path_canonicalize(fs2svn(path))
163 svn.core.svn_path_canonicalize(fs2svn(path))
164 )
164 )
165 except svn.core.SubversionException:
165 except svn.core.SubversionException:
166 # svn.client.url_from_path() fails with local repositories
166 # svn.client.url_from_path() fails with local repositories
167 pass
167 pass
168 if os.path.isdir(path):
168 if os.path.isdir(path):
169 path = os.path.normpath(util.abspath(path))
169 path = os.path.normpath(util.abspath(path))
170 if pycompat.iswindows:
170 if pycompat.iswindows:
171 path = b'/' + util.normpath(path)
171 path = b'/' + util.normpath(path)
172 # Module URL is later compared with the repository URL returned
172 # Module URL is later compared with the repository URL returned
173 # by svn API, which is UTF-8.
173 # by svn API, which is UTF-8.
174 path = fs2svn(path)
174 path = fs2svn(path)
175 path = b'file://%s' % quote(path)
175 path = b'file://%s' % quote(path)
176 return svn.core.svn_path_canonicalize(path)
176 return svn.core.svn_path_canonicalize(path)
177
177
178
178
179 def optrev(number):
179 def optrev(number):
180 optrev = svn.core.svn_opt_revision_t()
180 optrev = svn.core.svn_opt_revision_t()
181 optrev.kind = svn.core.svn_opt_revision_number
181 optrev.kind = svn.core.svn_opt_revision_number
182 optrev.value.number = number
182 optrev.value.number = number
183 return optrev
183 return optrev
184
184
185
185
186 class changedpath:
186 class changedpath:
187 def __init__(self, p):
187 def __init__(self, p):
188 self.copyfrom_path = p.copyfrom_path
188 self.copyfrom_path = p.copyfrom_path
189 self.copyfrom_rev = p.copyfrom_rev
189 self.copyfrom_rev = p.copyfrom_rev
190 self.action = p.action
190 self.action = p.action
191
191
192
192
193 def get_log_child(
193 def get_log_child(
194 fp,
194 fp,
195 url,
195 url,
196 paths,
196 paths,
197 start,
197 start,
198 end,
198 end,
199 limit=0,
199 limit=0,
200 discover_changed_paths=True,
200 discover_changed_paths=True,
201 strict_node_history=False,
201 strict_node_history=False,
202 ):
202 ):
203 protocol = -1
203 protocol = -1
204
204
205 def receiver(orig_paths, revnum, author, date, message, pool):
205 def receiver(orig_paths, revnum, author, date, message, pool):
206 paths = {}
206 paths = {}
207 if orig_paths is not None:
207 if orig_paths is not None:
208 for k, v in orig_paths.items():
208 for k, v in orig_paths.items():
209 paths[k] = changedpath(v)
209 paths[k] = changedpath(v)
210 pickle.dump((paths, revnum, author, date, message), fp, protocol)
210 pickle.dump((paths, revnum, author, date, message), fp, protocol)
211
211
212 try:
212 try:
213 # Use an ra of our own so that our parent can consume
213 # Use an ra of our own so that our parent can consume
214 # our results without confusing the server.
214 # our results without confusing the server.
215 t = transport.SvnRaTransport(url=url)
215 t = transport.SvnRaTransport(url=url)
216 svn.ra.get_log(
216 svn.ra.get_log(
217 t.ra,
217 t.ra,
218 paths,
218 paths,
219 start,
219 start,
220 end,
220 end,
221 limit,
221 limit,
222 discover_changed_paths,
222 discover_changed_paths,
223 strict_node_history,
223 strict_node_history,
224 receiver,
224 receiver,
225 )
225 )
226 except IOError:
226 except IOError:
227 # Caller may interrupt the iteration
227 # Caller may interrupt the iteration
228 pickle.dump(None, fp, protocol)
228 pickle.dump(None, fp, protocol)
229 except Exception as inst:
229 except Exception as inst:
230 pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
230 pickle.dump(stringutil.forcebytestr(inst), fp, protocol)
231 else:
231 else:
232 pickle.dump(None, fp, protocol)
232 pickle.dump(None, fp, protocol)
233 fp.flush()
233 fp.flush()
234 # With large history, cleanup process goes crazy and suddenly
234 # With large history, cleanup process goes crazy and suddenly
235 # consumes *huge* amount of memory. The output file being closed,
235 # consumes *huge* amount of memory. The output file being closed,
236 # there is no need for clean termination.
236 # there is no need for clean termination.
237 os._exit(0)
237 os._exit(0)
238
238
239
239
240 def debugsvnlog(ui, **opts):
240 def debugsvnlog(ui, **opts):
241 """Fetch SVN log in a subprocess and channel them back to parent to
241 """Fetch SVN log in a subprocess and channel them back to parent to
242 avoid memory collection issues.
242 avoid memory collection issues.
243 """
243 """
244 with util.with_lc_ctype():
244 with util.with_lc_ctype():
245 if svn is None:
245 if svn is None:
246 raise error.Abort(
246 raise error.Abort(
247 _(b'debugsvnlog could not load Subversion python bindings')
247 _(b'debugsvnlog could not load Subversion python bindings')
248 )
248 )
249
249
250 args = decodeargs(ui.fin.read())
250 args = decodeargs(ui.fin.read())
251 get_log_child(ui.fout, *args)
251 get_log_child(ui.fout, *args)
252
252
253
253
254 class logstream:
254 class logstream:
255 """Interruptible revision log iterator."""
255 """Interruptible revision log iterator."""
256
256
257 def __init__(self, stdout):
257 def __init__(self, stdout):
258 self._stdout = stdout
258 self._stdout = stdout
259
259
260 def __iter__(self):
260 def __iter__(self):
261 while True:
261 while True:
262 try:
262 try:
263 entry = pickle.load(self._stdout)
263 entry = pickle.load(self._stdout)
264 except EOFError:
264 except EOFError:
265 raise error.Abort(
265 raise error.Abort(
266 _(
266 _(
267 b'Mercurial failed to run itself, check'
267 b'Mercurial failed to run itself, check'
268 b' hg executable is in PATH'
268 b' hg executable is in PATH'
269 )
269 )
270 )
270 )
271 try:
271 try:
272 orig_paths, revnum, author, date, message = entry
272 orig_paths, revnum, author, date, message = entry
273 except (TypeError, ValueError):
273 except (TypeError, ValueError):
274 if entry is None:
274 if entry is None:
275 break
275 break
276 raise error.Abort(_(b"log stream exception '%s'") % entry)
276 raise error.Abort(_(b"log stream exception '%s'") % entry)
277 yield entry
277 yield entry
278
278
279 def close(self):
279 def close(self):
280 if self._stdout:
280 if self._stdout:
281 self._stdout.close()
281 self._stdout.close()
282 self._stdout = None
282 self._stdout = None
283
283
284
284
285 class directlogstream(list):
285 class directlogstream(list):
286 """Direct revision log iterator.
286 """Direct revision log iterator.
287 This can be used for debugging and development but it will probably leak
287 This can be used for debugging and development but it will probably leak
288 memory and is not suitable for real conversions."""
288 memory and is not suitable for real conversions."""
289
289
290 def __init__(
290 def __init__(
291 self,
291 self,
292 url,
292 url,
293 paths,
293 paths,
294 start,
294 start,
295 end,
295 end,
296 limit=0,
296 limit=0,
297 discover_changed_paths=True,
297 discover_changed_paths=True,
298 strict_node_history=False,
298 strict_node_history=False,
299 ):
299 ):
300 def receiver(orig_paths, revnum, author, date, message, pool):
300 def receiver(orig_paths, revnum, author, date, message, pool):
301 paths = {}
301 paths = {}
302 if orig_paths is not None:
302 if orig_paths is not None:
303 for k, v in orig_paths.items():
303 for k, v in orig_paths.items():
304 paths[k] = changedpath(v)
304 paths[k] = changedpath(v)
305 self.append((paths, revnum, author, date, message))
305 self.append((paths, revnum, author, date, message))
306
306
307 # Use an ra of our own so that our parent can consume
307 # Use an ra of our own so that our parent can consume
308 # our results without confusing the server.
308 # our results without confusing the server.
309 t = transport.SvnRaTransport(url=url)
309 t = transport.SvnRaTransport(url=url)
310 svn.ra.get_log(
310 svn.ra.get_log(
311 t.ra,
311 t.ra,
312 paths,
312 paths,
313 start,
313 start,
314 end,
314 end,
315 limit,
315 limit,
316 discover_changed_paths,
316 discover_changed_paths,
317 strict_node_history,
317 strict_node_history,
318 receiver,
318 receiver,
319 )
319 )
320
320
321 def close(self):
321 def close(self):
322 pass
322 pass
323
323
324
324
325 # Check to see if the given path is a local Subversion repo. Verify this by
325 # Check to see if the given path is a local Subversion repo. Verify this by
326 # looking for several svn-specific files and directories in the given
326 # looking for several svn-specific files and directories in the given
327 # directory.
327 # directory.
328 def filecheck(ui, path, proto):
328 def filecheck(ui, path, proto):
329 for x in (b'locks', b'hooks', b'format', b'db'):
329 for x in (b'locks', b'hooks', b'format', b'db'):
330 if not os.path.exists(os.path.join(path, x)):
330 if not os.path.exists(os.path.join(path, x)):
331 return False
331 return False
332 return True
332 return True
333
333
334
334
335 # Check to see if a given path is the root of an svn repo over http. We verify
335 # Check to see if a given path is the root of an svn repo over http. We verify
336 # this by requesting a version-controlled URL we know can't exist and looking
336 # this by requesting a version-controlled URL we know can't exist and looking
337 # for the svn-specific "not found" XML.
337 # for the svn-specific "not found" XML.
338 def httpcheck(ui, path, proto):
338 def httpcheck(ui, path, proto):
339 try:
339 try:
340 opener = urlreq.buildopener()
340 opener = urlreq.buildopener()
341 rsp = opener.open(
341 rsp = opener.open(
342 pycompat.strurl(b'%s://%s/!svn/ver/0/.svn' % (proto, path)), b'rb'
342 pycompat.strurl(b'%s://%s/!svn/ver/0/.svn' % (proto, path)), b'rb'
343 )
343 )
344 data = rsp.read()
344 data = rsp.read()
345 except urlerr.httperror as inst:
345 except urlerr.httperror as inst:
346 if inst.code != 404:
346 if inst.code != 404:
347 # Except for 404 we cannot know for sure this is not an svn repo
347 # Except for 404 we cannot know for sure this is not an svn repo
348 ui.warn(
348 ui.warn(
349 _(
349 _(
350 b'svn: cannot probe remote repository, assume it could '
350 b'svn: cannot probe remote repository, assume it could '
351 b'be a subversion repository. Use --source-type if you '
351 b'be a subversion repository. Use --source-type if you '
352 b'know better.\n'
352 b'know better.\n'
353 )
353 )
354 )
354 )
355 return True
355 return True
356 data = inst.fp.read()
356 data = inst.fp.read()
357 except Exception:
357 except Exception:
358 # Could be urlerr.urlerror if the URL is invalid or anything else.
358 # Could be urlerr.urlerror if the URL is invalid or anything else.
359 return False
359 return False
360 return b'<m:human-readable errcode="160013">' in data
360 return b'<m:human-readable errcode="160013">' in data
361
361
362
362
363 protomap = {
363 protomap = {
364 b'http': httpcheck,
364 b'http': httpcheck,
365 b'https': httpcheck,
365 b'https': httpcheck,
366 b'file': filecheck,
366 b'file': filecheck,
367 }
367 }
368
368
369
369
370 def issvnurl(ui, url):
370 def issvnurl(ui, url):
371 try:
371 try:
372 proto, path = url.split(b'://', 1)
372 proto, path = url.split(b'://', 1)
373 if proto == b'file':
373 if proto == b'file':
374 if (
374 if (
375 pycompat.iswindows
375 pycompat.iswindows
376 and path[:1] == b'/'
376 and path[:1] == b'/'
377 and path[1:2].isalpha()
377 and path[1:2].isalpha()
378 and path[2:6].lower() == b'%3a/'
378 and path[2:6].lower() == b'%3a/'
379 ):
379 ):
380 path = path[:2] + b':/' + path[6:]
380 path = path[:2] + b':/' + path[6:]
381 try:
381 try:
382 unicodepath = path.decode(fsencoding)
382 unicodepath = path.decode(fsencoding)
383 except UnicodeDecodeError:
383 except UnicodeDecodeError:
384 ui.warn(
384 ui.warn(
385 _(
385 _(
386 b'Subversion requires that file URLs can be converted '
386 b'Subversion requires that file URLs can be converted '
387 b'to Unicode using the current locale encoding (%s)\n'
387 b'to Unicode using the current locale encoding (%s)\n'
388 )
388 )
389 % pycompat.sysbytes(fsencoding)
389 % pycompat.sysbytes(fsencoding)
390 )
390 )
391 return False
391 return False
392
392
393 # Subversion paths are Unicode. Since it does percent-decoding on
393 # Subversion paths are Unicode. Since it does percent-decoding on
394 # UTF-8-encoded strings, percent-encoded bytes are interpreted as
394 # UTF-8-encoded strings, percent-encoded bytes are interpreted as
395 # UTF-8.
395 # UTF-8.
396 # On Python 3, we have to pass unicode to urlreq.url2pathname().
396 # On Python 3, we have to pass unicode to urlreq.url2pathname().
397 # Percent-decoded bytes get decoded using UTF-8 and the 'replace'
397 # Percent-decoded bytes get decoded using UTF-8 and the 'replace'
398 # error handler.
398 # error handler.
399 unicodepath = urlreq.url2pathname(unicodepath)
399 unicodepath = urlreq.url2pathname(unicodepath)
400 if u'\N{REPLACEMENT CHARACTER}' in unicodepath:
400 if u'\N{REPLACEMENT CHARACTER}' in unicodepath:
401 ui.warn(
401 ui.warn(
402 _(
402 _(
403 b'Subversion does not support non-UTF-8 '
403 b'Subversion does not support non-UTF-8 '
404 b'percent-encoded bytes in file URLs\n'
404 b'percent-encoded bytes in file URLs\n'
405 )
405 )
406 )
406 )
407 return False
407 return False
408
408
409 # Below, we approximate how Subversion checks the path. On Unix, we
409 # Below, we approximate how Subversion checks the path. On Unix, we
410 # should therefore convert the path to bytes using `fsencoding`
410 # should therefore convert the path to bytes using `fsencoding`
411 # (like Subversion does). On Windows, the right thing would
411 # (like Subversion does). On Windows, the right thing would
412 # actually be to leave the path as unicode. For now, we restrict
412 # actually be to leave the path as unicode. For now, we restrict
413 # the path to MBCS.
413 # the path to MBCS.
414 path = unicodepath.encode(fsencoding)
414 path = unicodepath.encode(fsencoding)
415 except ValueError:
415 except ValueError:
416 proto = b'file'
416 proto = b'file'
417 path = util.abspath(url)
417 path = util.abspath(url)
418 try:
418 try:
419 path.decode(fsencoding)
419 path.decode(fsencoding)
420 except UnicodeDecodeError:
420 except UnicodeDecodeError:
421 ui.warn(
421 ui.warn(
422 _(
422 _(
423 b'Subversion requires that paths can be converted to '
423 b'Subversion requires that paths can be converted to '
424 b'Unicode using the current locale encoding (%s)\n'
424 b'Unicode using the current locale encoding (%s)\n'
425 )
425 )
426 % pycompat.sysbytes(fsencoding)
426 % pycompat.sysbytes(fsencoding)
427 )
427 )
428 return False
428 return False
429 if proto == b'file':
429 if proto == b'file':
430 path = util.pconvert(path)
430 path = util.pconvert(path)
431 elif proto in (b'http', 'https'):
431 elif proto in (b'http', 'https'):
432 if not encoding.isasciistr(path):
432 if not encoding.isasciistr(path):
433 ui.warn(
433 ui.warn(
434 _(
434 _(
435 b"Subversion sources don't support non-ASCII characters in "
435 b"Subversion sources don't support non-ASCII characters in "
436 b"HTTP(S) URLs. Please percent-encode them.\n"
436 b"HTTP(S) URLs. Please percent-encode them.\n"
437 )
437 )
438 )
438 )
439 return False
439 return False
440 check = protomap.get(proto, lambda *args: False)
440 check = protomap.get(proto, lambda *args: False)
441 while b'/' in path:
441 while b'/' in path:
442 if check(ui, path, proto):
442 if check(ui, path, proto):
443 return True
443 return True
444 path = path.rsplit(b'/', 1)[0]
444 path = path.rsplit(b'/', 1)[0]
445 return False
445 return False
446
446
447
447
448 # SVN conversion code stolen from bzr-svn and tailor
448 # SVN conversion code stolen from bzr-svn and tailor
449 #
449 #
450 # Subversion looks like a versioned filesystem, branches structures
450 # Subversion looks like a versioned filesystem, branches structures
451 # are defined by conventions and not enforced by the tool. First,
451 # are defined by conventions and not enforced by the tool. First,
452 # we define the potential branches (modules) as "trunk" and "branches"
452 # we define the potential branches (modules) as "trunk" and "branches"
453 # children directories. Revisions are then identified by their
453 # children directories. Revisions are then identified by their
454 # module and revision number (and a repository identifier).
454 # module and revision number (and a repository identifier).
455 #
455 #
456 # The revision graph is really a tree (or a forest). By default, a
456 # The revision graph is really a tree (or a forest). By default, a
457 # revision parent is the previous revision in the same module. If the
457 # revision parent is the previous revision in the same module. If the
458 # module directory is copied/moved from another module then the
458 # module directory is copied/moved from another module then the
459 # revision is the module root and its parent the source revision in
459 # revision is the module root and its parent the source revision in
460 # the parent module. A revision has at most one parent.
460 # the parent module. A revision has at most one parent.
461 #
461 #
462 class svn_source(converter_source):
462 class svn_source(converter_source):
463 def __init__(self, ui, repotype, url, revs=None):
463 def __init__(self, ui, repotype, url, revs=None):
464 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
464 super(svn_source, self).__init__(ui, repotype, url, revs=revs)
465
465
466 init_fsencoding()
466 init_fsencoding()
467 if not (
467 if not (
468 url.startswith(b'svn://')
468 url.startswith(b'svn://')
469 or url.startswith(b'svn+ssh://')
469 or url.startswith(b'svn+ssh://')
470 or (
470 or (
471 os.path.exists(url)
471 os.path.exists(url)
472 and os.path.exists(os.path.join(url, b'.svn'))
472 and os.path.exists(os.path.join(url, b'.svn'))
473 )
473 )
474 or issvnurl(ui, url)
474 or issvnurl(ui, url)
475 ):
475 ):
476 raise NoRepo(
476 raise NoRepo(
477 _(b"%s does not look like a Subversion repository") % url
477 _(b"%s does not look like a Subversion repository") % url
478 )
478 )
479 if svn is None:
479 if svn is None:
480 raise MissingTool(_(b'could not load Subversion python bindings'))
480 raise MissingTool(_(b'could not load Subversion python bindings'))
481
481
482 try:
482 try:
483 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
483 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
484 if version < (1, 4):
484 if version < (1, 4):
485 raise MissingTool(
485 raise MissingTool(
486 _(
486 _(
487 b'Subversion python bindings %d.%d found, '
487 b'Subversion python bindings %d.%d found, '
488 b'1.4 or later required'
488 b'1.4 or later required'
489 )
489 )
490 % version
490 % version
491 )
491 )
492 except AttributeError:
492 except AttributeError:
493 raise MissingTool(
493 raise MissingTool(
494 _(
494 _(
495 b'Subversion python bindings are too old, 1.4 '
495 b'Subversion python bindings are too old, 1.4 '
496 b'or later required'
496 b'or later required'
497 )
497 )
498 )
498 )
499
499
500 self.lastrevs = {}
500 self.lastrevs = {}
501
501
502 latest = None
502 latest = None
503 try:
503 try:
504 # Support file://path@rev syntax. Useful e.g. to convert
504 # Support file://path@rev syntax. Useful e.g. to convert
505 # deleted branches.
505 # deleted branches.
506 at = url.rfind(b'@')
506 at = url.rfind(b'@')
507 if at >= 0:
507 if at >= 0:
508 latest = int(url[at + 1 :])
508 latest = int(url[at + 1 :])
509 url = url[:at]
509 url = url[:at]
510 except ValueError:
510 except ValueError:
511 pass
511 pass
512 self.url = geturl(url)
512 self.url = geturl(url)
513 self.encoding = b'UTF-8' # Subversion is always nominal UTF-8
513 self.encoding = b'UTF-8' # Subversion is always nominal UTF-8
514 try:
514 try:
515 with util.with_lc_ctype():
515 with util.with_lc_ctype():
516 self.transport = transport.SvnRaTransport(url=self.url)
516 self.transport = transport.SvnRaTransport(url=self.url)
517 self.ra = self.transport.ra
517 self.ra = self.transport.ra
518 self.ctx = self.transport.client
518 self.ctx = self.transport.client
519 self.baseurl = svn.ra.get_repos_root(self.ra)
519 self.baseurl = svn.ra.get_repos_root(self.ra)
520 # Module is either empty or a repository path starting with
520 # Module is either empty or a repository path starting with
521 # a slash and not ending with a slash.
521 # a slash and not ending with a slash.
522 self.module = urlreq.unquote(self.url[len(self.baseurl) :])
522 self.module = urlreq.unquote(self.url[len(self.baseurl) :])
523 self.prevmodule = None
523 self.prevmodule = None
524 self.rootmodule = self.module
524 self.rootmodule = self.module
525 self.commits = {}
525 self.commits = {}
526 self.paths = {}
526 self.paths = {}
527 self.uuid = svn.ra.get_uuid(self.ra)
527 self.uuid = svn.ra.get_uuid(self.ra)
528 except svn.core.SubversionException:
528 except svn.core.SubversionException:
529 ui.traceback()
529 ui.traceback()
530 svnversion = b'%d.%d.%d' % (
530 svnversion = b'%d.%d.%d' % (
531 svn.core.SVN_VER_MAJOR,
531 svn.core.SVN_VER_MAJOR,
532 svn.core.SVN_VER_MINOR,
532 svn.core.SVN_VER_MINOR,
533 svn.core.SVN_VER_MICRO,
533 svn.core.SVN_VER_MICRO,
534 )
534 )
535 raise NoRepo(
535 raise NoRepo(
536 _(
536 _(
537 b"%s does not look like a Subversion repository "
537 b"%s does not look like a Subversion repository "
538 b"to libsvn version %s"
538 b"to libsvn version %s"
539 )
539 )
540 % (self.url, svnversion)
540 % (self.url, svnversion)
541 )
541 )
542
542
543 if revs:
543 if revs:
544 if len(revs) > 1:
544 if len(revs) > 1:
545 raise error.Abort(
545 raise error.Abort(
546 _(
546 _(
547 b'subversion source does not support '
547 b'subversion source does not support '
548 b'specifying multiple revisions'
548 b'specifying multiple revisions'
549 )
549 )
550 )
550 )
551 try:
551 try:
552 latest = int(revs[0])
552 latest = int(revs[0])
553 except ValueError:
553 except ValueError:
554 raise error.Abort(
554 raise error.Abort(
555 _(b'svn: revision %s is not an integer') % revs[0]
555 _(b'svn: revision %s is not an integer') % revs[0]
556 )
556 )
557
557
558 trunkcfg = self.ui.config(b'convert', b'svn.trunk')
558 trunkcfg = self.ui.config(b'convert', b'svn.trunk')
559 if trunkcfg is None:
559 if trunkcfg is None:
560 trunkcfg = b'trunk'
560 trunkcfg = b'trunk'
561 self.trunkname = trunkcfg.strip(b'/')
561 self.trunkname = trunkcfg.strip(b'/')
562 self.startrev = self.ui.config(b'convert', b'svn.startrev')
562 self.startrev = self.ui.config(b'convert', b'svn.startrev')
563 try:
563 try:
564 self.startrev = int(self.startrev)
564 self.startrev = int(self.startrev)
565 if self.startrev < 0:
565 if self.startrev < 0:
566 self.startrev = 0
566 self.startrev = 0
567 except ValueError:
567 except ValueError:
568 raise error.Abort(
568 raise error.Abort(
569 _(b'svn: start revision %s is not an integer') % self.startrev
569 _(b'svn: start revision %s is not an integer') % self.startrev
570 )
570 )
571
571
572 try:
572 try:
573 with util.with_lc_ctype():
573 with util.with_lc_ctype():
574 self.head = self.latest(self.module, latest)
574 self.head = self.latest(self.module, latest)
575 except SvnPathNotFound:
575 except SvnPathNotFound:
576 self.head = None
576 self.head = None
577 if not self.head:
577 if not self.head:
578 raise error.Abort(
578 raise error.Abort(
579 _(b'no revision found in module %s') % self.module
579 _(b'no revision found in module %s') % self.module
580 )
580 )
581 self.last_changed = self.revnum(self.head)
581 self.last_changed = self.revnum(self.head)
582
582
583 self._changescache = (None, None)
583 self._changescache = (None, None)
584
584
585 if os.path.exists(os.path.join(url, b'.svn/entries')):
585 if os.path.exists(os.path.join(url, b'.svn/entries')):
586 self.wc = url
586 self.wc = url
587 else:
587 else:
588 self.wc = None
588 self.wc = None
589 self.convertfp = None
589 self.convertfp = None
590
590
591 def before(self):
591 def before(self):
592 self.with_lc_ctype = util.with_lc_ctype()
592 self.with_lc_ctype = util.with_lc_ctype()
593 self.with_lc_ctype.__enter__()
593 self.with_lc_ctype.__enter__()
594
594
595 def after(self):
595 def after(self):
596 self.with_lc_ctype.__exit__(None, None, None)
596 self.with_lc_ctype.__exit__(None, None, None)
597
597
598 def setrevmap(self, revmap):
598 def setrevmap(self, revmap):
599 lastrevs = {}
599 lastrevs = {}
600 for revid in revmap:
600 for revid in revmap:
601 uuid, module, revnum = revsplit(revid)
601 uuid, module, revnum = revsplit(revid)
602 lastrevnum = lastrevs.setdefault(module, revnum)
602 lastrevnum = lastrevs.setdefault(module, revnum)
603 if revnum > lastrevnum:
603 if revnum > lastrevnum:
604 lastrevs[module] = revnum
604 lastrevs[module] = revnum
605 self.lastrevs = lastrevs
605 self.lastrevs = lastrevs
606
606
607 def exists(self, path, optrev):
607 def exists(self, path, optrev):
608 try:
608 try:
609 svn.client.ls(
609 svn.client.ls(
610 self.url.rstrip(b'/') + b'/' + quote(path),
610 self.url.rstrip(b'/') + b'/' + quote(path),
611 optrev,
611 optrev,
612 False,
612 False,
613 self.ctx,
613 self.ctx,
614 )
614 )
615 return True
615 return True
616 except svn.core.SubversionException:
616 except svn.core.SubversionException:
617 return False
617 return False
618
618
619 def getheads(self):
619 def getheads(self):
620 def isdir(path, revnum):
620 def isdir(path, revnum):
621 kind = self._checkpath(path, revnum)
621 kind = self._checkpath(path, revnum)
622 return kind == svn.core.svn_node_dir
622 return kind == svn.core.svn_node_dir
623
623
624 def getcfgpath(name, rev):
624 def getcfgpath(name, rev):
625 cfgpath = self.ui.config(b'convert', b'svn.' + name)
625 cfgpath = self.ui.config(b'convert', b'svn.' + name)
626 if cfgpath is not None and cfgpath.strip() == b'':
626 if cfgpath is not None and cfgpath.strip() == b'':
627 return None
627 return None
628 path = (cfgpath or name).strip(b'/')
628 path = (cfgpath or name).strip(b'/')
629 if not self.exists(path, rev):
629 if not self.exists(path, rev):
630 if self.module.endswith(path) and name == b'trunk':
630 if self.module.endswith(path) and name == b'trunk':
631 # we are converting from inside this directory
631 # we are converting from inside this directory
632 return None
632 return None
633 if cfgpath:
633 if cfgpath:
634 raise error.Abort(
634 raise error.Abort(
635 _(b'expected %s to be at %r, but not found')
635 _(b'expected %s to be at %r, but not found')
636 % (name, path)
636 % (name, path)
637 )
637 )
638 return None
638 return None
639 self.ui.note(
639 self.ui.note(
640 _(b'found %s at %r\n') % (name, pycompat.bytestr(path))
640 _(b'found %s at %r\n') % (name, pycompat.bytestr(path))
641 )
641 )
642 return path
642 return path
643
643
644 rev = optrev(self.last_changed)
644 rev = optrev(self.last_changed)
645 oldmodule = b''
645 oldmodule = b''
646 trunk = getcfgpath(b'trunk', rev)
646 trunk = getcfgpath(b'trunk', rev)
647 self.tags = getcfgpath(b'tags', rev)
647 self.tags = getcfgpath(b'tags', rev)
648 branches = getcfgpath(b'branches', rev)
648 branches = getcfgpath(b'branches', rev)
649
649
650 # If the project has a trunk or branches, we will extract heads
650 # If the project has a trunk or branches, we will extract heads
651 # from them. We keep the project root otherwise.
651 # from them. We keep the project root otherwise.
652 if trunk:
652 if trunk:
653 oldmodule = self.module or b''
653 oldmodule = self.module or b''
654 self.module += b'/' + trunk
654 self.module += b'/' + trunk
655 self.head = self.latest(self.module, self.last_changed)
655 self.head = self.latest(self.module, self.last_changed)
656 if not self.head:
656 if not self.head:
657 raise error.Abort(
657 raise error.Abort(
658 _(b'no revision found in module %s') % self.module
658 _(b'no revision found in module %s') % self.module
659 )
659 )
660
660
661 # First head in the list is the module's head
661 # First head in the list is the module's head
662 self.heads = [self.head]
662 self.heads = [self.head]
663 if self.tags is not None:
663 if self.tags is not None:
664 self.tags = b'%s/%s' % (oldmodule, (self.tags or b'tags'))
664 self.tags = b'%s/%s' % (oldmodule, (self.tags or b'tags'))
665
665
666 # Check if branches bring a few more heads to the list
666 # Check if branches bring a few more heads to the list
667 if branches:
667 if branches:
668 rpath = self.url.strip(b'/')
668 rpath = self.url.strip(b'/')
669 branchnames = svn.client.ls(
669 branchnames = svn.client.ls(
670 rpath + b'/' + quote(branches), rev, False, self.ctx
670 rpath + b'/' + quote(branches), rev, False, self.ctx
671 )
671 )
672 for branch in sorted(branchnames):
672 for branch in sorted(branchnames):
673 module = b'%s/%s/%s' % (oldmodule, branches, branch)
673 module = b'%s/%s/%s' % (oldmodule, branches, branch)
674 if not isdir(module, self.last_changed):
674 if not isdir(module, self.last_changed):
675 continue
675 continue
676 brevid = self.latest(module, self.last_changed)
676 brevid = self.latest(module, self.last_changed)
677 if not brevid:
677 if not brevid:
678 self.ui.note(_(b'ignoring empty branch %s\n') % branch)
678 self.ui.note(_(b'ignoring empty branch %s\n') % branch)
679 continue
679 continue
680 self.ui.note(
680 self.ui.note(
681 _(b'found branch %s at %d\n')
681 _(b'found branch %s at %d\n')
682 % (branch, self.revnum(brevid))
682 % (branch, self.revnum(brevid))
683 )
683 )
684 self.heads.append(brevid)
684 self.heads.append(brevid)
685
685
686 if self.startrev and self.heads:
686 if self.startrev and self.heads:
687 if len(self.heads) > 1:
687 if len(self.heads) > 1:
688 raise error.Abort(
688 raise error.Abort(
689 _(
689 _(
690 b'svn: start revision is not supported '
690 b'svn: start revision is not supported '
691 b'with more than one branch'
691 b'with more than one branch'
692 )
692 )
693 )
693 )
694 revnum = self.revnum(self.heads[0])
694 revnum = self.revnum(self.heads[0])
695 if revnum < self.startrev:
695 if revnum < self.startrev:
696 raise error.Abort(
696 raise error.Abort(
697 _(b'svn: no revision found after start revision %d')
697 _(b'svn: no revision found after start revision %d')
698 % self.startrev
698 % self.startrev
699 )
699 )
700
700
701 return self.heads
701 return self.heads
702
702
703 def _getchanges(self, rev, full):
703 def _getchanges(self, rev, full):
704 (paths, parents) = self.paths[rev]
704 (paths, parents) = self.paths[rev]
705 copies = {}
705 copies = {}
706 if parents:
706 if parents:
707 files, self.removed, copies = self.expandpaths(rev, paths, parents)
707 files, self.removed, copies = self.expandpaths(rev, paths, parents)
708 if full or not parents:
708 if full or not parents:
709 # Perform a full checkout on roots
709 # Perform a full checkout on roots
710 uuid, module, revnum = revsplit(rev)
710 uuid, module, revnum = revsplit(rev)
711 entries = svn.client.ls(
711 entries = svn.client.ls(
712 self.baseurl + quote(module), optrev(revnum), True, self.ctx
712 self.baseurl + quote(module), optrev(revnum), True, self.ctx
713 )
713 )
714 files = [
714 files = [
715 n
715 n
716 for n, e in entries.items()
716 for n, e in entries.items()
717 if e.kind == svn.core.svn_node_file
717 if e.kind == svn.core.svn_node_file
718 ]
718 ]
719 self.removed = set()
719 self.removed = set()
720
720
721 files.sort()
721 files.sort()
722 files = pycompat.ziplist(files, [rev] * len(files))
722 files = pycompat.ziplist(files, [rev] * len(files))
723 return (files, copies)
723 return (files, copies)
724
724
725 def getchanges(self, rev, full):
725 def getchanges(self, rev, full):
726 # reuse cache from getchangedfiles
726 # reuse cache from getchangedfiles
727 if self._changescache[0] == rev and not full:
727 if self._changescache[0] == rev and not full:
728 # TODO: add type hints to avoid this warning, instead of
729 # suppressing it:
730 # No attribute '__iter__' on None [attribute-error]
731
732 # pytype: disable=attribute-error
728 (files, copies) = self._changescache[1]
733 (files, copies) = self._changescache[1]
734 # pytype: enable=attribute-error
729 else:
735 else:
730 (files, copies) = self._getchanges(rev, full)
736 (files, copies) = self._getchanges(rev, full)
731 # caller caches the result, so free it here to release memory
737 # caller caches the result, so free it here to release memory
732 del self.paths[rev]
738 del self.paths[rev]
733 return (files, copies, set())
739 return (files, copies, set())
734
740
735 def getchangedfiles(self, rev, i):
741 def getchangedfiles(self, rev, i):
736 # called from filemap - cache computed values for reuse in getchanges
742 # called from filemap - cache computed values for reuse in getchanges
737 (files, copies) = self._getchanges(rev, False)
743 (files, copies) = self._getchanges(rev, False)
738 self._changescache = (rev, (files, copies))
744 self._changescache = (rev, (files, copies))
739 return [f[0] for f in files]
745 return [f[0] for f in files]
740
746
741 def getcommit(self, rev):
747 def getcommit(self, rev):
742 if rev not in self.commits:
748 if rev not in self.commits:
743 uuid, module, revnum = revsplit(rev)
749 uuid, module, revnum = revsplit(rev)
744 self.module = module
750 self.module = module
745 self.reparent(module)
751 self.reparent(module)
746 # We assume that:
752 # We assume that:
747 # - requests for revisions after "stop" come from the
753 # - requests for revisions after "stop" come from the
748 # revision graph backward traversal. Cache all of them
754 # revision graph backward traversal. Cache all of them
749 # down to stop, they will be used eventually.
755 # down to stop, they will be used eventually.
750 # - requests for revisions before "stop" come to get
756 # - requests for revisions before "stop" come to get
751 # isolated branches parents. Just fetch what is needed.
757 # isolated branches parents. Just fetch what is needed.
752 stop = self.lastrevs.get(module, 0)
758 stop = self.lastrevs.get(module, 0)
753 if revnum < stop:
759 if revnum < stop:
754 stop = revnum + 1
760 stop = revnum + 1
755 self._fetch_revisions(revnum, stop)
761 self._fetch_revisions(revnum, stop)
756 if rev not in self.commits:
762 if rev not in self.commits:
757 raise error.Abort(_(b'svn: revision %s not found') % revnum)
763 raise error.Abort(_(b'svn: revision %s not found') % revnum)
758 revcommit = self.commits[rev]
764 revcommit = self.commits[rev]
759 # caller caches the result, so free it here to release memory
765 # caller caches the result, so free it here to release memory
760 del self.commits[rev]
766 del self.commits[rev]
761 return revcommit
767 return revcommit
762
768
763 def checkrevformat(self, revstr, mapname=b'splicemap'):
769 def checkrevformat(self, revstr, mapname=b'splicemap'):
764 """fails if revision format does not match the correct format"""
770 """fails if revision format does not match the correct format"""
765 if not re.match(
771 if not re.match(
766 br'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
772 br'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
767 br'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
773 br'[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
768 br'{12,12}(.*)@[0-9]+$',
774 br'{12,12}(.*)@[0-9]+$',
769 revstr,
775 revstr,
770 ):
776 ):
771 raise error.Abort(
777 raise error.Abort(
772 _(b'%s entry %s is not a valid revision identifier')
778 _(b'%s entry %s is not a valid revision identifier')
773 % (mapname, revstr)
779 % (mapname, revstr)
774 )
780 )
775
781
776 def numcommits(self):
782 def numcommits(self):
777 return int(self.head.rsplit(b'@', 1)[1]) - self.startrev
783 return int(self.head.rsplit(b'@', 1)[1]) - self.startrev
778
784
779 def gettags(self):
785 def gettags(self):
780 tags = {}
786 tags = {}
781 if self.tags is None:
787 if self.tags is None:
782 return tags
788 return tags
783
789
784 # svn tags are just a convention, project branches left in a
790 # svn tags are just a convention, project branches left in a
785 # 'tags' directory. There is no other relationship than
791 # 'tags' directory. There is no other relationship than
786 # ancestry, which is expensive to discover and makes them hard
792 # ancestry, which is expensive to discover and makes them hard
787 # to update incrementally. Worse, past revisions may be
793 # to update incrementally. Worse, past revisions may be
788 # referenced by tags far away in the future, requiring a deep
794 # referenced by tags far away in the future, requiring a deep
789 # history traversal on every calculation. Current code
795 # history traversal on every calculation. Current code
790 # performs a single backward traversal, tracking moves within
796 # performs a single backward traversal, tracking moves within
791 # the tags directory (tag renaming) and recording a new tag
797 # the tags directory (tag renaming) and recording a new tag
792 # everytime a project is copied from outside the tags
798 # everytime a project is copied from outside the tags
793 # directory. It also lists deleted tags, this behaviour may
799 # directory. It also lists deleted tags, this behaviour may
794 # change in the future.
800 # change in the future.
795 pendings = []
801 pendings = []
796 tagspath = self.tags
802 tagspath = self.tags
797 start = svn.ra.get_latest_revnum(self.ra)
803 start = svn.ra.get_latest_revnum(self.ra)
798 stream = self._getlog([self.tags], start, self.startrev)
804 stream = self._getlog([self.tags], start, self.startrev)
799 try:
805 try:
800 for entry in stream:
806 for entry in stream:
801 origpaths, revnum, author, date, message = entry
807 origpaths, revnum, author, date, message = entry
802 if not origpaths:
808 if not origpaths:
803 origpaths = []
809 origpaths = []
804 copies = [
810 copies = [
805 (e.copyfrom_path, e.copyfrom_rev, p)
811 (e.copyfrom_path, e.copyfrom_rev, p)
806 for p, e in origpaths.items()
812 for p, e in origpaths.items()
807 if e.copyfrom_path
813 if e.copyfrom_path
808 ]
814 ]
809 # Apply moves/copies from more specific to general
815 # Apply moves/copies from more specific to general
810 copies.sort(reverse=True)
816 copies.sort(reverse=True)
811
817
812 srctagspath = tagspath
818 srctagspath = tagspath
813 if copies and copies[-1][2] == tagspath:
819 if copies and copies[-1][2] == tagspath:
814 # Track tags directory moves
820 # Track tags directory moves
815 srctagspath = copies.pop()[0]
821 srctagspath = copies.pop()[0]
816
822
817 for source, sourcerev, dest in copies:
823 for source, sourcerev, dest in copies:
818 if not dest.startswith(tagspath + b'/'):
824 if not dest.startswith(tagspath + b'/'):
819 continue
825 continue
820 for tag in pendings:
826 for tag in pendings:
821 if tag[0].startswith(dest):
827 if tag[0].startswith(dest):
822 tagpath = source + tag[0][len(dest) :]
828 tagpath = source + tag[0][len(dest) :]
823 tag[:2] = [tagpath, sourcerev]
829 tag[:2] = [tagpath, sourcerev]
824 break
830 break
825 else:
831 else:
826 pendings.append([source, sourcerev, dest])
832 pendings.append([source, sourcerev, dest])
827
833
828 # Filter out tags with children coming from different
834 # Filter out tags with children coming from different
829 # parts of the repository like:
835 # parts of the repository like:
830 # /tags/tag.1 (from /trunk:10)
836 # /tags/tag.1 (from /trunk:10)
831 # /tags/tag.1/foo (from /branches/foo:12)
837 # /tags/tag.1/foo (from /branches/foo:12)
832 # Here/tags/tag.1 discarded as well as its children.
838 # Here/tags/tag.1 discarded as well as its children.
833 # It happens with tools like cvs2svn. Such tags cannot
839 # It happens with tools like cvs2svn. Such tags cannot
834 # be represented in mercurial.
840 # be represented in mercurial.
835 addeds = {
841 addeds = {
836 p: e.copyfrom_path
842 p: e.copyfrom_path
837 for p, e in origpaths.items()
843 for p, e in origpaths.items()
838 if e.action == b'A' and e.copyfrom_path
844 if e.action == b'A' and e.copyfrom_path
839 }
845 }
840 badroots = set()
846 badroots = set()
841 for destroot in addeds:
847 for destroot in addeds:
842 for source, sourcerev, dest in pendings:
848 for source, sourcerev, dest in pendings:
843 if not dest.startswith(
849 if not dest.startswith(
844 destroot + b'/'
850 destroot + b'/'
845 ) or source.startswith(addeds[destroot] + b'/'):
851 ) or source.startswith(addeds[destroot] + b'/'):
846 continue
852 continue
847 badroots.add(destroot)
853 badroots.add(destroot)
848 break
854 break
849
855
850 for badroot in badroots:
856 for badroot in badroots:
851 pendings = [
857 pendings = [
852 p
858 p
853 for p in pendings
859 for p in pendings
854 if p[2] != badroot
860 if p[2] != badroot
855 and not p[2].startswith(badroot + b'/')
861 and not p[2].startswith(badroot + b'/')
856 ]
862 ]
857
863
858 # Tell tag renamings from tag creations
864 # Tell tag renamings from tag creations
859 renamings = []
865 renamings = []
860 for source, sourcerev, dest in pendings:
866 for source, sourcerev, dest in pendings:
861 tagname = dest.split(b'/')[-1]
867 tagname = dest.split(b'/')[-1]
862 if source.startswith(srctagspath):
868 if source.startswith(srctagspath):
863 renamings.append([source, sourcerev, tagname])
869 renamings.append([source, sourcerev, tagname])
864 continue
870 continue
865 if tagname in tags:
871 if tagname in tags:
866 # Keep the latest tag value
872 # Keep the latest tag value
867 continue
873 continue
868 # From revision may be fake, get one with changes
874 # From revision may be fake, get one with changes
869 try:
875 try:
870 tagid = self.latest(source, sourcerev)
876 tagid = self.latest(source, sourcerev)
871 if tagid and tagname not in tags:
877 if tagid and tagname not in tags:
872 tags[tagname] = tagid
878 tags[tagname] = tagid
873 except SvnPathNotFound:
879 except SvnPathNotFound:
874 # It happens when we are following directories
880 # It happens when we are following directories
875 # we assumed were copied with their parents
881 # we assumed were copied with their parents
876 # but were really created in the tag
882 # but were really created in the tag
877 # directory.
883 # directory.
878 pass
884 pass
879 pendings = renamings
885 pendings = renamings
880 tagspath = srctagspath
886 tagspath = srctagspath
881 finally:
887 finally:
882 stream.close()
888 stream.close()
883 return tags
889 return tags
884
890
885 def converted(self, rev, destrev):
891 def converted(self, rev, destrev):
886 if not self.wc:
892 if not self.wc:
887 return
893 return
888 if self.convertfp is None:
894 if self.convertfp is None:
889 self.convertfp = open(
895 self.convertfp = open(
890 os.path.join(self.wc, b'.svn', b'hg-shamap'), b'ab'
896 os.path.join(self.wc, b'.svn', b'hg-shamap'), b'ab'
891 )
897 )
892 self.convertfp.write(
898 self.convertfp.write(
893 util.tonativeeol(b'%s %d\n' % (destrev, self.revnum(rev)))
899 util.tonativeeol(b'%s %d\n' % (destrev, self.revnum(rev)))
894 )
900 )
895 self.convertfp.flush()
901 self.convertfp.flush()
896
902
897 def revid(self, revnum, module=None):
903 def revid(self, revnum, module=None):
898 return b'svn:%s%s@%d' % (self.uuid, module or self.module, revnum)
904 return b'svn:%s%s@%d' % (self.uuid, module or self.module, revnum)
899
905
900 def revnum(self, rev):
906 def revnum(self, rev):
901 return int(rev.split(b'@')[-1])
907 return int(rev.split(b'@')[-1])
902
908
903 def latest(self, path, stop=None):
909 def latest(self, path, stop=None):
904 """Find the latest revid affecting path, up to stop revision
910 """Find the latest revid affecting path, up to stop revision
905 number. If stop is None, default to repository latest
911 number. If stop is None, default to repository latest
906 revision. It may return a revision in a different module,
912 revision. It may return a revision in a different module,
907 since a branch may be moved without a change being
913 since a branch may be moved without a change being
908 reported. Return None if computed module does not belong to
914 reported. Return None if computed module does not belong to
909 rootmodule subtree.
915 rootmodule subtree.
910 """
916 """
911
917
912 def findchanges(path, start, stop=None):
918 def findchanges(path, start, stop=None):
913 stream = self._getlog([path], start, stop or 1)
919 stream = self._getlog([path], start, stop or 1)
914 try:
920 try:
915 for entry in stream:
921 for entry in stream:
916 paths, revnum, author, date, message = entry
922 paths, revnum, author, date, message = entry
917 if stop is None and paths:
923 if stop is None and paths:
918 # We do not know the latest changed revision,
924 # We do not know the latest changed revision,
919 # keep the first one with changed paths.
925 # keep the first one with changed paths.
920 break
926 break
921 if stop is not None and revnum <= stop:
927 if stop is not None and revnum <= stop:
922 break
928 break
923
929
924 for p in paths:
930 for p in paths:
925 if not path.startswith(p) or not paths[p].copyfrom_path:
931 if not path.startswith(p) or not paths[p].copyfrom_path:
926 continue
932 continue
927 newpath = paths[p].copyfrom_path + path[len(p) :]
933 newpath = paths[p].copyfrom_path + path[len(p) :]
928 self.ui.debug(
934 self.ui.debug(
929 b"branch renamed from %s to %s at %d\n"
935 b"branch renamed from %s to %s at %d\n"
930 % (path, newpath, revnum)
936 % (path, newpath, revnum)
931 )
937 )
932 path = newpath
938 path = newpath
933 break
939 break
934 if not paths:
940 if not paths:
935 revnum = None
941 revnum = None
936 return revnum, path
942 return revnum, path
937 finally:
943 finally:
938 stream.close()
944 stream.close()
939
945
940 if not path.startswith(self.rootmodule):
946 if not path.startswith(self.rootmodule):
941 # Requests on foreign branches may be forbidden at server level
947 # Requests on foreign branches may be forbidden at server level
942 self.ui.debug(b'ignoring foreign branch %r\n' % path)
948 self.ui.debug(b'ignoring foreign branch %r\n' % path)
943 return None
949 return None
944
950
945 if stop is None:
951 if stop is None:
946 stop = svn.ra.get_latest_revnum(self.ra)
952 stop = svn.ra.get_latest_revnum(self.ra)
947 try:
953 try:
948 prevmodule = self.reparent(b'')
954 prevmodule = self.reparent(b'')
949 dirent = svn.ra.stat(self.ra, path.strip(b'/'), stop)
955 dirent = svn.ra.stat(self.ra, path.strip(b'/'), stop)
950 self.reparent(prevmodule)
956 self.reparent(prevmodule)
951 except svn.core.SubversionException:
957 except svn.core.SubversionException:
952 dirent = None
958 dirent = None
953 if not dirent:
959 if not dirent:
954 raise SvnPathNotFound(
960 raise SvnPathNotFound(
955 _(b'%s not found up to revision %d') % (path, stop)
961 _(b'%s not found up to revision %d') % (path, stop)
956 )
962 )
957
963
958 # stat() gives us the previous revision on this line of
964 # stat() gives us the previous revision on this line of
959 # development, but it might be in *another module*. Fetch the
965 # development, but it might be in *another module*. Fetch the
960 # log and detect renames down to the latest revision.
966 # log and detect renames down to the latest revision.
961 revnum, realpath = findchanges(path, stop, dirent.created_rev)
967 revnum, realpath = findchanges(path, stop, dirent.created_rev)
962 if revnum is None:
968 if revnum is None:
963 # Tools like svnsync can create empty revision, when
969 # Tools like svnsync can create empty revision, when
964 # synchronizing only a subtree for instance. These empty
970 # synchronizing only a subtree for instance. These empty
965 # revisions created_rev still have their original values
971 # revisions created_rev still have their original values
966 # despite all changes having disappeared and can be
972 # despite all changes having disappeared and can be
967 # returned by ra.stat(), at least when stating the root
973 # returned by ra.stat(), at least when stating the root
968 # module. In that case, do not trust created_rev and scan
974 # module. In that case, do not trust created_rev and scan
969 # the whole history.
975 # the whole history.
970 revnum, realpath = findchanges(path, stop)
976 revnum, realpath = findchanges(path, stop)
971 if revnum is None:
977 if revnum is None:
972 self.ui.debug(b'ignoring empty branch %r\n' % realpath)
978 self.ui.debug(b'ignoring empty branch %r\n' % realpath)
973 return None
979 return None
974
980
975 if not realpath.startswith(self.rootmodule):
981 if not realpath.startswith(self.rootmodule):
976 self.ui.debug(b'ignoring foreign branch %r\n' % realpath)
982 self.ui.debug(b'ignoring foreign branch %r\n' % realpath)
977 return None
983 return None
978 return self.revid(revnum, realpath)
984 return self.revid(revnum, realpath)
979
985
980 def reparent(self, module):
986 def reparent(self, module):
981 """Reparent the svn transport and return the previous parent."""
987 """Reparent the svn transport and return the previous parent."""
982 if self.prevmodule == module:
988 if self.prevmodule == module:
983 return module
989 return module
984 svnurl = self.baseurl + quote(module)
990 svnurl = self.baseurl + quote(module)
985 prevmodule = self.prevmodule
991 prevmodule = self.prevmodule
986 if prevmodule is None:
992 if prevmodule is None:
987 prevmodule = b''
993 prevmodule = b''
988 self.ui.debug(b"reparent to %s\n" % svnurl)
994 self.ui.debug(b"reparent to %s\n" % svnurl)
989 svn.ra.reparent(self.ra, svnurl)
995 svn.ra.reparent(self.ra, svnurl)
990 self.prevmodule = module
996 self.prevmodule = module
991 return prevmodule
997 return prevmodule
992
998
993 def expandpaths(self, rev, paths, parents):
999 def expandpaths(self, rev, paths, parents):
994 changed, removed = set(), set()
1000 changed, removed = set(), set()
995 copies = {}
1001 copies = {}
996
1002
997 new_module, revnum = revsplit(rev)[1:]
1003 new_module, revnum = revsplit(rev)[1:]
998 if new_module != self.module:
1004 if new_module != self.module:
999 self.module = new_module
1005 self.module = new_module
1000 self.reparent(self.module)
1006 self.reparent(self.module)
1001
1007
1002 progress = self.ui.makeprogress(
1008 progress = self.ui.makeprogress(
1003 _(b'scanning paths'), unit=_(b'paths'), total=len(paths)
1009 _(b'scanning paths'), unit=_(b'paths'), total=len(paths)
1004 )
1010 )
1005 for i, (path, ent) in enumerate(paths):
1011 for i, (path, ent) in enumerate(paths):
1006 progress.update(i, item=path)
1012 progress.update(i, item=path)
1007 entrypath = self.getrelpath(path)
1013 entrypath = self.getrelpath(path)
1008
1014
1009 kind = self._checkpath(entrypath, revnum)
1015 kind = self._checkpath(entrypath, revnum)
1010 if kind == svn.core.svn_node_file:
1016 if kind == svn.core.svn_node_file:
1011 changed.add(self.recode(entrypath))
1017 changed.add(self.recode(entrypath))
1012 if not ent.copyfrom_path or not parents:
1018 if not ent.copyfrom_path or not parents:
1013 continue
1019 continue
1014 # Copy sources not in parent revisions cannot be
1020 # Copy sources not in parent revisions cannot be
1015 # represented, ignore their origin for now
1021 # represented, ignore their origin for now
1016 pmodule, prevnum = revsplit(parents[0])[1:]
1022 pmodule, prevnum = revsplit(parents[0])[1:]
1017 if ent.copyfrom_rev < prevnum:
1023 if ent.copyfrom_rev < prevnum:
1018 continue
1024 continue
1019 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
1025 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
1020 if not copyfrom_path:
1026 if not copyfrom_path:
1021 continue
1027 continue
1022 self.ui.debug(
1028 self.ui.debug(
1023 b"copied to %s from %s@%d\n"
1029 b"copied to %s from %s@%d\n"
1024 % (entrypath, copyfrom_path, ent.copyfrom_rev)
1030 % (entrypath, copyfrom_path, ent.copyfrom_rev)
1025 )
1031 )
1026 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
1032 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
1027 elif kind == 0: # gone, but had better be a deleted *file*
1033 elif kind == 0: # gone, but had better be a deleted *file*
1028 self.ui.debug(b"gone from %d\n" % ent.copyfrom_rev)
1034 self.ui.debug(b"gone from %d\n" % ent.copyfrom_rev)
1029 pmodule, prevnum = revsplit(parents[0])[1:]
1035 pmodule, prevnum = revsplit(parents[0])[1:]
1030 parentpath = pmodule + b"/" + entrypath
1036 parentpath = pmodule + b"/" + entrypath
1031 fromkind = self._checkpath(entrypath, prevnum, pmodule)
1037 fromkind = self._checkpath(entrypath, prevnum, pmodule)
1032
1038
1033 if fromkind == svn.core.svn_node_file:
1039 if fromkind == svn.core.svn_node_file:
1034 removed.add(self.recode(entrypath))
1040 removed.add(self.recode(entrypath))
1035 elif fromkind == svn.core.svn_node_dir:
1041 elif fromkind == svn.core.svn_node_dir:
1036 oroot = parentpath.strip(b'/')
1042 oroot = parentpath.strip(b'/')
1037 nroot = path.strip(b'/')
1043 nroot = path.strip(b'/')
1038 children = self._iterfiles(oroot, prevnum)
1044 children = self._iterfiles(oroot, prevnum)
1039 for childpath in children:
1045 for childpath in children:
1040 childpath = childpath.replace(oroot, nroot)
1046 childpath = childpath.replace(oroot, nroot)
1041 childpath = self.getrelpath(b"/" + childpath, pmodule)
1047 childpath = self.getrelpath(b"/" + childpath, pmodule)
1042 if childpath:
1048 if childpath:
1043 removed.add(self.recode(childpath))
1049 removed.add(self.recode(childpath))
1044 else:
1050 else:
1045 self.ui.debug(
1051 self.ui.debug(
1046 b'unknown path in revision %d: %s\n' % (revnum, path)
1052 b'unknown path in revision %d: %s\n' % (revnum, path)
1047 )
1053 )
1048 elif kind == svn.core.svn_node_dir:
1054 elif kind == svn.core.svn_node_dir:
1049 if ent.action == b'M':
1055 if ent.action == b'M':
1050 # If the directory just had a prop change,
1056 # If the directory just had a prop change,
1051 # then we shouldn't need to look for its children.
1057 # then we shouldn't need to look for its children.
1052 continue
1058 continue
1053 if ent.action == b'R' and parents:
1059 if ent.action == b'R' and parents:
1054 # If a directory is replacing a file, mark the previous
1060 # If a directory is replacing a file, mark the previous
1055 # file as deleted
1061 # file as deleted
1056 pmodule, prevnum = revsplit(parents[0])[1:]
1062 pmodule, prevnum = revsplit(parents[0])[1:]
1057 pkind = self._checkpath(entrypath, prevnum, pmodule)
1063 pkind = self._checkpath(entrypath, prevnum, pmodule)
1058 if pkind == svn.core.svn_node_file:
1064 if pkind == svn.core.svn_node_file:
1059 removed.add(self.recode(entrypath))
1065 removed.add(self.recode(entrypath))
1060 elif pkind == svn.core.svn_node_dir:
1066 elif pkind == svn.core.svn_node_dir:
1061 # We do not know what files were kept or removed,
1067 # We do not know what files were kept or removed,
1062 # mark them all as changed.
1068 # mark them all as changed.
1063 for childpath in self._iterfiles(pmodule, prevnum):
1069 for childpath in self._iterfiles(pmodule, prevnum):
1064 childpath = self.getrelpath(b"/" + childpath)
1070 childpath = self.getrelpath(b"/" + childpath)
1065 if childpath:
1071 if childpath:
1066 changed.add(self.recode(childpath))
1072 changed.add(self.recode(childpath))
1067
1073
1068 for childpath in self._iterfiles(path, revnum):
1074 for childpath in self._iterfiles(path, revnum):
1069 childpath = self.getrelpath(b"/" + childpath)
1075 childpath = self.getrelpath(b"/" + childpath)
1070 if childpath:
1076 if childpath:
1071 changed.add(self.recode(childpath))
1077 changed.add(self.recode(childpath))
1072
1078
1073 # Handle directory copies
1079 # Handle directory copies
1074 if not ent.copyfrom_path or not parents:
1080 if not ent.copyfrom_path or not parents:
1075 continue
1081 continue
1076 # Copy sources not in parent revisions cannot be
1082 # Copy sources not in parent revisions cannot be
1077 # represented, ignore their origin for now
1083 # represented, ignore their origin for now
1078 pmodule, prevnum = revsplit(parents[0])[1:]
1084 pmodule, prevnum = revsplit(parents[0])[1:]
1079 if ent.copyfrom_rev < prevnum:
1085 if ent.copyfrom_rev < prevnum:
1080 continue
1086 continue
1081 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
1087 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
1082 if not copyfrompath:
1088 if not copyfrompath:
1083 continue
1089 continue
1084 self.ui.debug(
1090 self.ui.debug(
1085 b"mark %s came from %s:%d\n"
1091 b"mark %s came from %s:%d\n"
1086 % (path, copyfrompath, ent.copyfrom_rev)
1092 % (path, copyfrompath, ent.copyfrom_rev)
1087 )
1093 )
1088 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
1094 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
1089 for childpath in children:
1095 for childpath in children:
1090 childpath = self.getrelpath(b"/" + childpath, pmodule)
1096 childpath = self.getrelpath(b"/" + childpath, pmodule)
1091 if not childpath:
1097 if not childpath:
1092 continue
1098 continue
1093 copytopath = path + childpath[len(copyfrompath) :]
1099 copytopath = path + childpath[len(copyfrompath) :]
1094 copytopath = self.getrelpath(copytopath)
1100 copytopath = self.getrelpath(copytopath)
1095 copies[self.recode(copytopath)] = self.recode(childpath)
1101 copies[self.recode(copytopath)] = self.recode(childpath)
1096
1102
1097 progress.complete()
1103 progress.complete()
1098 changed.update(removed)
1104 changed.update(removed)
1099 return (list(changed), removed, copies)
1105 return (list(changed), removed, copies)
1100
1106
1101 def _fetch_revisions(self, from_revnum, to_revnum):
1107 def _fetch_revisions(self, from_revnum, to_revnum):
1102 if from_revnum < to_revnum:
1108 if from_revnum < to_revnum:
1103 from_revnum, to_revnum = to_revnum, from_revnum
1109 from_revnum, to_revnum = to_revnum, from_revnum
1104
1110
1105 self.child_cset = None
1111 self.child_cset = None
1106
1112
1107 def parselogentry(orig_paths, revnum, author, date, message):
1113 def parselogentry(orig_paths, revnum, author, date, message):
1108 """Return the parsed commit object or None, and True if
1114 """Return the parsed commit object or None, and True if
1109 the revision is a branch root.
1115 the revision is a branch root.
1110 """
1116 """
1111 self.ui.debug(
1117 self.ui.debug(
1112 b"parsing revision %d (%d changes)\n"
1118 b"parsing revision %d (%d changes)\n"
1113 % (revnum, len(orig_paths))
1119 % (revnum, len(orig_paths))
1114 )
1120 )
1115
1121
1116 branched = False
1122 branched = False
1117 rev = self.revid(revnum)
1123 rev = self.revid(revnum)
1118 # branch log might return entries for a parent we already have
1124 # branch log might return entries for a parent we already have
1119
1125
1120 if rev in self.commits or revnum < to_revnum:
1126 if rev in self.commits or revnum < to_revnum:
1121 return None, branched
1127 return None, branched
1122
1128
1123 parents = []
1129 parents = []
1124 # check whether this revision is the start of a branch or part
1130 # check whether this revision is the start of a branch or part
1125 # of a branch renaming
1131 # of a branch renaming
1126 orig_paths = sorted(orig_paths.items())
1132 orig_paths = sorted(orig_paths.items())
1127 root_paths = [
1133 root_paths = [
1128 (p, e) for p, e in orig_paths if self.module.startswith(p)
1134 (p, e) for p, e in orig_paths if self.module.startswith(p)
1129 ]
1135 ]
1130 if root_paths:
1136 if root_paths:
1131 path, ent = root_paths[-1]
1137 path, ent = root_paths[-1]
1132 if ent.copyfrom_path:
1138 if ent.copyfrom_path:
1133 branched = True
1139 branched = True
1134 newpath = ent.copyfrom_path + self.module[len(path) :]
1140 newpath = ent.copyfrom_path + self.module[len(path) :]
1135 # ent.copyfrom_rev may not be the actual last revision
1141 # ent.copyfrom_rev may not be the actual last revision
1136 previd = self.latest(newpath, ent.copyfrom_rev)
1142 previd = self.latest(newpath, ent.copyfrom_rev)
1137 if previd is not None:
1143 if previd is not None:
1138 prevmodule, prevnum = revsplit(previd)[1:]
1144 prevmodule, prevnum = revsplit(previd)[1:]
1139 if prevnum >= self.startrev:
1145 if prevnum >= self.startrev:
1140 parents = [previd]
1146 parents = [previd]
1141 self.ui.note(
1147 self.ui.note(
1142 _(b'found parent of branch %s at %d: %s\n')
1148 _(b'found parent of branch %s at %d: %s\n')
1143 % (self.module, prevnum, prevmodule)
1149 % (self.module, prevnum, prevmodule)
1144 )
1150 )
1145 else:
1151 else:
1146 self.ui.debug(b"no copyfrom path, don't know what to do.\n")
1152 self.ui.debug(b"no copyfrom path, don't know what to do.\n")
1147
1153
1148 paths = []
1154 paths = []
1149 # filter out unrelated paths
1155 # filter out unrelated paths
1150 for path, ent in orig_paths:
1156 for path, ent in orig_paths:
1151 if self.getrelpath(path) is None:
1157 if self.getrelpath(path) is None:
1152 continue
1158 continue
1153 paths.append((path, ent))
1159 paths.append((path, ent))
1154
1160
1155 date = parsesvndate(date)
1161 date = parsesvndate(date)
1156 if self.ui.configbool(b'convert', b'localtimezone'):
1162 if self.ui.configbool(b'convert', b'localtimezone'):
1157 date = makedatetimestamp(date[0])
1163 date = makedatetimestamp(date[0])
1158
1164
1159 if message:
1165 if message:
1160 log = self.recode(message)
1166 log = self.recode(message)
1161 else:
1167 else:
1162 log = b''
1168 log = b''
1163
1169
1164 if author:
1170 if author:
1165 author = self.recode(author)
1171 author = self.recode(author)
1166 else:
1172 else:
1167 author = b''
1173 author = b''
1168
1174
1169 try:
1175 try:
1170 branch = self.module.split(b"/")[-1]
1176 branch = self.module.split(b"/")[-1]
1171 if branch == self.trunkname:
1177 if branch == self.trunkname:
1172 branch = None
1178 branch = None
1173 except IndexError:
1179 except IndexError:
1174 branch = None
1180 branch = None
1175
1181
1176 cset = commit(
1182 cset = commit(
1177 author=author,
1183 author=author,
1178 date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
1184 date=dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2'),
1179 desc=log,
1185 desc=log,
1180 parents=parents,
1186 parents=parents,
1181 branch=branch,
1187 branch=branch,
1182 rev=rev,
1188 rev=rev,
1183 )
1189 )
1184
1190
1185 self.commits[rev] = cset
1191 self.commits[rev] = cset
1186 # The parents list is *shared* among self.paths and the
1192 # The parents list is *shared* among self.paths and the
1187 # commit object. Both will be updated below.
1193 # commit object. Both will be updated below.
1188 self.paths[rev] = (paths, cset.parents)
1194 self.paths[rev] = (paths, cset.parents)
1189 if self.child_cset and not self.child_cset.parents:
1195 if self.child_cset and not self.child_cset.parents:
1190 self.child_cset.parents[:] = [rev]
1196 self.child_cset.parents[:] = [rev]
1191 self.child_cset = cset
1197 self.child_cset = cset
1192 return cset, branched
1198 return cset, branched
1193
1199
1194 self.ui.note(
1200 self.ui.note(
1195 _(b'fetching revision log for "%s" from %d to %d\n')
1201 _(b'fetching revision log for "%s" from %d to %d\n')
1196 % (self.module, from_revnum, to_revnum)
1202 % (self.module, from_revnum, to_revnum)
1197 )
1203 )
1198
1204
1199 try:
1205 try:
1200 firstcset = None
1206 firstcset = None
1201 lastonbranch = False
1207 lastonbranch = False
1202 stream = self._getlog([self.module], from_revnum, to_revnum)
1208 stream = self._getlog([self.module], from_revnum, to_revnum)
1203 try:
1209 try:
1204 for entry in stream:
1210 for entry in stream:
1205 paths, revnum, author, date, message = entry
1211 paths, revnum, author, date, message = entry
1206 if revnum < self.startrev:
1212 if revnum < self.startrev:
1207 lastonbranch = True
1213 lastonbranch = True
1208 break
1214 break
1209 if not paths:
1215 if not paths:
1210 self.ui.debug(b'revision %d has no entries\n' % revnum)
1216 self.ui.debug(b'revision %d has no entries\n' % revnum)
1211 # If we ever leave the loop on an empty
1217 # If we ever leave the loop on an empty
1212 # revision, do not try to get a parent branch
1218 # revision, do not try to get a parent branch
1213 lastonbranch = lastonbranch or revnum == 0
1219 lastonbranch = lastonbranch or revnum == 0
1214 continue
1220 continue
1215 cset, lastonbranch = parselogentry(
1221 cset, lastonbranch = parselogentry(
1216 paths, revnum, author, date, message
1222 paths, revnum, author, date, message
1217 )
1223 )
1218 if cset:
1224 if cset:
1219 firstcset = cset
1225 firstcset = cset
1220 if lastonbranch:
1226 if lastonbranch:
1221 break
1227 break
1222 finally:
1228 finally:
1223 stream.close()
1229 stream.close()
1224
1230
1225 if not lastonbranch and firstcset and not firstcset.parents:
1231 if not lastonbranch and firstcset and not firstcset.parents:
1226 # The first revision of the sequence (the last fetched one)
1232 # The first revision of the sequence (the last fetched one)
1227 # has invalid parents if not a branch root. Find the parent
1233 # has invalid parents if not a branch root. Find the parent
1228 # revision now, if any.
1234 # revision now, if any.
1229 try:
1235 try:
1230 firstrevnum = self.revnum(firstcset.rev)
1236 firstrevnum = self.revnum(firstcset.rev)
1231 if firstrevnum > 1:
1237 if firstrevnum > 1:
1232 latest = self.latest(self.module, firstrevnum - 1)
1238 latest = self.latest(self.module, firstrevnum - 1)
1233 if latest:
1239 if latest:
1234 firstcset.parents.append(latest)
1240 firstcset.parents.append(latest)
1235 except SvnPathNotFound:
1241 except SvnPathNotFound:
1236 pass
1242 pass
1237 except svn.core.SubversionException as xxx_todo_changeme:
1243 except svn.core.SubversionException as xxx_todo_changeme:
1238 (inst, num) = xxx_todo_changeme.args
1244 (inst, num) = xxx_todo_changeme.args
1239 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
1245 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
1240 raise error.Abort(
1246 raise error.Abort(
1241 _(b'svn: branch has no revision %s') % to_revnum
1247 _(b'svn: branch has no revision %s') % to_revnum
1242 )
1248 )
1243 raise
1249 raise
1244
1250
1245 def getfile(self, file, rev):
1251 def getfile(self, file, rev):
1246 # TODO: ra.get_file transmits the whole file instead of diffs.
1252 # TODO: ra.get_file transmits the whole file instead of diffs.
1247 if file in self.removed:
1253 if file in self.removed:
1248 return None, None
1254 return None, None
1249 try:
1255 try:
1250 new_module, revnum = revsplit(rev)[1:]
1256 new_module, revnum = revsplit(rev)[1:]
1251 if self.module != new_module:
1257 if self.module != new_module:
1252 self.module = new_module
1258 self.module = new_module
1253 self.reparent(self.module)
1259 self.reparent(self.module)
1254 io = stringio()
1260 io = stringio()
1255 info = svn.ra.get_file(self.ra, file, revnum, io)
1261 info = svn.ra.get_file(self.ra, file, revnum, io)
1256 data = io.getvalue()
1262 data = io.getvalue()
1257 # ra.get_file() seems to keep a reference on the input buffer
1263 # ra.get_file() seems to keep a reference on the input buffer
1258 # preventing collection. Release it explicitly.
1264 # preventing collection. Release it explicitly.
1259 io.close()
1265 io.close()
1260 if isinstance(info, list):
1266 if isinstance(info, list):
1261 info = info[-1]
1267 info = info[-1]
1262 mode = (b"svn:executable" in info) and b'x' or b''
1268 mode = (b"svn:executable" in info) and b'x' or b''
1263 mode = (b"svn:special" in info) and b'l' or mode
1269 mode = (b"svn:special" in info) and b'l' or mode
1264 except svn.core.SubversionException as e:
1270 except svn.core.SubversionException as e:
1265 notfound = (
1271 notfound = (
1266 svn.core.SVN_ERR_FS_NOT_FOUND,
1272 svn.core.SVN_ERR_FS_NOT_FOUND,
1267 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
1273 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND,
1268 )
1274 )
1269 if e.apr_err in notfound: # File not found
1275 if e.apr_err in notfound: # File not found
1270 return None, None
1276 return None, None
1271 raise
1277 raise
1272 if mode == b'l':
1278 if mode == b'l':
1273 link_prefix = b"link "
1279 link_prefix = b"link "
1274 if data.startswith(link_prefix):
1280 if data.startswith(link_prefix):
1275 data = data[len(link_prefix) :]
1281 data = data[len(link_prefix) :]
1276 return data, mode
1282 return data, mode
1277
1283
1278 def _iterfiles(self, path, revnum):
1284 def _iterfiles(self, path, revnum):
1279 """Enumerate all files in path at revnum, recursively."""
1285 """Enumerate all files in path at revnum, recursively."""
1280 path = path.strip(b'/')
1286 path = path.strip(b'/')
1281 pool = svn.core.Pool()
1287 pool = svn.core.Pool()
1282 rpath = b'/'.join([self.baseurl, quote(path)]).strip(b'/')
1288 rpath = b'/'.join([self.baseurl, quote(path)]).strip(b'/')
1283 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1289 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1284 if path:
1290 if path:
1285 path += b'/'
1291 path += b'/'
1286 return (
1292 return (
1287 (path + p)
1293 (path + p)
1288 for p, e in entries.items()
1294 for p, e in entries.items()
1289 if e.kind == svn.core.svn_node_file
1295 if e.kind == svn.core.svn_node_file
1290 )
1296 )
1291
1297
1292 def getrelpath(self, path, module=None):
1298 def getrelpath(self, path, module=None):
1293 if module is None:
1299 if module is None:
1294 module = self.module
1300 module = self.module
1295 # Given the repository url of this wc, say
1301 # Given the repository url of this wc, say
1296 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1302 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1297 # extract the "entry" portion (a relative path) from what
1303 # extract the "entry" portion (a relative path) from what
1298 # svn log --xml says, i.e.
1304 # svn log --xml says, i.e.
1299 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1305 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1300 # that is to say "tests/PloneTestCase.py"
1306 # that is to say "tests/PloneTestCase.py"
1301 if path.startswith(module):
1307 if path.startswith(module):
1302 relative = path.rstrip(b'/')[len(module) :]
1308 relative = path.rstrip(b'/')[len(module) :]
1303 if relative.startswith(b'/'):
1309 if relative.startswith(b'/'):
1304 return relative[1:]
1310 return relative[1:]
1305 elif relative == b'':
1311 elif relative == b'':
1306 return relative
1312 return relative
1307
1313
1308 # The path is outside our tracked tree...
1314 # The path is outside our tracked tree...
1309 self.ui.debug(
1315 self.ui.debug(
1310 b'%r is not under %r, ignoring\n'
1316 b'%r is not under %r, ignoring\n'
1311 % (pycompat.bytestr(path), pycompat.bytestr(module))
1317 % (pycompat.bytestr(path), pycompat.bytestr(module))
1312 )
1318 )
1313 return None
1319 return None
1314
1320
1315 def _checkpath(self, path, revnum, module=None):
1321 def _checkpath(self, path, revnum, module=None):
1316 if module is not None:
1322 if module is not None:
1317 prevmodule = self.reparent(b'')
1323 prevmodule = self.reparent(b'')
1318 path = module + b'/' + path
1324 path = module + b'/' + path
1319 try:
1325 try:
1320 # ra.check_path does not like leading slashes very much, it leads
1326 # ra.check_path does not like leading slashes very much, it leads
1321 # to PROPFIND subversion errors
1327 # to PROPFIND subversion errors
1322 return svn.ra.check_path(self.ra, path.strip(b'/'), revnum)
1328 return svn.ra.check_path(self.ra, path.strip(b'/'), revnum)
1323 finally:
1329 finally:
1324 if module is not None:
1330 if module is not None:
1325 self.reparent(prevmodule)
1331 self.reparent(prevmodule)
1326
1332
1327 def _getlog(
1333 def _getlog(
1328 self,
1334 self,
1329 paths,
1335 paths,
1330 start,
1336 start,
1331 end,
1337 end,
1332 limit=0,
1338 limit=0,
1333 discover_changed_paths=True,
1339 discover_changed_paths=True,
1334 strict_node_history=False,
1340 strict_node_history=False,
1335 ):
1341 ):
1336 # Normalize path names, svn >= 1.5 only wants paths relative to
1342 # Normalize path names, svn >= 1.5 only wants paths relative to
1337 # supplied URL
1343 # supplied URL
1338 relpaths = []
1344 relpaths = []
1339 for p in paths:
1345 for p in paths:
1340 if not p.startswith(b'/'):
1346 if not p.startswith(b'/'):
1341 p = self.module + b'/' + p
1347 p = self.module + b'/' + p
1342 relpaths.append(p.strip(b'/'))
1348 relpaths.append(p.strip(b'/'))
1343 args = [
1349 args = [
1344 self.baseurl,
1350 self.baseurl,
1345 relpaths,
1351 relpaths,
1346 start,
1352 start,
1347 end,
1353 end,
1348 limit,
1354 limit,
1349 discover_changed_paths,
1355 discover_changed_paths,
1350 strict_node_history,
1356 strict_node_history,
1351 ]
1357 ]
1352 # developer config: convert.svn.debugsvnlog
1358 # developer config: convert.svn.debugsvnlog
1353 if not self.ui.configbool(b'convert', b'svn.debugsvnlog'):
1359 if not self.ui.configbool(b'convert', b'svn.debugsvnlog'):
1354 return directlogstream(*args)
1360 return directlogstream(*args)
1355 arg = encodeargs(args)
1361 arg = encodeargs(args)
1356 hgexe = procutil.hgexecutable()
1362 hgexe = procutil.hgexecutable()
1357 cmd = b'%s debugsvnlog' % procutil.shellquote(hgexe)
1363 cmd = b'%s debugsvnlog' % procutil.shellquote(hgexe)
1358 stdin, stdout = procutil.popen2(cmd)
1364 stdin, stdout = procutil.popen2(cmd)
1359 stdin.write(arg)
1365 stdin.write(arg)
1360 try:
1366 try:
1361 stdin.close()
1367 stdin.close()
1362 except IOError:
1368 except IOError:
1363 raise error.Abort(
1369 raise error.Abort(
1364 _(
1370 _(
1365 b'Mercurial failed to run itself, check'
1371 b'Mercurial failed to run itself, check'
1366 b' hg executable is in PATH'
1372 b' hg executable is in PATH'
1367 )
1373 )
1368 )
1374 )
1369 return logstream(stdout)
1375 return logstream(stdout)
1370
1376
1371
1377
1372 pre_revprop_change_template = b'''#!/bin/sh
1378 pre_revprop_change_template = b'''#!/bin/sh
1373
1379
1374 REPOS="$1"
1380 REPOS="$1"
1375 REV="$2"
1381 REV="$2"
1376 USER="$3"
1382 USER="$3"
1377 PROPNAME="$4"
1383 PROPNAME="$4"
1378 ACTION="$5"
1384 ACTION="$5"
1379
1385
1380 %(rules)s
1386 %(rules)s
1381
1387
1382 echo "Changing prohibited revision property" >&2
1388 echo "Changing prohibited revision property" >&2
1383 exit 1
1389 exit 1
1384 '''
1390 '''
1385
1391
1386
1392
1387 def gen_pre_revprop_change_hook(prop_actions_allowed):
1393 def gen_pre_revprop_change_hook(prop_actions_allowed):
1388 rules = []
1394 rules = []
1389 for action, propname in prop_actions_allowed:
1395 for action, propname in prop_actions_allowed:
1390 rules.append(
1396 rules.append(
1391 (
1397 (
1392 b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
1398 b'if [ "$ACTION" = "%s" -a "$PROPNAME" = "%s" ]; '
1393 b'then exit 0; fi'
1399 b'then exit 0; fi'
1394 )
1400 )
1395 % (action, propname)
1401 % (action, propname)
1396 )
1402 )
1397 return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
1403 return pre_revprop_change_template % {b'rules': b'\n'.join(rules)}
1398
1404
1399
1405
1400 class svn_sink(converter_sink, commandline):
1406 class svn_sink(converter_sink, commandline):
1401 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1407 commit_re = re.compile(br'Committed revision (\d+).', re.M)
1402 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1408 uuid_re = re.compile(br'Repository UUID:\s*(\S+)', re.M)
1403
1409
1404 def prerun(self):
1410 def prerun(self):
1405 if self.wc:
1411 if self.wc:
1406 os.chdir(self.wc)
1412 os.chdir(self.wc)
1407
1413
1408 def postrun(self):
1414 def postrun(self):
1409 if self.wc:
1415 if self.wc:
1410 os.chdir(self.cwd)
1416 os.chdir(self.cwd)
1411
1417
1412 def join(self, name):
1418 def join(self, name):
1413 return os.path.join(self.wc, b'.svn', name)
1419 return os.path.join(self.wc, b'.svn', name)
1414
1420
1415 def revmapfile(self):
1421 def revmapfile(self):
1416 return self.join(b'hg-shamap')
1422 return self.join(b'hg-shamap')
1417
1423
1418 def authorfile(self):
1424 def authorfile(self):
1419 return self.join(b'hg-authormap')
1425 return self.join(b'hg-authormap')
1420
1426
1421 def __init__(self, ui, repotype, path):
1427 def __init__(self, ui, repotype, path):
1422
1428
1423 converter_sink.__init__(self, ui, repotype, path)
1429 converter_sink.__init__(self, ui, repotype, path)
1424 commandline.__init__(self, ui, b'svn')
1430 commandline.__init__(self, ui, b'svn')
1425 self.delete = []
1431 self.delete = []
1426 self.setexec = []
1432 self.setexec = []
1427 self.delexec = []
1433 self.delexec = []
1428 self.copies = []
1434 self.copies = []
1429 self.wc = None
1435 self.wc = None
1430 self.cwd = encoding.getcwd()
1436 self.cwd = encoding.getcwd()
1431
1437
1432 created = False
1438 created = False
1433 if os.path.isfile(os.path.join(path, b'.svn', b'entries')):
1439 if os.path.isfile(os.path.join(path, b'.svn', b'entries')):
1434 self.wc = os.path.realpath(path)
1440 self.wc = os.path.realpath(path)
1435 self.run0(b'update')
1441 self.run0(b'update')
1436 else:
1442 else:
1437 if not re.search(br'^(file|http|https|svn|svn\+ssh)://', path):
1443 if not re.search(br'^(file|http|https|svn|svn\+ssh)://', path):
1438 path = os.path.realpath(path)
1444 path = os.path.realpath(path)
1439 if os.path.isdir(os.path.dirname(path)):
1445 if os.path.isdir(os.path.dirname(path)):
1440 if not os.path.exists(
1446 if not os.path.exists(
1441 os.path.join(path, b'db', b'fs-type')
1447 os.path.join(path, b'db', b'fs-type')
1442 ):
1448 ):
1443 ui.status(
1449 ui.status(
1444 _(b"initializing svn repository '%s'\n")
1450 _(b"initializing svn repository '%s'\n")
1445 % os.path.basename(path)
1451 % os.path.basename(path)
1446 )
1452 )
1447 commandline(ui, b'svnadmin').run0(b'create', path)
1453 commandline(ui, b'svnadmin').run0(b'create', path)
1448 created = path
1454 created = path
1449 path = util.normpath(path)
1455 path = util.normpath(path)
1450 if not path.startswith(b'/'):
1456 if not path.startswith(b'/'):
1451 path = b'/' + path
1457 path = b'/' + path
1452 path = b'file://' + path
1458 path = b'file://' + path
1453
1459
1454 wcpath = os.path.join(
1460 wcpath = os.path.join(
1455 encoding.getcwd(), os.path.basename(path) + b'-wc'
1461 encoding.getcwd(), os.path.basename(path) + b'-wc'
1456 )
1462 )
1457 ui.status(
1463 ui.status(
1458 _(b"initializing svn working copy '%s'\n")
1464 _(b"initializing svn working copy '%s'\n")
1459 % os.path.basename(wcpath)
1465 % os.path.basename(wcpath)
1460 )
1466 )
1461 self.run0(b'checkout', path, wcpath)
1467 self.run0(b'checkout', path, wcpath)
1462
1468
1463 self.wc = wcpath
1469 self.wc = wcpath
1464 self.opener = vfsmod.vfs(self.wc)
1470 self.opener = vfsmod.vfs(self.wc)
1465 self.wopener = vfsmod.vfs(self.wc)
1471 self.wopener = vfsmod.vfs(self.wc)
1466 self.childmap = mapfile(ui, self.join(b'hg-childmap'))
1472 self.childmap = mapfile(ui, self.join(b'hg-childmap'))
1467 if util.checkexec(self.wc):
1473 if util.checkexec(self.wc):
1468 self.is_exec = util.isexec
1474 self.is_exec = util.isexec
1469 else:
1475 else:
1470 self.is_exec = None
1476 self.is_exec = None
1471
1477
1472 if created:
1478 if created:
1473 prop_actions_allowed = [
1479 prop_actions_allowed = [
1474 (b'M', b'svn:log'),
1480 (b'M', b'svn:log'),
1475 (b'A', b'hg:convert-branch'),
1481 (b'A', b'hg:convert-branch'),
1476 (b'A', b'hg:convert-rev'),
1482 (b'A', b'hg:convert-rev'),
1477 ]
1483 ]
1478
1484
1479 if self.ui.configbool(
1485 if self.ui.configbool(
1480 b'convert', b'svn.dangerous-set-commit-dates'
1486 b'convert', b'svn.dangerous-set-commit-dates'
1481 ):
1487 ):
1482 prop_actions_allowed.append((b'M', b'svn:date'))
1488 prop_actions_allowed.append((b'M', b'svn:date'))
1483
1489
1484 hook = os.path.join(created, b'hooks', b'pre-revprop-change')
1490 hook = os.path.join(created, b'hooks', b'pre-revprop-change')
1485 fp = open(hook, b'wb')
1491 fp = open(hook, b'wb')
1486 fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
1492 fp.write(gen_pre_revprop_change_hook(prop_actions_allowed))
1487 fp.close()
1493 fp.close()
1488 util.setflags(hook, False, True)
1494 util.setflags(hook, False, True)
1489
1495
1490 output = self.run0(b'info')
1496 output = self.run0(b'info')
1491 self.uuid = self.uuid_re.search(output).group(1).strip()
1497 self.uuid = self.uuid_re.search(output).group(1).strip()
1492
1498
1493 def wjoin(self, *names):
1499 def wjoin(self, *names):
1494 return os.path.join(self.wc, *names)
1500 return os.path.join(self.wc, *names)
1495
1501
1496 @propertycache
1502 @propertycache
1497 def manifest(self):
1503 def manifest(self):
1498 # As of svn 1.7, the "add" command fails when receiving
1504 # As of svn 1.7, the "add" command fails when receiving
1499 # already tracked entries, so we have to track and filter them
1505 # already tracked entries, so we have to track and filter them
1500 # ourselves.
1506 # ourselves.
1501 m = set()
1507 m = set()
1502 output = self.run0(b'ls', recursive=True, xml=True)
1508 output = self.run0(b'ls', recursive=True, xml=True)
1503 doc = xml.dom.minidom.parseString(output)
1509 doc = xml.dom.minidom.parseString(output)
1504 for e in doc.getElementsByTagName('entry'):
1510 for e in doc.getElementsByTagName('entry'):
1505 for n in e.childNodes:
1511 for n in e.childNodes:
1506 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1512 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1507 continue
1513 continue
1508 name = ''.join(
1514 name = ''.join(
1509 c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
1515 c.data for c in n.childNodes if c.nodeType == c.TEXT_NODE
1510 )
1516 )
1511 # Entries are compared with names coming from
1517 # Entries are compared with names coming from
1512 # mercurial, so bytes with undefined encoding. Our
1518 # mercurial, so bytes with undefined encoding. Our
1513 # best bet is to assume they are in local
1519 # best bet is to assume they are in local
1514 # encoding. They will be passed to command line calls
1520 # encoding. They will be passed to command line calls
1515 # later anyway, so they better be.
1521 # later anyway, so they better be.
1516 m.add(encoding.unitolocal(name))
1522 m.add(encoding.unitolocal(name))
1517 break
1523 break
1518 return m
1524 return m
1519
1525
1520 def putfile(self, filename, flags, data):
1526 def putfile(self, filename, flags, data):
1521 if b'l' in flags:
1527 if b'l' in flags:
1522 self.wopener.symlink(data, filename)
1528 self.wopener.symlink(data, filename)
1523 else:
1529 else:
1524 try:
1530 try:
1525 if os.path.islink(self.wjoin(filename)):
1531 if os.path.islink(self.wjoin(filename)):
1526 os.unlink(filename)
1532 os.unlink(filename)
1527 except OSError:
1533 except OSError:
1528 pass
1534 pass
1529
1535
1530 if self.is_exec:
1536 if self.is_exec:
1531 # We need to check executability of the file before the change,
1537 # We need to check executability of the file before the change,
1532 # because `vfs.write` is able to reset exec bit.
1538 # because `vfs.write` is able to reset exec bit.
1533 wasexec = False
1539 wasexec = False
1534 if os.path.exists(self.wjoin(filename)):
1540 if os.path.exists(self.wjoin(filename)):
1535 wasexec = self.is_exec(self.wjoin(filename))
1541 wasexec = self.is_exec(self.wjoin(filename))
1536
1542
1537 self.wopener.write(filename, data)
1543 self.wopener.write(filename, data)
1538
1544
1539 if self.is_exec:
1545 if self.is_exec:
1540 if wasexec:
1546 if wasexec:
1541 if b'x' not in flags:
1547 if b'x' not in flags:
1542 self.delexec.append(filename)
1548 self.delexec.append(filename)
1543 else:
1549 else:
1544 if b'x' in flags:
1550 if b'x' in flags:
1545 self.setexec.append(filename)
1551 self.setexec.append(filename)
1546 util.setflags(self.wjoin(filename), False, b'x' in flags)
1552 util.setflags(self.wjoin(filename), False, b'x' in flags)
1547
1553
1548 def _copyfile(self, source, dest):
1554 def _copyfile(self, source, dest):
1549 # SVN's copy command pukes if the destination file exists, but
1555 # SVN's copy command pukes if the destination file exists, but
1550 # our copyfile method expects to record a copy that has
1556 # our copyfile method expects to record a copy that has
1551 # already occurred. Cross the semantic gap.
1557 # already occurred. Cross the semantic gap.
1552 wdest = self.wjoin(dest)
1558 wdest = self.wjoin(dest)
1553 exists = os.path.lexists(wdest)
1559 exists = os.path.lexists(wdest)
1554 if exists:
1560 if exists:
1555 fd, tempname = pycompat.mkstemp(
1561 fd, tempname = pycompat.mkstemp(
1556 prefix=b'hg-copy-', dir=os.path.dirname(wdest)
1562 prefix=b'hg-copy-', dir=os.path.dirname(wdest)
1557 )
1563 )
1558 os.close(fd)
1564 os.close(fd)
1559 os.unlink(tempname)
1565 os.unlink(tempname)
1560 os.rename(wdest, tempname)
1566 os.rename(wdest, tempname)
1561 try:
1567 try:
1562 self.run0(b'copy', source, dest)
1568 self.run0(b'copy', source, dest)
1563 finally:
1569 finally:
1564 self.manifest.add(dest)
1570 self.manifest.add(dest)
1565 if exists:
1571 if exists:
1566 try:
1572 try:
1567 os.unlink(wdest)
1573 os.unlink(wdest)
1568 except OSError:
1574 except OSError:
1569 pass
1575 pass
1570 os.rename(tempname, wdest)
1576 os.rename(tempname, wdest)
1571
1577
1572 def dirs_of(self, files):
1578 def dirs_of(self, files):
1573 dirs = set()
1579 dirs = set()
1574 for f in files:
1580 for f in files:
1575 if os.path.isdir(self.wjoin(f)):
1581 if os.path.isdir(self.wjoin(f)):
1576 dirs.add(f)
1582 dirs.add(f)
1577 i = len(f)
1583 i = len(f)
1578 for i in iter(lambda: f.rfind(b'/', 0, i), -1):
1584 for i in iter(lambda: f.rfind(b'/', 0, i), -1):
1579 dirs.add(f[:i])
1585 dirs.add(f[:i])
1580 return dirs
1586 return dirs
1581
1587
1582 def add_dirs(self, files):
1588 def add_dirs(self, files):
1583 add_dirs = [
1589 add_dirs = [
1584 d for d in sorted(self.dirs_of(files)) if d not in self.manifest
1590 d for d in sorted(self.dirs_of(files)) if d not in self.manifest
1585 ]
1591 ]
1586 if add_dirs:
1592 if add_dirs:
1587 self.manifest.update(add_dirs)
1593 self.manifest.update(add_dirs)
1588 self.xargs(add_dirs, b'add', non_recursive=True, quiet=True)
1594 self.xargs(add_dirs, b'add', non_recursive=True, quiet=True)
1589 return add_dirs
1595 return add_dirs
1590
1596
1591 def add_files(self, files):
1597 def add_files(self, files):
1592 files = [f for f in files if f not in self.manifest]
1598 files = [f for f in files if f not in self.manifest]
1593 if files:
1599 if files:
1594 self.manifest.update(files)
1600 self.manifest.update(files)
1595 self.xargs(files, b'add', quiet=True)
1601 self.xargs(files, b'add', quiet=True)
1596 return files
1602 return files
1597
1603
1598 def addchild(self, parent, child):
1604 def addchild(self, parent, child):
1599 self.childmap[parent] = child
1605 self.childmap[parent] = child
1600
1606
1601 def revid(self, rev):
1607 def revid(self, rev):
1602 return b"svn:%s@%s" % (self.uuid, rev)
1608 return b"svn:%s@%s" % (self.uuid, rev)
1603
1609
1604 def putcommit(
1610 def putcommit(
1605 self, files, copies, parents, commit, source, revmap, full, cleanp2
1611 self, files, copies, parents, commit, source, revmap, full, cleanp2
1606 ):
1612 ):
1607 for parent in parents:
1613 for parent in parents:
1608 try:
1614 try:
1609 return self.revid(self.childmap[parent])
1615 return self.revid(self.childmap[parent])
1610 except KeyError:
1616 except KeyError:
1611 pass
1617 pass
1612
1618
1613 # Apply changes to working copy
1619 # Apply changes to working copy
1614 for f, v in files:
1620 for f, v in files:
1615 data, mode = source.getfile(f, v)
1621 data, mode = source.getfile(f, v)
1616 if data is None:
1622 if data is None:
1617 self.delete.append(f)
1623 self.delete.append(f)
1618 else:
1624 else:
1619 self.putfile(f, mode, data)
1625 self.putfile(f, mode, data)
1620 if f in copies:
1626 if f in copies:
1621 self.copies.append([copies[f], f])
1627 self.copies.append([copies[f], f])
1622 if full:
1628 if full:
1623 self.delete.extend(sorted(self.manifest.difference(files)))
1629 self.delete.extend(sorted(self.manifest.difference(files)))
1624 files = [f[0] for f in files]
1630 files = [f[0] for f in files]
1625
1631
1626 entries = set(self.delete)
1632 entries = set(self.delete)
1627 files = frozenset(files)
1633 files = frozenset(files)
1628 entries.update(self.add_dirs(files.difference(entries)))
1634 entries.update(self.add_dirs(files.difference(entries)))
1629 if self.copies:
1635 if self.copies:
1630 for s, d in self.copies:
1636 for s, d in self.copies:
1631 self._copyfile(s, d)
1637 self._copyfile(s, d)
1632 self.copies = []
1638 self.copies = []
1633 if self.delete:
1639 if self.delete:
1634 self.xargs(self.delete, b'delete')
1640 self.xargs(self.delete, b'delete')
1635 for f in self.delete:
1641 for f in self.delete:
1636 self.manifest.remove(f)
1642 self.manifest.remove(f)
1637 self.delete = []
1643 self.delete = []
1638 entries.update(self.add_files(files.difference(entries)))
1644 entries.update(self.add_files(files.difference(entries)))
1639 if self.delexec:
1645 if self.delexec:
1640 self.xargs(self.delexec, b'propdel', b'svn:executable')
1646 self.xargs(self.delexec, b'propdel', b'svn:executable')
1641 self.delexec = []
1647 self.delexec = []
1642 if self.setexec:
1648 if self.setexec:
1643 self.xargs(self.setexec, b'propset', b'svn:executable', b'*')
1649 self.xargs(self.setexec, b'propset', b'svn:executable', b'*')
1644 self.setexec = []
1650 self.setexec = []
1645
1651
1646 fd, messagefile = pycompat.mkstemp(prefix=b'hg-convert-')
1652 fd, messagefile = pycompat.mkstemp(prefix=b'hg-convert-')
1647 fp = os.fdopen(fd, 'wb')
1653 fp = os.fdopen(fd, 'wb')
1648 fp.write(util.tonativeeol(commit.desc))
1654 fp.write(util.tonativeeol(commit.desc))
1649 fp.close()
1655 fp.close()
1650 try:
1656 try:
1651 output = self.run0(
1657 output = self.run0(
1652 b'commit',
1658 b'commit',
1653 username=stringutil.shortuser(commit.author),
1659 username=stringutil.shortuser(commit.author),
1654 file=messagefile,
1660 file=messagefile,
1655 encoding=b'utf-8',
1661 encoding=b'utf-8',
1656 )
1662 )
1657 try:
1663 try:
1658 rev = self.commit_re.search(output).group(1)
1664 rev = self.commit_re.search(output).group(1)
1659 except AttributeError:
1665 except AttributeError:
1660 if not files:
1666 if not files:
1661 return parents[0] if parents else b'None'
1667 return parents[0] if parents else b'None'
1662 self.ui.warn(_(b'unexpected svn output:\n'))
1668 self.ui.warn(_(b'unexpected svn output:\n'))
1663 self.ui.warn(output)
1669 self.ui.warn(output)
1664 raise error.Abort(_(b'unable to cope with svn output'))
1670 raise error.Abort(_(b'unable to cope with svn output'))
1665 if commit.rev:
1671 if commit.rev:
1666 self.run(
1672 self.run(
1667 b'propset',
1673 b'propset',
1668 b'hg:convert-rev',
1674 b'hg:convert-rev',
1669 commit.rev,
1675 commit.rev,
1670 revprop=True,
1676 revprop=True,
1671 revision=rev,
1677 revision=rev,
1672 )
1678 )
1673 if commit.branch and commit.branch != b'default':
1679 if commit.branch and commit.branch != b'default':
1674 self.run(
1680 self.run(
1675 b'propset',
1681 b'propset',
1676 b'hg:convert-branch',
1682 b'hg:convert-branch',
1677 commit.branch,
1683 commit.branch,
1678 revprop=True,
1684 revprop=True,
1679 revision=rev,
1685 revision=rev,
1680 )
1686 )
1681
1687
1682 if self.ui.configbool(
1688 if self.ui.configbool(
1683 b'convert', b'svn.dangerous-set-commit-dates'
1689 b'convert', b'svn.dangerous-set-commit-dates'
1684 ):
1690 ):
1685 # Subverson always uses UTC to represent date and time
1691 # Subverson always uses UTC to represent date and time
1686 date = dateutil.parsedate(commit.date)
1692 date = dateutil.parsedate(commit.date)
1687 date = (date[0], 0)
1693 date = (date[0], 0)
1688
1694
1689 # The only way to set date and time for svn commit is to use propset after commit is done
1695 # The only way to set date and time for svn commit is to use propset after commit is done
1690 self.run(
1696 self.run(
1691 b'propset',
1697 b'propset',
1692 b'svn:date',
1698 b'svn:date',
1693 formatsvndate(date),
1699 formatsvndate(date),
1694 revprop=True,
1700 revprop=True,
1695 revision=rev,
1701 revision=rev,
1696 )
1702 )
1697
1703
1698 for parent in parents:
1704 for parent in parents:
1699 self.addchild(parent, rev)
1705 self.addchild(parent, rev)
1700 return self.revid(rev)
1706 return self.revid(rev)
1701 finally:
1707 finally:
1702 os.unlink(messagefile)
1708 os.unlink(messagefile)
1703
1709
1704 def puttags(self, tags):
1710 def puttags(self, tags):
1705 self.ui.warn(_(b'writing Subversion tags is not yet implemented\n'))
1711 self.ui.warn(_(b'writing Subversion tags is not yet implemented\n'))
1706 return None, None
1712 return None, None
1707
1713
1708 def hascommitfrommap(self, rev):
1714 def hascommitfrommap(self, rev):
1709 # We trust that revisions referenced in a map still is present
1715 # We trust that revisions referenced in a map still is present
1710 # TODO: implement something better if necessary and feasible
1716 # TODO: implement something better if necessary and feasible
1711 return True
1717 return True
1712
1718
1713 def hascommitforsplicemap(self, rev):
1719 def hascommitforsplicemap(self, rev):
1714 # This is not correct as one can convert to an existing subversion
1720 # This is not correct as one can convert to an existing subversion
1715 # repository and childmap would not list all revisions. Too bad.
1721 # repository and childmap would not list all revisions. Too bad.
1716 if rev in self.childmap:
1722 if rev in self.childmap:
1717 return True
1723 return True
1718 raise error.Abort(
1724 raise error.Abort(
1719 _(
1725 _(
1720 b'splice map revision %s not found in subversion '
1726 b'splice map revision %s not found in subversion '
1721 b'child map (revision lookups are not implemented)'
1727 b'child map (revision lookups are not implemented)'
1722 )
1728 )
1723 % rev
1729 % rev
1724 )
1730 )
General Comments 0
You need to be logged in to leave comments. Login now