##// END OF EJS Templates
convert: use branchmap to change default branch in destination (issue3469)...
lstewart -
r20331:1d155582 stable
parent child Browse files
Show More
@@ -1,523 +1,528 b''
1 1 # convcmd - convert extension commands definition
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 9 from cvs import convert_cvs
10 10 from darcs import darcs_source
11 11 from git import convert_git
12 12 from hg import mercurial_source, mercurial_sink
13 13 from subversion import svn_source, svn_sink
14 14 from monotone import monotone_source
15 15 from gnuarch import gnuarch_source
16 16 from bzr import bzr_source
17 17 from p4 import p4_source
18 18 import filemap
19 19
20 20 import os, shutil, shlex
21 21 from mercurial import hg, util, encoding
22 22 from mercurial.i18n import _
23 23
24 24 orig_encoding = 'ascii'
25 25
26 26 def recode(s):
27 27 if isinstance(s, unicode):
28 28 return s.encode(orig_encoding, 'replace')
29 29 else:
30 30 return s.decode('utf-8').encode(orig_encoding, 'replace')
31 31
32 32 source_converters = [
33 33 ('cvs', convert_cvs, 'branchsort'),
34 34 ('git', convert_git, 'branchsort'),
35 35 ('svn', svn_source, 'branchsort'),
36 36 ('hg', mercurial_source, 'sourcesort'),
37 37 ('darcs', darcs_source, 'branchsort'),
38 38 ('mtn', monotone_source, 'branchsort'),
39 39 ('gnuarch', gnuarch_source, 'branchsort'),
40 40 ('bzr', bzr_source, 'branchsort'),
41 41 ('p4', p4_source, 'branchsort'),
42 42 ]
43 43
44 44 sink_converters = [
45 45 ('hg', mercurial_sink),
46 46 ('svn', svn_sink),
47 47 ]
48 48
49 49 def convertsource(ui, path, type, rev):
50 50 exceptions = []
51 51 if type and type not in [s[0] for s in source_converters]:
52 52 raise util.Abort(_('%s: invalid source repository type') % type)
53 53 for name, source, sortmode in source_converters:
54 54 try:
55 55 if not type or name == type:
56 56 return source(ui, path, rev), sortmode
57 57 except (NoRepo, MissingTool), inst:
58 58 exceptions.append(inst)
59 59 if not ui.quiet:
60 60 for inst in exceptions:
61 61 ui.write("%s\n" % inst)
62 62 raise util.Abort(_('%s: missing or unsupported repository') % path)
63 63
64 64 def convertsink(ui, path, type):
65 65 if type and type not in [s[0] for s in sink_converters]:
66 66 raise util.Abort(_('%s: invalid destination repository type') % type)
67 67 for name, sink in sink_converters:
68 68 try:
69 69 if not type or name == type:
70 70 return sink(ui, path)
71 71 except NoRepo, inst:
72 72 ui.note(_("convert: %s\n") % inst)
73 73 except MissingTool, inst:
74 74 raise util.Abort('%s\n' % inst)
75 75 raise util.Abort(_('%s: unknown repository type') % path)
76 76
77 77 class progresssource(object):
78 78 def __init__(self, ui, source, filecount):
79 79 self.ui = ui
80 80 self.source = source
81 81 self.filecount = filecount
82 82 self.retrieved = 0
83 83
84 84 def getfile(self, file, rev):
85 85 self.retrieved += 1
86 86 self.ui.progress(_('getting files'), self.retrieved,
87 87 item=file, total=self.filecount)
88 88 return self.source.getfile(file, rev)
89 89
90 90 def lookuprev(self, rev):
91 91 return self.source.lookuprev(rev)
92 92
93 93 def close(self):
94 94 self.ui.progress(_('getting files'), None)
95 95
96 96 class converter(object):
97 97 def __init__(self, ui, source, dest, revmapfile, opts):
98 98
99 99 self.source = source
100 100 self.dest = dest
101 101 self.ui = ui
102 102 self.opts = opts
103 103 self.commitcache = {}
104 104 self.authors = {}
105 105 self.authorfile = None
106 106
107 107 # Record converted revisions persistently: maps source revision
108 108 # ID to target revision ID (both strings). (This is how
109 109 # incremental conversions work.)
110 110 self.map = mapfile(ui, revmapfile)
111 111
112 112 # Read first the dst author map if any
113 113 authorfile = self.dest.authorfile()
114 114 if authorfile and os.path.exists(authorfile):
115 115 self.readauthormap(authorfile)
116 116 # Extend/Override with new author map if necessary
117 117 if opts.get('authormap'):
118 118 self.readauthormap(opts.get('authormap'))
119 119 self.authorfile = self.dest.authorfile()
120 120
121 121 self.splicemap = self.parsesplicemap(opts.get('splicemap'))
122 122 self.branchmap = mapfile(ui, opts.get('branchmap'))
123 123
124 124 def parsesplicemap(self, path):
125 125 """ check and validate the splicemap format and
126 126 return a child/parents dictionary.
127 127 Format checking has two parts.
128 128 1. generic format which is same across all source types
129 129 2. specific format checking which may be different for
130 130 different source type. This logic is implemented in
131 131 checkrevformat function in source files like
132 132 hg.py, subversion.py etc.
133 133 """
134 134
135 135 if not path:
136 136 return {}
137 137 m = {}
138 138 try:
139 139 fp = open(path, 'r')
140 140 for i, line in enumerate(fp):
141 141 line = line.splitlines()[0].rstrip()
142 142 if not line:
143 143 # Ignore blank lines
144 144 continue
145 145 # split line
146 146 lex = shlex.shlex(line, posix=True)
147 147 lex.whitespace_split = True
148 148 lex.whitespace += ','
149 149 line = list(lex)
150 150 # check number of parents
151 151 if not (2 <= len(line) <= 3):
152 152 raise util.Abort(_('syntax error in %s(%d): child parent1'
153 153 '[,parent2] expected') % (path, i + 1))
154 154 for part in line:
155 155 self.source.checkrevformat(part)
156 156 child, p1, p2 = line[0], line[1:2], line[2:]
157 157 if p1 == p2:
158 158 m[child] = p1
159 159 else:
160 160 m[child] = p1 + p2
161 161 # if file does not exist or error reading, exit
162 162 except IOError:
163 163 raise util.Abort(_('splicemap file not found or error reading %s:')
164 164 % path)
165 165 return m
166 166
167 167
168 168 def walktree(self, heads):
169 169 '''Return a mapping that identifies the uncommitted parents of every
170 170 uncommitted changeset.'''
171 171 visit = heads
172 172 known = set()
173 173 parents = {}
174 174 while visit:
175 175 n = visit.pop(0)
176 176 if n in known or n in self.map:
177 177 continue
178 178 known.add(n)
179 179 self.ui.progress(_('scanning'), len(known), unit=_('revisions'))
180 180 commit = self.cachecommit(n)
181 181 parents[n] = []
182 182 for p in commit.parents:
183 183 parents[n].append(p)
184 184 visit.append(p)
185 185 self.ui.progress(_('scanning'), None)
186 186
187 187 return parents
188 188
189 189 def mergesplicemap(self, parents, splicemap):
190 190 """A splicemap redefines child/parent relationships. Check the
191 191 map contains valid revision identifiers and merge the new
192 192 links in the source graph.
193 193 """
194 194 for c in sorted(splicemap):
195 195 if c not in parents:
196 196 if not self.dest.hascommit(self.map.get(c, c)):
197 197 # Could be in source but not converted during this run
198 198 self.ui.warn(_('splice map revision %s is not being '
199 199 'converted, ignoring\n') % c)
200 200 continue
201 201 pc = []
202 202 for p in splicemap[c]:
203 203 # We do not have to wait for nodes already in dest.
204 204 if self.dest.hascommit(self.map.get(p, p)):
205 205 continue
206 206 # Parent is not in dest and not being converted, not good
207 207 if p not in parents:
208 208 raise util.Abort(_('unknown splice map parent: %s') % p)
209 209 pc.append(p)
210 210 parents[c] = pc
211 211
212 212 def toposort(self, parents, sortmode):
213 213 '''Return an ordering such that every uncommitted changeset is
214 214 preceded by all its uncommitted ancestors.'''
215 215
216 216 def mapchildren(parents):
217 217 """Return a (children, roots) tuple where 'children' maps parent
218 218 revision identifiers to children ones, and 'roots' is the list of
219 219 revisions without parents. 'parents' must be a mapping of revision
220 220 identifier to its parents ones.
221 221 """
222 222 visit = sorted(parents)
223 223 seen = set()
224 224 children = {}
225 225 roots = []
226 226
227 227 while visit:
228 228 n = visit.pop(0)
229 229 if n in seen:
230 230 continue
231 231 seen.add(n)
232 232 # Ensure that nodes without parents are present in the
233 233 # 'children' mapping.
234 234 children.setdefault(n, [])
235 235 hasparent = False
236 236 for p in parents[n]:
237 237 if p not in self.map:
238 238 visit.append(p)
239 239 hasparent = True
240 240 children.setdefault(p, []).append(n)
241 241 if not hasparent:
242 242 roots.append(n)
243 243
244 244 return children, roots
245 245
246 246 # Sort functions are supposed to take a list of revisions which
247 247 # can be converted immediately and pick one
248 248
249 249 def makebranchsorter():
250 250 """If the previously converted revision has a child in the
251 251 eligible revisions list, pick it. Return the list head
252 252 otherwise. Branch sort attempts to minimize branch
253 253 switching, which is harmful for Mercurial backend
254 254 compression.
255 255 """
256 256 prev = [None]
257 257 def picknext(nodes):
258 258 next = nodes[0]
259 259 for n in nodes:
260 260 if prev[0] in parents[n]:
261 261 next = n
262 262 break
263 263 prev[0] = next
264 264 return next
265 265 return picknext
266 266
267 267 def makesourcesorter():
268 268 """Source specific sort."""
269 269 keyfn = lambda n: self.commitcache[n].sortkey
270 270 def picknext(nodes):
271 271 return sorted(nodes, key=keyfn)[0]
272 272 return picknext
273 273
274 274 def makeclosesorter():
275 275 """Close order sort."""
276 276 keyfn = lambda n: ('close' not in self.commitcache[n].extra,
277 277 self.commitcache[n].sortkey)
278 278 def picknext(nodes):
279 279 return sorted(nodes, key=keyfn)[0]
280 280 return picknext
281 281
282 282 def makedatesorter():
283 283 """Sort revisions by date."""
284 284 dates = {}
285 285 def getdate(n):
286 286 if n not in dates:
287 287 dates[n] = util.parsedate(self.commitcache[n].date)
288 288 return dates[n]
289 289
290 290 def picknext(nodes):
291 291 return min([(getdate(n), n) for n in nodes])[1]
292 292
293 293 return picknext
294 294
295 295 if sortmode == 'branchsort':
296 296 picknext = makebranchsorter()
297 297 elif sortmode == 'datesort':
298 298 picknext = makedatesorter()
299 299 elif sortmode == 'sourcesort':
300 300 picknext = makesourcesorter()
301 301 elif sortmode == 'closesort':
302 302 picknext = makeclosesorter()
303 303 else:
304 304 raise util.Abort(_('unknown sort mode: %s') % sortmode)
305 305
306 306 children, actives = mapchildren(parents)
307 307
308 308 s = []
309 309 pendings = {}
310 310 while actives:
311 311 n = picknext(actives)
312 312 actives.remove(n)
313 313 s.append(n)
314 314
315 315 # Update dependents list
316 316 for c in children.get(n, []):
317 317 if c not in pendings:
318 318 pendings[c] = [p for p in parents[c] if p not in self.map]
319 319 try:
320 320 pendings[c].remove(n)
321 321 except ValueError:
322 322 raise util.Abort(_('cycle detected between %s and %s')
323 323 % (recode(c), recode(n)))
324 324 if not pendings[c]:
325 325 # Parents are converted, node is eligible
326 326 actives.insert(0, c)
327 327 pendings[c] = None
328 328
329 329 if len(s) != len(parents):
330 330 raise util.Abort(_("not all revisions were sorted"))
331 331
332 332 return s
333 333
334 334 def writeauthormap(self):
335 335 authorfile = self.authorfile
336 336 if authorfile:
337 337 self.ui.status(_('writing author map file %s\n') % authorfile)
338 338 ofile = open(authorfile, 'w+')
339 339 for author in self.authors:
340 340 ofile.write("%s=%s\n" % (author, self.authors[author]))
341 341 ofile.close()
342 342
343 343 def readauthormap(self, authorfile):
344 344 afile = open(authorfile, 'r')
345 345 for line in afile:
346 346
347 347 line = line.strip()
348 348 if not line or line.startswith('#'):
349 349 continue
350 350
351 351 try:
352 352 srcauthor, dstauthor = line.split('=', 1)
353 353 except ValueError:
354 354 msg = _('ignoring bad line in author map file %s: %s\n')
355 355 self.ui.warn(msg % (authorfile, line.rstrip()))
356 356 continue
357 357
358 358 srcauthor = srcauthor.strip()
359 359 dstauthor = dstauthor.strip()
360 360 if self.authors.get(srcauthor) in (None, dstauthor):
361 361 msg = _('mapping author %s to %s\n')
362 362 self.ui.debug(msg % (srcauthor, dstauthor))
363 363 self.authors[srcauthor] = dstauthor
364 364 continue
365 365
366 366 m = _('overriding mapping for author %s, was %s, will be %s\n')
367 367 self.ui.status(m % (srcauthor, self.authors[srcauthor], dstauthor))
368 368
369 369 afile.close()
370 370
371 371 def cachecommit(self, rev):
372 372 commit = self.source.getcommit(rev)
373 373 commit.author = self.authors.get(commit.author, commit.author)
374 commit.branch = self.branchmap.get(commit.branch, commit.branch)
374 # If commit.branch is None, this commit is coming from the source
375 # repository's default branch and destined for the default branch in the
376 # destination repository. For such commits, passing a literal "None"
377 # string to branchmap.get() below allows the user to map "None" to an
378 # alternate default branch in the destination repository.
379 commit.branch = self.branchmap.get(str(commit.branch), commit.branch)
375 380 self.commitcache[rev] = commit
376 381 return commit
377 382
378 383 def copy(self, rev):
379 384 commit = self.commitcache[rev]
380 385
381 386 changes = self.source.getchanges(rev)
382 387 if isinstance(changes, basestring):
383 388 if changes == SKIPREV:
384 389 dest = SKIPREV
385 390 else:
386 391 dest = self.map[changes]
387 392 self.map[rev] = dest
388 393 return
389 394 files, copies = changes
390 395 pbranches = []
391 396 if commit.parents:
392 397 for prev in commit.parents:
393 398 if prev not in self.commitcache:
394 399 self.cachecommit(prev)
395 400 pbranches.append((self.map[prev],
396 401 self.commitcache[prev].branch))
397 402 self.dest.setbranch(commit.branch, pbranches)
398 403 try:
399 404 parents = self.splicemap[rev]
400 405 self.ui.status(_('spliced in %s as parents of %s\n') %
401 406 (parents, rev))
402 407 parents = [self.map.get(p, p) for p in parents]
403 408 except KeyError:
404 409 parents = [b[0] for b in pbranches]
405 410 source = progresssource(self.ui, self.source, len(files))
406 411 newnode = self.dest.putcommit(files, copies, parents, commit,
407 412 source, self.map)
408 413 source.close()
409 414 self.source.converted(rev, newnode)
410 415 self.map[rev] = newnode
411 416
412 417 def convert(self, sortmode):
413 418 try:
414 419 self.source.before()
415 420 self.dest.before()
416 421 self.source.setrevmap(self.map)
417 422 self.ui.status(_("scanning source...\n"))
418 423 heads = self.source.getheads()
419 424 parents = self.walktree(heads)
420 425 self.mergesplicemap(parents, self.splicemap)
421 426 self.ui.status(_("sorting...\n"))
422 427 t = self.toposort(parents, sortmode)
423 428 num = len(t)
424 429 c = None
425 430
426 431 self.ui.status(_("converting...\n"))
427 432 for i, c in enumerate(t):
428 433 num -= 1
429 434 desc = self.commitcache[c].desc
430 435 if "\n" in desc:
431 436 desc = desc.splitlines()[0]
432 437 # convert log message to local encoding without using
433 438 # tolocal() because the encoding.encoding convert()
434 439 # uses is 'utf-8'
435 440 self.ui.status("%d %s\n" % (num, recode(desc)))
436 441 self.ui.note(_("source: %s\n") % recode(c))
437 442 self.ui.progress(_('converting'), i, unit=_('revisions'),
438 443 total=len(t))
439 444 self.copy(c)
440 445 self.ui.progress(_('converting'), None)
441 446
442 447 tags = self.source.gettags()
443 448 ctags = {}
444 449 for k in tags:
445 450 v = tags[k]
446 451 if self.map.get(v, SKIPREV) != SKIPREV:
447 452 ctags[k] = self.map[v]
448 453
449 454 if c and ctags:
450 455 nrev, tagsparent = self.dest.puttags(ctags)
451 456 if nrev and tagsparent:
452 457 # write another hash correspondence to override the previous
453 458 # one so we don't end up with extra tag heads
454 459 tagsparents = [e for e in self.map.iteritems()
455 460 if e[1] == tagsparent]
456 461 if tagsparents:
457 462 self.map[tagsparents[0][0]] = nrev
458 463
459 464 bookmarks = self.source.getbookmarks()
460 465 cbookmarks = {}
461 466 for k in bookmarks:
462 467 v = bookmarks[k]
463 468 if self.map.get(v, SKIPREV) != SKIPREV:
464 469 cbookmarks[k] = self.map[v]
465 470
466 471 if c and cbookmarks:
467 472 self.dest.putbookmarks(cbookmarks)
468 473
469 474 self.writeauthormap()
470 475 finally:
471 476 self.cleanup()
472 477
473 478 def cleanup(self):
474 479 try:
475 480 self.dest.after()
476 481 finally:
477 482 self.source.after()
478 483 self.map.close()
479 484
480 485 def convert(ui, src, dest=None, revmapfile=None, **opts):
481 486 global orig_encoding
482 487 orig_encoding = encoding.encoding
483 488 encoding.encoding = 'UTF-8'
484 489
485 490 # support --authors as an alias for --authormap
486 491 if not opts.get('authormap'):
487 492 opts['authormap'] = opts.get('authors')
488 493
489 494 if not dest:
490 495 dest = hg.defaultdest(src) + "-hg"
491 496 ui.status(_("assuming destination %s\n") % dest)
492 497
493 498 destc = convertsink(ui, dest, opts.get('dest_type'))
494 499
495 500 try:
496 501 srcc, defaultsort = convertsource(ui, src, opts.get('source_type'),
497 502 opts.get('rev'))
498 503 except Exception:
499 504 for path in destc.created:
500 505 shutil.rmtree(path, True)
501 506 raise
502 507
503 508 sortmodes = ('branchsort', 'datesort', 'sourcesort', 'closesort')
504 509 sortmode = [m for m in sortmodes if opts.get(m)]
505 510 if len(sortmode) > 1:
506 511 raise util.Abort(_('more than one sort mode specified'))
507 512 sortmode = sortmode and sortmode[0] or defaultsort
508 513 if sortmode == 'sourcesort' and not srcc.hasnativeorder():
509 514 raise util.Abort(_('--sourcesort is not supported by this data source'))
510 515 if sortmode == 'closesort' and not srcc.hasnativeclose():
511 516 raise util.Abort(_('--closesort is not supported by this data source'))
512 517
513 518 fmap = opts.get('filemap')
514 519 if fmap:
515 520 srcc = filemap.filemap_source(ui, srcc, fmap)
516 521 destc.setfilemapmode(True)
517 522
518 523 if not revmapfile:
519 524 revmapfile = destc.revmapfile()
520 525
521 526 c = converter(ui, srcc, destc, revmapfile, opts)
522 527 c.convert(sortmode)
523 528
@@ -1,98 +1,130 b''
1 1
2 2 $ "$TESTDIR/hghave" svn svn-bindings || exit 80
3 3
4 4 $ cat >> $HGRCPATH <<EOF
5 5 > [extensions]
6 6 > convert =
7 7 > EOF
8 8
9 9 $ svnadmin create svn-repo
10 10 $ svnadmin load -q svn-repo < "$TESTDIR/svn/branches.svndump"
11 11
12 12 Convert trunk and branches
13 13
14 14 $ cat > branchmap <<EOF
15 15 > old3 newbranch
16 16 >
17 17 >
18 18 > EOF
19 19 $ hg convert --branchmap=branchmap --datesort -r 10 svn-repo A-hg
20 20 initializing destination A-hg repository
21 21 scanning source...
22 22 sorting...
23 23 converting...
24 24 10 init projA
25 25 9 hello
26 26 8 branch trunk, remove c and dir
27 27 7 change a
28 28 6 change b
29 29 5 move and update c
30 30 4 move and update c
31 31 3 change b again
32 32 2 move to old2
33 33 1 move back to old
34 34 0 last change to a
35 35
36 36 Test template keywords
37 37
38 38 $ hg -R A-hg log --template '{rev} {svnuuid}{svnpath}@{svnrev}\n'
39 39 10 644ede6c-2b81-4367-9dc8-d786514f2cde/trunk@10
40 40 9 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old@9
41 41 8 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old2@8
42 42 7 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old@7
43 43 6 644ede6c-2b81-4367-9dc8-d786514f2cde/trunk@6
44 44 5 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old@6
45 45 4 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old@5
46 46 3 644ede6c-2b81-4367-9dc8-d786514f2cde/trunk@4
47 47 2 644ede6c-2b81-4367-9dc8-d786514f2cde/branches/old@3
48 48 1 644ede6c-2b81-4367-9dc8-d786514f2cde/trunk@2
49 49 0 644ede6c-2b81-4367-9dc8-d786514f2cde/trunk@1
50 50
51 51 Convert again
52 52
53 53 $ hg convert --branchmap=branchmap --datesort svn-repo A-hg
54 54 scanning source...
55 55 sorting...
56 56 converting...
57 57 0 branch trunk@1 into old3
58 58
59 59 $ cd A-hg
60 60 $ hg log -G --template 'branch={branches} {rev} {desc|firstline} files: {files}\n'
61 61 o branch=newbranch 11 branch trunk@1 into old3 files:
62 62 |
63 63 | o branch= 10 last change to a files: a
64 64 | |
65 65 | | o branch=old 9 move back to old files:
66 66 | | |
67 67 | | o branch=old2 8 move to old2 files:
68 68 | | |
69 69 | | o branch=old 7 change b again files: b
70 70 | | |
71 71 | o | branch= 6 move and update c files: b
72 72 | | |
73 73 | | o branch=old 5 move and update c files: c
74 74 | | |
75 75 | | o branch=old 4 change b files: b
76 76 | | |
77 77 | o | branch= 3 change a files: a
78 78 | | |
79 79 | | o branch=old 2 branch trunk, remove c and dir files: c
80 80 | |/
81 81 | o branch= 1 hello files: a b c dir/e
82 82 |/
83 83 o branch= 0 init projA files:
84 84
85 85
86 86 $ hg branches
87 87 newbranch 11:a6d7cc050ad1
88 88 default 10:6e2b33404495
89 89 old 9:93c4b0f99529
90 90 old2 8:b52884d7bead (inactive)
91 91 $ hg tags -q
92 92 tip
93 93 $ cd ..
94 94
95 95 Test hg failing to call itself
96 96
97 97 $ HG=foobar hg convert svn-repo B-hg 2>&1 | grep abort
98 98 abort: Mercurial failed to run itself, check hg executable is in PATH
99
100 Convert 'trunk' to branch other than 'default'
101
102 $ cat > branchmap <<EOF
103 > None hgtrunk
104 >
105 >
106 > EOF
107 $ hg convert --branchmap=branchmap --datesort -r 10 svn-repo C-hg
108 initializing destination C-hg repository
109 scanning source...
110 sorting...
111 converting...
112 10 init projA
113 9 hello
114 8 branch trunk, remove c and dir
115 7 change a
116 6 change b
117 5 move and update c
118 4 move and update c
119 3 change b again
120 2 move to old2
121 1 move back to old
122 0 last change to a
123
124 $ cd C-hg
125 $ hg branches
126 hgtrunk 10:745f063703b4
127 old 9:aa50d7b8d922
128 old2 8:c85a22267b6e (inactive)
129 $ cd ..
130
General Comments 0
You need to be logged in to leave comments. Login now