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