klayout/src/tl/tl/tlGit.cc

422 lines
11 KiB
C++

/*
KLayout Layout Viewer
Copyright (C) 2006-2023 Matthias Koefferlein
This program 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 2 of the License, or
(at your option) any later version.
This program 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 this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#if defined(HAVE_GIT2)
#include "tlGit.h"
#include "tlFileUtils.h"
#include "tlProgress.h"
#include "tlStaticObjects.h"
#include "tlLog.h"
#include <git2.h>
#include <cstdio>
namespace tl
{
// ---------------------------------------------------------------
// Library initialization helper
namespace {
class InitHelper
{
public:
InitHelper ()
{
git_libgit2_init ();
}
~InitHelper ()
{
git_libgit2_shutdown ();
}
static void ensure_initialized ()
{
if (ms_init) {
return;
}
ms_init = new InitHelper ();
tl::StaticObjects::reg (&ms_init);
}
private:
static InitHelper *ms_init;
};
InitHelper *InitHelper::ms_init = 0;
}
// ---------------------------------------------------------------
// GitCollection implementation
GitObject::GitObject (const std::string &local_path)
: m_local_path (local_path), m_is_temp (false)
{
InitHelper::ensure_initialized ();
if (local_path.empty ()) {
m_local_path = tl::tmpdir ("git2klayout");
m_is_temp = true;
}
// ensures the directory is clean
if (! m_is_temp) {
if (! tl::rm_dir_recursive (m_local_path)) {
throw tl::Exception (tl::to_string (tr ("Unable to clean local Git repo path: %s")), m_local_path);
}
if (! tl::mkpath (m_local_path)) {
throw tl::Exception (tl::to_string (tr ("Unable to regenerate local Git repo path: %s")), m_local_path);
}
}
}
GitObject::~GitObject ()
{
if (m_is_temp) {
tl::rm_dir_recursive (m_local_path);
}
}
static int
#if LIBGIT2_VER_MAJOR >= 1
fetch_progress (const git_indexer_progress *stats, void *payload)
#else
fetch_progress (const git_transfer_progress *stats, void *payload)
#endif
{
tl::RelativeProgress *progress = reinterpret_cast<tl::RelativeProgress *> (payload);
// first half of progress
size_t count = size_t (5000.0 * double (stats->received_objects) / double (std::max (1u, stats->total_objects)) + 1e-10);
try {
progress->set (count);
} catch (...) {
// TODO: stop
}
return 0;
}
static void
checkout_progress(const char * /*path*/, size_t cur, size_t tot, void *payload)
{
tl::RelativeProgress *progress = reinterpret_cast<tl::RelativeProgress *> (payload);
// first half of progress
size_t count = size_t (5000.0 * double (cur) / double (std::max (size_t (1), tot)) + 1e-10);
try {
progress->set (count + 5000u);
} catch (...) {
// ignore cancel requests (TODO: how to stop?)
}
}
static void check (int error)
{
if (error != 0) {
#if LIBGIT2_VER_MAJOR > 0 || (LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR >= 28)
const git_error *err = git_error_last ();
#else
const git_error *err = giterr_last ();
#endif
throw tl::Exception (tl::to_string (tr ("Error cloning Git repo: %s")), (const char *) err->message);
}
}
static bool
ref_matches (const char *name, const std::string &ref)
{
if (!name) {
return false;
} else if (name == ref) {
return true;
} else if (name == "refs/heads/" + ref) {
return true;
} else if (name == "refs/tags/" + ref) {
return true;
} else {
return false;
}
}
namespace
{
class GitBuffer
{
public:
GitBuffer ()
{
m_buf = GIT_BUF_INIT_CONST (NULL, 0);
}
~GitBuffer ()
{
#if LIBGIT2_VER_MAJOR > 0 || (LIBGIT2_VER_MAJOR == 0 && LIBGIT2_VER_MINOR >= 28)
git_buf_dispose (&m_buf);
#else
git_buf_free (&m_buf);
#endif
}
const char *c_str () const { return m_buf.ptr; }
git_buf *get () { return &m_buf; }
const git_buf *get () const { return &m_buf; }
private:
git_buf m_buf;
};
}
static void
checkout_branch (git_repository *repo, git_remote *remote, const git_checkout_options *co_opts, const char *branch)
{
GitBuffer remote_branch;
git_oid oid;
// if no branch is given, use the default branch
if (! branch) {
check (git_remote_default_branch (remote_branch.get (), remote));
branch = remote_branch.c_str ();
if (tl::verbosity () >= 10) {
tl::info << tr ("Git checkout: Using default branch for repository ") << git_remote_url (remote) << ": " << branch;
}
} else {
if (tl::verbosity () >= 10) {
tl::info << tr ("Git checkout: Checking out branch for repository ") << git_remote_url (remote) << ": " << branch;
}
}
// resolve the branch by using ls-remote:
size_t n = 0;
const git_remote_head **ls = NULL;
check (git_remote_ls (&ls, &n, remote));
if (tl::verbosity () >= 20) {
tl::info << "Git checkout: ls-remote on " << git_remote_url (remote) << ":";
}
bool found = false;
for (size_t i = 0; i < n; ++i) {
const git_remote_head *rh = ls[i];
if (tl::verbosity () >= 20) {
char oid_fmt [80];
git_oid_tostr (oid_fmt, sizeof (oid_fmt), &rh->oid);
tl::info << " " << rh->name << ": " << (const char *) oid_fmt;
}
if (ref_matches (rh->name, branch)) {
oid = rh->oid;
found = true;
}
}
if (! found) {
throw tl::Exception (tl::to_string (tr ("Git checkout - Unable to resolve reference name: ")) + branch);
}
if (tl::verbosity () >= 10) {
char oid_fmt [80];
git_oid_tostr (oid_fmt, sizeof (oid_fmt), &oid);
tl::info << tr ("Git checkout: resolving ") << branch << tr (" to ") << (const char *) oid_fmt;
}
check (git_repository_set_head_detached (repo, &oid));
check (git_checkout_head (repo, co_opts));
}
int
credentials_cb (git_credential ** /*out*/, const char * /*url*/, const char * /*username*/, unsigned int /*allowed_types*/, void *)
{
// no credentials aquired
git_error_set_str (GIT_ERROR_NONE, "anonymous access is supported only, but server requests credentials");
return GIT_EUSER;
}
void
GitObject::read (const std::string &org_url, const std::string &org_filter, const std::string &subfolder, const std::string &branch, double timeout, tl::InputHttpStreamCallback *callback)
{
std::string url = org_url;
std::string filter = org_filter;
if (! subfolder.empty ()) {
if (filter.empty ()) {
filter = subfolder + "/**";
} else {
filter = subfolder + "/" + filter;
}
}
// TODO: use callback, timeout?
tl::RelativeProgress progress (tl::to_string (tr ("Download progress")), 10000, 1 /*yield always*/);
// build checkout options
git_checkout_options checkout_opts = GIT_CHECKOUT_OPTIONS_INIT;
checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
const char *paths_cstr[1];
paths_cstr[0] = filter.c_str ();
if (! filter.empty ()) {
checkout_opts.paths.count = 1;
checkout_opts.paths.strings = (char **) &paths_cstr;
}
checkout_opts.progress_cb = &checkout_progress;
checkout_opts.progress_payload = (void *) &progress;
// build fetch options
git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;
fetch_opts.download_tags = GIT_REMOTE_DOWNLOAD_TAGS_AUTO;
#if LIBGIT2_VER_MAJOR > 1 || (LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR >= 7)
fetch_opts.depth = 1; // shallow (single commit)
#endif
fetch_opts.callbacks.transfer_progress = &fetch_progress;
fetch_opts.callbacks.credentials = &credentials_cb;
fetch_opts.callbacks.payload = (void *) &progress;
// build refspecs in case they are needed
char *refs[] = { (char *) branch.c_str () };
git_strarray refspecs;
refspecs.count = 1;
refspecs.strings = refs;
git_strarray *refspecs_p = branch.empty () ? NULL : &refspecs;
// Make repository
git_repository *cloned_repo = NULL;
git_remote *remote = NULL;
try {
check (git_repository_init (&cloned_repo, m_local_path.c_str (), 0));
check (git_remote_create (&remote, cloned_repo, "download", url.c_str ()));
// actually fetch
if (tl::verbosity () >= 10) {
tl::info << tr ("Fetching Git repo from ") << git_remote_url (remote) << " ...";
}
check (git_remote_fetch (remote, refspecs_p, &fetch_opts, NULL));
// checkout
checkout_branch (cloned_repo, remote, &checkout_opts, branch.empty () ? 0 : branch.c_str ());
// free the repo and remote
git_repository_free (cloned_repo);
git_remote_free (remote);
// get rid of ".git" - we do not need it anymore
tl::rm_dir_recursive (tl::combine_path (m_local_path, ".git"));
} catch (...) {
// free the repo in the error case
if (cloned_repo != NULL) {
git_repository_free (cloned_repo);
}
if (remote != NULL) {
git_remote_free (remote);
}
throw;
}
// pull subfolder files to target path level
if (! subfolder.empty ()) {
std::string pp = tl::combine_path (m_local_path, subfolder);
if (! tl::is_dir (pp)) {
throw tl::Exception (tl::to_string (tr ("Error cloning Git repo - failed to fetch subdirectory: ")) + pp);
}
// rename the source to a temporary folder so we don't overwrite the source folder with something from within
std::string tmp_dir = "__temp__";
for (unsigned int i = 0; ; ++i) {
if (! tl::file_exists (tl::combine_path (m_local_path, tmp_dir + tl::to_string (i)))) {
tmp_dir += tl::to_string (i);
break;
}
}
auto pc = tl::split (subfolder, "/");
if (! tl::rename_file (tl::combine_path (m_local_path, pc.front ()), tmp_dir)) {
throw tl::Exception (tl::to_string (tr ("Error cloning Git repo - failed to rename temp folder")));
}
pc.front () = tmp_dir;
if (! tl::mv_dir_recursive (tl::combine_path (m_local_path, tl::join (pc, "/")), m_local_path)) {
throw tl::Exception (tl::to_string (tr ("Error cloning Git repo - failed to move subdir components")));
}
}
}
bool
GitObject::download (const std::string &url, const std::string &target, const std::string &subfolder, const std::string &branch, double timeout, tl::InputHttpStreamCallback *callback)
{
try {
GitObject obj (target);
obj.read (url, std::string (), subfolder, branch, timeout, callback);
return true;
} catch (tl::Exception &ex) {
tl::error << tl::sprintf (tl::to_string (tr ("Error downloading Git repo from %s (subdir '%s', ref '%s')")), url, subfolder, branch);
return false;
}
}
tl::InputStream *
GitObject::download_item (const std::string &url, const std::string &file, const std::string &subfolder, const std::string &branch, double timeout, tl::InputHttpStreamCallback *callback)
{
GitObject obj;
obj.read (url, file, subfolder, branch, timeout, callback);
// extract the file and return a memory blob, so we can delete the temp folder
tl::InputStream file_stream (tl::combine_path (obj.local_path (), file));
std::string data = file_stream.read_all ();
char *data_copy = new char [data.size ()];
memcpy (data_copy, data.c_str (), data.size ());
return new tl::InputStream (new tl::InputMemoryStream (data_copy, data.size (), true));
}
}
#endif