3 # pylint: disable=fixme,invalid-name
4 # pylint: disable=too-many-branches,too-many-locals,too-many-statements
6 """Repack git repos fully the way I like them."""
8 from __future__ import print_function
12 from pathlib import Path
17 from typing import Dict, List, Optional
20 def mount_settings() -> Dict[str, str]:
21 """Return dict mapping path to its type"""
23 with Path('/proc/mounts').open(encoding='utf-8') as fp:
30 def is_git_dir(path: Path) -> bool:
31 """Whether |path| is a .git dir"""
32 return ((path / 'refs').is_dir() and
33 (path / 'objects').is_dir() and
34 (path / 'config').is_file())
37 def find_git_dir(path: Path) -> Path:
38 """Try to find the .git dir to operate on"""
40 real_path = path = path.resolve()
43 if (path / '.git').is_dir():
44 curr_path = path / '.git'
46 if is_git_dir(curr_path):
51 raise ValueError('could not locate .git dir: %s (%s)' %
52 (orig_path, real_path))
56 def find_temp_dir() -> Optional[Path]:
57 """Find a good temp dir (one backed by tmpfs)"""
62 tempfile.gettempdir(),
64 mounts = mount_settings()
65 for path in SEARCH_PATHS:
66 if mounts.get(path) == 'tmpfs':
71 def readfile(path: Path) -> str:
72 """Read |path| and return its data"""
74 return path.read_text(encoding='utf-8')
78 def clean_hooks(path):
79 """Strip out sample files from hooks/"""
80 for hook in (path / 'hooks').glob('*.sample'):
81 print('Trimming hook:', hook)
85 def clean_packs(path):
86 """Strip out temp files from objects/packs/"""
87 for pack in (path / 'objects' / 'packs').glob('tmp_pack_*'):
88 print('Trimming pack:', pack)
93 """See if the git repo is already packed"""
94 obj_path = path / 'objects'
95 paths = {x.name for x in obj_path.iterdir()}
96 if paths not in ({'info', 'pack'}, {'pack'}):
98 packs = tuple((obj_path / 'pack').iterdir())
104 def repack(path: Path):
105 """Clean up and trim cruft and repack |path|"""
106 path = find_git_dir(path)
107 print('Repacking', path)
109 # Repack any submodules this project might use.
110 modules_path = path / 'modules'
111 if modules_path.is_dir():
112 for root, dirs, _ in os.walk(modules_path):
117 if is_git_dir(mod_path):
120 tmpdir = find_temp_dir()
122 tmpdir = Path(tempfile.mkdtemp(prefix='git-repack.', dir=tmpdir))
123 print('Using tempdir:', tmpdir)
125 # Doesn't matter for these needs.
126 os.environ['GIT_WORK_TREE'] = str(tmpdir)
130 # Push/pop the graft & alternate paths so we don't read them.
131 # XXX: In some cases, this is bad, but I don't use them that way ...
132 graft_file = path / 'info' / 'grafts'
133 grafts = readfile(graft_file)
134 graft_file.unlink(missing_ok=True)
136 alt_file = path / 'objects' / 'info' / 'alternates'
137 alts = readfile(alt_file)
138 alt_file.unlink(missing_ok=True)
142 # XXX: Should do this for all remotes?
143 origin_path = path / 'refs' / 'remotes' / 'origin'
144 # Delete remote HEAD as we don't need it, and it might be stale.
145 head = origin_path / 'HEAD'
146 head.unlink(missing_ok=True)
147 packed_refs = readfile(path / 'packed-refs')
148 if origin_path.exists() or 'refs/remotes/origin/' in packed_refs:
149 cmd = ['git', '--git-dir', str(path), 'remote', 'prune', 'origin']
150 subprocess.run(cmd, cwd='/', check=True)
155 print('Git repo is already packed; nothing to do')
159 print('Syncing git repo to tempdir')
160 shutil.copytree(path, tmpdir, symlinks=True)
165 cmd = ['git', '--git-dir', str(rundir), 'reflog', 'expire', '--all', '--stale-fix']
166 print('Cleaning reflog:', ' '.join(cmd))
167 subprocess.run(cmd, cwd='/', check=True)
169 # This also packs refs/tags for us.
170 cmd = ['git', '--git-dir', str(rundir), 'gc', '--aggressive', '--prune=all']
171 print('Repacking git repo:', ' '.join(cmd))
172 subprocess.run(cmd, cwd='/', check=True)
175 cmd = ['find', str(rundir), '-depth', '-type', 'd', '-exec', 'rmdir', '{}', '+']
176 subprocess.run(cmd, stderr=subprocess.DEVNULL, check=False)
178 # There's a few dirs we need to exist even if they're empty.
179 refdir = rundir / 'refs'
180 refdir.mkdir(exist_ok=True)
183 cmd = ['rsync', '-a', '--delete', str(tmpdir) + '/', str(path) + '/']
184 print('Syncing back git repo:', ' '.join(cmd))
185 subprocess.run(cmd, cwd='/', check=True)
186 cmd = ['find', str(path) + '/', '-exec', 'chmod', 'u+rw', '{}', '+']
187 subprocess.run(cmd, cwd='/', check=True)
191 graft_file.write_text(grafts, encoding='utf-8')
193 alt_file.write_text(alts, encoding='utf-8')
195 shutil.rmtree(tmpdir, ignore_errors=True)
199 """Get the command line parser"""
200 parser = argparse.ArgumentParser(description=__doc__)
201 parser.add_argument('dir', type=Path, help='The git repo to process')
205 def main(argv: List[str]):
206 """The main script entry point"""
207 parser = get_parser()
208 opts = parser.parse_args(argv)
212 if __name__ == '__main__':
213 sys.exit(main(sys.argv[1:]))