diff --git a/doc/tigrc.5.adoc b/doc/tigrc.5.adoc index 4e99c2afc..69640f3b9 100644 --- a/doc/tigrc.5.adoc +++ b/doc/tigrc.5.adoc @@ -617,6 +617,9 @@ bind generic G none # User-defined external command to amend the last commit bind status + !git commit --amend +# Toggle amend mode in the status or stage view +bind status a status-amend + # User-defined internal command that reloads ~/.tigrc bind generic S :source ~/.tigrc @@ -922,6 +925,7 @@ View-specific actions [frame="none",grid="none",cols="25flags.internal) { request = run_prompt_command(view, argv); diff --git a/src/repo.c b/src/repo.c index f1d4953f6..9115dc137 100644 --- a/src/repo.c +++ b/src/repo.c @@ -16,6 +16,8 @@ #include "tig/io.h" #include "tig/refdb.h" #include "tig/git.h" +#include "tig/display.h" +#include "tig/status.h" #define REPO_INFO_GIT_DIR "--git-dir" #define REPO_INFO_WORK_TREE "--is-inside-work-tree" @@ -131,6 +133,31 @@ load_repo_head(void) struct repo_info repo; +const char * +repo_staged_parent(void) +{ + return repo.amend_mode ? "HEAD^" : "HEAD"; +} + +bool +repo_amend_mode_enabled(void) +{ + return repo.amend_mode; +} + +bool +repo_toggle_amend_mode(void) +{ + if (!repo.amend_mode && is_initial_commit()) { + report("Amend mode requires an existing commit"); + return false; + } + + repo.amend_mode = !repo.amend_mode; + report("Amend mode %s", repo.amend_mode ? "enabled" : "disabled"); + return true; +} + /* * Git index utils. */ @@ -154,6 +181,7 @@ index_diff(struct index_diff *diff, bool untracked, bool count_all) const char *status_argv[] = { "git", "status", "--porcelain", "-z", untracked_arg, NULL }; + const char *staged_parent = repo_staged_parent(); struct io io; struct buffer buf; bool ok = true; @@ -185,6 +213,41 @@ index_diff(struct index_diff *diff, bool untracked, bool count_all) ok = false; io_done(&io); + if (!ok || !repo.amend_mode || is_initial_commit()) + return ok; + + { + const char *diff_index_argv[] = { + "git", "diff-index", "--cached", "--diff-filter=ACDMRTXB", + "-z", staged_parent, "--", NULL + }; + int staged = 0; + struct status parsed = {0}; + + if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff_index_argv)) + return false; + + while (io_get(&io, &buf, 0, true) && (ok = buf.size > 3)) { + if (!status_get_diff(&parsed, buf.data, buf.size)) { + ok = false; + break; + } + staged++; + if (parsed.status == 'R' || parsed.status == 'C') + io_get(&io, &buf, 0, true); + if (!io_get(&io, &buf, 0, true)) + break; + if (!count_all && staged && diff->unstaged && + (!untracked || diff->untracked)) + break; + } + + if (io_error(&io)) + ok = false; + io_done(&io); + diff->staged = staged; + } + return ok; } diff --git a/src/stage.c b/src/stage.c index 9b4ddec60..d4af42932 100644 --- a/src/stage.c +++ b/src/stage.c @@ -452,11 +452,14 @@ find_deleted_line_in_head(struct view *view, struct line *line) { unsigned long line_number_in_head, line_number = 0; long bias_by_staged_changes = 0; char buf[SIZEOF_STR] = ""; + const char *staged_parent = repo_staged_parent(); char file_in_head_pathspec[sizeof("HEAD:") + SIZEOF_STR], file_in_index_pathspec[sizeof(":") + SIZEOF_STR]; const char *file_in_head = NULL; const char *ls_tree_argv[] = { - "git", "ls-tree", "-z", "HEAD", view->env->file, NULL + "git", "ls-tree", "-z", + stage_line_type == LINE_STAT_STAGED ? staged_parent : "HEAD", + view->env->file, NULL }; const char *diff_argv[] = { "git", "diff", file_in_head_pathspec, file_in_index_pathspec, @@ -473,7 +476,9 @@ find_deleted_line_in_head(struct view *view, struct line *line) { } else { // The file might might be renamed in the index. Find its old name. const char *diff_index_argv[] = { "git", "diff-index", "--cached", "-C", - "--diff-filter=ACR", "-z", "HEAD", NULL + "--diff-filter=ACR", "-z", + stage_line_type == LINE_STAT_STAGED ? staged_parent : "HEAD", + NULL }; if (!io_run(&io, IO_RD, repo.exec_dir, NULL, diff_index_argv) || io.status) return false; @@ -565,6 +570,12 @@ stage_request(struct view *view, enum request request, struct line *line) return REQ_NONE; break; + case REQ_STATUS_AMEND: + if (!repo_toggle_amend_mode()) + return REQ_NONE; + load_repo_head(); + break; + case REQ_STATUS_REVERT: if (!stage_revert(view, line)) return REQ_NONE; @@ -655,7 +666,8 @@ stage_request(struct view *view, enum request request, struct line *line) view->env->ref[0] = 0; if (find_deleted_line_in_head(view, line)) - string_copy(view->env->ref, "HEAD"); + string_copy(view->env->ref, + stage_line_type == LINE_STAT_STAGED ? repo_staged_parent() : "HEAD"); else view->env->goto_lineno = diff_get_lineno(view, line, false); if (view->env->goto_lineno > 0) @@ -698,7 +710,8 @@ stage_request(struct view *view, enum request request, struct line *line) static void stage_select(struct view *view, struct line *line) { - const char *changes_msg = stage_line_type == LINE_STAT_STAGED ? "Staged changes" + const char *changes_msg = stage_line_type == LINE_STAT_STAGED + ? repo_amend_mode_enabled() ? "Amend changes" : "Staged changes" : stage_line_type == LINE_STAT_UNSTAGED ? "Unstaged changes" : NULL; @@ -713,9 +726,11 @@ stage_open(struct view *view, enum open_flags flags) stage_status.new.name) }; const char *index_show_argv[] = { - GIT_DIFF_STAGED(encoding_arg, diff_context_arg(), diff_prefix_arg(), - ignore_space_arg(), word_diff_arg(), stage_status.old.name, - stage_status.new.name) + "git", "diff-index", encoding_arg, "--textconv", "--patch-with-stat", "-C", + "--cached", "--diff-filter=ACDMRTXB", DIFF_ARGS, "%(cmdlineargs)", + diff_context_arg(), diff_prefix_arg(), ignore_space_arg(), + word_diff_arg(), repo_staged_parent(), "--", + stage_status.old.name, stage_status.new.name, NULL }; const char *files_show_argv[] = { GIT_DIFF_UNSTAGED(encoding_arg, diff_context_arg(), diff_prefix_arg(), diff --git a/src/status.c b/src/status.c index 52c726bc1..0a7f609f2 100644 --- a/src/status.c +++ b/src/status.c @@ -185,7 +185,6 @@ status_run(struct view *view, const char *argv[], char status, enum line_type ty return true; } -static const char *status_diff_index_argv[] = { GIT_DIFF_STAGED_FILES("-z") }; static const char *status_diff_files_argv[] = { GIT_DIFF_UNSTAGED_FILES("-z") }; static const char *status_list_other_argv[] = { @@ -352,6 +351,14 @@ status_update_onbranch(void) if (!string_format(status_onbranch, fmt, prefix, head, tracking_info)) string_copy(status_onbranch, repo.head); + if (repo_amend_mode_enabled()) { + char amend_status[SIZEOF_STR]; + + if (string_nformat(amend_status, sizeof(amend_status), NULL, + "%s [amend]", status_onbranch)) { + string_copy(status_onbranch, amend_status); + } + } return; } @@ -379,6 +386,10 @@ status_read_untracked(struct view *view) static enum status_code status_open(struct view *view, enum open_flags flags) { + const char *status_diff_index_argv[] = { + "git", "diff-index", "-z", "%(cmdlineargs)", "--diff-filter=ACDMRTXB", + "-C", "--cached", repo_staged_parent(), "--", NULL + }; const char **staged_argv = is_initial_commit() ? status_list_no_head_argv : status_diff_index_argv; char staged_status = staged_argv == status_list_no_head_argv ? 'A' : 0; @@ -423,7 +434,9 @@ status_get_column_data(struct view *view, const struct line *line, struct view_c switch (line->type) { case LINE_STAT_STAGED: type = LINE_SECTION; - text = "Changes to be committed:"; + text = repo_amend_mode_enabled() ? + "Changes to be amended:" : + "Changes to be committed:"; break; case LINE_STAT_UNSTAGED: @@ -722,6 +735,12 @@ status_request(struct view *view, enum request request, struct line *line) return REQ_NONE; break; + case REQ_STATUS_AMEND: + if (!repo_toggle_amend_mode()) + return REQ_NONE; + load_repo_head(); + break; + case REQ_STATUS_REVERT: if (!status_revert(status, line->type, status_has_none(view, line))) return REQ_NONE; @@ -791,9 +810,13 @@ status_stage_info_(char *buf, size_t bufsize, switch (type) { case LINE_STAT_STAGED: if (status && status->status) - info = "Staged changes to %s"; + info = repo_amend_mode_enabled() ? + "Amend changes to %s" : + "Staged changes to %s"; else - info = "Staged changes"; + info = repo_amend_mode_enabled() ? + "Amend changes" : + "Staged changes"; break; case LINE_STAT_UNSTAGED: @@ -831,15 +854,21 @@ status_select(struct view *view, struct line *line) switch (line->type) { case LINE_STAT_STAGED: - text = "Press %s to unstage %s for commit"; + text = repo_amend_mode_enabled() ? + "Press %s to unstage %s from amend commit" : + "Press %s to unstage %s for commit"; break; case LINE_STAT_UNSTAGED: - text = "Press %s to stage %s for commit"; + text = repo_amend_mode_enabled() ? + "Press %s to stage %s for amend commit" : + "Press %s to stage %s for commit"; break; case LINE_STAT_UNTRACKED: - text = "Press %s to stage %s for addition"; + text = repo_amend_mode_enabled() ? + "Press %s to stage %s for amend commit" : + "Press %s to stage %s for addition"; break; default: diff --git a/test/help/all-keybindings-test.expected b/test/help/all-keybindings-test.expected index b27c78b3b..84d1203e6 100755 --- a/test/help/all-keybindings-test.expected +++ b/test/help/all-keybindings-test.expected @@ -104,6 +104,7 @@ External commands: [-] status bindings View-specific actions u status-update Stage/unstage chunk or file changes + a status-amend Toggle amend mode ! status-revert Revert chunk or file changes M status-merge Merge file using external tool External commands: @@ -111,6 +112,7 @@ External commands: [-] stage bindings View-specific actions u status-update Stage/unstage chunk or file changes + a status-amend Toggle amend mode ! status-revert Revert chunk or file changes 1 stage-update-line Stage/unstage single line 2 stage-update-part Stage/unstage part of a chunk @@ -154,6 +156,4 @@ Toggle keys (enter: o ): - - -[help] - line 2 of 150 100% +[help] - line 2 of 152 100% diff --git a/test/status/amend-mode-test b/test/status/amend-mode-test new file mode 100755 index 000000000..e35934c8b --- /dev/null +++ b/test/status/amend-mode-test @@ -0,0 +1,114 @@ +#!/bin/sh + +. libtest.sh +. libgit.sh + +export LINES=15 +export COLUMNS=120 + +steps ' + :save-display start.screen + a + :3 + :save-display amend.screen + + :save-display amend-stage.screen + :view-close + C + a + :save-display final.screen +' + +tigrc <' to jump to file diff - line 1 100% +EOF + +assert_equals 'final.screen' < quit # Close all views and quit # View specific bind status u status-update # Stage/unstage changes in file +bind status a status-amend # Toggle amend mode bind status ! status-revert # Revert changes in file bind status M status-merge # Open git-mergetool(1) # bind status ??? :toggle status # Show short or long status labels bind stage u status-update # Stage/unstage current diff (c)hunk +bind stage a status-amend # Toggle amend mode bind stage 1 stage-update-line # Stage/unstage current line bind stage 2 stage-update-part # Stage/unstage part of chunk bind stage ! status-revert # Revert current diff (c)hunk