1*4882a593Smuzhiyun#!/usr/bin/env python3 2*4882a593Smuzhiyun# 3*4882a593Smuzhiyun# Copyright BitBake Contributors 4*4882a593Smuzhiyun# 5*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 6*4882a593Smuzhiyun# 7*4882a593Smuzhiyun 8*4882a593Smuzhiyun"""git-make-shallow: make the current git repository shallow 9*4882a593Smuzhiyun 10*4882a593SmuzhiyunRemove the history of the specified revisions, then optionally filter the 11*4882a593Smuzhiyunavailable refs to those specified. 12*4882a593Smuzhiyun""" 13*4882a593Smuzhiyun 14*4882a593Smuzhiyunimport argparse 15*4882a593Smuzhiyunimport collections 16*4882a593Smuzhiyunimport errno 17*4882a593Smuzhiyunimport itertools 18*4882a593Smuzhiyunimport os 19*4882a593Smuzhiyunimport subprocess 20*4882a593Smuzhiyunimport sys 21*4882a593Smuzhiyunimport warnings 22*4882a593Smuzhiyunwarnings.simplefilter("default") 23*4882a593Smuzhiyun 24*4882a593Smuzhiyunversion = 1.0 25*4882a593Smuzhiyun 26*4882a593Smuzhiyun 27*4882a593Smuzhiyundef main(): 28*4882a593Smuzhiyun if sys.version_info < (3, 4, 0): 29*4882a593Smuzhiyun sys.exit('Python 3.4 or greater is required') 30*4882a593Smuzhiyun 31*4882a593Smuzhiyun git_dir = check_output(['git', 'rev-parse', '--git-dir']).rstrip() 32*4882a593Smuzhiyun shallow_file = os.path.join(git_dir, 'shallow') 33*4882a593Smuzhiyun if os.path.exists(shallow_file): 34*4882a593Smuzhiyun try: 35*4882a593Smuzhiyun check_output(['git', 'fetch', '--unshallow']) 36*4882a593Smuzhiyun except subprocess.CalledProcessError: 37*4882a593Smuzhiyun try: 38*4882a593Smuzhiyun os.unlink(shallow_file) 39*4882a593Smuzhiyun except OSError as exc: 40*4882a593Smuzhiyun if exc.errno != errno.ENOENT: 41*4882a593Smuzhiyun raise 42*4882a593Smuzhiyun 43*4882a593Smuzhiyun args = process_args() 44*4882a593Smuzhiyun revs = check_output(['git', 'rev-list'] + args.revisions).splitlines() 45*4882a593Smuzhiyun 46*4882a593Smuzhiyun make_shallow(shallow_file, args.revisions, args.refs) 47*4882a593Smuzhiyun 48*4882a593Smuzhiyun ref_revs = check_output(['git', 'rev-list'] + args.refs).splitlines() 49*4882a593Smuzhiyun remaining_history = set(revs) & set(ref_revs) 50*4882a593Smuzhiyun for rev in remaining_history: 51*4882a593Smuzhiyun if check_output(['git', 'rev-parse', '{}^@'.format(rev)]): 52*4882a593Smuzhiyun sys.exit('Error: %s was not made shallow' % rev) 53*4882a593Smuzhiyun 54*4882a593Smuzhiyun filter_refs(args.refs) 55*4882a593Smuzhiyun 56*4882a593Smuzhiyun if args.shrink: 57*4882a593Smuzhiyun shrink_repo(git_dir) 58*4882a593Smuzhiyun subprocess.check_call(['git', 'fsck', '--unreachable']) 59*4882a593Smuzhiyun 60*4882a593Smuzhiyun 61*4882a593Smuzhiyundef process_args(): 62*4882a593Smuzhiyun # TODO: add argument to automatically keep local-only refs, since they 63*4882a593Smuzhiyun # can't be easily restored with a git fetch. 64*4882a593Smuzhiyun parser = argparse.ArgumentParser(description='Remove the history of the specified revisions, then optionally filter the available refs to those specified.') 65*4882a593Smuzhiyun parser.add_argument('--ref', '-r', metavar='REF', action='append', dest='refs', help='remove all but the specified refs (cumulative)') 66*4882a593Smuzhiyun parser.add_argument('--shrink', '-s', action='store_true', help='shrink the git repository by repacking and pruning') 67*4882a593Smuzhiyun parser.add_argument('revisions', metavar='REVISION', nargs='+', help='a git revision/commit') 68*4882a593Smuzhiyun if len(sys.argv) < 2: 69*4882a593Smuzhiyun parser.print_help() 70*4882a593Smuzhiyun sys.exit(2) 71*4882a593Smuzhiyun 72*4882a593Smuzhiyun args = parser.parse_args() 73*4882a593Smuzhiyun 74*4882a593Smuzhiyun if args.refs: 75*4882a593Smuzhiyun args.refs = check_output(['git', 'rev-parse', '--symbolic-full-name'] + args.refs).splitlines() 76*4882a593Smuzhiyun else: 77*4882a593Smuzhiyun args.refs = get_all_refs(lambda r, t, tt: t == 'commit' or tt == 'commit') 78*4882a593Smuzhiyun 79*4882a593Smuzhiyun args.refs = list(filter(lambda r: not r.endswith('/HEAD'), args.refs)) 80*4882a593Smuzhiyun args.revisions = check_output(['git', 'rev-parse'] + ['%s^{}' % i for i in args.revisions]).splitlines() 81*4882a593Smuzhiyun return args 82*4882a593Smuzhiyun 83*4882a593Smuzhiyun 84*4882a593Smuzhiyundef check_output(cmd, input=None): 85*4882a593Smuzhiyun return subprocess.check_output(cmd, universal_newlines=True, input=input) 86*4882a593Smuzhiyun 87*4882a593Smuzhiyun 88*4882a593Smuzhiyundef make_shallow(shallow_file, revisions, refs): 89*4882a593Smuzhiyun """Remove the history of the specified revisions.""" 90*4882a593Smuzhiyun for rev in follow_history_intersections(revisions, refs): 91*4882a593Smuzhiyun print("Processing %s" % rev) 92*4882a593Smuzhiyun with open(shallow_file, 'a') as f: 93*4882a593Smuzhiyun f.write(rev + '\n') 94*4882a593Smuzhiyun 95*4882a593Smuzhiyun 96*4882a593Smuzhiyundef get_all_refs(ref_filter=None): 97*4882a593Smuzhiyun """Return all the existing refs in this repository, optionally filtering the refs.""" 98*4882a593Smuzhiyun ref_output = check_output(['git', 'for-each-ref', '--format=%(refname)\t%(objecttype)\t%(*objecttype)']) 99*4882a593Smuzhiyun ref_split = [tuple(iter_extend(l.rsplit('\t'), 3)) for l in ref_output.splitlines()] 100*4882a593Smuzhiyun if ref_filter: 101*4882a593Smuzhiyun ref_split = (e for e in ref_split if ref_filter(*e)) 102*4882a593Smuzhiyun refs = [r[0] for r in ref_split] 103*4882a593Smuzhiyun return refs 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun 106*4882a593Smuzhiyundef iter_extend(iterable, length, obj=None): 107*4882a593Smuzhiyun """Ensure that iterable is the specified length by extending with obj.""" 108*4882a593Smuzhiyun return itertools.islice(itertools.chain(iterable, itertools.repeat(obj)), length) 109*4882a593Smuzhiyun 110*4882a593Smuzhiyun 111*4882a593Smuzhiyundef filter_refs(refs): 112*4882a593Smuzhiyun """Remove all but the specified refs from the git repository.""" 113*4882a593Smuzhiyun all_refs = get_all_refs() 114*4882a593Smuzhiyun to_remove = set(all_refs) - set(refs) 115*4882a593Smuzhiyun if to_remove: 116*4882a593Smuzhiyun check_output(['xargs', '-0', '-n', '1', 'git', 'update-ref', '-d', '--no-deref'], 117*4882a593Smuzhiyun input=''.join(l + '\0' for l in to_remove)) 118*4882a593Smuzhiyun 119*4882a593Smuzhiyun 120*4882a593Smuzhiyundef follow_history_intersections(revisions, refs): 121*4882a593Smuzhiyun """Determine all the points where the history of the specified revisions intersects the specified refs.""" 122*4882a593Smuzhiyun queue = collections.deque(revisions) 123*4882a593Smuzhiyun seen = set() 124*4882a593Smuzhiyun 125*4882a593Smuzhiyun for rev in iter_except(queue.popleft, IndexError): 126*4882a593Smuzhiyun if rev in seen: 127*4882a593Smuzhiyun continue 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun parents = check_output(['git', 'rev-parse', '%s^@' % rev]).splitlines() 130*4882a593Smuzhiyun 131*4882a593Smuzhiyun yield rev 132*4882a593Smuzhiyun seen.add(rev) 133*4882a593Smuzhiyun 134*4882a593Smuzhiyun if not parents: 135*4882a593Smuzhiyun continue 136*4882a593Smuzhiyun 137*4882a593Smuzhiyun check_refs = check_output(['git', 'merge-base', '--independent'] + sorted(refs)).splitlines() 138*4882a593Smuzhiyun for parent in parents: 139*4882a593Smuzhiyun for ref in check_refs: 140*4882a593Smuzhiyun print("Checking %s vs %s" % (parent, ref)) 141*4882a593Smuzhiyun try: 142*4882a593Smuzhiyun merge_base = check_output(['git', 'merge-base', parent, ref]).rstrip() 143*4882a593Smuzhiyun except subprocess.CalledProcessError: 144*4882a593Smuzhiyun continue 145*4882a593Smuzhiyun else: 146*4882a593Smuzhiyun queue.append(merge_base) 147*4882a593Smuzhiyun 148*4882a593Smuzhiyun 149*4882a593Smuzhiyundef iter_except(func, exception, start=None): 150*4882a593Smuzhiyun """Yield a function repeatedly until it raises an exception.""" 151*4882a593Smuzhiyun try: 152*4882a593Smuzhiyun if start is not None: 153*4882a593Smuzhiyun yield start() 154*4882a593Smuzhiyun while True: 155*4882a593Smuzhiyun yield func() 156*4882a593Smuzhiyun except exception: 157*4882a593Smuzhiyun pass 158*4882a593Smuzhiyun 159*4882a593Smuzhiyun 160*4882a593Smuzhiyundef shrink_repo(git_dir): 161*4882a593Smuzhiyun """Shrink the newly shallow repository, removing the unreachable objects.""" 162*4882a593Smuzhiyun subprocess.check_call(['git', 'reflog', 'expire', '--expire-unreachable=now', '--all']) 163*4882a593Smuzhiyun subprocess.check_call(['git', 'repack', '-ad']) 164*4882a593Smuzhiyun try: 165*4882a593Smuzhiyun os.unlink(os.path.join(git_dir, 'objects', 'info', 'alternates')) 166*4882a593Smuzhiyun except OSError as exc: 167*4882a593Smuzhiyun if exc.errno != errno.ENOENT: 168*4882a593Smuzhiyun raise 169*4882a593Smuzhiyun subprocess.check_call(['git', 'prune', '--expire', 'now']) 170*4882a593Smuzhiyun 171*4882a593Smuzhiyun 172*4882a593Smuzhiyunif __name__ == '__main__': 173*4882a593Smuzhiyun main() 174