Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 95 additions & 55 deletions git-context
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ git-context — Generate AI-friendly context for any git repo.
Dump project structure, git log, file contents, and branch topology
in one optimized prompt-ready block.

Usage: git context [--depth N] [--files] [--log N] [--output file] [--dir <path>]
Usage: git context [--depth N] [--files] [--log N] [--output file] [--dir <path>] [--json]
"""

import argparse
Expand All @@ -14,6 +14,7 @@ import sys
import fnmatch
from pathlib import Path
from datetime import datetime
import json

DEFAULT_IGNORE = {
'.git', 'node_modules', '.next', 'dist', 'build', 'target',
Expand Down Expand Up @@ -67,7 +68,7 @@ def tree(path, prefix="", ignored=DEFAULT_IGNORE, depth=3, current_depth=0):
result += tree(fp, prefix + deeper, ignored, depth, current_depth + 1)
return result

def file_contents(path, ignored=DEFAULT_IGNORE, max_total=15000):
def file_contents(path, ignored=DEFAULT_IGNORE, max_total=15000, as_json=False):
ext_map = {
'.py': 'py', '.js': 'js', '.ts': 'ts', '.tsx': 'tsx', '.jsx': 'jsx',
'.go': 'go', '.rs': 'rs', '.rb': 'rb', '.java': 'java', '.kt': 'kt',
Expand All @@ -86,7 +87,8 @@ def file_contents(path, ignored=DEFAULT_IGNORE, max_total=15000):
'.json', '.yaml', '.yml', '.toml', '.md', '.sh', '.bash',
'.zsh', '.sql', '.graphql', '.proto', '.tf', '.conf', '.ini'}

result = ""
result_str = ""
result_list = []
total = 0
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if not d.startswith('.') and d not in ignored and d != 'node_modules']
Expand All @@ -100,19 +102,27 @@ def file_contents(path, ignored=DEFAULT_IGNORE, max_total=15000):
try:
content = Path(fp).read_text(encoding='utf-8', errors='replace')
rel = os.path.relpath(fp, path)
block = f"\n--- {rel} ---\n```{ext_map.get(ext, '')}\n{content.strip()}\n```\n"
if total + len(block) > max_total:
remaining = max_total - total
result += block[:remaining] + f"\n... (truncated, more files in {rel})"
total = max_total
break
result += block
total += len(block)

if as_json:
if total + len(content) > max_total:
continue
result_list.append({"path": rel, "content": content.strip()})
total += len(content)
else:
block = f"\n--- {rel} ---\n```{ext_map.get(ext, '')}\n{content.strip()}\n```\n"
if total + len(block) > max_total:
remaining = max_total - total
result_str += block[:remaining] + f"\n... (truncated, more files in {rel})"
total = max_total
break
result_str += block
total += len(block)
except Exception:
continue
if total >= max_total:
break
return result

return result_list if as_json else result_str

def fmt_timestamp(ts):
try:
Expand All @@ -128,6 +138,7 @@ def main():
p.add_argument('--log', type=int, default=20, help='Number of recent commits (default: 20, 0=skip)')
p.add_argument('--output', '-o', help='Write to file instead of stdout')
p.add_argument('--dir', default=os.getcwd(), help='Target directory (default: cwd)')
p.add_argument('--json', action='store_true', help='Output in JSON format')
args = p.parse_args()

target = os.path.abspath(args.dir)
Expand All @@ -136,54 +147,83 @@ def main():
sys.exit(1)

repo_name = os.path.basename(target)
sections = []
sections.append(f"# git-context: {repo_name}")
sections.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
sections.append(f"Path: {target}")
sections.append("")


# Git info
branch = run(["git", "rev-parse", "--abbrev-ref", "HEAD"], target)
remote = run(["git", "remote", "get-url", "origin"], target)
sections.append(f"## Git Info\n- Branch: `{branch}`")
sections.append(f"- Remote: {remote}")

has_unstaged = run(["git", "diff", "--stat"], target)
has_staged = run(["git", "diff", "--cached", "--stat"], target)
status = ""
if has_unstaged: status += f"\n- Unstaged changes: {has_unstaged.split(chr(10))[-1]}"
if has_staged: status += f"\n- Staged changes: {has_staged.split(chr(10))[-1]}"
if not has_unstaged and not has_staged:
status += "\n- Working tree: clean"
sections.append(status)

# Recent commits
if args.log > 0:
log = run(["git", "log", f"--max-count={args.log}", "--oneline", "--graph",
"--pretty=format:%h %d %s (%an, %ar)"], target)
if log:
sections.append(f"\n## Recent Commits (last {args.log})")
sections.append(f"```\n{log}\n```")

# Branch topology
branches = run(["git", "branch", "-a"], target)
if branches:
sections.append("\n## Branches")
sections.append(f"```\n{branches}\n```")

# Directory tree
tree_out = tree(target, ignored=DEFAULT_IGNORE, depth=args.depth)
sections.append(f"\n## Project Structure (depth={args.depth})")
sections.append(f"```\n{tree_out}\n```")

# File contents
if args.files:
contents = file_contents(target)
if contents:
sections.append("\n## File Contents")
sections.append(contents)

output = "\n".join(sections)

if args.json:
out_data = {
"repo_name": repo_name,
"generated_at": datetime.now().isoformat(),
"path": target,
"git_info": {
"branch": branch,
"remote": remote,
"unstaged_changes": has_unstaged if has_unstaged else None,
"staged_changes": has_staged if has_staged else None,
"clean": not has_unstaged and not has_staged
}
}
if args.log > 0:
log_str = run(["git", "log", f"--max-count={args.log}", "--oneline", "--graph", "--pretty=format:%h %d %s (%an, %ar)"], target)
out_data["recent_commits"] = log_str if log_str else None

branches = run(["git", "branch", "-a"], target)
out_data["branches"] = branches if branches else None

out_data["project_structure"] = tree(target, ignored=DEFAULT_IGNORE, depth=args.depth)

if args.files:
out_data["file_contents"] = file_contents(target, as_json=True)

output = json.dumps(out_data, indent=2)
else:
sections = []
sections.append(f"# git-context: {repo_name}")
sections.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
sections.append(f"Path: {target}")
sections.append("")

sections.append(f"## Git Info\n- Branch: `{branch}`")
sections.append(f"- Remote: {remote}")

status = ""
if has_unstaged: status += f"\n- Unstaged changes: {has_unstaged.split(chr(10))[-1]}"
if has_staged: status += f"\n- Staged changes: {has_staged.split(chr(10))[-1]}"
if not has_unstaged and not has_staged:
status += "\n- Working tree: clean"
sections.append(status)

# Recent commits
if args.log > 0:
log = run(["git", "log", f"--max-count={args.log}", "--oneline", "--graph",
"--pretty=format:%h %d %s (%an, %ar)"], target)
if log:
sections.append(f"\n## Recent Commits (last {args.log})")
sections.append(f"```\n{log}\n```")

# Branch topology
branches = run(["git", "branch", "-a"], target)
if branches:
sections.append("\n## Branches")
sections.append(f"```\n{branches}\n```")

# Directory tree
tree_out = tree(target, ignored=DEFAULT_IGNORE, depth=args.depth)
sections.append(f"\n## Project Structure (depth={args.depth})")
sections.append(f"```\n{tree_out}\n```")

# File contents
if args.files:
contents = file_contents(target)
if contents:
sections.append("\n## File Contents")
sections.append(contents)

output = "\n".join(sections)

if args.output:
Path(args.output).write_text(output)
Expand Down