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