diff --git a/scripts/changelog.py b/scripts/changelog.py index 5993b1ddf..5fd8f57ff 100644 --- a/scripts/changelog.py +++ b/scripts/changelog.py @@ -1,69 +1,164 @@ -#!/usr/bin/env python -# Example of usage: changelog.py - - import argparse -import collections +import logging +import os import subprocess +import sys +from enum import Enum + +# Setting up basic logging +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + + +class Section(Enum): + GENERAL = "General" + MULTINODE = "Multinode" + SATELLITE = "Satellite" + STORAGENODE = "Storagenode" + TEST = "Test" + UPLINK = "Uplink" + -GENERAL = "General" -MULTINODE = "Multinode" -SATELLITE = "Satellite" -STORAGENODE = "Storagenode" -TEST = "Test" -UPLINK = "Uplink" GITHUB_LINK = "[{0}](https://github.com/storj/storj/commit/{0})" def git_ref_field(from_ref, to_ref): - # Execute command to show diff without cherry-picks - cmd = "git cherry {} {} -v | grep '^+'".format(from_ref, to_ref) - ps = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - output = ps.communicate()[0] - return output.decode() + """ + Executes a git command to find the difference in commits between two references. + Assumes 'from_ref' and 'to_ref' are valid Git references. + + Args: + from_ref (str): The source reference. + to_ref (str): The target reference. + + Returns: + str: A string containing the git commit differences. + """ + cmd = ["git", "cherry", from_ref, to_ref, "-v"] + try: + result = subprocess.run(cmd, text=True, capture_output=True, check=True) + return result.stdout + except subprocess.CalledProcessError as e: + logging.error(f"Error executing git command: {e.stderr}") + raise + + +def validate_git_refs(from_ref, to_ref): + """ + Validates the provided Git references. + + Args: + from_ref (str): The source reference. + to_ref (str): The target reference. + + Returns: + bool: True if references are valid, False otherwise. + """ + for ref in [from_ref, to_ref]: + result = subprocess.run(["git", "rev-parse", "--verify", ref], text=True, capture_output=True) + if result.returncode != 0: + logging.error(f"Invalid Git reference: {ref}") + return False + return True + + +def categorize_commit(commit, section_dict): + """ + Categorizes a single commit into the appropriate section. + Handles unexpected commit formats by logging a warning and defaulting to the GENERAL section. + + Args: + commit (str): A git commit message. + section_dict (dict): Dictionary of sections. + + Returns: + None + """ + try: + commit_category = commit[42:].split(":")[0].lower() + for category in section_dict: + if category.name.lower() in commit_category: + section_dict[category].append(generate_line(commit)) + return + section_dict[Section.GENERAL].append(generate_line(commit)) + except IndexError: + logging.warning(f"Unexpected commit format: {commit}") + section_dict[Section.GENERAL].append(generate_line(commit)) def generate_changelog(commits): + """ + Generates a formatted changelog from a string of commits. + Args: + commits (str): A string containing git commit messages. + Returns: + str: The formatted changelog. + """ + if not commits: + return "No new commits found or error occurred." + changelog = "# Changelog\n" - section = {SATELLITE: [], MULTINODE: [], STORAGENODE: [], TEST: [], UPLINK: [], GENERAL: []} + section_dict = {s: [] for s in Section} - # Sorting and populating the dictionary d with commit hash and message for commit in commits.splitlines(): - if TEST.lower() in commit[42:].split(":")[0]: - section[TEST].append(generate_line(commit)) - elif MULTINODE.lower() in commit[42:].split(":")[0]: - section[MULTINODE].append(generate_line(commit)) - elif STORAGENODE.lower() in commit[42:].split(":")[0]: - section[STORAGENODE].append(generate_line(commit)) - elif UPLINK.lower() in commit[42:].split(":")[0]: - section[UPLINK].append(generate_line(commit)) - elif SATELLITE.lower() in commit[42:].split(":")[0]: - section[SATELLITE].append(generate_line(commit)) - else: - section[GENERAL].append(generate_line(commit)) + categorize_commit(commit, section_dict) + + for title, lines in section_dict.items(): + if lines: + changelog += f'### {title.value}\n' + ''.join(lines) - for title in collections.OrderedDict(sorted(section.items())): - if section[title]: - changelog += ('### {}\n'.format(title)) - for line in section[title]: - changelog += line return changelog def generate_line(commit): - return "- {}{} \n".format(GITHUB_LINK.format(commit[2:9]), commit[42:]) + """ + Formats a single commit line for the changelog. + Args: + commit (str): A git commit message. + Returns: + str: The formatted commit line. + """ + return f"- {GITHUB_LINK.format(commit[2:9])} {commit[42:]}\n" + + +def prompt_for_refs(args): + """ + Prompts user for 'from_ref' and 'to_ref' if not provided. + Args: + args: Parsed command-line arguments. + Returns: + None + """ + if not args.from_ref: + args.from_ref = input("Enter the starting Git reference (from_ref): ") + if not args.to_ref: + args.to_ref = input("Enter the ending Git reference (to_ref): ") def main(): - p = argparse.ArgumentParser(description=( - "generate changelog sorted by topics.")) - p.add_argument("from_ref", help="the ref to show the path from") - p.add_argument("to_ref", help="the ref to show the path to") - args = p.parse_args() - commits = git_ref_field(args.from_ref, args.to_ref) + """ + Main function to parse arguments, validate them, and print the changelog. + If run interactively, prompts the user for input. + """ + parser = argparse.ArgumentParser(description="Generate a sorted changelog from Git commits.") + parser.add_argument("from_ref", nargs='?', help="The ref to show the path from") + parser.add_argument("to_ref", nargs='?', help="The ref to show the path to") - print(generate_changelog(commits)) + args = parser.parse_args() + # Check if the script is running interactively + if os.isatty(sys.stdin.fileno()): + prompt_for_refs(args) + + if not (args.from_ref and args.to_ref) or not validate_git_refs(args.from_ref, args.to_ref): + parser.print_help() + sys.exit(1) + + try: + commits = git_ref_field(args.from_ref, args.to_ref) + changelog = generate_changelog(commits) + print(changelog) + except Exception as e: + logging.error(f"An error occurred: {str(e)}") if __name__ == "__main__": main()