gen_md_links
1 #!/usr/bin/env python3 2 3 """ 4 Given some markdown text of the general kind we use in Arti changelogs, 5 look for reference-style links to MRs, issues, and commits, and generate 6 the appropriate https URLs for them. 7 8 Takes input either from a file, or from stdin. 9 10 Example: 11 ./gen_md_links < new_changelog 12 """ 13 14 import json 15 import subprocess 16 17 18 def links(s): 19 """Extract unresolved markdown links from a string. 20 21 >>> list(links("Hello [world]. This [is a link]")) 22 ['world', 'is a link'] 23 >>> list(links("This [link](is resolved).")) 24 [] 25 """ 26 27 # It would have been better to import extract-md-links 28 # as a Python module. But see the comment in its main block. 29 30 p = subprocess.Popen( 31 ["maint/extract-md-links"], 32 stdin=subprocess.PIPE, 33 stdout=subprocess.PIPE, 34 encoding="utf-8", 35 ) 36 output, dummy = p.communicate(s) 37 p.wait() 38 assert p.returncode == 0 39 40 output = json.loads(output) 41 return output["used"] 42 43 44 def is_commit(s): 45 """Return true if `s` looks like a git commit. 46 47 >>> is_commit("a3bcD445") 48 True 49 >>> is_commit("xyzzy123") 50 False 51 """ 52 53 if len(s) >= 6: 54 try: 55 int(s, 16) 56 return True 57 except ValueError: 58 pass 59 return False 60 61 62 def lookup_git_commit(short): 63 """Expand a git commit from its short version. 64 65 >>> lookup_git_commit("214c251e41") 66 '214c251e41a7583397cc5939b9447b89752ee323' 67 >>> lookup_git_commit("00000000000000") 68 Traceback (most recent call last): 69 ... 70 ValueError: Unrecognized git commit 00000000000000 71 """ 72 73 p = subprocess.Popen( 74 ["git", "rev-parse", short], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL 75 ) 76 p.wait() 77 if p.returncode != 0: 78 raise ValueError(f"Unrecognized git commit {short}") 79 return p.stdout.read().strip().decode("ascii") 80 81 82 class LinkType: 83 MergeRequest = 1 84 Issue = 2 85 Commit = 3 86 Other = 4 87 88 89 class Link: 90 def __init__(self, s): 91 self._s = s 92 if s.startswith("!") and s[1:].isdecimal(): 93 self._type = LinkType.MergeRequest 94 self._id = int(s[1:]) 95 elif s.startswith("#") and s[1:].isdecimal(): 96 self._type = LinkType.Issue 97 self._id = int(s[1:]) 98 elif is_commit(s): 99 self._type = LinkType.Commit 100 self._id = s.lower() 101 else: 102 self._type = LinkType.Other 103 self._id = s 104 105 def sort_key(self): 106 return (self._type, self._id) 107 108 def link(self): 109 if self._type == LinkType.MergeRequest: 110 return f"https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/{self._id}" 111 elif self._type == LinkType.Issue: 112 return f"https://gitlab.torproject.org/tpo/core/arti/-/issues/{self._id}" 113 elif self._type == LinkType.Commit: 114 full_id = lookup_git_commit(self._id) 115 return f"https://gitlab.torproject.org/tpo/core/arti/-/commit/{full_id}" 116 elif self._type == LinkType.Other: 117 return "" 118 119 def text(self): 120 return "[{}]: {}\n".format(self._s, self.link()) 121 122 123 def process(s): 124 """Given a string with a bunch of markdown links in the style we use 125 in our changelog, generate the following material to insert in 126 the changelog. 127 128 >>> print(process("Hello [#123] [!456]"), end="") 129 [!456]: https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/456 130 [#123]: https://gitlab.torproject.org/tpo/core/arti/-/issues/123 131 """ 132 133 items = sorted((Link(lnk) for lnk in set(links(s))), key=Link.sort_key) 134 return "".join(lnk.text() for lnk in items) 135 136 137 if __name__ == "__main__": 138 import sys 139 import argparse 140 141 parser = argparse.ArgumentParser(prog="gen_md_links") 142 parser.add_argument("filename", nargs="?", default="-") 143 args = parser.parse_args() 144 145 if args.filename == "-": 146 in_file = sys.stdin 147 else: 148 in_file = open(args.filename, "r") 149 150 text = in_file.read() 151 print(process(text))