diff options
Diffstat (limited to 'gitstatus/src/repo_cache.cc')
-rw-r--r-- | gitstatus/src/repo_cache.cc | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/gitstatus/src/repo_cache.cc b/gitstatus/src/repo_cache.cc new file mode 100644 index 00000000..d7f5f9ad --- /dev/null +++ b/gitstatus/src/repo_cache.cc @@ -0,0 +1,167 @@ +// Copyright 2019 Roman Perepelitsa. +// +// This file is part of GitStatus. +// +// GitStatus is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// GitStatus is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with GitStatus. If not, see <https://www.gnu.org/licenses/>. + +#include "repo_cache.h" + +#include <cstring> + +#include "check.h" +#include "git.h" +#include "print.h" +#include "scope_guard.h" +#include "string_view.h" + +namespace gitstatus { + +namespace { + +void GitDirs(const char* dir, bool from_dotgit, std::string& gitdir, std::string& workdir) { + git_buf gitdir_buf = {}; + git_buf workdir_buf = {}; + ON_SCOPE_EXIT(&) { + git_buf_free(&gitdir_buf); + git_buf_free(&workdir_buf); + }; + int flags = from_dotgit ? GIT_REPOSITORY_OPEN_NO_SEARCH | GIT_REPOSITORY_OPEN_NO_DOTGIT : 0; + switch (git_repository_discover_ex(&gitdir_buf, &workdir_buf, NULL, NULL, dir, flags, nullptr)) { + case 0: + gitdir.assign(gitdir_buf.ptr, gitdir_buf.size); + workdir.assign(workdir_buf.ptr, workdir_buf.size); + VERIFY(!gitdir.empty() && gitdir.front() == '/' && gitdir.back() == '/'); + VERIFY(!workdir.empty() && workdir.front() == '/' && workdir.back() == '/'); + break; + case GIT_ENOTFOUND: + gitdir.clear(); + workdir.clear(); + break; + default: + LOG(ERROR) << "git_repository_open_ext: " << Print(dir) << ": " << GitError(); + throw Exception(); + } +} + +git_repository* OpenRepo(const std::string& dir, bool from_dotgit) { + git_repository* repo = nullptr; + int flags = from_dotgit ? GIT_REPOSITORY_OPEN_NO_SEARCH | GIT_REPOSITORY_OPEN_NO_DOTGIT : 0; + switch (git_repository_open_ext(&repo, dir.c_str(), flags, nullptr)) { + case 0: + return repo; + case GIT_ENOTFOUND: + return nullptr; + default: + LOG(ERROR) << "git_repository_open_ext: " << Print(dir) << ": " << GitError(); + throw Exception(); + } +} + +std::string DirName(std::string path) { + if (path.empty()) return ""; + while (path.back() == '/') { + path.pop_back(); + if (path.empty()) return ""; + } + do { + path.pop_back(); + if (path.empty()) return ""; + } while (path.back() != '/'); + return path; +} + +} // namespace + +Repo* RepoCache::Open(const std::string& dir, bool from_dotgit) { + if (dir.empty() || dir.front() != '/') return nullptr; + + std::string gitdir, workdir; + GitDirs(dir.c_str(), from_dotgit, gitdir, workdir); + if (gitdir.empty()) { + // This isn't quite correct because of differences in canonicalization, .git files and GIT_DIR. + // A proper solution would require tracking the "discovery dir" for every repository and + // performing path canonicalization. + if (from_dotgit) { + Erase(cache_.find(dir.back() == '/' ? dir : dir + '/')); + } else { + std::string path = dir; + if (path.back() != '/') path += '/'; + do { + Erase(cache_.find(path + ".git/")); + path = DirName(path); + } while (!path.empty()); + } + return nullptr; + } + + auto it = cache_.find(gitdir); + if (it != cache_.end()) { + lru_.erase(it->second->lru); + it->second->lru = lru_.insert({Clock::now(), it}); + return it->second.get(); + } + + // Opening from gitdir is faster but we cannot use it when gitdir came from a .git file. + git_repository* repo = + DirName(gitdir) == workdir ? OpenRepo(gitdir, true) : OpenRepo(dir, from_dotgit); + if (!repo) return nullptr; + ON_SCOPE_EXIT(&) { + if (repo) git_repository_free(repo); + }; + if (git_repository_is_bare(repo)) return nullptr; + workdir = git_repository_workdir(repo) ?: ""; + if (workdir.empty()) return nullptr; + VERIFY(workdir.front() == '/' && workdir.back() == '/') << Print(workdir); + + auto x = cache_.emplace(gitdir, nullptr); + std::unique_ptr<Entry>& elem = x.first->second; + if (elem) { + lru_.erase(elem->lru); + } else { + LOG(INFO) << "Initializing new repository: " << Print(gitdir); + + // Libgit2 initializes odb and refdb lazily with double-locking. To avoid useless work + // when multiple threads attempt to initialize the same db at the same time, we trigger + // initialization manually before threads are in play. + git_odb* odb; + VERIFY(!git_repository_odb(&odb, repo)) << GitError(); + git_odb_free(odb); + + git_refdb* refdb; + VERIFY(!git_repository_refdb(&refdb, repo)) << GitError(); + git_refdb_free(refdb); + + elem = std::make_unique<Entry>(std::exchange(repo, nullptr), lim_); + } + elem->lru = lru_.insert({Clock::now(), x.first}); + return elem.get(); +} + +void RepoCache::Free(Time cutoff) { + while (true) { + if (lru_.empty()) break; + auto it = lru_.begin(); + if (it->first > cutoff) break; + Erase(it->second); + } +} + +void RepoCache::Erase(Cache::iterator it) { + if (it == cache_.end()) return; + LOG(INFO) << "Closing repository: " << Print(it->first); + lru_.erase(it->second->lru); + cache_.erase(it); +} + +} // namespace gitstatus |