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