hg-to-git: fix parent analysis
[git/git.git] / contrib / hg-to-git / hg-to-git.py
1 #! /usr/bin/python
2
3 """ hg-to-git.py - A Mercurial to GIT converter
4
5 Copyright (C)2007 Stelian Pop <stelian@popies.net>
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2, or (at your option)
10 any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20 """
21
22 import os, os.path, sys
23 import tempfile, popen2, pickle, getopt
24 import re
25
26 # Maps hg version -> git version
27 hgvers = {}
28 # List of children for each hg revision
29 hgchildren = {}
30 # List of parents for each hg revision
31 hgparents = {}
32 # Current branch for each hg revision
33 hgbranch = {}
34 # Number of new changesets converted from hg
35 hgnewcsets = 0
36
37 #------------------------------------------------------------------------------
38
39 def usage():
40
41 print """\
42 %s: [OPTIONS] <hgprj>
43
44 options:
45 -s, --gitstate=FILE: name of the state to be saved/read
46 for incrementals
47 -n, --nrepack=INT: number of changesets that will trigger
48 a repack (default=0, -1 to deactivate)
49
50 required:
51 hgprj: name of the HG project to import (directory)
52 """ % sys.argv[0]
53
54 #------------------------------------------------------------------------------
55
56 def getgitenv(user, date):
57 env = ''
58 elems = re.compile('(.*?)\s+<(.*)>').match(user)
59 if elems:
60 env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1)
61 env += 'export GIT_COMMITER_NAME="%s" ;' % elems.group(1)
62 env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2)
63 env += 'export GIT_COMMITER_EMAIL="%s" ;' % elems.group(2)
64 else:
65 env += 'export GIT_AUTHOR_NAME="%s" ;' % user
66 env += 'export GIT_COMMITER_NAME="%s" ;' % user
67 env += 'export GIT_AUTHOR_EMAIL= ;'
68 env += 'export GIT_COMMITER_EMAIL= ;'
69
70 env += 'export GIT_AUTHOR_DATE="%s" ;' % date
71 env += 'export GIT_COMMITTER_DATE="%s" ;' % date
72 return env
73
74 #------------------------------------------------------------------------------
75
76 state = ''
77 opt_nrepack = 0
78
79 try:
80 opts, args = getopt.getopt(sys.argv[1:], 's:t:n:', ['gitstate=', 'tempdir=', 'nrepack='])
81 for o, a in opts:
82 if o in ('-s', '--gitstate'):
83 state = a
84 state = os.path.abspath(state)
85 if o in ('-n', '--nrepack'):
86 opt_nrepack = int(a)
87 if len(args) != 1:
88 raise('params')
89 except:
90 usage()
91 sys.exit(1)
92
93 hgprj = args[0]
94 os.chdir(hgprj)
95
96 if state:
97 if os.path.exists(state):
98 print 'State does exist, reading'
99 f = open(state, 'r')
100 hgvers = pickle.load(f)
101 else:
102 print 'State does not exist, first run'
103
104 tip = os.popen('hg tip --template "{rev}"').read()
105 print 'tip is', tip
106
107 # Calculate the branches
108 print 'analysing the branches...'
109 hgchildren["0"] = ()
110 hgparents["0"] = (None, None)
111 hgbranch["0"] = "master"
112 for cset in range(1, int(tip) + 1):
113 hgchildren[str(cset)] = ()
114 prnts = os.popen('hg log -r %d --template "{parents}"' % cset).read().strip().split(' ')
115 prnts = map(lambda x: x[:x.find(':')], prnts)
116 if prnts[0] != '':
117 parent = prnts[0].strip()
118 else:
119 parent = str(cset - 1)
120 hgchildren[parent] += ( str(cset), )
121 if len(prnts) > 1:
122 mparent = prnts[1].strip()
123 hgchildren[mparent] += ( str(cset), )
124 else:
125 mparent = None
126
127 hgparents[str(cset)] = (parent, mparent)
128
129 if mparent:
130 # For merge changesets, take either one, preferably the 'master' branch
131 if hgbranch[mparent] == 'master':
132 hgbranch[str(cset)] = 'master'
133 else:
134 hgbranch[str(cset)] = hgbranch[parent]
135 else:
136 # Normal changesets
137 # For first children, take the parent branch, for the others create a new branch
138 if hgchildren[parent][0] == str(cset):
139 hgbranch[str(cset)] = hgbranch[parent]
140 else:
141 hgbranch[str(cset)] = "branch-" + str(cset)
142
143 if not hgvers.has_key("0"):
144 print 'creating repository'
145 os.system('git-init-db')
146
147 # loop through every hg changeset
148 for cset in range(int(tip) + 1):
149
150 # incremental, already seen
151 if hgvers.has_key(str(cset)):
152 continue
153 hgnewcsets += 1
154
155 # get info
156 log_data = os.popen('hg log -r %d --template "{tags}\n{date|date}\n{author}\n"' % cset).readlines()
157 tag = log_data[0].strip()
158 date = log_data[1].strip()
159 user = log_data[2].strip()
160 parent = hgparents[str(cset)][0]
161 mparent = hgparents[str(cset)][1]
162
163 #get comment
164 (fdcomment, filecomment) = tempfile.mkstemp()
165 csetcomment = os.popen('hg log -r %d --template "{desc}"' % cset).read().strip()
166 os.write(fdcomment, csetcomment)
167 os.close(fdcomment)
168
169 print '-----------------------------------------'
170 print 'cset:', cset
171 print 'branch:', hgbranch[str(cset)]
172 print 'user:', user
173 print 'date:', date
174 print 'comment:', csetcomment
175 if parent:
176 print 'parent:', parent
177 if mparent:
178 print 'mparent:', mparent
179 if tag:
180 print 'tag:', tag
181 print '-----------------------------------------'
182
183 # checkout the parent if necessary
184 if cset != 0:
185 if hgbranch[str(cset)] == "branch-" + str(cset):
186 print 'creating new branch', hgbranch[str(cset)]
187 os.system('git-checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent]))
188 else:
189 print 'checking out branch', hgbranch[str(cset)]
190 os.system('git-checkout %s' % hgbranch[str(cset)])
191
192 # merge
193 if mparent:
194 if hgbranch[parent] == hgbranch[str(cset)]:
195 otherbranch = hgbranch[mparent]
196 else:
197 otherbranch = hgbranch[parent]
198 print 'merging', otherbranch, 'into', hgbranch[str(cset)]
199 os.system(getgitenv(user, date) + 'git-merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch))
200
201 # remove everything except .git and .hg directories
202 os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
203
204 # repopulate with checkouted files
205 os.system('hg update -C %d' % cset)
206
207 # add new files
208 os.system('git-ls-files -x .hg --others | git-update-index --add --stdin')
209 # delete removed files
210 os.system('git-ls-files -x .hg --deleted | git-update-index --remove --stdin')
211
212 # commit
213 os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment)
214 os.unlink(filecomment)
215
216 # tag
217 if tag and tag != 'tip':
218 os.system(getgitenv(user, date) + 'git-tag %s' % tag)
219
220 # delete branch if not used anymore...
221 if mparent and len(hgchildren[str(cset)]):
222 print "Deleting unused branch:", otherbranch
223 os.system('git-branch -d %s' % otherbranch)
224
225 # retrieve and record the version
226 vvv = os.popen('git-show --quiet --pretty=format:%H').read()
227 print 'record', cset, '->', vvv
228 hgvers[str(cset)] = vvv
229
230 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
231 os.system('git-repack -a -d')
232
233 # write the state for incrementals
234 if state:
235 print 'Writing state'
236 f = open(state, 'w')
237 pickle.dump(hgvers, f)
238
239 # vim: et ts=8 sw=4 sts=4