|
#!/usr/bin/env python3 |
|
""" |
|
Git Branch Age Report Generator |
|
Generates formatted reports showing the age of all remote branches |
|
""" |
|
|
|
import subprocess |
|
import sys |
|
import csv |
|
import argparse |
|
from datetime import datetime |
|
from io import StringIO |
|
|
|
|
|
def run_git_command(command): |
|
"""Execute a git command and return the output""" |
|
try: |
|
result = subprocess.run( |
|
command.split(), |
|
capture_output=True, |
|
text=True, |
|
check=True |
|
) |
|
return result.stdout.strip() |
|
except subprocess.CalledProcessError as e: |
|
print(f"Git command failed: {e}", file=sys.stderr) |
|
return None |
|
except FileNotFoundError: |
|
print("Error: git command not found", file=sys.stderr) |
|
return None |
|
|
|
|
|
def check_git_repo(): |
|
"""Check if we're in a git repository""" |
|
result = run_git_command("git rev-parse --git-dir") |
|
return result is not None |
|
|
|
|
|
def get_branch_data(sort_newest=False): |
|
"""Get branch data from git""" |
|
sort_param = "-committerdate" if sort_newest else "committerdate" |
|
|
|
command = (f"git for-each-ref --sort={sort_param} " |
|
f"--format=%(refname:short)|%(committerdate:relative)|" |
|
f"%(committerdate:short)|%(committerdate:iso) refs/remotes") |
|
|
|
output = run_git_command(command) |
|
if not output: |
|
return [] |
|
|
|
branches = [] |
|
for line in output.split('\n'): |
|
if not line or '/HEAD' in line: |
|
continue |
|
|
|
parts = line.split('|') |
|
if len(parts) >= 4: |
|
branch = parts[0].replace('origin/', '') # Clean branch name |
|
relative_date = parts[1] |
|
short_date = parts[2] |
|
iso_date = parts[3] |
|
|
|
branches.append({ |
|
'branch': branch, |
|
'relative_date': relative_date, |
|
'short_date': short_date, |
|
'iso_date': iso_date |
|
}) |
|
|
|
return branches |
|
|
|
|
|
def output_table(branches, sort_newest=False): |
|
"""Output formatted table""" |
|
if not branches: |
|
print("No remote branches found.") |
|
return |
|
|
|
# Calculate column widths |
|
max_branch_len = max(len(b['branch']) for b in branches) |
|
max_relative_len = max(len(b['relative_date']) for b in branches) |
|
max_date_len = max(len(b['short_date']) for b in branches) |
|
|
|
# Ensure minimum widths for headers |
|
branch_width = max(max_branch_len, len('Branch')) |
|
relative_width = max(max_relative_len, len('Last Activity')) |
|
date_width = max(max_date_len, len('Date')) |
|
|
|
# Print header |
|
print(f"Git Branch Age Report - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") |
|
print(f"Sort order: {'newest first' if sort_newest else 'oldest first'}") |
|
print() |
|
|
|
# Print table header with proper spacing |
|
header_format = f"{{:<{branch_width}}} | {{:<{relative_width}}} | {{:<{date_width}}}" |
|
separator_format = f"{{:-<{branch_width}}}-+-{{:-<{relative_width}}}-+-{{:-<{date_width}}}" |
|
|
|
print(header_format.format('Branch', 'Last Activity', 'Date')) |
|
print(separator_format.format('', '', '')) |
|
|
|
# Print data rows |
|
for branch_data in branches: |
|
print(header_format.format( |
|
branch_data['branch'], |
|
branch_data['relative_date'], |
|
branch_data['short_date'] |
|
)) |
|
|
|
|
|
def output_markdown(branches, sort_newest=False): |
|
"""Output markdown table""" |
|
print("# Git Branch Age Report") |
|
print() |
|
print(f"Report generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") |
|
print(f"Sort order: {'newest first' if sort_newest else 'oldest first'}") |
|
print() |
|
|
|
if not branches: |
|
print("No remote branches found.") |
|
return |
|
|
|
print("| Branch | Last Activity | Date |") |
|
print("|--------|---------------|------|") |
|
|
|
for branch_data in branches: |
|
print(f"| `{branch_data['branch']}` | {branch_data['relative_date']} | {branch_data['short_date']} |") |
|
|
|
print() |
|
print("---") |
|
print("*Generated with git for-each-ref*") |
|
|
|
|
|
def output_csv(branches): |
|
"""Output CSV format""" |
|
if not branches: |
|
print("branch,last_activity,date,iso_date") |
|
return |
|
|
|
output = StringIO() |
|
writer = csv.writer(output) |
|
|
|
# Write header |
|
writer.writerow(['branch', 'last_activity', 'date', 'iso_date']) |
|
|
|
# Write data |
|
for branch_data in branches: |
|
writer.writerow([ |
|
branch_data['branch'], |
|
branch_data['relative_date'], |
|
branch_data['short_date'], |
|
branch_data['iso_date'] |
|
]) |
|
|
|
print(output.getvalue().strip()) |
|
|
|
|
|
def main(): |
|
parser = argparse.ArgumentParser( |
|
description='Generate reports showing the age of git remote branches' |
|
) |
|
|
|
# Sort options |
|
sort_group = parser.add_mutually_exclusive_group() |
|
sort_group.add_argument( |
|
'-n', '--newest', |
|
action='store_true', |
|
help='Sort by newest branches first (default: oldest first)' |
|
) |
|
sort_group.add_argument( |
|
'-o', '--oldest', |
|
action='store_true', |
|
help='Sort by oldest branches first (default)' |
|
) |
|
|
|
# Output format options |
|
format_group = parser.add_mutually_exclusive_group() |
|
format_group.add_argument( |
|
'-t', '--table', |
|
action='store_true', |
|
default=True, |
|
help='Output as formatted table (default)' |
|
) |
|
format_group.add_argument( |
|
'-m', '--markdown', |
|
action='store_true', |
|
help='Output as markdown table' |
|
) |
|
format_group.add_argument( |
|
'-c', '--csv', |
|
action='store_true', |
|
help='Output as CSV' |
|
) |
|
|
|
args = parser.parse_args() |
|
|
|
# Check if we're in a git repository |
|
if not check_git_repo(): |
|
print("Error: Not in a git repository", file=sys.stderr) |
|
sys.exit(1) |
|
|
|
# Get branch data |
|
sort_newest = args.newest |
|
branches = get_branch_data(sort_newest) |
|
|
|
# Output in requested format |
|
if args.csv: |
|
output_csv(branches) |
|
elif args.markdown: |
|
output_markdown(branches, sort_newest) |
|
else: # default table format |
|
output_table(branches, sort_newest) |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |