d72ffbb7773268514d8de29c5d5c33fdaf31f339
1 #! /usr/bin/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, popen2, pickle, getopt
24 import re
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
37 #------------------------------------------------------------------------------
39 def usage():
41 print """\
42 %s: [OPTIONS] <hgprj>
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)
50 required:
51 hgprj: name of the HG project to import (directory)
52 """ % sys.argv[0]
54 #------------------------------------------------------------------------------
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= ;'
70 env += 'export GIT_AUTHOR_DATE="%s" ;' % date
71 env += 'export GIT_COMMITTER_DATE="%s" ;' % date
72 return env
74 #------------------------------------------------------------------------------
76 state = ''
77 opt_nrepack = 0
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)
93 hgprj = args[0]
94 os.chdir(hgprj)
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'
104 tip = os.popen('hg tip --template "{rev}"').read()
105 print 'tip is', tip
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
127 hgparents[str(cset)] = (parent, mparent)
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)
143 if not hgvers.has_key("0"):
144 print 'creating repository'
145 os.system('git-init-db')
147 # loop through every hg changeset
148 for cset in range(int(tip) + 1):
150 # incremental, already seen
151 if hgvers.has_key(str(cset)):
152 continue
153 hgnewcsets += 1
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]
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)
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 '-----------------------------------------'
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)])
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))
201 # remove everything except .git and .hg directories
202 os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
204 # repopulate with checkouted files
205 os.system('hg update -C %d' % cset)
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')
212 # commit
213 os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment)
214 os.unlink(filecomment)
216 # tag
217 if tag and tag != 'tip':
218 os.system(getgitenv(user, date) + 'git-tag %s' % tag)
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)
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
230 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
231 os.system('git-repack -a -d')
233 # write the state for incrementals
234 if state:
235 print 'Writing state'
236 f = open(state, 'w')
237 pickle.dump(hgvers, f)
239 # vim: et ts=8 sw=4 sts=4