Show More
@@ -1,176 +1,176 | |||
|
1 | 1 | # |
|
2 | 2 | # Perforce source for convert extension. |
|
3 | 3 | # |
|
4 | 4 | # Copyright 2009, Frank Kingswood <frank@kingswood-consulting.co.uk> |
|
5 | 5 | # |
|
6 | 6 | # This software may be used and distributed according to the terms |
|
7 | 7 | # of the GNU General Public License, incorporated herein by reference. |
|
8 | 8 | # |
|
9 | 9 | |
|
10 | 10 | from mercurial import util |
|
11 | 11 | from mercurial.i18n import _ |
|
12 | 12 | |
|
13 | 13 | from common import commit, converter_source, checktool |
|
14 | 14 | import marshal |
|
15 | 15 | |
|
16 | 16 | def loaditer(f): |
|
17 | 17 | "Yield the dictionary objects generated by p4" |
|
18 | 18 | try: |
|
19 | 19 | while True: |
|
20 | 20 | d = marshal.load(f) |
|
21 | 21 | if not d: |
|
22 | 22 | break |
|
23 | 23 | yield d |
|
24 | 24 | except EOFError: |
|
25 | 25 | pass |
|
26 | 26 | |
|
27 | 27 | class p4_source(converter_source): |
|
28 | 28 | def __init__(self, ui, path, rev=None): |
|
29 | 29 | super(p4_source, self).__init__(ui, path, rev=rev) |
|
30 | 30 | |
|
31 | checktool('p4') | |
|
31 | checktool('p4', abort=False) | |
|
32 | 32 | |
|
33 | 33 | self.p4changes = {} |
|
34 | 34 | self.heads = {} |
|
35 | 35 | self.changeset = {} |
|
36 | 36 | self.files = {} |
|
37 | 37 | self.tags = {} |
|
38 | 38 | self.lastbranch = {} |
|
39 | 39 | self.parent = {} |
|
40 | 40 | self.encoding = "latin_1" |
|
41 | 41 | self.depotname = {} # mapping from local name to depot name |
|
42 | 42 | self.modecache = {} |
|
43 | 43 | |
|
44 | 44 | self._parse(ui, path) |
|
45 | 45 | |
|
46 | 46 | def _parse_view(self, path): |
|
47 | 47 | "Read changes affecting the path" |
|
48 | 48 | cmd = "p4 -G changes -s submitted '%s'" % path |
|
49 | 49 | stdout = util.popen(cmd) |
|
50 | 50 | for d in loaditer(stdout): |
|
51 | 51 | c = d.get("change", None) |
|
52 | 52 | if c: |
|
53 | 53 | self.p4changes[c] = True |
|
54 | 54 | |
|
55 | 55 | def _parse(self, ui, path): |
|
56 | 56 | "Prepare list of P4 filenames and revisions to import" |
|
57 | 57 | ui.status(_('reading p4 views\n')) |
|
58 | 58 | |
|
59 | 59 | # read client spec or view |
|
60 | 60 | if "/" in path: |
|
61 | 61 | self._parse_view(path) |
|
62 | 62 | if path.startswith("//") and path.endswith("/..."): |
|
63 | 63 | views = {path[:-3]:""} |
|
64 | 64 | else: |
|
65 | 65 | views = {"//": ""} |
|
66 | 66 | else: |
|
67 | 67 | cmd = "p4 -G client -o '%s'" % path |
|
68 | 68 | clientspec = marshal.load(util.popen(cmd)) |
|
69 | 69 | |
|
70 | 70 | views = {} |
|
71 | 71 | for client in clientspec: |
|
72 | 72 | if client.startswith("View"): |
|
73 | 73 | sview, cview = clientspec[client].split() |
|
74 | 74 | self._parse_view(sview) |
|
75 | 75 | if sview.endswith("...") and cview.endswith("..."): |
|
76 | 76 | sview = sview[:-3] |
|
77 | 77 | cview = cview[:-3] |
|
78 | 78 | cview = cview[2:] |
|
79 | 79 | cview = cview[cview.find("/") + 1:] |
|
80 | 80 | views[sview] = cview |
|
81 | 81 | |
|
82 | 82 | # list of changes that affect our source files |
|
83 | 83 | self.p4changes = self.p4changes.keys() |
|
84 | 84 | self.p4changes.sort(key=int) |
|
85 | 85 | |
|
86 | 86 | # list with depot pathnames, longest first |
|
87 | 87 | vieworder = views.keys() |
|
88 | 88 | vieworder.sort(key=lambda x: -len(x)) |
|
89 | 89 | |
|
90 | 90 | # handle revision limiting |
|
91 | 91 | startrev = self.ui.config('convert', 'p4.startrev', default=0) |
|
92 | 92 | self.p4changes = [x for x in self.p4changes |
|
93 | 93 | if ((not startrev or int(x) >= int(startrev)) and |
|
94 | 94 | (not self.rev or int(x) <= int(self.rev)))] |
|
95 | 95 | |
|
96 | 96 | # now read the full changelists to get the list of file revisions |
|
97 | 97 | ui.status(_('collecting p4 changelists\n')) |
|
98 | 98 | lastid = None |
|
99 | 99 | for change in self.p4changes: |
|
100 | 100 | cmd = "p4 -G describe %s" % change |
|
101 | 101 | stdout = util.popen(cmd) |
|
102 | 102 | d = marshal.load(stdout) |
|
103 | 103 | |
|
104 | 104 | desc = self.recode(d["desc"]) |
|
105 | 105 | shortdesc = desc.split("\n", 1)[0] |
|
106 | 106 | t = '%s %s' % (d["change"], repr(shortdesc)[1:-1]) |
|
107 | 107 | ui.status(util.ellipsis(t, 80) + '\n') |
|
108 | 108 | |
|
109 | 109 | if lastid: |
|
110 | 110 | parents = [lastid] |
|
111 | 111 | else: |
|
112 | 112 | parents = [] |
|
113 | 113 | |
|
114 | 114 | date = (int(d["time"]), 0) # timezone not set |
|
115 | 115 | c = commit(author=self.recode(d["user"]), date=util.datestr(date), |
|
116 | 116 | parents=parents, desc=desc, branch='', extra={"p4": change}) |
|
117 | 117 | |
|
118 | 118 | files = [] |
|
119 | 119 | i = 0 |
|
120 | 120 | while ("depotFile%d" % i) in d and ("rev%d" % i) in d: |
|
121 | 121 | oldname = d["depotFile%d" % i] |
|
122 | 122 | filename = None |
|
123 | 123 | for v in vieworder: |
|
124 | 124 | if oldname.startswith(v): |
|
125 | 125 | filename = views[v] + oldname[len(v):] |
|
126 | 126 | break |
|
127 | 127 | if filename: |
|
128 | 128 | files.append((filename, d["rev%d" % i])) |
|
129 | 129 | self.depotname[filename] = oldname |
|
130 | 130 | i += 1 |
|
131 | 131 | self.changeset[change] = c |
|
132 | 132 | self.files[change] = files |
|
133 | 133 | lastid = change |
|
134 | 134 | |
|
135 | 135 | if lastid: |
|
136 | 136 | self.heads = [lastid] |
|
137 | 137 | |
|
138 | 138 | def getheads(self): |
|
139 | 139 | return self.heads |
|
140 | 140 | |
|
141 | 141 | def getfile(self, name, rev): |
|
142 | 142 | cmd = "p4 -G print '%s#%s'" % (self.depotname[name], rev) |
|
143 | 143 | stdout = util.popen(cmd) |
|
144 | 144 | |
|
145 | 145 | mode = None |
|
146 | 146 | data = "" |
|
147 | 147 | |
|
148 | 148 | for d in loaditer(stdout): |
|
149 | 149 | if d["code"] == "stat": |
|
150 | 150 | if "+x" in d["type"]: |
|
151 | 151 | mode = "x" |
|
152 | 152 | else: |
|
153 | 153 | mode = "" |
|
154 | 154 | elif d["code"] == "text": |
|
155 | 155 | data += d["data"] |
|
156 | 156 | |
|
157 | 157 | if mode is None: |
|
158 | 158 | raise IOError() |
|
159 | 159 | |
|
160 | 160 | self.modecache[(name, rev)] = mode |
|
161 | 161 | return data |
|
162 | 162 | |
|
163 | 163 | def getmode(self, name, rev): |
|
164 | 164 | return self.modecache[(name, rev)] |
|
165 | 165 | |
|
166 | 166 | def getchanges(self, rev): |
|
167 | 167 | return self.files[rev], {} |
|
168 | 168 | |
|
169 | 169 | def getcommit(self, rev): |
|
170 | 170 | return self.changeset[rev] |
|
171 | 171 | |
|
172 | 172 | def gettags(self): |
|
173 | 173 | return self.tags |
|
174 | 174 | |
|
175 | 175 | def getchangedfiles(self, rev, i): |
|
176 | 176 | return util.sort([x[0] for x in self.files[rev]]) |
@@ -1,46 +1,50 | |||
|
1 | 1 | #!/bin/sh |
|
2 | 2 | |
|
3 | 3 | cat >> $HGRCPATH <<EOF |
|
4 | 4 | [extensions] |
|
5 | 5 | convert= |
|
6 | 6 | [convert] |
|
7 | 7 | hg.saverev=False |
|
8 | 8 | EOF |
|
9 | 9 | |
|
10 | 10 | hg help convert |
|
11 | 11 | |
|
12 | 12 | hg init a |
|
13 | 13 | cd a |
|
14 | 14 | echo a > a |
|
15 | 15 | hg ci -d'0 0' -Ama |
|
16 | 16 | hg cp a b |
|
17 | 17 | hg ci -d'1 0' -mb |
|
18 | 18 | hg rm a |
|
19 | 19 | hg ci -d'2 0' -mc |
|
20 | 20 | hg mv b a |
|
21 | 21 | hg ci -d'3 0' -md |
|
22 | 22 | echo a >> a |
|
23 | 23 | hg ci -d'4 0' -me |
|
24 | 24 | |
|
25 | 25 | cd .. |
|
26 | 26 | hg convert a 2>&1 | grep -v 'subversion python bindings could not be loaded' |
|
27 | 27 | hg --cwd a-hg pull ../a |
|
28 | 28 | |
|
29 | 29 | touch bogusfile |
|
30 | 30 | echo % should fail |
|
31 | 31 | hg convert a bogusfile |
|
32 | 32 | |
|
33 | 33 | mkdir bogusdir |
|
34 | 34 | chmod 000 bogusdir |
|
35 | 35 | |
|
36 | 36 | echo % should fail |
|
37 | 37 | hg convert a bogusdir |
|
38 | 38 | |
|
39 | 39 | echo % should succeed |
|
40 | 40 | chmod 700 bogusdir |
|
41 | 41 | hg convert a bogusdir |
|
42 | 42 | |
|
43 | 43 | echo % test pre and post conversion actions |
|
44 | 44 | echo 'include b' > filemap |
|
45 | 45 | hg convert --debug --filemap filemap a partialb | \ |
|
46 | 46 | grep 'run hg' |
|
47 | ||
|
48 | echo % converting empty dir should fail "nicely" | |
|
49 | mkdir emptydir | |
|
50 | PATH=$BINDIR hg convert emptydir 2>&1 | sed 's,file://.*/emptydir,.../emptydir,g' |
@@ -1,228 +1,241 | |||
|
1 | 1 | hg convert [OPTION]... SOURCE [DEST [REVMAP]] |
|
2 | 2 | |
|
3 | 3 | convert a foreign SCM repository to a Mercurial one. |
|
4 | 4 | |
|
5 | 5 | Accepted source formats [identifiers]: |
|
6 | 6 | - Mercurial [hg] |
|
7 | 7 | - CVS [cvs] |
|
8 | 8 | - Darcs [darcs] |
|
9 | 9 | - git [git] |
|
10 | 10 | - Subversion [svn] |
|
11 | 11 | - Monotone [mtn] |
|
12 | 12 | - GNU Arch [gnuarch] |
|
13 | 13 | - Bazaar [bzr] |
|
14 | 14 | - Perforce [p4] |
|
15 | 15 | |
|
16 | 16 | Accepted destination formats [identifiers]: |
|
17 | 17 | - Mercurial [hg] |
|
18 | 18 | - Subversion [svn] (history on branches is not preserved) |
|
19 | 19 | |
|
20 | 20 | If no revision is given, all revisions will be converted. Otherwise, |
|
21 | 21 | convert will only import up to the named revision (given in a format |
|
22 | 22 | understood by the source). |
|
23 | 23 | |
|
24 | 24 | If no destination directory name is specified, it defaults to the |
|
25 | 25 | basename of the source with '-hg' appended. If the destination |
|
26 | 26 | repository doesn't exist, it will be created. |
|
27 | 27 | |
|
28 | 28 | If <REVMAP> isn't given, it will be put in a default location |
|
29 | 29 | (<dest>/.hg/shamap by default). The <REVMAP> is a simple text |
|
30 | 30 | file that maps each source commit ID to the destination ID for |
|
31 | 31 | that revision, like so: |
|
32 | 32 | <source ID> <destination ID> |
|
33 | 33 | |
|
34 | 34 | If the file doesn't exist, it's automatically created. It's updated |
|
35 | 35 | on each commit copied, so convert-repo can be interrupted and can |
|
36 | 36 | be run repeatedly to copy new commits. |
|
37 | 37 | |
|
38 | 38 | The [username mapping] file is a simple text file that maps each source |
|
39 | 39 | commit author to a destination commit author. It is handy for source SCMs |
|
40 | 40 | that use unix logins to identify authors (eg: CVS). One line per author |
|
41 | 41 | mapping and the line format is: |
|
42 | 42 | srcauthor=whatever string you want |
|
43 | 43 | |
|
44 | 44 | The filemap is a file that allows filtering and remapping of files |
|
45 | 45 | and directories. Comment lines start with '#'. Each line can |
|
46 | 46 | contain one of the following directives: |
|
47 | 47 | |
|
48 | 48 | include path/to/file |
|
49 | 49 | |
|
50 | 50 | exclude path/to/file |
|
51 | 51 | |
|
52 | 52 | rename from/file to/file |
|
53 | 53 | |
|
54 | 54 | The 'include' directive causes a file, or all files under a |
|
55 | 55 | directory, to be included in the destination repository, and the |
|
56 | 56 | exclusion of all other files and dirs not explicitely included. |
|
57 | 57 | The 'exclude' directive causes files or directories to be omitted. |
|
58 | 58 | The 'rename' directive renames a file or directory. To rename from a |
|
59 | 59 | subdirectory into the root of the repository, use '.' as the path to |
|
60 | 60 | rename to. |
|
61 | 61 | |
|
62 | 62 | The splicemap is a file that allows insertion of synthetic |
|
63 | 63 | history, letting you specify the parents of a revision. This is |
|
64 | 64 | useful if you want to e.g. give a Subversion merge two parents, or |
|
65 | 65 | graft two disconnected series of history together. Each entry |
|
66 | 66 | contains a key, followed by a space, followed by one or two |
|
67 | 67 | values, separated by spaces. The key is the revision ID in the |
|
68 | 68 | source revision control system whose parents should be modified |
|
69 | 69 | (same format as a key in .hg/shamap). The values are the revision |
|
70 | 70 | IDs (in either the source or destination revision control system) |
|
71 | 71 | that should be used as the new parents for that node. |
|
72 | 72 | |
|
73 | 73 | Mercurial Source |
|
74 | 74 | ----------------- |
|
75 | 75 | |
|
76 | 76 | --config convert.hg.ignoreerrors=False (boolean) |
|
77 | 77 | ignore integrity errors when reading. Use it to fix Mercurial |
|
78 | 78 | repositories with missing revlogs, by converting from and to |
|
79 | 79 | Mercurial. |
|
80 | 80 | --config convert.hg.saverev=False (boolean) |
|
81 | 81 | store original revision ID in changeset (forces target IDs to change) |
|
82 | 82 | --config convert.hg.startrev=0 (hg revision identifier) |
|
83 | 83 | convert start revision and its descendants |
|
84 | 84 | |
|
85 | 85 | CVS Source |
|
86 | 86 | ---------- |
|
87 | 87 | |
|
88 | 88 | CVS source will use a sandbox (i.e. a checked-out copy) from CVS |
|
89 | 89 | to indicate the starting point of what will be converted. Direct |
|
90 | 90 | access to the repository files is not needed, unless of course |
|
91 | 91 | the repository is :local:. The conversion uses the top level |
|
92 | 92 | directory in the sandbox to find the CVS repository, and then uses |
|
93 | 93 | CVS rlog commands to find files to convert. This means that unless |
|
94 | 94 | a filemap is given, all files under the starting directory will be |
|
95 | 95 | converted, and that any directory reorganisation in the CVS |
|
96 | 96 | sandbox is ignored. |
|
97 | 97 | |
|
98 | 98 | Because CVS does not have changesets, it is necessary to collect |
|
99 | 99 | individual commits to CVS and merge them into changesets. CVS |
|
100 | 100 | source uses its internal changeset merging code by default but can |
|
101 | 101 | be configured to call the external 'cvsps' program by setting: |
|
102 | 102 | --config convert.cvsps='cvsps -A -u --cvs-direct -q' |
|
103 | 103 | This is a legacy option and may be removed in future. |
|
104 | 104 | |
|
105 | 105 | The options shown are the defaults. |
|
106 | 106 | |
|
107 | 107 | Internal cvsps is selected by setting |
|
108 | 108 | --config convert.cvsps=builtin |
|
109 | 109 | and has a few more configurable options: |
|
110 | 110 | --config convert.cvsps.fuzz=60 (integer) |
|
111 | 111 | Specify the maximum time (in seconds) that is allowed between |
|
112 | 112 | commits with identical user and log message in a single |
|
113 | 113 | changeset. When very large files were checked in as part |
|
114 | 114 | of a changeset then the default may not be long enough. |
|
115 | 115 | --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}' |
|
116 | 116 | Specify a regular expression to which commit log messages are |
|
117 | 117 | matched. If a match occurs, then the conversion process will |
|
118 | 118 | insert a dummy revision merging the branch on which this log |
|
119 | 119 | message occurs to the branch indicated in the regex. |
|
120 | 120 | --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}' |
|
121 | 121 | Specify a regular expression to which commit log messages are |
|
122 | 122 | matched. If a match occurs, then the conversion process will |
|
123 | 123 | add the most recent revision on the branch indicated in the |
|
124 | 124 | regex as the second parent of the changeset. |
|
125 | 125 | |
|
126 | 126 | The hgext/convert/cvsps wrapper script allows the builtin changeset |
|
127 | 127 | merging code to be run without doing a conversion. Its parameters and |
|
128 | 128 | output are similar to that of cvsps 2.1. |
|
129 | 129 | |
|
130 | 130 | Subversion Source |
|
131 | 131 | ----------------- |
|
132 | 132 | |
|
133 | 133 | Subversion source detects classical trunk/branches/tags layouts. |
|
134 | 134 | By default, the supplied "svn://repo/path/" source URL is |
|
135 | 135 | converted as a single branch. If "svn://repo/path/trunk" exists |
|
136 | 136 | it replaces the default branch. If "svn://repo/path/branches" |
|
137 | 137 | exists, its subdirectories are listed as possible branches. If |
|
138 | 138 | "svn://repo/path/tags" exists, it is looked for tags referencing |
|
139 | 139 | converted branches. Default "trunk", "branches" and "tags" values |
|
140 | 140 | can be overriden with following options. Set them to paths |
|
141 | 141 | relative to the source URL, or leave them blank to disable |
|
142 | 142 | autodetection. |
|
143 | 143 | |
|
144 | 144 | --config convert.svn.branches=branches (directory name) |
|
145 | 145 | specify the directory containing branches |
|
146 | 146 | --config convert.svn.tags=tags (directory name) |
|
147 | 147 | specify the directory containing tags |
|
148 | 148 | --config convert.svn.trunk=trunk (directory name) |
|
149 | 149 | specify the name of the trunk branch |
|
150 | 150 | |
|
151 | 151 | Source history can be retrieved starting at a specific revision, |
|
152 | 152 | instead of being integrally converted. Only single branch |
|
153 | 153 | conversions are supported. |
|
154 | 154 | |
|
155 | 155 | --config convert.svn.startrev=0 (svn revision number) |
|
156 | 156 | specify start Subversion revision. |
|
157 | 157 | |
|
158 | 158 | Perforce Source |
|
159 | 159 | --------------- |
|
160 | 160 | |
|
161 | 161 | The Perforce (P4) importer can be given a p4 depot path or a client |
|
162 | 162 | specification as source. It will convert all files in the source to |
|
163 | 163 | a flat Mercurial repository, ignoring labels, branches and integrations. |
|
164 | 164 | Note that when a depot path is given you then usually should specify a |
|
165 | 165 | target directory, because otherwise the target may be named ...-hg. |
|
166 | 166 | |
|
167 | 167 | It is possible to limit the amount of source history to be converted |
|
168 | 168 | by specifying an initial Perforce revision. |
|
169 | 169 | |
|
170 | 170 | --config convert.p4.startrev=0 (perforce changelist number) |
|
171 | 171 | specify initial Perforce revision. |
|
172 | 172 | |
|
173 | 173 | |
|
174 | 174 | Mercurial Destination |
|
175 | 175 | --------------------- |
|
176 | 176 | |
|
177 | 177 | --config convert.hg.clonebranches=False (boolean) |
|
178 | 178 | dispatch source branches in separate clones. |
|
179 | 179 | --config convert.hg.tagsbranch=default (branch name) |
|
180 | 180 | tag revisions branch name |
|
181 | 181 | --config convert.hg.usebranchnames=True (boolean) |
|
182 | 182 | preserve branch names |
|
183 | 183 | |
|
184 | 184 | options: |
|
185 | 185 | |
|
186 | 186 | -A --authors username mapping filename |
|
187 | 187 | -d --dest-type destination repository type |
|
188 | 188 | --filemap remap file names using contents of file |
|
189 | 189 | -r --rev import up to target revision REV |
|
190 | 190 | -s --source-type source repository type |
|
191 | 191 | --splicemap splice synthesized history into place |
|
192 | 192 | --datesort try to sort changesets by date |
|
193 | 193 | |
|
194 | 194 | use "hg -v help convert" to show global options |
|
195 | 195 | adding a |
|
196 | 196 | assuming destination a-hg |
|
197 | 197 | initializing destination a-hg repository |
|
198 | 198 | scanning source... |
|
199 | 199 | sorting... |
|
200 | 200 | converting... |
|
201 | 201 | 4 a |
|
202 | 202 | 3 b |
|
203 | 203 | 2 c |
|
204 | 204 | 1 d |
|
205 | 205 | 0 e |
|
206 | 206 | pulling from ../a |
|
207 | 207 | searching for changes |
|
208 | 208 | no changes found |
|
209 | 209 | % should fail |
|
210 | 210 | initializing destination bogusfile repository |
|
211 | 211 | abort: cannot create new bundle repository |
|
212 | 212 | % should fail |
|
213 | 213 | abort: Permission denied: bogusdir |
|
214 | 214 | % should succeed |
|
215 | 215 | initializing destination bogusdir repository |
|
216 | 216 | scanning source... |
|
217 | 217 | sorting... |
|
218 | 218 | converting... |
|
219 | 219 | 4 a |
|
220 | 220 | 3 b |
|
221 | 221 | 2 c |
|
222 | 222 | 1 d |
|
223 | 223 | 0 e |
|
224 | 224 | % test pre and post conversion actions |
|
225 | 225 | run hg source pre-conversion action |
|
226 | 226 | run hg sink pre-conversion action |
|
227 | 227 | run hg sink post-conversion action |
|
228 | 228 | run hg source post-conversion action |
|
229 | % converting empty dir should fail nicely | |
|
230 | assuming destination emptydir-hg | |
|
231 | initializing destination emptydir-hg repository | |
|
232 | emptydir does not look like a CVS checkout | |
|
233 | emptydir does not look like a Git repo | |
|
234 | .../emptydir does not look like a Subversion repo | |
|
235 | emptydir is not a local Mercurial repo | |
|
236 | emptydir does not look like a darcs repo | |
|
237 | cannot find required "mtn" tool | |
|
238 | emptydir does not look like a GNU Arch repo | |
|
239 | emptydir does not look like a Bazaar repo | |
|
240 | cannot find required "p4" tool | |
|
241 | abort: emptydir: missing or unsupported repository |
General Comments 0
You need to be logged in to leave comments.
Login now