xref: /OK3568_Linux_fs/yocto/bitbake/bin/git-make-shallow (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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