Merge branch 'am/stash-branch'
[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     -v, --verbose:       be verbose
50
51 required:
52     hgprj:  name of the HG project to import (directory)
53 """ % sys.argv[0]
54
55 #------------------------------------------------------------------------------
56
57 def getgitenv(user, date):
58     env = ''
59     elems = re.compile('(.*?)\s+<(.*)>').match(user)
60     if elems:
61         env += 'export GIT_AUTHOR_NAME="%s" ;' % elems.group(1)
62         env += 'export GIT_COMMITER_NAME="%s" ;' % elems.group(1)
63         env += 'export GIT_AUTHOR_EMAIL="%s" ;' % elems.group(2)
64         env += 'export GIT_COMMITER_EMAIL="%s" ;' % elems.group(2)
65     else:
66         env += 'export GIT_AUTHOR_NAME="%s" ;' % user
67         env += 'export GIT_COMMITER_NAME="%s" ;' % user
68         env += 'export GIT_AUTHOR_EMAIL= ;'
69         env += 'export GIT_COMMITER_EMAIL= ;'
70
71     env += 'export GIT_AUTHOR_DATE="%s" ;' % date
72     env += 'export GIT_COMMITTER_DATE="%s" ;' % date
73     return env
74
75 #------------------------------------------------------------------------------
76
77 state = ''
78 opt_nrepack = 0
79 verbose = False
80
81 try:
82     opts, args = getopt.getopt(sys.argv[1:], 's:t:n:v', ['gitstate=', 'tempdir=', 'nrepack=', 'verbose'])
83     for o, a in opts:
84         if o in ('-s', '--gitstate'):
85             state = a
86             state = os.path.abspath(state)
87         if o in ('-n', '--nrepack'):
88             opt_nrepack = int(a)
89         if o in ('-v', '--verbose'):
90             verbose = True
91     if len(args) != 1:
92         raise Exception('params')
93 except:
94     usage()
95     sys.exit(1)
96
97 hgprj = args[0]
98 os.chdir(hgprj)
99
100 if state:
101     if os.path.exists(state):
102         if verbose:
103             print 'State does exist, reading'
104         f = open(state, 'r')
105         hgvers = pickle.load(f)
106     else:
107         print 'State does not exist, first run'
108
109 sock = os.popen('hg tip --template "{rev}"')
110 tip = sock.read()
111 if sock.close():
112     sys.exit(1)
113 if verbose:
114     print 'tip is', tip
115
116 # Calculate the branches
117 if verbose:
118     print 'analysing the branches...'
119 hgchildren["0"] = ()
120 hgparents["0"] = (None, None)
121 hgbranch["0"] = "master"
122 for cset in range(1, int(tip) + 1):
123     hgchildren[str(cset)] = ()
124     prnts = os.popen('hg log -r %d --template "{parents}"' % cset).read().strip().split(' ')
125     prnts = map(lambda x: x[:x.find(':')], prnts)
126     if prnts[0] != '':
127         parent = prnts[0].strip()
128     else:
129         parent = str(cset - 1)
130     hgchildren[parent] += ( str(cset), )
131     if len(prnts) > 1:
132         mparent = prnts[1].strip()
133         hgchildren[mparent] += ( str(cset), )
134     else:
135         mparent = None
136
137     hgparents[str(cset)] = (parent, mparent)
138
139     if mparent:
140         # For merge changesets, take either one, preferably the 'master' branch
141         if hgbranch[mparent] == 'master':
142             hgbranch[str(cset)] = 'master'
143         else:
144             hgbranch[str(cset)] = hgbranch[parent]
145     else:
146         # Normal changesets
147         # For first children, take the parent branch, for the others create a new branch
148         if hgchildren[parent][0] == str(cset):
149             hgbranch[str(cset)] = hgbranch[parent]
150         else:
151             hgbranch[str(cset)] = "branch-" + str(cset)
152
153 if not hgvers.has_key("0"):
154     print 'creating repository'
155     os.system('git init')
156
157 # loop through every hg changeset
158 for cset in range(int(tip) + 1):
159
160     # incremental, already seen
161     if hgvers.has_key(str(cset)):
162         continue
163     hgnewcsets += 1
164
165     # get info
166     log_data = os.popen('hg log -r %d --template "{tags}\n{date|date}\n{author}\n"' % cset).readlines()
167     tag = log_data[0].strip()
168     date = log_data[1].strip()
169     user = log_data[2].strip()
170     parent = hgparents[str(cset)][0]
171     mparent = hgparents[str(cset)][1]
172
173     #get comment
174     (fdcomment, filecomment) = tempfile.mkstemp()
175     csetcomment = os.popen('hg log -r %d --template "{desc}"' % cset).read().strip()
176     os.write(fdcomment, csetcomment)
177     os.close(fdcomment)
178
179     print '-----------------------------------------'
180     print 'cset:', cset
181     print 'branch:', hgbranch[str(cset)]
182     print 'user:', user
183     print 'date:', date
184     print 'comment:', csetcomment
185     if parent:
186         print 'parent:', parent
187     if mparent:
188         print 'mparent:', mparent
189     if tag:
190         print 'tag:', tag
191     print '-----------------------------------------'
192
193     # checkout the parent if necessary
194     if cset != 0:
195         if hgbranch[str(cset)] == "branch-" + str(cset):
196             print 'creating new branch', hgbranch[str(cset)]
197             os.system('git checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent]))
198         else:
199             print 'checking out branch', hgbranch[str(cset)]
200             os.system('git checkout %s' % hgbranch[str(cset)])
201
202     # merge
203     if mparent:
204         if hgbranch[parent] == hgbranch[str(cset)]:
205             otherbranch = hgbranch[mparent]
206         else:
207             otherbranch = hgbranch[parent]
208         print 'merging', otherbranch, 'into', hgbranch[str(cset)]
209         os.system(getgitenv(user, date) + 'git merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch))
210
211     # remove everything except .git and .hg directories
212     os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
213
214     # repopulate with checkouted files
215     os.system('hg update -C %d' % cset)
216
217     # add new files
218     os.system('git ls-files -x .hg --others | git update-index --add --stdin')
219     # delete removed files
220     os.system('git ls-files -x .hg --deleted | git update-index --remove --stdin')
221
222     # commit
223     os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment)
224     os.unlink(filecomment)
225
226     # tag
227     if tag and tag != 'tip':
228         os.system(getgitenv(user, date) + 'git tag %s' % tag)
229
230     # delete branch if not used anymore...
231     if mparent and len(hgchildren[str(cset)]):
232         print "Deleting unused branch:", otherbranch
233         os.system('git branch -d %s' % otherbranch)
234
235     # retrieve and record the version
236     vvv = os.popen('git show --quiet --pretty=format:%H').read()
237     print 'record', cset, '->', vvv
238     hgvers[str(cset)] = vvv
239
240 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
241     os.system('git repack -a -d')
242
243 # write the state for incrementals
244 if state:
245     if verbose:
246         print 'Writing state'
247     f = open(state, 'w')
248     pickle.dump(hgvers, f)
249
250 # vim: et ts=8 sw=4 sts=4