|
| 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) |
0 commit comments