251 lines
7.4 KiB
C++
251 lines
7.4 KiB
C++
// 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());
|
|
}
|
|
|
|
CommitMessage GetCommitMessage(git_repository* repo, const git_oid& id) {
|
|
git_commit* commit;
|
|
VERIFY(!git_commit_lookup(&commit, repo, &id)) << GitError();
|
|
ON_SCOPE_EXIT(=) { git_commit_free(commit); };
|
|
return {.encoding = git_commit_message_encoding(commit) ?: "",
|
|
.summary = git_commit_summary(commit) ?: ""};
|
|
}
|
|
|
|
} // namespace gitstatus
|