aboutsummaryrefslogtreecommitdiff
path: root/bpkg/fetch-git.cxx
diff options
context:
space:
mode:
Diffstat (limited to 'bpkg/fetch-git.cxx')
-rw-r--r--bpkg/fetch-git.cxx725
1 files changed, 608 insertions, 117 deletions
diff --git a/bpkg/fetch-git.cxx b/bpkg/fetch-git.cxx
index 0c21af6..d2c30a1 100644
--- a/bpkg/fetch-git.cxx
+++ b/bpkg/fetch-git.cxx
@@ -5,12 +5,11 @@
#include <map>
-#include <libbutl/git.mxx>
-#include <libbutl/utility.mxx> // digit(), xdigit()
-#include <libbutl/filesystem.mxx> // path_entry
-#include <libbutl/path-pattern.mxx>
-#include <libbutl/semantic-version.mxx>
-#include <libbutl/standard-version.mxx> // parse_standard_version()
+#include <libbutl/git.hxx>
+#include <libbutl/filesystem.hxx> // path_entry
+#include <libbutl/path-pattern.hxx>
+#include <libbutl/semantic-version.hxx>
+#include <libbutl/standard-version.hxx> // parse_standard_version()
#include <bpkg/diagnostics.hxx>
@@ -286,20 +285,7 @@ namespace bpkg
try
{
- ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
-
- // We could probably write something like this, instead:
- //
- // *diag_stream << is.rdbuf () << flush;
- //
- // However, it would never throw and we could potentially miss the
- // reading failure, unless we decide to additionally mess with the
- // diagnostics stream exception mask.
- //
- for (string l; !eof (getline (is, l)); )
- *diag_stream << l << endl;
-
- is.close ();
+ dump_stderr (move (pipe.in));
// Fall through.
}
@@ -550,7 +536,11 @@ namespace bpkg
// For HTTP(S) sense the protocol type by sending the first HTTP request of
// the fetch operation handshake and analyzing the first line of the
// response. Fail if connecting to the server failed, the response code
- // differs from 200, or reading the response body failed.
+ // differs from 200 and 401, or reading the response body failed. If the
+ // response code is 401 (requires authentication), then consider protocol as
+ // smart. The thinking here is that a git repository with support for
+ // authentication is likely one of the hosting places (like git{hub,lab})
+ // and is unlikely to be dumb.
//
// Note that, as a side-effect, this function checks the HTTP(S) server
// availability and so must be called prior to any git command that involves
@@ -566,21 +556,16 @@ namespace bpkg
// URLs, if possible. That's why the function requires the git version
// parameter.
//
- enum class capabilities
- {
- dumb, // No shallow clone support.
- smart, // Support for shallow clone, but not for unadvertised refs fetch.
- unadv // Support for shallow clone and for unadvertised refs fetch.
- };
+ using capabilities = git_protocol_capabilities;
static capabilities
sense_capabilities (const common_options& co,
- repository_url url,
+ const repository_url& repo_url,
const semantic_version& git_ver)
{
- assert (url.path);
+ assert (repo_url.path);
- switch (url.scheme)
+ switch (repo_url.scheme)
{
case repository_protocol::git:
case repository_protocol::ssh:
@@ -589,6 +574,9 @@ namespace bpkg
case repository_protocol::https: break; // Ask the server (see below).
}
+ // Craft the URL for sensing the capabilities.
+ //
+ repository_url url (repo_url);
path& up (*url.path);
if (!up.to_directory ())
@@ -602,19 +590,94 @@ namespace bpkg
url.query = "service=git-upload-pack";
string u (url.string ());
- process pr (start_fetch (co,
- u,
- path () /* out */,
- "git/" + git_ver.string ()));
+
+ // Start fetching, also trying to retrieve the HTTP status code.
+ //
+ // We unset failbit to properly handle an empty response (no refs) from
+ // the dumb server.
+ //
+ ifdstream is (ifdstream::badbit);
+
+ pair<process, uint16_t> ps (
+ start_fetch_http (co,
+ u,
+ is /* out */,
+ fdstream_mode::skip | fdstream_mode::binary,
+ stderr_mode::redirect_quiet,
+ "git/" + git_ver.string ()));
+
+ process& pr (ps.first);
+
+ // If the fetch program stderr is redirected, then read it out and pass
+ // through.
+ //
+ auto dump_stderr = [&pr] ()
+ {
+ if (pr.in_efd != nullfd)
+ try
+ {
+ bpkg::dump_stderr (move (pr.in_efd));
+ }
+ catch (const io_error&)
+ {
+ // Not much we can do here.
+ }
+ };
try
{
- // We unset failbit to properly handle an empty response (no refs) from
- // the dumb server.
+ // If authentication is required (HTTP status code is 401), then
+ // consider the protocol as smart. Drop the diagnostics if that's the
+ // case and dump it otherwise.
//
- ifdstream is (move (pr.in_ofd),
- fdstream_mode::skip | fdstream_mode::binary,
- ifdstream::badbit);
+ if (ps.second == 401)
+ {
+ if (verb >= 2)
+ {
+ info << "smart git protocol assumed for repository " << repo_url
+ << " due to authentication requirement" <<
+ info << "use --git-capabilities to override or suppress this "
+ << "diagnostics";
+ }
+
+ // Note that we don't care about the process exit code here and just
+ // silently wait for the process completion in the process object
+ // destructor. We, however, close the stream (reading out the
+ // content), so that the process won't get blocked writing to it.
+ //
+ // Also note that we drop the potentially redirected process stderr
+ // stream content. We even don't read it out, since we assume it fully
+ // fits into the pipe buffer.
+ //
+ is.close ();
+
+ return capabilities::smart;
+ }
+
+ // Fail on any other HTTP error (e.g., 404). In the case of a success
+ // code other than 200 (e.g. 204 (No Content)) just let the capabilities
+ // detection to take its course.
+ //
+ if (ps.second != 0 && (ps.second < 200 || ps.second >= 300))
+ {
+ // Note that we don't care about the process exit code here (see above
+ // for the reasoning).
+ //
+ is.close ();
+
+ // Dump the potentially redirected process stderr stream content since
+ // it may be helpful to the user.
+ //
+ // Note, however, that we don't know if it really contains the error
+ // description since the fetch program may even exit successfully (see
+ // start_fetch_http() for details). Thus, we additionally print the
+ // HTTP status code in the diagnostics.
+ //
+ dump_stderr ();
+
+ fail << "unable to fetch " << url <<
+ info << "HTTP status code " << ps.second << endg;
+ }
string l;
getline (is, l); // Is empty if no refs returned by the dumb server.
@@ -640,7 +703,7 @@ namespace bpkg
? capabilities::smart
: capabilities::dumb);
- // If the transport is smart let's see it the server also supports
+ // If the transport is smart let's see if the server also supports
// unadvertised refs fetch.
//
if (r == capabilities::smart && !is.eof ())
@@ -668,6 +731,8 @@ namespace bpkg
is.close ();
+ dump_stderr ();
+
if (pr.wait ())
return r;
@@ -675,6 +740,8 @@ namespace bpkg
}
catch (const io_error&)
{
+ dump_stderr ();
+
if (pr.wait ())
fail << "unable to read fetched " << url << endg;
@@ -874,28 +941,29 @@ namespace bpkg
if (i != repository_refs.end ())
return i->second;
- if (verb && !co.no_progress ())
+ if ((verb && !co.no_progress ()) || co.progress ())
text << "querying " << url;
refs rs;
- fdpipe pipe (open_pipe ());
-
- // Note: ls-remote doesn't print anything to stderr, so no progress
- // suppression is required.
- //
- process pr (start_git (co,
- pipe, 2 /* stderr */,
- timeout_opts (co, url.scheme),
- co.git_option (),
- "ls-remote",
- to_git_url (url)));
-
- // Shouldn't throw, unless something is severely damaged.
- //
- pipe.out.close ();
for (;;) // Breakout loop.
{
+ fdpipe pipe (open_pipe ());
+
+ // Note: ls-remote doesn't print anything to stderr, so no progress
+ // suppression is required.
+ //
+ process pr (start_git (co,
+ pipe, 2 /* stderr */,
+ timeout_opts (co, url.scheme),
+ co.git_option (),
+ "ls-remote",
+ to_git_url (url)));
+
+ // Shouldn't throw, unless something is severely damaged.
+ //
+ pipe.out.close ();
+
try
{
ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
@@ -1084,7 +1152,25 @@ namespace bpkg
// the first call, and so git version get assigned (and checked).
//
if (!cap)
- cap = sense_capabilities (co, url (), git_ver);
+ {
+ const repository_url& u (url ());
+
+ // Check if the protocol capabilities are overridden for this
+ // repository.
+ //
+ const git_capabilities_map& gcs (co.git_capabilities ());
+
+ if (!gcs.empty () && u.scheme != repository_protocol::file)
+ {
+ auto i (gcs.find_sup (u.string ()));
+
+ if (i != gcs.end ())
+ cap = i->second;
+ }
+
+ if (!cap)
+ cap = sense_capabilities (co, u, git_ver);
+ }
return *cap;
};
@@ -1402,7 +1488,7 @@ namespace bpkg
catch (const logic_error&)
{
fail << "'" << s << "' doesn't appear to contain a git commit "
- "timestamp" << endg;
+ << "timestamp" << endg;
}
}
@@ -1505,7 +1591,7 @@ namespace bpkg
if (progress)
{
- if (verb == 1 && stderr_term)
+ if ((verb == 1 && stderr_term) || co.progress ())
v.push_back ("--progress");
}
else
@@ -1557,7 +1643,7 @@ namespace bpkg
// Print progress.
//
- if (verb && !co.no_progress ())
+ if ((verb && !co.no_progress ()) || co.progress ())
{
// Note that the clone command prints the following line prior to the
// progress lines:
@@ -1581,7 +1667,7 @@ namespace bpkg
dr << "from " << url ();
if (verb >= 2)
- dr << " in '" << dir.posix_string () << "'"; // Is used by tests.
+ dr << " in '" << dir.string () << "'"; // Used by tests.
}
// Print information messages prior to the deep fetching.
@@ -1702,6 +1788,255 @@ namespace bpkg
submodule_failure (d, prefix, e);
};
+ // Use git-config to obtain the submodules names/paths and then
+ // git-ls-files to obtain their commits.
+ //
+ // Note that previously we used git-submodule--helper-list subcommand to
+ // obtain the submodules commits/paths and then git-submodule--helper-name
+ // to obtain their names. However, git 2.38 has removed these subcommands.
+
+ // Obtain the submodules names/paths.
+ //
+ for (;;) // Breakout loop.
+ {
+ fdpipe pipe (open_pipe ());
+
+ process pr (start_git (co,
+ pipe, 2 /* stderr */,
+ co.git_option (),
+ "-C", dir,
+ "config",
+ "--list",
+ "--file", gitmodules_file,
+ "-z"));
+
+ // Shouldn't throw, unless something is severely damaged.
+ //
+ pipe.out.close ();
+
+ try
+ {
+ ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
+
+ for (string l; !eof (getline (is, l, '\0')); )
+ {
+ auto bad = [&l] ()
+ {
+ throw runtime_error ("invalid submodule option '" + l + '\'');
+ };
+
+ // The submodule configuration option line is NULL-terminated and
+ // has the following form:
+ //
+ // submodule.<submodule-name>.<option-name><NEWLINE><value>
+ //
+ // For example:
+ //
+ // submodule.style.path
+ // doc/style
+ //
+ l4 ([&]{trace << "submodule option: " << l;});
+
+ // If this is a submodule path option, then extract its name and
+ // path and add the entry to the resulting list.
+ //
+ size_t n (l.find ('\n'));
+
+ if (n != string::npos &&
+ n >= 15 &&
+ l.compare (0, 10, "submodule.") == 0 &&
+ l.compare (n - 5, 5, ".path") == 0)
+ {
+ string nm (l, 10, n - 15);
+ dir_path p (l, n + 1, l.size () - n - 1);
+
+ // For good measure verify that the name and path are not empty.
+ //
+ if (nm.empty () || p.empty ())
+ bad ();
+
+ r.push_back (submodule {move (p), move (nm), empty_string});
+ }
+ }
+
+ is.close ();
+
+ if (pr.wait ())
+ break;
+
+ // Fall through.
+ }
+ catch (const invalid_path& e)
+ {
+ if (pr.wait ())
+ failure ("invalid submodule directory path '" + e.path + '\'');
+
+ // Fall through.
+ }
+ catch (const io_error& e)
+ {
+ if (pr.wait ())
+ failure ("unable to read submodule options", &e);
+
+ // Fall through.
+ }
+ // Note that the io_error class inherits from the runtime_error class,
+ // so this catch-clause must go last.
+ //
+ catch (const runtime_error& e)
+ {
+ if (pr.wait ())
+ failure (e.what ());
+
+ // Fall through.
+ }
+
+ // We should only get here if the child exited with an error status.
+ //
+ assert (!pr.wait ());
+
+ failure ("unable to list submodule options");
+ }
+
+ // Note that we could potentially bail out here if the submodules list is
+ // empty. Let's however continue and verify that via git-ls-files, for
+ // good measure.
+
+ // Complete the resulting submodules information with their commits.
+ //
+ for (;;) // Breakout loop.
+ {
+ fdpipe pipe (open_pipe ());
+
+ process pr (start_git (co,
+ pipe, 2 /* stderr */,
+ co.git_option (),
+ "-C", dir,
+ "ls-files",
+ "--stage",
+ "-z"));
+
+ // Shouldn't throw, unless something is severely damaged.
+ //
+ pipe.out.close ();
+
+ try
+ {
+ ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
+
+ for (string l; !eof (getline (is, l, '\0')); )
+ {
+ auto bad = [&l] ()
+ {
+ throw runtime_error ("invalid file description '" + l + '\'');
+ };
+
+ // The line describing a file is NULL-terminated and has the
+ // following form:
+ //
+ // <mode><SPACE><object><SPACE><stage><TAB><path>
+ //
+ // The mode is a 6-digit octal representation of the file type and
+ // permission bits mask. For a submodule directory it is 160000 (see
+ // git index format documentation for gitlink object type). For
+ // example:
+ //
+ // 160000 59dcc1bea3509e37b65905ac472f86f4c55eb510 0 doc/style
+ //
+ if (!(l.size () > 50 && l[48] == '0' && l[49] == '\t'))
+ bad ();
+
+ // For submodules permission bits are always zero, so we can match
+ // the mode as a string.
+ //
+ if (l.compare (0, 6, "160000") == 0)
+ {
+ l4 ([&]{trace << "submodule: " << l;});
+
+ dir_path d (l, 50, l.size () - 50);
+
+ auto i (find_if (r.begin (), r.end (),
+ [&d] (const submodule& sm) {return sm.path == d;}));
+
+ if (i == r.end ())
+ bad ();
+
+ i->commit = string (l, 7, 40);
+ }
+ }
+
+ is.close ();
+
+ if (pr.wait ())
+ break;
+
+ // Fall through.
+ }
+ catch (const invalid_path& e)
+ {
+ if (pr.wait ())
+ failure ("invalid submodule directory path '" + e.path + '\'');
+
+ // Fall through.
+ }
+ catch (const io_error& e)
+ {
+ if (pr.wait ())
+ failure ("unable to read repository file list", &e);
+
+ // Fall through.
+ }
+ // Note that the io_error class inherits from the runtime_error class,
+ // so this catch-clause must go last.
+ //
+ catch (const runtime_error& e)
+ {
+ if (pr.wait ())
+ failure (e.what ());
+
+ // Fall through.
+ }
+
+ // We should only get here if the child exited with an error status.
+ //
+ assert (!pr.wait ());
+
+ failure ("unable to list repository files");
+ }
+
+ // Make sure that we have deduced commits for all the submodules.
+ //
+ for (const submodule& sm: r)
+ {
+ if (sm.commit.empty ())
+ failure ("unable to deduce commit for submodule " + sm.name);
+ }
+
+ return r;
+ }
+
+ // @@ TMP Old, submodule--helper-{list,name} subcommands-based,
+ // implementation of find_submodules().
+ //
+#if 0
+ static submodules
+ find_submodules (const common_options& co,
+ const dir_path& dir,
+ const dir_path& prefix,
+ bool gitmodules = true)
+ {
+ tracer trace ("find_submodules");
+
+ submodules r;
+
+ if (gitmodules && !exists (dir / gitmodules_file))
+ return r;
+
+ auto failure = [&prefix] (const string& d, const exception* e = nullptr)
+ {
+ submodule_failure (d, prefix, e);
+ };
+
fdpipe pipe (open_pipe ());
process pr (start_git (co,
@@ -1731,7 +2066,7 @@ namespace bpkg
l4 ([&]{trace << "submodule: " << l;});
if (!(l.size () > 50 && l[48] == '0' && l[49] == '\t'))
- throw runtime_error ("invalid submodule description '" + l + "'");
+ throw runtime_error ("invalid submodule description '" + l + '\'');
dir_path d (string (l, 50));
@@ -1765,7 +2100,7 @@ namespace bpkg
catch (const invalid_path& e)
{
if (pr.wait ())
- failure ("invalid submodule path '" + e.path + "'");
+ failure ("invalid submodule path '" + e.path + '\'');
// Fall through.
}
@@ -1793,6 +2128,7 @@ namespace bpkg
submodule_failure ("unable to list submodules", prefix);
}
+#endif
// Return commit id for the submodule directory or nullopt if the submodule
// is not initialized (directory doesn't exist, doesn't contain .git entry,
@@ -1840,13 +2176,15 @@ namespace bpkg
co.git_option (),
"-C", dir,
- // Note that older git versions don't recognize the --super-prefix
- // option but seem to behave correctly without any additional
- // efforts when it is omitted.
+ // Note that git versions outside the [2.14.0 2.38.0) range don't
+ // recognize the --super-prefix option but seem to behave correctly
+ // without any additional efforts when it is omitted.
//
- !prefix.empty () && git_ver >= semantic_version {2, 14, 0}
- ? strings ({"--super-prefix", prefix.posix_representation ()})
- : strings (),
+ (!prefix.empty () &&
+ git_ver >= semantic_version {2, 14, 0} &&
+ git_ver < semantic_version {2, 38, 0}
+ ? strings ({"--super-prefix", prefix.posix_representation ()})
+ : strings ()),
"submodule--helper", "init",
verb < 2 ? "-q" : nullptr))
@@ -1884,7 +2222,7 @@ namespace bpkg
if (u && *u == "none")
{
- if (verb >= 2 && !co.no_progress ())
+ if ((verb >= 2 && !co.no_progress ()) || co.progress ())
text << "skipping submodule '" << psd << "'";
// Note that the submodule can be enabled for some other snapshot we
@@ -1959,7 +2297,7 @@ namespace bpkg
catch (const invalid_path& e)
{
failure ("invalid submodule '" + sm.name + "' repository path '" +
- e.path + "'");
+ e.path + '\'');
}
catch (const invalid_argument& e)
{
@@ -1994,7 +2332,7 @@ namespace bpkg
// Let's make the message match the git-submodule script output (again,
// except for capitalization).
//
- if (verb && !co.no_progress ())
+ if ((verb && !co.no_progress ()) || co.progress ())
text << "submodule path '" << psd << "': checked out '" << sm.commit
<< "'";
@@ -2149,21 +2487,11 @@ namespace bpkg
dir_path () /* prefix */);
}
-#ifndef _WIN32
-
- // Noop on POSIX.
- //
- bool
- git_fixup_worktree (const common_options&, const dir_path&, bool)
- {
- return false;
- }
-
-#else
-
- // Find symlinks in the repository (non-recursive submodule-wise).
+ // Find symlinks in a working tree of a top repository or submodule
+ // (non-recursive submodule-wise) and return their relative paths together
+ // with the respective git object ids.
//
- static paths
+ static vector<pair<path, string>>
find_symlinks (const common_options& co,
const dir_path& dir,
const dir_path& prefix)
@@ -2194,12 +2522,12 @@ namespace bpkg
try
{
- paths r;
+ vector<pair<path, string>> r;
ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit);
for (string l; !eof (getline (is, l, '\0')); )
{
- // The line describing a file is NUL-terminated and has the following
+ // The line describing a file is NULL-terminated and has the following
// form:
//
// <mode><SPACE><object><SPACE><stage><TAB><path>
@@ -2209,16 +2537,18 @@ namespace bpkg
//
// 100644 165b42ec7a10fb6dd4a60b756fa1966c1065ef85 0 README
//
- l4 ([&]{trace << "file: " << l;});
-
if (!(l.size () > 50 && l[48] == '0' && l[49] == '\t'))
- throw runtime_error ("invalid file description '" + l + "'");
+ throw runtime_error ("invalid file description '" + l + '\'');
// For symlinks permission bits are always zero, so we can match the
// mode as a string.
//
if (l.compare (0, 6, "120000") == 0)
- r.push_back (path (string (l, 50)));
+ {
+ l4 ([&]{trace << "symlink: " << l;});
+
+ r.push_back (make_pair (path (string (l, 50)), string (l, 7, 40)));
+ }
}
is.close ();
@@ -2231,7 +2561,7 @@ namespace bpkg
catch (const invalid_path& e)
{
if (pr.wait ())
- failure ("invalid repository symlink path '" + e.path + "'");
+ failure ("invalid repository symlink path '" + e.path + '\'');
// Fall through.
}
@@ -2263,6 +2593,127 @@ namespace bpkg
submodule_failure ("unable to list repository files", prefix);
}
+ // Verify symlinks in a working tree of a top repository or submodule,
+ // recursively.
+ //
+ // Specifically, fail if the symlink target is not a valid relative path or
+ // refers outside the top repository directory.
+ //
+ static void
+ verify_symlinks (const common_options& co,
+ const dir_path& dir,
+ const dir_path& prefix)
+ {
+ auto failure = [&prefix] (const string& d, const exception* e = nullptr)
+ {
+ submodule_failure (d, prefix, e);
+ };
+
+ for (const auto& l: find_symlinks (co, dir, prefix))
+ {
+ const path& lp (l.first);
+
+ // Obtain the symlink target path.
+ //
+ path tp;
+
+ fdpipe pipe (open_pipe ());
+ process pr (start_git (co,
+ pipe, 2 /* stderr */,
+ co.git_option (),
+ "-C", dir,
+ "cat-file",
+ "-p",
+ l.second + "^{object}"));
+
+ // Shouldn't throw, unless something is severely damaged.
+ //
+ pipe.out.close ();
+
+ try
+ {
+ ifdstream is (move (pipe.in), fdstream_mode::skip);
+ string s (is.read_text ()); // Note: is not newline-terminated.
+ is.close ();
+
+ if (pr.wait () && !s.empty ())
+ try
+ {
+ tp = path (move (s));
+ }
+ catch (const invalid_path& e)
+ {
+ failure ("invalid target path '" + e.path + "' for symlink '" +
+ lp.string () + '\'',
+ &e);
+ }
+
+ // Fall through.
+ }
+ catch (const io_error&)
+ {
+ // Fall through.
+ }
+
+ if (tp.empty ())
+ failure ("unable to read target path for symlink '" + lp.string () +
+ "'");
+
+ // Verify that the symlink target path is relative.
+ //
+ if (tp.absolute ())
+ failure ("absolute target path '" + tp.string () + "' for symlink '" +
+ lp.string () + '\'');
+
+ // Verify that the symlink target path refers inside the top repository
+ // directory.
+ //
+ path rtp (prefix / lp.directory () / tp); // Relative to top directory.
+ rtp.normalize (); // Note: can't throw since the path is relative.
+
+ // Normalizing non-empty path can't end up with an empty path.
+ //
+ assert (!rtp.empty ());
+
+ // Make sure that the relative to the top repository directory target
+ // path doesn't start with '..'.
+ //
+ if (dir_path::traits_type::parent (*rtp.begin ()))
+ failure ("target path '" + tp.string () + "' for symlink '" +
+ lp.string () + "' refers outside repository");
+ }
+
+ // Verify symlinks for submodules.
+ //
+ for (const submodule& sm: find_submodules (co, dir, prefix))
+ verify_symlinks (co, dir / sm.path, prefix / sm.path);
+ }
+
+ void
+ git_verify_symlinks (const common_options& co, const dir_path& dir)
+ {
+ if ((verb && !co.no_progress ()) || co.progress ())
+ text << "verifying symlinks...";
+
+ verify_symlinks (co, dir, dir_path () /* prefix */);
+ }
+
+#ifndef _WIN32
+
+ // Noop on POSIX.
+ //
+ optional<bool>
+ git_fixup_worktree (const common_options&,
+ const dir_path&,
+ bool revert,
+ bool)
+ {
+ assert (!revert);
+ return false;
+ }
+
+#else
+
// Fix up or revert the previously made fixes in a working tree of a top
// repository or submodule (see git_fixup_worktree() description for
// details). Return nullopt if no changes are required (because real symlink
@@ -2304,7 +2755,7 @@ namespace bpkg
//
if (r)
failure ("unexpected real symlink in submodule '" +
- sm.path.string () + "'");
+ sm.path.string () + '\'');
return nullopt;
}
@@ -2324,13 +2775,35 @@ namespace bpkg
// skipping those with not-yet-existing target, unless no links were
// created at the previous run, in which case we fail.
//
- paths ls (find_symlinks (co, dir, prefix));
+ vector<pair<path, string>> ls (find_symlinks (co, dir, prefix));
vector<pair<path, path>> links; // List of the link/target path pairs.
+ // Mark the being replaced in the working tree links as unchanged,
+ // running git-update-index(1) for multiple links per run.
+ //
+ strings unchanged_links; // Links to mark as unchanged.
+
+ auto mark_unchanged = [&unchanged_links, &co, &dir, &failure] ()
+ {
+ if (!unchanged_links.empty ())
+ {
+ if (!run_git (co,
+ co.git_option (),
+ "-C", dir,
+ "update-index",
+ "--assume-unchanged",
+ unchanged_links))
+ failure ("unable to mark symlinks as unchanged");
+
+ unchanged_links.clear ();
+ }
+ };
+
// Cache/remove filesystem-agnostic symlinks.
//
- for (auto& l: ls)
+ for (auto& li: ls)
{
+ path& l (li.first);
path lp (dir / l); // Absolute or relative to the current directory.
// Check the symlink type to see if we need to replace it or can bail
@@ -2340,7 +2813,7 @@ namespace bpkg
// "elevated console mode":
//
// - file symlinks are currently not supported (see
- // libbutl/filesystem.mxx for details).
+ // libbutl/filesystem.hxx for details).
//
// - git creates symlinks to directories, rather than junctions. This
// makes things to fall apart as Windows API seems to be unable to
@@ -2356,14 +2829,14 @@ namespace bpkg
if (e.second.type == entry_type::symlink)
{
if (r)
- failure ("unexpected real symlink '" + l.string () + "'");
+ failure ("unexpected real symlink '" + l.string () + '\'');
return nullopt;
}
}
catch (const system_error& e)
{
- failure ("unable to stat symlink '" + l.string () + "'", &e);
+ failure ("unable to stat symlink '" + l.string () + '\'', &e);
}
// Read the symlink target path.
@@ -2378,7 +2851,7 @@ namespace bpkg
catch (const invalid_path& e)
{
failure ("invalid target path '" + e.path + "' for symlink '" +
- l.string () + "'",
+ l.string () + '\'',
&e);
}
catch (const io_error& e)
@@ -2390,14 +2863,14 @@ namespace bpkg
// Mark the symlink as unchanged and remove it.
//
- if (!run_git (co,
- co.git_option (),
- "-C", dir,
- "update-index",
- "--assume-unchanged",
- l))
- failure ("unable to mark symlink '" + l.string () +
- "' as unchanged");
+ // Note that we restrict the batch to 100 symlinks not to exceed the
+ // Windows command line max size, which is about 32K, and assuming
+ // that _MAX_PATH is 256 characters.
+ //
+ unchanged_links.push_back (l.string ());
+
+ if (unchanged_links.size () == 100)
+ mark_unchanged ();
links.emplace_back (move (l), move (t));
@@ -2405,6 +2878,8 @@ namespace bpkg
r = true;
}
+ mark_unchanged (); // Mark the rest.
+
// Create real links (hardlinks, symlinks, and junctions).
//
while (!links.empty ())
@@ -2440,7 +2915,7 @@ namespace bpkg
catch (const system_error& e)
{
failure ("unable to stat target '" + t.string () +
- "' for symlink '" + l.string () + "'",
+ "' for symlink '" + l.string () + '\'',
&e);
}
@@ -2458,7 +2933,7 @@ namespace bpkg
{
failure (string ("unable to create ") +
(dir_target ? "junction" : "hardlink") + " '" +
- l.string () + "' with target '" + t.string () + "'",
+ l.string () + "' with target '" + t.string () + '\'',
&e);
}
@@ -2486,8 +2961,10 @@ namespace bpkg
// filesystem entry. To prevent this, we remove all links ourselves
// first.
//
- for (const path& l: find_symlinks (co, dir, prefix))
+ for (const auto& li: find_symlinks (co, dir, prefix))
{
+ const path& l (li.first);
+
try
{
try_rmfile (dir / l);
@@ -2495,7 +2972,7 @@ namespace bpkg
catch (const system_error& e)
{
failure ("unable to remove hardlink, symlink, or junction '" +
- l.string () + "'",
+ l.string () + '\'',
&e);
}
}
@@ -2523,15 +3000,29 @@ namespace bpkg
return r;
}
- bool
+ optional<bool>
git_fixup_worktree (const common_options& co,
const dir_path& dir,
- bool revert)
+ bool revert,
+ bool ie)
{
- optional<bool> r (
- fixup_worktree (co, dir, revert, dir_path () /* prefix */));
+ if (!revert && ((verb && !co.no_progress ()) || co.progress ()))
+ text << "fixing up symlinks...";
+
+ try
+ {
+ optional<bool> r (
+ fixup_worktree (co, dir, revert, dir_path () /* prefix */));
- return r ? *r : false;
+ return r ? *r : false;
+ }
+ catch (const failed&)
+ {
+ if (ie)
+ return nullopt;
+
+ throw;
+ }
}
#endif