1#!/usr/bin/env python3 2 3import argparse 4import configparser 5import os 6import re 7import sys 8import datetime 9from subprocess import check_call, check_output 10 11from weboob.tools.misc import to_unicode 12 13WORKTREE = 'release_tmp' 14 15 16def make_tarball(tag, wheel): 17 # Create and enter a temporary worktree 18 if os.path.isdir(WORKTREE): 19 check_call(['git', 'worktree', 'remove', '--force', WORKTREE]) 20 check_call(['git', 'worktree', 'add', WORKTREE, tag]) 21 assert os.path.isdir(WORKTREE) 22 os.chdir(WORKTREE) 23 24 check_call([sys.executable, 'setup.py'] + 25 ['sdist', 26 '--keep', 27 '--dist-dir', '../dist']) 28 if wheel: 29 check_call([sys.executable, 'setup.py'] + 30 ['bdist_wheel', 31 '--keep', 32 '--dist-dir', '../dist']) 33 34 # Clean up the temporary worktree 35 os.chdir(os.pardir) 36 check_call(['git', 'worktree', 'remove', '--force', WORKTREE]) 37 assert not os.path.isdir(WORKTREE) 38 39 files = ['dist/weboob-qt-%s.tar.gz' % tag] 40 if wheel: 41 files.append('dist/weboob_qt-%s-py2.py3-none-any.whl' % tag) 42 for f in files: 43 if not os.path.exists(f): 44 raise Exception('Generated file not found at %s' % f) 45 else: 46 print('Generated file: %s' % f) 47 print('To upload to PyPI, run: twine upload -s %s' % ' '.join(files)) 48 49 50def changed_modules(changes, changetype): 51 for change in changes: 52 change = change.decode('utf-8').split() 53 if change[0] == changetype: 54 m = re.match(r'modules/([^/]+)/__init__\.py', change[1]) 55 if m: 56 yield m.group(1) 57 58 59def get_caps(module, config): 60 try: 61 return sorted(c for c in config[module]['capabilities'].split() if c != 'CapCollection') 62 except KeyError: 63 return ['**** FILL ME **** (running weboob update could help)'] 64 65def new_modules(start, end): 66 os.chdir(os.path.join(os.path.dirname(__file__), os.path.pardir)) 67 modules_info = configparser.ConfigParser() 68 try: 69 with open('modules/modules.list') as f: 70 modules_info.read_file(f) 71 except FileNotFoundError: 72 modules_info = {} 73 git_cmd = ['git', 'diff', '--no-renames', '--name-status', '%s..%s' % (start, end), '--', 'modules/'] 74 75 added_modules = sorted(changed_modules(check_output(git_cmd).splitlines(), 'A')) 76 deleted_modules = sorted(changed_modules(check_output(git_cmd).splitlines(), 'D')) 77 78 for added_module in added_modules: 79 yield 'New %s module (%s)' % (added_module, ', '.join(get_caps(added_module, modules_info))) 80 for deleted_module in deleted_modules: 81 yield 'Deleted %s module' % deleted_module 82 83 84def changelog(start, end='HEAD'): 85 def sortkey(d): 86 """Put the commits with multiple domains at the end""" 87 return (len(d), d) 88 89 commits = {} 90 for commithash in check_output(['git', 'rev-list', '{}..{}'.format(start, end)]).splitlines(): 91 title, domains = commitinfo(commithash) 92 commits.setdefault(domains, []).append(title) 93 94 for line in new_modules(start, end): 95 commits.setdefault(('General',), []).append(line) 96 97 cl = '' 98 for domains in sorted(commits.keys(), key=sortkey): 99 cl += '\n\n\t' + '\n\t'.join(domains) 100 for title in commits[domains]: 101 cl += '\n\t* ' + title 102 103 return cl.lstrip('\n') 104 105 106def domain(path): 107 dirs = os.path.dirname(path).split('/') 108 if dirs == ['']: 109 return 'General: Core' 110 if dirs[0] == 'man' or path == 'tools/py3-compatible.modules': 111 return None 112 if dirs[0] == 'weboob': 113 try: 114 if dirs[1] in ('core', 'tools'): 115 return 'General: Core' 116 elif dirs[1] == 'capabilities': 117 return 'Capabilities' 118 elif dirs[1] == 'browser': 119 try: 120 if dirs[2] == 'filters': 121 return 'Browser: Filters' 122 except IndexError: 123 return 'Browser' 124 elif dirs[1] == 'applications': 125 try: 126 return 'Applications: {}'.format(dirs[2]) 127 except IndexError: 128 return 'Applications' 129 elif dirs[1] == 'application': 130 try: 131 return 'Applications: {}'.format(dirs[2].title()) 132 except IndexError: 133 return 'Applications' 134 except IndexError: 135 return 'General: Core' 136 if dirs[0] in ('contrib', 'tools'): 137 return 'Tools' 138 if dirs[0] in ('docs', 'icons'): 139 return 'Documentation' 140 if dirs[0] == 'modules': 141 try: 142 return 'Modules: {}'.format(dirs[1]) 143 except IndexError: 144 return 'General: Core' 145 return 'Unknown' 146 147 148def commitinfo(commithash): 149 info = check_output(['git', 'show', '--format=%s', '--name-only', commithash]).decode('utf-8').splitlines() 150 title = to_unicode(info[0]) 151 domains = set([domain(p) for p in info[2:] if domain(p)]) 152 if 'Unknown' in domains and len(domains) > 1: 153 domains.remove('Unknown') 154 if not domains or len(domains) > 5: 155 domains = set(['Unknown']) 156 157 if 'Unknown' not in domains: 158 # When the domains are known, hide the title prefixes 159 title = re.sub(r'^(?:[\w\./\s]+:|\[[\w\./\s]+\])\s*', '', title, flags=re.UNICODE) 160 161 return title, tuple(sorted(domains)) 162 163 164def previous_version(): 165 """ 166 Get the highest version tag 167 """ 168 for v in check_output(['git', 'tag', '-l', '*.*', '--sort=-v:refname']).splitlines(): 169 return v.decode() 170 171 172def prepare(start, end, version): 173 print('Weboob %s (%s)\n' % (version, datetime.date.today().strftime('%Y-%m-%d'))) 174 print(changelog(start, end)) 175 176 177if __name__ == '__main__': 178 parser = argparse.ArgumentParser( 179 description="Prepare and export a release.", 180 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 181 epilog='This is mostly meant to be called from release.sh for now.', 182 ) 183 184 subparsers = parser.add_subparsers() 185 186 prepare_parser = subparsers.add_parser( 187 'prepare', 188 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 189 prepare_parser.add_argument('version') 190 prepare_parser.add_argument('--start', default=previous_version(), help='Commit of the previous release') 191 prepare_parser.add_argument('--end', default='HEAD', help='Last commit before the new release') 192 prepare_parser.set_defaults(mode='prepare') 193 194 tarball_parser = subparsers.add_parser( 195 'tarball', 196 formatter_class=argparse.ArgumentDefaultsHelpFormatter) 197 tarball_parser.add_argument('tag') 198 tarball_parser.add_argument('--no-wheel', action='store_false', dest='wheel') 199 tarball_parser.set_defaults(mode='tarball') 200 201 args = parser.parse_args() 202 if args.mode == 'prepare': 203 prepare(args.start, args.end, args.version) 204 elif args.mode == 'tarball': 205 make_tarball(args.tag, args.wheel) 206