]> git.wh0rd.org - home.git/blob - .bin/git-repack
eb0061b1a7d181090e72cd558f2185490440a31b
[home.git] / .bin / git-repack
1 #!/usr/bin/python
2
3 """Repack git repos fully the way I like them."""
4
5 from __future__ import print_function
6
7 import argparse
8 import glob
9 import os
10 import shutil
11 import subprocess
12 import sys
13 import tempfile
14
15
16 def mount_settings():
17 """Return dict mapping path to its type"""
18 ret = {}
19 with open('/proc/mounts') as fp:
20 for line in fp:
21 ele = line.split()
22 ret[ele[1]] = ele[2]
23 return ret
24
25
26 def is_git_dir(path):
27 """Whether |path| is a .git dir"""
28 return (os.path.isdir(os.path.join(path, 'refs')) and
29 os.path.isdir(os.path.join(path, 'objects')) and
30 os.path.isfile(os.path.join(path, 'config')))
31
32
33 def find_git_dir(path):
34 """Try to find the .git dir to operate on"""
35 orig_path = path
36 real_path = path = os.path.realpath(path)
37 while True:
38 curr_path = path
39 if os.path.isdir(os.path.join(path, '.git')):
40 curr_path = os.path.join(path, '.git')
41
42 if is_git_dir(curr_path):
43 return curr_path
44
45 path = os.path.dirname(path)
46
47 if path == '/':
48 raise ValueError('could not locate .git dir: %s (%s)' %
49 (orig_path, real_path))
50
51
52 def find_temp_dir():
53 """Find a good temp dir (one backed by tmpfs)"""
54 SEARCH_PATHS = (
55 '/var/tmp/portage',
56 '/var/tmp',
57 '/tmp',
58 tempfile.gettempdir(),
59 )
60 mounts = mount_settings()
61 for path in SEARCH_PATHS:
62 if mounts.get(path) == 'tmpfs':
63 return path
64 return None
65
66
67 def readfile(path):
68 """Read |path| and return its data"""
69 if os.path.isfile(path):
70 return open(path).read()
71 return ''
72
73
74 def unlink(path):
75 """Unlink |path| if it exists else do nothing"""
76 if os.path.isfile(path):
77 os.unlink(path)
78
79
80 def clean_hooks(path):
81 """Strip out sample files from hooks/"""
82 hooks_path = os.path.join(path, 'hooks')
83 for hook in glob.glob(os.path.join(hooks_path, '*.sample')):
84 print('Trimming hook: %s' % hook)
85 os.unlink(hook)
86
87
88 def clean_packs(path):
89 """Strip out temp files from objects/packs/"""
90 packs_path = os.path.join(path, 'objects', 'packs')
91 for pack in glob.glob(os.path.join(packs_path, 'tmp_pack_*')):
92 print('Trimming pack: %s' % pack)
93 os.unlink(pack)
94
95
96 def is_packed(path):
97 """See if the git repo is already packed"""
98 obj_path = os.path.join(path, 'objects')
99 paths = set(os.listdir(obj_path))
100 if {'info', 'pack'} != paths and {'pack'} != paths:
101 return False
102 packs = os.listdir(os.path.join(obj_path, 'pack'))
103 if len(packs) != 2:
104 return False
105 return True
106
107
108 def repack(path):
109 """Clean up and trim cruft and repack |path|"""
110 path = find_git_dir(path)
111 print('Repacking %s' % path)
112
113 # Repack any submodules this project might use.
114 modules_path = os.path.join(path, 'modules')
115 if os.path.isdir(modules_path):
116 for root, dirs, _ in os.walk(modules_path):
117 dirs.sort()
118 for d in dirs:
119 mod_path = os.path.join(root, d)
120 if is_git_dir(mod_path):
121 repack(mod_path)
122
123 tmpdir = find_temp_dir()
124 if tmpdir:
125 tmpdir = tempfile.mkdtemp(prefix='git-repack.', dir=tmpdir)
126 print('Using tempdir: %s' % tmpdir)
127 os.rmdir(tmpdir)
128 # Doesn't matter for these needs.
129 os.environ['GIT_WORK_TREE'] = tmpdir
130
131 grafts = alts = None
132 try:
133 # Push/pop the graft & alternate paths so we don't read them.
134 # XXX: In some cases, this is bad, but I don't use them that way ...
135 graft_file = os.path.join(path, 'info', 'grafts')
136 grafts = readfile(graft_file)
137 unlink(graft_file)
138
139 alt_file = os.path.join(path, 'objects', 'info', 'alternates')
140 alts = readfile(alt_file)
141 unlink(alt_file)
142
143 clean_hooks(path)
144
145 # XXX: Should do this for all remotes?
146 origin_path = os.path.join(path, 'refs', 'remotes', 'origin')
147 packed_refs = readfile(os.path.join(path, 'packed-refs'))
148 if os.path.exists(origin_path) or 'refs/remotes/origin/' in packed_refs:
149 cmd = ['git', '--git-dir', path, 'remote', 'prune', 'origin']
150 subprocess.check_call(cmd, cwd='/')
151
152 clean_packs(path)
153
154 if is_packed(path):
155 print('Git repo is already packed; nothing to do')
156 return
157
158 if tmpdir:
159 print('Syncing git repo to tempdir')
160 shutil.copytree(path, tmpdir, symlinks=True)
161 rundir = tmpdir
162 else:
163 rundir = path
164
165 cmd = ['git', '--git-dir', rundir, 'reflog', 'expire', '--all', '--stale-fix']
166 print('Cleaning reflog: %s' % ' '.join(cmd))
167 subprocess.check_call(cmd, cwd='/')
168
169 # This also packs refs/tags for us.
170 cmd = ['git', '--git-dir', rundir, 'gc', '--aggressive', '--prune=all']
171 print('Repacking git repo: %s' % ' '.join(cmd))
172 subprocess.check_call(cmd, cwd='/')
173
174 # Clean empty dirs.
175 cmd = ['find', rundir, '-depth', '-type', 'd', '-exec', 'rmdir', '{}', '+']
176 subprocess.call(cmd, stderr=open('/dev/null', 'w'))
177
178 # There's a few dirs we need to exist even if they're empty.
179 refdir = os.path.join(rundir, 'refs')
180 if not os.path.isdir(refdir):
181 os.mkdir(refdir)
182
183 if tmpdir:
184 cmd = ['rsync', '-a', '--delete', tmpdir + '/', path + '/']
185 print('Syncing back git repo: %s' % ' '.join(cmd))
186 subprocess.check_call(cmd, cwd='/')
187 cmd = ['find', path + '/', '-exec', 'chmod', 'u+rw', '{}', '+']
188 subprocess.check_call(cmd, cwd='/')
189
190 finally:
191 if grafts:
192 open(graft_file, 'w').write(grafts)
193 if alts:
194 open(alt_file, 'w').write(alts)
195 if tmpdir and os.path.exists(tmpdir):
196 shutil.rmtree(tmpdir)
197
198
199 def get_parser():
200 """Get the command line parser"""
201 parser = argparse.ArgumentParser(description=__doc__)
202 parser.add_argument('dir', help='The git repo to process')
203 return parser
204
205
206 def main(argv):
207 """The main script entry point"""
208 parser = get_parser()
209 opts = parser.parse_args(argv)
210 repack(opts.dir)
211
212
213 if __name__ == '__main__':
214 exit(main(sys.argv[1:]))