-
Notifications
You must be signed in to change notification settings - Fork 0
/
git-split-subtree.py
executable file
·107 lines (84 loc) · 4.77 KB
/
git-split-subtree.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#!/usr/bin/env python
import argparse
import errno
import os
import shutil
import stat
def delete_file_or_directory(target):
""" Deletes (with prejudice) a file or directory (recursively). Read-only files/directories are first set to writable, then deleted.
@type target string
@param target /path/to/dir or /path/to/file.ext
@rtype bool
@returns True if file/directory is deleted (or not there to begin with).
"""
def make_writeable_and_try_again(func, path, exc_info):
""" Error callback for shutil.rmtree.
Sets parent directory writable (if necessary) and the file itself writeable, then reexecutes the function that previously failed. """
if func in (os.rmdir, os.remove) and exc_info[1].errno == errno.EACCES:
# ensure parent directory is writeable too
pardir = os.path.abspath(os.path.join(path, os.path.pardir))
if not os.access(pardir, os.W_OK):
os.chmod(pardir, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
func(path)
else:
raise
if os.path.exists(target):
if os.path.isfile(target):
os.remove(target)
else:
try:
shutil.rmtree(target, ignore_errors=False, onerror=make_writeable_and_try_again)
except OSError:
return False
return True
def print_and_run(cmd):
print '>>>', cmd
os.system(cmd)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Split a subdirectory out of one git repo into the root of a new repo.')
parser.add_argument('-s', '--source-repo', type=str, required=True, help='The path to the source repository.')
parser.add_argument('-d', '--dest-repo', type=str, required=True, help='The path to the destination repository.')
parser.add_argument('--subdir', type=str, nargs='+', required=True, help='The directory name to create a subtree from, including previous names (renames), in reverse history order.')
parser.add_argument('--clean', default=False, action='store_true', help='Clean up merged branches (assumes git-flow master and develop permanent branches).')
args = parser.parse_args()
# prep subdir arguments for later
cat_slash_subdirs = '|'.join('{}/'.format(x) for x in args.subdir)
cat_backslash_subdirs = '|'.join('{}\\'.format(x) for x in args.subdir)
if not os.path.exists(args.source_repo):
print '>>> MISSING source repo directory'
exit(1)
if os.path.exists(args.dest_repo):
print '>>> Cleaning previous destination repo directory...'
delete_file_or_directory(args.dest_repo)
# make a copy of the source repo
print_and_run('cp -a {} {}'.format(args.source_repo, args.dest_repo))
# grab all branches
os.chdir(args.dest_repo)
print_and_run('git fetch')
print_and_run('for i in $(git branch -r | sed "s/.*origin\///"); do git branch -t $i origin/$i; done')
print_and_run('git remote rm origin')
# remove everything but the subdirs you want to keep and move them to the repo root
print_and_run('git filter-branch -f --tag-name-filter cat --prune-empty --index-filter \' \
git ls-files -z | egrep -zv "^({})" | xargs -0 -r git rm --cached -q \n\
git ls-files -s | sed -e "s-\\t\\({})/-\\t-" | sort | uniq | GIT_INDEX_FILE=$GIT_INDEX_FILE.new git update-index --index-info \n\
if [ -f "$GIT_INDEX_FILE.new" ]; then mv $GIT_INDEX_FILE.new $GIT_INDEX_FILE; fi \n\
\' -- --all'.format(cat_slash_subdirs, cat_backslash_subdirs))
# remove all "empty" merge commits (using ruby because that's how I found it...)
with open('/tmp/subtree.rb', 'w') as f:
f.write('#!/usr/bin/env ruby\n')
f.write('old_parents = gets.chomp.gsub("-p ", " ")\n')
f.write('new_parents = old_parents.empty? ? [] : `git show-branch --independent #{old_parents}`.split\n')
f.write('puts new_parents.map{|p| "-p " + p}.join(" ")\n')
os.chmod('/tmp/subtree.rb', stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
print_and_run('git filter-branch -f --tag-name-filter cat --prune-empty --parent-filter /tmp/subtree.rb -- --all')
delete_file_or_directory('/tmp/subtree.rb')
# clean up left overs
print_and_run('git reset --hard')
print_and_run('git for-each-ref --format="%(refname)" refs/original/ | xargs -n 1 git update-ref -d')
print_and_run('git reflog expire --expire=now --all')
print_and_run('git gc --aggressive --prune=now')
# clean up merged branches
if (args.clean):
print_and_run('git checkout develop')
print_and_run('git branch --merged | grep -v "\*" | grep -v master | grep -v develop | xargs -n 1 git branch -d')