release.py 6.8 KB
Newer Older
1
#!/usr/bin/env python3
2 3

import argparse
4
import configparser
5
import os
6
import re
7
import sys
8
import datetime
9 10 11
from subprocess import check_call, check_output

from weboob.tools.misc import to_unicode
12 13 14 15

WORKTREE = 'release_tmp'
OPTIONS = ['--qt', '--xdg']

16 17

def make_tarball(tag, wheel):
18 19 20 21 22 23 24
    # Create and enter a temporary worktree
    if os.path.isdir(WORKTREE):
        check_call(['git', 'worktree', 'remove', '--force', WORKTREE])
    check_call(['git', 'worktree', 'add', WORKTREE, tag])
    assert os.path.isdir(WORKTREE)
    os.chdir(WORKTREE)

25 26 27 28 29 30 31 32 33
    check_call([sys.executable, 'setup.py'] + OPTIONS +
               ['sdist',
                '--keep',
                '--dist-dir', '../dist'])
    if wheel:
        check_call([sys.executable, 'setup.py'] + OPTIONS +
                   ['bdist_wheel',
                    '--keep',
                    '--dist-dir', '../dist'])
34 35 36 37 38 39

    # Clean up the temporary worktree
    os.chdir(os.pardir)
    check_call(['git', 'worktree', 'remove', '--force', WORKTREE])
    assert not os.path.isdir(WORKTREE)

40 41 42 43 44 45 46 47 48
    files = ['dist/weboob-%s.tar.gz' % tag]
    if wheel:
        files.append('dist/weboob-%s-py2.py3-none-any.whl' % tag)
    for f in files:
        if not os.path.exists(f):
            raise Exception('Generated file not found at %s' % f)
        else:
            print('Generated file: %s' % f)
    print('To upload to PyPI, run: twine upload -s %s' % ' '.join(files))
49 50


51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
def changed_modules(changes, changetype):
    for change in changes:
        change = change.decode('utf-8').split()
        if change[0] == changetype:
            m = re.match(r'modules/([^/]+)/__init__\.py', change[1])
            if m:
                yield m.group(1)


def get_caps(module, config):
    try:
        return sorted(c for c in config[module]['capabilities'].split() if c != 'CapCollection')
    except KeyError:
        return ['**** FILL ME **** (running weboob update could help)']

def new_modules(start, end):
67
    os.chdir(os.path.join(os.path.dirname(__file__), os.path.pardir))
68 69 70 71 72 73 74 75 76 77 78 79 80 81
    modules_info = configparser.ConfigParser()
    with open('modules/modules.list') as f:
        modules_info.read_file(f)
    git_cmd = ['git', 'diff', '--no-renames', '--name-status', '%s..%s' % (start, end), '--', 'modules/']

    added_modules = sorted(changed_modules(check_output(git_cmd).splitlines(), 'A'))
    deleted_modules = sorted(changed_modules(check_output(git_cmd).splitlines(), 'D'))

    for added_module in added_modules:
        yield 'New %s module (%s)' % (added_module, ', '.join(get_caps(added_module, modules_info)))
    for deleted_module in deleted_modules:
        yield 'Deleted %s module' % deleted_module


82 83 84 85 86 87 88 89 90 91
def changelog(start, end='HEAD'):
    def sortkey(d):
        """Put the commits with multiple domains at the end"""
        return (len(d), d)

    commits = {}
    for commithash in check_output(['git', 'rev-list', '{}..{}'.format(start, end)]).splitlines():
        title, domains = commitinfo(commithash)
        commits.setdefault(domains, []).append(title)

92 93 94
    for line in new_modules(start, end):
        commits.setdefault(('General',), []).append(line)

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
    cl = ''
    for domains in sorted(commits.keys(), key=sortkey):
        cl += '\n\n\t' + '\n\t'.join(domains)
        for title in commits[domains]:
            cl += '\n\t* ' + title

    return cl.lstrip('\n')


def domain(path):
    dirs = os.path.dirname(path).split('/')
    if dirs == ['']:
        return 'General: Core'
    if dirs[0] == 'man' or path == 'tools/py3-compatible.modules':
        return None
    if dirs[0] == 'weboob':
        try:
            if dirs[1] in ('core', 'tools'):
                return 'General: Core'
            elif dirs[1] == 'capabilities':
                return 'Capabilities'
            elif dirs[1] == 'browser':
                try:
                    if dirs[2] == 'filters':
                        return 'Browser: Filters'
                except IndexError:
                    return 'Browser'
            elif dirs[1] == 'applications':
                try:
                    return 'Applications: {}'.format(dirs[2])
                except IndexError:
                    return 'Applications'
            elif dirs[1] == 'application':
                try:
                    return 'Applications: {}'.format(dirs[2].title())
                except IndexError:
                    return 'Applications'
        except IndexError:
            return 'General: Core'
    if dirs[0] in ('contrib', 'tools'):
        return 'Tools'
    if dirs[0] in ('docs', 'icons'):
        return 'Documentation'
    if dirs[0] == 'modules':
        try:
            return 'Modules: {}'.format(dirs[1])
        except IndexError:
            return 'General: Core'
    return 'Unknown'


def commitinfo(commithash):
147
    info = check_output(['git', 'show', '--format=%s', '--name-only', commithash]).decode('utf-8').splitlines()
148 149 150 151 152 153 154 155 156
    title = to_unicode(info[0])
    domains = set([domain(p) for p in info[2:] if domain(p)])
    if 'Unknown' in domains and len(domains) > 1:
        domains.remove('Unknown')
    if not domains or len(domains) > 5:
        domains = set(['Unknown'])

    if 'Unknown' not in domains:
        # When the domains are known, hide the title prefixes
157
        title = re.sub(r'^(?:[\w\./\s]+:|\[[\w\./\s]+\])\s*', '', title, flags=re.UNICODE)
158 159 160 161 162 163 164 165 166

    return title, tuple(sorted(domains))


def previous_version():
    """
    Get the highest version tag
    """
    for v in check_output(['git', 'tag', '-l', '*.*', '--sort=-v:refname']).splitlines():
167
        return v.decode()
168 169 170


def prepare(start, end, version):
171 172
    print('Weboob %s (%s)\n' % (version, datetime.date.today().strftime('%Y-%m-%d')))
    print(changelog(start, end))
173 174


175
if __name__ == '__main__':
176 177 178 179 180 181
    parser = argparse.ArgumentParser(
        description="Prepare and export a release.",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        epilog='This is mostly meant to be called from release.sh for now.',
    )

182 183
    subparsers = parser.add_subparsers()

184 185 186 187 188 189 190 191 192 193 194
    prepare_parser = subparsers.add_parser(
        'prepare',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    prepare_parser.add_argument('version')
    prepare_parser.add_argument('--start', default=previous_version(), help='Commit of the previous release')
    prepare_parser.add_argument('--end', default='HEAD', help='Last commit before the new release')
    prepare_parser.set_defaults(mode='prepare')

    tarball_parser = subparsers.add_parser(
        'tarball',
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
195
    tarball_parser.add_argument('tag')
196
    tarball_parser.add_argument('--no-wheel', action='store_false', dest='wheel')
197 198 199
    tarball_parser.set_defaults(mode='tarball')

    args = parser.parse_args()
200 201 202
    if args.mode == 'prepare':
        prepare(args.start, args.end, args.version)
    elif args.mode == 'tarball':
203
        make_tarball(args.tag, args.wheel)