mirror of
https://github.com/libgit2/libgit2.git
synced 2026-01-25 11:06:32 +00:00
We currently do not set up a project version within CMake, meaning that it can't be use by other projects including libgit2 as a sub-project and also not by other tools like IDEs. This commit changes this to always set up a project version, but instead of extracting it from the "version.h" header we now set it up directly. This is mostly to avoid mis-use of the previous `LIBGIT2_VERSION` variables, as we should now always use the `libgit2_VERSION` ones that are set up by CMake if one provides the "VERSION" keyword to the `project()` call. While this is one more moving target we need to adjust on releases, this commit also adjusts our release script to verify that the project version was incremented as expected.
192 lines
7.0 KiB
Python
Executable File
192 lines
7.0 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
from collections import namedtuple
|
|
|
|
import argparse
|
|
import base64
|
|
import copy
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
class Error(Exception):
|
|
pass
|
|
|
|
class Version(object):
|
|
def __init__(self, version):
|
|
versions = version.split(sep='.')
|
|
if len(versions) < 2 or len(versions) > 3:
|
|
raise Error("Invalid version string '{}'".format(version))
|
|
self.major = int(versions[0])
|
|
self.minor = int(versions[1])
|
|
self.revision = int(versions[2]) if len(versions) == 3 else 0
|
|
|
|
def __str__(self):
|
|
return '{}.{}.{}'.format(self.major, self.minor, self.revision)
|
|
|
|
def __eq__(self, other):
|
|
return self.major == other.major and self.minor == other.minor and self.revision == other.revision
|
|
|
|
def verify_version(version):
|
|
expected = {
|
|
'VERSION': [ '"{}"'.format(version), None ],
|
|
'VER_MAJOR': [ str(version.major), None ],
|
|
'VER_MINOR': [ str(version.minor), None ],
|
|
'VER_REVISION': [ str(version.revision), None ],
|
|
'VER_PATCH': [ '0', None ],
|
|
'SOVERSION': [ '"{}.{}"'.format(version.major, version.minor), None ],
|
|
}
|
|
|
|
# Parse CMakeLists
|
|
with open('CMakeLists.txt') as f:
|
|
for line in f.readlines():
|
|
if line.startswith('project(libgit2 VERSION "{}"'.format(version)):
|
|
break
|
|
else:
|
|
raise Error("cmake: invalid project definition")
|
|
|
|
# Parse version.h
|
|
with open('include/git2/version.h') as f:
|
|
lines = f.readlines()
|
|
|
|
for key in expected.keys():
|
|
define = '#define LIBGIT2_{} '.format(key)
|
|
for line in lines:
|
|
if line.startswith(define):
|
|
expected[key][1] = line[len(define):].strip()
|
|
break
|
|
else:
|
|
raise Error("version.h: missing define for '{}'".format(key))
|
|
|
|
for k, v in expected.items():
|
|
if v[0] != v[1]:
|
|
raise Error("version.h: define '{}' does not match (got '{}', expected '{}')".format(k, v[0], v[1]))
|
|
|
|
with open('package.json') as f:
|
|
pkg = json.load(f)
|
|
|
|
try:
|
|
pkg_version = Version(pkg["version"])
|
|
except KeyError as err:
|
|
raise Error("package.json: missing the field {}".format(err))
|
|
|
|
if pkg_version != version:
|
|
raise Error("package.json: version does not match (got '{}', expected '{}')".format(pkg_version, version))
|
|
|
|
def generate_relnotes(tree, version):
|
|
with open('docs/changelog.md') as f:
|
|
lines = f.readlines()
|
|
|
|
if not lines[0].startswith('v'):
|
|
raise Error("changelog.md: missing section for v{}".format(version))
|
|
try:
|
|
v = Version(lines[0][1:].strip())
|
|
except:
|
|
raise Error("changelog.md: invalid version string {}".format(lines[0].strip()))
|
|
if v != version:
|
|
raise Error("changelog.md: changelog version doesn't match (got {}, expected {})".format(v, version))
|
|
if not lines[1].startswith('----'):
|
|
raise Error("changelog.md: missing version header")
|
|
if lines[2] != '\n':
|
|
raise Error("changelog.md: missing newline after version header")
|
|
|
|
for i, line in enumerate(lines[3:]):
|
|
if not line.startswith('v'):
|
|
continue
|
|
try:
|
|
Version(line[1:].strip())
|
|
break
|
|
except:
|
|
continue
|
|
else:
|
|
raise Error("changelog.md: cannot find section header of preceding release")
|
|
|
|
return ''.join(lines[3:i + 3]).strip()
|
|
|
|
def git(*args):
|
|
process = subprocess.run([ 'git', *args ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
if process.returncode != 0:
|
|
raise Error('Failed executing git {}: {}'.format(' '.join(args), process.stderr.decode()))
|
|
return process.stdout
|
|
|
|
def post(url, data, contenttype, user, password):
|
|
request = urllib.request.Request(url, data=data)
|
|
request.add_header('Accept', 'application/json')
|
|
request.add_header('Content-Type', contenttype)
|
|
request.add_header('Content-Length', len(data))
|
|
request.add_header('Authorization', 'Basic ' + base64.b64encode('{}:{}'.format(user, password).encode()).decode())
|
|
|
|
try:
|
|
response = urllib.request.urlopen(request)
|
|
if response.getcode() != 201:
|
|
raise Error("POST to '{}' failed: {}".format(url, response.reason))
|
|
except urllib.error.URLError as e:
|
|
raise Error("POST to '{}' failed: {}".format(url, e))
|
|
data = json.load(response)
|
|
|
|
return data
|
|
|
|
def generate_asset(version, tree, archive_format):
|
|
Asset = namedtuple('Asset', ['name', 'label', 'mimetype', 'data'])
|
|
mimetype = 'application/{}'.format('gzip' if archive_format == 'tar.gz' else 'zip')
|
|
return Asset(
|
|
"libgit2-{}.{}".format(version, archive_format), "Release sources: libgit2-{}.{}".format(version, archive_format), mimetype,
|
|
git('archive', '--format', archive_format, '--prefix', 'libgit2-{}/'.format(version), tree)
|
|
)
|
|
|
|
def release(args):
|
|
params = {
|
|
"tag_name": 'v' + str(args.version),
|
|
"name": 'libgit2 v' + str(args.version),
|
|
"target_commitish": git('rev-parse', args.tree).decode().strip(),
|
|
"body": generate_relnotes(args.tree, args.version),
|
|
}
|
|
assets = [
|
|
generate_asset(args.version, args.tree, 'tar.gz'),
|
|
generate_asset(args.version, args.tree, 'zip'),
|
|
]
|
|
|
|
if args.dryrun:
|
|
for k, v in params.items():
|
|
print('{}: {}'.format(k, v))
|
|
for asset in assets:
|
|
print('asset: name={}, label={}, mimetype={}, bytes={}'.format(asset.name, asset.label, asset.mimetype, len(asset.data)))
|
|
return
|
|
|
|
try:
|
|
url = 'https://api.github.com/repos/{}/releases'.format(args.repository)
|
|
response = post(url, json.dumps(params).encode(), 'application/json', args.user, args.password)
|
|
except Error as e:
|
|
raise Error('Could not create release: ' + str(e))
|
|
|
|
for asset in assets:
|
|
try:
|
|
url = list(urllib.parse.urlparse(response['upload_url'].split('{?')[0]))
|
|
url[4] = urllib.parse.urlencode({ 'name': asset.name, 'label': asset.label })
|
|
post(urllib.parse.urlunparse(url), asset.data, asset.mimetype, args.user, args.password)
|
|
except Error as e:
|
|
raise Error('Could not upload asset: ' + str(e))
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Create a libgit2 release')
|
|
parser.add_argument('--tree', default='HEAD', help='tree to create release for (default: HEAD)')
|
|
parser.add_argument('--dryrun', action='store_true', help='generate release, but do not post it')
|
|
parser.add_argument('--repository', default='libgit2/libgit2', help='GitHub repository to create repository in')
|
|
parser.add_argument('--user', help='user to authenticate as')
|
|
parser.add_argument('--password', help='password to authenticate with')
|
|
parser.add_argument('version', type=Version, help='version of the new release')
|
|
args = parser.parse_args()
|
|
|
|
verify_version(args.version)
|
|
release(args)
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
main()
|
|
except Error as e:
|
|
print(e)
|
|
sys.exit(1)
|