]>
Commit | Line | Data |
---|---|---|
0c9c969a UG |
1 | #!/usr/bin/env python |
2 | ||
3 | from collections import namedtuple | |
4 | ||
5 | import argparse | |
6 | import base64 | |
7 | import copy | |
8 | import json | |
9 | import subprocess | |
10 | import sys | |
11 | import urllib.parse | |
12 | import urllib.request | |
13 | import urllib.error | |
14 | ||
15 | class Error(Exception): | |
16 | pass | |
17 | ||
18 | class Version(object): | |
19 | def __init__(self, version): | |
20 | versions = version.split(sep='.') | |
21 | if len(versions) < 2 or len(versions) > 3: | |
22 | raise Error("Invalid version string '{}'".format(version)) | |
23 | self.major = int(versions[0]) | |
24 | self.minor = int(versions[1]) | |
25 | self.revision = int(versions[2]) if len(versions) == 3 else 0 | |
26 | ||
27 | def __str__(self): | |
28 | return '{}.{}.{}'.format(self.major, self.minor, self.revision) | |
29 | ||
30 | def __eq__(self, other): | |
31 | return self.major == other.major and self.minor == other.minor and self.revision == other.revision | |
32 | ||
33 | def verify_version(version): | |
34 | expected = { | |
35 | 'VERSION': [ '"{}"'.format(version), None ], | |
36 | 'VER_MAJOR': [ str(version.major), None ], | |
37 | 'VER_MINOR': [ str(version.minor), None ], | |
38 | 'VER_REVISION': [ str(version.revision), None ], | |
39 | 'VER_PATCH': [ '0', None ], | |
40 | 'SOVERSION': [ '"{}.{}"'.format(version.major, version.minor), None ], | |
41 | } | |
42 | ||
43 | with open('include/git2/version.h') as f: | |
44 | lines = f.readlines() | |
45 | ||
46 | for key in expected.keys(): | |
47 | define = '#define LIBGIT2_{} '.format(key) | |
48 | for line in lines: | |
49 | if line.startswith(define): | |
50 | expected[key][1] = line[len(define):].strip() | |
51 | break | |
52 | else: | |
53 | raise Error("version.h: missing define for '{}'".format(key)) | |
54 | ||
55 | for k, v in expected.items(): | |
56 | if v[0] != v[1]: | |
57 | raise Error("version.h: define '{}' does not match (got '{}', expected '{}')".format(k, v[0], v[1])) | |
58 | ||
59 | def generate_relnotes(tree, version): | |
60 | with open('docs/changelog.md') as f: | |
61 | lines = f.readlines() | |
62 | ||
63 | if not lines[0].startswith('v'): | |
64 | raise Error("changelog.md: missing section for v{}".format(version)) | |
65 | try: | |
66 | v = Version(lines[0][1:].strip()) | |
67 | except: | |
68 | raise Error("changelog.md: invalid version string {}".format(lines[0].strip())) | |
69 | if v != version: | |
70 | raise Error("changelog.md: changelog version doesn't match (got {}, expected {})".format(v, version)) | |
71 | if not lines[1].startswith('----'): | |
72 | raise Error("changelog.md: missing version header") | |
73 | if lines[2] != '\n': | |
74 | raise Error("changelog.md: missing newline after version header") | |
75 | ||
76 | for i, line in enumerate(lines[3:]): | |
77 | if not line.startswith('v'): | |
78 | continue | |
79 | try: | |
80 | Version(line[1:].strip()) | |
81 | break | |
82 | except: | |
83 | continue | |
84 | else: | |
85 | raise Error("changelog.md: cannot find section header of preceding release") | |
86 | ||
87 | return ''.join(lines[3:i + 3]).strip() | |
88 | ||
89 | def git(*args): | |
90 | process = subprocess.run([ 'git', *args ], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
91 | if process.returncode != 0: | |
92 | raise Error('Failed executing git {}: {}'.format(' '.join(args), process.stderr.decode())) | |
93 | return process.stdout | |
94 | ||
95 | def post(url, data, contenttype, user, password): | |
96 | request = urllib.request.Request(url, data=data) | |
97 | request.add_header('Accept', 'application/json') | |
98 | request.add_header('Content-Type', contenttype) | |
99 | request.add_header('Content-Length', len(data)) | |
100 | request.add_header('Authorization', 'Basic ' + base64.b64encode('{}:{}'.format(user, password).encode()).decode()) | |
101 | ||
102 | try: | |
103 | response = urllib.request.urlopen(request) | |
104 | if response.getcode() != 201: | |
105 | raise Error("POST to '{}' failed: {}".format(url, response.reason)) | |
106 | except urllib.error.URLError as e: | |
107 | raise Error("POST to '{}' failed: {}".format(url, e)) | |
108 | data = json.load(response) | |
109 | ||
110 | return data | |
111 | ||
112 | def generate_asset(version, tree, archive_format): | |
113 | Asset = namedtuple('Asset', ['name', 'label', 'mimetype', 'data']) | |
114 | mimetype = 'application/{}'.format('gzip' if archive_format == 'tar.gz' else 'zip') | |
115 | return Asset( | |
116 | "libgit2-{}.{}".format(version, archive_format), "Release sources: libgit2-{}.{}".format(version, archive_format), mimetype, | |
117 | git('archive', '--format', archive_format, '--prefix', 'libgit2-{}/'.format(version), tree) | |
118 | ) | |
119 | ||
120 | def release(args): | |
121 | params = { | |
122 | "tag_name": 'v' + str(args.version), | |
123 | "name": 'libgit2 v' + str(args.version), | |
124 | "target_commitish": git('rev-parse', args.tree).decode().strip(), | |
125 | "body": generate_relnotes(args.tree, args.version), | |
126 | } | |
127 | assets = [ | |
128 | generate_asset(args.version, args.tree, 'tar.gz'), | |
129 | generate_asset(args.version, args.tree, 'zip'), | |
130 | ] | |
131 | ||
132 | if args.dryrun: | |
133 | for k, v in params.items(): | |
134 | print('{}: {}'.format(k, v)) | |
135 | for asset in assets: | |
136 | print('asset: name={}, label={}, mimetype={}, bytes={}'.format(asset.name, asset.label, asset.mimetype, len(asset.data))) | |
137 | return | |
138 | ||
139 | try: | |
140 | url = 'https://api.github.com/repos/{}/releases'.format(args.repository) | |
141 | response = post(url, json.dumps(params).encode(), 'application/json', args.user, args.password) | |
142 | except Error as e: | |
143 | raise Error('Could not create release: ' + str(e)) | |
144 | ||
145 | for asset in assets: | |
146 | try: | |
147 | url = list(urllib.parse.urlparse(response['upload_url'].split('{?')[0])) | |
148 | url[4] = urllib.parse.urlencode({ 'name': asset.name, 'label': asset.label }) | |
149 | post(urllib.parse.urlunparse(url), asset.data, asset.mimetype, args.user, args.password) | |
150 | except Error as e: | |
151 | raise Error('Could not upload asset: ' + str(e)) | |
152 | ||
153 | def main(): | |
154 | parser = argparse.ArgumentParser(description='Create a libgit2 release') | |
155 | parser.add_argument('--tree', default='HEAD', help='tree to create release for (default: HEAD)') | |
156 | parser.add_argument('--dryrun', action='store_true', help='generate release, but do not post it') | |
157 | parser.add_argument('--repository', default='libgit2/libgit2', help='GitHub repository to create repository in') | |
158 | parser.add_argument('--user', help='user to authenitcate as') | |
159 | parser.add_argument('--password', help='password to authenticate with') | |
160 | parser.add_argument('version', type=Version, help='version of the new release') | |
161 | args = parser.parse_args() | |
162 | ||
163 | verify_version(args.version) | |
164 | release(args) | |
165 | ||
166 | if __name__ == '__main__': | |
167 | try: | |
168 | main() | |
169 | except Error as e: | |
170 | print(e) | |
171 | sys.exit(1) |