diff --git a/main.py b/main.py index b045114..05dce18 100755 --- a/main.py +++ b/main.py @@ -165,6 +165,23 @@ For detailed help on each command, use: terminal_parser = subparsers.add_parser("terminal", help="Open repository in a new GNOME Terminal tab") add_identifier_arguments(terminal_parser) + release_parser = subparsers.add_parser( + "release", + help="Create a release for repository/ies by incrementing version and updating the changelog." + ) + release_parser.add_argument( + "release_type", + choices=["major", "minor", "patch"], + help="Type of version increment for the release (major, minor, patch)." + ) + release_parser.add_argument( + "-m", "--message", + default="", + help="Optional release message to add to the changelog and tag." + ) + add_identifier_arguments(release_parser) + + code_parser = subparsers.add_parser("code", help="Open repository workspace with VS Code") add_identifier_arguments(code_parser) @@ -253,6 +270,35 @@ For detailed help on each command, use: quiet=args.quiet, update_dependencies=args.dependencies ) + elif args.command == "release": + if not selected: + print("No repositories selected for release.") + exit(1) + # Import the release function from pkgmgr/release.py + from pkgmgr import release as rel + # Save the original working directory. + original_dir = os.getcwd() + for repo in selected: + # Determine the repository directory + repo_dir = repo.get("directory") + if not repo_dir: + from pkgmgr.get_repo_dir import get_repo_dir + repo_dir = get_repo_dir(REPOSITORIES_BASE_DIR, repo) + # Dynamically determine the file paths for pyproject.toml and CHANGELOG.md. + pyproject_path = os.path.join(repo_dir, "pyproject.toml") + changelog_path = os.path.join(repo_dir, "CHANGELOG.md") + print(f"Releasing repository '{repo.get('repository')}' in '{repo_dir}'...") + # Change into the repository directory so Git commands run in the right context. + os.chdir(repo_dir) + # Call the release function with the proper parameters. + rel.release( + pyproject_path=pyproject_path, + changelog_path=changelog_path, + release_type=args.release_type, + message=args.message + ) + # Change back to the original working directory. + os.chdir(original_dir) elif args.command == "status": status_repos(selected,REPOSITORIES_BASE_DIR, ALL_REPOSITORIES, args.extra_args, list_only=args.list, system_status=args.system, preview=args.preview) elif args.command == "explore": diff --git a/pkgmgr/release.py b/pkgmgr/release.py new file mode 100644 index 0000000..65e5867 --- /dev/null +++ b/pkgmgr/release.py @@ -0,0 +1,152 @@ +""" +pkgmgr/release.py + +This module defines a 'release' function that: + - Increments the version in pyproject.toml based on the release type (major, minor, patch) + - Updates the CHANGELOG.md with a new release entry (including an optional message) + - Executes Git commands to commit, tag, and push the release. +""" + +import re +import subprocess +from datetime import date +import sys +import argparse + +def bump_version(version_str: str, release_type: str) -> str: + """ + Parse the version string and return the incremented version. + + Parameters: + version_str: The current version in the form "X.Y.Z". + release_type: One of "major", "minor", or "patch". + + Returns: + The bumped version string. + """ + parts = version_str.split('.') + if len(parts) != 3: + raise ValueError("Version format is unexpected. Expected format: X.Y.Z") + major, minor, patch = map(int, parts) + if release_type == "major": + major += 1 + minor = 0 + patch = 0 + elif release_type == "minor": + minor += 1 + patch = 0 + elif release_type == "patch": + patch += 1 + else: + raise ValueError("release_type must be 'major', 'minor', or 'patch'.") + return f"{major}.{minor}.{patch}" + +def update_pyproject_version(pyproject_path: str, new_version: str): + """ + Update the version in pyproject.toml with the new version. + + Parameters: + pyproject_path: Path to the pyproject.toml file. + new_version: The new version string. + """ + with open(pyproject_path, "r") as f: + content = f.read() + # Search for the version string in the format: version = "X.Y.Z" + new_content, count = re.subn(r'(version\s*=\s*")([\d\.]+)(")', r'\1' + new_version + r'\3', content) + if count == 0: + print("Could not find version line in pyproject.toml") + sys.exit(1) + with open(pyproject_path, "w") as f: + f.write(new_content) + print(f"Updated pyproject.toml version to {new_version}") + +def update_changelog(changelog_path: str, new_version: str, message: str = None): + """ + Prepend a new release section to CHANGELOG.md with the new version, + today’s date and an optional release message. + + Parameters: + changelog_path: Path to the CHANGELOG.md file. + new_version: The new version string. + message: An optional release message. + """ + release_date = date.today().isoformat() + header = f"## [{new_version}] - {release_date}\n" + if message: + header += f"{message}\n" + header += "\n" + try: + with open(changelog_path, "r") as f: + changelog = f.read() + except FileNotFoundError: + changelog = "" + new_changelog = header + changelog + with open(changelog_path, "w") as f: + f.write(new_changelog) + print(f"Updated CHANGELOG.md with version {new_version}") + +def run_git_command(cmd: str): + """ + Execute a shell command via Git and exit if it fails. + + Parameters: + cmd: The shell command to run. + """ + print(f"Running: {cmd}") + result = subprocess.run(cmd, shell=True) + if result.returncode != 0: + print(f"Command failed: {cmd}") + sys.exit(result.returncode) + +def release(pyproject_path: str = "pyproject.toml", + changelog_path: str = "CHANGELOG.md", + release_type: str = "patch", + message: str = None): + """ + Perform a release by incrementing the version in pyproject.toml, + updating CHANGELOG.md with the release version and message, then executing + the Git commands to commit, tag, and push the changes. + + Parameters: + pyproject_path: The path to pyproject.toml. + changelog_path: The path to CHANGELOG.md. + release_type: A string indicating the type of release ("major", "minor", "patch"). + message: An optional release message to include in CHANGELOG.md and Git tag. + """ + try: + with open(pyproject_path, "r") as f: + content = f.read() + except FileNotFoundError: + print(f"{pyproject_path} not found.") + sys.exit(1) + + match = re.search(r'version\s*=\s*"([\d\.]+)"', content) + if not match: + print("Could not find version in pyproject.toml") + sys.exit(1) + current_version = match.group(1) + new_version = bump_version(current_version, release_type) + + # Update files. + update_pyproject_version(pyproject_path, new_version) + update_changelog(changelog_path, new_version, message) + + # Execute Git commands. + commit_msg = f"Release version {new_version}" + run_git_command(f'git commit -am "{commit_msg}"') + run_git_command(f'git tag -a v{new_version} -m "{commit_msg}"') + run_git_command("git push origin main") + run_git_command("git push origin --tags") + print(f"Release {new_version} completed successfully.") + +# Allow the script to be used as a CLI tool. +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Perform a release by updating version and changelog, then executing Git commands." + ) + parser.add_argument("release_type", choices=["major", "minor", "patch"], + help="Type of release increment (major, minor, patch).") + parser.add_argument("-m", "--message", help="Optional release message for changelog and tag.", default=None) + + args = parser.parse_args() + release(release_type=args.release_type, message=args.message) \ No newline at end of file