#include "git-utils.hh" #include "input-accessor.hh" #include "cache.hh" #include "finally.hh" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace std { template<> struct hash { size_t operator()(const git_oid & oid) const { return * (size_t *) oid.id; } }; } std::ostream & operator << (std::ostream & str, const git_oid & oid) { str << git_oid_tostr_s(&oid); return str; } bool operator == (const git_oid & oid1, const git_oid & oid2) { return git_oid_equal(&oid1, &oid2); } namespace nix { // Some wrapper types that ensure that the git_*_free functions get called. template struct Deleter { template void operator()(T * p) const { del(p); }; }; typedef std::unique_ptr> Repository; typedef std::unique_ptr> TreeEntry; typedef std::unique_ptr> Tree; typedef std::unique_ptr> TreeBuilder; typedef std::unique_ptr> Blob; typedef std::unique_ptr> Object; typedef std::unique_ptr> Commit; typedef std::unique_ptr> Reference; typedef std::unique_ptr> DescribeResult; typedef std::unique_ptr> StatusList; typedef std::unique_ptr> Remote; typedef std::unique_ptr> GitConfig; typedef std::unique_ptr> ConfigIterator; // A helper to ensure that we don't leak objects returned by libgit2. template struct Setter { T & t; typename T::pointer p = nullptr; Setter(T & t) : t(t) { } ~Setter() { if (p) t = T(p); } operator typename T::pointer * () { return &p; } }; Hash toHash(const git_oid & oid) { #ifdef GIT_EXPERIMENTAL_SHA256 assert(oid.type == GIT_OID_SHA1); #endif Hash hash(htSHA1); memcpy(hash.hash, oid.id, hash.hashSize); return hash; } static void initLibGit2() { if (git_libgit2_init() < 0) throw Error("initialising libgit2: %s", git_error_last()->message); } git_oid hashToOID(const Hash & hash) { git_oid oid; if (git_oid_fromstr(&oid, hash.gitRev().c_str())) throw Error("cannot convert '%s' to a Git OID", hash.gitRev()); return oid; } Object lookupObject(git_repository * repo, const git_oid & oid) { Object obj; if (git_object_lookup(Setter(obj), repo, &oid, GIT_OBJECT_ANY)) { auto err = git_error_last(); throw Error("getting Git object '%s': %s", oid, err->message); } return obj; } template T peelObject(git_repository * repo, git_object * obj, git_object_t type) { T obj2; if (git_object_peel((git_object * *) (typename T::pointer *) Setter(obj2), obj, type)) { auto err = git_error_last(); throw Error("peeling Git object '%s': %s", git_object_id(obj), err->message); } return obj2; } int statusCallbackTrampoline(const char * path, unsigned int statusFlags, void * payload) { return (*((std::function *) payload))(path, statusFlags); } struct GitRepoImpl : GitRepo, std::enable_shared_from_this { CanonPath path; Repository repo; GitRepoImpl(CanonPath _path, bool create, bool bare) : path(std::move(_path)) { initLibGit2(); if (pathExists(path.abs())) { if (git_repository_open(Setter(repo), path.c_str())) throw Error("opening Git repository '%s': %s", path, git_error_last()->message); } else { if (git_repository_init(Setter(repo), path.c_str(), bare)) throw Error("creating Git repository '%s': %s", path, git_error_last()->message); } } operator git_repository * () { return repo.get(); } uint64_t getRevCount(const Hash & rev) override { std::unordered_set done; std::queue todo; todo.push(peelObject(*this, lookupObject(*this, hashToOID(rev)).get(), GIT_OBJECT_COMMIT)); while (auto commit = pop(todo)) { if (!done.insert(*git_commit_id(commit->get())).second) continue; for (size_t n = 0; n < git_commit_parentcount(commit->get()); ++n) { git_commit * parent; if (git_commit_parent(&parent, commit->get(), n)) throw Error("getting parent of Git commit '%s': %s", *git_commit_id(commit->get()), git_error_last()->message); todo.push(Commit(parent)); } } return done.size(); } uint64_t getLastModified(const Hash & rev) override { auto commit = peelObject(*this, lookupObject(*this, hashToOID(rev)).get(), GIT_OBJECT_COMMIT); return git_commit_time(commit.get()); } bool isShallow() override { return git_repository_is_shallow(*this); } Hash resolveRef(std::string ref) override { // Handle revisions used as refs. { git_oid oid; if (git_oid_fromstr(&oid, ref.c_str()) == 0) return toHash(oid); } // Resolve short names like 'master'. Reference ref2; if (!git_reference_dwim(Setter(ref2), *this, ref.c_str())) ref = git_reference_name(ref2.get()); // Resolve full references like 'refs/heads/master'. Reference ref3; if (git_reference_lookup(Setter(ref3), *this, ref.c_str())) throw Error("resolving Git reference '%s': %s", ref, git_error_last()->message); auto oid = git_reference_target(ref3.get()); if (!oid) throw Error("cannot get OID for Git reference '%s'", git_reference_name(ref3.get())); return toHash(*oid); } std::vector parseSubmodules(const CanonPath & configFile) { GitConfig config; if (git_config_open_ondisk(Setter(config), configFile.abs().c_str())) throw Error("parsing .gitmodules file: %s", git_error_last()->message); ConfigIterator it; if (git_config_iterator_glob_new(Setter(it), config.get(), "^submodule\\..*\\.(path|url|branch)$")) throw Error("iterating over .gitmodules: %s", git_error_last()->message); std::map entries; while (true) { git_config_entry * entry = nullptr; if (auto err = git_config_next(&entry, it.get())) { if (err == GIT_ITEROVER) break; throw Error("iterating over .gitmodules: %s", git_error_last()->message); } entries.emplace(entry->name + 10, entry->value); } std::vector result; for (auto & [key, value] : entries) { if (!hasSuffix(key, ".path")) continue; std::string key2(key, 0, key.size() - 5); auto path = CanonPath(value); result.push_back(Submodule { .path = path, .url = entries[key2 + ".url"], .branch = entries[key2 + ".branch"], }); } return result; } WorkdirInfo getWorkdirInfo() override { WorkdirInfo info; /* Get the head revision, if any. */ git_oid headRev; if (auto err = git_reference_name_to_id(&headRev, *this, "HEAD")) { if (err != GIT_ENOTFOUND) throw Error("resolving HEAD: %s", git_error_last()->message); } else info.headRev = toHash(headRev); /* Get all tracked files and determine whether the working directory is dirty. */ std::function statusCallback = [&](const char * path, unsigned int statusFlags) { if (!(statusFlags & GIT_STATUS_INDEX_DELETED) && !(statusFlags & GIT_STATUS_WT_DELETED)) info.files.insert(CanonPath(path)); if (statusFlags != GIT_STATUS_CURRENT) info.isDirty = true; return 0; }; git_status_options options = GIT_STATUS_OPTIONS_INIT; options.flags |= GIT_STATUS_OPT_INCLUDE_UNMODIFIED; options.flags |= GIT_STATUS_OPT_EXCLUDE_SUBMODULES; if (git_status_foreach_ext(*this, &options, &statusCallbackTrampoline, &statusCallback)) throw Error("getting working directory status: %s", git_error_last()->message); /* Get submodule info. */ auto modulesFile = path + ".gitmodules"; if (pathExists(modulesFile.abs())) info.submodules = parseSubmodules(modulesFile); return info; } std::optional getWorkdirRef() override { Reference ref; if (git_reference_lookup(Setter(ref), *this, "HEAD")) throw Error("looking up HEAD: %s", git_error_last()->message); if (auto target = git_reference_symbolic_target(ref.get())) return target; return std::nullopt; } std::vector> getSubmodules(const Hash & rev) override; std::string resolveSubmoduleUrl(const std::string & url) override { git_buf buf = GIT_BUF_INIT; if (git_submodule_resolve_url(&buf, *this, url.c_str())) throw Error("resolving Git submodule URL '%s'", url); Finally cleanup = [&]() { git_buf_dispose(&buf); }; return buf.ptr; } bool hasObject(const Hash & oid_) override { auto oid = hashToOID(oid_); Object obj; if (auto errCode = git_object_lookup(Setter(obj), *this, &oid, GIT_OBJECT_ANY)) { if (errCode == GIT_ENOTFOUND) return false; auto err = git_error_last(); throw Error("getting Git object '%s': %s", oid, err->message); } return true; } ref getAccessor(const Hash & rev) override; void fetch( const std::string & url, const std::string & refspec) override { /* FIXME: use libgit2. Unfortunately, it doesn't support ssh_config at the moment. */ #if 0 Remote remote; if (git_remote_create_anonymous(Setter(remote), *this, url.c_str())) throw Error("cannot create Git remote '%s': %s", url, git_error_last()->message); char * refspecs[] = {(char *) refspec.c_str()}; git_strarray refspecs2 { .strings = refspecs, .count = 1 }; if (git_remote_fetch(remote.get(), &refspecs2, nullptr, nullptr)) throw Error("fetching '%s' from '%s': %s", refspec, url, git_error_last()->message); #endif // FIXME: git stderr messes up our progress indicator, so // we're using --quiet for now. Should process its stderr. runProgram("git", true, { "-C", path.abs(), "--bare", "fetch", "--quiet", "--force", "--", url, refspec }, {}, true); } }; ref GitRepo::openRepo(const CanonPath & path, bool create, bool bare) { return make_ref(path, create, bare); } struct GitInputAccessor : InputAccessor { ref repo; Tree root; GitInputAccessor(ref repo_, const Hash & rev) : repo(repo_) , root(peelObject(*repo, lookupObject(*repo, hashToOID(rev)).get(), GIT_OBJECT_TREE)) { } std::string readBlob(const CanonPath & path, bool symlink) { auto blob = getBlob(path, symlink); auto data = std::string_view((const char *) git_blob_rawcontent(blob.get()), git_blob_rawsize(blob.get())); return std::string(data); } std::string readFile(const CanonPath & path) override { return readBlob(path, false); } bool pathExists(const CanonPath & path) override { return path.isRoot() ? true : (bool) lookup(path); } Stat lstat(const CanonPath & path) override { if (path.isRoot()) return Stat { .type = tDirectory }; auto entry = need(path); auto mode = git_tree_entry_filemode(entry); if (mode == GIT_FILEMODE_TREE) return Stat { .type = tDirectory }; else if (mode == GIT_FILEMODE_BLOB) return Stat { .type = tRegular }; else if (mode == GIT_FILEMODE_BLOB_EXECUTABLE) return Stat { .type = tRegular, .isExecutable = true }; else if (mode == GIT_FILEMODE_LINK) return Stat { .type = tSymlink }; else if (mode == GIT_FILEMODE_COMMIT) // Treat submodules as an empty directory. return Stat { .type = tDirectory }; else throw Error("file '%s' has an unsupported Git file type"); } DirEntries readDirectory(const CanonPath & path) override { return std::visit(overloaded { [&](Tree tree) { DirEntries res; auto count = git_tree_entrycount(tree.get()); for (size_t n = 0; n < count; ++n) { auto entry = git_tree_entry_byindex(tree.get(), n); // FIXME: add to cache res.emplace(std::string(git_tree_entry_name(entry)), DirEntry{}); } return res; }, [&](Submodule) { return DirEntries(); } }, getTree(path)); } std::string readLink(const CanonPath & path) override { return readBlob(path, true); } Hash getSubmoduleRev(const CanonPath & path) { auto entry = need(path); if (git_tree_entry_type(entry) != GIT_OBJECT_COMMIT) throw Error("'%s' is not a submodule", showPath(path)); return toHash(*git_tree_entry_id(entry)); } std::map lookupCache; /* Recursively look up 'path' relative to the root. */ git_tree_entry * lookup(const CanonPath & path) { if (path.isRoot()) return nullptr; auto i = lookupCache.find(path); if (i == lookupCache.end()) { TreeEntry entry; if (auto err = git_tree_entry_bypath(Setter(entry), root.get(), std::string(path.rel()).c_str())) { if (err != GIT_ENOTFOUND) throw Error("looking up '%s': %s", showPath(path), git_error_last()->message); } i = lookupCache.emplace(path, std::move(entry)).first; } return &*i->second; } git_tree_entry * need(const CanonPath & path) { auto entry = lookup(path); if (!entry) throw Error("'%s' does not exist", showPath(path)); return entry; } struct Submodule { }; std::variant getTree(const CanonPath & path) { if (path.isRoot()) { Tree tree; if (git_tree_dup(Setter(tree), root.get())) throw Error("duplicating directory '%s': %s", showPath(path), git_error_last()->message); return tree; } auto entry = need(path); if (git_tree_entry_type(entry) == GIT_OBJECT_COMMIT) return Submodule(); if (git_tree_entry_type(entry) != GIT_OBJECT_TREE) throw Error("'%s' is not a directory", showPath(path)); Tree tree; if (git_tree_entry_to_object((git_object * *) (git_tree * *) Setter(tree), *repo, entry)) throw Error("looking up directory '%s': %s", showPath(path), git_error_last()->message); return tree; } Blob getBlob(const CanonPath & path, bool expectSymlink) { auto notExpected = [&]() { throw Error( expectSymlink ? "'%s' is not a symlink" : "'%s' is not a regular file", showPath(path)); }; if (path.isRoot()) notExpected(); auto entry = need(path); if (git_tree_entry_type(entry) != GIT_OBJECT_BLOB) notExpected(); auto mode = git_tree_entry_filemode(entry); if (expectSymlink) { if (mode != GIT_FILEMODE_LINK) notExpected(); } else { if (mode != GIT_FILEMODE_BLOB && mode != GIT_FILEMODE_BLOB_EXECUTABLE) notExpected(); } Blob blob; if (git_tree_entry_to_object((git_object * *) (git_blob * *) Setter(blob), *repo, entry)) throw Error("looking up file '%s': %s", showPath(path), git_error_last()->message); return blob; } }; ref GitRepoImpl::getAccessor(const Hash & rev) { return make_ref(ref(shared_from_this()), rev); } std::vector> GitRepoImpl::getSubmodules(const Hash & rev) { /* Read the .gitmodules files from this revision. */ CanonPath modulesFile(".gitmodules"); auto accessor = getAccessor(rev); if (!accessor->pathExists(modulesFile)) return {}; /* Parse it and get the revision of each submodule. */ auto configS = accessor->readFile(modulesFile); auto [fdTemp, pathTemp] = createTempFile("nix-git-submodules"); writeFull(fdTemp.get(), configS); std::vector> result; for (auto & submodule : parseSubmodules(CanonPath(pathTemp))) { auto rev = accessor.dynamic_pointer_cast()->getSubmoduleRev(submodule.path); result.push_back({std::move(submodule), rev}); } return result; } }