##// END OF EJS Templates
convert: convert os.devnull to bytes before trying to join it with other bytes...
Ian Moody -
r43569:8c0fe77f default
parent child Browse files
Show More
@@ -1,375 +1,376
1 # gnuarch.py - GNU Arch support for the convert extension
1 # gnuarch.py - GNU Arch support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 # and others
4 # and others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import email.parser as emailparser
10 import email.parser as emailparser
11 import os
11 import os
12 import shutil
12 import shutil
13 import stat
13 import stat
14 import tempfile
14 import tempfile
15
15
16 from mercurial.i18n import _
16 from mercurial.i18n import _
17 from mercurial import (
17 from mercurial import (
18 encoding,
18 encoding,
19 error,
19 error,
20 pycompat,
20 pycompat,
21 util,
21 util,
22 )
22 )
23 from mercurial.utils import (
23 from mercurial.utils import (
24 dateutil,
24 dateutil,
25 procutil,
25 procutil,
26 )
26 )
27 from . import common
27 from . import common
28
28
29
29
30 class gnuarch_source(common.converter_source, common.commandline):
30 class gnuarch_source(common.converter_source, common.commandline):
31 class gnuarch_rev(object):
31 class gnuarch_rev(object):
32 def __init__(self, rev):
32 def __init__(self, rev):
33 self.rev = rev
33 self.rev = rev
34 self.summary = b''
34 self.summary = b''
35 self.date = None
35 self.date = None
36 self.author = b''
36 self.author = b''
37 self.continuationof = None
37 self.continuationof = None
38 self.add_files = []
38 self.add_files = []
39 self.mod_files = []
39 self.mod_files = []
40 self.del_files = []
40 self.del_files = []
41 self.ren_files = {}
41 self.ren_files = {}
42 self.ren_dirs = {}
42 self.ren_dirs = {}
43
43
44 def __init__(self, ui, repotype, path, revs=None):
44 def __init__(self, ui, repotype, path, revs=None):
45 super(gnuarch_source, self).__init__(ui, repotype, path, revs=revs)
45 super(gnuarch_source, self).__init__(ui, repotype, path, revs=revs)
46
46
47 if not os.path.exists(os.path.join(path, b'{arch}')):
47 if not os.path.exists(os.path.join(path, b'{arch}')):
48 raise common.NoRepo(
48 raise common.NoRepo(
49 _(b"%s does not look like a GNU Arch repository") % path
49 _(b"%s does not look like a GNU Arch repository") % path
50 )
50 )
51
51
52 # Could use checktool, but we want to check for baz or tla.
52 # Could use checktool, but we want to check for baz or tla.
53 self.execmd = None
53 self.execmd = None
54 if procutil.findexe(b'baz'):
54 if procutil.findexe(b'baz'):
55 self.execmd = b'baz'
55 self.execmd = b'baz'
56 else:
56 else:
57 if procutil.findexe(b'tla'):
57 if procutil.findexe(b'tla'):
58 self.execmd = b'tla'
58 self.execmd = b'tla'
59 else:
59 else:
60 raise error.Abort(_(b'cannot find a GNU Arch tool'))
60 raise error.Abort(_(b'cannot find a GNU Arch tool'))
61
61
62 common.commandline.__init__(self, ui, self.execmd)
62 common.commandline.__init__(self, ui, self.execmd)
63
63
64 self.path = os.path.realpath(path)
64 self.path = os.path.realpath(path)
65 self.tmppath = None
65 self.tmppath = None
66
66
67 self.treeversion = None
67 self.treeversion = None
68 self.lastrev = None
68 self.lastrev = None
69 self.changes = {}
69 self.changes = {}
70 self.parents = {}
70 self.parents = {}
71 self.tags = {}
71 self.tags = {}
72 self.catlogparser = emailparser.Parser()
72 self.catlogparser = emailparser.Parser()
73 self.encoding = encoding.encoding
73 self.encoding = encoding.encoding
74 self.archives = []
74 self.archives = []
75
75
76 def before(self):
76 def before(self):
77 # Get registered archives
77 # Get registered archives
78 self.archives = [
78 self.archives = [
79 i.rstrip(b'\n') for i in self.runlines0(b'archives', b'-n')
79 i.rstrip(b'\n') for i in self.runlines0(b'archives', b'-n')
80 ]
80 ]
81
81
82 if self.execmd == b'tla':
82 if self.execmd == b'tla':
83 output = self.run0(b'tree-version', self.path)
83 output = self.run0(b'tree-version', self.path)
84 else:
84 else:
85 output = self.run0(b'tree-version', b'-d', self.path)
85 output = self.run0(b'tree-version', b'-d', self.path)
86 self.treeversion = output.strip()
86 self.treeversion = output.strip()
87
87
88 # Get name of temporary directory
88 # Get name of temporary directory
89 version = self.treeversion.split(b'/')
89 version = self.treeversion.split(b'/')
90 self.tmppath = os.path.join(
90 self.tmppath = os.path.join(
91 pycompat.fsencode(tempfile.gettempdir()), b'hg-%s' % version[1]
91 pycompat.fsencode(tempfile.gettempdir()), b'hg-%s' % version[1]
92 )
92 )
93
93
94 # Generate parents dictionary
94 # Generate parents dictionary
95 self.parents[None] = []
95 self.parents[None] = []
96 treeversion = self.treeversion
96 treeversion = self.treeversion
97 child = None
97 child = None
98 while treeversion:
98 while treeversion:
99 self.ui.status(_(b'analyzing tree version %s...\n') % treeversion)
99 self.ui.status(_(b'analyzing tree version %s...\n') % treeversion)
100
100
101 archive = treeversion.split(b'/')[0]
101 archive = treeversion.split(b'/')[0]
102 if archive not in self.archives:
102 if archive not in self.archives:
103 self.ui.status(
103 self.ui.status(
104 _(
104 _(
105 b'tree analysis stopped because it points to '
105 b'tree analysis stopped because it points to '
106 b'an unregistered archive %s...\n'
106 b'an unregistered archive %s...\n'
107 )
107 )
108 % archive
108 % archive
109 )
109 )
110 break
110 break
111
111
112 # Get the complete list of revisions for that tree version
112 # Get the complete list of revisions for that tree version
113 output, status = self.runlines(
113 output, status = self.runlines(
114 b'revisions', b'-r', b'-f', treeversion
114 b'revisions', b'-r', b'-f', treeversion
115 )
115 )
116 self.checkexit(
116 self.checkexit(
117 status, b'failed retrieving revisions for %s' % treeversion
117 status, b'failed retrieving revisions for %s' % treeversion
118 )
118 )
119
119
120 # No new iteration unless a revision has a continuation-of header
120 # No new iteration unless a revision has a continuation-of header
121 treeversion = None
121 treeversion = None
122
122
123 for l in output:
123 for l in output:
124 rev = l.strip()
124 rev = l.strip()
125 self.changes[rev] = self.gnuarch_rev(rev)
125 self.changes[rev] = self.gnuarch_rev(rev)
126 self.parents[rev] = []
126 self.parents[rev] = []
127
127
128 # Read author, date and summary
128 # Read author, date and summary
129 catlog, status = self.run(b'cat-log', b'-d', self.path, rev)
129 catlog, status = self.run(b'cat-log', b'-d', self.path, rev)
130 if status:
130 if status:
131 catlog = self.run0(b'cat-archive-log', rev)
131 catlog = self.run0(b'cat-archive-log', rev)
132 self._parsecatlog(catlog, rev)
132 self._parsecatlog(catlog, rev)
133
133
134 # Populate the parents map
134 # Populate the parents map
135 self.parents[child].append(rev)
135 self.parents[child].append(rev)
136
136
137 # Keep track of the current revision as the child of the next
137 # Keep track of the current revision as the child of the next
138 # revision scanned
138 # revision scanned
139 child = rev
139 child = rev
140
140
141 # Check if we have to follow the usual incremental history
141 # Check if we have to follow the usual incremental history
142 # or if we have to 'jump' to a different treeversion given
142 # or if we have to 'jump' to a different treeversion given
143 # by the continuation-of header.
143 # by the continuation-of header.
144 if self.changes[rev].continuationof:
144 if self.changes[rev].continuationof:
145 treeversion = b'--'.join(
145 treeversion = b'--'.join(
146 self.changes[rev].continuationof.split(b'--')[:-1]
146 self.changes[rev].continuationof.split(b'--')[:-1]
147 )
147 )
148 break
148 break
149
149
150 # If we reached a base-0 revision w/o any continuation-of
150 # If we reached a base-0 revision w/o any continuation-of
151 # header, it means the tree history ends here.
151 # header, it means the tree history ends here.
152 if rev[-6:] == b'base-0':
152 if rev[-6:] == b'base-0':
153 break
153 break
154
154
155 def after(self):
155 def after(self):
156 self.ui.debug(b'cleaning up %s\n' % self.tmppath)
156 self.ui.debug(b'cleaning up %s\n' % self.tmppath)
157 shutil.rmtree(self.tmppath, ignore_errors=True)
157 shutil.rmtree(self.tmppath, ignore_errors=True)
158
158
159 def getheads(self):
159 def getheads(self):
160 return self.parents[None]
160 return self.parents[None]
161
161
162 def getfile(self, name, rev):
162 def getfile(self, name, rev):
163 if rev != self.lastrev:
163 if rev != self.lastrev:
164 raise error.Abort(_(b'internal calling inconsistency'))
164 raise error.Abort(_(b'internal calling inconsistency'))
165
165
166 if not os.path.lexists(os.path.join(self.tmppath, name)):
166 if not os.path.lexists(os.path.join(self.tmppath, name)):
167 return None, None
167 return None, None
168
168
169 return self._getfile(name, rev)
169 return self._getfile(name, rev)
170
170
171 def getchanges(self, rev, full):
171 def getchanges(self, rev, full):
172 if full:
172 if full:
173 raise error.Abort(_(b"convert from arch does not support --full"))
173 raise error.Abort(_(b"convert from arch does not support --full"))
174 self._update(rev)
174 self._update(rev)
175 changes = []
175 changes = []
176 copies = {}
176 copies = {}
177
177
178 for f in self.changes[rev].add_files:
178 for f in self.changes[rev].add_files:
179 changes.append((f, rev))
179 changes.append((f, rev))
180
180
181 for f in self.changes[rev].mod_files:
181 for f in self.changes[rev].mod_files:
182 changes.append((f, rev))
182 changes.append((f, rev))
183
183
184 for f in self.changes[rev].del_files:
184 for f in self.changes[rev].del_files:
185 changes.append((f, rev))
185 changes.append((f, rev))
186
186
187 for src in self.changes[rev].ren_files:
187 for src in self.changes[rev].ren_files:
188 to = self.changes[rev].ren_files[src]
188 to = self.changes[rev].ren_files[src]
189 changes.append((src, rev))
189 changes.append((src, rev))
190 changes.append((to, rev))
190 changes.append((to, rev))
191 copies[to] = src
191 copies[to] = src
192
192
193 for src in self.changes[rev].ren_dirs:
193 for src in self.changes[rev].ren_dirs:
194 to = self.changes[rev].ren_dirs[src]
194 to = self.changes[rev].ren_dirs[src]
195 chgs, cps = self._rendirchanges(src, to)
195 chgs, cps = self._rendirchanges(src, to)
196 changes += [(f, rev) for f in chgs]
196 changes += [(f, rev) for f in chgs]
197 copies.update(cps)
197 copies.update(cps)
198
198
199 self.lastrev = rev
199 self.lastrev = rev
200 return sorted(set(changes)), copies, set()
200 return sorted(set(changes)), copies, set()
201
201
202 def getcommit(self, rev):
202 def getcommit(self, rev):
203 changes = self.changes[rev]
203 changes = self.changes[rev]
204 return common.commit(
204 return common.commit(
205 author=changes.author,
205 author=changes.author,
206 date=changes.date,
206 date=changes.date,
207 desc=changes.summary,
207 desc=changes.summary,
208 parents=self.parents[rev],
208 parents=self.parents[rev],
209 rev=rev,
209 rev=rev,
210 )
210 )
211
211
212 def gettags(self):
212 def gettags(self):
213 return self.tags
213 return self.tags
214
214
215 def _execute(self, cmd, *args, **kwargs):
215 def _execute(self, cmd, *args, **kwargs):
216 cmdline = [self.execmd, cmd]
216 cmdline = [self.execmd, cmd]
217 cmdline += args
217 cmdline += args
218 cmdline = [procutil.shellquote(arg) for arg in cmdline]
218 cmdline = [procutil.shellquote(arg) for arg in cmdline]
219 cmdline += [b'>', os.devnull, b'2>', os.devnull]
219 bdevnull = pycompat.bytestr(os.devnull)
220 cmdline += [b'>', bdevnull, b'2>', bdevnull]
220 cmdline = procutil.quotecommand(b' '.join(cmdline))
221 cmdline = procutil.quotecommand(b' '.join(cmdline))
221 self.ui.debug(cmdline, b'\n')
222 self.ui.debug(cmdline, b'\n')
222 return os.system(pycompat.rapply(procutil.tonativestr, cmdline))
223 return os.system(pycompat.rapply(procutil.tonativestr, cmdline))
223
224
224 def _update(self, rev):
225 def _update(self, rev):
225 self.ui.debug(b'applying revision %s...\n' % rev)
226 self.ui.debug(b'applying revision %s...\n' % rev)
226 changeset, status = self.runlines(b'replay', b'-d', self.tmppath, rev)
227 changeset, status = self.runlines(b'replay', b'-d', self.tmppath, rev)
227 if status:
228 if status:
228 # Something went wrong while merging (baz or tla
229 # Something went wrong while merging (baz or tla
229 # issue?), get latest revision and try from there
230 # issue?), get latest revision and try from there
230 shutil.rmtree(self.tmppath, ignore_errors=True)
231 shutil.rmtree(self.tmppath, ignore_errors=True)
231 self._obtainrevision(rev)
232 self._obtainrevision(rev)
232 else:
233 else:
233 old_rev = self.parents[rev][0]
234 old_rev = self.parents[rev][0]
234 self.ui.debug(
235 self.ui.debug(
235 b'computing changeset between %s and %s...\n' % (old_rev, rev)
236 b'computing changeset between %s and %s...\n' % (old_rev, rev)
236 )
237 )
237 self._parsechangeset(changeset, rev)
238 self._parsechangeset(changeset, rev)
238
239
239 def _getfile(self, name, rev):
240 def _getfile(self, name, rev):
240 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
241 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
241 if stat.S_ISLNK(mode):
242 if stat.S_ISLNK(mode):
242 data = util.readlink(os.path.join(self.tmppath, name))
243 data = util.readlink(os.path.join(self.tmppath, name))
243 if mode:
244 if mode:
244 mode = b'l'
245 mode = b'l'
245 else:
246 else:
246 mode = b''
247 mode = b''
247 else:
248 else:
248 data = util.readfile(os.path.join(self.tmppath, name))
249 data = util.readfile(os.path.join(self.tmppath, name))
249 mode = (mode & 0o111) and b'x' or b''
250 mode = (mode & 0o111) and b'x' or b''
250 return data, mode
251 return data, mode
251
252
252 def _exclude(self, name):
253 def _exclude(self, name):
253 exclude = [b'{arch}', b'.arch-ids', b'.arch-inventory']
254 exclude = [b'{arch}', b'.arch-ids', b'.arch-inventory']
254 for exc in exclude:
255 for exc in exclude:
255 if name.find(exc) != -1:
256 if name.find(exc) != -1:
256 return True
257 return True
257 return False
258 return False
258
259
259 def _readcontents(self, path):
260 def _readcontents(self, path):
260 files = []
261 files = []
261 contents = os.listdir(path)
262 contents = os.listdir(path)
262 while len(contents) > 0:
263 while len(contents) > 0:
263 c = contents.pop()
264 c = contents.pop()
264 p = os.path.join(path, c)
265 p = os.path.join(path, c)
265 # os.walk could be used, but here we avoid internal GNU
266 # os.walk could be used, but here we avoid internal GNU
266 # Arch files and directories, thus saving a lot time.
267 # Arch files and directories, thus saving a lot time.
267 if not self._exclude(p):
268 if not self._exclude(p):
268 if os.path.isdir(p):
269 if os.path.isdir(p):
269 contents += [os.path.join(c, f) for f in os.listdir(p)]
270 contents += [os.path.join(c, f) for f in os.listdir(p)]
270 else:
271 else:
271 files.append(c)
272 files.append(c)
272 return files
273 return files
273
274
274 def _rendirchanges(self, src, dest):
275 def _rendirchanges(self, src, dest):
275 changes = []
276 changes = []
276 copies = {}
277 copies = {}
277 files = self._readcontents(os.path.join(self.tmppath, dest))
278 files = self._readcontents(os.path.join(self.tmppath, dest))
278 for f in files:
279 for f in files:
279 s = os.path.join(src, f)
280 s = os.path.join(src, f)
280 d = os.path.join(dest, f)
281 d = os.path.join(dest, f)
281 changes.append(s)
282 changes.append(s)
282 changes.append(d)
283 changes.append(d)
283 copies[d] = s
284 copies[d] = s
284 return changes, copies
285 return changes, copies
285
286
286 def _obtainrevision(self, rev):
287 def _obtainrevision(self, rev):
287 self.ui.debug(b'obtaining revision %s...\n' % rev)
288 self.ui.debug(b'obtaining revision %s...\n' % rev)
288 output = self._execute(b'get', rev, self.tmppath)
289 output = self._execute(b'get', rev, self.tmppath)
289 self.checkexit(output)
290 self.checkexit(output)
290 self.ui.debug(b'analyzing revision %s...\n' % rev)
291 self.ui.debug(b'analyzing revision %s...\n' % rev)
291 files = self._readcontents(self.tmppath)
292 files = self._readcontents(self.tmppath)
292 self.changes[rev].add_files += files
293 self.changes[rev].add_files += files
293
294
294 def _stripbasepath(self, path):
295 def _stripbasepath(self, path):
295 if path.startswith(b'./'):
296 if path.startswith(b'./'):
296 return path[2:]
297 return path[2:]
297 return path
298 return path
298
299
299 def _parsecatlog(self, data, rev):
300 def _parsecatlog(self, data, rev):
300 try:
301 try:
301 catlog = self.catlogparser.parsestr(data)
302 catlog = self.catlogparser.parsestr(data)
302
303
303 # Commit date
304 # Commit date
304 self.changes[rev].date = dateutil.datestr(
305 self.changes[rev].date = dateutil.datestr(
305 dateutil.strdate(catlog[b'Standard-date'], b'%Y-%m-%d %H:%M:%S')
306 dateutil.strdate(catlog[b'Standard-date'], b'%Y-%m-%d %H:%M:%S')
306 )
307 )
307
308
308 # Commit author
309 # Commit author
309 self.changes[rev].author = self.recode(catlog[b'Creator'])
310 self.changes[rev].author = self.recode(catlog[b'Creator'])
310
311
311 # Commit description
312 # Commit description
312 self.changes[rev].summary = b'\n\n'.join(
313 self.changes[rev].summary = b'\n\n'.join(
313 (catlog[b'Summary'], catlog.get_payload())
314 (catlog[b'Summary'], catlog.get_payload())
314 )
315 )
315 self.changes[rev].summary = self.recode(self.changes[rev].summary)
316 self.changes[rev].summary = self.recode(self.changes[rev].summary)
316
317
317 # Commit revision origin when dealing with a branch or tag
318 # Commit revision origin when dealing with a branch or tag
318 if b'Continuation-of' in catlog:
319 if b'Continuation-of' in catlog:
319 self.changes[rev].continuationof = self.recode(
320 self.changes[rev].continuationof = self.recode(
320 catlog[b'Continuation-of']
321 catlog[b'Continuation-of']
321 )
322 )
322 except Exception:
323 except Exception:
323 raise error.Abort(_(b'could not parse cat-log of %s') % rev)
324 raise error.Abort(_(b'could not parse cat-log of %s') % rev)
324
325
325 def _parsechangeset(self, data, rev):
326 def _parsechangeset(self, data, rev):
326 for l in data:
327 for l in data:
327 l = l.strip()
328 l = l.strip()
328 # Added file (ignore added directory)
329 # Added file (ignore added directory)
329 if l.startswith(b'A') and not l.startswith(b'A/'):
330 if l.startswith(b'A') and not l.startswith(b'A/'):
330 file = self._stripbasepath(l[1:].strip())
331 file = self._stripbasepath(l[1:].strip())
331 if not self._exclude(file):
332 if not self._exclude(file):
332 self.changes[rev].add_files.append(file)
333 self.changes[rev].add_files.append(file)
333 # Deleted file (ignore deleted directory)
334 # Deleted file (ignore deleted directory)
334 elif l.startswith(b'D') and not l.startswith(b'D/'):
335 elif l.startswith(b'D') and not l.startswith(b'D/'):
335 file = self._stripbasepath(l[1:].strip())
336 file = self._stripbasepath(l[1:].strip())
336 if not self._exclude(file):
337 if not self._exclude(file):
337 self.changes[rev].del_files.append(file)
338 self.changes[rev].del_files.append(file)
338 # Modified binary file
339 # Modified binary file
339 elif l.startswith(b'Mb'):
340 elif l.startswith(b'Mb'):
340 file = self._stripbasepath(l[2:].strip())
341 file = self._stripbasepath(l[2:].strip())
341 if not self._exclude(file):
342 if not self._exclude(file):
342 self.changes[rev].mod_files.append(file)
343 self.changes[rev].mod_files.append(file)
343 # Modified link
344 # Modified link
344 elif l.startswith(b'M->'):
345 elif l.startswith(b'M->'):
345 file = self._stripbasepath(l[3:].strip())
346 file = self._stripbasepath(l[3:].strip())
346 if not self._exclude(file):
347 if not self._exclude(file):
347 self.changes[rev].mod_files.append(file)
348 self.changes[rev].mod_files.append(file)
348 # Modified file
349 # Modified file
349 elif l.startswith(b'M'):
350 elif l.startswith(b'M'):
350 file = self._stripbasepath(l[1:].strip())
351 file = self._stripbasepath(l[1:].strip())
351 if not self._exclude(file):
352 if not self._exclude(file):
352 self.changes[rev].mod_files.append(file)
353 self.changes[rev].mod_files.append(file)
353 # Renamed file (or link)
354 # Renamed file (or link)
354 elif l.startswith(b'=>'):
355 elif l.startswith(b'=>'):
355 files = l[2:].strip().split(b' ')
356 files = l[2:].strip().split(b' ')
356 if len(files) == 1:
357 if len(files) == 1:
357 files = l[2:].strip().split(b'\t')
358 files = l[2:].strip().split(b'\t')
358 src = self._stripbasepath(files[0])
359 src = self._stripbasepath(files[0])
359 dst = self._stripbasepath(files[1])
360 dst = self._stripbasepath(files[1])
360 if not self._exclude(src) and not self._exclude(dst):
361 if not self._exclude(src) and not self._exclude(dst):
361 self.changes[rev].ren_files[src] = dst
362 self.changes[rev].ren_files[src] = dst
362 # Conversion from file to link or from link to file (modified)
363 # Conversion from file to link or from link to file (modified)
363 elif l.startswith(b'ch'):
364 elif l.startswith(b'ch'):
364 file = self._stripbasepath(l[2:].strip())
365 file = self._stripbasepath(l[2:].strip())
365 if not self._exclude(file):
366 if not self._exclude(file):
366 self.changes[rev].mod_files.append(file)
367 self.changes[rev].mod_files.append(file)
367 # Renamed directory
368 # Renamed directory
368 elif l.startswith(b'/>'):
369 elif l.startswith(b'/>'):
369 dirs = l[2:].strip().split(b' ')
370 dirs = l[2:].strip().split(b' ')
370 if len(dirs) == 1:
371 if len(dirs) == 1:
371 dirs = l[2:].strip().split(b'\t')
372 dirs = l[2:].strip().split(b'\t')
372 src = self._stripbasepath(dirs[0])
373 src = self._stripbasepath(dirs[0])
373 dst = self._stripbasepath(dirs[1])
374 dst = self._stripbasepath(dirs[1])
374 if not self._exclude(src) and not self._exclude(dst):
375 if not self._exclude(src) and not self._exclude(dst):
375 self.changes[rev].ren_dirs[src] = dst
376 self.changes[rev].ren_dirs[src] = dst
General Comments 0
You need to be logged in to leave comments. Login now