From 45271d06c07ea1d08f916f3f69dfdc3399efd6e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czarnecki?= Date: Wed, 1 Jun 2022 11:26:56 -0700 Subject: [PATCH 1/2] Get libgit2 working for 8thwall. (#24) Upgrade libgit2 with support for 8thwall cloud editor. --- deps/picosha2/picosha2-c.cc | 17 ++ deps/picosha2/picosha2-c.h | 14 + deps/picosha2/picosha2.h | 370 +++++++++++++++++++++++ include/git2/sys/transport.h | 10 +- src/libgit2/refdb_fs.c | 2 +- src/libgit2/transports/emscriptenhttp.cc | 269 ++++++++++++++++ src/libgit2/transports/http.c | 2 +- src/util/thread.c | 8 + src/util/vector.h | 2 +- 9 files changed, 690 insertions(+), 4 deletions(-) create mode 100644 deps/picosha2/picosha2-c.cc create mode 100644 deps/picosha2/picosha2-c.h create mode 100644 deps/picosha2/picosha2.h create mode 100644 src/libgit2/transports/emscriptenhttp.cc diff --git a/deps/picosha2/picosha2-c.cc b/deps/picosha2/picosha2-c.cc new file mode 100644 index 00000000000..847d1150001 --- /dev/null +++ b/deps/picosha2/picosha2-c.cc @@ -0,0 +1,17 @@ +#include "picosha2-c.h" +#include "picosha2.h" + +#include +#include +#include + +extern "C" { + +void picosha2_256(const char *buffer, int len, char *dest) { + std::array hash; + picosha2::hash256(buffer, buffer + len, std::begin(hash), std::end(hash)); + std::string sha256 = + picosha2::bytes_to_hex_string(std::begin(hash), std::end(hash)); + std::memcpy(dest, sha256.c_str(), sha256.size() + 1); +} +} diff --git a/deps/picosha2/picosha2-c.h b/deps/picosha2/picosha2-c.h new file mode 100644 index 00000000000..3bc1dcb1a9b --- /dev/null +++ b/deps/picosha2/picosha2-c.h @@ -0,0 +1,14 @@ +#ifndef INCLUDE_picosha2_c_h__ +#define INCLUDE_picosha2_c_h__ + +#ifdef __cplusplus +extern "C" { +#endif + +void picosha2_256(const char* buffer, int len, char* dest); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/deps/picosha2/picosha2.h b/deps/picosha2/picosha2.h new file mode 100644 index 00000000000..2fcfea43014 --- /dev/null +++ b/deps/picosha2/picosha2.h @@ -0,0 +1,370 @@ +/* +The MIT License (MIT) +Copyright (C) 2017 okdshin +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ +#pragma once +// picosha2:20140213 + +#ifndef PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR +#define PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR \ + 1048576 //=1024*1024: default is 1MB memory +#endif + +#include +#include +#include +#include +#include +#include +namespace picosha2 { +typedef unsigned long word_t; +typedef unsigned char byte_t; + +static const size_t k_digest_size = 32; + +namespace detail { +inline byte_t mask_8bit(byte_t x) { return x & 0xff; } + +inline word_t mask_32bit(word_t x) { return x & 0xffffffff; } + +const word_t add_constant[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, + 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, + 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, + 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, + 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, + 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2}; + +const word_t initial_message_digest[8] = {0x6a09e667, 0xbb67ae85, 0x3c6ef372, + 0xa54ff53a, 0x510e527f, 0x9b05688c, + 0x1f83d9ab, 0x5be0cd19}; + +inline word_t ch(word_t x, word_t y, word_t z) { return (x & y) ^ ((~x) & z); } + +inline word_t maj(word_t x, word_t y, word_t z) { + return (x & y) ^ (x & z) ^ (y & z); +} + +inline word_t rotr(word_t x, std::size_t n) { + assert(n < 32); + return mask_32bit((x >> n) | (x << (32 - n))); +} + +inline word_t bsig0(word_t x) { return rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); } + +inline word_t bsig1(word_t x) { return rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); } + +inline word_t shr(word_t x, std::size_t n) { + assert(n < 32); + return x >> n; +} + +inline word_t ssig0(word_t x) { return rotr(x, 7) ^ rotr(x, 18) ^ shr(x, 3); } + +inline word_t ssig1(word_t x) { return rotr(x, 17) ^ rotr(x, 19) ^ shr(x, 10); } + +template +void hash256_block(RaIter1 message_digest, RaIter2 first, RaIter2 last) { + assert(first + 64 == last); + static_cast(last); // for avoiding unused-variable warning + word_t w[64]; + std::fill(w, w + 64, 0); + for (std::size_t i = 0; i < 16; ++i) { + w[i] = (static_cast(mask_8bit(*(first + i * 4))) << 24) | + (static_cast(mask_8bit(*(first + i * 4 + 1))) << 16) | + (static_cast(mask_8bit(*(first + i * 4 + 2))) << 8) | + (static_cast(mask_8bit(*(first + i * 4 + 3)))); + } + for (std::size_t i = 16; i < 64; ++i) { + w[i] = + mask_32bit(ssig1(w[i - 2]) + w[i - 7] + ssig0(w[i - 15]) + w[i - 16]); + } + + word_t a = *message_digest; + word_t b = *(message_digest + 1); + word_t c = *(message_digest + 2); + word_t d = *(message_digest + 3); + word_t e = *(message_digest + 4); + word_t f = *(message_digest + 5); + word_t g = *(message_digest + 6); + word_t h = *(message_digest + 7); + + for (std::size_t i = 0; i < 64; ++i) { + word_t temp1 = h + bsig1(e) + ch(e, f, g) + add_constant[i] + w[i]; + word_t temp2 = bsig0(a) + maj(a, b, c); + h = g; + g = f; + f = e; + e = mask_32bit(d + temp1); + d = c; + c = b; + b = a; + a = mask_32bit(temp1 + temp2); + } + *message_digest += a; + *(message_digest + 1) += b; + *(message_digest + 2) += c; + *(message_digest + 3) += d; + *(message_digest + 4) += e; + *(message_digest + 5) += f; + *(message_digest + 6) += g; + *(message_digest + 7) += h; + for (std::size_t i = 0; i < 8; ++i) { + *(message_digest + i) = mask_32bit(*(message_digest + i)); + } +} + +} // namespace detail + +template +void output_hex(InIter first, InIter last, std::ostream &os) { + os.setf(std::ios::hex, std::ios::basefield); + while (first != last) { + os.width(2); + os.fill('0'); + os << static_cast(*first); + ++first; + } + os.setf(std::ios::dec, std::ios::basefield); +} + +template +void bytes_to_hex_string(InIter first, InIter last, std::string &hex_str) { + std::ostringstream oss; + output_hex(first, last, oss); + hex_str.assign(oss.str()); +} + +template +void bytes_to_hex_string(const InContainer &bytes, std::string &hex_str) { + bytes_to_hex_string(bytes.begin(), bytes.end(), hex_str); +} + +template +std::string bytes_to_hex_string(InIter first, InIter last) { + std::string hex_str; + bytes_to_hex_string(first, last, hex_str); + return hex_str; +} + +template +std::string bytes_to_hex_string(const InContainer &bytes) { + std::string hex_str; + bytes_to_hex_string(bytes, hex_str); + return hex_str; +} + +class hash256_one_by_one { +public: + hash256_one_by_one() { init(); } + + void init() { + buffer_.clear(); + std::fill(data_length_digits_, data_length_digits_ + 4, 0); + std::copy(detail::initial_message_digest, + detail::initial_message_digest + 8, h_); + } + + template void process(RaIter first, RaIter last) { + add_to_data_length(static_cast(std::distance(first, last))); + std::copy(first, last, std::back_inserter(buffer_)); + std::size_t i = 0; + for (; i + 64 <= buffer_.size(); i += 64) { + detail::hash256_block(h_, buffer_.begin() + i, buffer_.begin() + i + 64); + } + buffer_.erase(buffer_.begin(), buffer_.begin() + i); + } + + void finish() { + byte_t temp[64]; + std::fill(temp, temp + 64, 0); + std::size_t remains = buffer_.size(); + std::copy(buffer_.begin(), buffer_.end(), temp); + temp[remains] = 0x80; + + if (remains > 55) { + std::fill(temp + remains + 1, temp + 64, 0); + detail::hash256_block(h_, temp, temp + 64); + std::fill(temp, temp + 64 - 4, 0); + } else { + std::fill(temp + remains + 1, temp + 64 - 4, 0); + } + + write_data_bit_length(&(temp[56])); + detail::hash256_block(h_, temp, temp + 64); + } + + template + void get_hash_bytes(OutIter first, OutIter last) const { + for (const word_t *iter = h_; iter != h_ + 8; ++iter) { + for (std::size_t i = 0; i < 4 && first != last; ++i) { + *(first++) = + detail::mask_8bit(static_cast((*iter >> (24 - 8 * i)))); + } + } + } + +private: + void add_to_data_length(word_t n) { + word_t carry = 0; + data_length_digits_[0] += n; + for (std::size_t i = 0; i < 4; ++i) { + data_length_digits_[i] += carry; + if (data_length_digits_[i] >= 65536u) { + carry = data_length_digits_[i] >> 16; + data_length_digits_[i] &= 65535u; + } else { + break; + } + } + } + void write_data_bit_length(byte_t *begin) { + word_t data_bit_length_digits[4]; + std::copy(data_length_digits_, data_length_digits_ + 4, + data_bit_length_digits); + + // convert byte length to bit length (multiply 8 or shift 3 times left) + word_t carry = 0; + for (std::size_t i = 0; i < 4; ++i) { + word_t before_val = data_bit_length_digits[i]; + data_bit_length_digits[i] <<= 3; + data_bit_length_digits[i] |= carry; + data_bit_length_digits[i] &= 65535u; + carry = (before_val >> (16 - 3)) & 65535u; + } + + // write data_bit_length + for (int i = 3; i >= 0; --i) { + (*begin++) = static_cast(data_bit_length_digits[i] >> 8); + (*begin++) = static_cast(data_bit_length_digits[i]); + } + } + std::vector buffer_; + word_t data_length_digits_[4]; // as 64bit integer (16bit x 4 integer) + word_t h_[8]; +}; + +inline void get_hash_hex_string(const hash256_one_by_one &hasher, + std::string &hex_str) { + byte_t hash[k_digest_size]; + hasher.get_hash_bytes(hash, hash + k_digest_size); + return bytes_to_hex_string(hash, hash + k_digest_size, hex_str); +} + +inline std::string get_hash_hex_string(const hash256_one_by_one &hasher) { + std::string hex_str; + get_hash_hex_string(hasher, hex_str); + return hex_str; +} + +namespace impl { +template +void hash256_impl(RaIter first, RaIter last, OutIter first2, OutIter last2, int, + std::random_access_iterator_tag) { + hash256_one_by_one hasher; + // hasher.init(); + hasher.process(first, last); + hasher.finish(); + hasher.get_hash_bytes(first2, last2); +} + +template +void hash256_impl(InputIter first, InputIter last, OutIter first2, + OutIter last2, int buffer_size, std::input_iterator_tag) { + std::vector buffer(buffer_size); + hash256_one_by_one hasher; + // hasher.init(); + while (first != last) { + int size = buffer_size; + for (int i = 0; i != buffer_size; ++i, ++first) { + if (first == last) { + size = i; + break; + } + buffer[i] = *first; + } + hasher.process(buffer.begin(), buffer.begin() + size); + } + hasher.finish(); + hasher.get_hash_bytes(first2, last2); +} +} // namespace impl + +template +void hash256(InIter first, InIter last, OutIter first2, OutIter last2, + int buffer_size = PICOSHA2_BUFFER_SIZE_FOR_INPUT_ITERATOR) { + picosha2::impl::hash256_impl( + first, last, first2, last2, buffer_size, + typename std::iterator_traits::iterator_category()); +} + +template +void hash256(InIter first, InIter last, OutContainer &dst) { + hash256(first, last, dst.begin(), dst.end()); +} + +template +void hash256(const InContainer &src, OutIter first, OutIter last) { + hash256(src.begin(), src.end(), first, last); +} + +template +void hash256(const InContainer &src, OutContainer &dst) { + hash256(src.begin(), src.end(), dst.begin(), dst.end()); +} + +template +void hash256_hex_string(InIter first, InIter last, std::string &hex_str) { + byte_t hashed[k_digest_size]; + hash256(first, last, hashed, hashed + k_digest_size); + std::ostringstream oss; + output_hex(hashed, hashed + k_digest_size, oss); + hex_str.assign(oss.str()); +} + +template +std::string hash256_hex_string(InIter first, InIter last) { + std::string hex_str; + hash256_hex_string(first, last, hex_str); + return hex_str; +} + +inline void hash256_hex_string(const std::string &src, std::string &hex_str) { + hash256_hex_string(src.begin(), src.end(), hex_str); +} + +template +void hash256_hex_string(const InContainer &src, std::string &hex_str) { + hash256_hex_string(src.begin(), src.end(), hex_str); +} + +template +std::string hash256_hex_string(const InContainer &src) { + return hash256_hex_string(src.begin(), src.end()); +} +template +void hash256(std::ifstream &f, OutIter first, OutIter last) { + hash256(std::istreambuf_iterator(f), std::istreambuf_iterator(), + first, last); +} +} // namespace picosha2 diff --git a/include/git2/sys/transport.h b/include/git2/sys/transport.h index 06ae7079ffd..930c281e23a 100644 --- a/include/git2/sys/transport.h +++ b/include/git2/sys/transport.h @@ -8,6 +8,10 @@ #ifndef INCLUDE_sys_git_transport_h #define INCLUDE_sys_git_transport_h +#ifdef __cplusplus +extern "C" { +#endif + #include "git2/net.h" #include "git2/proxy.h" #include "git2/remote.h" @@ -401,11 +405,11 @@ typedef struct git_smart_subtransport_definition { * @param owner The smart transport to own this subtransport * @return 0 or an error code */ + GIT_EXTERN(int) git_smart_subtransport_http( git_smart_subtransport **out, git_transport *owner, void *param); - /** * Create an instance of the git subtransport. * @@ -432,4 +436,8 @@ GIT_EXTERN(int) git_smart_subtransport_ssh( /** @} */ GIT_END_DECL + +#ifdef __cplusplus +} +#endif #endif diff --git a/src/libgit2/refdb_fs.c b/src/libgit2/refdb_fs.c index 43283b3e472..11ad8694664 100644 --- a/src/libgit2/refdb_fs.c +++ b/src/libgit2/refdb_fs.c @@ -1634,7 +1634,7 @@ static int refdb_fs_backend__prune_refs( error = git_futils_rmdir_r(ref_name + commonlen, git_str_cstr(&base_path), - GIT_RMDIR_EMPTY_PARENTS | GIT_RMDIR_SKIP_ROOT); + GIT_RMDIR_EMPTY_PARENTS | GIT_RMDIR_SKIP_ROOT | GIT_RMDIR_REMOVE_FILES); if (error == GIT_ENOTFOUND) error = 0; diff --git a/src/libgit2/transports/emscriptenhttp.cc b/src/libgit2/transports/emscriptenhttp.cc new file mode 100644 index 00000000000..7f344fa9335 --- /dev/null +++ b/src/libgit2/transports/emscriptenhttp.cc @@ -0,0 +1,269 @@ +#ifdef __EMSCRIPTEN__ + +#include "emscripten.h" +#include "emscripten/fetch.h" + +extern "C" { +#include "common.h" +#include "git2/transport.h" +#include "smart.h" +} + +#include + +#include "deps/picosha2/picosha2.h" + +static const char *upload_pack_ls_service_url = "/info/refs?service=git-upload-pack"; +static const char *upload_pack_service_url = "/git-upload-pack"; +static const char *receive_pack_ls_service_url = "/info/refs?service=git-receive-pack"; +static const char *receive_pack_service_url = "/git-receive-pack"; + +namespace { +struct StreamInternal { + std::string url; + std::vector writeBuffer; + std::vector headers; // [key1, value1, key2, value2, ...] + emscripten_fetch_attr_t attr; + emscripten_fetch_t *fetch{nullptr}; + size_t totalBytesRead{0}; +}; + +uint64_t connectionCount{0}; +std::map connectionMap; + +uint64_t xhrConnect( + const std::string url, const char *method, const std::vector headers) { + auto connectionNumber = connectionCount++; + auto &connection = connectionMap[connectionNumber]; + connection.url = url; + connection.headers = headers; + emscripten_fetch_attr_init(&connection.attr); + strcpy(connection.attr.requestMethod, method); + // NOTE(pawel) EMSCRIPTEN_FETCH_REPLACE is needed for synchronous to work... + // https://github.com/emscripten-core/emscripten/issues/8183 + connection.attr.attributes = + EMSCRIPTEN_FETCH_LOAD_TO_MEMORY | EMSCRIPTEN_FETCH_SYNCHRONOUS | EMSCRIPTEN_FETCH_REPLACE; + if (std::string(method) == "GET") { + auto headersToSend = connection.headers; + headersToSend.push_back(0); + connection.attr.requestHeaders = headersToSend.data(); + connection.fetch = emscripten_fetch(&connection.attr, url.c_str()); + } + return connectionNumber; +} + +// This call buffers the writes. The buffer is sent on the wire when xhrRead() is called. +int xhrWrite(uint64_t connectionNumber, const char *buffer, size_t size) { + if (connectionMap.count(connectionNumber) != 1) { + printf("Attempting to write to connection %l but it is not connected", connectionNumber); + return -1; + } + auto &connection = connectionMap[connectionNumber]; + connection.writeBuffer.insert(end(connection.writeBuffer), buffer, buffer + size); + return 0; +} + +// Sends pending writes and returns response async. The result is buffered so this can be invoked +// until the full length of the buffer is read. +int xhrRead(uint64_t connectionNumber, char *buffer, size_t bufferSize, size_t *bytesRead) { + if (connectionMap.count(connectionNumber) != 1) { + printf("Attempting to read from connection %l but it is not connected\n", connectionNumber); + *bytesRead = 0; + return -1; + } + auto &connection = connectionMap[connectionNumber]; + std::vector headersToSend = connection.headers; + std::string sha256; + if (!connection.writeBuffer.empty()) { + const auto data = connection.writeBuffer.data(); + const auto dataSize = connection.writeBuffer.size(); + connection.attr.requestData = data; + connection.attr.requestDataSize = dataSize; + + std::vector hash(picosha2::k_digest_size); + picosha2::hash256(data, data + dataSize, hash.begin(), hash.end()); + sha256 = picosha2::bytes_to_hex_string(hash.begin(), hash.end()); + headersToSend.push_back("x-amz-content-sha256"); + headersToSend.push_back(sha256.c_str()); // This pointer is only valid in this scope. + } + auto &f = connection.fetch; + if (!f) { + headersToSend.push_back(0); // null terminate the array. + connection.attr.requestHeaders = headersToSend.data(); + + f = emscripten_fetch(&connection.attr, connection.url.c_str()); + if (f->status != 200) { + printf("%d %s %s\n", f->status, f->statusText, f->url); + } + if (f->readyState != 4) { + printf("Connection is not in ready state (%d)\n", f->readyState); + } + } + connection.writeBuffer.clear(); + *bytesRead = min(f->numBytes - connection.totalBytesRead, bufferSize); + std::memcpy(buffer, f->data + connection.totalBytesRead, *bytesRead); + connection.totalBytesRead += *bytesRead; + return 0; +} + +void xhrFree(uint64_t connectionNumber) { + if (connectionMap.count(connectionNumber != 1)) { + printf("Attempting to free unkown connection %l", connectionNumber); + return; + } + auto &connection = connectionMap[connectionNumber]; + emscripten_fetch_close(connection.fetch); + connectionMap.erase(connectionNumber); +} +} // namespace + +namespace { +typedef struct { + git_smart_subtransport_stream parent; + git_str service_url; + uint64_t connectionNo; +} emscriptenhttp_stream; + +typedef struct { + git_smart_subtransport parent; + transport_smart *owner; +} emscriptenhttp_subtransport; + +// Since these types are being sent back to C, we need to ensure they are PODs. +static_assert(std::is_pod()); +static_assert(std::is_pod()); + +static int emscriptenhttp_stream_read( + git_smart_subtransport_stream *stream, char *buffer, size_t buf_size, size_t *bytes_read) { + emscriptenhttp_stream *s = (emscriptenhttp_stream *)stream; + + if (s->connectionNo == -1) { + s->connectionNo = xhrConnect(git_str_cstr(&s->service_url), "GET", {}); + } + return xhrRead(s->connectionNo, buffer, buf_size, bytes_read); +} + +static int emscriptenhttp_stream_write_single( + git_smart_subtransport_stream *stream, const char *buffer, size_t len) { + emscriptenhttp_stream *s = (emscriptenhttp_stream *)stream; + + if (s->connectionNo == -1) { + + auto serviceUrl = git_str_cstr(&s->service_url); + bool uploadPack = strstr(serviceUrl, "git-upload-pack") != 0; + + s->connectionNo = xhrConnect( + serviceUrl, + "POST", + { + "Content-Type", + uploadPack ? "application/x-git-upload-pack-request" + : "application/x-git-receive-pack-request", + "Pragma", + "no-cache", + }); + } + return xhrWrite(s->connectionNo, buffer, len); +} + +static void emscriptenhttp_stream_free(git_smart_subtransport_stream *stream) { + emscriptenhttp_stream *s = (emscriptenhttp_stream *)stream; + if (s->connectionNo != -1) { + xhrFree(s->connectionNo); + } + git_str_dispose(&s->service_url); + git__free(s); +} + +static int emscriptenhttp_stream_alloc( + emscriptenhttp_subtransport *t, emscriptenhttp_stream **stream) { + emscriptenhttp_stream *s; + + if (!stream) + return -1; + + s = reinterpret_cast(git__calloc(1, sizeof(emscriptenhttp_stream))); + GIT_ERROR_CHECK_ALLOC(s); + + s->parent.subtransport = &t->parent; + s->parent.read = emscriptenhttp_stream_read; + s->parent.write = emscriptenhttp_stream_write_single; + s->parent.free = emscriptenhttp_stream_free; + s->connectionNo = -1; + s->service_url = GIT_STR_INIT; + + *stream = s; + + return 0; +} + +static int emscriptenhttp_action( + git_smart_subtransport_stream **stream, + git_smart_subtransport *subtransport, + const char *url, + git_smart_service_t action) { + emscriptenhttp_subtransport *t = (emscriptenhttp_subtransport *)subtransport; + emscriptenhttp_stream *s; + + if (emscriptenhttp_stream_alloc(t, &s) < 0) { + return -1; + } + + switch (action) { + case GIT_SERVICE_UPLOADPACK_LS: + git_str_printf(&s->service_url, "%s%s", url, upload_pack_ls_service_url); + break; + case GIT_SERVICE_UPLOADPACK: + git_str_printf(&s->service_url, "%s%s", url, upload_pack_service_url); + break; + case GIT_SERVICE_RECEIVEPACK_LS: + git_str_printf(&s->service_url, "%s%s", url, receive_pack_ls_service_url); + break; + case GIT_SERVICE_RECEIVEPACK: + git_str_printf(&s->service_url, "%s%s", url, receive_pack_service_url); + break; + } + + if (git_str_oom(&s->service_url)) { + return -1; + } + + *stream = &s->parent; + return 0; +} + +static int emscriptenhttp_close(git_smart_subtransport *subtransport) { return 0; } + +static void emscriptenhttp_free(git_smart_subtransport *subtransport) { + emscriptenhttp_subtransport *t = (emscriptenhttp_subtransport *)subtransport; + emscriptenhttp_close(subtransport); + git__free(t); +} + +} // namespace + +extern "C" { +int git_smart_subtransport_http(git_smart_subtransport **out, git_transport *owner, void *param) { + emscriptenhttp_subtransport *t; + + GIT_UNUSED(param); + + if (!out) + return -1; + + t = reinterpret_cast( + git__calloc(1, sizeof(emscriptenhttp_subtransport))); + GIT_ERROR_CHECK_ALLOC(t); + + t->owner = (transport_smart *)owner; + t->parent.action = emscriptenhttp_action; + t->parent.close = emscriptenhttp_close; + t->parent.free = emscriptenhttp_free; + + *out = (git_smart_subtransport *)t; + + return 0; +} +} + +#endif /* __EMSCRIPTEN__ */ diff --git a/src/libgit2/transports/http.c b/src/libgit2/transports/http.c index 7db5582cab4..d096da03fb4 100644 --- a/src/libgit2/transports/http.c +++ b/src/libgit2/transports/http.c @@ -7,7 +7,7 @@ #include "common.h" -#ifndef GIT_WINHTTP +#if !defined(GIT_WINHTTP) && !defined(__EMSCRIPTEN__) #include "http_parser.h" #include "net.h" diff --git a/src/util/thread.c b/src/util/thread.c index bc7364f8c1a..c3f953cab03 100644 --- a/src/util/thread.c +++ b/src/util/thread.c @@ -67,6 +67,14 @@ int git_tlsdata_dispose(git_tlsdata_key key) if (value && destroy_fn) destroy_fn(value); + // NOTE(pawel) In a single-threaded environment this should be okay but multithreaded + // setups may need some additional verification (my intuition is there is an edge case here). + if (key == (tlsdata_cnt - 1)) { + tlsdata_cnt--; + } else { + printf("warning git_tlsdata_dispose() key (%d) != tlsdata_cnt (%d)", key, tlsdata_cnt); + } + return 0; } diff --git a/src/util/vector.h b/src/util/vector.h index e50cdfefcbd..343d821d83f 100644 --- a/src/util/vector.h +++ b/src/util/vector.h @@ -84,7 +84,7 @@ GIT_INLINE(void *) git_vector_last(const git_vector *v) int git_vector_insert(git_vector *v, void *element); int git_vector_insert_sorted(git_vector *v, void *element, - int (*on_dup)(void **old, void *new)); + int (*on_dup)(void **old, void *new_)); int git_vector_remove(git_vector *v, size_t idx); void git_vector_pop(git_vector *v); void git_vector_uniq(git_vector *v, void (*git_free_cb)(void *)); From a5c563a546053f81b9a1745becf320ef1062086d Mon Sep 17 00:00:00 2001 From: Pawel Czarnecki Date: Fri, 5 Aug 2022 16:26:39 -0700 Subject: [PATCH 2/2] [sparse] squash and rebase #6169 sparse checkout support --- include/git2.h | 1 + include/git2/checkout.h | 6 + include/git2/diff.h | 5 + include/git2/sparse.h | 114 +++ src/libgit2/attr_file.c | 149 ++++ src/libgit2/attr_file.h | 2 + src/libgit2/checkout.c | 13 +- src/libgit2/config_cache.c | 1 + src/libgit2/diff_generate.c | 16 +- src/libgit2/ignore.c | 202 +----- src/libgit2/ignore.h | 7 + src/libgit2/index.c | 32 +- src/libgit2/iterator.c | 87 ++- src/libgit2/iterator.h | 2 + src/libgit2/repository.h | 5 +- src/libgit2/sparse.c | 654 ++++++++++++++++++ src/libgit2/sparse.h | 39 ++ tests/libgit2/sparse/add.c | 54 ++ tests/libgit2/sparse/checkout.c | 120 ++++ tests/libgit2/sparse/disable.c | 69 ++ tests/libgit2/sparse/index.c | 235 +++++++ tests/libgit2/sparse/init.c | 108 +++ tests/libgit2/sparse/list.c | 30 + tests/libgit2/sparse/reapply.c | 81 +++ tests/libgit2/sparse/set.c | 75 ++ tests/libgit2/sparse/status.c | 408 +++++++++++ tests/libgit2/sparse/worktree.c | 103 +++ tests/resources/sparse/.gitted/COMMIT_EDITMSG | 1 + tests/resources/sparse/.gitted/HEAD | 1 + tests/resources/sparse/.gitted/ORIG_HEAD | 1 + tests/resources/sparse/.gitted/config | 7 + tests/resources/sparse/.gitted/description | 1 + .../.gitted/hooks/applypatch-msg.sample | 15 + .../sparse/.gitted/hooks/commit-msg.sample | 24 + .../.gitted/hooks/fsmonitor-watchman.sample | 173 +++++ .../sparse/.gitted/hooks/post-update.sample | 8 + .../.gitted/hooks/pre-applypatch.sample | 14 + .../sparse/.gitted/hooks/pre-commit.sample | 49 ++ .../.gitted/hooks/pre-merge-commit.sample | 13 + .../sparse/.gitted/hooks/pre-push.sample | 53 ++ .../sparse/.gitted/hooks/pre-rebase.sample | 169 +++++ .../sparse/.gitted/hooks/pre-receive.sample | 24 + .../.gitted/hooks/prepare-commit-msg.sample | 42 ++ .../.gitted/hooks/push-to-checkout.sample | 78 +++ .../sparse/.gitted/hooks/update.sample | 128 ++++ tests/resources/sparse/.gitted/index | Bin 0 -> 1082 bytes tests/resources/sparse/.gitted/info/exclude | 7 + tests/resources/sparse/.gitted/logs/HEAD | 11 + .../sparse/.gitted/logs/refs/heads/main | 7 + .../13/85f264afb75a56a5bec74243be9b367ba4ca08 | Bin 0 -> 19 bytes .../16/c726e0fd7c59a94a0a2ada27fca340fea5de3c | Bin 0 -> 129 bytes .../1f/247c26afea82b4a4dedfe0ebe7946e3dcaa226 | 2 + .../24/7ba04bb6ec6c1dd5b850e700b6a9c421436c8a | Bin 0 -> 129 bytes .../25/0db0309633412161808594a7c466d512b1a70d | Bin 0 -> 129 bytes .../35/e0dddab1fda55a937272c72c941e1877a47300 | 2 + .../42/df9f7ff8f57a2051a8ed49f3d7b122c59ed113 | Bin 0 -> 20 bytes .../45/8fb3875b4a025eda64ab9cde035ff0a360eda5 | Bin 0 -> 75 bytes .../45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 | Bin 0 -> 18 bytes .../46/6cd582210eceaec48d949c7adaa0ceb2042db6 | Bin 0 -> 159 bytes .../47/33065c6f0d153b022bb07c11bdcb21235e5f21 | Bin 0 -> 75 bytes .../47/641b05db7cdc469e58e90c9c0e9c6156fd447a | Bin 0 -> 20 bytes .../4f/ca807ffec80984bc710c1b3c641819dece2978 | Bin 0 -> 103 bytes .../5d/b59d25cf0bc7ed07c647175acc05fde8371c2c | 1 + .../64/198fc3a6ae0127863ec2cc781066bd27d202d8 | 3 + .../69/7bf19b0a83d30f031c5c9e6445f2c7f1eb0aa9 | Bin 0 -> 103 bytes .../6b/09eb82e3ead02161c09b8f6e23edd62a036dd0 | Bin 0 -> 164 bytes .../75/7d3de1be565af60d471d877d4f4bf8c7ee7945 | Bin 0 -> 129 bytes .../79/25b6dde1b5ac0f1f666a986da43ef72fd79fb4 | Bin 0 -> 20 bytes .../83/0d97438565f3d14f7fce1b650cabeb7928a84c | Bin 0 -> 23 bytes .../8e/b7cab136eb121ba503b2027f71fcb00f50088e | 3 + .../9b/f6a60c1a7c55f9a3067de627f173c38a030c73 | 3 + .../b4/bc6aa463c07521cb74a9311d6177a89c8f993f | Bin 0 -> 23 bytes .../be/0c516c0cd0f3233b9bfa912a609ad6affa9637 | Bin 0 -> 160 bytes .../be/505c0d986659a6a69c4d99996c73d634a65ae9 | Bin 0 -> 75 bytes .../bf/cd8c4284caaf988d6ed92317a963493ac9e2cc | Bin 0 -> 75 bytes .../ce/013625030ba8dba906f756967f9e9ca394464a | Bin 0 -> 21 bytes .../d9/6ddcf5ba0d41a1cf4596235c546cc96f925e1f | Bin 0 -> 23 bytes .../de/381dd894508c7afe703d317c8a86294e71fdee | Bin 0 -> 20 bytes .../eb/40db816ce2827b304d9f88c534b91cea2fdfbd | 3 + .../eb/7aa582fbfef2fc645152cb6e3fcf5d9f8e3c8a | Bin 0 -> 23 bytes .../resources/sparse/.gitted/refs/heads/main | 1 + tests/resources/sparse/a/file3 | 1 + tests/resources/sparse/a/file4 | 1 + tests/resources/sparse/b/c/file7 | 1 + tests/resources/sparse/b/c/file8 | 1 + tests/resources/sparse/b/d/file10 | 1 + tests/resources/sparse/b/d/file9 | 1 + tests/resources/sparse/b/file12.txt | 1 + tests/resources/sparse/b/file5 | 1 + tests/resources/sparse/b/file6 | 1 + tests/resources/sparse/file1 | 1 + tests/resources/sparse/file11.txt | 1 + tests/resources/sparse/file2 | 1 + 93 files changed, 3297 insertions(+), 177 deletions(-) create mode 100644 include/git2/sparse.h create mode 100644 src/libgit2/sparse.c create mode 100644 src/libgit2/sparse.h create mode 100644 tests/libgit2/sparse/add.c create mode 100644 tests/libgit2/sparse/checkout.c create mode 100644 tests/libgit2/sparse/disable.c create mode 100644 tests/libgit2/sparse/index.c create mode 100644 tests/libgit2/sparse/init.c create mode 100644 tests/libgit2/sparse/list.c create mode 100644 tests/libgit2/sparse/reapply.c create mode 100644 tests/libgit2/sparse/set.c create mode 100644 tests/libgit2/sparse/status.c create mode 100644 tests/libgit2/sparse/worktree.c create mode 100644 tests/resources/sparse/.gitted/COMMIT_EDITMSG create mode 100644 tests/resources/sparse/.gitted/HEAD create mode 100644 tests/resources/sparse/.gitted/ORIG_HEAD create mode 100644 tests/resources/sparse/.gitted/config create mode 100644 tests/resources/sparse/.gitted/description create mode 100755 tests/resources/sparse/.gitted/hooks/applypatch-msg.sample create mode 100755 tests/resources/sparse/.gitted/hooks/commit-msg.sample create mode 100755 tests/resources/sparse/.gitted/hooks/fsmonitor-watchman.sample create mode 100755 tests/resources/sparse/.gitted/hooks/post-update.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-applypatch.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-commit.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-merge-commit.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-push.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-rebase.sample create mode 100755 tests/resources/sparse/.gitted/hooks/pre-receive.sample create mode 100755 tests/resources/sparse/.gitted/hooks/prepare-commit-msg.sample create mode 100755 tests/resources/sparse/.gitted/hooks/push-to-checkout.sample create mode 100755 tests/resources/sparse/.gitted/hooks/update.sample create mode 100644 tests/resources/sparse/.gitted/index create mode 100644 tests/resources/sparse/.gitted/info/exclude create mode 100644 tests/resources/sparse/.gitted/logs/HEAD create mode 100644 tests/resources/sparse/.gitted/logs/refs/heads/main create mode 100644 tests/resources/sparse/.gitted/objects/13/85f264afb75a56a5bec74243be9b367ba4ca08 create mode 100644 tests/resources/sparse/.gitted/objects/16/c726e0fd7c59a94a0a2ada27fca340fea5de3c create mode 100644 tests/resources/sparse/.gitted/objects/1f/247c26afea82b4a4dedfe0ebe7946e3dcaa226 create mode 100644 tests/resources/sparse/.gitted/objects/24/7ba04bb6ec6c1dd5b850e700b6a9c421436c8a create mode 100644 tests/resources/sparse/.gitted/objects/25/0db0309633412161808594a7c466d512b1a70d create mode 100644 tests/resources/sparse/.gitted/objects/35/e0dddab1fda55a937272c72c941e1877a47300 create mode 100644 tests/resources/sparse/.gitted/objects/42/df9f7ff8f57a2051a8ed49f3d7b122c59ed113 create mode 100644 tests/resources/sparse/.gitted/objects/45/8fb3875b4a025eda64ab9cde035ff0a360eda5 create mode 100644 tests/resources/sparse/.gitted/objects/45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 create mode 100644 tests/resources/sparse/.gitted/objects/46/6cd582210eceaec48d949c7adaa0ceb2042db6 create mode 100644 tests/resources/sparse/.gitted/objects/47/33065c6f0d153b022bb07c11bdcb21235e5f21 create mode 100644 tests/resources/sparse/.gitted/objects/47/641b05db7cdc469e58e90c9c0e9c6156fd447a create mode 100644 tests/resources/sparse/.gitted/objects/4f/ca807ffec80984bc710c1b3c641819dece2978 create mode 100644 tests/resources/sparse/.gitted/objects/5d/b59d25cf0bc7ed07c647175acc05fde8371c2c create mode 100644 tests/resources/sparse/.gitted/objects/64/198fc3a6ae0127863ec2cc781066bd27d202d8 create mode 100644 tests/resources/sparse/.gitted/objects/69/7bf19b0a83d30f031c5c9e6445f2c7f1eb0aa9 create mode 100644 tests/resources/sparse/.gitted/objects/6b/09eb82e3ead02161c09b8f6e23edd62a036dd0 create mode 100644 tests/resources/sparse/.gitted/objects/75/7d3de1be565af60d471d877d4f4bf8c7ee7945 create mode 100644 tests/resources/sparse/.gitted/objects/79/25b6dde1b5ac0f1f666a986da43ef72fd79fb4 create mode 100644 tests/resources/sparse/.gitted/objects/83/0d97438565f3d14f7fce1b650cabeb7928a84c create mode 100644 tests/resources/sparse/.gitted/objects/8e/b7cab136eb121ba503b2027f71fcb00f50088e create mode 100644 tests/resources/sparse/.gitted/objects/9b/f6a60c1a7c55f9a3067de627f173c38a030c73 create mode 100644 tests/resources/sparse/.gitted/objects/b4/bc6aa463c07521cb74a9311d6177a89c8f993f create mode 100644 tests/resources/sparse/.gitted/objects/be/0c516c0cd0f3233b9bfa912a609ad6affa9637 create mode 100644 tests/resources/sparse/.gitted/objects/be/505c0d986659a6a69c4d99996c73d634a65ae9 create mode 100644 tests/resources/sparse/.gitted/objects/bf/cd8c4284caaf988d6ed92317a963493ac9e2cc create mode 100644 tests/resources/sparse/.gitted/objects/ce/013625030ba8dba906f756967f9e9ca394464a create mode 100644 tests/resources/sparse/.gitted/objects/d9/6ddcf5ba0d41a1cf4596235c546cc96f925e1f create mode 100644 tests/resources/sparse/.gitted/objects/de/381dd894508c7afe703d317c8a86294e71fdee create mode 100644 tests/resources/sparse/.gitted/objects/eb/40db816ce2827b304d9f88c534b91cea2fdfbd create mode 100644 tests/resources/sparse/.gitted/objects/eb/7aa582fbfef2fc645152cb6e3fcf5d9f8e3c8a create mode 100644 tests/resources/sparse/.gitted/refs/heads/main create mode 100644 tests/resources/sparse/a/file3 create mode 100644 tests/resources/sparse/a/file4 create mode 100644 tests/resources/sparse/b/c/file7 create mode 100644 tests/resources/sparse/b/c/file8 create mode 100644 tests/resources/sparse/b/d/file10 create mode 100644 tests/resources/sparse/b/d/file9 create mode 100644 tests/resources/sparse/b/file12.txt create mode 100644 tests/resources/sparse/b/file5 create mode 100644 tests/resources/sparse/b/file6 create mode 100644 tests/resources/sparse/file1 create mode 100644 tests/resources/sparse/file11.txt create mode 100644 tests/resources/sparse/file2 diff --git a/include/git2.h b/include/git2.h index 3457e5f0476..8b01cd1c236 100644 --- a/include/git2.h +++ b/include/git2.h @@ -60,6 +60,7 @@ #include "git2/revparse.h" #include "git2/revwalk.h" #include "git2/signature.h" +#include "git2/sparse.h" #include "git2/stash.h" #include "git2/status.h" #include "git2/submodule.h" diff --git a/include/git2/checkout.h b/include/git2/checkout.h index 9f834111a67..16b1f3da8e9 100644 --- a/include/git2/checkout.h +++ b/include/git2/checkout.h @@ -186,6 +186,12 @@ typedef enum { /** Include common ancestor data in zdiff3 format for conflicts */ GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3 = (1u << 25), + /** + * Remove files that are excluded by the sparse-checkout ruleset. + * Does nothing when GIT_CHECKOUT_SAFE is set. + */ + GIT_CHECKOUT_REMOVE_SPARSE_FILES = (1u << 26), + /** * THE FOLLOWING OPTIONS ARE NOT YET IMPLEMENTED */ diff --git a/include/git2/diff.h b/include/git2/diff.h index 850d215a613..430c0ce2d7d 100644 --- a/include/git2/diff.h +++ b/include/git2/diff.h @@ -445,6 +445,11 @@ typedef struct { * Defaults to "b". */ const char *new_prefix; + + /** Skip files in the diff that are excluded by the `sparse-checkout` file. + * Set to 1 to skip sparse files, 0 otherwise + */ + int skip_sparse_files; } git_diff_options; /* The current version of the diff options structure */ diff --git a/include/git2/sparse.h b/include/git2/sparse.h new file mode 100644 index 00000000000..499cf015fc1 --- /dev/null +++ b/include/git2/sparse.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_git_sparse_h__ +#define INCLUDE_git_sparse_h__ + +#include "common.h" +#include "types.h" + +GIT_BEGIN_DECL + +typedef struct { + unsigned int version; /**< The version */ + + /** + * Set to zero (false) to consider sparse-checkout patterns as + * full patterns, or non-zero for cone patterns. + */ + /* int cone; */ +} git_sparse_checkout_init_options; + +#define GIT_SPARSE_CHECKOUT_INIT_OPTIONS_VERSION 1 +#define GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT {GIT_SPARSE_CHECKOUT_INIT_OPTIONS_VERSION}; + +/** + * Enable the core.sparseCheckout setting. If the sparse-checkout + * file does not exist, then populate it with patterns that match + * every file in the root directory and no other directories. + * + * @param repo Repository where to find the sparse-checkout file + * @param opts The `git_sparse_checkout_init_options` when + * initializing the sparse-checkout file + * @return 0 or an error code + */ +GIT_EXTERN(int) git_sparse_checkout_init( + git_repository *repo, + git_sparse_checkout_init_options *opts); + +/** + * Fill a list with all the patterns in the sparse-checkout file + * + * @param patterns Pointer to a git_strarray structure where + * the patterns will be stored + * @param repo Repository where to find the sparse-checkout file + * @return 0 or an error code + */ +GIT_EXTERN(int) git_sparse_checkout_list( + git_strarray *patterns, + git_repository *repo); + +/** + * Write a set of patterns to the sparse-checkout file. + * Update the working directory to match the new patterns. + * Enable the core.sparseCheckout config setting if it is not + * already enabled. + * + * @param repo Repository where to find the sparse-checkout file + * @param patterns Pointer to a git_strarray structure where + * the patterns to set can be found + * @return 0 or an error code + */ +GIT_EXTERN(int) git_sparse_checkout_set( + git_repository *repo, + git_strarray *patterns); + +/** + * Update the sparse-checkout file to include additional patterns. + * + * @param repo Repository where to find the sparse-checkout file + * @param patterns Pointer to a git_strarray structure where + * the patterns to set can be found + * @return 0 or an error code + */ +GIT_EXTERN(int) git_sparse_checkout_add( + git_repository *repo, + git_strarray *patterns); + +GIT_EXTERN(int) git_sparse_checkout_reapply(git_repository *repo); + +/** + * Disable the core.sparseCheckout config setting, and restore the + * working directory to include all files. Leaves the sparse-checkout + * file intact so a later git sparse-checkout init command may return + * the working directory to the same state. + * + * @param repo Repository where to find the sparse-checkout file + * @return 0 or an error code + */ +GIT_EXTERN(int) git_sparse_checkout_disable(git_repository *repo); + +/** + * Test if the sparse-checkout rules apply to a given path. + * + * This function checks the sparse-checkout rules to see if they would apply + * to the given path. This indicates if the path would be included on checkout. + * + * @param checkout boolean returning 1 if the sparse-checkout rules apply + * (the file will be checked out), 0 if they do not + * @param repo Repository where to find the sparse-checkout file + * @param path the path to check sparse-checkout rules for, relative to the repo's workdir. + * @return 0 if sparse-checkout rules could be processed for the path + * (regardless of whether it exists or not), or an error < 0 if they could not. + */ +GIT_EXTERN(int) git_sparse_check_path( + int *checkout, + git_repository *repo, + const char *path); + +GIT_END_DECL + +#endif diff --git a/src/libgit2/attr_file.c b/src/libgit2/attr_file.c index afa8ec7b379..be285a33f7f 100644 --- a/src/libgit2/attr_file.c +++ b/src/libgit2/attr_file.c @@ -1025,3 +1025,152 @@ void git_attr_session__free(git_attr_session *session) memset(session, 0, sizeof(git_attr_session)); } + + +/** + * A negative ignore pattern can negate a positive one without + * wildcards if it is a basename only and equals the basename of + * the positive pattern. Thus + * + * foo/bar + * !bar + * + * would result in foo/bar being unignored again while + * + * moo/foo/bar + * !foo/bar + * + * would do nothing. The reverse also holds true: a positive + * basename pattern can be negated by unignoring the basename in + * subdirectories. Thus + * + * bar + * !foo/bar + * + * would result in foo/bar being unignored again. As with the + * first case, + * + * foo/bar + * !moo/foo/bar + * + * would do nothing, again. + */ +static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg) +{ + int (*cmp)(const char *, const char *, size_t); + git_attr_fnmatch *longer, *shorter; + char *p; + + if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0 + || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) + return false; + + if (neg->flags & GIT_ATTR_FNMATCH_ICASE) + cmp = git__strncasecmp; + else + cmp = git__strncmp; + + /* If lengths match we need to have an exact match */ + if (rule->length == neg->length) { + return cmp(rule->pattern, neg->pattern, rule->length) == 0; + } else if (rule->length < neg->length) { + shorter = rule; + longer = neg; + } else { + shorter = neg; + longer = rule; + } + + /* Otherwise, we need to check if the shorter + * rule is a basename only (that is, it contains + * no path separator) and, if so, if it + * matches the tail of the longer rule */ + p = longer->pattern + longer->length - shorter->length; + + if (p[-1] != '/') + return false; + if (memchr(shorter->pattern, '/', shorter->length) != NULL) + return false; + + return cmp(p, shorter->pattern, shorter->length) == 0; +} + +/** + * A negative ignore can only unignore a file which is given explicitly before, thus + * + * foo + * !foo/bar + * + * does not unignore 'foo/bar' as it's not in the list. However + * + * foo/ + * !foo/bar + * + * does unignore 'foo/bar', as it is contained within the 'foo/' rule. + */ +int git_attr__does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match) +{ + int error = 0, wildmatch_flags, effective_flags; + size_t i; + git_attr_fnmatch *rule; + char *path; + git_str buf = GIT_STR_INIT; + + *out = 0; + + wildmatch_flags = WM_PATHNAME; + if (match->flags & GIT_ATTR_FNMATCH_ICASE) + wildmatch_flags |= WM_CASEFOLD; + + /* path of the file relative to the workdir, so we match the rules in subdirs */ + if (match->containing_dir) { + git_str_puts(&buf, match->containing_dir); + } + if (git_str_puts(&buf, match->pattern) < 0) + return -1; + + path = git_str_detach(&buf); + + git_vector_foreach(rules, i, rule) { + if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) { + if (does_negate_pattern(rule, match)) { + error = 0; + *out = 1; + goto out; + } + else + continue; + } + + git_str_clear(&buf); + if (rule->containing_dir) + git_str_puts(&buf, rule->containing_dir); + git_str_puts(&buf, rule->pattern); + + if (git_str_oom(&buf)) + goto out; + + /* + * if rule isn't for full path we match without PATHNAME flag + * as lines like *.txt should match something like dir/test.txt + * requiring * to also match / + */ + effective_flags = wildmatch_flags; + if (!(rule->flags & GIT_ATTR_FNMATCH_FULLPATH)) + effective_flags &= ~WM_PATHNAME; + + /* if we found a match, we want to keep this rule */ + if ((wildmatch(git_str_cstr(&buf), path, effective_flags)) == WM_MATCH) { + *out = 1; + error = 0; + goto out; + } + } + + error = 0; + + out: + git__free(path); + git_str_dispose(&buf); + return error; +} diff --git a/src/libgit2/attr_file.h b/src/libgit2/attr_file.h index 08630d1a6eb..5e68a211abb 100644 --- a/src/libgit2/attr_file.h +++ b/src/libgit2/attr_file.h @@ -238,4 +238,6 @@ extern int git_attr_assignment__parse( git_vector *assigns, const char **scan); +extern int git_attr__does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match); + #endif diff --git a/src/libgit2/checkout.c b/src/libgit2/checkout.c index 6a4643196b0..ba45d2e13f1 100644 --- a/src/libgit2/checkout.c +++ b/src/libgit2/checkout.c @@ -390,8 +390,15 @@ static int checkout_action_wd_only( if (wd->mode != GIT_FILEMODE_TREE) { if (!error) { /* found by git_index__find_pos call */ - notify = GIT_CHECKOUT_NOTIFY_DIRTY; - remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + + /* Sparse checkout will set the SKIP_WORKTREE bit if a file should be skipped */ + const git_index_entry *e = git_index_get_byindex(data->index, pos); + if (e == NULL || + (e->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE) == 0 || + (data->strategy & GIT_CHECKOUT_REMOVE_SPARSE_FILES) != 0) { + notify = GIT_CHECKOUT_NOTIFY_DIRTY; + remove = ((data->strategy & GIT_CHECKOUT_FORCE) != 0); + } } else if (error != GIT_ENOTFOUND) return error; else @@ -2571,6 +2578,8 @@ int git_checkout_iterator( GIT_DIFF_INCLUDE_TYPECHANGE_TREES | GIT_DIFF_SKIP_BINARY_CHECK | GIT_DIFF_INCLUDE_CASECHANGE; + diff_opts.skip_sparse_files = 1; + if (data.opts.checkout_strategy & GIT_CHECKOUT_DISABLE_PATHSPEC_MATCH) diff_opts.flags |= GIT_DIFF_DISABLE_PATHSPEC_MATCH; if (data.opts.paths.count > 0) diff --git a/src/libgit2/config_cache.c b/src/libgit2/config_cache.c index 4bb91f52b9f..5e6a49008d1 100644 --- a/src/libgit2/config_cache.c +++ b/src/libgit2/config_cache.c @@ -87,6 +87,7 @@ static struct map_data _configmaps[] = { {"core.protectntfs", NULL, 0, GIT_PROTECTNTFS_DEFAULT }, {"core.fsyncobjectfiles", NULL, 0, GIT_FSYNCOBJECTFILES_DEFAULT }, {"core.longpaths", NULL, 0, GIT_LONGPATHS_DEFAULT }, + {"core.sparsecheckout", NULL, 0, GIT_SPARSECHECKOUT_DEFAULT }, }; int git_config__configmap_lookup(int *out, git_config *config, git_configmap_item item) diff --git a/src/libgit2/diff_generate.c b/src/libgit2/diff_generate.c index a88ce8c3230..ac9988699c1 100644 --- a/src/libgit2/diff_generate.c +++ b/src/libgit2/diff_generate.c @@ -799,6 +799,10 @@ static int maybe_modified( if (!diff_pathspec_match(&matched_pathspec, diff, oitem)) return 0; + if (diff->base.opts.skip_sparse_files && + git_iterator_current_skip_checkout(info->new_iter)) + return 0; + /* on platforms with no symlinks, preserve mode of existing symlinks */ if (S_ISLNK(omode) && S_ISREG(nmode) && new_is_workdir && !(diff->diffcaps & GIT_DIFFCAPS_HAS_SYMLINKS)) @@ -1028,6 +1032,11 @@ static int handle_unmatched_new_item( const git_index_entry *nitem = info->nitem; git_delta_t delta_type = GIT_DELTA_UNTRACKED; bool contains_oitem; + + /* check if this item should be skipped due to sparse checkout */ + if (diff->base.opts.skip_sparse_files && + git_iterator_current_skip_checkout(info->new_iter)) + return iterator_advance(&info->nitem, info->new_iter); /* check if this is a prefix of the other side */ contains_oitem = entry_is_prefixed(diff, info->oitem, nitem); @@ -1188,7 +1197,12 @@ static int handle_unmatched_old_item( if (git_index_entry_is_conflict(info->oitem)) delta_type = GIT_DELTA_CONFLICTED; - if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0) + if ((diff->base.opts.skip_sparse_files && + git_iterator_current_skip_checkout(info->new_iter)) || + (info->oitem->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE) != 0) + delta_type = GIT_DELTA_UNMODIFIED; + + else if ((error = diff_delta__from_one(diff, delta_type, info->oitem, NULL)) < 0) return error; /* if we are generating TYPECHANGE records then check for that diff --git a/src/libgit2/ignore.c b/src/libgit2/ignore.c index cee58d7f15f..bc97a2de93d 100644 --- a/src/libgit2/ignore.c +++ b/src/libgit2/ignore.c @@ -19,160 +19,16 @@ #define GIT_IGNORE_DEFAULT_RULES ".\n..\n.git\n" -/** - * A negative ignore pattern can negate a positive one without - * wildcards if it is a basename only and equals the basename of - * the positive pattern. Thus - * - * foo/bar - * !bar - * - * would result in foo/bar being unignored again while - * - * moo/foo/bar - * !foo/bar - * - * would do nothing. The reverse also holds true: a positive - * basename pattern can be negated by unignoring the basename in - * subdirectories. Thus - * - * bar - * !foo/bar - * - * would result in foo/bar being unignored again. As with the - * first case, - * - * foo/bar - * !moo/foo/bar - * - * would do nothing, again. - */ -static int does_negate_pattern(git_attr_fnmatch *rule, git_attr_fnmatch *neg) -{ - int (*cmp)(const char *, const char *, size_t); - git_attr_fnmatch *longer, *shorter; - char *p; - - if ((rule->flags & GIT_ATTR_FNMATCH_NEGATIVE) != 0 - || (neg->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) - return false; - - if (neg->flags & GIT_ATTR_FNMATCH_ICASE) - cmp = git__strncasecmp; - else - cmp = git__strncmp; - - /* If lengths match we need to have an exact match */ - if (rule->length == neg->length) { - return cmp(rule->pattern, neg->pattern, rule->length) == 0; - } else if (rule->length < neg->length) { - shorter = rule; - longer = neg; - } else { - shorter = neg; - longer = rule; - } - - /* Otherwise, we need to check if the shorter - * rule is a basename only (that is, it contains - * no path separator) and, if so, if it - * matches the tail of the longer rule */ - p = longer->pattern + longer->length - shorter->length; - - if (p[-1] != '/') - return false; - if (memchr(shorter->pattern, '/', shorter->length) != NULL) - return false; - - return cmp(p, shorter->pattern, shorter->length) == 0; -} - -/** - * A negative ignore can only unignore a file which is given explicitly before, thus - * - * foo - * !foo/bar - * - * does not unignore 'foo/bar' as it's not in the list. However - * - * foo/ - * !foo/bar - * - * does unignore 'foo/bar', as it is contained within the 'foo/' rule. - */ -static int does_negate_rule(int *out, git_vector *rules, git_attr_fnmatch *match) -{ - int error = 0, wildmatch_flags, effective_flags; - size_t i; - git_attr_fnmatch *rule; - char *path; - git_str buf = GIT_STR_INIT; - - *out = 0; - - wildmatch_flags = WM_PATHNAME; - if (match->flags & GIT_ATTR_FNMATCH_ICASE) - wildmatch_flags |= WM_CASEFOLD; - - /* path of the file relative to the workdir, so we match the rules in subdirs */ - if (match->containing_dir) { - git_str_puts(&buf, match->containing_dir); - } - if (git_str_puts(&buf, match->pattern) < 0) - return -1; - - path = git_str_detach(&buf); - - git_vector_foreach(rules, i, rule) { - if (!(rule->flags & GIT_ATTR_FNMATCH_HASWILD)) { - if (does_negate_pattern(rule, match)) { - error = 0; - *out = 1; - goto out; - } - else - continue; - } - - git_str_clear(&buf); - if (rule->containing_dir) - git_str_puts(&buf, rule->containing_dir); - git_str_puts(&buf, rule->pattern); - - if (git_str_oom(&buf)) - goto out; - - /* - * if rule isn't for full path we match without PATHNAME flag - * as lines like *.txt should match something like dir/test.txt - * requiring * to also match / - */ - effective_flags = wildmatch_flags; - if (!(rule->flags & GIT_ATTR_FNMATCH_FULLPATH)) - effective_flags &= ~WM_PATHNAME; - - /* if we found a match, we want to keep this rule */ - if ((wildmatch(git_str_cstr(&buf), path, effective_flags)) == WM_MATCH) { - *out = 1; - error = 0; - goto out; - } - } - - error = 0; - -out: - git__free(path); - git_str_dispose(&buf); - return error; -} - -static int parse_ignore_file( - git_repository *repo, git_attr_file *attrs, const char *data, bool allow_macros) +int parse_ignore_file( + git_repository *repo, + git_attr_file *attrs, + const char *data, + const char *context, + bool allow_macros) { int error = 0; int ignore_case = false; - const char *scan = data, *context = NULL; + const char *scan = data; git_attr_fnmatch *match = NULL; GIT_UNUSED(allow_macros); @@ -180,14 +36,8 @@ static int parse_ignore_file( if (git_repository__configmap_lookup(&ignore_case, repo, GIT_CONFIGMAP_IGNORECASE) < 0) git_error_clear(); - /* if subdir file path, convert context for file paths */ - if (attrs->entry && - git_fs_path_root(attrs->entry->path) < 0 && - !git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE)) - context = attrs->entry->path; - - if (git_mutex_lock(&attrs->lock) < 0) { - git_error_set(GIT_ERROR_OS, "failed to lock ignore file"); + if (git_mutex_lock(&attrs->lock)) { + git_error_set(GIT_ERROR_OS, "failed to lock %s file", attrs->source.filename); return -1; } @@ -199,8 +49,7 @@ static int parse_ignore_file( break; } - match->flags = - GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG; + match->flags = GIT_ATTR_FNMATCH_ALLOWSPACE | GIT_ATTR_FNMATCH_ALLOWNEG; if (!(error = git_attr_fnmatch__parse( match, &attrs->pool, context, &scan))) @@ -219,8 +68,8 @@ static int parse_ignore_file( * do not optimize away these rules, though. * */ if (match->flags & GIT_ATTR_FNMATCH_NEGATIVE - && !(match->flags & GIT_ATTR_FNMATCH_HASWILD)) - error = does_negate_rule(&valid_rule, &attrs->rules, match); + && !(match->flags & GIT_ATTR_FNMATCH_HASWILD)) + error = git_attr__does_negate_rule(&valid_rule, &attrs->rules, match); if (!error && valid_rule) error = git_vector_insert(&attrs->rules, match); @@ -242,6 +91,25 @@ static int parse_ignore_file( return error; } +static int git_ignore__parse_ignore_file( + git_repository *repo, + git_attr_file *attrs, + const char *data, + bool allow_macros) +{ + const char *context = NULL; + + GIT_UNUSED(allow_macros); + + /* if subdir file path, convert context for file paths */ + if (attrs->entry && + git_fs_path_root(attrs->entry->path) < 0 && + !git__suffixcmp(attrs->entry->path, "/" GIT_IGNORE_FILE)) + context = attrs->entry->path; + + return parse_ignore_file(repo, attrs, data, context, allow_macros); +} + static int push_ignore_file( git_ignores *ignores, git_vector *which_list, @@ -252,7 +120,7 @@ static int push_ignore_file( git_attr_file *file = NULL; int error = 0; - error = git_attr_cache__get(&file, ignores->repo, NULL, &source, parse_ignore_file, false); + error = git_attr_cache__get(&file, ignores->repo, NULL, &source, git_ignore__parse_ignore_file, false); if (error < 0) return error; @@ -284,7 +152,7 @@ static int get_internal_ignores(git_attr_file **out, git_repository *repo) /* if internal rules list is empty, insert default rules */ if (!error && !(*out)->rules.length) - error = parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false); + error = git_ignore__parse_ignore_file(repo, *out, GIT_IGNORE_DEFAULT_RULES, false); return error; } @@ -504,7 +372,7 @@ int git_ignore_add_rule(git_repository *repo, const char *rules) if ((error = get_internal_ignores(&ign_internal, repo)) < 0) return error; - error = parse_ignore_file(repo, ign_internal, rules, false); + error = git_ignore__parse_ignore_file(repo, ign_internal, rules, false); git_attr_file__free(ign_internal); return error; @@ -519,7 +387,7 @@ int git_ignore_clear_internal_rules(git_repository *repo) return error; if (!(error = git_attr_file__clear_rules(ign_internal, true))) - error = parse_ignore_file( + error = git_ignore__parse_ignore_file( repo, ign_internal, GIT_IGNORE_DEFAULT_RULES, false); git_attr_file__free(ign_internal); diff --git a/src/libgit2/ignore.h b/src/libgit2/ignore.h index aa5ca62b7af..009fd285275 100644 --- a/src/libgit2/ignore.h +++ b/src/libgit2/ignore.h @@ -62,4 +62,11 @@ extern int git_ignore__lookup(int *out, git_ignores *ign, const char *path, git_ extern int git_ignore__check_pathspec_for_exact_ignores( git_repository *repo, git_vector *pathspec, bool no_fnmatch); +int parse_ignore_file( + git_repository *repo, + git_attr_file *attrs, + const char *data, + const char *context, + bool allow_macros); + #endif diff --git a/src/libgit2/index.c b/src/libgit2/index.c index 1821f6027f7..a6d88248461 100644 --- a/src/libgit2/index.c +++ b/src/libgit2/index.c @@ -21,11 +21,13 @@ #include "diff.h" #include "varint.h" #include "path.h" +#include "sparse.h" #include "git2/odb.h" #include "git2/oid.h" #include "git2/blob.h" #include "git2/config.h" +#include "git2/sparse.h" #include "git2/sys/index.h" static int index_apply_to_wd_diff(git_index *index, int action, const git_strarray *paths, @@ -966,7 +968,8 @@ static int index_entry_init( struct stat st; git_oid oid; git_repository *repo; - + int checkout = 0; + if (INDEX_OWNER(index) == NULL) return create_index_error(-1, "could not initialize index entry. " @@ -1005,6 +1008,9 @@ static int index_entry_init( entry->id = oid; git_index_entry__init_from_stat(entry, &st, !index->distrust_filemode); + if (git_sparse_check_path(&checkout, repo, rel_path) == 0 && checkout == 0) + entry->flags_extended = GIT_INDEX_ENTRY_SKIP_WORKTREE; + *entry_out = (git_index_entry *)entry; return 0; } @@ -3108,6 +3114,7 @@ typedef struct read_tree_data { git_vector *new_entries; git_vector_cmp entry_cmp; git_tree_cache *tree; + git_sparse *sparse; } read_tree_data; static int read_tree_cb( @@ -3144,6 +3151,13 @@ static int read_tree_cb( index_entry_adjust_namemask(entry, path.size); git_str_dispose(&path); + + if (data->sparse) { + git_sparse_status status = GIT_SPARSE_CHECKOUT; + if (git_sparse__lookup(&status, data->sparse, entry->path, GIT_DIR_FLAG_FALSE) == 0 && + status == GIT_SPARSE_NOCHECKOUT) + entry->flags_extended = GIT_INDEX_ENTRY_SKIP_WORKTREE; + } if (git_vector_insert(data->new_entries, entry) < 0) { index_entry_free(entry); @@ -3161,7 +3175,9 @@ int git_index_read_tree(git_index *index, const git_tree *tree) read_tree_data data; size_t i; git_index_entry *e; - + git_sparse sparse; + int sparse_checkout_enabled = false; + git_repository* repo = INDEX_OWNER(index); if (git_idxmap_new(&entries_map) < 0) return -1; @@ -3171,7 +3187,13 @@ int git_index_read_tree(git_index *index, const git_tree *tree) data.old_entries = &index->entries; data.new_entries = &entries; data.entry_cmp = index->entries_search; - + + if (repo == NULL || git_repository__configmap_lookup(&sparse_checkout_enabled, repo, GIT_CONFIGMAP_SPARSECHECKOUT) < 0 || + sparse_checkout_enabled == false || git_sparse__init(repo, &sparse) < 0) + data.sparse = NULL; + else + data.sparse = &sparse; + index->tree = NULL; git_pool_clear(&index->tree_pool); @@ -3206,6 +3228,10 @@ int git_index_read_tree(git_index *index, const git_tree *tree) cleanup: git_vector_free(&entries); git_idxmap_free(entries_map); + + if (data.sparse != NULL) + git_sparse__free(&sparse); + if (error < 0) return error; diff --git a/src/libgit2/iterator.c b/src/libgit2/iterator.c index 1ee8e25f505..19d07aea8c3 100644 --- a/src/libgit2/iterator.c +++ b/src/libgit2/iterator.c @@ -10,10 +10,12 @@ #include "tree.h" #include "index.h" #include "path.h" +#include "sparse.h" #define GIT_ITERATOR_FIRST_ACCESS (1 << 15) #define GIT_ITERATOR_HONOR_IGNORES (1 << 16) #define GIT_ITERATOR_IGNORE_DOT_GIT (1 << 17) +#define GIT_ITERATOR_HONOR_SPARSE (1 << 18) #define iterator__flag(I,F) ((((git_iterator *)(I))->flags & GIT_ITERATOR_ ## F) != 0) #define iterator__ignore_case(I) iterator__flag(I,IGNORE_CASE) @@ -25,7 +27,7 @@ #define iterator__honor_ignores(I) iterator__flag(I,HONOR_IGNORES) #define iterator__ignore_dot_git(I) iterator__flag(I,IGNORE_DOT_GIT) #define iterator__descend_symlinks(I) iterator__flag(I,DESCEND_SYMLINKS) - +#define iterator__honor_sparse(I) iterator__flag(I,HONOR_SPARSE) static void iterator_set_ignore_case(git_iterator *iter, bool ignore_case) { @@ -443,6 +445,8 @@ typedef struct { */ git_vector similar_trees; git_array_t(git_str) similar_paths; + + git_sparse_status sparse_status; } tree_iterator_frame; typedef struct { @@ -455,6 +459,9 @@ typedef struct { /* a pool of entries to reduce the number of allocations */ git_pool entry_pool; + + git_sparse sparse; + git_sparse_status current_sparse_status; } tree_iterator; GIT_INLINE(tree_iterator_frame *) tree_iterator_parent_frame( @@ -651,6 +658,19 @@ GIT_INLINE(int) tree_iterator_frame_push_neighbors( return error; } +GIT_INLINE(void) tree_iterator_frame_handle_sparse_checkout( + tree_iterator *iter, tree_iterator_entry *entry, tree_iterator_frame *parent_frame, tree_iterator_frame *frame) +{ + if (git_sparse__lookup(&frame->sparse_status, + &iter->sparse, entry->tree_entry->filename, GIT_DIR_FLAG_TRUE) < 0) { + git_error_clear(); + frame->sparse_status = GIT_SPARSE_NOTFOUND; + } else if (frame->sparse_status <= GIT_SPARSE_NOTFOUND) { + /* inherit sparse_status from parent if no rule specified */ + frame->sparse_status = parent_frame->sparse_status; + } +} + GIT_INLINE(int) tree_iterator_frame_push( tree_iterator *iter, tree_iterator_entry *entry) { @@ -673,7 +693,10 @@ GIT_INLINE(int) tree_iterator_frame_push( if (iterator__ignore_case(&iter->base)) error = tree_iterator_frame_push_neighbors(iter, parent_frame, frame, entry->tree_entry->filename); - + + if (iterator__honor_sparse(&iter->base)) + tree_iterator_frame_handle_sparse_checkout(iter, entry, parent_frame, frame); + done: git_tree_free(tree); return error; @@ -740,6 +763,7 @@ static void tree_iterator_set_current( iter->entry.mode = tree_entry->attr; iter->entry.path = iter->entry_path.ptr; + iter->current_sparse_status = GIT_SPARSE_UNCHECKED; git_oid_cpy(&iter->entry.id, &tree_entry->oid); } @@ -894,6 +918,9 @@ static void tree_iterator_clear(tree_iterator *iter) git_pool_clear(&iter->entry_pool); git_str_clear(&iter->entry_path); + + if (iterator__honor_sparse(&iter->base)) + git_sparse__free(&iter->sparse); iterator_clear(&iter->base); } @@ -906,6 +933,10 @@ static int tree_iterator_init(tree_iterator *iter) (error = tree_iterator_frame_init(iter, iter->root, NULL)) < 0) return error; + if (iterator__honor_sparse(&iter->base) && + (error = git_sparse__init(iter->base.repo, &iter->sparse)) < 0) + return error; + iter->base.flags &= ~GIT_ITERATOR_FIRST_ACCESS; return 0; @@ -936,6 +967,8 @@ int git_iterator_for_tree( { tree_iterator *iter; int error; + int sparse_checkout_enabled = false; + git_repository* repo = NULL; static git_iterator_callbacks callbacks = { tree_iterator_current, @@ -951,14 +984,22 @@ int git_iterator_for_tree( if (tree == NULL) return git_iterator_for_nothing(out, options); + repo = git_tree_owner(tree); + iter = git__calloc(1, sizeof(tree_iterator)); GIT_ERROR_CHECK_ALLOC(iter); iter->base.type = GIT_ITERATOR_TREE; iter->base.cb = &callbacks; + + if (git_repository__configmap_lookup(&sparse_checkout_enabled, repo, GIT_CONFIGMAP_SPARSECHECKOUT) < 0) + git_error_clear(); + + if (sparse_checkout_enabled == true) + options->flags |= GIT_ITERATOR_HONOR_SPARSE; if ((error = iterator_init_common(&iter->base, - git_tree_owner(tree), NULL, options)) < 0 || + repo, NULL, options)) < 0 || (error = git_tree_dup(&iter->root, tree)) < 0 || (error = tree_iterator_init(iter)) < 0) goto on_error; @@ -1766,6 +1807,46 @@ bool git_iterator_current_tree_is_ignored(git_iterator *i) return (frame->is_ignored == GIT_IGNORE_TRUE); } +static void tree_iterator_update_sparse_checkout(tree_iterator *iter) +{ + tree_iterator_frame *frame; + git_dir_flag dir_flag = entry_dir_flag(&iter->entry); + + if (git_sparse__lookup(&iter->current_sparse_status, + &iter->sparse, iter->entry.path, dir_flag) < 0) { + git_error_clear(); + iter->current_sparse_status = GIT_SPARSE_NOTFOUND; + } + + /* use sparse checkout from containing frame stack */ + if (iter->current_sparse_status <= GIT_SPARSE_NOTFOUND) { + frame = tree_iterator_current_frame(iter); + iter->current_sparse_status = frame->sparse_status; + } +} + +GIT_INLINE(bool) tree_iterator_current_skip_checkout( + tree_iterator *iter) +{ + if (iter->current_sparse_status == GIT_SPARSE_UNCHECKED) + tree_iterator_update_sparse_checkout(iter); + + return (iter->current_sparse_status == GIT_SPARSE_NOCHECKOUT); +} + +bool git_iterator_current_skip_checkout(git_iterator *i) +{ + tree_iterator *iter = NULL; + + if (i->type != GIT_ITERATOR_TREE) + return false; + + iter = GIT_CONTAINER_OF(i, tree_iterator, base); + if (iterator__honor_sparse(&iter->base) == false) + return false; + + return tree_iterator_current_skip_checkout(iter); +} static int filesystem_iterator_advance_over( const git_index_entry **out, git_iterator_status_t *status, diff --git a/src/libgit2/iterator.h b/src/libgit2/iterator.h index 6bb8489d035..493e28f45ac 100644 --- a/src/libgit2/iterator.h +++ b/src/libgit2/iterator.h @@ -276,6 +276,8 @@ extern bool git_iterator_current_is_ignored(git_iterator *iter); extern bool git_iterator_current_tree_is_ignored(git_iterator *iter); +extern bool git_iterator_current_skip_checkout(git_iterator *iter); + /** * Get full path of the current item from a workdir iterator. This will * return NULL for a non-workdir iterator. The git_str is still owned by diff --git a/src/libgit2/repository.h b/src/libgit2/repository.h index a488f2bf2fe..81d7daece14 100644 --- a/src/libgit2/repository.h +++ b/src/libgit2/repository.h @@ -53,6 +53,7 @@ typedef enum { GIT_CONFIGMAP_PROTECTNTFS, /* core.protectNTFS */ GIT_CONFIGMAP_FSYNCOBJECTFILES, /* core.fsyncObjectFiles */ GIT_CONFIGMAP_LONGPATHS, /* core.longpaths */ + GIT_CONFIGMAP_SPARSECHECKOUT, /* core.sparseCheckout */ GIT_CONFIGMAP_CACHE_MAX } git_configmap_item; @@ -119,7 +120,9 @@ typedef enum { /* core.fsyncObjectFiles */ GIT_FSYNCOBJECTFILES_DEFAULT = GIT_CONFIGMAP_FALSE, /* core.longpaths */ - GIT_LONGPATHS_DEFAULT = GIT_CONFIGMAP_FALSE + GIT_LONGPATHS_DEFAULT = GIT_CONFIGMAP_FALSE, + /* core.sparseCheckout */ + GIT_SPARSECHECKOUT_DEFAULT = GIT_CONFIGMAP_FALSE, } git_configmap_value; /* internal repository init flags */ diff --git a/src/libgit2/sparse.c b/src/libgit2/sparse.c new file mode 100644 index 00000000000..ecceb09b1f1 --- /dev/null +++ b/src/libgit2/sparse.c @@ -0,0 +1,654 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ + +#include "sparse.h" +#include "attrcache.h" +#include "git2/sparse.h" +#include "config.h" +#include "filebuf.h" +#include "index.h" +#include "ignore.h" + +static bool sparse_lookup_in_rules( + int *checkout, + git_attr_file *file, + git_attr_path *path) +{ + size_t j; + git_attr_fnmatch *match; + + git_vector_rforeach(&file->rules, j, match) { + if (match->flags & GIT_ATTR_FNMATCH_DIRECTORY && + path->is_dir == GIT_DIR_FLAG_FALSE) + continue; + if (git_attr_fnmatch__match(match, path)) { + *checkout = ((match->flags & GIT_ATTR_FNMATCH_NEGATIVE) == 0) ? + GIT_SPARSE_CHECKOUT : GIT_SPARSE_NOCHECKOUT; + return true; + } + } + + return false; +} + +static int parse_sparse_file( + git_repository *repo, + git_attr_file *attrs, + const char *data, + bool allow_macros) +{ + /* Todo: Support for cone mode */ + return parse_ignore_file( + repo, + attrs, + data, + NULL, + allow_macros); +} + +int git_sparse_attr_file__init_( + int *file_exists, + git_repository *repo, + git_sparse *sparse) +{ + int error = 0; + git_str infopath = GIT_STR_INIT; + const char *filename = GIT_SPARSE_CHECKOUT_FILE; + git_attr_file_source source = { GIT_ATTR_FILE_SOURCE_FILE, git_str_cstr(&infopath), filename, NULL }; + git_str filepath = GIT_STR_INIT; + + if ((error = git_str_joinpath(&infopath, repo->gitdir, "info")) < 0) { + if (error != GIT_ENOTFOUND) + goto done; + error = 0; + } + + source.base = git_str_cstr(&infopath); + source.filename = filename; + + git_str_joinpath(&filepath, infopath.ptr, filename); + + /* Don't overwrite any existing sparse-checkout file */ + *file_exists = git_fs_path_exists(git_str_cstr(&filepath)); + if (!*file_exists) { + if ((error = git_futils_creat_withpath(git_str_cstr(&filepath), 0777, 0666)) < 0) + goto done; + } + + error = git_attr_cache__get(&sparse->sparse, repo, NULL, &source, parse_sparse_file, false); + +done: + git_str_dispose(&infopath); + return error; +} + +int git_sparse_attr_file__init( + git_repository *repo, + git_sparse *sparse) +{ + int b = false; + int error = git_sparse_attr_file__init_(&b, repo, sparse); + return error; +} + +int git_sparse__init_( + int *file_exists, + git_repository *repo, + git_sparse *sparse) +{ + int error = 0; + + assert(repo && sparse); + + memset(sparse, 0, sizeof(*sparse)); + sparse->repo = repo; + + /* Read the ignore_case flag */ + if ((error = git_repository__configmap_lookup( + &sparse->ignore_case, repo, GIT_CONFIGMAP_IGNORECASE)) < 0) + goto cleanup; + + if ((error = git_attr_cache__init(repo)) < 0) + goto cleanup; + + if ((error = git_sparse_attr_file__init_(file_exists, repo, sparse)) < 0) { + if (error != GIT_ENOTFOUND) + goto cleanup; + error = 0; + } + +cleanup: + if (error < 0) + git_sparse__free(sparse); + + return error; +} + +int git_sparse__init( + git_repository *repo, + git_sparse *sparse) +{ + int b = false; + int error = git_sparse__init_(&b, repo, sparse); + return error; +} + +int git_sparse__lookup( + git_sparse_status* status, + git_sparse *sparse, + const char* pathname, + git_dir_flag dir_flag) +{ + git_attr_path path; + const char *workdir; + int error; + + GIT_ASSERT_ARG(status); + GIT_ASSERT_ARG(sparse); + GIT_ASSERT_ARG(pathname); + + *status = GIT_SPARSE_CHECKOUT; + + workdir = git_repository_workdir(sparse->repo); + if ((error = git_attr_path__init(&path, pathname, workdir, dir_flag))) + return -1; + + /* No match -> no checkout */ + *status = GIT_SPARSE_NOCHECKOUT; + + while (1) { + if (sparse_lookup_in_rules(status, sparse->sparse, &path)) + goto cleanup; + + /* move up one directory */ + if (path.basename == path.path) + break; + path.basename[-1] = '\0'; + while (path.basename > path.path && *path.basename != '/') + path.basename--; + if (path.basename > path.path) + path.basename++; + path.is_dir = 1; + } + +cleanup: + git_attr_path__free(&path); + return 0; +} + +void git_sparse__free(git_sparse *sparse) +{ + git_attr_file__free(sparse->sparse); +} + +int git_sparse_checkout__list( + git_vector *patterns, + git_sparse *sparse) +{ + int error = 0; + git_str data = GIT_STR_INIT; + char *scan, *buf; + + GIT_ASSERT_ARG(patterns); + GIT_ASSERT_ARG(sparse); + + if ((error = git_futils_readbuffer(&data, sparse->sparse->entry->fullpath)) < 0) + return error; + + scan = (char *)git_str_cstr(&data); + while (!error && *scan) { + + buf = git__strtok(&scan, "\r\n"); + if (buf) + error = git_vector_insert(patterns, buf); + } + + return error; +} + +int git_sparse_checkout_list(git_strarray *patterns, git_repository *repo) { + + int error = 0; + git_sparse sparse; + git_vector patternlist; + + GIT_ASSERT_ARG(patterns); + GIT_ASSERT_ARG(repo); + + if ((error = git_sparse__init(repo, &sparse))) + goto done; + + if ((error = git_vector_init(&patternlist, 0, NULL)) < 0) + goto done; + + if ((error = git_sparse_checkout__list(&patternlist, &sparse))) + goto done; + + patterns->strings = (char **) git_vector_detach(&patterns->count, NULL, &patternlist); + +done: + git_sparse__free(&sparse); + git_vector_free(&patternlist); + + + return error; +} + +int git_sparse_checkout__reapply(git_repository *repo, git_sparse *sparse) +{ + int error = 0; + git_index *index; + size_t i = 0; + git_index_entry *entry; + git_vector paths_to_checkout; + git_checkout_options copts; + const char *workdir = repo->workdir; + + if ((error = git_repository_index(&index, repo)) < 0) + goto done; + + if ((error = git_vector_init(&paths_to_checkout, 0, NULL)) < 0) + goto done; + + git_vector_foreach(&index->entries, i, entry) + { + int is_submodule = false; + int has_conflict = false; + unsigned int status_flags; + int checkout = GIT_SPARSE_CHECKOUT; + git_str fullpath = GIT_STR_INIT; + + /* Don't touch submodules */ + is_submodule = S_ISGITLINK(entry->mode); + if (is_submodule) + continue; + + /* Don't touch files with conflicts */ + has_conflict = GIT_INDEX_ENTRY_STAGE(entry) > 0; + if (has_conflict) + continue; + + /* Don't touch files that aren't current */ + if ((error = git_status_file(&status_flags, repo, entry->path)) < 0) + goto done; + if (status_flags != GIT_STATUS_CURRENT) + continue; + + if ((error = git_str_joinpath(&fullpath, repo->workdir, entry->path)) < 0) + goto done; + + if (git_sparse__lookup(&checkout, sparse, entry->path, GIT_DIR_FLAG_FALSE) == 0 && + checkout == GIT_SPARSE_NOCHECKOUT) + { + entry->flags_extended |= GIT_INDEX_ENTRY_SKIP_WORKTREE; + + if (!git_fs_path_exists(git_str_cstr(&fullpath))) + continue; + + if ((error = git_futils_rmdir_r(entry->path, workdir, GIT_RMDIR_REMOVE_FILES | GIT_RMDIR_EMPTY_PARENTS)) < 0) + goto done; + } + else + { + entry->flags_extended &= ~GIT_INDEX_ENTRY_SKIP_WORKTREE; + git_vector_insert(&paths_to_checkout, (void*) entry->path); + } + } + + if ((error = git_checkout_options_init(&copts, GIT_CHECKOUT_OPTIONS_VERSION)) < 0) + goto done; + + copts.paths.strings = (char**) git_vector_detach(&copts.paths.count, NULL, &paths_to_checkout); + + copts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_RECREATE_MISSING; + if ((error = git_checkout_index(repo, index, &copts)) < 0) + goto done; + + error = git_index_write(index); + +done: + git_index_free(index); + git_vector_free(&paths_to_checkout); + + return error; +} + +int git_sparse_checkout__set( + git_vector *patterns, + git_repository *repo, + git_sparse *sparse) +{ + int error; + size_t i; + const char *pattern; + git_str content = GIT_STR_INIT; + + git_vector_foreach(patterns, i, pattern) { + git_str_join(&content, '\n', git_str_cstr(&content), pattern); + } + + if ((error = git_futils_truncate(sparse->sparse->entry->fullpath, 0777)) < 0) + goto done; + + if ((error = git_futils_writebuffer(&content, sparse->sparse->entry->fullpath, O_WRONLY, 0644)) < 0) + goto done; + + /* Refresh the rules in the sparse info */ + git_vector_clear(&sparse->sparse->rules); + if ((error = git_sparse_attr_file__init(repo, sparse)) < 0) + goto done; + +done: + git_str_dispose(&content); + + return error; +} + +int git_sparse_checkout__enable(git_repository *repo, git_sparse_checkout_init_options *opts) +{ + int error = 0; + git_config *cfg; + + /* Can be used once cone mode is supported */ + GIT_UNUSED(opts); + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + if ((error = git_config_set_bool(cfg, "core.sparseCheckout", true)) < 0) + goto done; + +done: + git_config_free(cfg); + return error; +} + +int git_sparse_checkout_init(git_repository *repo, git_sparse_checkout_init_options *opts) +{ + int error = 0; + git_sparse sparse; + int file_exists = false; + git_vector default_patterns = GIT_VECTOR_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(opts); + + if ((error = git_sparse_checkout__enable(repo, opts)) < 0) + return error; + + if ((error = git_sparse__init_(&file_exists, repo, &sparse)) < 0) + goto cleanup; + + if (!file_exists) { + + /* Default patterns that match every file in the root directory and no other directories */ + git_vector_insert(&default_patterns, "/*"); + git_vector_insert(&default_patterns, "!/*/"); + + if ((error = git_sparse_checkout__set(&default_patterns, repo, &sparse)) < 0) + goto cleanup; + } + + if ((error = git_sparse_checkout__reapply(repo, &sparse)) < 0) + goto cleanup; + +cleanup: + git_sparse__free(&sparse); + return error; +} + +int git_sparse_checkout_set( + git_repository *repo, + git_strarray *patterns) +{ + int error; + git_config *cfg; + git_sparse sparse; + + size_t i = 0; + git_vector patternlist; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(patterns); + + if ((error = git_repository_config(&cfg, repo)) < 0) + goto done; + + if ((error = git_sparse_checkout__enable(repo, &opts) < 0)) + goto done; + + if ((error = git_sparse__init(repo, &sparse)) < 0) + goto done; + + if ((error = git_vector_init(&patternlist, 0, NULL)) < 0) + goto done; + + for (i = 0; i < patterns->count; i++) { + git_vector_insert(&patternlist, patterns->strings[i]); + } + + if ((error = git_sparse_checkout__set(&patternlist, repo, &sparse)) < 0) + goto done; + + if ((error = git_sparse_checkout__reapply(repo, &sparse)) < 0) + goto done; + +done: + git_config_free(cfg); + git_sparse__free(&sparse); + git_vector_free(&patternlist); + + return error; +} + +int git_sparse_checkout__restore_wd(git_repository *repo) +{ + int error = 0; + git_sparse sparse; + git_vector old_patterns, patterns = GIT_VECTOR_INIT; + + if ((error = git_sparse__init(repo, &sparse)) < 0) + return error; + + /* Store the old patterns so that we can put them back later */ + if ((error = git_vector_init(&old_patterns, 0 ,NULL)) < 0) + goto done; + + if ((error = git_sparse_checkout__list(&old_patterns, &sparse)) < 0) + goto done; + + /* Write down a pattern that will include everything */ + if ((error = git_vector_init(&patterns, 0 ,NULL)) < 0) + goto done; + + if ((error = git_vector_insert(&patterns, "/*")) < 0) + goto done; + + if ((error = git_sparse_checkout__set(&patterns, repo, &sparse)) < 0) + goto done; + + /* Re-apply sparsity with our catch-all pattern */ + if ((error = git_sparse_checkout__reapply(repo, &sparse)) < 0) + goto done; + + /* Restore the sparse-checkout patterns to how they were before */ + if ((error = git_sparse_checkout__set(&old_patterns, repo, &sparse)) < 0) + goto done; + +done: + git_sparse__free(&sparse); + git_vector_free(&old_patterns); + git_vector_free(&patterns); + return error; +} + +int git_sparse_checkout_disable(git_repository *repo) +{ + int error = 0; + git_config *cfg; + + GIT_ASSERT_ARG(repo); + + if ((error = git_repository_config(&cfg, repo)) < 0) + return error; + + if ((error = git_config_set_bool(cfg, "core.sparseCheckout", false)) < 0) + goto done; + + if ((error = git_sparse_checkout__restore_wd(repo)) < 0) + goto done; + +done: + git_config_free(cfg); + + return error; +} + +int git_sparse_checkout__add( + git_repository *repo, + git_vector *patterns, + git_sparse *sparse) +{ + int error = 0; + size_t i = 0; + git_vector existing_patterns; + git_vector new_patterns; + char* pattern; + + if ((error = git_vector_init(&existing_patterns, 0, NULL)) < 0) + goto done; + + if ((error = git_vector_init(&new_patterns, 0, NULL)) < 0) + goto done; + + if ((error = git_sparse_checkout__list(&existing_patterns, sparse)) < 0) + goto done; + + git_vector_foreach(&existing_patterns, i, pattern) { + git_vector_insert(&new_patterns, pattern); + } + + git_vector_foreach(patterns, i, pattern) { + git_vector_insert(&new_patterns, pattern); + } + + if ((error = git_sparse_checkout__set(&new_patterns, repo, sparse)) < 0) + goto done; + +done: + git_vector_free(&existing_patterns); + git_vector_free(&new_patterns); + + return error; +} + +int git_sparse_checkout_add( + git_repository *repo, + git_strarray *patterns) +{ + int error; + int is_enabled = false; + git_config *cfg; + git_sparse sparse; + git_vector patternlist; + size_t i; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(patterns); + + if ((error = git_repository_config__weakptr(&cfg, repo)) < 0) + return error; + + error = git_config_get_bool(&is_enabled, cfg, "core.sparseCheckout"); + if (error < 0 && error != GIT_ENOTFOUND) + goto done; + + if (!is_enabled) + { + git_error_set(GIT_ERROR_INVALID, "sparse checkout is not enabled"); + git_config_free(cfg); + return -1; + } + + if ((error = git_sparse__init(repo, &sparse)) < 0) + goto done; + + if ((error = git_vector_init(&patternlist, 0, NULL))) + goto done; + + for (i = 0; i < patterns->count; i++) { + if ((error = git_vector_insert(&patternlist, patterns->strings[i])) < 0) + return error; + } + + if ((error = git_sparse_checkout__add(repo, &patternlist, &sparse)) < 0) + goto done; + + if ((error = git_sparse_checkout__reapply(repo, &sparse)) < 0) + goto done; + +done: + git_config_free(cfg); + git_sparse__free(&sparse); + git_vector_free(&patternlist); + + return error; +} + +int git_sparse_checkout_reapply(git_repository *repo) { + int error; + git_sparse sparse; + + GIT_ASSERT_ARG(repo); + + if ((error = git_sparse__init(repo, &sparse)) < 0) + return error; + + if ((error = git_sparse_checkout__reapply(repo, &sparse)) < 0) + goto done; + +done: + git_sparse__free(&sparse); + return error; +} + +int git_sparse_check_path( + int *checkout, + git_repository *repo, + const char *pathname) +{ + int error; + int sparse_checkout_enabled = false; + git_sparse sparse; + git_dir_flag dir_flag = GIT_DIR_FLAG_FALSE; + + GIT_ASSERT_ARG(repo); + GIT_ASSERT_ARG(checkout); + GIT_ASSERT_ARG(pathname); + + *checkout = GIT_SPARSE_CHECKOUT; + + if ((error = git_repository__configmap_lookup(&sparse_checkout_enabled, repo, GIT_CONFIGMAP_SPARSECHECKOUT)) < 0 || + sparse_checkout_enabled == false) + return 0; + + if ((error = git_sparse__init(repo, &sparse)) < 0) + goto cleanup; + + if (!git__suffixcmp(pathname, "/")) + dir_flag = GIT_DIR_FLAG_TRUE; + else if (git_repository_is_bare(repo)) + dir_flag = GIT_DIR_FLAG_FALSE; + + error = git_sparse__lookup(checkout, &sparse, pathname, dir_flag); + + cleanup: + git_sparse__free(&sparse); + return error; +} diff --git a/src/libgit2/sparse.h b/src/libgit2/sparse.h new file mode 100644 index 00000000000..6d7ef556448 --- /dev/null +++ b/src/libgit2/sparse.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) the libgit2 contributors. All rights reserved. + * + * This file is part of libgit2, distributed under the GNU GPL v2 with + * a Linking Exception. For full terms see the included COPYING file. + */ +#ifndef INCLUDE_sparse_h__ +#define INCLUDE_sparse_h__ + +#include "common.h" + +#include "repository.h" +#include "attr_file.h" + +#define GIT_SPARSE_CHECKOUT_FILE "sparse-checkout" +typedef struct { + git_repository *repo; + git_attr_file *sparse; + int ignore_case; +} git_sparse; + +typedef enum { + GIT_SPARSE_UNCHECKED = -2, + GIT_SPARSE_NOTFOUND = -1, + GIT_SPARSE_NOCHECKOUT = 0, + GIT_SPARSE_CHECKOUT = 1, +} git_sparse_status; + +extern int git_sparse__init(git_repository *repo, git_sparse *ign); + +extern void git_sparse__free(git_sparse *sparse); + +extern int git_sparse__lookup( + git_sparse_status* checkout, + git_sparse *sparse, + const char* pathname, + git_dir_flag dir_flag); + +#endif diff --git a/tests/libgit2/sparse/add.c b/tests/libgit2/sparse/add.c new file mode 100644 index 00000000000..5534c482a45 --- /dev/null +++ b/tests/libgit2/sparse/add.c @@ -0,0 +1,54 @@ +#include "clar_libgit2.h" +#include "sparse.h" +#include "git2/sparse.h" +#include "util.h" + +static git_repository *g_repo = NULL; + +void test_sparse_add__initialize(void) +{ +} + +void test_sparse_add__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_add__appends_to_patterns(void) +{ + size_t i = 0; + git_strarray found_patterns = { 0 }; + + char *expected_pattern_strings[] = { "/*", "!/*/", "/a/" }; + git_strarray expected_patterns = { expected_pattern_strings, ARRAY_SIZE(expected_pattern_strings) }; + + char *pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + + cl_git_pass(git_sparse_checkout_list(&found_patterns, g_repo)); + for (i = 0; i < found_patterns.count; i++) { + cl_assert_equal_s(found_patterns.strings[i], expected_patterns.strings[i]); + } +} + +void test_sparse_add__applies_sparsity(void) +{ + char *pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} diff --git a/tests/libgit2/sparse/checkout.c b/tests/libgit2/sparse/checkout.c new file mode 100644 index 00000000000..e20c32b962f --- /dev/null +++ b/tests/libgit2/sparse/checkout.c @@ -0,0 +1,120 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "sparse.h" +#include "git2/checkout.h" +#include "commit.h" + +static git_repository *g_repo = NULL; + +void test_sparse_checkout__initialize(void) +{ +} + +void test_sparse_checkout__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void checkout_first_commit(void) +{ + git_object *obj; + const char *commit_sha = "35e0dddab1fda55a937272c72c941e1877a47300"; + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + cl_git_pass(git_revparse_single(&obj, g_repo, commit_sha)); + + opts.checkout_strategy = GIT_CHECKOUT_FORCE; + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head_detached(g_repo, git_object_id(obj))); + + git_object_free(obj); +} + +void checkout_head(void) +{ + git_object *obj; + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + + cl_git_pass(git_revparse_single(&obj, g_repo, "main")); + cl_git_pass(git_checkout_tree(g_repo, obj, &opts)); + cl_git_pass(git_repository_set_head(g_repo, "refs/heads/main")); + + git_object_free(obj); +} + +void test_sparse_checkout__skips_sparse_files(void) +{ + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + checkout_first_commit(); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + checkout_head(); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/file5"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/c/file7"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/d/file9"), false); +} + +void test_sparse_checkout__checksout_files(void) +{ + char* pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + checkout_first_commit(); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + + checkout_head(); + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} + +void test_sparse_checkout__checksout_all_files(void) +{ + char *pattern_strings[] = { "/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + g_repo = cl_git_sandbox_init("sparse"); + + checkout_first_commit(); + + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + + checkout_head(); + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/file5"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/c/file7"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/d/file9"), true); +} + +void test_sparse_checkout__updates_index(void) +{ + char *pattern_strings[] = { "/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + git_index_iterator* iterator; + git_index* index; + const git_index_entry *entry; + g_repo = cl_git_sandbox_init("sparse"); + + checkout_first_commit(); + + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + + checkout_head(); + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_index_iterator_new(&iterator, index)); + while (git_index_iterator_next(&entry, iterator) != GIT_ITEROVER) + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_index_iterator_free(iterator); + git_index_free(index); +} \ No newline at end of file diff --git a/tests/libgit2/sparse/disable.c b/tests/libgit2/sparse/disable.c new file mode 100644 index 00000000000..31b4fa1e9ce --- /dev/null +++ b/tests/libgit2/sparse/disable.c @@ -0,0 +1,69 @@ + +#include "path.h" +#include +#include "futils.h" + +static git_repository *g_repo = NULL; + +void test_sparse_disable__initialize(void) +{ +} + +void test_sparse_disable__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_disable__disables_sparse_checkout(void) +{ + git_config *config; + int b; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + cl_git_pass(git_repository_config(&config, g_repo)); + cl_git_pass(git_config_get_bool(&b, config, "core.sparseCheckout")); + cl_assert_equal_b(b, false); + + git_config_free(config); +} + +void test_sparse_disable__leaves_sparse_checkout_file_intact(void) +{ + const char *path; + + git_str before_content = GIT_STR_INIT; + git_str after_content = GIT_STR_INIT; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + path = "sparse/.git/info/sparse-checkout"; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_futils_readbuffer(&before_content, path)); + + cl_git_pass(git_sparse_checkout_disable(g_repo)); + cl_git_pass(git_futils_readbuffer(&after_content, path)); + + cl_assert_equal_b(git_fs_path_exists(path), true); + cl_assert_equal_s_(git_str_cstr(&before_content), git_str_cstr(&after_content), "git_sparse_checkout_disable should not modify or remove the sparse-checkout file"); +} + +void test_sparse_disable__restores_working_directory(void) +{ + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/file5"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/c/file7"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/b/d/file9"), true); +} \ No newline at end of file diff --git a/tests/libgit2/sparse/index.c b/tests/libgit2/sparse/index.c new file mode 100644 index 00000000000..05b0d8bf86b --- /dev/null +++ b/tests/libgit2/sparse/index.c @@ -0,0 +1,235 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "sparse.h" +#include "index.h" + +static git_repository *g_repo = NULL; + +void test_sparse_index__initialize(void) +{ +} + +void test_sparse_index__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_index__add_bypath(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_git_mkfile("sparse/newfile", "/hello world\n"); + cl_git_pass(git_index_add_bypath(index, "newfile")); + cl_assert(entry = git_index_get_bypath(index, "newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_index_free(index); +} + +void test_sparse_index__add_bypath_sparse(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/newfile", "/hello world\n"); + cl_git_pass(git_index_add_bypath(index, "a/newfile")); + cl_assert(entry = git_index_get_bypath(index, "a/newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, GIT_INDEX_ENTRY_SKIP_WORKTREE); + + git_index_free(index); +} + +void test_sparse_index__add_bypath_disabled_sparse(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/newfile", "/hello world\n"); + cl_git_pass(git_index_add_bypath(index, "a/newfile")); + cl_assert(entry = git_index_get_bypath(index, "a/newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_index_free(index); +} + +void test_sparse_index__add_all(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_git_mkfile("sparse/newfile", "/hello world\n"); + cl_git_pass(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL)); + cl_assert(entry = git_index_get_bypath(index, "newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_index_free(index); +} + +void test_sparse_index__add_all_sparse(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/newfile", "/hello world\n"); + cl_git_pass(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL)); + cl_assert(entry = git_index_get_bypath(index, "a/newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, GIT_INDEX_ENTRY_SKIP_WORKTREE); + + git_index_free(index); +} + +void test_sparse_index__add_all_disabled_sparse(void) +{ + git_index* index; + const git_index_entry* entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + cl_git_pass(git_repository_index(&index, g_repo)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/newfile", "/hello world\n"); + cl_git_pass(git_index_add_all(index, NULL, GIT_INDEX_ADD_DEFAULT, NULL, NULL)); + cl_assert(entry = git_index_get_bypath(index, "a/newfile", 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_index_free(index); +} + +void test_sparse_index__read_tree_sets_skip_worktree(void) +{ + git_index* index; + git_tree* tree; + git_oid tree_id; + const git_index_entry* entry; + const char** test_file; + const char *test_files[] = { + "a/file3", + "a/file4", + NULL + }; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + git_oid_fromstr(&tree_id, "466cd582210eceaec48d949c7adaa0ceb2042db6"); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id)); + + cl_git_pass(git_index_read_tree(index, tree)); + + for (test_file = test_files; *test_file != NULL; ++test_file) { + cl_assert(entry = git_index_get_bypath(index, *test_file, 0)); + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + } + + git_tree_free(tree); + git_index_free(index); +} + +void test_sparse_index__read_tree_sets_skip_worktree_disabled(void) +{ + git_index* index; + git_tree* tree; + git_oid tree_id; + git_index_iterator* iterator; + const git_index_entry *entry; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + git_oid_fromstr(&tree_id, "466cd582210eceaec48d949c7adaa0ceb2042db6"); + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id)); + + cl_git_pass(git_index_read_tree(index, tree)); + + cl_git_pass(git_index_iterator_new(&iterator, index)); + while (git_index_iterator_next(&entry, iterator) != GIT_ITEROVER) + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, 0); + + git_tree_free(tree); + git_index_iterator_free(iterator); + git_index_free(index); +} + +void test_sparse_index__read_tree_sets_skip_worktree_all_sparse(void) +{ + git_index* index; + git_tree* tree; + git_oid tree_id; + git_index_iterator* iterator; + const git_index_entry *entry; + + g_repo = cl_git_sandbox_init("sparse"); + git_oid_fromstr(&tree_id, "466cd582210eceaec48d949c7adaa0ceb2042db6"); + + { + char *pattern_strings[] = { "!/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + } + + cl_git_pass(git_repository_index(&index, g_repo)); + cl_git_pass(git_tree_lookup(&tree, g_repo, &tree_id)); + + cl_git_pass(git_index_read_tree(index, tree)); + + cl_git_pass(git_index_iterator_new(&iterator, index)); + while (git_index_iterator_next(&entry, iterator) != GIT_ITEROVER) + cl_assert_equal_i(entry->flags_extended & GIT_INDEX_ENTRY_SKIP_WORKTREE, GIT_INDEX_ENTRY_SKIP_WORKTREE); + + git_tree_free(tree); + git_index_iterator_free(iterator); + git_index_free(index); +} diff --git a/tests/libgit2/sparse/init.c b/tests/libgit2/sparse/init.c new file mode 100644 index 00000000000..5233a3a7a38 --- /dev/null +++ b/tests/libgit2/sparse/init.c @@ -0,0 +1,108 @@ +#include "clar_libgit2.h" +#include "sparse.h" +#include "git2/sparse.h" + +static git_repository *g_repo = NULL; + +void test_sparse_init__initialize(void) +{ +} + +void test_sparse_init__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_init__enables_sparse_checkout(void) +{ + git_config *config; + int b; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + + cl_git_pass(git_repository_config(&config, g_repo)); + cl_git_pass(git_config_get_bool(&b, config, "core.sparseCheckout")); + cl_assert_(b, "sparse checkout should be enabled"); + + git_config_free(config); +} + +void test_sparse_init__writes_sparse_checkout_file(void) +{ + const char *path; + git_str content = GIT_STR_INIT; + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + path = "sparse/.git/info/sparse-checkout"; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + cl_assert_equal_b(git_fs_path_exists(path), true); + + cl_git_pass(git_futils_readbuffer(&content, path)); + cl_assert_(strlen(git_str_cstr(&content)) > 1,"git_sparse_checkout_init should not init an empty file"); +} + +void test_sparse_init__sets_default_patterns(void) +{ + size_t i = 0; + char *default_pattern_strings[] = { "/*", "!/*/" }; + git_strarray default_patterns = { default_pattern_strings, ARRAY_SIZE(default_pattern_strings) }; + git_strarray found_patterns = { 0 }; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + + cl_git_pass(git_sparse_checkout_list(&found_patterns, g_repo)); + for (i = 0; i < found_patterns.count; i++) { + cl_assert_equal_s(found_patterns.strings[i], default_patterns.strings[i]); + } +} + +void test_sparse_init__does_not_overwrite_existing_file(void) +{ + size_t i = 0; + char *initial_pattern_strings[] = { "foo", "bar", "biz", "baz" }; + git_strarray initial_patterns = { initial_pattern_strings, ARRAY_SIZE(initial_pattern_strings) }; + git_strarray found_patterns = { 0 }; + + git_sparse_checkout_init_options opts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_set(g_repo, &initial_patterns)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + cl_git_pass(git_sparse_checkout_init(g_repo, &opts)); + + cl_git_pass(git_sparse_checkout_list(&found_patterns, g_repo)); + for (i = 0; i < found_patterns.count; i++) { + cl_assert_equal_s(found_patterns.strings[i], initial_patterns.strings[i]); + } +} + +void test_sparse_init__applies_sparsity(void) +{ + git_object* object; + git_checkout_options opts = GIT_CHECKOUT_OPTIONS_INIT; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_revparse_single(&object, g_repo, "HEAD")); + cl_git_pass(git_checkout_tree(g_repo, object, &opts)); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/file5"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/c/file7"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/b/d/file9"), false); +} \ No newline at end of file diff --git a/tests/libgit2/sparse/list.c b/tests/libgit2/sparse/list.c new file mode 100644 index 00000000000..a9877e1e48b --- /dev/null +++ b/tests/libgit2/sparse/list.c @@ -0,0 +1,30 @@ +#include + +static git_repository *g_repo = NULL; + +void test_sparse_list__initialize(void) +{ +} + +void test_sparse_list__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_list__lists_all_patterns(void) +{ + git_strarray patterns = {0}; + size_t i = 0; + + char *default_pattern__strings[] = { "/*", "!/*/" }; + git_strarray default_patterns = {default_pattern__strings, ARRAY_SIZE(default_pattern__strings) }; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_sparse_checkout_list(&patterns, g_repo)); + for (i = 0; i < patterns.count; i++) { + cl_assert_equal_s(patterns.strings[i], default_patterns.strings[i]); + } +} diff --git a/tests/libgit2/sparse/reapply.c b/tests/libgit2/sparse/reapply.c new file mode 100644 index 00000000000..32d5ebde773 --- /dev/null +++ b/tests/libgit2/sparse/reapply.c @@ -0,0 +1,81 @@ + +#include +#include "path.h" + +static git_repository *g_repo = NULL; + +void test_sparse_reapply__initialize(void) +{ +} + +void test_sparse_reapply__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void rewrite_sparse_checkout_file(void) +{ + /* Manually updating the sparse-checkout file, so we don't trigger a re-apply */ + const char *path = "sparse/.git/info/sparse-checkout"; + cl_git_rewritefile(path, "/a/"); +} + +void test_sparse_reapply__updates_working_directory(void) +{ + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), false); + + rewrite_sparse_checkout_file(); + cl_git_pass(git_sparse_checkout_reapply(g_repo)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} + +void test_sparse_reapply__leaves_modified_files_intact(void) +{ + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), false); + + /* Modify one of the checked out files */ + cl_git_rewritefile("sparse/file1", "what's up?"); + + rewrite_sparse_checkout_file(); + cl_git_pass(git_sparse_checkout_reapply(g_repo)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} + +void test_sparse_reapply__leaves_submodules_intact(void) +{ + git_submodule *sm; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_pass(git_submodule_add_setup(&sm, g_repo, "../TestGitRepository", "TestGitRepository", 1)); + git_submodule_free(sm); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/TestGitRepository/.git"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), false); + + rewrite_sparse_checkout_file(); + cl_git_pass(git_sparse_checkout_reapply(g_repo)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/TestGitRepository/.git"), true); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} diff --git a/tests/libgit2/sparse/set.c b/tests/libgit2/sparse/set.c new file mode 100644 index 00000000000..540dc8f2811 --- /dev/null +++ b/tests/libgit2/sparse/set.c @@ -0,0 +1,75 @@ +#include "clar_libgit2.h" +#include "sparse.h" +#include "git2/sparse.h" +#include "util.h" + +static git_repository *g_repo = NULL; + +void test_sparse_set__initialize(void) +{ +} + +void test_sparse_set__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_set__enables_sparse_checkout(void) +{ + const char *path; + + git_config *config; + int b; + + char *pattern_strings[] = { "/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + path = "sparse/.git/info/sparse-checkout"; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + + cl_git_pass(git_repository_config(&config, g_repo)); + cl_git_pass(git_config_get_bool(&b, config, "core.sparseCheckout")); + cl_assert_(b, "sparse checkout should be enabled"); + cl_assert_equal_b(git_fs_path_exists(path), true); + + git_config_free(config); +} + +void test_sparse_set__rewrites_sparse_checkout_file(void) +{ + const char *path; + git_str after_content = GIT_STR_INIT; + + char *initial_pattern_strings[] = { "foo", "bar", "biz", "baz" }; + git_strarray initial_patterns = { initial_pattern_strings, ARRAY_SIZE(initial_pattern_strings) }; + + char *after_pattern_strings[] = { "bar", "baz" }; + git_strarray after_patterns = { after_pattern_strings, ARRAY_SIZE(after_pattern_strings) }; + const char *expected_string = "bar\nbaz"; + + path = "sparse/.git/info/sparse-checkout"; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_set(g_repo, &initial_patterns)); + + cl_git_pass(git_sparse_checkout_set(g_repo, &after_patterns)); + cl_git_pass(git_futils_readbuffer(&after_content, path)); + + cl_assert_equal_s_(git_str_cstr(&after_content), expected_string, "git_sparse_checkout_set should overwrite existing patterns in the sparse-checkout file"); +} + +void test_sparse_set__applies_sparsity(void) +{ + char* pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + + cl_assert_equal_b(git_fs_path_exists("sparse/file1"), false); + cl_assert_equal_b(git_fs_path_exists("sparse/a/file3"), true); +} diff --git a/tests/libgit2/sparse/status.c b/tests/libgit2/sparse/status.c new file mode 100644 index 00000000000..7ceab19a2e4 --- /dev/null +++ b/tests/libgit2/sparse/status.c @@ -0,0 +1,408 @@ +#include "clar_libgit2.h" +#include "futils.h" +#include "git2/attr.h" +#include "sparse.h" +#include "status/status_helpers.h" + +static git_repository *g_repo = NULL; + +void test_sparse_status__initialize(void) +{ +} + +void test_sparse_status__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +static void assert_ckeckout_( + bool expected, const char *filepath, + const char *file, const char *func, int line) +{ + int checkout = 0; + cl_git_expect( + git_sparse_check_path(&checkout, g_repo, filepath), 0, file, func, line); + clar__assert( + (expected != 0) == (checkout != 0), + file, func, line, "expected != checkout", filepath, 1); +} + +#define assert_checkout(expected, filepath) \ +assert_ckeckout_(expected, filepath, __FILE__, __func__, __LINE__) +#define assert_is_checkout(filepath) \ +assert_ckeckout_(true, filepath, __FILE__, __func__, __LINE__) +#define refute_is_checkout(filepath) \ +assert_ckeckout_(false, filepath, __FILE__, __func__, __LINE__) + +#define define_test_cases \ +struct test_case{ \ + const char *path; \ + int expected; \ +} test_cases[] = { \ + /* include all pattern from info/sparse-checkout */ \ + { "file1", 1 }, \ + { "file2", 1 }, \ + { "file11.txt", 1 }, \ + \ + /* exclude subfolder pattern from info/sparse-checkout */ \ + { "a/", 0 }, \ + { "a/file3", 0 }, \ + { "a/file4", 0 }, \ + \ + { "b/", 0 }, \ + { "b/file12.txt", 0 }, \ + { "b/file5", 0 }, \ + { "b/file6", 0 }, \ + \ + { "b/c/", 0 }, \ + { "b/c/file7", 0 }, \ + { "b/c/file8", 0 }, \ + \ + { "b/d/", 0 }, \ + { "b/d/file10", 0 }, \ + { "b/d/file9", 0 }, \ + \ + { NULL, 0 } \ +}, *one_test; \ + +void test_sparse_status__0(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + git_attr_cache_flush(g_repo); + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); + + /* confirm that sparse-checkout file is cached */ + cl_assert(git_attr_cache__is_cached( + g_repo, GIT_ATTR_FILE_SOURCE_FILE, ".git/info/sparse-checkout")); +} + +static const char* paths[] = { + "file1", + "file2", + "file11.txt", + "a/", + "a/file3", + "a/file4", + "b/", + "b/file12.txt", + "b/file5", + "b/file6", + "b/c/", + "b/c/file7", + "b/c/file8", + "b/d/", + "b/d/file10", + "b/d/file9", + NULL +}; + +void test_sparse_status__disabled(void) +{ + const char** path; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_pass(git_sparse_checkout_disable(g_repo)); + + for (path = paths; *path != NULL; path++) + assert_is_checkout(*path); +} + +void test_sparse_status__full_checkout(void) +{ + const char** path; + g_repo = cl_git_sandbox_init("sparse"); + { + char *pattern_strings[] = { "/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + } + + for (path = paths; *path != NULL; path++) + assert_is_checkout(*path); +} + +void test_sparse_status__no_checkout(void) +{ + const char** path; + g_repo = cl_git_sandbox_init("sparse"); + { + char *pattern_strings[] = { "!/*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_set(g_repo, &patterns)); + } + + for (path = paths; *path != NULL; path++) + refute_is_checkout(*path); +} + +void test_sparse_status__no_sparse_file(void) +{ + const char** path; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + cl_git_rmfile("sparse/.git/info/sparse-checkout"); + + for (path = paths; *path != NULL; path++) + refute_is_checkout(*path); +} + +void test_sparse_status__append_folder(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "/a/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[3].expected = 1; + test_cases[4].expected = 1; + test_cases[5].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__ignore_subfolders(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "/b/", "!/b/*/" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[6].expected = 1; + test_cases[7].expected = 1; + test_cases[8].expected = 1; + test_cases[9].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__append_file(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "/b/c/file7" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[11].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__append_suffix(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "*.txt" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[7].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__exclude_single_file_suffix(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "*.txt", "!file11.txt" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[2].expected = 0; + test_cases[7].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__match_wildcard(void) +{ + define_test_cases + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "file1*" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + test_cases[7].expected = 1; + test_cases[14].expected = 1; + + for (one_test = test_cases; one_test->path != NULL; one_test++) + assert_checkout(one_test->expected, one_test->path); +} + +void test_sparse_status__clean(void) +{ + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(0, st.count); +} + +void test_sparse_status__clean_unmodified(void) +{ + git_status_options opts = GIT_STATUS_OPTIONS_INIT; + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + memset(&st, 0, sizeof(st)); + + opts.flags = GIT_STATUS_OPT_DEFAULTS | GIT_STATUS_OPT_INCLUDE_UNMODIFIED; + cl_git_pass(git_status_foreach_ext(g_repo, &opts, cb_status__single, &st)); + cl_assert_equal_i(12, st.count); + cl_assert(st.status == GIT_STATUS_CURRENT); +} + +void test_sparse_status__new_file(void) +{ + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_git_mkfile("sparse/newfile", "/hello world\n"); + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + assert_is_checkout("newfile"); +} + +void test_sparse_status__new_file_new_folder(void) +{ + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_must_pass(git_futils_mkdir("sparse/new", 0777, 0)); + cl_git_mkfile("sparse/new/newfile", "/hello world\n"); + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + refute_is_checkout("new/newfile"); +} + +void test_sparse_status__new_file_sparse_folder(void) +{ + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/newfile", "/hello world\n"); + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(1, st.count); + cl_assert(st.status == GIT_STATUS_WT_NEW); + + refute_is_checkout("new/newfile"); +} + +void test_sparse_status__new_sparse_file_sparse_folder(void) +{ + status_entry_single st; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + + cl_must_pass(git_futils_mkdir("sparse/a", 0777, 0)); + cl_git_mkfile("sparse/a/file3", "/hello world\n"); + memset(&st, 0, sizeof(st)); + cl_git_pass(git_status_foreach(g_repo, cb_status__single, &st)); + cl_assert_equal_i(0, st.count); + + refute_is_checkout("new/newfile"); +} + +void test_sparse_status__ignorecase(void) +{ + bool ignore_case; + git_index *index; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_sparse_checkout_init(g_repo, &scopts)); + { + char *pattern_strings[] = { "/b/file5" }; + git_strarray patterns = { pattern_strings, ARRAY_SIZE(pattern_strings) }; + cl_git_pass(git_sparse_checkout_add(g_repo, &patterns)); + } + + cl_must_pass(git_futils_mkdir("sparse/b", 0777, 0)); + cl_git_mkfile("sparse/b/File5", "/hello world\n"); + + cl_git_pass(git_repository_index(&index, g_repo)); + ignore_case = (git_index_caps(index) & GIT_INDEX_CAPABILITY_IGNORE_CASE) != 0; + git_index_free(index); + + if (ignore_case) + assert_is_checkout("b/File5"); + else + refute_is_checkout("b/File5"); + + git_index_free(index); +} diff --git a/tests/libgit2/sparse/worktree.c b/tests/libgit2/sparse/worktree.c new file mode 100644 index 00000000000..4b053ed81f6 --- /dev/null +++ b/tests/libgit2/sparse/worktree.c @@ -0,0 +1,103 @@ + +#include "repository.h" +#include "clar_libgit2.h" + +static git_repository *g_repo = NULL; + +void test_sparse_worktree__initialize(void) +{ +} + +void test_sparse_worktree__cleanup(void) +{ + cl_git_sandbox_cleanup(); +} + +void test_sparse_worktree__writes_sparse_checkout_file(void) +{ + const char *path; + git_str content = GIT_STR_INIT; + + git_str wtpath = GIT_STR_INIT; + git_worktree *wt; + git_worktree_add_options opts = GIT_WORKTREE_ADD_OPTIONS_INIT; + + git_repository *wt_repo; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + path = "sparse/.git/worktrees/sparse-worktree-foo/info/sparse-checkout"; + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_str_joinpath(&wtpath, g_repo->workdir, "../sparse-worktree-foo")); + cl_git_pass(git_worktree_add(&wt, g_repo, "sparse-worktree-foo", wtpath.ptr, &opts)); + cl_git_pass(git_repository_open(&wt_repo, wtpath.ptr)); + + cl_git_pass(git_sparse_checkout_init(wt_repo, &scopts)); + cl_assert_equal_b(git_fs_path_exists(path), true); + + cl_git_pass(git_futils_readbuffer(&content, path)); + cl_assert_(strlen(git_str_cstr(&content)) > 1,"git_sparse_checkout_init should not init an empty file"); +} + +void test_sparse_worktree__honours_sparsity(void) +{ + git_str path = GIT_STR_INIT; + git_worktree *wt; + git_worktree_add_options opts = GIT_WORKTREE_ADD_OPTIONS_INIT; + + git_repository *wt_repo; + git_sparse_checkout_init_options scopts = GIT_SPARSE_CHECKOUT_INIT_OPTIONS_INIT; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_str_joinpath(&path, g_repo->workdir, "../sparse-worktree-bar")); + cl_git_pass(git_worktree_add(&wt, g_repo, "sparse-worktree-bar", path.ptr, &opts)); + cl_git_pass(git_repository_open(&wt_repo, path.ptr)); + + cl_git_pass(git_sparse_checkout_init(wt_repo, &scopts)); + + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-bar/file1"), true); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-bar/a/file3"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-bar/b/file5"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-bar/b/c/file7"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-bar/b/d/file9"), false); +} + +void test_sparse_worktree__honours_sparsity_on_different_worktrees(void) +{ + git_str path1 = GIT_STR_INIT; + git_worktree *wt1; + git_repository *wt_repo1; + git_worktree_add_options opts = GIT_WORKTREE_ADD_OPTIONS_INIT; + + char* pattern_strings1[] = { "/a/" }; + git_strarray patterns1 = { pattern_strings1, ARRAY_SIZE(pattern_strings1) }; + + git_str path2 = GIT_STR_INIT; + git_worktree *wt2; + git_repository *wt_repo2; + + char* pattern_strings2[] = { "/b/" }; + git_strarray patterns2 = { pattern_strings2, ARRAY_SIZE(pattern_strings2) }; + + g_repo = cl_git_sandbox_init("sparse"); + + cl_git_pass(git_str_joinpath(&path1, g_repo->workdir, "../sparse-worktree-1")); + cl_git_pass(git_worktree_add(&wt1, g_repo, "sparse-worktree-1", path1.ptr, &opts)); + cl_git_pass(git_repository_open(&wt_repo1, path1.ptr)); + + cl_git_pass(git_str_joinpath(&path2, g_repo->workdir, "../sparse-worktree-2")); + cl_git_pass(git_worktree_add(&wt2, g_repo, "sparse-worktree-2", path2.ptr, &opts)); + cl_git_pass(git_repository_open(&wt_repo2, path2.ptr)); + + cl_git_pass(git_sparse_checkout_set(wt_repo1, &patterns1)); + cl_git_pass(git_sparse_checkout_set(wt_repo2, &patterns2)); + + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-1/file1"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-1/a/file3"), true); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-1/b/file5"), false); + + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-2/file1"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-2/a/file3"), false); + cl_assert_equal_b(git_fs_path_exists("sparse-worktree-2/b/file5"), true); +} diff --git a/tests/resources/sparse/.gitted/COMMIT_EDITMSG b/tests/resources/sparse/.gitted/COMMIT_EDITMSG new file mode 100644 index 00000000000..bb945e228ea --- /dev/null +++ b/tests/resources/sparse/.gitted/COMMIT_EDITMSG @@ -0,0 +1 @@ +sixth commit diff --git a/tests/resources/sparse/.gitted/HEAD b/tests/resources/sparse/.gitted/HEAD new file mode 100644 index 00000000000..b870d82622c --- /dev/null +++ b/tests/resources/sparse/.gitted/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/tests/resources/sparse/.gitted/ORIG_HEAD b/tests/resources/sparse/.gitted/ORIG_HEAD new file mode 100644 index 00000000000..b46a587c810 --- /dev/null +++ b/tests/resources/sparse/.gitted/ORIG_HEAD @@ -0,0 +1 @@ +9bf6a60c1a7c55f9a3067de627f173c38a030c73 diff --git a/tests/resources/sparse/.gitted/config b/tests/resources/sparse/.gitted/config new file mode 100644 index 00000000000..6c9406b7d93 --- /dev/null +++ b/tests/resources/sparse/.gitted/config @@ -0,0 +1,7 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = false + logallrefupdates = true + ignorecase = true + precomposeunicode = true diff --git a/tests/resources/sparse/.gitted/description b/tests/resources/sparse/.gitted/description new file mode 100644 index 00000000000..498b267a8c7 --- /dev/null +++ b/tests/resources/sparse/.gitted/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/resources/sparse/.gitted/hooks/applypatch-msg.sample b/tests/resources/sparse/.gitted/hooks/applypatch-msg.sample new file mode 100755 index 00000000000..a5d7b84a673 --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/applypatch-msg.sample @@ -0,0 +1,15 @@ +#!/bin/sh +# +# An example hook script to check the commit log message taken by +# applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. The hook is +# allowed to edit the commit message file. +# +# To enable this hook, rename this file to "applypatch-msg". + +. git-sh-setup +commitmsg="$(git rev-parse --git-path hooks/commit-msg)" +test -x "$commitmsg" && exec "$commitmsg" ${1+"$@"} +: diff --git a/tests/resources/sparse/.gitted/hooks/commit-msg.sample b/tests/resources/sparse/.gitted/hooks/commit-msg.sample new file mode 100755 index 00000000000..b58d1184a9d --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/commit-msg.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to check the commit log message. +# Called by "git commit" with one argument, the name of the file +# that has the commit message. The hook should exit with non-zero +# status after issuing an appropriate message if it wants to stop the +# commit. The hook is allowed to edit the commit message file. +# +# To enable this hook, rename this file to "commit-msg". + +# Uncomment the below to add a Signed-off-by line to the message. +# Doing this in a hook is a bad idea in general, but the prepare-commit-msg +# hook is more suited to it. +# +# SOB=$(git var GIT_AUTHOR_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1" + +# This example catches duplicate Signed-off-by lines. + +test "" = "$(grep '^Signed-off-by: ' "$1" | + sort | uniq -c | sed -e '/^[ ]*1[ ]/d')" || { + echo >&2 Duplicate Signed-off-by lines. + exit 1 +} diff --git a/tests/resources/sparse/.gitted/hooks/fsmonitor-watchman.sample b/tests/resources/sparse/.gitted/hooks/fsmonitor-watchman.sample new file mode 100755 index 00000000000..14ed0aa42de --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/fsmonitor-watchman.sample @@ -0,0 +1,173 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use IPC::Open2; + +# An example hook script to integrate Watchman +# (https://facebook.github.io/watchman/) with git to speed up detecting +# new and modified files. +# +# The hook is passed a version (currently 2) and last update token +# formatted as a string and outputs to stdout a new update token and +# all files that have been modified since the update token. Paths must +# be relative to the root of the working tree and separated by a single NUL. +# +# To enable this hook, rename this file to "query-watchman" and set +# 'git config core.fsmonitor .git/hooks/query-watchman' +# +my ($version, $last_update_token) = @ARGV; + +# Uncomment for debugging +# print STDERR "$0 $version $last_update_token\n"; + +# Check the hook interface version +if ($version ne 2) { + die "Unsupported query-fsmonitor hook version '$version'.\n" . + "Falling back to scanning...\n"; +} + +my $git_work_tree = get_working_dir(); + +my $retry = 1; + +my $json_pkg; +eval { + require JSON::XS; + $json_pkg = "JSON::XS"; + 1; +} or do { + require JSON::PP; + $json_pkg = "JSON::PP"; +}; + +launch_watchman(); + +sub launch_watchman { + my $o = watchman_query(); + if (is_work_tree_watched($o)) { + output_result($o->{clock}, @{$o->{files}}); + } +} + +sub output_result { + my ($clockid, @files) = @_; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # binmode $fh, ":utf8"; + # print $fh "$clockid\n@files\n"; + # close $fh; + + binmode STDOUT, ":utf8"; + print $clockid; + print "\0"; + local $, = "\0"; + print @files; +} + +sub watchman_clock { + my $response = qx/watchman clock "$git_work_tree"/; + die "Failed to get clock id on '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + + return $json_pkg->new->utf8->decode($response); +} + +sub watchman_query { + my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'watchman -j --no-pretty') + or die "open2() failed: $!\n" . + "Falling back to scanning...\n"; + + # In the query expression below we're asking for names of files that + # changed since $last_update_token but not from the .git folder. + # + # To accomplish this, we're using the "since" generator to use the + # recency index to select candidate nodes and "fields" to limit the + # output to file names only. Then we're using the "expression" term to + # further constrain the results. + if (substr($last_update_token, 0, 1) eq "c") { + $last_update_token = "\"$last_update_token\""; + } + my $query = <<" END"; + ["query", "$git_work_tree", { + "since": $last_update_token, + "fields": ["name"], + "expression": ["not", ["dirname", ".git"]] + }] + END + + # Uncomment for debugging the watchman query + # open (my $fh, ">", ".git/watchman-query.json"); + # print $fh $query; + # close $fh; + + print CHLD_IN $query; + close CHLD_IN; + my $response = do {local $/; }; + + # Uncomment for debugging the watch response + # open ($fh, ">", ".git/watchman-response.json"); + # print $fh $response; + # close $fh; + + die "Watchman: command returned no output.\n" . + "Falling back to scanning...\n" if $response eq ""; + die "Watchman: command returned invalid output: $response\n" . + "Falling back to scanning...\n" unless $response =~ /^\{/; + + return $json_pkg->new->utf8->decode($response); +} + +sub is_work_tree_watched { + my ($output) = @_; + my $error = $output->{error}; + if ($retry > 0 and $error and $error =~ m/unable to resolve root .* directory (.*) is not watched/) { + $retry--; + my $response = qx/watchman watch "$git_work_tree"/; + die "Failed to make watchman watch '$git_work_tree'.\n" . + "Falling back to scanning...\n" if $? != 0; + $output = $json_pkg->new->utf8->decode($response); + $error = $output->{error}; + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + # Uncomment for debugging watchman output + # open (my $fh, ">", ".git/watchman-output.out"); + # close $fh; + + # Watchman will always return all files on the first query so + # return the fast "everything is dirty" flag to git and do the + # Watchman query just to get it over with now so we won't pay + # the cost in git to look up each individual file. + my $o = watchman_clock(); + $error = $output->{error}; + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + output_result($o->{clock}, ("/")); + $last_update_token = $o->{clock}; + + eval { launch_watchman() }; + return 0; + } + + die "Watchman: $error.\n" . + "Falling back to scanning...\n" if $error; + + return 1; +} + +sub get_working_dir { + my $working_dir; + if ($^O =~ 'msys' || $^O =~ 'cygwin') { + $working_dir = Win32::GetCwd(); + $working_dir =~ tr/\\/\//; + } else { + require Cwd; + $working_dir = Cwd::cwd(); + } + + return $working_dir; +} diff --git a/tests/resources/sparse/.gitted/hooks/post-update.sample b/tests/resources/sparse/.gitted/hooks/post-update.sample new file mode 100755 index 00000000000..ec17ec1939b --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/post-update.sample @@ -0,0 +1,8 @@ +#!/bin/sh +# +# An example hook script to prepare a packed repository for use over +# dumb transports. +# +# To enable this hook, rename this file to "post-update". + +exec git update-server-info diff --git a/tests/resources/sparse/.gitted/hooks/pre-applypatch.sample b/tests/resources/sparse/.gitted/hooks/pre-applypatch.sample new file mode 100755 index 00000000000..4142082bcb9 --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-applypatch.sample @@ -0,0 +1,14 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed +# by applypatch from an e-mail message. +# +# The hook should exit with non-zero status after issuing an +# appropriate message if it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-applypatch". + +. git-sh-setup +precommit="$(git rev-parse --git-path hooks/pre-commit)" +test -x "$precommit" && exec "$precommit" ${1+"$@"} +: diff --git a/tests/resources/sparse/.gitted/hooks/pre-commit.sample b/tests/resources/sparse/.gitted/hooks/pre-commit.sample new file mode 100755 index 00000000000..e144712c85c --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-commit.sample @@ -0,0 +1,49 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git commit" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message if +# it wants to stop the commit. +# +# To enable this hook, rename this file to "pre-commit". + +if git rev-parse --verify HEAD >/dev/null 2>&1 +then + against=HEAD +else + # Initial commit: diff against an empty tree object + against=$(git hash-object -t tree /dev/null) +fi + +# If you want to allow non-ASCII filenames set this variable to true. +allownonascii=$(git config --type=bool hooks.allownonascii) + +# Redirect output to stderr. +exec 1>&2 + +# Cross platform projects tend to avoid non-ASCII filenames; prevent +# them from being added to the repository. We exploit the fact that the +# printable range starts at the space character and ends with tilde. +if [ "$allownonascii" != "true" ] && + # Note that the use of brackets around a tr range is ok here, (it's + # even required, for portability to Solaris 10's /usr/bin/tr), since + # the square bracket bytes happen to fall in the designated range. + test $(git diff --cached --name-only --diff-filter=A -z $against | + LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 +then + cat <<\EOF +Error: Attempt to add a non-ASCII file name. + +This can cause problems if you want to work with people on other platforms. + +To be portable it is advisable to rename the file. + +If you know what you are doing you can disable this check using: + + git config hooks.allownonascii true +EOF + exit 1 +fi + +# If there are whitespace errors, print the offending file names and fail. +exec git diff-index --check --cached $against -- diff --git a/tests/resources/sparse/.gitted/hooks/pre-merge-commit.sample b/tests/resources/sparse/.gitted/hooks/pre-merge-commit.sample new file mode 100755 index 00000000000..399eab1924e --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-merge-commit.sample @@ -0,0 +1,13 @@ +#!/bin/sh +# +# An example hook script to verify what is about to be committed. +# Called by "git merge" with no arguments. The hook should +# exit with non-zero status after issuing an appropriate message to +# stderr if it wants to stop the merge commit. +# +# To enable this hook, rename this file to "pre-merge-commit". + +. git-sh-setup +test -x "$GIT_DIR/hooks/pre-commit" && + exec "$GIT_DIR/hooks/pre-commit" +: diff --git a/tests/resources/sparse/.gitted/hooks/pre-push.sample b/tests/resources/sparse/.gitted/hooks/pre-push.sample new file mode 100755 index 00000000000..4ce688d32b7 --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-push.sample @@ -0,0 +1,53 @@ +#!/bin/sh + +# An example hook script to verify what is about to be pushed. Called by "git +# push" after it has checked the remote status, but before anything has been +# pushed. If this script exits with a non-zero status nothing will be pushed. +# +# This hook is called with the following parameters: +# +# $1 -- Name of the remote to which the push is being done +# $2 -- URL to which the push is being done +# +# If pushing without using a named remote those arguments will be equal. +# +# Information about the commits which are being pushed is supplied as lines to +# the standard input in the form: +# +# +# +# This sample shows how to prevent push of commits where the log message starts +# with "WIP" (work in progress). + +remote="$1" +url="$2" + +zero=$(git hash-object --stdin &2 "Found WIP commit in $local_ref, not pushing" + exit 1 + fi + fi +done + +exit 0 diff --git a/tests/resources/sparse/.gitted/hooks/pre-rebase.sample b/tests/resources/sparse/.gitted/hooks/pre-rebase.sample new file mode 100755 index 00000000000..6cbef5c370d --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-rebase.sample @@ -0,0 +1,169 @@ +#!/bin/sh +# +# Copyright (c) 2006, 2008 Junio C Hamano +# +# The "pre-rebase" hook is run just before "git rebase" starts doing +# its job, and can prevent the command from running by exiting with +# non-zero status. +# +# The hook is called with the following parameters: +# +# $1 -- the upstream the series was forked from. +# $2 -- the branch being rebased (or empty when rebasing the current branch). +# +# This sample shows how to prevent topic branches that are already +# merged to 'next' branch from getting rebased, because allowing it +# would result in rebasing already published history. + +publish=next +basebranch="$1" +if test "$#" = 2 +then + topic="refs/heads/$2" +else + topic=`git symbolic-ref HEAD` || + exit 0 ;# we do not interrupt rebasing detached HEAD +fi + +case "$topic" in +refs/heads/??/*) + ;; +*) + exit 0 ;# we do not interrupt others. + ;; +esac + +# Now we are dealing with a topic branch being rebased +# on top of master. Is it OK to rebase it? + +# Does the topic really exist? +git show-ref -q "$topic" || { + echo >&2 "No such branch $topic" + exit 1 +} + +# Is topic fully merged to master? +not_in_master=`git rev-list --pretty=oneline ^master "$topic"` +if test -z "$not_in_master" +then + echo >&2 "$topic is fully merged to master; better remove it." + exit 1 ;# we could allow it, but there is no point. +fi + +# Is topic ever merged to next? If so you should not be rebasing it. +only_next_1=`git rev-list ^master "^$topic" ${publish} | sort` +only_next_2=`git rev-list ^master ${publish} | sort` +if test "$only_next_1" = "$only_next_2" +then + not_in_topic=`git rev-list "^$topic" master` + if test -z "$not_in_topic" + then + echo >&2 "$topic is already up to date with master" + exit 1 ;# we could allow it, but there is no point. + else + exit 0 + fi +else + not_in_next=`git rev-list --pretty=oneline ^${publish} "$topic"` + /usr/bin/perl -e ' + my $topic = $ARGV[0]; + my $msg = "* $topic has commits already merged to public branch:\n"; + my (%not_in_next) = map { + /^([0-9a-f]+) /; + ($1 => 1); + } split(/\n/, $ARGV[1]); + for my $elem (map { + /^([0-9a-f]+) (.*)$/; + [$1 => $2]; + } split(/\n/, $ARGV[2])) { + if (!exists $not_in_next{$elem->[0]}) { + if ($msg) { + print STDERR $msg; + undef $msg; + } + print STDERR " $elem->[1]\n"; + } + } + ' "$topic" "$not_in_next" "$not_in_master" + exit 1 +fi + +<<\DOC_END + +This sample hook safeguards topic branches that have been +published from being rewound. + +The workflow assumed here is: + + * Once a topic branch forks from "master", "master" is never + merged into it again (either directly or indirectly). + + * Once a topic branch is fully cooked and merged into "master", + it is deleted. If you need to build on top of it to correct + earlier mistakes, a new topic branch is created by forking at + the tip of the "master". This is not strictly necessary, but + it makes it easier to keep your history simple. + + * Whenever you need to test or publish your changes to topic + branches, merge them into "next" branch. + +The script, being an example, hardcodes the publish branch name +to be "next", but it is trivial to make it configurable via +$GIT_DIR/config mechanism. + +With this workflow, you would want to know: + +(1) ... if a topic branch has ever been merged to "next". Young + topic branches can have stupid mistakes you would rather + clean up before publishing, and things that have not been + merged into other branches can be easily rebased without + affecting other people. But once it is published, you would + not want to rewind it. + +(2) ... if a topic branch has been fully merged to "master". + Then you can delete it. More importantly, you should not + build on top of it -- other people may already want to + change things related to the topic as patches against your + "master", so if you need further changes, it is better to + fork the topic (perhaps with the same name) afresh from the + tip of "master". + +Let's look at this example: + + o---o---o---o---o---o---o---o---o---o "next" + / / / / + / a---a---b A / / + / / / / + / / c---c---c---c B / + / / / \ / + / / / b---b C \ / + / / / / \ / + ---o---o---o---o---o---o---o---o---o---o---o "master" + + +A, B and C are topic branches. + + * A has one fix since it was merged up to "next". + + * B has finished. It has been fully merged up to "master" and "next", + and is ready to be deleted. + + * C has not merged to "next" at all. + +We would want to allow C to be rebased, refuse A, and encourage +B to be deleted. + +To compute (1): + + git rev-list ^master ^topic next + git rev-list ^master next + + if these match, topic has not merged in next at all. + +To compute (2): + + git rev-list master..topic + + if this is empty, it is fully merged to "master". + +DOC_END diff --git a/tests/resources/sparse/.gitted/hooks/pre-receive.sample b/tests/resources/sparse/.gitted/hooks/pre-receive.sample new file mode 100755 index 00000000000..a1fd29ec148 --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/pre-receive.sample @@ -0,0 +1,24 @@ +#!/bin/sh +# +# An example hook script to make use of push options. +# The example simply echoes all push options that start with 'echoback=' +# and rejects all pushes when the "reject" push option is used. +# +# To enable this hook, rename this file to "pre-receive". + +if test -n "$GIT_PUSH_OPTION_COUNT" +then + i=0 + while test "$i" -lt "$GIT_PUSH_OPTION_COUNT" + do + eval "value=\$GIT_PUSH_OPTION_$i" + case "$value" in + echoback=*) + echo "echo from the pre-receive-hook: ${value#*=}" >&2 + ;; + reject) + exit 1 + esac + i=$((i + 1)) + done +fi diff --git a/tests/resources/sparse/.gitted/hooks/prepare-commit-msg.sample b/tests/resources/sparse/.gitted/hooks/prepare-commit-msg.sample new file mode 100755 index 00000000000..10fa14c5ab0 --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/prepare-commit-msg.sample @@ -0,0 +1,42 @@ +#!/bin/sh +# +# An example hook script to prepare the commit log message. +# Called by "git commit" with the name of the file that has the +# commit message, followed by the description of the commit +# message's source. The hook's purpose is to edit the commit +# message file. If the hook fails with a non-zero status, +# the commit is aborted. +# +# To enable this hook, rename this file to "prepare-commit-msg". + +# This hook includes three examples. The first one removes the +# "# Please enter the commit message..." help message. +# +# The second includes the output of "git diff --name-status -r" +# into the message, just before the "git status" output. It is +# commented because it doesn't cope with --amend or with squashed +# commits. +# +# The third example adds a Signed-off-by line to the message, that can +# still be edited. This is rarely a good idea. + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +/usr/bin/perl -i.bak -ne 'print unless(m/^. Please enter the commit message/..m/^#$/)' "$COMMIT_MSG_FILE" + +# case "$COMMIT_SOURCE,$SHA1" in +# ,|template,) +# /usr/bin/perl -i.bak -pe ' +# print "\n" . `git diff --cached --name-status -r` +# if /^#/ && $first++ == 0' "$COMMIT_MSG_FILE" ;; +# *) ;; +# esac + +# SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p') +# git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE" +# if test -z "$COMMIT_SOURCE" +# then +# /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE" +# fi diff --git a/tests/resources/sparse/.gitted/hooks/push-to-checkout.sample b/tests/resources/sparse/.gitted/hooks/push-to-checkout.sample new file mode 100755 index 00000000000..af5a0c0018b --- /dev/null +++ b/tests/resources/sparse/.gitted/hooks/push-to-checkout.sample @@ -0,0 +1,78 @@ +#!/bin/sh + +# An example hook script to update a checked-out tree on a git push. +# +# This hook is invoked by git-receive-pack(1) when it reacts to git +# push and updates reference(s) in its repository, and when the push +# tries to update the branch that is currently checked out and the +# receive.denyCurrentBranch configuration variable is set to +# updateInstead. +# +# By default, such a push is refused if the working tree and the index +# of the remote repository has any difference from the currently +# checked out commit; when both the working tree and the index match +# the current commit, they are updated to match the newly pushed tip +# of the branch. This hook is to be used to override the default +# behaviour; however the code below reimplements the default behaviour +# as a starting point for convenient modification. +# +# The hook receives the commit with which the tip of the current +# branch is going to be updated: +commit=$1 + +# It can exit with a non-zero status to refuse the push (when it does +# so, it must not modify the index or the working tree). +die () { + echo >&2 "$*" + exit 1 +} + +# Or it can make any necessary changes to the working tree and to the +# index to bring them to the desired state when the tip of the current +# branch is updated to the new commit, and exit with a zero status. +# +# For example, the hook can simply run git read-tree -u -m HEAD "$1" +# in order to emulate git fetch that is run in the reverse direction +# with git push, as the two-tree form of git read-tree -u -m is +# essentially the same as git switch or git checkout that switches +# branches while keeping the local changes in the working tree that do +# not interfere with the difference between the branches. + +# The below is a more-or-less exact translation to shell of the C code +# for the default behaviour for git's push-to-checkout hook defined in +# the push_to_deploy() function in builtin/receive-pack.c. +# +# Note that the hook will be executed from the repository directory, +# not from the working tree, so if you want to perform operations on +# the working tree, you will have to adapt your code accordingly, e.g. +# by adding "cd .." or using relative paths. + +if ! git update-index -q --ignore-submodules --refresh +then + die "Up-to-date check failed" +fi + +if ! git diff-files --quiet --ignore-submodules -- +then + die "Working directory has unstaged changes" +fi + +# This is a rough translation of: +# +# head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX +if git cat-file -e HEAD 2>/dev/null +then + head=HEAD +else + head=$(git hash-object -t tree --stdin &2 + echo " (if you want, you could supply GIT_DIR then run" >&2 + echo " $0 )" >&2 + exit 1 +fi + +if [ -z "$refname" -o -z "$oldrev" -o -z "$newrev" ]; then + echo "usage: $0 " >&2 + exit 1 +fi + +# --- Config +allowunannotated=$(git config --type=bool hooks.allowunannotated) +allowdeletebranch=$(git config --type=bool hooks.allowdeletebranch) +denycreatebranch=$(git config --type=bool hooks.denycreatebranch) +allowdeletetag=$(git config --type=bool hooks.allowdeletetag) +allowmodifytag=$(git config --type=bool hooks.allowmodifytag) + +# check for no description +projectdesc=$(sed -e '1q' "$GIT_DIR/description") +case "$projectdesc" in +"Unnamed repository"* | "") + echo "*** Project description file hasn't been set" >&2 + exit 1 + ;; +esac + +# --- Check types +# if $newrev is 0000...0000, it's a commit to delete a ref. +zero=$(git hash-object --stdin &2 + echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2 + exit 1 + fi + ;; + refs/tags/*,delete) + # delete tag + if [ "$allowdeletetag" != "true" ]; then + echo "*** Deleting a tag is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/tags/*,tag) + # annotated tag + if [ "$allowmodifytag" != "true" ] && git rev-parse $refname > /dev/null 2>&1 + then + echo "*** Tag '$refname' already exists." >&2 + echo "*** Modifying a tag is not allowed in this repository." >&2 + exit 1 + fi + ;; + refs/heads/*,commit) + # branch + if [ "$oldrev" = "$zero" -a "$denycreatebranch" = "true" ]; then + echo "*** Creating a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/heads/*,delete) + # delete branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + refs/remotes/*,commit) + # tracking branch + ;; + refs/remotes/*,delete) + # delete tracking branch + if [ "$allowdeletebranch" != "true" ]; then + echo "*** Deleting a tracking branch is not allowed in this repository" >&2 + exit 1 + fi + ;; + *) + # Anything else (is there anything else?) + echo "*** Update hook: unknown type of update to ref $refname of type $newrev_type" >&2 + exit 1 + ;; +esac + +# --- Finished +exit 0 diff --git a/tests/resources/sparse/.gitted/index b/tests/resources/sparse/.gitted/index new file mode 100644 index 0000000000000000000000000000000000000000..52857c3608cc7199010857b526e271ea8b5e7dbb GIT binary patch literal 1082 zcmZ?q402{*U|#F~On~Nyd26}64TsSXa~xRF%;DIwCu>Rafl|fOB`XbO6U$f3>7Qxu z!0Et{q@N6SkvY)OAOJKcT4qxOKa7T&CxvDnYo+S8yAQXn;g?U#nvuK2?z{f=`CE|9 zvw#=}GH)rr#g* z8bAyLnln{>tLQQq4K>dO%{-1~-s#S*sh=~8o|y&;@|73?=~NCCsgN!0uN1Q-o5$NLwWIatGqfeUPoAuNm_;RJM7 z`MjfGcL8aryDHGkV>`!arpnB{;`U0m?_tyG=gnC>#mx)iE+eqJLV{dffs7^wLn8$v zF1MVkO^STy)*b1cGN 1614944487 +0100 commit (initial): first commit +35e0dddab1fda55a937272c72c941e1877a47300 eb40db816ce2827b304d9f88c534b91cea2fdfbd Jochen Hunz 1614944499 +0100 commit: second commit +eb40db816ce2827b304d9f88c534b91cea2fdfbd 8eb7cab136eb121ba503b2027f71fcb00f50088e Jochen Hunz 1614944522 +0100 commit: third commit +8eb7cab136eb121ba503b2027f71fcb00f50088e 64198fc3a6ae0127863ec2cc781066bd27d202d8 Jochen Hunz 1614944546 +0100 commit: fourth commit +64198fc3a6ae0127863ec2cc781066bd27d202d8 6b09eb82e3ead02161c09b8f6e23edd62a036dd0 Jochen Hunz 1614944554 +0100 commit: fight commit +6b09eb82e3ead02161c09b8f6e23edd62a036dd0 1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 Jochen Hunz 1614944599 +0100 commit (amend): fifth commit +1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 Jochen Hunz 1614944808 +0100 reset: moving to HEAD +1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 9bf6a60c1a7c55f9a3067de627f173c38a030c73 Jochen Hunz 1614945024 +0100 commit: sixth commit +9bf6a60c1a7c55f9a3067de627f173c38a030c73 9bf6a60c1a7c55f9a3067de627f173c38a030c73 Jochen Hunz 1614945051 +0100 reset: moving to HEAD +9bf6a60c1a7c55f9a3067de627f173c38a030c73 9bf6a60c1a7c55f9a3067de627f173c38a030c73 Jochen Hunz 1614945059 +0100 reset: moving to HEAD +9bf6a60c1a7c55f9a3067de627f173c38a030c73 9bf6a60c1a7c55f9a3067de627f173c38a030c73 Jochen Hunz 1614945090 +0100 reset: moving to HEAD diff --git a/tests/resources/sparse/.gitted/logs/refs/heads/main b/tests/resources/sparse/.gitted/logs/refs/heads/main new file mode 100644 index 00000000000..4b675c26ac6 --- /dev/null +++ b/tests/resources/sparse/.gitted/logs/refs/heads/main @@ -0,0 +1,7 @@ +0000000000000000000000000000000000000000 35e0dddab1fda55a937272c72c941e1877a47300 Jochen Hunz 1614944487 +0100 commit (initial): first commit +35e0dddab1fda55a937272c72c941e1877a47300 eb40db816ce2827b304d9f88c534b91cea2fdfbd Jochen Hunz 1614944499 +0100 commit: second commit +eb40db816ce2827b304d9f88c534b91cea2fdfbd 8eb7cab136eb121ba503b2027f71fcb00f50088e Jochen Hunz 1614944522 +0100 commit: third commit +8eb7cab136eb121ba503b2027f71fcb00f50088e 64198fc3a6ae0127863ec2cc781066bd27d202d8 Jochen Hunz 1614944546 +0100 commit: fourth commit +64198fc3a6ae0127863ec2cc781066bd27d202d8 6b09eb82e3ead02161c09b8f6e23edd62a036dd0 Jochen Hunz 1614944554 +0100 commit: fight commit +6b09eb82e3ead02161c09b8f6e23edd62a036dd0 1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 Jochen Hunz 1614944599 +0100 commit (amend): fifth commit +1f247c26afea82b4a4dedfe0ebe7946e3dcaa226 9bf6a60c1a7c55f9a3067de627f173c38a030c73 Jochen Hunz 1614945024 +0100 commit: sixth commit diff --git a/tests/resources/sparse/.gitted/objects/13/85f264afb75a56a5bec74243be9b367ba4ca08 b/tests/resources/sparse/.gitted/objects/13/85f264afb75a56a5bec74243be9b367ba4ca08 new file mode 100644 index 0000000000000000000000000000000000000000..cedb2a22e6914c3bbbed90bbedf8fd2095bf5a7d GIT binary patch literal 19 acmb-^#7GGZ_>00M<%hW%%IoLWw;pV6ClQ(1gvvZvL_M`s{%DGaXt zo7->7tS-bG;mLbNu=<77Q0 literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/1f/247c26afea82b4a4dedfe0ebe7946e3dcaa226 b/tests/resources/sparse/.gitted/objects/1f/247c26afea82b4a4dedfe0ebe7946e3dcaa226 new file mode 100644 index 00000000000..1edaf0622de --- /dev/null +++ b/tests/resources/sparse/.gitted/objects/1f/247c26afea82b4a4dedfe0ebe7946e3dcaa226 @@ -0,0 +1,2 @@ +xA0s+03#i$1/䑌lrdTu붮KҏZ=-&PAH#8JqR%W$QlUV Pd, +#hε>ggoKCo8W]r{W$D~ȧʹL}yH \ No newline at end of file diff --git a/tests/resources/sparse/.gitted/objects/24/7ba04bb6ec6c1dd5b850e700b6a9c421436c8a b/tests/resources/sparse/.gitted/objects/24/7ba04bb6ec6c1dd5b850e700b6a9c421436c8a new file mode 100644 index 0000000000000000000000000000000000000000..1784b3abb69bde2d09d91890a55190954afeaa5e GIT binary patch literal 129 zcmV-{0Dk{?0V^p=O;s>7GGZ_>00M0)iJ6z;`sw(`*IX+N4GhdoOcc^Gb5ac%Tz59_Guv*%-f?Hnp6xli8~#*p j2uD(2#Bh$$OqH2?#qE`B-@~TW&zrM&iklYzqbM_0LM1u6 literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/25/0db0309633412161808594a7c466d512b1a70d b/tests/resources/sparse/.gitted/objects/25/0db0309633412161808594a7c466d512b1a70d new file mode 100644 index 0000000000000000000000000000000000000000..17c9c4f7c690534d7bb71823099b4eb83008a254 GIT binary patch literal 129 zcmV-{0Dk{?0V^p=O;s>7GGZ_>00M6XD~D{Ff%bxNXyJgH8fzjXCZrINAe#UH literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 b/tests/resources/sparse/.gitted/objects/45/b983be36b73c0788dc9cbcb76cbb80fc7bb057 new file mode 100644 index 0000000000000000000000000000000000000000..7ca4ceed50400af7e36b25ff200a7be95f0bc61f GIT binary patch literal 18 acmb7Ghi?=00MEqr#T%JMQGXZ?;+Wl2aT=s*D)UF`B6|bFa9)lI?rgwEB5- N7Ef{W0sxZOLd*NFO1S_4 literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/47/33065c6f0d153b022bb07c11bdcb21235e5f21 b/tests/resources/sparse/.gitted/objects/47/33065c6f0d153b022bb07c11bdcb21235e5f21 new file mode 100644 index 0000000000000000000000000000000000000000..8f6a204db9e7d71ef5021687b0c95f466f37b780 GIT binary patch literal 75 zcmV-R0JQ&j0V^p=O;s>6V=y!@Ff%bxNXyJgHDqwz*}TteyA6BCojH59=j?9yQ@tS^ hNr4f=IYu*8X6_ZYSF(K%n^r$>&f+OW4H;Z_Ht#drZo}SjXU?AOIlCMFRBs4JQeebzj?qk&nR~_Um2BU`rq$1zvv`V| J7XX;yC(jw2Ex`Z) literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/5d/b59d25cf0bc7ed07c647175acc05fde8371c2c b/tests/resources/sparse/.gitted/objects/5d/b59d25cf0bc7ed07c647175acc05fde8371c2c new file mode 100644 index 00000000000..21f5c6086f8 --- /dev/null +++ b/tests/resources/sparse/.gitted/objects/5d/b59d25cf0bc7ed07c647175acc05fde8371c2c @@ -0,0 +1 @@ +x+)JMU03c040031QHI5e{.^Dž])DŽ̟'_W|}yO \ No newline at end of file diff --git a/tests/resources/sparse/.gitted/objects/64/198fc3a6ae0127863ec2cc781066bd27d202d8 b/tests/resources/sparse/.gitted/objects/64/198fc3a6ae0127863ec2cc781066bd27d202d8 new file mode 100644 index 00000000000..388e533d3b0 --- /dev/null +++ b/tests/resources/sparse/.gitted/objects/64/198fc3a6ae0127863ec2cc781066bd27d202d8 @@ -0,0 +1,3 @@ +xMA F]):#rIL +V5MfNշxg +jvF[Қ f9G)[m@u-&JSvH* '\h6EbdKm?{㸼"h6@Ĝ8b?m, /bK. \ No newline at end of file diff --git a/tests/resources/sparse/.gitted/objects/69/7bf19b0a83d30f031c5c9e6445f2c7f1eb0aa9 b/tests/resources/sparse/.gitted/objects/69/7bf19b0a83d30f031c5c9e6445f2c7f1eb0aa9 new file mode 100644 index 0000000000000000000000000000000000000000..a4ae69bb99bfefcaa47d7eede40a43c6881a2e60 GIT binary patch literal 103 zcmV-t0GR)H0V^p=O;xZoVK6ZO0)=FT{bzfeT28H>(VKTuS$t)(r`5?vXABJt%uGxa z(lT>WO&M3 literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/6b/09eb82e3ead02161c09b8f6e23edd62a036dd0 b/tests/resources/sparse/.gitted/objects/6b/09eb82e3ead02161c09b8f6e23edd62a036dd0 new file mode 100644 index 0000000000000000000000000000000000000000..4b2d15ec70d2932719eab1ffddb57e0366017bd1 GIT binary patch literal 164 zcmV;V09*ff0iBLP3c@fDMP26q*oi44epDMrNZr1-*A4O#yzj8>RzqORNE=7>jieJ>zwo6!4V13==WtS S|MF&-9%F;wjQIe@2uQI#lvhar literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/75/7d3de1be565af60d471d877d4f4bf8c7ee7945 b/tests/resources/sparse/.gitted/objects/75/7d3de1be565af60d471d877d4f4bf8c7ee7945 new file mode 100644 index 0000000000000000000000000000000000000000..b8dcd3a630cbe469789aff47adc91edef7a10e0e GIT binary patch literal 129 zcmV-{0Dk{?0V^p=O;s>7GGZ_>00Mdn7eq?8P>lq%w=>84GhdoOcc^Gb5ac%Tz59_Guv*%-f?Hnp6xli8~#*p j2uD(2#Bh$$OqH2?#qE`B-@~TW&zrM&iklYzatShte)Kvt literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/79/25b6dde1b5ac0f1f666a986da43ef72fd79fb4 b/tests/resources/sparse/.gitted/objects/79/25b6dde1b5ac0f1f666a986da43ef72fd79fb4 new file mode 100644 index 0000000000000000000000000000000000000000..4cf31a3c2e0d809facb243f2c82c16b7d9a4aa8a GIT binary patch literal 20 bcmbXrh literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/83/0d97438565f3d14f7fce1b650cabeb7928a84c b/tests/resources/sparse/.gitted/objects/83/0d97438565f3d14f7fce1b650cabeb7928a84c new file mode 100644 index 0000000000000000000000000000000000000000..c8071f428dde70b563fdadf49bb6bde371f493af GIT binary patch literal 23 fcmbc͕sb{r +gD5Rbe#eބK \ No newline at end of file diff --git a/tests/resources/sparse/.gitted/objects/9b/f6a60c1a7c55f9a3067de627f173c38a030c73 b/tests/resources/sparse/.gitted/objects/9b/f6a60c1a7c55f9a3067de627f173c38a030c73 new file mode 100644 index 00000000000..50f6de97b11 --- /dev/null +++ b/tests/resources/sparse/.gitted/objects/9b/f6a60c1a7c55f9a3067de627f173c38a030c73 @@ -0,0 +1,3 @@ +xA +0E]J2-o1LIPSOo{|ɏRPWU %t**4F90[ -a)jf^7Ghi?=00M<%hW%%IoLWw;pV6ClQ(1gvvZvL_M`s{%DGaXt zo7)IWy)|f_s-W{yp9XcyG~P%3CTH`KPgTgNr4%I)BXANKfYEe1g?1N`T6=r OrK9sM3IhNa>P6iCn@oHF literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/be/505c0d986659a6a69c4d99996c73d634a65ae9 b/tests/resources/sparse/.gitted/objects/be/505c0d986659a6a69c4d99996c73d634a65ae9 new file mode 100644 index 0000000000000000000000000000000000000000..6a5e295898e6c8bf9069201d28ebc3828477408e GIT binary patch literal 75 zcmV-R0JQ&j0V^p=O;s>6V=y!@Ff%bxNXyJgHD-8SwY2H?zfXTs0)tNH*`JS{-)GZ> hq`-v1Jw=-JcFi5Pc@Zyp=J3r)4EyU+1ppz@9eTAFB6a`( literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/bf/cd8c4284caaf988d6ed92317a963493ac9e2cc b/tests/resources/sparse/.gitted/objects/bf/cd8c4284caaf988d6ed92317a963493ac9e2cc new file mode 100644 index 0000000000000000000000000000000000000000..a01ea4953bdd1f60c1c21b231c9f1c6a3fa499d6 GIT binary patch literal 75 zcmV-R0JQ&j0V^p=O;s>6V=y!@Ff%bxNXyJgHD}neCu>Rafl|fOB`XbO6U$f3>7Qwj hq`-oqQgz$ihg;Y1%co_{$X#OhUH|(0EdbMZ9U_zvBbxvK literal 0 HcmV?d00001 diff --git a/tests/resources/sparse/.gitted/objects/ce/013625030ba8dba906f756967f9e9ca394464a b/tests/resources/sparse/.gitted/objects/ce/013625030ba8dba906f756967f9e9ca394464a new file mode 100644 index 0000000000000000000000000000000000000000..6802d49492403f3d23831789ec6a4a14984b1538 GIT binary patch literal 21 dcmb_ʲZGqT 6ģcR6`V4Wpڔ8&:η#P1x䝵: E&p.f:x,G9׆ +#E"Y/뛗J ~;hJ \ No newline at end of file diff --git a/tests/resources/sparse/.gitted/objects/eb/7aa582fbfef2fc645152cb6e3fcf5d9f8e3c8a b/tests/resources/sparse/.gitted/objects/eb/7aa582fbfef2fc645152cb6e3fcf5d9f8e3c8a new file mode 100644 index 0000000000000000000000000000000000000000..549a0bc50a42e5a872d6f1da57f0d1d9f9022ae5 GIT binary patch literal 23 fcmb