diff --git a/gtm b/gtm index 3ea7ecf..c44cf2d 100755 --- a/gtm +++ b/gtm @@ -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)