|
#!/usr/bin/env -S uv run --script |
|
# /// script |
|
# dependencies = [ |
|
# "rich>=13.0.0", |
|
# "typer>=0.12.0", |
|
# ] |
|
# /// |
|
""" |
|
git-submodule-manager — Comprehensive Git Submodule Management Tool |
|
|
|
A standalone Python script (PEP 723 uv inline, minimal external deps) that |
|
provides comprehensive git submodule status checking, workflow automation, |
|
and actionable recommendations. |
|
|
|
Run directly or install globally for easy access from any repository. |
|
|
|
Usage: |
|
./git_submodule_manager.py # Check current directory |
|
./git_submodule_manager.py /path/to/repo # Check specific repository |
|
./git_submodule_manager.py --verbose # Detailed output |
|
./git_submodule_manager.py --json # JSON output for automation |
|
|
|
Install: |
|
curl -fsSL https://gist.githubusercontent.com/tobiashochguertel/f859836fb27910e328cc653f45838354/raw/install.sh | bash |
|
|
|
For more information: https://gist.github.com/tobiashochguertel/f859836fb27910e328cc653f45838354 |
|
""" |
|
|
|
import json |
|
import os |
|
import subprocess |
|
import sys |
|
from dataclasses import dataclass, field |
|
from enum import Enum |
|
from pathlib import Path |
|
from typing import Optional |
|
|
|
import typer |
|
from rich.console import Console |
|
from rich.panel import Panel |
|
from rich.table import Table |
|
from rich.tree import Tree |
|
|
|
console = Console() |
|
app = typer.Typer( |
|
help="Git Submodule Manager - Comprehensive status checking and workflow automation" |
|
) |
|
|
|
|
|
class RepoStatus(Enum): |
|
"""Repository status codes.""" |
|
|
|
CLEAN = "clean" |
|
MODIFIED = "modified" |
|
UNTRACKED = "untracked" |
|
AHEAD = "ahead" |
|
BEHIND = "behind" |
|
DIVERGED = "diverged" |
|
DETACHED = "detached" |
|
ERROR = "error" |
|
|
|
|
|
@dataclass |
|
class SubmoduleInfo: |
|
"""Information about a git submodule.""" |
|
|
|
name: str |
|
path: str |
|
commit_hash: str |
|
status_prefix: str # +, -, ' ', U |
|
branch: Optional[str] = None |
|
has_changes: bool = False |
|
unpushed_commits: bool = False |
|
is_detached: bool = False |
|
|
|
|
|
@dataclass |
|
class RepoInfo: |
|
"""Information about a git repository.""" |
|
|
|
path: Path |
|
name: str |
|
is_submodule: bool = False |
|
current_branch: Optional[str] = None |
|
status: RepoStatus = RepoStatus.CLEAN |
|
is_detached: bool = False |
|
modified_files: list = field(default_factory=list) |
|
untracked_files: list = field(default_factory=list) |
|
staged_files: list = field(default_factory=list) |
|
ahead_count: int = 0 |
|
behind_count: int = 0 |
|
submodules: list = field(default_factory=list) |
|
error_message: Optional[str] = None |
|
|
|
|
|
def run_git_command( |
|
cmd: list[str], cwd: Path, check: bool = False |
|
) -> tuple[int, str, str]: |
|
"""Run a git command and return (returncode, stdout, stderr).""" |
|
try: |
|
result = subprocess.run( |
|
["git"] + cmd, cwd=cwd, capture_output=True, text=True, check=check |
|
) |
|
return result.returncode, result.stdout, result.stderr |
|
except Exception as e: |
|
return 1, "", str(e) |
|
|
|
|
|
def get_current_branch(repo_path: Path) -> Optional[str]: |
|
"""Get the current branch name or None if detached.""" |
|
code, stdout, _ = run_git_command(["branch", "--show-current"], repo_path) |
|
if code == 0 and stdout.strip(): |
|
return stdout.strip() |
|
return None |
|
|
|
|
|
def check_if_detached(repo_path: Path) -> bool: |
|
"""Check if the repository is in detached HEAD state.""" |
|
code, stdout, _ = run_git_command(["symbolic-ref", "--short", "HEAD"], repo_path) |
|
return code != 0 |
|
|
|
|
|
def get_commit_counts(repo_path: Path) -> tuple[int, int]: |
|
"""Get ahead and behind commit counts relative to upstream.""" |
|
code, stdout, _ = run_git_command( |
|
["rev-list", "--left-right", "--count", "HEAD...@{u}"], repo_path |
|
) |
|
if code == 0: |
|
parts = stdout.strip().split() |
|
if len(parts) == 2: |
|
return int(parts[0]), int(parts[1]) |
|
return 0, 0 |
|
|
|
|
|
def get_status_files(repo_path: Path) -> tuple[list, list, list]: |
|
"""Get modified, untracked, and staged files.""" |
|
modified = [] |
|
untracked = [] |
|
staged = [] |
|
|
|
# Get porcelain status |
|
code, stdout, _ = run_git_command(["status", "--porcelain"], repo_path) |
|
|
|
if code == 0: |
|
for line in stdout.strip().split("\n"): |
|
if not line: |
|
continue |
|
status_code = line[:2] |
|
filepath = line[3:] |
|
|
|
if status_code[0] in "MADRC": # Staged |
|
staged.append(filepath) |
|
if status_code[1] in "MD": # Modified |
|
modified.append(filepath) |
|
if status_code == "??": # Untracked |
|
untracked.append(filepath) |
|
|
|
return modified, untracked, staged |
|
|
|
|
|
def has_unpushed_commits(repo_path: Path) -> bool: |
|
"""Check if there are unpushed commits.""" |
|
code, stdout, _ = run_git_command(["log", "@{u}..HEAD", "--oneline"], repo_path) |
|
return code == 0 and bool(stdout.strip()) |
|
|
|
|
|
def get_submodules(repo_path: Path) -> list[SubmoduleInfo]: |
|
"""Get list of submodules with their status.""" |
|
submodules = [] |
|
|
|
code, stdout, _ = run_git_command(["submodule", "status"], repo_path) |
|
|
|
if code != 0 or not stdout.strip(): |
|
return submodules |
|
|
|
for line in stdout.strip().split("\n"): |
|
if not line: |
|
continue |
|
|
|
# Parse: [+-U ]<commit> <path> (<branch>) |
|
prefix = line[0] if line[0] in "+-U " else "" |
|
if prefix in "+-U ": |
|
line = line[1:] |
|
|
|
parts = line.split(maxsplit=2) |
|
if len(parts) >= 2: |
|
commit = parts[0] |
|
path = parts[1] |
|
name = parts[2].strip("()") if len(parts) > 2 else path |
|
|
|
sub_path = repo_path / path |
|
|
|
sub_info = SubmoduleInfo( |
|
name=name, |
|
path=path, |
|
commit_hash=commit[:12], |
|
status_prefix=prefix, |
|
branch=get_current_branch(sub_path), |
|
has_changes=bool(prefix == "+"), |
|
is_detached=check_if_detached(sub_path), |
|
unpushed_commits=has_unpushed_commits(sub_path), |
|
) |
|
submodules.append(sub_info) |
|
|
|
return submodules |
|
|
|
|
|
def analyze_repository(repo_path: Path, is_submodule: bool = False) -> RepoInfo: |
|
"""Analyze a git repository and return its status.""" |
|
repo_info = RepoInfo(path=repo_path, name=repo_path.name, is_submodule=is_submodule) |
|
|
|
# Check if it's actually a git repo |
|
git_dir = repo_path / ".git" |
|
if not git_dir.exists(): |
|
# Might be a submodule with .git as file pointing elsewhere |
|
git_file = repo_path / ".git" |
|
if not git_file.exists(): |
|
repo_info.status = RepoStatus.ERROR |
|
repo_info.error_message = "Not a git repository" |
|
return repo_info |
|
|
|
# Get branch info |
|
repo_info.current_branch = get_current_branch(repo_path) |
|
repo_info.is_detached = check_if_detached(repo_path) |
|
|
|
# Get file status |
|
modified, untracked, staged = get_status_files(repo_path) |
|
repo_info.modified_files = modified |
|
repo_info.untracked_files = untracked |
|
repo_info.staged_files = staged |
|
|
|
# Get commit counts |
|
repo_info.ahead_count, repo_info.behind_count = get_commit_counts(repo_path) |
|
|
|
# Determine overall status |
|
if repo_info.error_message: |
|
repo_info.status = RepoStatus.ERROR |
|
elif repo_info.is_detached: |
|
repo_info.status = RepoStatus.DETACHED |
|
elif repo_info.ahead_count > 0 and repo_info.behind_count > 0: |
|
repo_info.status = RepoStatus.DIVERGED |
|
elif repo_info.ahead_count > 0: |
|
repo_info.status = RepoStatus.AHEAD |
|
elif repo_info.behind_count > 0: |
|
repo_info.status = RepoStatus.BEHIND |
|
elif modified or staged: |
|
repo_info.status = RepoStatus.MODIFIED |
|
elif untracked: |
|
repo_info.status = RepoStatus.UNTRACKED |
|
else: |
|
repo_info.status = RepoStatus.CLEAN |
|
|
|
# Get submodules |
|
if not is_submodule: # Don't recursively check submodules of submodules |
|
repo_info.submodules = get_submodules(repo_path) |
|
|
|
return repo_info |
|
|
|
|
|
def format_status(status: RepoStatus) -> str: |
|
"""Format status with color codes.""" |
|
status_colors = { |
|
RepoStatus.CLEAN: "[green]✓ Clean[/green]", |
|
RepoStatus.MODIFIED: "[yellow]✏ Modified[/yellow]", |
|
RepoStatus.UNTRACKED: "[blue]? Untracked files[/blue]", |
|
RepoStatus.AHEAD: "[cyan]↑ Ahead[/cyan]", |
|
RepoStatus.BEHIND: "[orange]↓ Behind[/orange]", |
|
RepoStatus.DIVERGED: "[red]⇵ Diverged[/red]", |
|
RepoStatus.DETACHED: "[magenta]⚠ Detached HEAD[/magenta]", |
|
RepoStatus.ERROR: "[red]✗ Error[/red]", |
|
} |
|
return status_colors.get(status, str(status)) |
|
|
|
|
|
def format_submodule_prefix(prefix: str) -> str: |
|
"""Format submodule status prefix.""" |
|
prefix_map = { |
|
"+": "[yellow]+[/yellow] Modified", |
|
"-": "[red]-[/red] Not initialized", |
|
"U": "[red]U[/red] Merge conflict", |
|
" ": "[green] [/green] Current", |
|
} |
|
return prefix_map.get(prefix, prefix) |
|
|
|
|
|
def print_repo_status(repo_info: RepoInfo, verbose: bool = False): |
|
"""Print repository status in a nice format.""" |
|
# Main info table |
|
table = Table(title=f"Repository: {repo_info.name}", show_header=True) |
|
table.add_column("Property", style="cyan") |
|
table.add_column("Value", style="white") |
|
|
|
table.add_row("Path", str(repo_info.path)) |
|
table.add_row( |
|
"Branch", repo_info.current_branch or "[magenta]Detached HEAD[/magenta]" |
|
) |
|
table.add_row("Status", format_status(repo_info.status)) |
|
|
|
if repo_info.ahead_count > 0: |
|
table.add_row("Ahead by", f"{repo_info.ahead_count} commit(s)") |
|
if repo_info.behind_count > 0: |
|
table.add_row("Behind by", f"{repo_info.behind_count} commit(s)") |
|
|
|
table.add_row("Modified files", str(len(repo_info.modified_files))) |
|
table.add_row("Staged files", str(len(repo_info.staged_files))) |
|
table.add_row("Untracked files", str(len(repo_info.untracked_files))) |
|
|
|
if repo_info.error_message: |
|
table.add_row("Error", f"[red]{repo_info.error_message}[/red]") |
|
|
|
console.print(table) |
|
|
|
# Detailed file lists |
|
if verbose: |
|
if repo_info.modified_files: |
|
console.print("\n[yellow]Modified files:[/yellow]") |
|
for f in repo_info.modified_files: |
|
console.print(f" • {f}") |
|
|
|
if repo_info.staged_files: |
|
console.print("\n[green]Staged files:[/green]") |
|
for f in repo_info.staged_files: |
|
console.print(f" • {f}") |
|
|
|
if repo_info.untracked_files: |
|
console.print("\n[blue]Untracked files:[/blue]") |
|
for f in repo_info.untracked_files: |
|
console.print(f" • {f}") |
|
|
|
|
|
def print_submodules(submodules: list[SubmoduleInfo], verbose: bool = False): |
|
"""Print submodule information.""" |
|
if not submodules: |
|
console.print("\n[dim]No submodules configured[/dim]") |
|
return |
|
|
|
console.print(f"\n[bold]Submodules ({len(submodules)}):[/bold]") |
|
|
|
table = Table(show_header=True) |
|
table.add_column("Name", style="cyan") |
|
table.add_column("Path", style="dim") |
|
table.add_column("Commit", style="white") |
|
table.add_column("Status", style="yellow") |
|
table.add_column("Branch", style="green") |
|
|
|
for sub in submodules: |
|
status = format_submodule_prefix(sub.status_prefix) |
|
if sub.is_detached: |
|
status += " [magenta](detached)[/magenta]" |
|
if sub.unpushed_commits: |
|
status += " [cyan](unpushed)[/cyan]" |
|
|
|
branch = sub.branch or "[magenta]detached[/magenta]" |
|
|
|
table.add_row(sub.name, sub.path, sub.commit_hash, status, branch) |
|
|
|
console.print(table) |
|
|
|
|
|
def print_recommendations(repo_info: RepoInfo): |
|
"""Print actionable recommendations based on repository status.""" |
|
recommendations = [] |
|
|
|
if repo_info.status == RepoStatus.ERROR: |
|
recommendations.append(("error", "Fix the repository error before proceeding")) |
|
|
|
if repo_info.status == RepoStatus.DETACHED: |
|
recommendations.append(("warning", "Repository is in detached HEAD state")) |
|
recommendations.append(("info", "Create a branch: git checkout -b my-branch")) |
|
|
|
if repo_info.modified_files: |
|
recommendations.append(("info", f"Stage changes: git add .")) |
|
recommendations.append( |
|
("info", f"Commit changes: git commit -m 'your message'") |
|
) |
|
|
|
if repo_info.ahead_count > 0: |
|
recommendations.append( |
|
("info", f"Push changes: git push origin {repo_info.current_branch}") |
|
) |
|
|
|
if repo_info.behind_count > 0: |
|
recommendations.append( |
|
("info", f"Pull latest: git pull origin {repo_info.current_branch}") |
|
) |
|
|
|
if repo_info.untracked_files: |
|
recommendations.append(("info", f"Add untracked files or update .gitignore")) |
|
|
|
# Submodule recommendations |
|
for sub in repo_info.submodules: |
|
if sub.status_prefix == "-": |
|
recommendations.append( |
|
("warning", f"Initialize {sub.name}: git submodule update --init") |
|
) |
|
if sub.status_prefix == "+": |
|
recommendations.append( |
|
("info", f"Stage {sub.name} update: git add {sub.path}") |
|
) |
|
if sub.is_detached: |
|
recommendations.append( |
|
("warning", f"{sub.name} is detached - create a branch to make changes") |
|
) |
|
|
|
if recommendations: |
|
console.print("\n[bold]Recommendations:[/bold]") |
|
for level, msg in recommendations: |
|
color = {"error": "red", "warning": "yellow", "info": "blue"}.get( |
|
level, "white" |
|
) |
|
console.print(f" [{color}]• {msg}[/{color}]") |
|
|
|
|
|
def output_json(repo_info: RepoInfo): |
|
"""Output repository status as JSON.""" |
|
data = { |
|
"name": repo_info.name, |
|
"path": str(repo_info.path), |
|
"is_submodule": repo_info.is_submodule, |
|
"branch": repo_info.current_branch, |
|
"status": repo_info.status.value, |
|
"is_detached": repo_info.is_detached, |
|
"files": { |
|
"modified": repo_info.modified_files, |
|
"staged": repo_info.staged_files, |
|
"untracked": repo_info.untracked_files, |
|
}, |
|
"commits": { |
|
"ahead": repo_info.ahead_count, |
|
"behind": repo_info.behind_count, |
|
}, |
|
"submodules": [ |
|
{ |
|
"name": sub.name, |
|
"path": sub.path, |
|
"commit": sub.commit_hash, |
|
"status_prefix": sub.status_prefix, |
|
"branch": sub.branch, |
|
"is_detached": sub.is_detached, |
|
"has_changes": sub.has_changes, |
|
"unpushed_commits": sub.unpushed_commits, |
|
} |
|
for sub in repo_info.submodules |
|
], |
|
} |
|
print(json.dumps(data, indent=2)) |
|
|
|
|
|
@app.command() |
|
def check( |
|
path: Optional[Path] = typer.Argument( |
|
None, |
|
help="Path to repository (default: current directory)", |
|
exists=True, |
|
file_okay=False, |
|
dir_okay=True, |
|
), |
|
verbose: bool = typer.Option( |
|
False, "--verbose", "-v", help="Show detailed information" |
|
), |
|
json_output: bool = typer.Option(False, "--json", "-j", help="Output as JSON"), |
|
no_recommendations: bool = typer.Option( |
|
False, "--no-recommendations", help="Skip recommendations" |
|
), |
|
): |
|
"""Check git status of a repository and its submodules.""" |
|
repo_path = path or Path.cwd() |
|
|
|
# Analyze repository |
|
repo_info = analyze_repository(repo_path) |
|
|
|
# Output |
|
if json_output: |
|
output_json(repo_info) |
|
return |
|
|
|
# Print header |
|
console.print( |
|
Panel.fit( |
|
f"[bold blue]Git Submodule Manager[/bold blue]\n" |
|
f"[dim]{repo_info.name}[/dim]", |
|
title="Status Check", |
|
border_style="blue", |
|
) |
|
) |
|
|
|
# Print status |
|
print_repo_status(repo_info, verbose) |
|
print_submodules(repo_info.submodules, verbose) |
|
|
|
if not no_recommendations: |
|
print_recommendations(repo_info) |
|
|
|
# Summary |
|
console.print( |
|
f"\n[dim]Checked {1 + len(repo_info.submodules)} repository(ies)[/dim]" |
|
) |
|
|
|
|
|
@app.command() |
|
def list_submodules( |
|
path: Optional[Path] = typer.Argument( |
|
None, |
|
help="Path to repository (default: current directory)", |
|
exists=True, |
|
file_okay=False, |
|
dir_okay=True, |
|
), |
|
): |
|
"""List all submodules in the repository.""" |
|
repo_path = path or Path.cwd() |
|
submodules = get_submodules(repo_path) |
|
|
|
if not submodules: |
|
console.print("[yellow]No submodules found[/yellow]") |
|
return |
|
|
|
console.print(f"[bold]Found {len(submodules)} submodule(s):[/bold]\n") |
|
|
|
for sub in submodules: |
|
console.print(f" [cyan]{sub.name}[/cyan]") |
|
console.print(f" Path: {sub.path}") |
|
console.print(f" Commit: {sub.commit_hash}") |
|
console.print(f" Status: {format_submodule_prefix(sub.status_prefix)}") |
|
if sub.branch: |
|
console.print(f" Branch: {sub.branch}") |
|
console.print() |
|
|
|
|
|
@app.command() |
|
def quick( |
|
path: Optional[Path] = typer.Argument( |
|
None, |
|
help="Path to repository (default: current directory)", |
|
), |
|
): |
|
"""Quick status check - shows only dirty repositories.""" |
|
repo_path = path or Path.cwd() |
|
repo_info = analyze_repository(repo_path) |
|
|
|
dirty = [] |
|
|
|
if repo_info.status != RepoStatus.CLEAN: |
|
dirty.append((repo_info.name, repo_info.status.value)) |
|
|
|
for sub in repo_info.submodules: |
|
if sub.status_prefix == "+" or sub.unpushed_commits or sub.is_detached: |
|
dirty.append( |
|
(sub.name, "modified" if sub.status_prefix == "+" else "unpushed") |
|
) |
|
|
|
if dirty: |
|
console.print("[yellow]Dirty repositories:[/yellow]") |
|
for name, status in dirty: |
|
console.print(f" • {name}: {status}") |
|
else: |
|
console.print("[green]All repositories are clean ✓[/green]") |
|
|
|
|
|
@app.command() |
|
def workflow_pull( |
|
path: Optional[Path] = typer.Argument( |
|
None, |
|
help="Path to repository (default: current directory)", |
|
), |
|
): |
|
"""Execute the recommended pull workflow: fetch all, pull parent, update submodules.""" |
|
repo_path = path or Path.cwd() |
|
|
|
console.print( |
|
Panel.fit("[bold blue]Git Pull Workflow[/bold blue]", border_style="blue") |
|
) |
|
|
|
# Step 1: Fetch all remotes |
|
console.print("\n[bold]Step 1:[/bold] Fetching all remotes...") |
|
code, _, stderr = run_git_command(["fetch", "--all"], repo_path) |
|
if code != 0: |
|
console.print(f"[red]✗ Failed to fetch: {stderr}[/red]") |
|
return |
|
|
|
for sub in get_submodules(repo_path): |
|
sub_path = repo_path / sub.path |
|
run_git_command(["fetch", "--all"], sub_path) |
|
console.print("[green]✓ Fetch complete[/green]") |
|
|
|
# Step 2: Pull parent |
|
console.print("\n[bold]Step 2:[/bold] Pulling parent repository...") |
|
branch = get_current_branch(repo_path) or "main" |
|
code, _, stderr = run_git_command(["pull", "origin", branch], repo_path) |
|
if code != 0: |
|
console.print(f"[red]✗ Failed to pull: {stderr}[/red]") |
|
return |
|
console.print("[green]✓ Parent repository updated[/green]") |
|
|
|
# Step 3: Update submodules |
|
console.print("\n[bold]Step 3:[/bold] Updating submodules...") |
|
code, _, stderr = run_git_command( |
|
["submodule", "update", "--init", "--recursive"], repo_path |
|
) |
|
if code != 0: |
|
console.print(f"[red]✗ Failed to update submodules: {stderr}[/red]") |
|
return |
|
console.print("[green]✓ Submodules updated[/green]") |
|
|
|
console.print("\n[bold green]✓ Pull workflow complete![/bold green]") |
|
|
|
|
|
@app.command() |
|
def workflow_push( |
|
path: Optional[Path] = typer.Argument( |
|
None, |
|
help="Path to repository (default: current directory)", |
|
), |
|
message: Optional[str] = typer.Option( |
|
None, |
|
"--message", |
|
"-m", |
|
help="Commit message (optional, will prompt if not provided)", |
|
), |
|
): |
|
"""Execute the recommended push workflow: commit submodules first, then parent.""" |
|
repo_path = path or Path.cwd() |
|
|
|
console.print( |
|
Panel.fit("[bold blue]Git Push Workflow[/bold blue]", border_style="blue") |
|
) |
|
|
|
# Get commit message if not provided |
|
commit_msg = message |
|
if not commit_msg: |
|
console.print("\n[yellow]Please provide a commit message:[/yellow]") |
|
commit_msg = input("> ").strip() |
|
if not commit_msg: |
|
console.print("[red]✗ Commit message is required[/red]") |
|
raise typer.Exit(1) |
|
|
|
# Step 1: Stage changes |
|
console.print("\n[bold]Step 1:[/bold] Staging changes...") |
|
for sub in get_submodules(repo_path): |
|
sub_path = repo_path / sub.path |
|
run_git_command(["add", "-A"], sub_path) |
|
run_git_command(["add", "-A"], repo_path) |
|
console.print("[green]✓ Changes staged[/green]") |
|
|
|
# Step 2: Commit submodule changes |
|
console.print("\n[bold]Step 2:[/bold] Committing submodule changes...") |
|
for sub in get_submodules(repo_path): |
|
sub_path = repo_path / sub.path |
|
# Check if there are changes to commit |
|
code, stdout, _ = run_git_command(["status", "--porcelain"], sub_path) |
|
if stdout.strip(): |
|
run_git_command(["commit", "-m", f"Update {sub.name}"], sub_path) |
|
console.print(f" [green]✓ Committed {sub.name}[/green]") |
|
|
|
# Step 3: Push submodules |
|
console.print("\n[bold]Step 3:[/bold] Pushing submodule changes...") |
|
for sub in get_submodules(repo_path): |
|
sub_path = repo_path / sub.path |
|
branch = get_current_branch(sub_path) |
|
if branch: |
|
code, _, _ = run_git_command(["push", "origin", branch], sub_path) |
|
if code == 0: |
|
console.print(f" [green]✓ Pushed {sub.name}[/green]") |
|
|
|
# Step 4: Stage and commit parent |
|
console.print("\n[bold]Step 4:[/bold] Committing parent repository...") |
|
run_git_command(["add", "-A"], repo_path) |
|
code, _, _ = run_git_command(["commit", "-m", commit_msg], repo_path) |
|
if code == 0: |
|
console.print("[green]✓ Parent repository committed[/green]") |
|
|
|
# Step 5: Push parent |
|
console.print("\n[bold]Step 5:[/bold] Pushing parent repository...") |
|
branch = get_current_branch(repo_path) or "main" |
|
code, _, _ = run_git_command(["push", "origin", branch], repo_path) |
|
if code == 0: |
|
console.print("[green]✓ Parent repository pushed[/green]") |
|
|
|
console.print("\n[bold green]✓ Push workflow complete![/bold green]") |
|
|
|
|
|
if __name__ == "__main__": |
|
app() |