#!/usr/bin/env python # # NopSCADlib Copyright Chris Palmer 2021 # nop.head@gmail.com # hydraraptor.blogspot.com # # This file is part of NopSCADlib. # # NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the # GNU General Public License as published by the Free Software Foundation, either version 3 of # the License, or (at your option) any later version. # # NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. # See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with NopSCADlib. # If not, see . # #! Creates the changelog from the git log from __future__ import print_function import sys import subprocess import re filename = 'CHANGELOG.md' def tag_version(t): """ Format a version tag """ return 'v%d.%d.%d' % t def initials(name): """ Convert full name to initials with a tooltip """ i = ''.join([n[0].upper() + '.' for n in name.split(' ')]) return '[%s](# "%s")' % (i, name) def get_remote_url(): """ Get the git remote URL for the repository """ url = subprocess.check_output(["git", "config", "--get", "remote.origin.url"]).decode("utf-8").strip("\n") if url.startswith("git@"): url = url.replace(":", "/", 1).replace("git@", "https://", 1) if url.endswith(".git"): url = url[:-4] return url def iscode(word): """ try to guess if the word is code """ endings = ['()', '*'] starts = ['$', '--'] anywhere = ['.', '_', '=', '[', '/'] words = ['center', 'false', 'true', 'ngb'] for w in words: if word == w: return True for end in endings: if word.endswith(end): return True for start in starts: if word.startswith(start): return True for any in anywhere: if word.find(any) >= 0: return True return False def codify(word, url): """ if a word is deemed code enclose it backticks """ if word: if re.match(r'#[0-9]+', word): return '[%s](%s "show issue")' % (word, url + '/issues/' + word[1:]) if iscode(word): return '`' + word + '`' return word def fixup_comment(comment, url): """ markup code words and fix new paragraphs """ result = '' word = '' code = False for i, c in enumerate(comment): if c == '`' or code: # Already a code block result += c # Copy verbatim if c == '`': code = not code # Keep track of state else: if c in ' \n' or (c == '.' and (i + 1 == len(comment) or comment[i + 1] in ' \n')): # if a word terminator result += codify(word, url) + c # Add codified word before terminator word = '' else: word += c # Accumulate next word result += codify(word, url) # In case comment ends without a terminator return result.replace('\n\n','\n\n * ') # Give new paragraphs a bullet point class Commit(): # members dynamically added from commit_fields pass blurb = """ # %s Changelog This changelog is generated by `changelog.py` using manually added semantic version tags to classify commits as breaking changes, additions or fixes. """ if __name__ == '__main__': url = get_remote_url() commit_fields = { 'hash': "%H|", # commit commit_hash 'tag': "%D|", # tag 'author': "%aN|", # author name 'date': " %as|", # author date short form 'comment': "%B~" # body } # Produce the git log format = ''.join([v for k, v in commit_fields.items()]) text = subprocess.check_output(["git", "log", "--topo-order", "--format=" + format]).decode("utf-8") # Process the log into a list of Commit objects commits = [] for line in text.split('~'): line = line.strip('\n') if line: fields = line.split('|') commit = Commit() for i, k in enumerate(commit_fields): exec('commit.%s = """%s"""' % (k, fields[i]), locals()) # Convert version tag to tuple if commit.tag: match = re.match(r'.*tag: v([0-9]+)\.([0-9]+)\.([0-9]+).*', commit.tag) commit.tag = (int(match.group(1)), int(match.group(2)), int(match.group(3))) if match else '' commits.append(commit) # Format the results from the Commit objects with open(filename, "wt") as file: print(blurb % url.split('/')[-1], file = file) for i, c in enumerate(commits): if c.tag: ver = tag_version(c.tag) level, type = (3, 'Fixes') if c.tag[2] else (2, 'Additions') if c.tag[1] else (1, 'Breaking Changes') if c.tag[0] else (1, 'First publicised version') # Find the previous tagged commit j = i + 1 diff = '' while j < len(commits): if commits[j].tag: last_ver = tag_version(commits[j].tag) diff = '[...](%s "diff with %s")' % (url + '/compare/' + last_ver + '...' + ver, last_ver) break j += 1 # Print verson info print('%s [%s](%s "show release") %s %s' % ('#' * (level + 1), ver, url + '/releases/tag/' + ver, type, diff), file = file) # Print commits excluding merges if not c.comment.startswith('Merge branch') and not c.comment.startswith('Merge pull'): print('* %s [`%s`](%s "show commit") %s %s\n' % (c.date, c.hash[:7], url + '/commit/' + c.hash, initials(c.author), fixup_comment(c.comment, url)), file = file)