diff --git a/include/tig/repo.h b/include/tig/repo.h index 652823ea5..1e90127f4 100644 --- a/include/tig/repo.h +++ b/include/tig/repo.h @@ -49,7 +49,7 @@ struct index_diff { int untracked; }; -bool index_diff(struct index_diff *diff, bool untracked, bool count_all); +bool index_diff(struct index_diff *diff, bool untracked, bool count_all, const char **files); bool update_index(void); #endif diff --git a/src/main.c b/src/main.c index 6fe28c38c..39c43b578 100644 --- a/src/main.c +++ b/src/main.c @@ -64,9 +64,20 @@ main_register_commit(struct view *view, struct commit *commit, const char *ids, string_copy_rev(commit->id, ids); /* FIXME: lazily check index state here instead of in main_open. */ - if ((state->add_changes_untracked || state->add_changes_unstaged || state->add_changes_staged) && is_head_commit(commit->id)) { - main_add_changes(view, state, ids); - state->add_changes_untracked = state->add_changes_unstaged = state->add_changes_staged = false; + if (state->add_changes_untracked || state->add_changes_unstaged || state->add_changes_staged) { + /* When filtering by file, history simplification may prune + * the HEAD commit from the log. Anchor the changes commits + * to the first commit of the filtered log instead, so they + * stay at the top of the view. The changes commits have a + * null id and must not trigger this themselves. */ + bool anchor = is_head_commit(commit->id) || + (opt_file_args && opt_file_filter && + strcmp(commit->id, NULL_ID)); + + if (anchor) { + main_add_changes(view, state, ids); + state->add_changes_untracked = state->add_changes_unstaged = state->add_changes_staged = false; + } } if (state->with_graph) @@ -160,7 +171,8 @@ main_check_index(struct view *view, struct main_state *state) { struct index_diff diff; - if (!index_diff(&diff, opt_show_untracked, false)) + if (!index_diff(&diff, opt_show_untracked, false, + opt_file_filter ? opt_file_args : NULL)) return false; if (!diff.untracked) { @@ -427,6 +439,16 @@ main_read(struct view *view, struct buffer *buf, bool force_stop) if (!buf) { main_flush_commit(view, commit); + /* The filtered log may be empty, e.g. when the paths have no + * commit history; add the changes commits on their own. */ + if (opt_file_args && opt_file_filter && + (state->add_changes_untracked || state->add_changes_unstaged || + state->add_changes_staged)) { + main_add_changes(view, state, repo.head_id); + state->add_changes_untracked = state->add_changes_unstaged = + state->add_changes_staged = false; + } + if (!force_stop && failed_to_load_initial_view(view)) die("No revisions match the given arguments."); if (view->lines > 0) { diff --git a/src/repo.c b/src/repo.c index f1d4953f6..d9c9492f5 100644 --- a/src/repo.c +++ b/src/repo.c @@ -12,6 +12,7 @@ */ #include "tig/tig.h" +#include "tig/argv.h" #include "tig/repo.h" #include "tig/io.h" #include "tig/refdb.h" @@ -146,22 +147,34 @@ update_index(void) } bool -index_diff(struct index_diff *diff, bool untracked, bool count_all) +index_diff(struct index_diff *diff, bool untracked, bool count_all, const char **files) { const char *untracked_arg = !untracked ? "--untracked-files=no" : count_all ? "--untracked-files=all" : "--untracked-files=normal"; const char *status_argv[] = { - "git", "status", "--porcelain", "-z", untracked_arg, NULL + "git", "status", "--porcelain", "-z", untracked_arg, "--", NULL }; + const char **argv = NULL; struct io io; struct buffer buf; bool ok = true; memset(diff, 0, sizeof(*diff)); - if (!io_run(&io, IO_RD, repo.exec_dir, NULL, status_argv)) + /* Run in the current directory so relative pathspecs in the file + * arguments resolve like they do for the views, which do not use + * repo.exec_dir either (e.g. the main view). */ + if (!argv_append_array(&argv, status_argv) || + (files && !argv_append_array(&argv, files)) || + !io_run(&io, IO_RD, NULL, NULL, argv)) { + argv_free(argv); + free(argv); return false; + } + + argv_free(argv); + free(argv); while (io_get(&io, &buf, 0, true) && (ok = buf.size > 3)) { if (buf.data[0] == '?') diff --git a/src/watch.c b/src/watch.c index ebf988711..208e74f59 100644 --- a/src/watch.c +++ b/src/watch.c @@ -115,7 +115,7 @@ watch_index_handler(struct watch_handler *handler, enum watch_event event, enum if (event == WATCH_EVENT_SWITCH_VIEW) return WATCH_NONE; - if (!index_diff(&diff, opt_show_untracked, opt_status_show_untracked_files)) + if (!index_diff(&diff, opt_show_untracked, opt_status_show_untracked_files, NULL)) return check_file_mtime(&handler->last_modified, "%s/index", repo.git_dir) ? check : WATCH_NONE; diff --git a/test/main/filtered-changes-test b/test/main/filtered-changes-test new file mode 100755 index 000000000..b0ca871d0 --- /dev/null +++ b/test/main/filtered-changes-test @@ -0,0 +1,160 @@ +#!/bin/sh +# +# Test that the main view shows index changes commits when filtering by +# file, also when history simplification prunes the HEAD commit from the +# filtered log. + +. libtest.sh +. libgit.sh +. "$source_dir/util.sh" + +export LINES=10 + +tigrc < a + echo "b 1" > b + echo "root foo" > foo + mkdir sub + echo "sub foo" > sub/foo + git add . + commit "2009-02-13 23:31 +0000" "change a and b" + + echo "b 2" >> b + git add b + commit "2009-02-22 11:53 +0000" "change b" + + # Staged changes to a, c and sub/foo; the root foo stays clean. + echo "a 2" >> a + git add a + echo "c 1" > c + git add c + echo "sub foo 2" >> sub/foo + git add sub/foo +} + +test_tig_script() { + name="$1"; shift + + tig_script "$name" " + :view-main + :save-display $name.screen + " + + cat > "$name.expected" + + test_tig "$@" + + assert_equals "$name.screen" < "$name.expected" +} + +# HEAD ("change b") is pruned from the filtered log; the changes commit +# is expected at the top of the view, anchored to the first commit. +test_tig_script 'filtered-with-staged' -- a <