scripts/draft-release.sh: improve changelog script (#6508)
- Improved commit message parsing for better format handling - Added argument validation and user prompts for interactive mode - Implemented enhanced error handling and logging
This commit is contained in:
parent
3c2596e3e2
commit
fb894b3720
@ -1,69 +1,164 @@
|
||||
#!/usr/bin/env python
|
||||
# Example of usage: changelog.py <old-release-tag> <new-release-tag>
|
||||
|
||||
|
||||
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()
|
||||
|
Loading…
Reference in New Issue
Block a user