123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- #!/usr/bin/env python3
- #
- # Copyright BitBake Contributors
- #
- # SPDX-License-Identifier: GPL-2.0-only
- #
- """git-make-shallow: make the current git repository shallow
- Remove the history of the specified revisions, then optionally filter the
- available refs to those specified.
- """
- import argparse
- import collections
- import errno
- import itertools
- import os
- import subprocess
- import sys
- import warnings
- warnings.simplefilter("default")
- version = 1.0
- def main():
- if sys.version_info < (3, 4, 0):
- sys.exit('Python 3.4 or greater is required')
- git_dir = check_output(['git', 'rev-parse', '--git-dir']).rstrip()
- shallow_file = os.path.join(git_dir, 'shallow')
- if os.path.exists(shallow_file):
- try:
- check_output(['git', 'fetch', '--unshallow'])
- except subprocess.CalledProcessError:
- try:
- os.unlink(shallow_file)
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- raise
- args = process_args()
- revs = check_output(['git', 'rev-list'] + args.revisions).splitlines()
- make_shallow(shallow_file, args.revisions, args.refs)
- ref_revs = check_output(['git', 'rev-list'] + args.refs).splitlines()
- remaining_history = set(revs) & set(ref_revs)
- for rev in remaining_history:
- if check_output(['git', 'rev-parse', '{}^@'.format(rev)]):
- sys.exit('Error: %s was not made shallow' % rev)
- filter_refs(args.refs)
- if args.shrink:
- shrink_repo(git_dir)
- subprocess.check_call(['git', 'fsck', '--unreachable'])
- def process_args():
- # TODO: add argument to automatically keep local-only refs, since they
- # can't be easily restored with a git fetch.
- parser = argparse.ArgumentParser(description='Remove the history of the specified revisions, then optionally filter the available refs to those specified.')
- parser.add_argument('--ref', '-r', metavar='REF', action='append', dest='refs', help='remove all but the specified refs (cumulative)')
- parser.add_argument('--shrink', '-s', action='store_true', help='shrink the git repository by repacking and pruning')
- parser.add_argument('revisions', metavar='REVISION', nargs='+', help='a git revision/commit')
- if len(sys.argv) < 2:
- parser.print_help()
- sys.exit(2)
- args = parser.parse_args()
- if args.refs:
- args.refs = check_output(['git', 'rev-parse', '--symbolic-full-name'] + args.refs).splitlines()
- else:
- args.refs = get_all_refs(lambda r, t, tt: t == 'commit' or tt == 'commit')
- args.refs = list(filter(lambda r: not r.endswith('/HEAD'), args.refs))
- args.revisions = check_output(['git', 'rev-parse'] + ['%s^{}' % i for i in args.revisions]).splitlines()
- return args
- def check_output(cmd, input=None):
- return subprocess.check_output(cmd, universal_newlines=True, input=input)
- def make_shallow(shallow_file, revisions, refs):
- """Remove the history of the specified revisions."""
- for rev in follow_history_intersections(revisions, refs):
- print("Processing %s" % rev)
- with open(shallow_file, 'a') as f:
- f.write(rev + '\n')
- def get_all_refs(ref_filter=None):
- """Return all the existing refs in this repository, optionally filtering the refs."""
- ref_output = check_output(['git', 'for-each-ref', '--format=%(refname)\t%(objecttype)\t%(*objecttype)'])
- ref_split = [tuple(iter_extend(l.rsplit('\t'), 3)) for l in ref_output.splitlines()]
- if ref_filter:
- ref_split = (e for e in ref_split if ref_filter(*e))
- refs = [r[0] for r in ref_split]
- return refs
- def iter_extend(iterable, length, obj=None):
- """Ensure that iterable is the specified length by extending with obj."""
- return itertools.islice(itertools.chain(iterable, itertools.repeat(obj)), length)
- def filter_refs(refs):
- """Remove all but the specified refs from the git repository."""
- all_refs = get_all_refs()
- to_remove = set(all_refs) - set(refs)
- if to_remove:
- check_output(['xargs', '-0', '-n', '1', 'git', 'update-ref', '-d', '--no-deref'],
- input=''.join(l + '\0' for l in to_remove))
- def follow_history_intersections(revisions, refs):
- """Determine all the points where the history of the specified revisions intersects the specified refs."""
- queue = collections.deque(revisions)
- seen = set()
- for rev in iter_except(queue.popleft, IndexError):
- if rev in seen:
- continue
- parents = check_output(['git', 'rev-parse', '%s^@' % rev]).splitlines()
- yield rev
- seen.add(rev)
- if not parents:
- continue
- check_refs = check_output(['git', 'merge-base', '--independent'] + sorted(refs)).splitlines()
- for parent in parents:
- for ref in check_refs:
- print("Checking %s vs %s" % (parent, ref))
- try:
- merge_base = check_output(['git', 'merge-base', parent, ref]).rstrip()
- except subprocess.CalledProcessError:
- continue
- else:
- queue.append(merge_base)
- def iter_except(func, exception, start=None):
- """Yield a function repeatedly until it raises an exception."""
- try:
- if start is not None:
- yield start()
- while True:
- yield func()
- except exception:
- pass
- def shrink_repo(git_dir):
- """Shrink the newly shallow repository, removing the unreachable objects."""
- subprocess.check_call(['git', 'reflog', 'expire', '--expire-unreachable=now', '--all'])
- subprocess.check_call(['git', 'repack', '-ad'])
- try:
- os.unlink(os.path.join(git_dir, 'objects', 'info', 'alternates'))
- except OSError as exc:
- if exc.errno != errno.ENOENT:
- raise
- subprocess.check_call(['git', 'prune', '--expire', 'now'])
- if __name__ == '__main__':
- main()
|