##// END OF EJS Templates
convert: add support for converting git submodule (issue3528)...
YaNan Xu -
r17929:0eed6632 default
parent child Browse files
Show More
@@ -1,217 +1,279
1 1 # git.py - git support for the convert extension
2 2 #
3 3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
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 import os
9 from mercurial import util
9 from mercurial import util, config
10 10 from mercurial.node import hex, nullid
11 11 from mercurial.i18n import _
12 12
13 13 from common import NoRepo, commit, converter_source, checktool
14 14
15 class submodule(object):
16 def __init__(self, path, node, url):
17 self.path = path
18 self.node = node
19 self.url = url
20
21 def hgsub(self):
22 return "%s = [git]%s" % (self.path, self.url)
23
24 def hgsubstate(self):
25 return "%s %s" % (self.node, self.path)
26
15 27 class convert_git(converter_source):
16 28 # Windows does not support GIT_DIR= construct while other systems
17 29 # cannot remove environment variable. Just assume none have
18 30 # both issues.
19 31 if util.safehasattr(os, 'unsetenv'):
20 32 def gitopen(self, s, noerr=False):
21 33 prevgitdir = os.environ.get('GIT_DIR')
22 34 os.environ['GIT_DIR'] = self.path
23 35 try:
24 36 if noerr:
25 37 (stdin, stdout, stderr) = util.popen3(s)
26 38 return stdout
27 39 else:
28 40 return util.popen(s, 'rb')
29 41 finally:
30 42 if prevgitdir is None:
31 43 del os.environ['GIT_DIR']
32 44 else:
33 45 os.environ['GIT_DIR'] = prevgitdir
34 46 else:
35 47 def gitopen(self, s, noerr=False):
36 48 if noerr:
37 49 (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s))
38 50 return so
39 51 else:
40 52 return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb')
41 53
42 54 def gitread(self, s):
43 55 fh = self.gitopen(s)
44 56 data = fh.read()
45 57 return data, fh.close()
46 58
47 59 def __init__(self, ui, path, rev=None):
48 60 super(convert_git, self).__init__(ui, path, rev=rev)
49 61
50 62 if os.path.isdir(path + "/.git"):
51 63 path += "/.git"
52 64 if not os.path.exists(path + "/objects"):
53 65 raise NoRepo(_("%s does not look like a Git repository") % path)
54 66
55 67 checktool('git', 'git')
56 68
57 69 self.path = path
70 self.submodules = []
58 71
59 72 def getheads(self):
60 73 if not self.rev:
61 74 heads, ret = self.gitread('git rev-parse --branches --remotes')
62 75 heads = heads.splitlines()
63 76 else:
64 77 heads, ret = self.gitread("git rev-parse --verify %s" % self.rev)
65 78 heads = [heads[:-1]]
66 79 if ret:
67 80 raise util.Abort(_('cannot retrieve git heads'))
68 81 return heads
69 82
70 83 def catfile(self, rev, type):
71 84 if rev == hex(nullid):
72 85 raise IOError
73 86 data, ret = self.gitread("git cat-file %s %s" % (type, rev))
74 87 if ret:
75 88 raise util.Abort(_('cannot read %r object at %s') % (type, rev))
76 89 return data
77 90
78 91 def getfile(self, name, rev):
79 data = self.catfile(rev, "blob")
80 mode = self.modecache[(name, rev)]
92 if name == '.hgsub':
93 data = '\n'.join([m.hgsub() for m in self.submoditer()])
94 mode = ''
95 elif name == '.hgsubstate':
96 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
97 mode = ''
98 else:
99 data = self.catfile(rev, "blob")
100 mode = self.modecache[(name, rev)]
81 101 return data, mode
82 102
103 def submoditer(self):
104 null = hex(nullid)
105 for m in sorted(self.submodules, key=lambda p: p.path):
106 if m.node != null:
107 yield m
108
109 def parsegitmodules(self, content):
110 """Parse the formatted .gitmodules file, example file format:
111 [submodule "sub"]\n
112 \tpath = sub\n
113 \turl = git://giturl\n
114 """
115 self.submodules = []
116 c = config.config()
117 # Each item in .gitmodules starts with \t that cant be parsed
118 c.parse('.gitmodules', content.replace('\t',''))
119 for sec in c.sections():
120 s = c[sec]
121 if 'url' in s and 'path' in s:
122 self.submodules.append(submodule(s['path'], '', s['url']))
123
124 def retrievegitmodules(self, version):
125 modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules'))
126 if ret:
127 raise util.Abort(_('cannot read submodules config file in %s') % version)
128 self.parsegitmodules(modules)
129 for m in self.submodules:
130 node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path))
131 if ret:
132 continue
133 m.node = node.strip()
134
83 135 def getchanges(self, version):
84 136 self.modecache = {}
85 137 fh = self.gitopen("git diff-tree -z --root -m -r %s" % version)
86 138 changes = []
87 139 seen = set()
88 140 entry = None
141 subexists = False
89 142 for l in fh.read().split('\x00'):
90 143 if not entry:
91 144 if not l.startswith(':'):
92 145 continue
93 146 entry = l
94 147 continue
95 148 f = l
96 149 if f not in seen:
97 150 seen.add(f)
98 151 entry = entry.split()
99 152 h = entry[3]
100 if entry[1] == '160000':
101 raise util.Abort('git submodules are not supported!')
102 153 p = (entry[1] == "100755")
103 154 s = (entry[1] == "120000")
104 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
105 changes.append((f, h))
155
156 if f == '.gitmodules':
157 subexists = True
158 changes.append(('.hgsub', ''))
159 elif entry[1] == '160000' or entry[0] == ':160000':
160 subexists = True
161 else:
162 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
163 changes.append((f, h))
106 164 entry = None
107 165 if fh.close():
108 166 raise util.Abort(_('cannot read changes in %s') % version)
167
168 if subexists:
169 self.retrievegitmodules(version)
170 changes.append(('.hgsubstate', ''))
109 171 return (changes, {})
110 172
111 173 def getcommit(self, version):
112 174 c = self.catfile(version, "commit") # read the commit hash
113 175 end = c.find("\n\n")
114 176 message = c[end + 2:]
115 177 message = self.recode(message)
116 178 l = c[:end].splitlines()
117 179 parents = []
118 180 author = committer = None
119 181 for e in l[1:]:
120 182 n, v = e.split(" ", 1)
121 183 if n == "author":
122 184 p = v.split()
123 185 tm, tz = p[-2:]
124 186 author = " ".join(p[:-2])
125 187 if author[0] == "<": author = author[1:-1]
126 188 author = self.recode(author)
127 189 if n == "committer":
128 190 p = v.split()
129 191 tm, tz = p[-2:]
130 192 committer = " ".join(p[:-2])
131 193 if committer[0] == "<": committer = committer[1:-1]
132 194 committer = self.recode(committer)
133 195 if n == "parent":
134 196 parents.append(v)
135 197
136 198 if committer and committer != author:
137 199 message += "\ncommitter: %s\n" % committer
138 200 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
139 201 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
140 202 date = tm + " " + str(tz)
141 203
142 204 c = commit(parents=parents, date=date, author=author, desc=message,
143 205 rev=version)
144 206 return c
145 207
146 208 def gettags(self):
147 209 tags = {}
148 210 alltags = {}
149 211 fh = self.gitopen('git ls-remote --tags "%s"' % self.path)
150 212 prefix = 'refs/tags/'
151 213
152 214 # Build complete list of tags, both annotated and bare ones
153 215 for line in fh:
154 216 line = line.strip()
155 217 node, tag = line.split(None, 1)
156 218 if not tag.startswith(prefix):
157 219 continue
158 220 alltags[tag[len(prefix):]] = node
159 221 if fh.close():
160 222 raise util.Abort(_('cannot read tags from %s') % self.path)
161 223
162 224 # Filter out tag objects for annotated tag refs
163 225 for tag in alltags:
164 226 if tag.endswith('^{}'):
165 227 tags[tag[:-3]] = alltags[tag]
166 228 else:
167 229 if tag + '^{}' in alltags:
168 230 continue
169 231 else:
170 232 tags[tag] = alltags[tag]
171 233
172 234 return tags
173 235
174 236 def getchangedfiles(self, version, i):
175 237 changes = []
176 238 if i is None:
177 239 fh = self.gitopen("git diff-tree --root -m -r %s" % version)
178 240 for l in fh:
179 241 if "\t" not in l:
180 242 continue
181 243 m, f = l[:-1].split("\t")
182 244 changes.append(f)
183 245 else:
184 246 fh = self.gitopen('git diff-tree --name-only --root -r %s '
185 247 '"%s^%s" --' % (version, version, i + 1))
186 248 changes = [f.rstrip('\n') for f in fh]
187 249 if fh.close():
188 250 raise util.Abort(_('cannot read changes in %s') % version)
189 251
190 252 return changes
191 253
192 254 def getbookmarks(self):
193 255 bookmarks = {}
194 256
195 257 # Interesting references in git are prefixed
196 258 prefix = 'refs/heads/'
197 259 prefixlen = len(prefix)
198 260
199 261 # factor two commands
200 262 gitcmd = { 'remote/': 'git ls-remote --heads origin',
201 263 '': 'git show-ref'}
202 264
203 265 # Origin heads
204 266 for reftype in gitcmd:
205 267 try:
206 268 fh = self.gitopen(gitcmd[reftype], noerr=True)
207 269 for line in fh:
208 270 line = line.strip()
209 271 rev, name = line.split(None, 1)
210 272 if not name.startswith(prefix):
211 273 continue
212 274 name = '%s%s' % (reftype, name[prefixlen:])
213 275 bookmarks[name] = rev
214 276 except Exception:
215 277 pass
216 278
217 279 return bookmarks
@@ -1,300 +1,347
1 1
2 2 $ "$TESTDIR/hghave" git || exit 80
3 3 $ echo "[core]" >> $HOME/.gitconfig
4 4 $ echo "autocrlf = false" >> $HOME/.gitconfig
5 5 $ echo "[core]" >> $HOME/.gitconfig
6 6 $ echo "autocrlf = false" >> $HOME/.gitconfig
7 7 $ echo "[extensions]" >> $HGRCPATH
8 8 $ echo "convert=" >> $HGRCPATH
9 9 $ echo 'hgext.graphlog =' >> $HGRCPATH
10 10 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
11 11 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
12 12 $ GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE
13 13 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
14 14 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
15 15 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
16 16 $ count=10
17 17 $ commit()
18 18 > {
19 19 > GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000"
20 20 > GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
21 21 > git commit "$@" >/dev/null 2>/dev/null || echo "git commit error"
22 22 > count=`expr $count + 1`
23 23 > }
24 24 $ mkdir git-repo
25 25 $ cd git-repo
26 26 $ git init-db >/dev/null 2>/dev/null
27 27 $ echo a > a
28 28 $ mkdir d
29 29 $ echo b > d/b
30 30 $ git add a d
31 31 $ commit -a -m t1
32 32
33 33 Remove the directory, then try to replace it with a file
34 34 (issue 754)
35 35
36 36 $ git rm -f d/b
37 37 rm 'd/b'
38 38 $ commit -m t2
39 39 $ echo d > d
40 40 $ git add d
41 41 $ commit -m t3
42 42 $ echo b >> a
43 43 $ commit -a -m t4.1
44 44 $ git checkout -b other HEAD~ >/dev/null 2>/dev/null
45 45 $ echo c > a
46 46 $ echo a >> a
47 47 $ commit -a -m t4.2
48 48 $ git checkout master >/dev/null 2>/dev/null
49 49 $ git pull --no-commit . other > /dev/null 2>/dev/null
50 50 $ commit -m 'Merge branch other'
51 51 $ cd ..
52 52 $ hg convert --datesort git-repo
53 53 assuming destination git-repo-hg
54 54 initializing destination git-repo-hg repository
55 55 scanning source...
56 56 sorting...
57 57 converting...
58 58 5 t1
59 59 4 t2
60 60 3 t3
61 61 2 t4.1
62 62 1 t4.2
63 63 0 Merge branch other
64 64 updating bookmarks
65 65 $ hg up -q -R git-repo-hg
66 66 $ hg -R git-repo-hg tip -v
67 67 changeset: 5:c78094926be2
68 68 bookmark: master
69 69 tag: tip
70 70 parent: 3:f5f5cb45432b
71 71 parent: 4:4e174f80c67c
72 72 user: test <test@example.org>
73 73 date: Mon Jan 01 00:00:15 2007 +0000
74 74 files: a
75 75 description:
76 76 Merge branch other
77 77
78 78
79 79 $ count=10
80 80 $ mkdir git-repo2
81 81 $ cd git-repo2
82 82 $ git init-db >/dev/null 2>/dev/null
83 83 $ echo foo > foo
84 84 $ git add foo
85 85 $ commit -a -m 'add foo'
86 86 $ echo >> foo
87 87 $ commit -a -m 'change foo'
88 88 $ git checkout -b Bar HEAD~ >/dev/null 2>/dev/null
89 89 $ echo quux >> quux
90 90 $ git add quux
91 91 $ commit -a -m 'add quux'
92 92 $ echo bar > bar
93 93 $ git add bar
94 94 $ commit -a -m 'add bar'
95 95 $ git checkout -b Baz HEAD~ >/dev/null 2>/dev/null
96 96 $ echo baz > baz
97 97 $ git add baz
98 98 $ commit -a -m 'add baz'
99 99 $ git checkout master >/dev/null 2>/dev/null
100 100 $ git pull --no-commit . Bar Baz > /dev/null 2>/dev/null
101 101 $ commit -m 'Octopus merge'
102 102 $ echo bar >> bar
103 103 $ commit -a -m 'change bar'
104 104 $ git checkout -b Foo HEAD~ >/dev/null 2>/dev/null
105 105 $ echo >> foo
106 106 $ commit -a -m 'change foo'
107 107 $ git checkout master >/dev/null 2>/dev/null
108 108 $ git pull --no-commit -s ours . Foo > /dev/null 2>/dev/null
109 109 $ commit -m 'Discard change to foo'
110 110 $ cd ..
111 111 $ glog()
112 112 > {
113 113 > hg glog --template '{rev} "{desc|firstline}" files: {files}\n' "$@"
114 114 > }
115 115 $ splitrepo()
116 116 > {
117 117 > msg="$1"
118 118 > files="$2"
119 119 > opts=$3
120 120 > echo "% $files: $msg"
121 121 > prefix=`echo "$files" | sed -e 's/ /-/g'`
122 122 > fmap="$prefix.fmap"
123 123 > repo="$prefix.repo"
124 124 > for i in $files; do
125 125 > echo "include $i" >> "$fmap"
126 126 > done
127 127 > hg -q convert $opts --filemap "$fmap" --datesort git-repo2 "$repo"
128 128 > hg up -q -R "$repo"
129 129 > glog -R "$repo"
130 130 > hg -R "$repo" manifest --debug
131 131 > }
132 132
133 133 full conversion
134 134
135 135 $ hg -q convert --datesort git-repo2 fullrepo
136 136 $ hg up -q -R fullrepo
137 137 $ glog -R fullrepo
138 138 @ 9 "Discard change to foo" files: foo
139 139 |\
140 140 | o 8 "change foo" files: foo
141 141 | |
142 142 o | 7 "change bar" files: bar
143 143 |/
144 144 o 6 "(octopus merge fixup)" files:
145 145 |\
146 146 | o 5 "Octopus merge" files: baz
147 147 | |\
148 148 o | | 4 "add baz" files: baz
149 149 | | |
150 150 +---o 3 "add bar" files: bar
151 151 | |
152 152 o | 2 "add quux" files: quux
153 153 | |
154 154 | o 1 "change foo" files: foo
155 155 |/
156 156 o 0 "add foo" files: foo
157 157
158 158 $ hg -R fullrepo manifest --debug
159 159 245a3b8bc653999c2b22cdabd517ccb47aecafdf 644 bar
160 160 354ae8da6e890359ef49ade27b68bbc361f3ca88 644 baz
161 161 9277c9cc8dd4576fc01a17939b4351e5ada93466 644 foo
162 162 88dfeab657e8cf2cef3dec67b914f49791ae76b1 644 quux
163 163 $ splitrepo 'octopus merge' 'foo bar baz'
164 164 % foo bar baz: octopus merge
165 165 @ 8 "Discard change to foo" files: foo
166 166 |\
167 167 | o 7 "change foo" files: foo
168 168 | |
169 169 o | 6 "change bar" files: bar
170 170 |/
171 171 o 5 "(octopus merge fixup)" files:
172 172 |\
173 173 | o 4 "Octopus merge" files: baz
174 174 | |\
175 175 o | | 3 "add baz" files: baz
176 176 | | |
177 177 +---o 2 "add bar" files: bar
178 178 | |
179 179 | o 1 "change foo" files: foo
180 180 |/
181 181 o 0 "add foo" files: foo
182 182
183 183 245a3b8bc653999c2b22cdabd517ccb47aecafdf 644 bar
184 184 354ae8da6e890359ef49ade27b68bbc361f3ca88 644 baz
185 185 9277c9cc8dd4576fc01a17939b4351e5ada93466 644 foo
186 186 $ splitrepo 'only some parents of an octopus merge; "discard" a head' 'foo baz quux'
187 187 % foo baz quux: only some parents of an octopus merge; "discard" a head
188 188 @ 6 "Discard change to foo" files: foo
189 189 |
190 190 o 5 "change foo" files: foo
191 191 |
192 192 o 4 "Octopus merge" files:
193 193 |\
194 194 | o 3 "add baz" files: baz
195 195 | |
196 196 | o 2 "add quux" files: quux
197 197 | |
198 198 o | 1 "change foo" files: foo
199 199 |/
200 200 o 0 "add foo" files: foo
201 201
202 202 354ae8da6e890359ef49ade27b68bbc361f3ca88 644 baz
203 203 9277c9cc8dd4576fc01a17939b4351e5ada93466 644 foo
204 204 88dfeab657e8cf2cef3dec67b914f49791ae76b1 644 quux
205 205
206 206 test binary conversion (issue 1359)
207 207
208 208 $ mkdir git-repo3
209 209 $ cd git-repo3
210 210 $ git init-db >/dev/null 2>/dev/null
211 211 $ python -c 'file("b", "wb").write("".join([chr(i) for i in range(256)])*16)'
212 212 $ git add b
213 213 $ commit -a -m addbinary
214 214 $ cd ..
215 215
216 216 convert binary file
217 217
218 218 $ hg convert git-repo3 git-repo3-hg
219 219 initializing destination git-repo3-hg repository
220 220 scanning source...
221 221 sorting...
222 222 converting...
223 223 0 addbinary
224 224 updating bookmarks
225 225 $ cd git-repo3-hg
226 226 $ hg up -C
227 227 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
228 228 $ python -c 'print len(file("b", "rb").read())'
229 229 4096
230 230 $ cd ..
231 231
232 232 test author vs committer
233 233
234 234 $ mkdir git-repo4
235 235 $ cd git-repo4
236 236 $ git init-db >/dev/null 2>/dev/null
237 237 $ echo >> foo
238 238 $ git add foo
239 239 $ commit -a -m addfoo
240 240 $ echo >> foo
241 241 $ GIT_AUTHOR_NAME="nottest"
242 242 $ commit -a -m addfoo2
243 243 $ cd ..
244 244
245 245 convert author committer
246 246
247 247 $ hg convert git-repo4 git-repo4-hg
248 248 initializing destination git-repo4-hg repository
249 249 scanning source...
250 250 sorting...
251 251 converting...
252 252 1 addfoo
253 253 0 addfoo2
254 254 updating bookmarks
255 255 $ hg -R git-repo4-hg log -v
256 256 changeset: 1:d63e967f93da
257 257 bookmark: master
258 258 tag: tip
259 259 user: nottest <test@example.org>
260 260 date: Mon Jan 01 00:00:21 2007 +0000
261 261 files: foo
262 262 description:
263 263 addfoo2
264 264
265 265 committer: test <test@example.org>
266 266
267 267
268 268 changeset: 0:0735477b0224
269 269 user: test <test@example.org>
270 270 date: Mon Jan 01 00:00:20 2007 +0000
271 271 files: foo
272 272 description:
273 273 addfoo
274 274
275 275
276 276
277 277 --sourceorder should fail
278 278
279 279 $ hg convert --sourcesort git-repo4 git-repo4-sourcesort-hg
280 280 initializing destination git-repo4-sourcesort-hg repository
281 281 abort: --sourcesort is not supported by this data source
282 282 [255]
283 283
284 284 damage git repository and convert again
285 285
286 286 $ cat > damage.py <<EOF
287 287 > import os
288 288 > import stat
289 289 > for root, dirs, files in os.walk('git-repo4/.git/objects'):
290 290 > if files:
291 291 > path = os.path.join(root, files[0])
292 292 > if os.name == 'nt':
293 293 > os.chmod(path, stat.S_IWUSR)
294 294 > os.remove(path)
295 295 > break
296 296 > EOF
297 297 $ python damage.py
298 298 $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | \
299 299 > grep 'abort:' | sed 's/abort:.*/abort:/g'
300 300 abort:
301
302 test sub modules
303
304 $ mkdir git-repo5
305 $ cd git-repo5
306 $ git init-db >/dev/null 2>/dev/null
307 $ echo 'sub' >> foo
308 $ git add foo
309 $ commit -a -m 'addfoo'
310 $ BASE=${PWD}
311 $ cd ..
312 $ mkdir git-repo6
313 $ cd git-repo6
314 $ git init-db >/dev/null 2>/dev/null
315 $ git submodule add ${BASE} >/dev/null 2>/dev/null
316 $ commit -a -m 'addsubmodule' >/dev/null 2>/dev/null
317 $ cd ..
318
319 convert sub modules
320 $ hg convert git-repo6 git-repo6-hg
321 initializing destination git-repo6-hg repository
322 scanning source...
323 sorting...
324 converting...
325 0 addsubmodule
326 updating bookmarks
327 $ hg -R git-repo6-hg log -v
328 changeset: 0:* (glob)
329 bookmark: master
330 tag: tip
331 user: nottest <test@example.org>
332 date: Mon Jan 01 00:00:23 2007 +0000
333 files: .hgsub .hgsubstate
334 description:
335 addsubmodule
336
337 committer: test <test@example.org>
338
339
340
341 $ cd git-repo6-hg
342 $ hg up >/dev/null 2>/dev/null
343 $ cat .hgsubstate
344 * git-repo5 (glob)
345 $ cd git-repo5
346 $ cat foo
347 sub
General Comments 0
You need to be logged in to leave comments. Login now