Merge branch 'sb/push-options-via-transport'
[alt-git.git] / contrib / hg-to-git / hg-to-git.py
blob60dec86d37efbffb557e611cde4bf2b2dbfbf478
1 #!/usr/bin/env python
3 """ hg-to-git.py - A Mercurial to GIT converter
5 Copyright (C)2007 Stelian Pop <stelian@popies.net>
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.
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.
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 """
22 import os, os.path, sys
23 import tempfile, pickle, getopt
24 import re
26 if sys.hexversion < 0x02030000:
27 # The behavior of the pickle module changed significantly in 2.3
28 sys.stderr.write("hg-to-git.py: requires Python 2.3 or later.\n")
29 sys.exit(1)
31 # Maps hg version -> git version
32 hgvers = {}
33 # List of children for each hg revision
34 hgchildren = {}
35 # List of parents for each hg revision
36 hgparents = {}
37 # Current branch for each hg revision
38 hgbranch = {}
39 # Number of new changesets converted from hg
40 hgnewcsets = 0
42 #------------------------------------------------------------------------------
44 def usage():
46 print """\
47 %s: [OPTIONS] <hgprj>
49 options:
50 -s, --gitstate=FILE: name of the state to be saved/read
51 for incrementals
52 -n, --nrepack=INT: number of changesets that will trigger
53 a repack (default=0, -1 to deactivate)
54 -v, --verbose: be verbose
56 required:
57 hgprj: name of the HG project to import (directory)
58 """ % sys.argv[0]
60 #------------------------------------------------------------------------------
62 def getgitenv(user, date):
63 env = ''
64 elems = re.compile('(.*?)\s+<(.*)>').match(user)
65 if elems:
66 env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1)
67 env += 'export GIT_COMMITTER_NAME="%s" ;' % elems.group(1)
68 env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2)
69 env += 'export GIT_COMMITTER_EMAIL="%s" ;' % elems.group(2)
70 else:
71 env += 'export GIT_AUTHOR_NAME="%s" ;' % user
72 env += 'export GIT_COMMITTER_NAME="%s" ;' % user
73 env += 'export GIT_AUTHOR_EMAIL= ;'
74 env += 'export GIT_COMMITTER_EMAIL= ;'
76 env += 'export GIT_AUTHOR_DATE="%s" ;' % date
77 env += 'export GIT_COMMITTER_DATE="%s" ;' % date
78 return env
80 #------------------------------------------------------------------------------
82 state = ''
83 opt_nrepack = 0
84 verbose = False
86 try:
87 opts, args = getopt.getopt(sys.argv[1:], 's:t:n:v', ['gitstate=', 'tempdir=', 'nrepack=', 'verbose'])
88 for o, a in opts:
89 if o in ('-s', '--gitstate'):
90 state = a
91 state = os.path.abspath(state)
92 if o in ('-n', '--nrepack'):
93 opt_nrepack = int(a)
94 if o in ('-v', '--verbose'):
95 verbose = True
96 if len(args) != 1:
97 raise Exception('params')
98 except:
99 usage()
100 sys.exit(1)
102 hgprj = args[0]
103 os.chdir(hgprj)
105 if state:
106 if os.path.exists(state):
107 if verbose:
108 print 'State does exist, reading'
109 f = open(state, 'r')
110 hgvers = pickle.load(f)
111 else:
112 print 'State does not exist, first run'
114 sock = os.popen('hg tip --template "{rev}"')
115 tip = sock.read()
116 if sock.close():
117 sys.exit(1)
118 if verbose:
119 print 'tip is', tip
121 # Calculate the branches
122 if verbose:
123 print 'analysing the branches...'
124 hgchildren["0"] = ()
125 hgparents["0"] = (None, None)
126 hgbranch["0"] = "master"
127 for cset in range(1, int(tip) + 1):
128 hgchildren[str(cset)] = ()
129 prnts = os.popen('hg log -r %d --template "{parents}"' % cset).read().strip().split(' ')
130 prnts = map(lambda x: x[:x.find(':')], prnts)
131 if prnts[0] != '':
132 parent = prnts[0].strip()
133 else:
134 parent = str(cset - 1)
135 hgchildren[parent] += ( str(cset), )
136 if len(prnts) > 1:
137 mparent = prnts[1].strip()
138 hgchildren[mparent] += ( str(cset), )
139 else:
140 mparent = None
142 hgparents[str(cset)] = (parent, mparent)
144 if mparent:
145 # For merge changesets, take either one, preferably the 'master' branch
146 if hgbranch[mparent] == 'master':
147 hgbranch[str(cset)] = 'master'
148 else:
149 hgbranch[str(cset)] = hgbranch[parent]
150 else:
151 # Normal changesets
152 # For first children, take the parent branch, for the others create a new branch
153 if hgchildren[parent][0] == str(cset):
154 hgbranch[str(cset)] = hgbranch[parent]
155 else:
156 hgbranch[str(cset)] = "branch-" + str(cset)
158 if not hgvers.has_key("0"):
159 print 'creating repository'
160 os.system('git init')
162 # loop through every hg changeset
163 for cset in range(int(tip) + 1):
165 # incremental, already seen
166 if hgvers.has_key(str(cset)):
167 continue
168 hgnewcsets += 1
170 # get info
171 log_data = os.popen('hg log -r %d --template "{tags}\n{date|date}\n{author}\n"' % cset).readlines()
172 tag = log_data[0].strip()
173 date = log_data[1].strip()
174 user = log_data[2].strip()
175 parent = hgparents[str(cset)][0]
176 mparent = hgparents[str(cset)][1]
178 #get comment
179 (fdcomment, filecomment) = tempfile.mkstemp()
180 csetcomment = os.popen('hg log -r %d --template "{desc}"' % cset).read().strip()
181 os.write(fdcomment, csetcomment)
182 os.close(fdcomment)
184 print '-----------------------------------------'
185 print 'cset:', cset
186 print 'branch:', hgbranch[str(cset)]
187 print 'user:', user
188 print 'date:', date
189 print 'comment:', csetcomment
190 if parent:
191 print 'parent:', parent
192 if mparent:
193 print 'mparent:', mparent
194 if tag:
195 print 'tag:', tag
196 print '-----------------------------------------'
198 # checkout the parent if necessary
199 if cset != 0:
200 if hgbranch[str(cset)] == "branch-" + str(cset):
201 print 'creating new branch', hgbranch[str(cset)]
202 os.system('git checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent]))
203 else:
204 print 'checking out branch', hgbranch[str(cset)]
205 os.system('git checkout %s' % hgbranch[str(cset)])
207 # merge
208 if mparent:
209 if hgbranch[parent] == hgbranch[str(cset)]:
210 otherbranch = hgbranch[mparent]
211 else:
212 otherbranch = hgbranch[parent]
213 print 'merging', otherbranch, 'into', hgbranch[str(cset)]
214 os.system(getgitenv(user, date) + 'git merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch))
216 # remove everything except .git and .hg directories
217 os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
219 # repopulate with checkouted files
220 os.system('hg update -C %d' % cset)
222 # add new files
223 os.system('git ls-files -x .hg --others | git update-index --add --stdin')
224 # delete removed files
225 os.system('git ls-files -x .hg --deleted | git update-index --remove --stdin')
227 # commit
228 os.system(getgitenv(user, date) + 'git commit --allow-empty --allow-empty-message -a -F %s' % filecomment)
229 os.unlink(filecomment)
231 # tag
232 if tag and tag != 'tip':
233 os.system(getgitenv(user, date) + 'git tag %s' % tag)
235 # delete branch if not used anymore...
236 if mparent and len(hgchildren[str(cset)]):
237 print "Deleting unused branch:", otherbranch
238 os.system('git branch -d %s' % otherbranch)
240 # retrieve and record the version
241 vvv = os.popen('git show --quiet --pretty=format:%H').read()
242 print 'record', cset, '->', vvv
243 hgvers[str(cset)] = vvv
245 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
246 os.system('git repack -a -d')
248 # write the state for incrementals
249 if state:
250 if verbose:
251 print 'Writing state'
252 f = open(state, 'w')
253 pickle.dump(hgvers, f)
255 # vim: et ts=8 sw=4 sts=4