Skip to content

Commit dcad713

Browse files
committed
git clean
1 parent 8456c6f commit dcad713

File tree

4 files changed

+139
-24
lines changed

4 files changed

+139
-24
lines changed

src/utility/git.cpp

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

22
#include "git.hpp"
33
#include "defer.hpp"
4+
#include "path.hpp"
45
#include <git2.h>
56
#include <mutex>
67
#include <format>
@@ -184,7 +185,7 @@ enum class rev_match {
184185
* @param rev The rev to compare.
185186
* @retval error An error during query.
186187
*/
187-
[[nodiscard]] std::expected<rev_match, git_error> repository_matches_rev(::git_repository *repository, std::string const& rev)
188+
[[nodiscard]] static std::expected<rev_match, git_error> repository_matches_rev(::git_repository *repository, std::string const& rev)
188189
{
189190
::git_object *rev_obj = nullptr;
190191
::git_reference *rev_ref = nullptr;
@@ -237,35 +238,26 @@ enum class rev_match {
237238
/** Check if one of the remote has a url that matches the given url.
238239
*
239240
* @param repository The repository to check all the remotes
240-
* @param url The url to compare.
241-
* @retval true The url is a remote of the @a repository.
242-
* @retval false None of the remotes match.
241+
* @param remote_name The remote to get the url for.
242+
* @retval string The url of the remote.
243243
* @retval error An error during query.
244244
*/
245-
[[nodiscard]] std::expected<bool, git_error> repository_matches_url(::git_repository *repository, std::string const& url)
245+
[[nodiscard]] static std::expected<std::string, git_error> repository_remote_url(::git_repository *repository, std::string const& remote_name = std::string{"origin"})
246246
{
247247
assert(repository != nullptr);
248248

249-
auto remote_names = ::git_strarray{};
250-
if (auto const result = ::git_remote_list(&remote_names, repository); result != GIT_OK) {
249+
::git_remote *remote = nullptr;
250+
if (auto const result = ::git_remote_lookup(&remote, repository, remote_name.c_str()); result != GIT_OK) {
251251
return std::unexpected{make_git_error(result)};
252252
}
253-
auto const _ = defer{[&]{ ::git_strarray_dispose(&remote_names); }};
254-
255-
for (auto i = 0uz; i != remote_names.count; ++i) {
256-
::git_remote *remote = nullptr;
257-
if (auto const result = ::git_remote_lookup(&remote, repository, remote_names.strings[i]); result != GIT_OK) {
258-
return std::unexpected{make_git_error(result)};
259-
}
260-
auto const _ = defer{[&]{ ::git_remote_free(remote); }};
253+
auto const d1 = defer{[&]{ ::git_remote_free(remote); }};
261254

262-
auto const remote_url = ::git_remote_url(remote);
263-
if (remote_url and url == remote_url) {
264-
return true;
265-
}
255+
auto const remote_url = ::git_remote_url(remote);
256+
if (remote_url == nullptr) {
257+
return std::string{""};
266258
}
267259

268-
return false;
260+
return std::string{remote_url};
269261
}
270262

271263
[[nodiscard]] static git_error repository_fetch(::git_repository *repository, std::string const& remote_name = std::string{"origin"})
@@ -291,25 +283,99 @@ enum class rev_match {
291283
return git_error::ok;
292284
}
293285

286+
[[nodiscard]] static std::filesystem::path repository_workdir(::git_repository *repository)
287+
{
288+
assert(repository != nullptr);
289+
290+
if (auto *p = ::git_repository_workdir(repository); p != nullptr) {
291+
return std::filesystem::path{p};
292+
} else {
293+
return std::filesystem::path{};
294+
}
295+
}
296+
297+
/** Clean the repository in preperation for a new checkout.
298+
*
299+
* This will remove all untracked and ignored files. It is expected that these
300+
* are the only files that need to be removed to rebuild the repository.
301+
*
302+
* @param repository An open repository.
303+
* @return An error code or ok.
304+
*/
305+
[[nodiscard]] static git_error repository_clean(::git_repository *repository)
306+
{
307+
assert(repository != nullptr);
308+
309+
auto repository_dir = repository_workdir(repository);
310+
if (repository_dir.empty()) {
311+
return git_error::bare_repo;
312+
}
313+
if (not repository_dir.is_absolute()) {
314+
return git_error::relative_workdir;
315+
}
316+
repository_dir = std::filesystem::canonical(repository_dir);
317+
318+
auto status_opts = ::git_status_options{};
319+
if (::git_status_options_init(&status_opts, GIT_STATUS_OPTIONS_VERSION) != 0) {
320+
return git_error::error;
321+
}
322+
323+
status_opts.show = GIT_STATUS_SHOW_WORKDIR_ONLY;
324+
status_opts.flags |= GIT_STATUS_OPT_INCLUDE_UNTRACKED;
325+
status_opts.flags |= GIT_STATUS_OPT_RECURSE_UNTRACKED_DIRS;
326+
status_opts.flags |= GIT_STATUS_OPT_INCLUDE_IGNORED;
327+
328+
::git_status_list *status_list = nullptr;
329+
if (auto result = ::git_status_list_new(&status_list, repository, &status_opts); result != GIT_OK) {
330+
return make_git_error(result);
331+
}
332+
auto const d1 = defer{[&] { ::git_status_list_free(status_list); }};
333+
334+
auto const count = ::git_status_list_entrycount(status_list);
335+
for (auto i = 0uz; i != count; ++i) {
336+
auto const *entry = ::git_status_byindex(status_list, i);
337+
if (entry->head_to_index == nullptr) {
338+
continue;
339+
340+
} else if (entry->status == GIT_STATUS_WT_NEW or entry->status == GIT_STATUS_IGNORED) {
341+
auto path = repository_dir / entry->head_to_index->new_file.path;
342+
path = std::filesystem::canonical(path);
343+
344+
if (not is_subpath(path, repository_dir)) {
345+
std::print(stderr, "security: file '{}' is outside of directory of repository at '{}'", path, repository_dir);
346+
std::terminate();
347+
}
348+
349+
std::print(stderr, "info: removing file '{}'", path.string());
350+
auto ec = std::error_code{};
351+
if (not std::filesystem::remove(path, ec)) {
352+
std::print(stderr, "error: failed removing file '{}': {}", path.string(), ec);
353+
}
354+
}
355+
}
356+
357+
return git_error::ok;
358+
}
359+
294360
[[nodiscard]] git_error git_fetch_and_update(std::string const& url, std::string const& rev, std::filesystem::path path, git_checkout_flags flags)
295361
{
296362
auto const& _ = git_lib_initialize();
297363

298364
auto r = git_error::ok;
299365

300366
::git_repository *repository = nullptr;
301-
if (auto const result = ::git_repository_open(&repository, url.c_str()); result != GIT_OK) {
367+
if (auto const result = ::git_repository_open(&repository, path.string().c_str()); result != GIT_OK) {
302368
return make_git_error(result);
303369
}
304370
auto const d1 = defer{[&]{ ::git_repository_free(repository); }};
305371

306-
if (auto result = repository_matches_url(repository, url)) {
307-
if (not *result) {
372+
if (auto remote_url_o = repository_remote_url(repository, url)) {
373+
if (*remote_url_o != url) {
308374
std::print("The repository at {}, does not have a remote with the url {}", path.string(), url);
309375
return git_error::remote_url_mismatch;
310376
}
311377
} else {
312-
return result.error();
378+
return remote_url_o.error();
313379
}
314380

315381
auto fetch = to_bool(flags & git_checkout_flags::force_checkout);

src/utility/git.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ enum class git_error {
5454
/** The repository does have a remote that matches the expected URL.
5555
*/
5656
remote_url_mismatch,
57+
58+
/** The workdir of the repository is relative, instead of absolute.
59+
*/
60+
relative_workdir,
61+
62+
/** Found a file outside of the work dir.
63+
*
64+
* This is a security issue.
65+
*/
66+
file_outside_workdir
5767
};
5868

5969
enum class git_checkout_flags {

src/utility/path.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,33 @@ namespace hk {
3131
}
3232
}
3333

34+
[[nodiscard]] bool is_subpath(std::filesystem::path const& path, std::filesystem::path const& base)
35+
{
36+
auto ec = std::error_code{};
37+
38+
auto const path_ = std::filesystem::canonical(path, ec);
39+
if (not ec) {
40+
return false;
41+
}
42+
43+
auto const base_ = std::filesystem::canonical(path, ec);
44+
if (not ec) {
45+
return false;
46+
}
47+
48+
auto it = base_.begin();
49+
auto jt = path_.begin();
50+
51+
for (; it != base_.end(); ++it, ++jt) {
52+
if (jt == path_.end()) {
53+
return false;
54+
}
55+
56+
if (*it != *jt) {
57+
return false;
58+
}
59+
}
60+
return true;
61+
}
62+
3463
}

src/utility/path.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,15 @@ namespace hk {
2828
[[nodiscard]] std::expected<std::filesystem::path, std::error_code> absolute_to(
2929
std::filesystem::path const& path, std::filesystem::path const& base);
3030

31+
/** Is subpath.
32+
*
33+
* Make sure that @a path is a subpath inside of @a base.
34+
*
35+
* @param path The file path a subpath inside @a base.
36+
* @param base A directory containing @a path.
37+
* @retval true If @a path is contained with @a base.
38+
* @retval false If @a path is outside @a base, or an error occured.
39+
*/
40+
[[nodiscard]] bool is_subpath(std::filesystem::path const& path, std::filesystem::path const& base);
3141

3242
}

0 commit comments

Comments
 (0)