Merge branch 'jc/fix-clone-single-starting-at-a-tag' into next
[git] / contrib / hg-to-git / hg-to-git.py
1 #!/usr/bin/env 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, pickle, getopt
24 import re
25
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)
30
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
41
42 #------------------------------------------------------------------------------
43
44 def usage():
45
46         print """\
47 %s: [OPTIONS] <hgprj>
48
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
55
56 required:
57     hgprj:  name of the HG project to import (directory)
58 """ % sys.argv[0]
59
60 #------------------------------------------------------------------------------
61
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= ;'
75
76     env += 'export GIT_AUTHOR_DATE="%s" ;' % date
77     env += 'export GIT_COMMITTER_DATE="%s" ;' % date
78     return env
79
80 #------------------------------------------------------------------------------
81
82 state = ''
83 opt_nrepack = 0
84 verbose = False
85
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)
101
102 hgprj = args[0]
103 os.chdir(hgprj)
104
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'
113
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
120
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
141
142     hgparents[str(cset)] = (parent, mparent)
143
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)
157
158 if not hgvers.has_key("0"):
159     print 'creating repository'
160     os.system('git init')
161
162 # loop through every hg changeset
163 for cset in range(int(tip) + 1):
164
165     # incremental, already seen
166     if hgvers.has_key(str(cset)):
167         continue
168     hgnewcsets += 1
169
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]
177
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)
183
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 '-----------------------------------------'
197
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)])
206
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))
215
216     # remove everything except .git and .hg directories
217     os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
218
219     # repopulate with checkouted files
220     os.system('hg update -C %d' % cset)
221
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')
226
227     # commit
228     os.system(getgitenv(user, date) + 'git commit --allow-empty --allow-empty-message -a -F %s' % filecomment)
229     os.unlink(filecomment)
230
231     # tag
232     if tag and tag != 'tip':
233         os.system(getgitenv(user, date) + 'git tag %s' % tag)
234
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)
239
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
244
245 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
246     os.system('git repack -a -d')
247
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)
254
255 # vim: et ts=8 sw=4 sts=4