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 22import os, os.path, sys 23import tempfile, popen2, pickle, getopt 24import re 25 26# Maps hg version -> git version 27hgvers = {} 28# List of children for each hg revision 29hgchildren = {} 30# List of parents for each hg revision 31hgparents = {} 32# Current branch for each hg revision 33hgbranch = {} 34# Number of new changesets converted from hg 35hgnewcsets =0 36 37#------------------------------------------------------------------------------ 38 39defusage(): 40 41print"""\ 42%s: [OPTIONS] <hgprj> 43 44options: 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 50required: 51 hgprj: name of the HG project to import (directory) 52"""% sys.argv[0] 53 54#------------------------------------------------------------------------------ 55 56defgetgitenv(user, date): 57 env ='' 58 elems = re.compile('(.*?)\s+<(.*)>').match(user) 59if 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) 64else: 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= ;' 69 70 env +='export GIT_AUTHOR_DATE="%s" ;'% date 71 env +='export GIT_COMMITTER_DATE="%s" ;'% date 72return env 73 74#------------------------------------------------------------------------------ 75 76state ='' 77opt_nrepack =0 78 79try: 80 opts, args = getopt.getopt(sys.argv[1:],'s:t:n:', ['gitstate=','tempdir=','nrepack=']) 81for o, a in opts: 82if o in('-s','--gitstate'): 83 state = a 84 state = os.path.abspath(state) 85if o in('-n','--nrepack'): 86 opt_nrepack =int(a) 87iflen(args) !=1: 88raise('params') 89except: 90usage() 91 sys.exit(1) 92 93hgprj = args[0] 94os.chdir(hgprj) 95 96if state: 97if os.path.exists(state): 98print'State does exist, reading' 99 f =open(state,'r') 100 hgvers = pickle.load(f) 101else: 102print'State does not exist, first run' 103 104tip = os.popen('hg tip --template "{rev}"').read() 105print'tip is', tip 106 107# Calculate the branches 108print'analysing the branches...' 109hgchildren["0"] = () 110hgparents["0"] = (None,None) 111hgbranch["0"] ="master" 112for cset inrange(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) 116if prnts[0] !='': 117 parent = prnts[0].strip() 118else: 119 parent =str(cset -1) 120 hgchildren[parent] += (str(cset), ) 121iflen(prnts) >1: 122 mparent = prnts[1].strip() 123 hgchildren[mparent] += (str(cset), ) 124else: 125 mparent =None 126 127 hgparents[str(cset)] = (parent, mparent) 128 129if mparent: 130# For merge changesets, take either one, preferably the 'master' branch 131if hgbranch[mparent] =='master': 132 hgbranch[str(cset)] ='master' 133else: 134 hgbranch[str(cset)] = hgbranch[parent] 135else: 136# Normal changesets 137# For first children, take the parent branch, for the others create a new branch 138if hgchildren[parent][0] ==str(cset): 139 hgbranch[str(cset)] = hgbranch[parent] 140else: 141 hgbranch[str(cset)] ="branch-"+str(cset) 142 143if not hgvers.has_key("0"): 144print'creating repository' 145 os.system('git-init-db') 146 147# loop through every hg changeset 148for cset inrange(int(tip) +1): 149 150# incremental, already seen 151if hgvers.has_key(str(cset)): 152continue 153 hgnewcsets +=1 154 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] 162 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) 168 169print'-----------------------------------------' 170print'cset:', cset 171print'branch:', hgbranch[str(cset)] 172print'user:', user 173print'date:', date 174print'comment:', csetcomment 175if parent: 176print'parent:', parent 177if mparent: 178print'mparent:', mparent 179if tag: 180print'tag:', tag 181print'-----------------------------------------' 182 183# checkout the parent if necessary 184if cset !=0: 185if hgbranch[str(cset)] =="branch-"+str(cset): 186print'creating new branch', hgbranch[str(cset)] 187 os.system('git-checkout -b%s %s'% (hgbranch[str(cset)], hgvers[parent])) 188else: 189print'checking out branch', hgbranch[str(cset)] 190 os.system('git-checkout%s'% hgbranch[str(cset)]) 191 192# merge 193if mparent: 194if hgbranch[parent] == hgbranch[str(cset)]: 195 otherbranch = hgbranch[mparent] 196else: 197 otherbranch = hgbranch[parent] 198print'merging', otherbranch,'into', hgbranch[str(cset)] 199 os.system(getgitenv(user, date) +'git-merge --no-commit -s ours ""%s %s'% (hgbranch[str(cset)], otherbranch)) 200 201# remove everything except .git and .hg directories 202 os.system('find . \( -path "./.hg" -o -path "./.git" \) -prune -o ! -name "." -print | xargs rm -rf') 203 204# repopulate with checkouted files 205 os.system('hg update -C%d'% cset) 206 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') 211 212# commit 213 os.system(getgitenv(user, date) +'git commit --allow-empty -a -F%s'% filecomment) 214 os.unlink(filecomment) 215 216# tag 217if tag and tag !='tip': 218 os.system(getgitenv(user, date) +'git-tag%s'% tag) 219 220# delete branch if not used anymore... 221if mparent andlen(hgchildren[str(cset)]): 222print"Deleting unused branch:", otherbranch 223 os.system('git-branch -d%s'% otherbranch) 224 225# retrieve and record the version 226 vvv = os.popen('git-show --quiet --pretty=format:%H').read() 227print'record', cset,'->', vvv 228 hgvers[str(cset)] = vvv 229 230if hgnewcsets >= opt_nrepack and opt_nrepack != -1: 231 os.system('git-repack -a -d') 232 233# write the state for incrementals 234if state: 235print'Writing state' 236 f =open(state,'w') 237 pickle.dump(hgvers, f) 238 239# vim: et ts=8 sw=4 sts=4