diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4954ec9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + name: Build And Test + runs-on: ubuntu-22.04 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt update && sudo apt install -y libssl-dev + - name: Run tests + run: make test diff --git a/Makefile b/Makefile index 1d2b1ef..35a9c8d 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,9 @@ build: $(BUILD_TARGETS) build-bin: git-crypt +test: build-bin + tests/worktree.sh ./git-crypt + git-crypt: $(OBJFILES) $(CXX) $(CXXFLAGS) -o $@ $(OBJFILES) $(LDFLAGS) @@ -90,6 +93,6 @@ install-man: build-man install -m 644 man/man1/git-crypt.1 $(DESTDIR)$(MANDIR)/man1/ .PHONY: all \ - build build-bin build-man \ + build build-bin build-man test \ clean clean-bin clean-man \ install install-bin install-man diff --git a/commands.cpp b/commands.cpp index 6b3c498..6976522 100644 --- a/commands.cpp +++ b/commands.cpp @@ -239,16 +239,16 @@ static void validate_key_name_or_throw (const char* key_name) static std::string get_internal_state_path () { - // git rev-parse --git-dir + // git rev-parse --git-common-dir std::vector command; command.push_back("git"); command.push_back("rev-parse"); - command.push_back("--git-dir"); + command.push_back("--git-common-dir"); std::stringstream output; if (!successful_exit(exec_command(command, output))) { - throw Error("'git rev-parse --git-dir' failed - is this a Git repository?"); + throw Error("'git rev-parse --git-common-dir' failed - is this a Git repository?"); } std::string path; @@ -1705,4 +1705,3 @@ int status (int argc, const char** argv) return exit_status; } - diff --git a/tests/worktree.sh b/tests/worktree.sh new file mode 100755 index 0000000..fdb91cb --- /dev/null +++ b/tests/worktree.sh @@ -0,0 +1,160 @@ +#!/bin/sh + +set -eu + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 GIT_CRYPT_BINARY" >&2 + exit 2 +fi + +GIT_CRYPT=$1 + +case "$GIT_CRYPT" in +/*) ;; +*) + GIT_CRYPT=$(cd "$(dirname "$GIT_CRYPT")" && pwd)/$(basename "$GIT_CRYPT") + ;; +esac + +if [ ! -x "$GIT_CRYPT" ]; then + echo "git-crypt binary is not executable: $GIT_CRYPT" >&2 + exit 1 +fi + +TMP_ROOT=$(mktemp -d "${TMPDIR:-/tmp}/git-crypt-worktree-test.XXXXXX") +trap 'rm -rf "$TMP_ROOT"' EXIT INT TERM HUP + +fail () { + echo "not ok - $1" >&2 + exit 1 +} + +assert_file_equals () { + path=$1 + expected=$2 + + if [ ! -f "$path" ]; then + fail "missing file: $path" + fi + + actual=$(cat "$path") + if [ "$actual" != "$expected" ]; then + fail "unexpected contents in $path" + fi +} + +assert_clean_status () { + repo=$1 + status=$(git -C "$repo" status --short) + if [ -n "$status" ]; then + echo "$status" >&2 + fail "working tree is not clean: $repo" + fi +} + +init_repo () { + repo=$1 + + mkdir -p "$repo" + git -C "$repo" init >/dev/null + git -C "$repo" config user.name "Codex Test" + git -C "$repo" config user.email "codex@example.com" + git -C "$repo" config core.fsmonitor false + + ( + cd "$repo" + key_path=$(cd .. && pwd)/$(basename "$repo").key + "$GIT_CRYPT" init >/dev/null + printf 'secret.txt filter=git-crypt diff=git-crypt\n' > .gitattributes + printf 'topsecret\n' > secret.txt + git add .gitattributes secret.txt + git commit -m "init secret" >/dev/null + "$GIT_CRYPT" export-key "$key_path" >/dev/null + ) +} + +test_non_worktree_unlock () { + repo=$TMP_ROOT/non-worktree + + init_repo "$repo" + + ( + cd "$repo" + "$GIT_CRYPT" lock --force >/dev/null + "$GIT_CRYPT" unlock "$repo.key" >/dev/null + ) + + assert_file_equals "$repo/secret.txt" "topsecret" + assert_clean_status "$repo" +} + +test_worktree_checkout_uses_common_state () { + repo=$TMP_ROOT/worktree-main + wt=$TMP_ROOT/worktree-checkout + + init_repo "$repo" + git -C "$repo" worktree add "$wt" >/dev/null + git -C "$wt" config core.fsmonitor false + + assert_file_equals "$wt/secret.txt" "topsecret" + assert_clean_status "$wt" +} + +test_worktree_unlock_uses_common_state () { + repo=$TMP_ROOT/worktree-unlock-main + wt=$TMP_ROOT/worktree-unlock + + mkdir -p "$repo" + git -C "$repo" init >/dev/null + git -C "$repo" config user.name "Codex Test" + git -C "$repo" config user.email "codex@example.com" + git -C "$repo" config core.fsmonitor false + + ( + cd "$repo" + printf 'base\n' > README + git add README + git commit -m "base" >/dev/null + + key_path=$(cd .. && pwd)/$(basename "$repo").key + "$GIT_CRYPT" init >/dev/null + printf 'secret.txt filter=git-crypt diff=git-crypt\n' > .gitattributes + printf 'topsecret\n' > secret.txt + git add .gitattributes secret.txt + git commit -m "add secret" >/dev/null + encrypted_rev=$(git rev-parse HEAD) + printf '%s\n' "$encrypted_rev" > ../$(basename "$repo").rev + "$GIT_CRYPT" export-key "$key_path" >/dev/null + "$GIT_CRYPT" lock --force >/dev/null + ) + + git -C "$repo" worktree add --detach "$wt" HEAD~1 >/dev/null + git -C "$wt" config core.fsmonitor false + encrypted_rev=$(cat "$TMP_ROOT/$(basename "$repo").rev") + + ( + cd "$wt" + "$GIT_CRYPT" unlock "$repo.key" >/dev/null + git checkout "$encrypted_rev" >/dev/null + ) + + common_dir=$(git -C "$wt" rev-parse --git-common-dir) + git_dir=$(git -C "$wt" rev-parse --git-dir) + + if [ ! -f "$common_dir/git-crypt/keys/default" ]; then + fail "missing common-dir key state" + fi + + if [ -e "$git_dir/git-crypt/keys/default" ]; then + fail "unexpected worktree-local key state" + fi + + assert_file_equals "$wt/secret.txt" "topsecret" + assert_clean_status "$wt" +} + +test_non_worktree_unlock +test_worktree_checkout_uses_common_state +test_worktree_unlock_uses_common_state + +echo "ok - worktree regression tests passed"