-
Notifications
You must be signed in to change notification settings - Fork 18
/
latest_submodules.py
executable file
·153 lines (122 loc) · 4.77 KB
/
latest_submodules.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import argparse
import os
import os.path
import re
import subprocess
import sys
def run(*args, check=True):
return subprocess.run(args, capture_output=True, check=check, universal_newlines=True)
def get_submodules():
"""
Return list of submodules for current repo, sorted by name.
Each item looks like:
{
'name': 'aws-c-common',
'path': 'crt/aws-c-common',
'url': 'https://github.com/awslabs/aws-c-common.git',
}
"""
if not os.path.exists('.gitmodules'):
sys.exit(f'No .gitmodules found in {os.getcwd()}')
submodules = []
start_pattern = re.compile(r'\[submodule')
path_pattern = re.compile(r'\s+path = (\S+)')
url_pattern = re.compile(r'\s+url = (\S+)')
current = None
with open('.gitmodules', 'r') as f:
for line in f.readlines():
m = start_pattern.match(line)
if m:
current = {}
submodules.append(current)
continue
m = path_pattern.match(line)
if m:
current['path'] = m.group(1)
current['name'] = os.path.basename(current['path'])
continue
m = url_pattern.match(line)
if m:
current['url'] = m.group(1)
continue
return sorted(submodules, key=lambda x: x['name'])
def get_release_tags():
"""
Return list of release tags for current repo, sorted high to low.
Each item looks like:
{
'commit': 'e18f041a0c8d17189f2eae2a32f16e0a7a3f0f1c',
'version': 'v0.5.18'
'num_tuple': (0,5,18),
}
"""
git_output = run('git', 'ls-remote', '--tags').stdout
tags = []
for line in git_output.splitlines():
# line looks like: "e18f041a0c8d17189f2eae2a32f16e0a7a3f0f1c refs/tags/v0.5.18"
match = re.match(
r'([a-f0-9]+)\s+refs/tags/(v([0-9]+)\.([0-9]+)\.([0-9]+))$', line)
if not match:
# skip malformed release tags
continue
tags.append({
'commit': match.group(1),
'version': match.group(2),
'num_tuple': (int(match.group(3)), int(match.group(4)), int(match.group(5))),
})
# sort highest version first
return sorted(tags, reverse=True, key=lambda tag: tag['num_tuple'])
def get_current_commit():
git_output = run('git', 'rev-parse', 'HEAD').stdout
return git_output.splitlines()[0]
def is_ancestor(ancestor, descendant):
"""Return whether first commit is an ancestor to the second'"""
result = run('git', 'merge-base', '--is-ancestor',
ancestor, descendant, check=False)
return result.returncode == 0
def get_tag_for_commit(tags, commit):
for tag in tags:
if tag['commit'] == commit:
return tag
return None
def main():
parser = argparse.ArgumentParser(
description="Update submodules to latest tags")
parser.add_argument('ignore', nargs='*', help="submodules to ignore")
parser.add_argument('--dry-run', action='store_true',
help="print without actually updating")
args = parser.parse_args()
root_path = os.getcwd()
submodules = get_submodules()
name_pad = max([len(x['name']) for x in submodules])
for submodule in submodules:
name = submodule['name']
os.chdir(os.path.join(root_path, submodule['path']))
tags = get_release_tags()
current_commit = get_current_commit()
current_tag = get_tag_for_commit(tags, current_commit)
sync_from = current_tag['version'] if current_tag else current_commit
if name in args.ignore:
print(f"{name:<{name_pad}} {sync_from} (ignored)")
continue
latest_tag = tags[0]
sync_to = latest_tag['version']
# The only time we don't want to sync to the latest release is:
# The submodule is at some commit beyond the latest release,
# and the CRT team doesn't control this repo so can't just cut a new release
if sync_from != sync_to and current_tag is None:
if name in ['aws-lc', 's2n', 's2n-tls']:
# must fetch tags before we can check their ancestry
run('git', 'fetch', '--tags', '--prune', '--prune-tags', '--force')
if not is_ancestor(ancestor=current_commit, descendant=sync_to):
sync_to = sync_from
if sync_from == sync_to:
print(f"{name:<{name_pad}} {sync_from} ✓")
else:
print(f"{name:<{name_pad}} {sync_from} -> {sync_to}")
if not args.dry_run:
run('git', 'fetch', '--tags', '--prune', '--prune-tags', '--force')
run('git', 'checkout', sync_to)
run('git', 'submodule', 'update')
if __name__ == '__main__':
main()