diff --git a/builtin.cpp b/builtin.cpp index a7b173bb2..ebfecc920 100644 --- a/builtin.cpp +++ b/builtin.cpp @@ -3512,6 +3512,7 @@ static int builtin_history(parser_t &parser, wchar_t **argv) bool search_prefix = false; bool save_history = false; bool clear_history = false; + bool merge_history = false; static const struct woption long_options[] = { @@ -3521,6 +3522,7 @@ static int builtin_history(parser_t &parser, wchar_t **argv) { L"contains", no_argument, 0, 'c' }, { L"save", no_argument, 0, 'v' }, { L"clear", no_argument, 0, 'l' }, + { L"merge", no_argument, 0, 'm' }, { L"help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; @@ -3555,6 +3557,9 @@ static int builtin_history(parser_t &parser, wchar_t **argv) case 'l': clear_history = true; break; + case 'm': + merge_history = true; + break; case 'h': builtin_print_help(parser, argv[0], stdout_buffer); return STATUS_BUILTIN_OK; @@ -3584,6 +3589,11 @@ static int builtin_history(parser_t &parser, wchar_t **argv) return STATUS_BUILTIN_OK; } + if (merge_history) + { + history->incorporate_external_changes(); + } + if (search_history) { int res = STATUS_BUILTIN_ERROR; diff --git a/doc_src/history.txt b/doc_src/history.txt index 33ee4d662..8545361da 100644 --- a/doc_src/history.txt +++ b/doc_src/history.txt @@ -2,7 +2,7 @@ \subsection history-synopsis Synopsis
-history (--save | --clear)
+history (--save | --clear | --merge)
 history (--search | --delete ) (--prefix "prefix string" | --contains "search string")
 
@@ -16,6 +16,9 @@ The following options are available: saves the history file; this option is provided for internal use. - \c --clear clears the history file. A prompt is displayed before the history is erased. +- \c --merge immediately incorporates history changes from other sessions. Ordinarily +fish ignores history changes from sessions started after the current one. This command +applies those changes immediately. - \c --search returns history items in keeping with the \c --prefix or \c --contains options. - \c --delete deletes history items. diff --git a/fish_tests.cpp b/fish_tests.cpp index b29f7124a..6584380de 100644 --- a/fish_tests.cpp +++ b/fish_tests.cpp @@ -2688,7 +2688,8 @@ void history_tests_t::test_history_merge(void) const size_t count = 3; const wcstring name = L"merge_test"; history_t *hists[count] = {new history_t(name), new history_t(name), new history_t(name)}; - wcstring texts[count] = {L"History 1", L"History 2", L"History 3"}; + const wcstring texts[count] = {L"History 1", L"History 2", L"History 3"}; + const wcstring alt_texts[count] = {L"History Alt 1", L"History Alt 2", L"History Alt 3"}; /* Make sure history is clear */ for (size_t i=0; i < count; i++) @@ -2730,6 +2731,32 @@ void history_tests_t::test_history_merge(void) do_test(history_contains(everything, texts[i])); } + /* Tell all histories to merge. Now everybody should have everything. */ + for (size_t i=0; i < count; i++) + { + hists[i]->incorporate_external_changes(); + } + /* Add some more per-history items */ + for (size_t i=0; i < count; i++) + { + hists[i]->add(alt_texts[i]); + } + /* Everybody should have old items, but only one history should have each new item */ + for (size_t i = 0; i < count; i++) + { + for (size_t j=0; j < count; j++) + { + /* Old item */ + do_test(history_contains(hists[i], texts[j])); + + /* New item */ + bool does_contain = history_contains(hists[i], alt_texts[j]); + bool should_contain = (i == j); + do_test(should_contain == does_contain); + } + } + + /* Clean up */ for (size_t i=0; i < 3; i++) { diff --git a/history.cpp b/history.cpp index fdbc23594..52c50a97e 100644 --- a/history.cpp +++ b/history.cpp @@ -536,7 +536,7 @@ history_t::history_t(const wcstring &pname) : mmap_start(NULL), mmap_length(0), mmap_file_id(kInvalidFileID), - birth_timestamp(time(NULL)), + boundary_timestamp(time(NULL)), countdown_to_vacuum(-1), loaded_old(false), chaos_mode(false) @@ -606,10 +606,12 @@ void history_t::save_internal_unless_disabled() void history_t::add(const wcstring &str, history_identifier_t ident) { time_t when = time(NULL); - /* Big hack: do not allow timestamps equal to our birthdate. This is because we include items whose timestamps are equal to our birthdate when reading old history, so we can catch "just closed" items. But this means that we may interpret our own items, that we just wrote, as old items, if we wrote them in the same second as our birthdate. + /* Big hack: do not allow timestamps equal to our boundary date. This is because we include items whose timestamps are equal to our boundary when reading old history, so we can catch "just closed" items. But this means that we may interpret our own items, that we just wrote, as old items, if we wrote them in the same second as our birthdate. */ - if (when == this->birth_timestamp) + if (when == this->boundary_timestamp) + { when++; + } this->add(history_item_t(str, when, ident)); } @@ -1008,7 +1010,7 @@ void history_t::populate_from_mmap(void) size_t cursor = 0; for (;;) { - size_t offset = offset_of_next_item(mmap_start, mmap_length, mmap_type, &cursor, birth_timestamp); + size_t offset = offset_of_next_item(mmap_start, mmap_length, mmap_type, &cursor, boundary_timestamp); // If we get back -1, we're done if (offset == (size_t)(-1)) break; @@ -1259,6 +1261,7 @@ static wcstring history_filename(const wcstring &name, const wcstring &suffix) void history_t::clear_file_state() { + ASSERT_IS_LOCKED(lock); /* Erase everything we know about our file */ if (mmap_start != NULL && mmap_start != MAP_FAILED) { @@ -1692,6 +1695,20 @@ void history_t::populate_from_bash(FILE *stream) } } +void history_t::incorporate_external_changes() +{ + /* To incorporate new items, we simply update our timestamp to now, so that items from previous instances get added. We then clear the file state so that we remap the file. Note that this is somehwhat expensive because we will be going back over old items. An optimization would be to preserve old_item_offsets so that they don't have to be recomputed. (However, then items *deleted* in other instances would not show up here). */ + time_t new_timestamp = time(NULL); + scoped_lock locker(lock); + + /* If for some reason the clock went backwards, we don't want to start dropping items; therefore we only do work if time has progressed. This also makes multiple calls cheap. */ + if (new_timestamp > this->boundary_timestamp) + { + this->boundary_timestamp = new_timestamp; + this->clear_file_state(); + } +} + void history_init() { } diff --git a/history.h b/history.h index e6d486820..5b7292e83 100644 --- a/history.h +++ b/history.h @@ -154,8 +154,8 @@ private: /** The file ID of the file we mmap'd */ file_id_t mmap_file_id; - /** Timestamp of when this history was created */ - const time_t birth_timestamp; + /** The boundary timestamp distinguishes old items from new items. Items whose timestamps are <= the boundary are considered "old". Items whose timestemps are > the boundary are new, and are ignored by this instance (unless they came from this instance). The timestamp may be adjusted by incorporate_external_changes() */ + time_t boundary_timestamp; /** How many items we add until the next vacuum. Initially a random value. */ int countdown_to_vacuum; @@ -233,6 +233,9 @@ public: /** Populates from a bash history file */ void populate_from_bash(FILE *f); + /** Incorporates the history of other shells into this history */ + void incorporate_external_changes(); + /* Gets all the history into a string with ARRAY_SEP_STR. This is intended for the $history environment variable. This may be long! */ void get_string_representation(wcstring &str, const wcstring &separator);