diff --git a/git_integration.c b/git_integration.c index 4c4b6e8..a92cd9f 100644 --- a/git_integration.c +++ b/git_integration.c @@ -256,29 +256,29 @@ int check_branch_divergence(int *commits_ahead, int *commits_behind) { if (!commits_ahead || !commits_behind) { return 0; } - + *commits_ahead = 0; *commits_behind = 0; - + // Check if we have a remote tracking branch FILE *fp = popen("git rev-parse --abbrev-ref @{u} 2>/dev/null", "r"); if (!fp) { return 0; } - + char remote_branch[256]; if (fgets(remote_branch, sizeof(remote_branch), fp) == NULL) { pclose(fp); return 0; // No remote tracking branch } pclose(fp); - + // Remove newline char *newline = strchr(remote_branch, '\n'); if (newline) { *newline = '\0'; } - + // Get commits ahead (local commits not in remote) fp = popen("git rev-list --count @{u}..HEAD 2>/dev/null", "r"); if (fp) { @@ -288,7 +288,7 @@ int check_branch_divergence(int *commits_ahead, int *commits_behind) { } pclose(fp); } - + // Get commits behind (remote commits not in local) fp = popen("git rev-list --count HEAD..@{u} 2>/dev/null", "r"); if (fp) { @@ -298,7 +298,64 @@ int check_branch_divergence(int *commits_ahead, int *commits_behind) { } pclose(fp); } - + // Branch has diverged if we have commits both ahead and behind return (*commits_ahead > 0 && *commits_behind > 0) ? 1 : 0; } + +int create_git_stash(void) { + FILE *fp = popen("git status --porcelain 2>/dev/null", "r"); + if (!fp) + return 0; + + char line[256]; + int has_changes = 0; + if (fgets(line, sizeof(line), fp) != NULL) { + has_changes = 1; + } + pclose(fp); + + if (!has_changes) { + return 0; + } + + time_t now = time(NULL); + struct tm *timeinfo = localtime(&now); + + char cmd[512]; + snprintf(cmd, sizeof(cmd), "git stash push -m \"WIP: stashed at %04d-%02d-%02d %02d:%02d:%02d\" 2>/dev/null >/dev/null", + timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday, + timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec); + + int result = system(cmd); + return (result == 0) ? 1 : 0; +} + + +int get_git_stashes(char stashes[][512], int max_stashes) { + if (!stashes || max_stashes <= 0) { + return 0; + } + + FILE *fp = popen("git stash list --format=\"%gd: %gs\" 2>/dev/null", "r"); + if (!fp) { + return 0; + } + + int count = 0; + char line[512]; + + while (fgets(line, sizeof(line), fp) != NULL && count < max_stashes) { + char *newline = strchr(line, '\n'); + if (newline) { + *newline = '\0'; + } + + strncpy(stashes[count], line, 511); + stashes[count][511] = '\0'; + count++; + } + + pclose(fp); + return count; +} diff --git a/git_integration.h b/git_integration.h index 0e784b7..00b1ed7 100644 --- a/git_integration.h +++ b/git_integration.h @@ -51,9 +51,13 @@ char *get_git_status(void); * Check if the current branch has diverged from its remote tracking branch * * @param commits_ahead Pointer to store number of commits ahead of remote - * @param commits_behind Pointer to store number of commits behind remote + * @param commits_behind Pointer to store number of commits behind remote * @return 1 if diverged (both ahead and behind > 0), 0 otherwise */ int check_branch_divergence(int *commits_ahead, int *commits_behind); +int create_git_stash(void); + +int get_git_stashes(char stashed[][512], int max_stashes); + #endif // GIT_INTEGRATION_H diff --git a/git_integration.o b/git_integration.o index 6d09b9b..210897b 100644 Binary files a/git_integration.o and b/git_integration.o differ diff --git a/ncurses_diff_viewer.c b/ncurses_diff_viewer.c index 1c31489..eb1c331 100644 --- a/ncurses_diff_viewer.c +++ b/ncurses_diff_viewer.c @@ -62,19 +62,23 @@ int init_ncurses_diff_viewer(NCursesDiffViewer *viewer) { int available_height = viewer->terminal_height - 1 - viewer->status_bar_height; // Subtract top bar and status bar - viewer->file_panel_height = available_height * 0.6; // 60% of available height - viewer->commit_panel_height = - available_height - viewer->file_panel_height - 1; // Rest of height + viewer->file_panel_height = available_height * 0.4; + viewer->commit_panel_height = available_height * 0.4; + viewer->stash_panel_height = available_height - viewer->file_panel_height - + viewer->commit_panel_height - 2; // Position status bar right after the main content int status_bar_y = 1 + available_height; - // Create four windows + // Create five windows viewer->file_list_win = newwin(viewer->file_panel_height, viewer->file_panel_width, 1, 0); viewer->commit_list_win = newwin(viewer->commit_panel_height, viewer->file_panel_width, 1 + viewer->file_panel_height + 1, 0); + viewer->stash_list_win = newwin( + viewer->stash_panel_height, viewer->file_panel_width, + 1 + viewer->file_panel_height + 1 + viewer->commit_panel_height + 1, 0); viewer->file_content_win = newwin( available_height, viewer->terminal_width - viewer->file_panel_width - 1, 1, viewer->file_panel_width + 1); @@ -82,12 +86,13 @@ int init_ncurses_diff_viewer(NCursesDiffViewer *viewer) { viewer->terminal_width, status_bar_y, 0); if (!viewer->file_list_win || !viewer->file_content_win || - !viewer->commit_list_win || !viewer->status_bar_win) { - cleanup_ncurses_diff_viewer(viewer); - return 0; - } + !viewer->commit_list_win || !viewer->stash_list_win || + !viewer->status_bar_win) { + cleanup_ncurses_diff_viewer(viewer); + return 0; + } - return 1; + return 1; } /** @@ -514,29 +519,30 @@ int show_diverged_branch_dialog(int commits_ahead, int commits_behind) { // Draw dialog wattron(dialog_win, COLOR_PAIR(3)); // Red for warning box(dialog_win, 0, 0); - + // Title mvwprintw(dialog_win, 1, 2, "Branch has diverged!"); - + // Message mvwprintw(dialog_win, 3, 2, "Local: %d commit(s) ahead", commits_ahead); mvwprintw(dialog_win, 4, 2, "Remote: %d commit(s) ahead", commits_behind); - + // Instructions mvwprintw(dialog_win, 6, 2, "Force push anyway? (y/N):"); wattroff(dialog_win, COLOR_PAIR(3)); - + wrefresh(dialog_win); // Get user input int ch; int result = 0; - + while ((ch = wgetch(dialog_win)) != ERR) { if (ch == 'y' || ch == 'Y') { result = 1; break; - } else if (ch == 'n' || ch == 'N' || ch == 27 || ch == 'q') { // ESC or q or n + } else if (ch == 'n' || ch == 'N' || ch == 27 || + ch == 'q') { // ESC or q or n result = 0; break; } else if (ch == '\n' || ch == '\r') { @@ -552,7 +558,7 @@ int show_diverged_branch_dialog(int commits_ahead, int commits_behind) { wrefresh(saved_screen); delwin(saved_screen); } - + return result; } @@ -580,35 +586,35 @@ int show_reset_confirmation_dialog(void) { char input_buffer[10] = ""; int input_pos = 0; - + while (1) { // Clear and redraw dialog werase(dialog_win); wattron(dialog_win, COLOR_PAIR(3)); // Red for warning box(dialog_win, 0, 0); - + // Title mvwprintw(dialog_win, 1, 2, "HARD RESET WARNING!"); - + // Warning message mvwprintw(dialog_win, 3, 2, "This will permanently delete the most recent"); mvwprintw(dialog_win, 4, 2, "commit and ALL uncommitted changes!"); - + // Instructions mvwprintw(dialog_win, 6, 2, "Type 'yes' to confirm or ESC to cancel:"); - + // Input field mvwprintw(dialog_win, 7, 2, "> %s", input_buffer); - + wattroff(dialog_win, COLOR_PAIR(3)); wrefresh(dialog_win); // Position cursor wmove(dialog_win, 7, 4 + strlen(input_buffer)); - + // Get user input int ch = wgetch(dialog_win); - + if (ch == 27 || ch == 'q') { // ESC or q break; } else if (ch == '\n' || ch == '\r') { @@ -648,7 +654,7 @@ int show_reset_confirmation_dialog(void) { wrefresh(saved_screen); delwin(saved_screen); } - + return 0; } @@ -1197,7 +1203,7 @@ int push_commit(NCursesDiffViewer *viewer, int commit_index) { int commits_ahead = 0; int commits_behind = 0; int is_diverged = check_branch_divergence(&commits_ahead, &commits_behind); - + // If diverged, show confirmation dialog if (is_diverged) { if (!show_diverged_branch_dialog(commits_ahead, commits_behind)) { @@ -1220,7 +1226,8 @@ int push_commit(NCursesDiffViewer *viewer, int commit_index) { int result; if (is_diverged) { // Use force push with lease for safety - result = system("git push --force-with-lease origin 2>/dev/null >/dev/null"); + result = + system("git push --force-with-lease origin 2>/dev/null >/dev/null"); } else { // Normal push result = system("git push origin 2>/dev/null >/dev/null"); @@ -1231,7 +1238,7 @@ int push_commit(NCursesDiffViewer *viewer, int commit_index) { viewer->sync_status = SYNC_STATUS_PUSHED_APPEARING; viewer->animation_frame = 0; viewer->text_char_count = 0; - + // Refresh commit history to get proper push status get_commit_history(viewer); return 1; @@ -1465,7 +1472,6 @@ void render_commit_list_window(NCursesDiffViewer *viewer) { wrefresh(viewer->commit_list_win); } - /** * Render the file content window */ @@ -1580,7 +1586,7 @@ void render_status_bar(NCursesDiffViewer *viewer) { // Left side: Key bindings based on current mode char keybindings[256] = ""; if (viewer->current_mode == NCURSES_MODE_FILE_LIST) { - strcpy(keybindings, "Stage: | Stage All: a | Commit: c"); + strcpy(keybindings, "Stage: | Stage All: a | Stash: s | Commit: c"); } else if (viewer->current_mode == NCURSES_MODE_COMMIT_LIST) { strcpy(keybindings, "Push: P | Pull: p | Reset: r/R | Amend: a | Nav: j/k"); } else if (viewer->current_mode == NCURSES_MODE_FILE_VIEW) { @@ -2025,6 +2031,11 @@ int handle_ncurses_diff_input(NCursesDiffViewer *viewer, int key) { mark_all_files(viewer); break; + case 's': + case 'S': + create_ncurses_git_stash(viewer); + break; + case 'c': case 'C': // Commit marked files { @@ -2189,6 +2200,10 @@ int run_ncurses_diff_viewer(void) { // Get changed files (can be 0, that's okay) get_ncurses_changed_files(&viewer); + + //get stashes + get_ncurses_git_stashes(&viewer); + // Load commit history get_commit_history(&viewer); @@ -2202,7 +2217,7 @@ int run_ncurses_diff_viewer(void) { if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav Space=mark " - "A=all C=commit P=push | q=quit"); + "A=all S=stash C=commit P=push | q=quit"); } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll " @@ -2218,6 +2233,7 @@ int run_ncurses_diff_viewer(void) { render_file_list_window(&viewer); render_file_content_window(&viewer); render_commit_list_window(&viewer); + render_stash_list_window(&viewer); render_status_bar(&viewer); // Main display loop @@ -2235,7 +2251,7 @@ int run_ncurses_diff_viewer(void) { if (viewer.current_mode == NCURSES_MODE_FILE_LIST) { mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=nav " - "Space=mark A=all C=commit P=push | q=quit"); + "Space=mark A=all S=stash C=commit P=push | q=quit"); } else if (viewer.current_mode == NCURSES_MODE_FILE_VIEW) { mvprintw(0, 0, "Git Diff Viewer: 1=files 2=view 3=commits | j/k=scroll " @@ -2256,6 +2272,7 @@ int run_ncurses_diff_viewer(void) { render_file_list_window(&viewer); render_file_content_window(&viewer); render_commit_list_window(&viewer); + render_stash_list_window(&viewer); render_status_bar(&viewer); // Keep cursor hidden @@ -2274,6 +2291,102 @@ int run_ncurses_diff_viewer(void) { return 0; } +/** + * Get list of git stashes + */ +int get_ncurses_git_stashes(NCursesDiffViewer *viewer) { + if (!viewer) return 0; + + char stash_lines[MAX_STASHES][512]; + viewer->stash_count = get_git_stashes(stash_lines, MAX_STASHES); + + for (int i = 0; i < viewer->stash_count; i++) { + strncpy(viewer->stashes[i].stash_info, stash_lines[i], 511); + viewer->stashes[i].stash_info[511] = '\0'; + } + + return viewer->stash_count; +} + +/** + * Create a new git stash + */ +int create_ncurses_git_stash(NCursesDiffViewer *viewer) { + if (!viewer) return 0; + + int result = create_git_stash(); + + if (result) { + // Refresh everything after creating stash + get_ncurses_changed_files(viewer); + get_ncurses_git_stashes(viewer); + get_commit_history(viewer); + + // Reset file selection since changes are stashed + viewer->selected_file = 0; + viewer->file_line_count = 0; + viewer->file_scroll_offset = 0; + + // Reload current file if any files still exist + if (viewer->file_count > 0 && viewer->selected_file < viewer->file_count) { + load_full_file_with_diff(viewer, viewer->files[viewer->selected_file].filename); + } + } + + return result; +} + +/** + * Render the stash list window + */ +void render_stash_list_window(NCursesDiffViewer *viewer) { + if (!viewer || !viewer->stash_list_win) + return; + + // Clear the entire window first + werase(viewer->stash_list_win); + + // Draw rounded border and title + draw_rounded_box(viewer->stash_list_win); + mvwprintw(viewer->stash_list_win, 0, 2, " 4. Stashes "); + + int max_stashes_visible = viewer->stash_panel_height - 2; + + // Clear the content area with spaces + for (int y = 1; y < viewer->stash_panel_height - 1; y++) { + for (int x = 1; x < viewer->file_panel_width - 1; x++) { + mvwaddch(viewer->stash_list_win, y, x, ' '); + } + } + + if (viewer->stash_count == 0) { + // Show "No stashes" message + mvwprintw(viewer->stash_list_win, 1, 2, "No stashes available"); + } else { + for (int i = 0; i < max_stashes_visible && i < viewer->stash_count; i++) { + int y = i + 1; + + // Show stash info (truncated to fit panel) + int max_stash_len = viewer->file_panel_width - 4; + char truncated_stash[256]; + if ((int)strlen(viewer->stashes[i].stash_info) > max_stash_len) { + strncpy(truncated_stash, viewer->stashes[i].stash_info, max_stash_len - 2); + truncated_stash[max_stash_len - 2] = '\0'; + strcat(truncated_stash, ".."); + } else { + strcpy(truncated_stash, viewer->stashes[i].stash_info); + } + + // Color stash entries yellow + wattron(viewer->stash_list_win, COLOR_PAIR(4)); + mvwprintw(viewer->stash_list_win, y, 2, "%s", truncated_stash); + wattroff(viewer->stash_list_win, COLOR_PAIR(4)); + } + } + + wrefresh(viewer->stash_list_win); +} + /** * Clean up ncurses diff viewer resources */ @@ -2288,9 +2401,13 @@ void cleanup_ncurses_diff_viewer(NCursesDiffViewer *viewer) { if (viewer->commit_list_win) { delwin(viewer->commit_list_win); } + if (viewer->stash_list_win) { + delwin(viewer->stash_list_win); + } if (viewer->status_bar_win) { delwin(viewer->status_bar_win); } } endwin(); } + diff --git a/ncurses_diff_viewer.h b/ncurses_diff_viewer.h index 6ddd601..002c290 100644 --- a/ncurses_diff_viewer.h +++ b/ncurses_diff_viewer.h @@ -15,80 +15,89 @@ #define MAX_COMMITS 50 #define MAX_COMMIT_TITLE_LEN 256 #define MAX_AUTHOR_INITIALS 3 +#define MAX_STASHES 20 typedef struct { - char filename[MAX_FILENAME_LEN]; - char status; // 'M' = modified, 'A' = added, 'D' = deleted - int marked_for_commit; // 1 if marked for commit, 0 otherwise + char stash_info[512]; +} NCursesStash; + +typedef struct { + char filename[MAX_FILENAME_LEN]; + char status; // 'M' = modified, 'A' = added, 'D' = deleted + int marked_for_commit; // 1 if marked for commit, 0 otherwise } NCursesChangedFile; typedef struct { - char line[1024]; - char type; // '+' = addition, '-' = deletion, ' ' = context, '@' = hunk header - int is_diff_line; // 1 if this is a diff line, 0 if original file line + char line[1024]; + char type; // '+' = addition, '-' = deletion, ' ' = context, '@' = hunk header + int is_diff_line; // 1 if this is a diff line, 0 if original file line } NCursesFileLine; typedef struct { - char hash[16]; // Short commit hash - char author_initials[MAX_AUTHOR_INITIALS]; - char title[MAX_COMMIT_TITLE_LEN]; - int is_pushed; // 1 if pushed to remote, 0 if local only + char hash[16]; // Short commit hash + char author_initials[MAX_AUTHOR_INITIALS]; + char title[MAX_COMMIT_TITLE_LEN]; + int is_pushed; // 1 if pushed to remote, 0 if local only } NCursesCommit; typedef enum { - NCURSES_MODE_FILE_LIST, - NCURSES_MODE_FILE_VIEW, - NCURSES_MODE_COMMIT_LIST + NCURSES_MODE_FILE_LIST, + NCURSES_MODE_FILE_VIEW, + NCURSES_MODE_COMMIT_LIST } NCursesViewMode; typedef enum { - SYNC_STATUS_IDLE, - SYNC_STATUS_SYNCING_APPEARING, - SYNC_STATUS_SYNCING_VISIBLE, - SYNC_STATUS_SYNCING_DISAPPEARING, - SYNC_STATUS_PUSHING_APPEARING, - SYNC_STATUS_PUSHING_VISIBLE, - SYNC_STATUS_PUSHING_DISAPPEARING, - SYNC_STATUS_PULLING_APPEARING, - SYNC_STATUS_PULLING_VISIBLE, - SYNC_STATUS_PULLING_DISAPPEARING, - SYNC_STATUS_SYNCED_APPEARING, - SYNC_STATUS_SYNCED_VISIBLE, - SYNC_STATUS_SYNCED_DISAPPEARING, - SYNC_STATUS_PUSHED_APPEARING, - SYNC_STATUS_PUSHED_VISIBLE, - SYNC_STATUS_PUSHED_DISAPPEARING, - SYNC_STATUS_PULLED_APPEARING, - SYNC_STATUS_PULLED_VISIBLE, - SYNC_STATUS_PULLED_DISAPPEARING + SYNC_STATUS_IDLE, + SYNC_STATUS_SYNCING_APPEARING, + SYNC_STATUS_SYNCING_VISIBLE, + SYNC_STATUS_SYNCING_DISAPPEARING, + SYNC_STATUS_PUSHING_APPEARING, + SYNC_STATUS_PUSHING_VISIBLE, + SYNC_STATUS_PUSHING_DISAPPEARING, + SYNC_STATUS_PULLING_APPEARING, + SYNC_STATUS_PULLING_VISIBLE, + SYNC_STATUS_PULLING_DISAPPEARING, + SYNC_STATUS_SYNCED_APPEARING, + SYNC_STATUS_SYNCED_VISIBLE, + SYNC_STATUS_SYNCED_DISAPPEARING, + SYNC_STATUS_PUSHED_APPEARING, + SYNC_STATUS_PUSHED_VISIBLE, + SYNC_STATUS_PUSHED_DISAPPEARING, + SYNC_STATUS_PULLED_APPEARING, + SYNC_STATUS_PULLED_VISIBLE, + SYNC_STATUS_PULLED_DISAPPEARING } SyncStatus; typedef struct { - NCursesChangedFile files[MAX_FILES]; - int file_count; - int selected_file; - NCursesFileLine file_lines[MAX_FULL_FILE_LINES]; - int file_line_count; - int file_scroll_offset; - NCursesCommit commits[MAX_COMMITS]; - int commit_count; - int selected_commit; - WINDOW *file_list_win; - WINDOW *file_content_win; - WINDOW *commit_list_win; - WINDOW *status_bar_win; - int terminal_width; - int terminal_height; - int file_panel_width; - int file_panel_height; - int commit_panel_height; - int status_bar_height; - NCursesViewMode current_mode; - SyncStatus sync_status; - int spinner_frame; - time_t last_sync_time; - int animation_frame; - int text_char_count; + NCursesChangedFile files[MAX_FILES]; + int file_count; + int selected_file; + NCursesFileLine file_lines[MAX_FULL_FILE_LINES]; + int file_line_count; + int file_scroll_offset; + NCursesCommit commits[MAX_COMMITS]; + int commit_count; + int selected_commit; + NCursesStash stashes[MAX_STASHES]; + int stash_count; + WINDOW *file_list_win; + WINDOW *file_content_win; + WINDOW *commit_list_win; + WINDOW *stash_list_win; + WINDOW *status_bar_win; + int terminal_width; + int terminal_height; + int file_panel_width; + int file_panel_height; + int commit_panel_height; + int stash_panel_height; + int status_bar_height; + NCursesViewMode current_mode; + SyncStatus sync_status; + int spinner_frame; + time_t last_sync_time; + int animation_frame; + int text_char_count; } NCursesDiffViewer; /** @@ -159,7 +168,8 @@ void mark_all_files(NCursesDiffViewer *viewer); /** * Commit marked files with title and message */ -int commit_marked_files(NCursesDiffViewer *viewer, const char *commit_title, const char *commit_message); +int commit_marked_files(NCursesDiffViewer *viewer, const char *commit_title, + const char *commit_message); /** * Push specific commit @@ -179,7 +189,8 @@ void render_commit_list_window(NCursesDiffViewer *viewer); /** * Get commit title and message input from user */ -int get_commit_title_input(char *title, int max_len, char *message, int max_message_len); +int get_commit_title_input(char *title, int max_len, char *message, + int max_message_len); /** * Draw a box with rounded corners @@ -196,4 +207,10 @@ void render_status_bar(NCursesDiffViewer *viewer); */ void update_sync_status(NCursesDiffViewer *viewer); -#endif // NCURSES_DIFF_VIEWER_H \ No newline at end of file +int get_ncurses_git_stashes(NCursesDiffViewer *viewer); + +int create_ncurses_git_stash(NCursesDiffViewer *viewer); + +void render_stash_list_window(NCursesDiffViewer *viewer); + +#endif // NCURSES_DIFF_VIEWER_H diff --git a/ncurses_diff_viewer.o b/ncurses_diff_viewer.o index d49f91f..df685bf 100644 Binary files a/ncurses_diff_viewer.o and b/ncurses_diff_viewer.o differ diff --git a/shell b/shell index 6bb8385..a742743 100755 Binary files a/shell and b/shell differ