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