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()