diff options
Diffstat (limited to 'gitstatus/src/git.cc')
-rw-r--r-- | gitstatus/src/git.cc | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/gitstatus/src/git.cc b/gitstatus/src/git.cc new file mode 100644 index 00000000..029b02bf --- /dev/null +++ b/gitstatus/src/git.cc @@ -0,0 +1,242 @@ +// 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 "git.h" + +#include <cstdlib> +#include <cstring> +#include <fstream> +#include <sstream> +#include <utility> + +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "arena.h" +#include "check.h" +#include "print.h" +#include "scope_guard.h" + +namespace gitstatus { + +const char* GitError() { + const git_error* err = git_error_last(); + return err && err->message ? err->message : "unknown error"; +} + +std::string RepoState(git_repository* repo) { + Arena arena; + StringView gitdir(git_repository_path(repo)); + + // These names mostly match gitaction in vcs_info: + // https://github.com/zsh-users/zsh/blob/master/Functions/VCS_Info/Backends/VCS_INFO_get_data_git. + auto State = [&]() { + switch (git_repository_state(repo)) { + case GIT_REPOSITORY_STATE_NONE: + return ""; + case GIT_REPOSITORY_STATE_MERGE: + return "merge"; + case GIT_REPOSITORY_STATE_REVERT: + return "revert"; + case GIT_REPOSITORY_STATE_REVERT_SEQUENCE: + return "revert-seq"; + case GIT_REPOSITORY_STATE_CHERRYPICK: + return "cherry"; + case GIT_REPOSITORY_STATE_CHERRYPICK_SEQUENCE: + return "cherry-seq"; + case GIT_REPOSITORY_STATE_BISECT: + return "bisect"; + case GIT_REPOSITORY_STATE_REBASE: + return "rebase"; + case GIT_REPOSITORY_STATE_REBASE_INTERACTIVE: + return "rebase-i"; + case GIT_REPOSITORY_STATE_REBASE_MERGE: + return "rebase-m"; + case GIT_REPOSITORY_STATE_APPLY_MAILBOX: + return "am"; + case GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE: + return "am/rebase"; + } + return "action"; + }; + + auto DirExists = [&](StringView name) { + int fd = open(arena.StrCat(gitdir, "/", name), O_DIRECTORY | O_CLOEXEC); + if (fd < 0) return false; + CHECK(!close(fd)) << Errno(); + return true; + }; + + auto ReadFile = [&](StringView name) { + std::ifstream strm(arena.StrCat(gitdir, "/", name)); + std::string res; + strm >> res; + return res; + }; + + std::string next; + std::string last; + + if (DirExists("rebase-merge")) { + next = ReadFile("rebase-merge/msgnum"); + last = ReadFile("rebase-merge/end"); + } else if (DirExists("rebase-apply")) { + next = ReadFile("rebase-apply/next"); + last = ReadFile("rebase-apply/last"); + } + + std::ostringstream res; + res << State(); + if (!next.empty() && !last.empty()) res << ' ' << next << '/' << last; + return res.str(); +} + +size_t CountRange(git_repository* repo, const std::string& range) { + git_revwalk* walk = nullptr; + VERIFY(!git_revwalk_new(&walk, repo)) << GitError(); + ON_SCOPE_EXIT(=) { git_revwalk_free(walk); }; + VERIFY(!git_revwalk_push_range(walk, range.c_str())) << GitError(); + size_t res = 0; + while (true) { + git_oid oid; + switch (git_revwalk_next(&oid, walk)) { + case 0: + ++res; + break; + case GIT_ITEROVER: + return res; + default: + LOG(ERROR) << "git_revwalk_next: " << range << ": " << GitError(); + throw Exception(); + } + } +} + +size_t NumStashes(git_repository* repo) { + size_t res = 0; + auto* cb = +[](size_t index, const char* message, const git_oid* stash_id, void* payload) { + ++*static_cast<size_t*>(payload); + return 0; + }; + if (!git_stash_foreach(repo, cb, &res)) return res; + // Example error: failed to parse signature - malformed e-mail. + // See https://github.com/romkatv/powerlevel10k/issues/216. + LOG(WARN) << "git_stash_foreach: " << GitError(); + return 0; +} + +git_reference* Head(git_repository* repo) { + git_reference* symbolic = nullptr; + switch (git_reference_lookup(&symbolic, repo, "HEAD")) { + case 0: + break; + case GIT_ENOTFOUND: + return nullptr; + default: + LOG(ERROR) << "git_reference_lookup: " << GitError(); + throw Exception(); + } + + git_reference* direct = nullptr; + if (git_reference_resolve(&direct, symbolic)) { + LOG(INFO) << "Empty git repo (no HEAD)"; + return symbolic; + } + git_reference_free(symbolic); + return direct; +} + +const char* LocalBranchName(const git_reference* ref) { + CHECK(ref); + git_reference_t type = git_reference_type(ref); + switch (type) { + case GIT_REFERENCE_DIRECT: { + return git_reference_is_branch(ref) ? git_reference_shorthand(ref) : ""; + } + case GIT_REFERENCE_SYMBOLIC: { + static constexpr char kHeadPrefix[] = "refs/heads/"; + const char* target = git_reference_symbolic_target(ref); + if (!target) return ""; + size_t len = std::strlen(target); + if (len < sizeof(kHeadPrefix)) return ""; + if (std::memcmp(target, kHeadPrefix, sizeof(kHeadPrefix) - 1)) return ""; + return target + (sizeof(kHeadPrefix) - 1); + } + case GIT_REFERENCE_INVALID: + case GIT_REFERENCE_ALL: + break; + } + LOG(ERROR) << "Invalid reference type: " << type; + throw Exception(); +} + +RemotePtr GetRemote(git_repository* repo, const git_reference* local) { + git_remote* remote; + git_buf symref = {}; + if (git_branch_remote(&remote, &symref, repo, git_reference_name(local))) return nullptr; + ON_SCOPE_EXIT(&) { + git_remote_free(remote); + git_buf_free(&symref); + }; + + git_reference* ref; + if (git_reference_lookup(&ref, repo, symref.ptr)) return nullptr; + ON_SCOPE_EXIT(&) { if (ref) git_reference_free(ref); }; + + const char* branch = nullptr; + std::string name = remote ? git_remote_name(remote) : "."; + if (git_branch_name(&branch, ref)) { + branch = ""; + } else if (remote) { + VERIFY(std::strstr(branch, name.c_str()) == branch); + VERIFY(branch[name.size()] == '/'); + branch += name.size() + 1; + } + + auto res = std::make_unique<Remote>(); + res->name = std::move(name); + res->branch = branch; + res->url = remote ? (git_remote_url(remote) ?: "") : ""; + res->ref = std::exchange(ref, nullptr); + return RemotePtr(res.release()); +} + +PushRemotePtr GetPushRemote(git_repository* repo, const git_reference* local) { + git_remote* remote; + git_buf symref = {}; + if (git_branch_push_remote(&remote, &symref, repo, git_reference_name(local))) return nullptr; + ON_SCOPE_EXIT(&) { + git_remote_free(remote); + git_buf_free(&symref); + }; + + git_reference* ref; + if (git_reference_lookup(&ref, repo, symref.ptr)) return nullptr; + ON_SCOPE_EXIT(&) { if (ref) git_reference_free(ref); }; + + std::string name = remote ? git_remote_name(remote) : "."; + + auto res = std::make_unique<PushRemote>(); + res->name = std::move(name); + res->url = remote ? (git_remote_url(remote) ?: "") : ""; + res->ref = std::exchange(ref, nullptr); + return PushRemotePtr(res.release()); +} + +} // namespace gitstatus |