github.com/artpar/rclone@v1.67.3/bin/make_changelog.py (about) 1 #!/usr/bin/python3 2 """ 3 Generate a markdown changelog for the rclone project 4 """ 5 6 import os 7 import sys 8 import re 9 import datetime 10 import subprocess 11 from collections import defaultdict 12 13 IGNORE_RES = [ 14 r"^Add .* to contributors$", 15 r"^Start v\d+\.\d+(\.\d+)?-DEV development$", 16 r"^Version v\d+\.\d+(\.\d+)?$", 17 ] 18 19 IGNORE_RE = re.compile("(?:" + "|".join(IGNORE_RES) + ")") 20 21 CATEGORY = re.compile(r"(^[\w/ ]+(?:, *[\w/ ]+)*):\s*(.*)$") 22 23 backends = [ x for x in os.listdir("backend") if x != "all"] 24 25 backend_aliases = { 26 "google cloud storage" : "googlecloudstorage", 27 "gcs" : "googlecloudstorage", 28 "azblob" : "azureblob", 29 "mountlib": "mount", 30 "cmount": "mount", 31 "mount/cmount": "mount", 32 } 33 34 backend_titles = { 35 "googlecloudstorage": "Google Cloud Storage", 36 "azureblob": "Azure Blob", 37 "ftp": "FTP", 38 "sftp": "SFTP", 39 "http": "HTTP", 40 "webdav": "WebDAV", 41 } 42 43 STRIP_FIX_RE = re.compile(r"(\s+-)?\s+((fixes|addresses)\s+)?#\d+", flags=re.I) 44 45 STRIP_PATH_RE = re.compile(r"^(backend|fs)/") 46 47 IS_FIX_RE = re.compile(r"\b(fix|fixes)\b", flags=re.I) 48 49 def make_out(data, indent=""): 50 """Return a out, lines the first being a function for output into the second""" 51 out_lines = [] 52 def out(category, title=None): 53 if title == None: 54 title = category 55 lines = data.get(category) 56 if not lines: 57 return 58 del(data[category]) 59 if indent != "" and len(lines) == 1: 60 out_lines.append(indent+"* " + title+": " + lines[0]) 61 return 62 out_lines.append(indent+"* " + title) 63 for line in lines: 64 out_lines.append(indent+" * " + line) 65 return out, out_lines 66 67 68 def process_log(log): 69 """Process the incoming log into a category dict of lists""" 70 by_category = defaultdict(list) 71 for log_line in reversed(log.split("\n")): 72 log_line = log_line.strip() 73 hash, author, timestamp, message = log_line.split("|", 3) 74 message = message.strip() 75 if IGNORE_RE.search(message): 76 continue 77 match = CATEGORY.search(message) 78 categories = "UNKNOWN" 79 if match: 80 categories = match.group(1).lower() 81 message = match.group(2) 82 message = STRIP_FIX_RE.sub("", message) 83 message = message +" ("+author+")" 84 message = message[0].upper()+message[1:] 85 seen = set() 86 for category in categories.split(","): 87 category = category.strip() 88 category = STRIP_PATH_RE.sub("", category) 89 category = backend_aliases.get(category, category) 90 if category in seen: 91 continue 92 by_category[category].append(message) 93 seen.add(category) 94 #print category, hash, author, timestamp, message 95 return by_category 96 97 def main(): 98 if len(sys.argv) != 3: 99 print("Syntax: %s vX.XX vX.XY" % sys.argv[0], file=sys.stderr) 100 sys.exit(1) 101 version, next_version = sys.argv[1], sys.argv[2] 102 log = subprocess.check_output(["git", "log", '''--pretty=format:%H|%an|%aI|%s'''] + [version+".."+next_version]) 103 log = log.decode("utf-8") 104 by_category = process_log(log) 105 106 # Output backends first so remaining in by_category are core items 107 out, backend_lines = make_out(by_category) 108 out("mount", title="Mount") 109 out("vfs", title="VFS") 110 out("local", title="Local") 111 out("cache", title="Cache") 112 out("crypt", title="Crypt") 113 backend_names = sorted(x for x in list(by_category.keys()) if x in backends) 114 for backend_name in backend_names: 115 if backend_name in backend_titles: 116 backend_title = backend_titles[backend_name] 117 else: 118 backend_title = backend_name.title() 119 out(backend_name, title=backend_title) 120 121 # Split remaining in by_category into new features and fixes 122 new_features = defaultdict(list) 123 bugfixes = defaultdict(list) 124 for name, messages in by_category.items(): 125 for message in messages: 126 if IS_FIX_RE.search(message): 127 bugfixes[name].append(message) 128 else: 129 new_features[name].append(message) 130 131 # Output new features 132 out, new_features_lines = make_out(new_features, indent=" ") 133 for name in sorted(new_features.keys()): 134 out(name) 135 136 # Output bugfixes 137 out, bugfix_lines = make_out(bugfixes, indent=" ") 138 for name in sorted(bugfixes.keys()): 139 out(name) 140 141 # Read old changelog and split 142 with open("docs/content/changelog.md") as fd: 143 old_changelog = fd.read() 144 heading = "# Changelog" 145 i = old_changelog.find(heading) 146 if i < 0: 147 raise AssertionError("Couldn't find heading in old changelog") 148 i += len(heading) 149 old_head, old_tail = old_changelog[:i], old_changelog[i:] 150 151 # Update the build date 152 old_head = re.sub(r"\d\d\d\d-\d\d-\d\d", str(datetime.date.today()), old_head) 153 154 # Output combined changelog with new part 155 sys.stdout.write(old_head) 156 today = datetime.date.today() 157 new_features = "\n".join(new_features_lines) 158 bugfixes = "\n".join(bugfix_lines) 159 backend_changes = "\n".join(backend_lines) 160 sys.stdout.write(""" 161 162 ## %(next_version)s - %(today)s 163 164 [See commits](https://github.com/artpar/artpar/compare/%(version)s...%(next_version)s) 165 166 * New backends 167 * New commands 168 * New Features 169 %(new_features)s 170 * Bug Fixes 171 %(bugfixes)s 172 %(backend_changes)s""" % locals()) 173 sys.stdout.write(old_tail) 174 175 176 if __name__ == "__main__": 177 main()