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