##// END OF EJS Templates
convert: document source and sink identifiers, fix error message
Patrick Mezard -
r6976:b072266a default
parent child Browse files
Show More
@@ -1,196 +1,196 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 '''converting foreign VCS repositories to Mercurial'''
7 '''converting foreign VCS repositories to Mercurial'''
8
8
9 import convcmd
9 import convcmd
10 from mercurial import commands
10 from mercurial import commands
11
11
12 # Commands definition was moved elsewhere to ease demandload job.
12 # Commands definition was moved elsewhere to ease demandload job.
13
13
14 def convert(ui, src, dest=None, revmapfile=None, **opts):
14 def convert(ui, src, dest=None, revmapfile=None, **opts):
15 """Convert a foreign SCM repository to a Mercurial one.
15 """Convert a foreign SCM repository to a Mercurial one.
16
16
17 Accepted source formats:
17 Accepted source formats [identifiers]:
18 - Mercurial
18 - Mercurial [hg]
19 - CVS
19 - CVS [cvs]
20 - Darcs
20 - Darcs [darcs]
21 - git
21 - git [git]
22 - Subversion
22 - Subversion [svn]
23 - Monotone
23 - Monotone [mtn]
24 - GNU Arch
24 - GNU Arch [gnuarch]
25
25
26 Accepted destination formats:
26 Accepted destination formats [identifiers]:
27 - Mercurial
27 - Mercurial [hg]
28 - Subversion (history on branches is not preserved)
28 - Subversion [svn] (history on branches is not preserved)
29
29
30 If no revision is given, all revisions will be converted. Otherwise,
30 If no revision is given, all revisions will be converted. Otherwise,
31 convert will only import up to the named revision (given in a format
31 convert will only import up to the named revision (given in a format
32 understood by the source).
32 understood by the source).
33
33
34 If no destination directory name is specified, it defaults to the
34 If no destination directory name is specified, it defaults to the
35 basename of the source with '-hg' appended. If the destination
35 basename of the source with '-hg' appended. If the destination
36 repository doesn't exist, it will be created.
36 repository doesn't exist, it will be created.
37
37
38 If <REVMAP> isn't given, it will be put in a default location
38 If <REVMAP> isn't given, it will be put in a default location
39 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
39 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
40 file that maps each source commit ID to the destination ID for
40 file that maps each source commit ID to the destination ID for
41 that revision, like so:
41 that revision, like so:
42 <source ID> <destination ID>
42 <source ID> <destination ID>
43
43
44 If the file doesn't exist, it's automatically created. It's updated
44 If the file doesn't exist, it's automatically created. It's updated
45 on each commit copied, so convert-repo can be interrupted and can
45 on each commit copied, so convert-repo can be interrupted and can
46 be run repeatedly to copy new commits.
46 be run repeatedly to copy new commits.
47
47
48 The [username mapping] file is a simple text file that maps each source
48 The [username mapping] file is a simple text file that maps each source
49 commit author to a destination commit author. It is handy for source SCMs
49 commit author to a destination commit author. It is handy for source SCMs
50 that use unix logins to identify authors (eg: CVS). One line per author
50 that use unix logins to identify authors (eg: CVS). One line per author
51 mapping and the line format is:
51 mapping and the line format is:
52 srcauthor=whatever string you want
52 srcauthor=whatever string you want
53
53
54 The filemap is a file that allows filtering and remapping of files
54 The filemap is a file that allows filtering and remapping of files
55 and directories. Comment lines start with '#'. Each line can
55 and directories. Comment lines start with '#'. Each line can
56 contain one of the following directives:
56 contain one of the following directives:
57
57
58 include path/to/file
58 include path/to/file
59
59
60 exclude path/to/file
60 exclude path/to/file
61
61
62 rename from/file to/file
62 rename from/file to/file
63
63
64 The 'include' directive causes a file, or all files under a
64 The 'include' directive causes a file, or all files under a
65 directory, to be included in the destination repository, and the
65 directory, to be included in the destination repository, and the
66 exclusion of all other files and dirs not explicitely included.
66 exclusion of all other files and dirs not explicitely included.
67 The 'exclude' directive causes files or directories to be omitted.
67 The 'exclude' directive causes files or directories to be omitted.
68 The 'rename' directive renames a file or directory. To rename from a
68 The 'rename' directive renames a file or directory. To rename from a
69 subdirectory into the root of the repository, use '.' as the path to
69 subdirectory into the root of the repository, use '.' as the path to
70 rename to.
70 rename to.
71
71
72 The splicemap is a file that allows insertion of synthetic
72 The splicemap is a file that allows insertion of synthetic
73 history, letting you specify the parents of a revision. This is
73 history, letting you specify the parents of a revision. This is
74 useful if you want to e.g. give a Subversion merge two parents, or
74 useful if you want to e.g. give a Subversion merge two parents, or
75 graft two disconnected series of history together. Each entry
75 graft two disconnected series of history together. Each entry
76 contains a key, followed by a space, followed by one or two
76 contains a key, followed by a space, followed by one or two
77 values, separated by spaces. The key is the revision ID in the
77 values, separated by spaces. The key is the revision ID in the
78 source revision control system whose parents should be modified
78 source revision control system whose parents should be modified
79 (same format as a key in .hg/shamap). The values are the revision
79 (same format as a key in .hg/shamap). The values are the revision
80 IDs (in either the source or destination revision control system)
80 IDs (in either the source or destination revision control system)
81 that should be used as the new parents for that node.
81 that should be used as the new parents for that node.
82
82
83 Mercurial Source
83 Mercurial Source
84 -----------------
84 -----------------
85
85
86 --config convert.hg.saverev=True (boolean)
86 --config convert.hg.saverev=True (boolean)
87 allow target to preserve source revision ID
87 allow target to preserve source revision ID
88 --config convert.hg.startrev=0 (hg revision identifier)
88 --config convert.hg.startrev=0 (hg revision identifier)
89 convert start revision and its descendants
89 convert start revision and its descendants
90
90
91 CVS Source
91 CVS Source
92 ----------
92 ----------
93
93
94 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
94 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
95 to indicate the starting point of what will be converted. Direct
95 to indicate the starting point of what will be converted. Direct
96 access to the repository files is not needed, unless of course
96 access to the repository files is not needed, unless of course
97 the repository is :local:. The conversion uses the top level
97 the repository is :local:. The conversion uses the top level
98 directory in the sandbox to find the CVS repository, and then uses
98 directory in the sandbox to find the CVS repository, and then uses
99 CVS rlog commands to find files to convert. This means that unless
99 CVS rlog commands to find files to convert. This means that unless
100 a filemap is given, all files under the starting directory will be
100 a filemap is given, all files under the starting directory will be
101 converted, and that any directory reorganisation in the CVS
101 converted, and that any directory reorganisation in the CVS
102 sandbox is ignored.
102 sandbox is ignored.
103
103
104 Because CVS does not have changesets, it is necessary to collect
104 Because CVS does not have changesets, it is necessary to collect
105 individual commits to CVS and merge them into changesets. CVS source
105 individual commits to CVS and merge them into changesets. CVS source
106 can use the external 'cvsps' program (this is a legacy option and may
106 can use the external 'cvsps' program (this is a legacy option and may
107 be removed in future) or use its internal changeset merging code.
107 be removed in future) or use its internal changeset merging code.
108 External cvsps is default, and options may be passed to it by setting
108 External cvsps is default, and options may be passed to it by setting
109 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
109 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
110 The options shown are the defaults.
110 The options shown are the defaults.
111
111
112 Internal cvsps is selected by setting
112 Internal cvsps is selected by setting
113 --config convert.cvsps=builtin
113 --config convert.cvsps=builtin
114 and has a few more configurable options:
114 and has a few more configurable options:
115 --config convert.cvsps.fuzz=60 (integer)
115 --config convert.cvsps.fuzz=60 (integer)
116 Specify the maximum time (in seconds) that is allowed between
116 Specify the maximum time (in seconds) that is allowed between
117 commits with identical user and log message in a single
117 commits with identical user and log message in a single
118 changeset. When very large files were checked in as part
118 changeset. When very large files were checked in as part
119 of a changeset then the default may not be long enough.
119 of a changeset then the default may not be long enough.
120 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
120 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
121 Specify a regular expression to which commit log messages are
121 Specify a regular expression to which commit log messages are
122 matched. If a match occurs, then the conversion process will
122 matched. If a match occurs, then the conversion process will
123 insert a dummy revision merging the branch on which this log
123 insert a dummy revision merging the branch on which this log
124 message occurs to the branch indicated in the regex.
124 message occurs to the branch indicated in the regex.
125 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
125 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
126 Specify a regular expression to which commit log messages are
126 Specify a regular expression to which commit log messages are
127 matched. If a match occurs, then the conversion process will
127 matched. If a match occurs, then the conversion process will
128 add the most recent revision on the branch indicated in the
128 add the most recent revision on the branch indicated in the
129 regex as the second parent of the changeset.
129 regex as the second parent of the changeset.
130
130
131 The hgext/convert/cvsps wrapper script allows the builtin changeset
131 The hgext/convert/cvsps wrapper script allows the builtin changeset
132 merging code to be run without doing a conversion. Its parameters and
132 merging code to be run without doing a conversion. Its parameters and
133 output are similar to that of cvsps 2.1.
133 output are similar to that of cvsps 2.1.
134
134
135 Subversion Source
135 Subversion Source
136 -----------------
136 -----------------
137
137
138 Subversion source detects classical trunk/branches/tags layouts.
138 Subversion source detects classical trunk/branches/tags layouts.
139 By default, the supplied "svn://repo/path/" source URL is
139 By default, the supplied "svn://repo/path/" source URL is
140 converted as a single branch. If "svn://repo/path/trunk" exists
140 converted as a single branch. If "svn://repo/path/trunk" exists
141 it replaces the default branch. If "svn://repo/path/branches"
141 it replaces the default branch. If "svn://repo/path/branches"
142 exists, its subdirectories are listed as possible branches. If
142 exists, its subdirectories are listed as possible branches. If
143 "svn://repo/path/tags" exists, it is looked for tags referencing
143 "svn://repo/path/tags" exists, it is looked for tags referencing
144 converted branches. Default "trunk", "branches" and "tags" values
144 converted branches. Default "trunk", "branches" and "tags" values
145 can be overriden with following options. Set them to paths
145 can be overriden with following options. Set them to paths
146 relative to the source URL, or leave them blank to disable
146 relative to the source URL, or leave them blank to disable
147 autodetection.
147 autodetection.
148
148
149 --config convert.svn.branches=branches (directory name)
149 --config convert.svn.branches=branches (directory name)
150 specify the directory containing branches
150 specify the directory containing branches
151 --config convert.svn.tags=tags (directory name)
151 --config convert.svn.tags=tags (directory name)
152 specify the directory containing tags
152 specify the directory containing tags
153 --config convert.svn.trunk=trunk (directory name)
153 --config convert.svn.trunk=trunk (directory name)
154 specify the name of the trunk branch
154 specify the name of the trunk branch
155
155
156 Source history can be retrieved starting at a specific revision,
156 Source history can be retrieved starting at a specific revision,
157 instead of being integrally converted. Only single branch
157 instead of being integrally converted. Only single branch
158 conversions are supported.
158 conversions are supported.
159
159
160 --config convert.svn.startrev=0 (svn revision number)
160 --config convert.svn.startrev=0 (svn revision number)
161 specify start Subversion revision.
161 specify start Subversion revision.
162
162
163 Mercurial Destination
163 Mercurial Destination
164 ---------------------
164 ---------------------
165
165
166 --config convert.hg.clonebranches=False (boolean)
166 --config convert.hg.clonebranches=False (boolean)
167 dispatch source branches in separate clones.
167 dispatch source branches in separate clones.
168 --config convert.hg.tagsbranch=default (branch name)
168 --config convert.hg.tagsbranch=default (branch name)
169 tag revisions branch name
169 tag revisions branch name
170 --config convert.hg.usebranchnames=True (boolean)
170 --config convert.hg.usebranchnames=True (boolean)
171 preserve branch names
171 preserve branch names
172
172
173 """
173 """
174 return convcmd.convert(ui, src, dest, revmapfile, **opts)
174 return convcmd.convert(ui, src, dest, revmapfile, **opts)
175
175
176 def debugsvnlog(ui, **opts):
176 def debugsvnlog(ui, **opts):
177 return convcmd.debugsvnlog(ui, **opts)
177 return convcmd.debugsvnlog(ui, **opts)
178
178
179 commands.norepo += " convert debugsvnlog"
179 commands.norepo += " convert debugsvnlog"
180
180
181 cmdtable = {
181 cmdtable = {
182 "convert":
182 "convert":
183 (convert,
183 (convert,
184 [('A', 'authors', '', 'username mapping filename'),
184 [('A', 'authors', '', 'username mapping filename'),
185 ('d', 'dest-type', '', 'destination repository type'),
185 ('d', 'dest-type', '', 'destination repository type'),
186 ('', 'filemap', '', 'remap file names using contents of file'),
186 ('', 'filemap', '', 'remap file names using contents of file'),
187 ('r', 'rev', '', 'import up to target revision REV'),
187 ('r', 'rev', '', 'import up to target revision REV'),
188 ('s', 'source-type', '', 'source repository type'),
188 ('s', 'source-type', '', 'source repository type'),
189 ('', 'splicemap', '', 'splice synthesized history into place'),
189 ('', 'splicemap', '', 'splice synthesized history into place'),
190 ('', 'datesort', None, 'try to sort changesets by date')],
190 ('', 'datesort', None, 'try to sort changesets by date')],
191 'hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
191 'hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
192 "debugsvnlog":
192 "debugsvnlog":
193 (debugsvnlog,
193 (debugsvnlog,
194 [],
194 [],
195 'hg debugsvnlog'),
195 'hg debugsvnlog'),
196 }
196 }
@@ -1,337 +1,337 b''
1 # convcmd - convert extension commands definition
1 # convcmd - convert extension commands definition
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
8 from common import NoRepo, MissingTool, SKIPREV, mapfile
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from darcs import darcs_source
10 from darcs import darcs_source
11 from git import convert_git
11 from git import convert_git
12 from hg import mercurial_source, mercurial_sink
12 from hg import mercurial_source, mercurial_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
13 from subversion import debugsvnlog, svn_source, svn_sink
14 from monotone import monotone_source
14 from monotone import monotone_source
15 from gnuarch import gnuarch_source
15 from gnuarch import gnuarch_source
16 import filemap
16 import filemap
17
17
18 import os, shutil
18 import os, shutil
19 from mercurial import hg, util
19 from mercurial import hg, util
20 from mercurial.i18n import _
20 from mercurial.i18n import _
21
21
22 orig_encoding = 'ascii'
22 orig_encoding = 'ascii'
23
23
24 def recode(s):
24 def recode(s):
25 if isinstance(s, unicode):
25 if isinstance(s, unicode):
26 return s.encode(orig_encoding, 'replace')
26 return s.encode(orig_encoding, 'replace')
27 else:
27 else:
28 return s.decode('utf-8').encode(orig_encoding, 'replace')
28 return s.decode('utf-8').encode(orig_encoding, 'replace')
29
29
30 source_converters = [
30 source_converters = [
31 ('cvs', convert_cvs),
31 ('cvs', convert_cvs),
32 ('git', convert_git),
32 ('git', convert_git),
33 ('svn', svn_source),
33 ('svn', svn_source),
34 ('hg', mercurial_source),
34 ('hg', mercurial_source),
35 ('darcs', darcs_source),
35 ('darcs', darcs_source),
36 ('mtn', monotone_source),
36 ('mtn', monotone_source),
37 ('gnuarch', gnuarch_source),
37 ('gnuarch', gnuarch_source),
38 ]
38 ]
39
39
40 sink_converters = [
40 sink_converters = [
41 ('hg', mercurial_sink),
41 ('hg', mercurial_sink),
42 ('svn', svn_sink),
42 ('svn', svn_sink),
43 ]
43 ]
44
44
45 def convertsource(ui, path, type, rev):
45 def convertsource(ui, path, type, rev):
46 exceptions = []
46 exceptions = []
47 for name, source in source_converters:
47 for name, source in source_converters:
48 try:
48 try:
49 if not type or name == type:
49 if not type or name == type:
50 return source(ui, path, rev)
50 return source(ui, path, rev)
51 except (NoRepo, MissingTool), inst:
51 except (NoRepo, MissingTool), inst:
52 exceptions.append(inst)
52 exceptions.append(inst)
53 if not ui.quiet:
53 if not ui.quiet:
54 for inst in exceptions:
54 for inst in exceptions:
55 ui.write("%s\n" % inst)
55 ui.write("%s\n" % inst)
56 raise util.Abort(_('%s: unknown repository type') % path)
56 raise util.Abort(_('%s: missing or unsupported repository') % path)
57
57
58 def convertsink(ui, path, type):
58 def convertsink(ui, path, type):
59 for name, sink in sink_converters:
59 for name, sink in sink_converters:
60 try:
60 try:
61 if not type or name == type:
61 if not type or name == type:
62 return sink(ui, path)
62 return sink(ui, path)
63 except NoRepo, inst:
63 except NoRepo, inst:
64 ui.note(_("convert: %s\n") % inst)
64 ui.note(_("convert: %s\n") % inst)
65 raise util.Abort(_('%s: unknown repository type') % path)
65 raise util.Abort(_('%s: unknown repository type') % path)
66
66
67 class converter(object):
67 class converter(object):
68 def __init__(self, ui, source, dest, revmapfile, opts):
68 def __init__(self, ui, source, dest, revmapfile, opts):
69
69
70 self.source = source
70 self.source = source
71 self.dest = dest
71 self.dest = dest
72 self.ui = ui
72 self.ui = ui
73 self.opts = opts
73 self.opts = opts
74 self.commitcache = {}
74 self.commitcache = {}
75 self.authors = {}
75 self.authors = {}
76 self.authorfile = None
76 self.authorfile = None
77
77
78 self.map = mapfile(ui, revmapfile)
78 self.map = mapfile(ui, revmapfile)
79
79
80 # Read first the dst author map if any
80 # Read first the dst author map if any
81 authorfile = self.dest.authorfile()
81 authorfile = self.dest.authorfile()
82 if authorfile and os.path.exists(authorfile):
82 if authorfile and os.path.exists(authorfile):
83 self.readauthormap(authorfile)
83 self.readauthormap(authorfile)
84 # Extend/Override with new author map if necessary
84 # Extend/Override with new author map if necessary
85 if opts.get('authors'):
85 if opts.get('authors'):
86 self.readauthormap(opts.get('authors'))
86 self.readauthormap(opts.get('authors'))
87 self.authorfile = self.dest.authorfile()
87 self.authorfile = self.dest.authorfile()
88
88
89 self.splicemap = mapfile(ui, opts.get('splicemap'))
89 self.splicemap = mapfile(ui, opts.get('splicemap'))
90
90
91 def walktree(self, heads):
91 def walktree(self, heads):
92 '''Return a mapping that identifies the uncommitted parents of every
92 '''Return a mapping that identifies the uncommitted parents of every
93 uncommitted changeset.'''
93 uncommitted changeset.'''
94 visit = heads
94 visit = heads
95 known = {}
95 known = {}
96 parents = {}
96 parents = {}
97 while visit:
97 while visit:
98 n = visit.pop(0)
98 n = visit.pop(0)
99 if n in known or n in self.map: continue
99 if n in known or n in self.map: continue
100 known[n] = 1
100 known[n] = 1
101 commit = self.cachecommit(n)
101 commit = self.cachecommit(n)
102 parents[n] = []
102 parents[n] = []
103 for p in commit.parents:
103 for p in commit.parents:
104 parents[n].append(p)
104 parents[n].append(p)
105 visit.append(p)
105 visit.append(p)
106
106
107 return parents
107 return parents
108
108
109 def toposort(self, parents):
109 def toposort(self, parents):
110 '''Return an ordering such that every uncommitted changeset is
110 '''Return an ordering such that every uncommitted changeset is
111 preceeded by all its uncommitted ancestors.'''
111 preceeded by all its uncommitted ancestors.'''
112 visit = parents.keys()
112 visit = parents.keys()
113 seen = {}
113 seen = {}
114 children = {}
114 children = {}
115 actives = []
115 actives = []
116
116
117 while visit:
117 while visit:
118 n = visit.pop(0)
118 n = visit.pop(0)
119 if n in seen: continue
119 if n in seen: continue
120 seen[n] = 1
120 seen[n] = 1
121 # Ensure that nodes without parents are present in the 'children'
121 # Ensure that nodes without parents are present in the 'children'
122 # mapping.
122 # mapping.
123 children.setdefault(n, [])
123 children.setdefault(n, [])
124 hasparent = False
124 hasparent = False
125 for p in parents[n]:
125 for p in parents[n]:
126 if not p in self.map:
126 if not p in self.map:
127 visit.append(p)
127 visit.append(p)
128 hasparent = True
128 hasparent = True
129 children.setdefault(p, []).append(n)
129 children.setdefault(p, []).append(n)
130 if not hasparent:
130 if not hasparent:
131 actives.append(n)
131 actives.append(n)
132
132
133 del seen
133 del seen
134 del visit
134 del visit
135
135
136 if self.opts.get('datesort'):
136 if self.opts.get('datesort'):
137 dates = {}
137 dates = {}
138 def getdate(n):
138 def getdate(n):
139 if n not in dates:
139 if n not in dates:
140 dates[n] = util.parsedate(self.commitcache[n].date)
140 dates[n] = util.parsedate(self.commitcache[n].date)
141 return dates[n]
141 return dates[n]
142
142
143 def picknext(nodes):
143 def picknext(nodes):
144 return min([(getdate(n), n) for n in nodes])[1]
144 return min([(getdate(n), n) for n in nodes])[1]
145 else:
145 else:
146 prev = [None]
146 prev = [None]
147 def picknext(nodes):
147 def picknext(nodes):
148 # Return the first eligible child of the previously converted
148 # Return the first eligible child of the previously converted
149 # revision, or any of them.
149 # revision, or any of them.
150 next = nodes[0]
150 next = nodes[0]
151 for n in nodes:
151 for n in nodes:
152 if prev[0] in parents[n]:
152 if prev[0] in parents[n]:
153 next = n
153 next = n
154 break
154 break
155 prev[0] = next
155 prev[0] = next
156 return next
156 return next
157
157
158 s = []
158 s = []
159 pendings = {}
159 pendings = {}
160 while actives:
160 while actives:
161 n = picknext(actives)
161 n = picknext(actives)
162 actives.remove(n)
162 actives.remove(n)
163 s.append(n)
163 s.append(n)
164
164
165 # Update dependents list
165 # Update dependents list
166 for c in children.get(n, []):
166 for c in children.get(n, []):
167 if c not in pendings:
167 if c not in pendings:
168 pendings[c] = [p for p in parents[c] if p not in self.map]
168 pendings[c] = [p for p in parents[c] if p not in self.map]
169 try:
169 try:
170 pendings[c].remove(n)
170 pendings[c].remove(n)
171 except ValueError:
171 except ValueError:
172 raise util.Abort(_('cycle detected between %s and %s')
172 raise util.Abort(_('cycle detected between %s and %s')
173 % (recode(c), recode(n)))
173 % (recode(c), recode(n)))
174 if not pendings[c]:
174 if not pendings[c]:
175 # Parents are converted, node is eligible
175 # Parents are converted, node is eligible
176 actives.insert(0, c)
176 actives.insert(0, c)
177 pendings[c] = None
177 pendings[c] = None
178
178
179 if len(s) != len(parents):
179 if len(s) != len(parents):
180 raise util.Abort(_("not all revisions were sorted"))
180 raise util.Abort(_("not all revisions were sorted"))
181
181
182 return s
182 return s
183
183
184 def writeauthormap(self):
184 def writeauthormap(self):
185 authorfile = self.authorfile
185 authorfile = self.authorfile
186 if authorfile:
186 if authorfile:
187 self.ui.status(_('Writing author map file %s\n') % authorfile)
187 self.ui.status(_('Writing author map file %s\n') % authorfile)
188 ofile = open(authorfile, 'w+')
188 ofile = open(authorfile, 'w+')
189 for author in self.authors:
189 for author in self.authors:
190 ofile.write("%s=%s\n" % (author, self.authors[author]))
190 ofile.write("%s=%s\n" % (author, self.authors[author]))
191 ofile.close()
191 ofile.close()
192
192
193 def readauthormap(self, authorfile):
193 def readauthormap(self, authorfile):
194 afile = open(authorfile, 'r')
194 afile = open(authorfile, 'r')
195 for line in afile:
195 for line in afile:
196 if line.strip() == '':
196 if line.strip() == '':
197 continue
197 continue
198 try:
198 try:
199 srcauthor, dstauthor = line.split('=', 1)
199 srcauthor, dstauthor = line.split('=', 1)
200 srcauthor = srcauthor.strip()
200 srcauthor = srcauthor.strip()
201 dstauthor = dstauthor.strip()
201 dstauthor = dstauthor.strip()
202 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
202 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
203 self.ui.status(
203 self.ui.status(
204 _('Overriding mapping for author %s, was %s, will be %s\n')
204 _('Overriding mapping for author %s, was %s, will be %s\n')
205 % (srcauthor, self.authors[srcauthor], dstauthor))
205 % (srcauthor, self.authors[srcauthor], dstauthor))
206 else:
206 else:
207 self.ui.debug(_('Mapping author %s to %s\n')
207 self.ui.debug(_('Mapping author %s to %s\n')
208 % (srcauthor, dstauthor))
208 % (srcauthor, dstauthor))
209 self.authors[srcauthor] = dstauthor
209 self.authors[srcauthor] = dstauthor
210 except IndexError:
210 except IndexError:
211 self.ui.warn(
211 self.ui.warn(
212 _('Ignoring bad line in author map file %s: %s\n')
212 _('Ignoring bad line in author map file %s: %s\n')
213 % (authorfile, line.rstrip()))
213 % (authorfile, line.rstrip()))
214 afile.close()
214 afile.close()
215
215
216 def cachecommit(self, rev):
216 def cachecommit(self, rev):
217 commit = self.source.getcommit(rev)
217 commit = self.source.getcommit(rev)
218 commit.author = self.authors.get(commit.author, commit.author)
218 commit.author = self.authors.get(commit.author, commit.author)
219 self.commitcache[rev] = commit
219 self.commitcache[rev] = commit
220 return commit
220 return commit
221
221
222 def copy(self, rev):
222 def copy(self, rev):
223 commit = self.commitcache[rev]
223 commit = self.commitcache[rev]
224
224
225 changes = self.source.getchanges(rev)
225 changes = self.source.getchanges(rev)
226 if isinstance(changes, basestring):
226 if isinstance(changes, basestring):
227 if changes == SKIPREV:
227 if changes == SKIPREV:
228 dest = SKIPREV
228 dest = SKIPREV
229 else:
229 else:
230 dest = self.map[changes]
230 dest = self.map[changes]
231 self.map[rev] = dest
231 self.map[rev] = dest
232 return
232 return
233 files, copies = changes
233 files, copies = changes
234 pbranches = []
234 pbranches = []
235 if commit.parents:
235 if commit.parents:
236 for prev in commit.parents:
236 for prev in commit.parents:
237 if prev not in self.commitcache:
237 if prev not in self.commitcache:
238 self.cachecommit(prev)
238 self.cachecommit(prev)
239 pbranches.append((self.map[prev],
239 pbranches.append((self.map[prev],
240 self.commitcache[prev].branch))
240 self.commitcache[prev].branch))
241 self.dest.setbranch(commit.branch, pbranches)
241 self.dest.setbranch(commit.branch, pbranches)
242 try:
242 try:
243 parents = self.splicemap[rev].replace(',', ' ').split()
243 parents = self.splicemap[rev].replace(',', ' ').split()
244 self.ui.status(_('spliced in %s as parents of %s\n') %
244 self.ui.status(_('spliced in %s as parents of %s\n') %
245 (parents, rev))
245 (parents, rev))
246 parents = [self.map.get(p, p) for p in parents]
246 parents = [self.map.get(p, p) for p in parents]
247 except KeyError:
247 except KeyError:
248 parents = [b[0] for b in pbranches]
248 parents = [b[0] for b in pbranches]
249 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
249 newnode = self.dest.putcommit(files, copies, parents, commit, self.source)
250 self.source.converted(rev, newnode)
250 self.source.converted(rev, newnode)
251 self.map[rev] = newnode
251 self.map[rev] = newnode
252
252
253 def convert(self):
253 def convert(self):
254
254
255 try:
255 try:
256 self.source.before()
256 self.source.before()
257 self.dest.before()
257 self.dest.before()
258 self.source.setrevmap(self.map)
258 self.source.setrevmap(self.map)
259 self.ui.status(_("scanning source...\n"))
259 self.ui.status(_("scanning source...\n"))
260 heads = self.source.getheads()
260 heads = self.source.getheads()
261 parents = self.walktree(heads)
261 parents = self.walktree(heads)
262 self.ui.status(_("sorting...\n"))
262 self.ui.status(_("sorting...\n"))
263 t = self.toposort(parents)
263 t = self.toposort(parents)
264 num = len(t)
264 num = len(t)
265 c = None
265 c = None
266
266
267 self.ui.status(_("converting...\n"))
267 self.ui.status(_("converting...\n"))
268 for c in t:
268 for c in t:
269 num -= 1
269 num -= 1
270 desc = self.commitcache[c].desc
270 desc = self.commitcache[c].desc
271 if "\n" in desc:
271 if "\n" in desc:
272 desc = desc.splitlines()[0]
272 desc = desc.splitlines()[0]
273 # convert log message to local encoding without using
273 # convert log message to local encoding without using
274 # tolocal() because util._encoding conver() use it as
274 # tolocal() because util._encoding conver() use it as
275 # 'utf-8'
275 # 'utf-8'
276 self.ui.status("%d %s\n" % (num, recode(desc)))
276 self.ui.status("%d %s\n" % (num, recode(desc)))
277 self.ui.note(_("source: %s\n") % recode(c))
277 self.ui.note(_("source: %s\n") % recode(c))
278 self.copy(c)
278 self.copy(c)
279
279
280 tags = self.source.gettags()
280 tags = self.source.gettags()
281 ctags = {}
281 ctags = {}
282 for k in tags:
282 for k in tags:
283 v = tags[k]
283 v = tags[k]
284 if self.map.get(v, SKIPREV) != SKIPREV:
284 if self.map.get(v, SKIPREV) != SKIPREV:
285 ctags[k] = self.map[v]
285 ctags[k] = self.map[v]
286
286
287 if c and ctags:
287 if c and ctags:
288 nrev = self.dest.puttags(ctags)
288 nrev = self.dest.puttags(ctags)
289 # write another hash correspondence to override the previous
289 # write another hash correspondence to override the previous
290 # one so we don't end up with extra tag heads
290 # one so we don't end up with extra tag heads
291 if nrev:
291 if nrev:
292 self.map[c] = nrev
292 self.map[c] = nrev
293
293
294 self.writeauthormap()
294 self.writeauthormap()
295 finally:
295 finally:
296 self.cleanup()
296 self.cleanup()
297
297
298 def cleanup(self):
298 def cleanup(self):
299 try:
299 try:
300 self.dest.after()
300 self.dest.after()
301 finally:
301 finally:
302 self.source.after()
302 self.source.after()
303 self.map.close()
303 self.map.close()
304
304
305 def convert(ui, src, dest=None, revmapfile=None, **opts):
305 def convert(ui, src, dest=None, revmapfile=None, **opts):
306 global orig_encoding
306 global orig_encoding
307 orig_encoding = util._encoding
307 orig_encoding = util._encoding
308 util._encoding = 'UTF-8'
308 util._encoding = 'UTF-8'
309
309
310 if not dest:
310 if not dest:
311 dest = hg.defaultdest(src) + "-hg"
311 dest = hg.defaultdest(src) + "-hg"
312 ui.status(_("assuming destination %s\n") % dest)
312 ui.status(_("assuming destination %s\n") % dest)
313
313
314 destc = convertsink(ui, dest, opts.get('dest_type'))
314 destc = convertsink(ui, dest, opts.get('dest_type'))
315
315
316 try:
316 try:
317 srcc = convertsource(ui, src, opts.get('source_type'),
317 srcc = convertsource(ui, src, opts.get('source_type'),
318 opts.get('rev'))
318 opts.get('rev'))
319 except Exception:
319 except Exception:
320 for path in destc.created:
320 for path in destc.created:
321 shutil.rmtree(path, True)
321 shutil.rmtree(path, True)
322 raise
322 raise
323
323
324 fmap = opts.get('filemap')
324 fmap = opts.get('filemap')
325 if fmap:
325 if fmap:
326 srcc = filemap.filemap_source(ui, srcc, fmap)
326 srcc = filemap.filemap_source(ui, srcc, fmap)
327 destc.setfilemapmode(True)
327 destc.setfilemapmode(True)
328
328
329 if not revmapfile:
329 if not revmapfile:
330 try:
330 try:
331 revmapfile = destc.revmapfile()
331 revmapfile = destc.revmapfile()
332 except:
332 except:
333 revmapfile = os.path.join(destc, "map")
333 revmapfile = os.path.join(destc, "map")
334
334
335 c = converter(ui, srcc, destc, revmapfile, opts)
335 c = converter(ui, srcc, destc, revmapfile, opts)
336 c.convert()
336 c.convert()
337
337
@@ -1,205 +1,205 b''
1 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
1 hg convert [OPTION]... SOURCE [DEST [REVMAP]]
2
2
3 Convert a foreign SCM repository to a Mercurial one.
3 Convert a foreign SCM repository to a Mercurial one.
4
4
5 Accepted source formats:
5 Accepted source formats [identifiers]:
6 - Mercurial
6 - Mercurial [hg]
7 - CVS
7 - CVS [cvs]
8 - Darcs
8 - Darcs [darcs]
9 - git
9 - git [git]
10 - Subversion
10 - Subversion [svn]
11 - Monotone
11 - Monotone [mtn]
12 - GNU Arch
12 - GNU Arch [gnuarch]
13
13
14 Accepted destination formats:
14 Accepted destination formats [identifiers]:
15 - Mercurial
15 - Mercurial [hg]
16 - Subversion (history on branches is not preserved)
16 - Subversion [svn] (history on branches is not preserved)
17
17
18 If no revision is given, all revisions will be converted. Otherwise,
18 If no revision is given, all revisions will be converted. Otherwise,
19 convert will only import up to the named revision (given in a format
19 convert will only import up to the named revision (given in a format
20 understood by the source).
20 understood by the source).
21
21
22 If no destination directory name is specified, it defaults to the
22 If no destination directory name is specified, it defaults to the
23 basename of the source with '-hg' appended. If the destination
23 basename of the source with '-hg' appended. If the destination
24 repository doesn't exist, it will be created.
24 repository doesn't exist, it will be created.
25
25
26 If <REVMAP> isn't given, it will be put in a default location
26 If <REVMAP> isn't given, it will be put in a default location
27 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
27 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
28 file that maps each source commit ID to the destination ID for
28 file that maps each source commit ID to the destination ID for
29 that revision, like so:
29 that revision, like so:
30 <source ID> <destination ID>
30 <source ID> <destination ID>
31
31
32 If the file doesn't exist, it's automatically created. It's updated
32 If the file doesn't exist, it's automatically created. It's updated
33 on each commit copied, so convert-repo can be interrupted and can
33 on each commit copied, so convert-repo can be interrupted and can
34 be run repeatedly to copy new commits.
34 be run repeatedly to copy new commits.
35
35
36 The [username mapping] file is a simple text file that maps each source
36 The [username mapping] file is a simple text file that maps each source
37 commit author to a destination commit author. It is handy for source SCMs
37 commit author to a destination commit author. It is handy for source SCMs
38 that use unix logins to identify authors (eg: CVS). One line per author
38 that use unix logins to identify authors (eg: CVS). One line per author
39 mapping and the line format is:
39 mapping and the line format is:
40 srcauthor=whatever string you want
40 srcauthor=whatever string you want
41
41
42 The filemap is a file that allows filtering and remapping of files
42 The filemap is a file that allows filtering and remapping of files
43 and directories. Comment lines start with '#'. Each line can
43 and directories. Comment lines start with '#'. Each line can
44 contain one of the following directives:
44 contain one of the following directives:
45
45
46 include path/to/file
46 include path/to/file
47
47
48 exclude path/to/file
48 exclude path/to/file
49
49
50 rename from/file to/file
50 rename from/file to/file
51
51
52 The 'include' directive causes a file, or all files under a
52 The 'include' directive causes a file, or all files under a
53 directory, to be included in the destination repository, and the
53 directory, to be included in the destination repository, and the
54 exclusion of all other files and dirs not explicitely included.
54 exclusion of all other files and dirs not explicitely included.
55 The 'exclude' directive causes files or directories to be omitted.
55 The 'exclude' directive causes files or directories to be omitted.
56 The 'rename' directive renames a file or directory. To rename from a
56 The 'rename' directive renames a file or directory. To rename from a
57 subdirectory into the root of the repository, use '.' as the path to
57 subdirectory into the root of the repository, use '.' as the path to
58 rename to.
58 rename to.
59
59
60 The splicemap is a file that allows insertion of synthetic
60 The splicemap is a file that allows insertion of synthetic
61 history, letting you specify the parents of a revision. This is
61 history, letting you specify the parents of a revision. This is
62 useful if you want to e.g. give a Subversion merge two parents, or
62 useful if you want to e.g. give a Subversion merge two parents, or
63 graft two disconnected series of history together. Each entry
63 graft two disconnected series of history together. Each entry
64 contains a key, followed by a space, followed by one or two
64 contains a key, followed by a space, followed by one or two
65 values, separated by spaces. The key is the revision ID in the
65 values, separated by spaces. The key is the revision ID in the
66 source revision control system whose parents should be modified
66 source revision control system whose parents should be modified
67 (same format as a key in .hg/shamap). The values are the revision
67 (same format as a key in .hg/shamap). The values are the revision
68 IDs (in either the source or destination revision control system)
68 IDs (in either the source or destination revision control system)
69 that should be used as the new parents for that node.
69 that should be used as the new parents for that node.
70
70
71 Mercurial Source
71 Mercurial Source
72 -----------------
72 -----------------
73
73
74 --config convert.hg.saverev=True (boolean)
74 --config convert.hg.saverev=True (boolean)
75 allow target to preserve source revision ID
75 allow target to preserve source revision ID
76 --config convert.hg.startrev=0 (hg revision identifier)
76 --config convert.hg.startrev=0 (hg revision identifier)
77 convert start revision and its descendants
77 convert start revision and its descendants
78
78
79 CVS Source
79 CVS Source
80 ----------
80 ----------
81
81
82 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
82 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
83 to indicate the starting point of what will be converted. Direct
83 to indicate the starting point of what will be converted. Direct
84 access to the repository files is not needed, unless of course
84 access to the repository files is not needed, unless of course
85 the repository is :local:. The conversion uses the top level
85 the repository is :local:. The conversion uses the top level
86 directory in the sandbox to find the CVS repository, and then uses
86 directory in the sandbox to find the CVS repository, and then uses
87 CVS rlog commands to find files to convert. This means that unless
87 CVS rlog commands to find files to convert. This means that unless
88 a filemap is given, all files under the starting directory will be
88 a filemap is given, all files under the starting directory will be
89 converted, and that any directory reorganisation in the CVS
89 converted, and that any directory reorganisation in the CVS
90 sandbox is ignored.
90 sandbox is ignored.
91
91
92 Because CVS does not have changesets, it is necessary to collect
92 Because CVS does not have changesets, it is necessary to collect
93 individual commits to CVS and merge them into changesets. CVS source
93 individual commits to CVS and merge them into changesets. CVS source
94 can use the external 'cvsps' program (this is a legacy option and may
94 can use the external 'cvsps' program (this is a legacy option and may
95 be removed in future) or use its internal changeset merging code.
95 be removed in future) or use its internal changeset merging code.
96 External cvsps is default, and options may be passed to it by setting
96 External cvsps is default, and options may be passed to it by setting
97 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
97 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
98 The options shown are the defaults.
98 The options shown are the defaults.
99
99
100 Internal cvsps is selected by setting
100 Internal cvsps is selected by setting
101 --config convert.cvsps=builtin
101 --config convert.cvsps=builtin
102 and has a few more configurable options:
102 and has a few more configurable options:
103 --config convert.cvsps.fuzz=60 (integer)
103 --config convert.cvsps.fuzz=60 (integer)
104 Specify the maximum time (in seconds) that is allowed between
104 Specify the maximum time (in seconds) that is allowed between
105 commits with identical user and log message in a single
105 commits with identical user and log message in a single
106 changeset. When very large files were checked in as part
106 changeset. When very large files were checked in as part
107 of a changeset then the default may not be long enough.
107 of a changeset then the default may not be long enough.
108 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
108 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
109 Specify a regular expression to which commit log messages are
109 Specify a regular expression to which commit log messages are
110 matched. If a match occurs, then the conversion process will
110 matched. If a match occurs, then the conversion process will
111 insert a dummy revision merging the branch on which this log
111 insert a dummy revision merging the branch on which this log
112 message occurs to the branch indicated in the regex.
112 message occurs to the branch indicated in the regex.
113 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
113 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
114 Specify a regular expression to which commit log messages are
114 Specify a regular expression to which commit log messages are
115 matched. If a match occurs, then the conversion process will
115 matched. If a match occurs, then the conversion process will
116 add the most recent revision on the branch indicated in the
116 add the most recent revision on the branch indicated in the
117 regex as the second parent of the changeset.
117 regex as the second parent of the changeset.
118
118
119 The hgext/convert/cvsps wrapper script allows the builtin changeset
119 The hgext/convert/cvsps wrapper script allows the builtin changeset
120 merging code to be run without doing a conversion. Its parameters and
120 merging code to be run without doing a conversion. Its parameters and
121 output are similar to that of cvsps 2.1.
121 output are similar to that of cvsps 2.1.
122
122
123 Subversion Source
123 Subversion Source
124 -----------------
124 -----------------
125
125
126 Subversion source detects classical trunk/branches/tags layouts.
126 Subversion source detects classical trunk/branches/tags layouts.
127 By default, the supplied "svn://repo/path/" source URL is
127 By default, the supplied "svn://repo/path/" source URL is
128 converted as a single branch. If "svn://repo/path/trunk" exists
128 converted as a single branch. If "svn://repo/path/trunk" exists
129 it replaces the default branch. If "svn://repo/path/branches"
129 it replaces the default branch. If "svn://repo/path/branches"
130 exists, its subdirectories are listed as possible branches. If
130 exists, its subdirectories are listed as possible branches. If
131 "svn://repo/path/tags" exists, it is looked for tags referencing
131 "svn://repo/path/tags" exists, it is looked for tags referencing
132 converted branches. Default "trunk", "branches" and "tags" values
132 converted branches. Default "trunk", "branches" and "tags" values
133 can be overriden with following options. Set them to paths
133 can be overriden with following options. Set them to paths
134 relative to the source URL, or leave them blank to disable
134 relative to the source URL, or leave them blank to disable
135 autodetection.
135 autodetection.
136
136
137 --config convert.svn.branches=branches (directory name)
137 --config convert.svn.branches=branches (directory name)
138 specify the directory containing branches
138 specify the directory containing branches
139 --config convert.svn.tags=tags (directory name)
139 --config convert.svn.tags=tags (directory name)
140 specify the directory containing tags
140 specify the directory containing tags
141 --config convert.svn.trunk=trunk (directory name)
141 --config convert.svn.trunk=trunk (directory name)
142 specify the name of the trunk branch
142 specify the name of the trunk branch
143
143
144 Source history can be retrieved starting at a specific revision,
144 Source history can be retrieved starting at a specific revision,
145 instead of being integrally converted. Only single branch
145 instead of being integrally converted. Only single branch
146 conversions are supported.
146 conversions are supported.
147
147
148 --config convert.svn.startrev=0 (svn revision number)
148 --config convert.svn.startrev=0 (svn revision number)
149 specify start Subversion revision.
149 specify start Subversion revision.
150
150
151 Mercurial Destination
151 Mercurial Destination
152 ---------------------
152 ---------------------
153
153
154 --config convert.hg.clonebranches=False (boolean)
154 --config convert.hg.clonebranches=False (boolean)
155 dispatch source branches in separate clones.
155 dispatch source branches in separate clones.
156 --config convert.hg.tagsbranch=default (branch name)
156 --config convert.hg.tagsbranch=default (branch name)
157 tag revisions branch name
157 tag revisions branch name
158 --config convert.hg.usebranchnames=True (boolean)
158 --config convert.hg.usebranchnames=True (boolean)
159 preserve branch names
159 preserve branch names
160
160
161 options:
161 options:
162
162
163 -A --authors username mapping filename
163 -A --authors username mapping filename
164 -d --dest-type destination repository type
164 -d --dest-type destination repository type
165 --filemap remap file names using contents of file
165 --filemap remap file names using contents of file
166 -r --rev import up to target revision REV
166 -r --rev import up to target revision REV
167 -s --source-type source repository type
167 -s --source-type source repository type
168 --splicemap splice synthesized history into place
168 --splicemap splice synthesized history into place
169 --datesort try to sort changesets by date
169 --datesort try to sort changesets by date
170
170
171 use "hg -v help convert" to show global options
171 use "hg -v help convert" to show global options
172 adding a
172 adding a
173 assuming destination a-hg
173 assuming destination a-hg
174 initializing destination a-hg repository
174 initializing destination a-hg repository
175 scanning source...
175 scanning source...
176 sorting...
176 sorting...
177 converting...
177 converting...
178 4 a
178 4 a
179 3 b
179 3 b
180 2 c
180 2 c
181 1 d
181 1 d
182 0 e
182 0 e
183 pulling from ../a
183 pulling from ../a
184 searching for changes
184 searching for changes
185 no changes found
185 no changes found
186 % should fail
186 % should fail
187 initializing destination bogusfile repository
187 initializing destination bogusfile repository
188 abort: cannot create new bundle repository
188 abort: cannot create new bundle repository
189 % should fail
189 % should fail
190 abort: Permission denied: bogusdir
190 abort: Permission denied: bogusdir
191 % should succeed
191 % should succeed
192 initializing destination bogusdir repository
192 initializing destination bogusdir repository
193 scanning source...
193 scanning source...
194 sorting...
194 sorting...
195 converting...
195 converting...
196 4 a
196 4 a
197 3 b
197 3 b
198 2 c
198 2 c
199 1 d
199 1 d
200 0 e
200 0 e
201 % test pre and post conversion actions
201 % test pre and post conversion actions
202 run hg source pre-conversion action
202 run hg source pre-conversion action
203 run hg sink pre-conversion action
203 run hg sink pre-conversion action
204 run hg sink post-conversion action
204 run hg sink post-conversion action
205 run hg source post-conversion action
205 run hg source post-conversion action
General Comments 0
You need to be logged in to leave comments. Login now