Merge branch 'maint'
[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('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 tip = os.popen('hg tip --template "{rev}"').read()
110 if verbose:
111     print 'tip is', tip
112
113 # Calculate the branches
114 if verbose:
115     print 'analysing the branches...'
116 hgchildren["0"] = ()
117 hgparents["0"] = (None, None)
118 hgbranch["0"] = "master"
119 for cset in range(1, int(tip) + 1):
120     hgchildren[str(cset)] = ()
121     prnts = os.popen('hg log -r %d --template "{parents}"' % cset).read().strip().split(' ')
122     prnts = map(lambda x: x[:x.find(':')], prnts)
123     if prnts[0] != '':
124         parent = prnts[0].strip()
125     else:
126         parent = str(cset - 1)
127     hgchildren[parent] += ( str(cset), )
128     if len(prnts) > 1:
129         mparent = prnts[1].strip()
130         hgchildren[mparent] += ( str(cset), )
131     else:
132         mparent = None
133
134     hgparents[str(cset)] = (parent, mparent)
135
136     if mparent:
137         # For merge changesets, take either one, preferably the 'master' branch
138         if hgbranch[mparent] == 'master':
139             hgbranch[str(cset)] = 'master'
140         else:
141             hgbranch[str(cset)] = hgbranch[parent]
142     else:
143         # Normal changesets
144         # For first children, take the parent branch, for the others create a new branch
145         if hgchildren[parent][0] == str(cset):
146             hgbranch[str(cset)] = hgbranch[parent]
147         else:
148             hgbranch[str(cset)] = "branch-" + str(cset)
149
150 if not hgvers.has_key("0"):
151     print 'creating repository'
152     os.system('git-init-db')
153
154 # loop through every hg changeset
155 for cset in range(int(tip) + 1):
156
157     # incremental, already seen
158     if hgvers.has_key(str(cset)):
159         continue
160     hgnewcsets += 1
161
162     # get info
163     log_data = os.popen('hg log -r %d --template "{tags}\n{date|date}\n{author}\n"' % cset).readlines()
164     tag = log_data[0].strip()
165     date = log_data[1].strip()
166     user = log_data[2].strip()
167     parent = hgparents[str(cset)][0]
168     mparent = hgparents[str(cset)][1]
169
170     #get comment
171     (fdcomment, filecomment) = tempfile.mkstemp()
172     csetcomment = os.popen('hg log -r %d --template "{desc}"' % cset).read().strip()
173     os.write(fdcomment, csetcomment)
174     os.close(fdcomment)
175
176     print '-----------------------------------------'
177     print 'cset:', cset
178     print 'branch:', hgbranch[str(cset)]
179     print 'user:', user
180     print 'date:', date
181     print 'comment:', csetcomment
182     if parent:
183         print 'parent:', parent
184     if mparent:
185         print 'mparent:', mparent
186     if tag:
187         print 'tag:', tag
188     print '-----------------------------------------'
189
190     # checkout the parent if necessary
191     if cset != 0:
192         if hgbranch[str(cset)] == "branch-" + str(cset):
193             print 'creating new branch', hgbranch[str(cset)]
194             os.system('git-checkout -b %s %s' % (hgbranch[str(cset)], hgvers[parent]))
195         else:
196             print 'checking out branch', hgbranch[str(cset)]
197             os.system('git-checkout %s' % hgbranch[str(cset)])
198
199     # merge
200     if mparent:
201         if hgbranch[parent] == hgbranch[str(cset)]:
202             otherbranch = hgbranch[mparent]
203         else:
204             otherbranch = hgbranch[parent]
205         print 'merging', otherbranch, 'into', hgbranch[str(cset)]
206         os.system(getgitenv(user, date) + 'git-merge --no-commit -s ours "" %s %s' % (hgbranch[str(cset)], otherbranch))
207
208     # remove everything except .git and .hg directories
209     os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf')
210
211     # repopulate with checkouted files
212     os.system('hg update -C %d' % cset)
213
214     # add new files
215     os.system('git-ls-files -x .hg --others | git-update-index --add --stdin')
216     # delete removed files
217     os.system('git-ls-files -x .hg --deleted | git-update-index --remove --stdin')
218
219     # commit
220     os.system(getgitenv(user, date) + 'git commit --allow-empty -a -F %s' % filecomment)
221     os.unlink(filecomment)
222
223     # tag
224     if tag and tag != 'tip':
225         os.system(getgitenv(user, date) + 'git-tag %s' % tag)
226
227     # delete branch if not used anymore...
228     if mparent and len(hgchildren[str(cset)]):
229         print "Deleting unused branch:", otherbranch
230         os.system('git-branch -d %s' % otherbranch)
231
232     # retrieve and record the version
233     vvv = os.popen('git-show --quiet --pretty=format:%H').read()
234     print 'record', cset, '->', vvv
235     hgvers[str(cset)] = vvv
236
237 if hgnewcsets >= opt_nrepack and opt_nrepack != -1:
238     os.system('git-repack -a -d')
239
240 # write the state for incrementals
241 if state:
242     if verbose:
243         print 'Writing state'
244     f = open(state, 'w')
245     pickle.dump(hgvers, f)
246
247 # vim: et ts=8 sw=4 sts=4