feat: Add --diff-additions and --diff-deletions options with line highlighting

This commit is contained in:
n loewen (aider) 2025-06-07 19:52:45 +01:00
parent 77ecf27131
commit b5c852e544
1 changed files with 124 additions and 21 deletions

145
gtm
View File

@ -4,6 +4,7 @@ import curses
import os
import subprocess
import sys
import argparse
VERSION = "2025-06-07"
@ -17,7 +18,55 @@ def get_file_at_commit(commit_hash, filename):
result = subprocess.run(cmd, capture_output=True, text=True)
return result.stdout.splitlines()
def main(stdscr, filename):
def get_diff_info(current_commit, prev_commit, filename):
"""Get diff information between two commits for a file"""
if not prev_commit:
# For the first commit, we can't compare with a previous one
return [], []
# Get additions
cmd_additions = ['git', 'diff', '--unified=0', prev_commit, current_commit, '--', filename]
result_additions = subprocess.run(cmd_additions, capture_output=True, text=True)
added_lines = []
deleted_lines = []
for line in result_additions.stdout.splitlines():
# Parse the diff output to find line numbers of additions and deletions
if line.startswith('@@'):
# Parse the hunk header to get line numbers
parts = line.split()
if len(parts) >= 3:
# Format is like @@ -a,b +c,d @@
# We're interested in the +c,d part for additions
add_part = parts[2][1:] # Remove the + sign
if ',' in add_part:
start_line, count = map(int, add_part.split(','))
else:
start_line, count = int(add_part), 1
# For deletions, we look at the -a,b part
del_part = parts[1][1:] # Remove the - sign
if ',' in del_part:
del_start, del_count = map(int, del_part.split(','))
else:
del_start, del_count = int(del_part), 1
elif line.startswith('+') and not line.startswith('+++'):
# This is an added line
added_lines.append((start_line, line[1:]))
start_line += 1
elif line.startswith('-') and not line.startswith('---'):
# This is a deleted line
deleted_lines.append((del_start, line[1:]))
del_start += 1
elif not line.startswith('+') and not line.startswith('-') and not line.startswith('@@'):
# Context line, increment both counters
start_line += 1
del_start += 1
return added_lines, deleted_lines
def main(stdscr, filename, show_additions=False, show_deletions=False):
curses.curs_set(0)
curses.mousemask(curses.ALL_MOUSE_EVENTS)
curses.mouseinterval(0)
@ -28,6 +77,8 @@ def main(stdscr, filename):
curses.use_default_colors() # Use terminal's default colors
curses.init_pair(1, 5, 7) # Focused status bar
curses.init_pair(2, curses.COLOR_WHITE, 8) # Unfocused status bar
curses.init_pair(3, curses.COLOR_GREEN, -1) # Added lines
curses.init_pair(4, curses.COLOR_RED, -1) # Deleted lines
# Initialize key variable
key = 0
@ -45,6 +96,17 @@ def main(stdscr, filename):
commit_hash = commits[selected_commit].split()[0]
file_lines = get_file_at_commit(commit_hash, filename)
# Get previous commit hash for diff
prev_commit_hash = None
if selected_commit < len(commits) - 1:
prev_commit_hash = commits[selected_commit + 1].split()[0]
# Get diff information if needed
added_lines = []
deleted_lines = []
if show_additions or show_deletions:
added_lines, deleted_lines = get_diff_info(commit_hash, prev_commit_hash, filename)
# Enable nodelay for smoother scrolling
stdscr.nodelay(True)
@ -57,6 +119,14 @@ def main(stdscr, filename):
commit_hash = commits[selected_commit].split()[0]
file_lines = get_file_at_commit(commit_hash, filename)
# Update diff information when commit changes
prev_commit_hash = None
if selected_commit < len(commits) - 1:
prev_commit_hash = commits[selected_commit + 1].split()[0]
if show_additions or show_deletions:
added_lines, deleted_lines = get_diff_info(commit_hash, prev_commit_hash, filename)
max_scroll = max(0, len(file_lines) - (height - 1))
scroll_offset = min(scroll_offset, max_scroll)
visible_lines = file_lines[scroll_offset:scroll_offset + height - 1]
@ -95,7 +165,33 @@ def main(stdscr, filename):
for i, line in enumerate(visible_lines):
# Only draw what fits in the window
if i < height - 1:
stdscr.addnstr(i, divider_col + 2, line, right_width)
line_num = i + scroll_offset + 1 # 1-based line number
# Check if this is an added line
is_added = False
if show_additions:
for added_line_num, _ in added_lines:
if added_line_num == line_num:
is_added = True
break
# Display the line with appropriate formatting
if is_added:
stdscr.addstr(i, divider_col + 2, "+ ", curses.color_pair(3))
stdscr.addnstr(i, divider_col + 4, line, right_width - 2, curses.color_pair(3))
else:
stdscr.addnstr(i, divider_col + 2, line, right_width)
# Display deleted lines if enabled
if show_deletions:
# Check if there are deleted lines after the current line
for del_line_num, del_line_content in deleted_lines:
if del_line_num == line_num:
# Only show if we have space
if i + 1 < height - 1:
i += 1 # Move to next line for displaying the deleted content
stdscr.addstr(i, divider_col + 2, "- ", curses.color_pair(4))
stdscr.addnstr(i, divider_col + 4, del_line_content, right_width - 2, curses.color_pair(4))
# Status bars for both panes
visible_height = height - 1 # Reserve 1 line for the status bar
@ -226,30 +322,37 @@ def show_help():
print("A \"Git Time Machine\" for viewing file history.")
print()
print("Usage:")
print("\tgtm FILENAME")
print("\tgtm [OPTIONS] FILENAME")
print()
print("Flags:")
print("Options:")
print("\t--diff-additions\thighlight newly added lines in green")
print("\t--diff-deletions\tshow deleted lines in red")
print("\t-v, --version\tshow version number")
print("\t-h, --help\tshow this help")
if __name__ == "__main__":
if len(sys.argv) == 2:
if sys.argv[1] == "-v" or sys.argv[1] == "--version":
print(f"gtm version {VERSION}")
sys.exit(0)
elif sys.argv[1] == "-h" or sys.argv[1] == "--help":
show_help()
sys.exit(0)
filename = sys.argv[1]
# Check if the file exists
if not os.path.isfile(filename):
print(f"Error: File '{filename}' does not exist")
sys.exit(1)
curses.wrapper(main, filename)
else:
parser = argparse.ArgumentParser(add_help=False, description="A Git Time Machine for viewing file history.")
parser.add_argument("--diff-additions", action="store_true", help="Highlight newly added lines in green")
parser.add_argument("--diff-deletions", action="store_true", help="Show deleted lines in red")
parser.add_argument("-v", "--version", action="store_true", help="Show version number")
parser.add_argument("-h", "--help", action="store_true", help="Show help information")
parser.add_argument("filename", nargs="?", help="File to view history for")
args = parser.parse_args()
if args.version:
print(f"gtm version {VERSION}")
sys.exit(0)
elif args.help or not args.filename:
show_help()
sys.exit(0 if args.help else 1)
filename = args.filename
# Check if the file exists
if not os.path.isfile(filename):
print(f"Error: File '{filename}' does not exist")
sys.exit(1)
curses.wrapper(main, filename, args.diff_additions, args.diff_deletions)