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)