diff --git a/.github/workflows/codex-pgo-training.sh b/.github/workflows/codex-pgo-training.sh new file mode 100644 index 00000000000000..070d8b54cae486 --- /dev/null +++ b/.github/workflows/codex-pgo-training.sh @@ -0,0 +1,60 @@ +#!/bin/sh + +# Keep this workload short and biased toward the local Git operations Codex +# invokes frequently. The full Git test suite is too slow for each release +# target and would weight test-harness paths more heavily than status, diff, +# clone, fetch, and repository maintenance. + +set -eu + +git_bin="$PWD/bin-wrappers/git" +training_dir=$(mktemp -d "${TMPDIR:-/tmp}/codex-git-pgo.XXXXXX") +repo="$training_dir/repo" +clone="$training_dir/clone" + +cleanup () { + rm -rf "$training_dir" +} +trap cleanup EXIT HUP INT TERM + +mkdir -p "$training_dir/home" +export HOME="$training_dir/home" +export GIT_CONFIG_NOSYSTEM=1 +export GIT_TERMINAL_PROMPT=0 + +"$git_bin" clone --quiet --no-local "$PWD" "$repo" +"$git_bin" -C "$repo" config user.name "Codex Git PGO" +"$git_bin" -C "$repo" config user.email "codex-git-pgo@openai.com" + +i=0 +while test "$i" -lt 256 +do + dir="$repo/training/$((i % 16))" + mkdir -p "$dir" + printf '%s\n' "$i" >"$dir/file-$i" + i=$((i + 1)) +done + +"$git_bin" -C "$repo" status --porcelain=v2 --branch >/dev/null +"$git_bin" -C "$repo" status --porcelain=v2 --branch --untracked-files=all >/dev/null +"$git_bin" -C "$repo" ls-files --others --exclude-standard >/dev/null +"$git_bin" -C "$repo" add training +"$git_bin" -C "$repo" diff --cached --stat >/dev/null +"$git_bin" -C "$repo" commit --quiet -m "add training files" + +printf 'changed\n' >>"$repo/training/0/file-0" +rm "$repo/training/1/file-1" +mkdir -p "$repo/untracked" +printf 'new\n' >"$repo/untracked/file" + +"$git_bin" -C "$repo" status --porcelain=v2 --branch >/dev/null +"$git_bin" -C "$repo" diff --stat >/dev/null +"$git_bin" -C "$repo" diff --name-status >/dev/null +"$git_bin" -C "$repo" ls-files --stage >/dev/null +"$git_bin" -C "$repo" log --oneline --decorate -20 >/dev/null +"$git_bin" -C "$repo" rev-list --objects --all >/dev/null +"$git_bin" -C "$repo" for-each-ref --format='%(refname) %(objectname)' >/dev/null +"$git_bin" -C "$repo" repack -ad +"$git_bin" clone --quiet --no-local "$repo" "$clone" +"$git_bin" -C "$clone" status --porcelain=v2 --branch >/dev/null +"$git_bin" -C "$clone" fetch --quiet "$repo" diff --git a/.github/workflows/codex-release.yml b/.github/workflows/codex-release.yml new file mode 100644 index 00000000000000..778a67abf28b4f --- /dev/null +++ b/.github/workflows/codex-release.yml @@ -0,0 +1,582 @@ +name: Codex Git release + +on: + push: + branches: + - codex + - codex-unstable + +permissions: + contents: read + +concurrency: + group: codex-git-release-${{ github.sha }} + cancel-in-progress: false + +jobs: + publication: + name: Verify controller publication + runs-on: ubuntu-24.04 + if: github.event.deleted == false + outputs: + published: ${{ steps.verify.outputs.published }} + steps: + - name: Check the published controller output + id: verify + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + case "$GITHUB_REF" in + refs/heads/codex) + output_key=codex.output-tip + ;; + refs/heads/codex-unstable) + output_key=codex-unstable.output-tip + ;; + *) + printf 'unexpected release ref: %s\n' "$GITHUB_REF" >&2 + exit 1 + ;; + esac + + meta=$(gh api \ + "repos/$GITHUB_REPOSITORY/git/ref/heads/meta" \ + --jq '.object.sha') + recorded=$(gh api \ + "repos/$GITHUB_REPOSITORY/contents/codex.config?ref=$meta" \ + -H 'Accept: application/vnd.github.raw+json' | + git config --no-includes --file /dev/stdin \ + --get "$output_key") + + if test "$GITHUB_SHA" = "$recorded" + then + printf 'published=true\n' >>"$GITHUB_OUTPUT" + printf 'Releasing controller-published commit %s.\n' "$GITHUB_SHA" + else + printf 'published=false\n' >>"$GITHUB_OUTPUT" + printf 'Skipping non-controller publication %s.\n' "$GITHUB_SHA" + fi + + version: + name: Determine version + needs: publication + if: needs.publication.outputs.published == 'true' + runs-on: ubuntu-24.04 + outputs: + describe: ${{ steps.version.outputs.describe }} + upstream_tag: ${{ steps.version.outputs.upstream_tag }} + version: ${{ steps.version.outputs.version }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Derive OpenAI version from git describe + id: version + shell: bash + run: | + set -euo pipefail + describe="$(git describe \ + --match 'v[0-9]*' \ + --exclude 'v*-openai.*' \ + --long \ + --always \ + --abbrev=12 \ + "$GITHUB_SHA")" + + if [[ "$describe" =~ ^(.+)-([0-9]+)-g([0-9a-f]+)$ ]] + then + upstream_tag="${BASH_REMATCH[1]}" + version="$upstream_tag-openai.${BASH_REMATCH[2]}.g${BASH_REMATCH[3]}" + else + upstream_tag= + version="openai-$describe" + fi + git check-ref-format "refs/tags/$version" + printf 'describe=%s\n' "$describe" | tee -a "$GITHUB_OUTPUT" + printf 'upstream_tag=%s\n' "$upstream_tag" | tee -a "$GITHUB_OUTPUT" + printf 'version=%s\n' "$version" | tee -a "$GITHUB_OUTPUT" + + build: + name: ${{ matrix.name }} + needs: version + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + include: + - name: macOS arm64 + os: macos-15 + target_platform: macOS + asset_platform: macOS + arch: arm64 + binary: /tmp/build/git/bin/git + file_pattern: Mach-O 64-bit executable arm64 + has_gcm: false + lto: thin + profile_format: LLVM + llvm_profdata: xcrun llvm-profdata + max_tar_bytes: 67108864 + - name: macOS x64 + os: macos-15-intel + target_platform: macOS + asset_platform: macOS + arch: x64 + binary: /tmp/build/git/bin/git + file_pattern: Mach-O 64-bit executable x86_64 + has_gcm: false + lto: thin + profile_format: LLVM + llvm_profdata: xcrun llvm-profdata + max_tar_bytes: 67108864 + # Keep arm64 builds native so release smoke tests can execute them. + - name: Linux arm64 + os: ubuntu-22.04-arm + target_platform: ubuntu + asset_platform: ubuntu + arch: arm64 + binary: /tmp/build/git/bin/git + file_pattern: ELF 64-bit.*ARM aarch64 + has_gcm: false + lto: auto + profile_format: GCC + max_tar_bytes: 67108864 + - name: Linux x64 + os: ubuntu-22.04 + target_platform: ubuntu + asset_platform: ubuntu + arch: x64 + binary: /tmp/build/git/bin/git + file_pattern: ELF 64-bit.*x86-64 + has_gcm: false + lto: auto + profile_format: GCC + max_tar_bytes: 67108864 + - name: Windows arm64 + os: windows-11-arm + target_platform: win32 + asset_platform: windows + arch: arm64 + binary: /tmp/build/git/clangarm64/bin/git.exe + file_pattern: PE32\+.*ARM64 + has_gcm: true + lto: thin + profile_format: LLVM + llvm_profdata: llvm-profdata + max_tar_bytes: 134217728 + sdk_arch: aarch64 + sdk_flavor: full + mingw_dir: clangarm64 + mingit_arch: arm64 + mingit_filename: MinGit-2.55.0.2-arm64.zip + mingit_url: https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.2/MinGit-2.55.0.2-arm64.zip + mingit_sha256: 0b2b81fdce284efd174cbb51b886ccea2fd271679c4b5c21f07d9e03bae51413 + - name: Windows x64 + os: windows-2025 + target_platform: win32 + asset_platform: windows + arch: x64 + binary: /tmp/build/git/mingw64/bin/git.exe + file_pattern: PE32\+.*x86-64 + has_gcm: true + lto: auto + profile_format: GCC + max_tar_bytes: 134217728 + sdk_arch: x86_64 + sdk_flavor: full + mingw_dir: mingw64 + mingit_arch: amd64 + mingit_filename: MinGit-2.55.0.2-64-bit.zip + mingit_url: https://github.com/git-for-windows/git/releases/download/v2.55.0.windows.2/MinGit-2.55.0.2-64-bit.zip + mingit_sha256: e3ea2944cea4b3fabcd69c7c1669ef69b1b66c05ac7806d81224d0abad2dec31 + + steps: + # Keep the packaging contract, dependency pins, and platform build logic + # aligned with the artifacts already consumed by Codex and GitHub Desktop. + - name: Check out Dugite Native + uses: actions/checkout@v6 + with: + repository: dreynaud-oai/dugite-native + ref: b6f4473557acb85433fdf9deffe0854a34fd9cc5 + path: dugite-native + fetch-depth: 0 + persist-credentials: false + + - name: Check out this Git revision + uses: actions/checkout@v6 + with: + ref: ${{ github.sha }} + path: dugite-native/git + fetch-depth: 1 + persist-credentials: false + + - name: Give the source an immutable package version + shell: bash + working-directory: dugite-native/git + env: + VERSION: ${{ needs.version.outputs.version }} + run: | + git \ + -c 'user.name=github-actions[bot]' \ + -c 'user.email=41898282+github-actions[bot]@users.noreply.github.com' \ + tag -a "$VERSION" -m "$VERSION" + + - name: Install OpenAI build configuration + shell: bash + working-directory: dugite-native/git + run: cp config.mak.openai config.mak + + # Match Dugite Native's compatibility choice for its macOS x64 build. + - name: Select Xcode 16.4 + if: matrix.target_platform == 'macOS' && matrix.arch == 'x64' + run: | + sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer/ + sudo rm -rf /Library/Developer/CommandLineTools + + - name: Install Linux build dependencies + if: matrix.target_platform == 'ubuntu' + run: | + sudo apt-get update + sudo apt-get install -y \ + autoconf \ + automake \ + build-essential \ + ca-certificates \ + curl \ + gettext \ + jq \ + lsb-release \ + pkg-config + + - name: Install Linux x64 build dependencies + if: matrix.target_platform == 'ubuntu' && matrix.arch == 'x64' + run: | + sudo apt-get install -y \ + libcurl4-gnutls-dev \ + libexpat1-dev \ + libssl-dev \ + zlib1g-dev + + - name: Install Linux arm64 build dependencies + if: matrix.target_platform == 'ubuntu' && matrix.arch == 'arm64' + run: | + sudo apt-get install -y \ + binutils-aarch64-linux-gnu \ + gcc-aarch64-linux-gnu \ + libcurl4-gnutls-dev \ + libexpat1-dev \ + libssl-dev \ + zlib1g-dev + + # Dugite Native currently pins MinGit 2.53. Keep its build script and + # dependency schema, but match the runtime to the Git series we compile. + - name: Select the matching MinGit runtime + if: matrix.target_platform == 'win32' + shell: bash + working-directory: dugite-native + env: + MINGIT_ARCH: ${{ matrix.mingit_arch }} + MINGIT_FILENAME: ${{ matrix.mingit_filename }} + MINGIT_SHA256: ${{ matrix.mingit_sha256 }} + MINGIT_URL: ${{ matrix.mingit_url }} + MINGIT_VERSION: v2.55.0 + SOURCE_UPSTREAM_TAG: ${{ needs.version.outputs.upstream_tag }} + run: | + set -euo pipefail + test "$SOURCE_UPSTREAM_TAG" = "$MINGIT_VERSION" + updated="$(mktemp)" + jq \ + --arg arch "$MINGIT_ARCH" \ + --arg checksum "$MINGIT_SHA256" \ + --arg filename "$MINGIT_FILENAME" \ + --arg url "$MINGIT_URL" \ + --arg version "$MINGIT_VERSION" \ + '.git.version = $version | + (.git.files[] | + select(.platform == "windows" and .arch == $arch)) |= + (.filename = $filename | + .url = $url | + .checksum = $checksum)' \ + dependencies.json >"$updated" + mv "$updated" dependencies.json + + # Codex does not configure or invoke GCM on macOS or Linux. The + # self-contained .NET payload accounts for most of those bundles, while + # Windows MinGit configures credential.helper=manager and must retain it. + - name: Omit unused GCM from POSIX bundles + if: matrix.target_platform != 'win32' + shell: bash + working-directory: dugite-native + run: | + set -euo pipefail + updated="$(mktemp)" + jq '."git-credential-manager".files = []' \ + dependencies.json >"$updated" + mv "$updated" dependencies.json + + # Build an instrumented Git, run a representative local workload, then + # rebuild with its profile. Keep Git's optional Rust library disabled + # until its Makefile can direct Cargo at these targets. + - name: Build the Dugite Native distribution + shell: bash + working-directory: dugite-native + env: + NO_RUST: 1 + OPENAI_LLVM_PROFDATA: ${{ matrix.llvm_profdata }} + OPENAI_LTO: ${{ matrix.lto }} + OPENAI_PROFILE: BUILD + OPENAI_PROFILE_FORMAT: ${{ matrix.profile_format }} + TARGET_PLATFORM: ${{ matrix.target_platform }} + TARGET_ARCH: ${{ matrix.arch }} + run: | + set -euo pipefail + script/build.sh + + - name: Set up Git for Windows SDK + if: matrix.target_platform == 'win32' + uses: git-for-windows/setup-git-for-windows-sdk@v2 + with: + architecture: ${{ matrix.sdk_arch }} + flavor: ${{ matrix.sdk_flavor }} + cache: false + + # Dugite Native compiles its Git submodule on macOS and Linux. On + # Windows it starts from MinGit, so replace MinGit's Git programs with + # the build from this repository while retaining the portable runtime. + # MinGit omits dashed builtin aliases; installing them as copies would + # add hundreds of redundant MiB to the archive. + - name: Install this Git build into the Windows distribution + if: matrix.target_platform == 'win32' + shell: bash + working-directory: dugite-native/git + env: + MINGW_DIR: ${{ matrix.mingw_dir }} + OPENAI_LLVM_PROFDATA: ${{ matrix.llvm_profdata }} + OPENAI_LTO: ${{ matrix.lto }} + OPENAI_PROFILE_FORMAT: ${{ matrix.profile_format }} + run: | + set -euo pipefail + + make_args=( + "prefix=/$MINGW_DIR" + NO_PERL=YesPlease + NO_RUST=YesPlease + NO_TCLTK=YesPlease + NO_GETTEXT=YesPlease + NO_INSTALL_HARDLINKS=YesPlease + NO_CROSS_DIRECTORY_HARDLINKS=YesPlease + SKIP_DASHED_BUILT_INS=YesPlease + ) + jobs="${NUMBER_OF_PROCESSORS:-2}" + make -j"$jobs" "${make_args[@]}" OPENAI_PROFILE=BUILD all + make "${make_args[@]}" OPENAI_PROFILE=USE DESTDIR=/tmp/build/git strip install + + - name: Verify distribution layout and provenance + shell: bash + env: + TARGET_PLATFORM: ${{ matrix.target_platform }} + MINGW_DIR: ${{ matrix.mingw_dir }} + GIT_BINARY: ${{ matrix.binary }} + FILE_PATTERN: ${{ matrix.file_pattern }} + HAS_GCM: ${{ matrix.has_gcm }} + LTO: ${{ matrix.lto }} + PROFILE_FORMAT: ${{ matrix.profile_format }} + run: | + set -euo pipefail + if test "$TARGET_PLATFORM" = win32 + then + test -f /tmp/build/git/cmd/git.exe + test -f "/tmp/build/git/$MINGW_DIR/libexec/git-core/git-lfs.exe" + test -d "/tmp/build/git/$MINGW_DIR/share/git-core/templates" + test ! -e "/tmp/build/git/$MINGW_DIR/libexec/git-core/git-add.exe" + if test "$HAS_GCM" = true + then + test -f "/tmp/build/git/$MINGW_DIR/bin/git-credential-manager.exe" + fi + else + test -x /tmp/build/git/libexec/git-core/git-lfs + test -d /tmp/build/git/share/git-core/templates + if test "$HAS_GCM" = true + then + test -x /tmp/build/git/libexec/git-core/git-credential-manager + else + test ! -e /tmp/build/git/libexec/git-core/git-credential-manager + fi + fi + test -f /tmp/build/git/etc/gitconfig + + file "$GIT_BINARY" | tee /tmp/git-file-type + grep -E "$FILE_PATTERN" /tmp/git-file-type + strings "$GIT_BINARY" | grep -F "$GITHUB_SHA" + grep -F -- "-flto=$LTO" dugite-native/git/GIT-CFLAGS + if test "$PROFILE_FORMAT" = LLVM + then + grep -F -- "-fprofile-instr-use=" dugite-native/git/GIT-CFLAGS + else + grep -F -- "-fprofile-use=" dugite-native/git/GIT-CFLAGS + fi + + - name: Smoke-test the native distribution + shell: bash + env: + TARGET_PLATFORM: ${{ matrix.target_platform }} + MINGW_DIR: ${{ matrix.mingw_dir }} + HAS_GCM: ${{ matrix.has_gcm }} + run: | + set -euo pipefail + smoke=/tmp/codex-git-smoke + mkdir -p "$smoke/home" + + if test "$TARGET_PLATFORM" = win32 + then + git_binary=/tmp/build/git/cmd/git.exe + git_env=( + "PATH=/tmp/build/git/cmd:/tmp/build/git/$MINGW_DIR/bin:/tmp/build/git/usr/bin:$PATH" + ) + else + git_binary=/tmp/build/git/bin/git + git_env=( + GIT_CONFIG_SYSTEM=/tmp/build/git/etc/gitconfig + GIT_EXEC_PATH=/tmp/build/git/libexec/git-core + GIT_TEMPLATE_DIR=/tmp/build/git/share/git-core/templates + ) + if test "$TARGET_PLATFORM" = ubuntu + then + git_env+=( + GIT_SSL_CAINFO=/tmp/build/git/ssl/cacert.pem + PREFIX=/tmp/build/git + ) + fi + fi + git_env+=("HOME=$smoke/home" GIT_TERMINAL_PROMPT=0) + + build_options="$(env "${git_env[@]}" "$git_binary" --version --build-options)" + printf '%s\n' "$build_options" + grep -F "built from commit: $GITHUB_SHA" <<<"$build_options" + env "${git_env[@]}" "$git_binary" lfs version + if test "$HAS_GCM" = true + then + env "${git_env[@]}" "$git_binary" credential-manager --version + fi + + env "${git_env[@]}" "$git_binary" init --quiet "$smoke/repo" + echo test >"$smoke/repo/file" + env "${git_env[@]}" "$git_binary" -C "$smoke/repo" add file + env "${git_env[@]}" "$git_binary" -C "$smoke/repo" \ + -c user.name='Codex Git CI' \ + -c user.email='codex-git-ci@openai.com' \ + commit --quiet -m initial + test -z "$(env "${git_env[@]}" "$git_binary" -C "$smoke/repo" status --porcelain)" + + - name: Package with Dugite Native + shell: bash + working-directory: dugite-native + env: + ASSET_PLATFORM: ${{ matrix.asset_platform }} + TARGET_PLATFORM: ${{ matrix.target_platform }} + TARGET_ARCH: ${{ matrix.arch }} + VERSION: ${{ needs.version.outputs.version }} + MAX_TAR_BYTES: ${{ matrix.max_tar_bytes }} + run: | + set -euo pipefail + script/package.sh + + for extension in tar.gz lzma + do + matches=( + output/dugite-native-"$VERSION"-*-"$ASSET_PLATFORM"-"$TARGET_ARCH.$extension" + ) + test "${#matches[@]}" -eq 1 + test -f "${matches[0]}" + + destination="output/git-$VERSION-$ASSET_PLATFORM-$TARGET_ARCH.$extension" + mv "${matches[0]}" "$destination" + mv "${matches[0]}.sha256" "$destination.sha256" + done + + for checksum in output/*.sha256 + do + archive="${checksum%.sha256}" + expected="$(tr -d '\r\n' <"$checksum")" + if command -v sha256sum >/dev/null 2>&1 + then + actual="$(sha256sum "$archive" | awk '{print $1}')" + else + actual="$(shasum -a 256 "$archive" | awk '{print $1}')" + fi + test "$actual" = "$expected" + done + + tarball="output/git-$VERSION-$ASSET_PLATFORM-$TARGET_ARCH.tar.gz" + tar_bytes="$(wc -c <"$tarball")" + printf '%s bytes: %s\n' "$tar_bytes" "$tarball" + test "$tar_bytes" -le "$MAX_TAR_BYTES" + + - name: Upload release assets + uses: actions/upload-artifact@v7 + with: + name: git-${{ matrix.asset_platform }}-${{ matrix.arch }} + path: dugite-native/output/git-* + if-no-files-found: error + retention-days: 7 + + # The arm64 SDK puts its target Git first on PATH, but action cleanup + # runs on the x64 host and therefore needs the runner's native Git. + - name: Restore native Git for action cleanup + if: always() && matrix.target_platform == 'win32' + shell: pwsh + run: | + "C:\Program Files\Git\cmd" | + Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + release: + name: Publish GitHub prerelease + needs: + - version + - build + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Download release assets + uses: actions/download-artifact@v8 + with: + pattern: git-* + path: artifacts + merge-multiple: true + + - name: Publish immutable prerelease + env: + GH_TOKEN: ${{ github.token }} + SOURCE_DESCRIPTION: ${{ needs.version.outputs.describe }} + SOURCE_REF: ${{ github.ref }} + VERSION: ${{ needs.version.outputs.version }} + run: | + set -euo pipefail + assets=(artifacts/git-*) + release_notes=$( + printf '%s\n' \ + "source_ref=$SOURCE_REF" \ + "source_sha=$GITHUB_SHA" \ + "" \ + "OpenAI Git release artifacts for $SOURCE_DESCRIPTION, built from $GITHUB_SHA for Codex." + ) + + if gh release view "$VERSION" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 + then + gh release upload "$VERSION" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --clobber + else + gh release create "$VERSION" "${assets[@]}" \ + --repo "$GITHUB_REPOSITORY" \ + --target "$GITHUB_SHA" \ + --title "$VERSION" \ + --notes "$release_notes" \ + --prerelease + fi diff --git a/.github/workflows/codex.yml b/.github/workflows/codex.yml new file mode 100644 index 00000000000000..650cfd55baef14 --- /dev/null +++ b/.github/workflows/codex.yml @@ -0,0 +1,243 @@ +name: Refresh codex + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + inputs: + operation: + description: Refresh, scan, remove, or reorder a pinned topic + type: choice + options: + - refresh + - scan + - remove + - reorder + default: refresh + lane: + description: codex or codex-unstable for a plan operation + required: false + type: string + topic: + description: Exact topic branch for a plan operation + required: false + type: string + after: + description: Existing topic or root for reorder + required: false + type: string + plan_branch: + description: Optional codex-plan/* branch name + required: false + type: string + pull_request_target: + branches: + - meta + types: + - opened + - reopened + - synchronize + - ready_for_review + +permissions: + actions: read + contents: read + pull-requests: read + +jobs: + refresh: + if: >- + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/codex' && + inputs.operation == 'refresh' + uses: openai/git/.github/workflows/codex.yml@meta + topic_plan_scan: + name: Find one approved topic plan + if: >- + github.event_name == 'schedule' || + (github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/codex' && + inputs.operation == 'scan') + runs-on: ubuntu-24.04 + permissions: + contents: read + pull-requests: read + concurrency: + group: codex-topic-plan-scan + cancel-in-progress: false + outputs: + lane: ${{ steps.reviewed.outputs.lane }} + topic: ${{ steps.reviewed.outputs.topic }} + source_tip: ${{ steps.reviewed.outputs.source_tip }} + review_pr: ${{ steps.reviewed.outputs.review_pr }} + env: + GH_TOKEN: ${{ github.token }} + steps: + - name: Pin trusted meta + id: meta + run: | + set -euo pipefail + test "$GITHUB_REPOSITORY" = openai/git + test "$GITHUB_REF" = refs/heads/codex + sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/meta" \ + --jq .object.sha) + case "$sha" in + ''|*[!0-9a-f]*) exit 1 ;; + esac + test "${#sha}" = 40 + printf 'sha=%s\n' "$sha" >>"$GITHUB_OUTPUT" + + - name: Check out trusted meta + uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 + with: + repository: ${{ github.repository }} + ref: ${{ steps.meta.outputs.sha }} + fetch-depth: 0 + persist-credentials: false + + - name: Find one exact approved topic PR + id: reviewed + env: + META_SHA: ${{ steps.meta.outputs.sha }} + run: | + set -euo pipefail + + die () { + printf '%s\n' "$*" >&2 + exit 1 + } + + test "$GITHUB_REPOSITORY" = openai/git + test "$GITHUB_REF" = refs/heads/codex || + die "topic scan must run from the trusted default branch" + test "$(git rev-parse HEAD)" = "$META_SHA" || + die "trusted checkout does not match pinned meta" + gh auth setup-git + mkdir -p "$RUNNER_TEMP/codex-plan-scan" + for lane in codex codex-unstable + do + case "$lane" in + codex) plan=codex.plan ;; + codex-unstable) plan=codex-unstable.plan ;; + esac + test -f "$plan" || + die "trusted meta has no $plan" + gh pr list --repo "$GITHUB_REPOSITORY" --state open \ + --base "$lane" --limit 1000 \ + --json number,isDraft,headRefName,headRefOid,headRepository,reviewDecision | + jq -r --arg lane "$lane" ' + .[] | + select(.isDraft | not) | + select(.reviewDecision == "APPROVED") | + select(.headRepository.nameWithOwner == "openai/git") | + [$lane, .headRefName, .headRefOid, + (.number | tostring)] | @tsv + ' + done | sort -k4,4n >"$RUNNER_TEMP/codex-plan-scan/candidates" + + while IFS=$'\t' read -r lane topic source_tip review_pr + do + test -n "$review_pr" || continue + case "$review_pr" in + *[!0-9]*) die "approved topic PR has invalid number '$review_pr'" ;; + esac + case "$source_tip" in + *[!0-9a-f]*|'') die "approved topic PR has invalid source SHA" ;; + esac + test "${#source_tip}" = 40 || + die "approved topic PR has invalid source SHA" + git check-ref-format "refs/heads/$topic" >/dev/null 2>&1 || + die "approved topic PR has invalid branch '$topic'" + case "$topic" in + ??/codex/*) ;; + *) continue ;; + esac + suffix=${topic#??/codex/} + case "$suffix" in + ''|*/*|*-wip|*-stale) continue ;; + esac + case "$lane" in + codex) + case "$topic" in + *-unstable) continue ;; + esac + plan=codex.plan + ;; + codex-unstable) + case "$topic" in + *-unstable) ;; + *) continue ;; + esac + plan=codex-unstable.plan + ;; + *) die "approved topic PR has invalid lane '$lane'" ;; + esac + pinned=$(git config --no-includes \ + --file "$plan" \ + --get "branch.$topic.source-tip" || :) + test "$pinned" = "$source_tip" && continue + short=$(printf '%.12s' "$source_tip") + slug=${topic##*/} + plan_branch=codex-plan/$lane-$slug-$short + pending=$(gh pr list --repo "$GITHUB_REPOSITORY" \ + --state open --base meta --head "$plan_branch" \ + --json number --jq '.[0].number // empty') || + die "could not inspect pending Codex plan PR" + test -n "$pending" && continue + if ! sh .github/workflows/codex-branch.sh propose-plan \ + --remote origin --lane "$lane" --topic "$topic" \ + --action auto --source-tip "$source_tip" \ + --review-pr "$review_pr" --expected-meta "$META_SHA" \ + --no-push >/dev/null + then + printf 'skipping approved topic PR #%s: preflight failed\n' \ + "$review_pr" >&2 + continue + fi + { + printf 'lane=%s\n' "$lane" + printf 'topic=%s\n' "$topic" + printf 'source_tip=%s\n' "$source_tip" + printf 'review_pr=%s\n' "$review_pr" + } >>"$GITHUB_OUTPUT" + exit 0 + done <"$RUNNER_TEMP/codex-plan-scan/candidates" + topic_plan_propose: + name: Propose reviewed topic plan + needs: topic_plan_scan + if: needs.topic_plan_scan.outputs.review_pr != '' + permissions: + contents: read + pull-requests: read + uses: openai/git/.github/workflows/codex-plan-propose.yml@meta + with: + lane: ${{ needs.topic_plan_scan.outputs.lane }} + topic: ${{ needs.topic_plan_scan.outputs.topic }} + action: auto + source_tip: ${{ needs.topic_plan_scan.outputs.source_tip }} + review_pr: ${{ needs.topic_plan_scan.outputs.review_pr }} + policy_plan_propose: + name: Propose explicit plan policy + if: >- + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/codex' && + (inputs.operation == 'remove' || inputs.operation == 'reorder') + permissions: + contents: read + pull-requests: read + uses: openai/git/.github/workflows/codex-plan-propose.yml@meta + with: + lane: ${{ inputs.lane }} + topic: ${{ inputs.topic }} + action: ${{ inputs.operation }} + after: ${{ inputs.after }} + plan_branch: ${{ inputs.plan_branch }} + plan_admission: + name: Codex plan admission + if: >- + github.event_name == 'pull_request_target' && + github.event.pull_request.base.ref == 'meta' + permissions: + contents: read + pull-requests: write + uses: openai/git/.github/workflows/codex-plan-admission.yml@meta diff --git a/Documentation/Makefile b/Documentation/Makefile index 2699f0b24af192..ff7cee368e3f7d 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -142,6 +142,7 @@ TECH_DOCS += technical/send-pack-pipeline TECH_DOCS += technical/shallow TECH_DOCS += technical/sparse-checkout TECH_DOCS += technical/sparse-index +TECH_DOCS += technical/status-clean-proof TECH_DOCS += technical/trivial-merge TECH_DOCS += technical/unambiguous-types TECH_DOCS += technical/unit-tests diff --git a/Documentation/config/core.adoc b/Documentation/config/core.adoc index 340329edc38143..79af4e6038f255 100644 --- a/Documentation/config/core.adoc +++ b/Documentation/config/core.adoc @@ -729,6 +729,20 @@ relatively high IO latencies. When enabled, Git will do the index comparison to the filesystem data in parallel, allowing overlapping IO's. Defaults to true. +core.preloadIndexBulk:: + On supported filesystems, scan working tree directories in bulk before + the parallel index preload. ++ +This replaces per-entry filesystem lookups with a physical directory scan, +but may cost more than normal preload depending on filesystem and cache +state. Inconclusive scans are discarded before continuing with the normal +preload. Currently this is supported on APFS, ext-family filesystems, and +XFS, and only has an effect when `core.preloadIndex` is enabled. Defaults +to false, except that a whole-worktree, read-only `git status` may use it +after the native file system monitor discards a legacy +untracked cache. Setting this option explicitly to false also disables +that recovery optimization. + core.unsetenvvars:: Windows-only: comma-separated list of environment variables' names that need to be unset before spawning any other process. diff --git a/Documentation/gitformat-index.adoc b/Documentation/gitformat-index.adoc index f6a427cb495990..047310ec26d105 100644 --- a/Documentation/gitformat-index.adoc +++ b/Documentation/gitformat-index.adoc @@ -366,6 +366,49 @@ The remaining data of each directory block is grouped by type: - An ewah bitmap, the n-th bit indicates whether the n-th index entry is not CE_FSMONITOR_VALID. +== File System Monitor untracked-cache token + + The file system monitor untracked-cache token records the provider + token associated with an untracked-cache snapshot. The signature for + this extension is { 'F', 'S', 'U', 'C' }. + + The extension consists of: + + - 32-bit version number: the current version is 1. + + - A NUL-terminated string containing the opaque file system monitor + token associated with the untracked-cache data. + +== File System Monitor semantic proof + + The file system monitor semantic proof records the configuration and + attribute inputs for a completed worktree-content verification. Its + signature is { 'F', 'S', 'C', 'F' }. + + The extension consists of: + + - 32-bit version number (currently 1). + + - 32-bit magic number identifying version 1 records (`FSC1`). + + - 32-bit flags. The low four bits respectively indicate a complete + attribute manifest, a provider-token binding, a stat-data binding, and + coverage of the full index. All other bits must be zero. + + - 32-bit length of the provider token. + + - 32-bit length of the attribute manifest. + + - The provider token, without a terminating NUL. + + - Three hashes, using the index hash algorithm, over the relevant Git + configuration, semantic-conversion configuration, and attribute state. + + - The attribute manifest described by its length above. + + - A hash over all preceding bytes in this extension, using the index hash + algorithm. + == End of Index Entry The End of Index Entry (EOIE) is used to locate the end of the variable diff --git a/Documentation/technical/meson.build b/Documentation/technical/meson.build index ec07088c57617f..665977299cdab3 100644 --- a/Documentation/technical/meson.build +++ b/Documentation/technical/meson.build @@ -31,6 +31,7 @@ articles = [ 'shallow.adoc', 'sparse-checkout.adoc', 'sparse-index.adoc', + 'status-clean-proof.adoc', 'trivial-merge.adoc', 'unambiguous-types.adoc', 'unit-tests.adoc', diff --git a/Documentation/technical/status-clean-proof.adoc b/Documentation/technical/status-clean-proof.adoc new file mode 100644 index 00000000000000..fb5f24da58a133 --- /dev/null +++ b/Documentation/technical/status-clean-proof.adoc @@ -0,0 +1,184 @@ +Status clean-proof sidecar +========================== + +The status clean-proof sidecar is an optional proof for one empty clean +scan. It can answer the exact empty `git status --porcelain=v2` query, +or let a literal plain `git status` print its live long-format metadata +without repeating the scan. It is never a source of repository state or +cached human-readable output. A missing, stale, malformed, unsupported, +or raced sidecar makes status read the index and scan the worktree +normally. + +Location and scope +------------------ + +The sidecar for an index at `` is stored at `.csts`. This +keeps a proof for one index from being applied to an alternate index. +Sidecars are currently limited to the main worktree, the builtin file +system monitor, and an index and worktree root on local APFS file +systems. + +Readers pass `O_NONBLOCK` and do not follow symbolic links when opening +the sidecar, then accept only regular files no larger than 8192 bytes. +An open or validation failure falls back to ordinary status. Writers use +the normal lockfile protocol. + +Binary format +------------- + +All integers are stored in network byte order. Hashes use the +repository's object-format hash algorithm. Version 1 consists of: + +* Four-byte magic `CSTS`. + +* A 32-bit version number (currently 1). + +* 32-bit flags (currently zero). + +* Fourteen 64-bit fields describing the source index: device, inode, + mode, link count, uid, gid, size, mtime seconds and nanoseconds, ctime + seconds and nanoseconds, birth time seconds and nanoseconds, and + generation. + +* The 32-bit index format version and 32-bit cache-entry count. + +* The index checksum, which is all zero for an `index.skipHash` index, + and the `HEAD` tree object ID. + +* Hashes of status-relevant configuration and repository identity. The + repository identity covers the worktree and Git directory paths, the + worktree root identity, the local APFS identifiers of the held index + and worktree root, external-attribute contents, and locale inputs. + +* One object ID digesting the unique standard-exclude observations in + first-observation order. Each observation contains the source path, + symbolic-link lookup policy, presence, and contents. Transient file + system identities used to make an observation coherent are not part + of the digest. + +* A 32-bit provider-token length followed by the token without a + terminating NUL. Version 1 accepts only builtin-fsmonitor tokens. + +* A hash over all preceding bytes in the sidecar. + +Issuance +-------- + +Only a literal, top-level `status --porcelain=v2` invocation, or a +literal plain `status` after it restored external history, can issue a +sidecar. Status first completes the tracked and untracked scan, +semantic-conversion checks, and provider-token closure. The result must +be empty and use the bulk scanner's complete standard-exclude result. +The index must also contain persistent semantic history from an earlier +scan, so the first scan cannot certify itself. For plain status, +external checkpoint publication is attempted before the physical-index +proof is installed. + +Status then uses the held attribute snapshot and the scanner-sourced +standard-exclude digest, pins the named index, and checks its ordinary +expanded entries, `HEAD`, and the cache tree. External attribute +sources, effective replacement refs, untracked-cache results, and other +unsupported repository or index shapes prevent issuance. A null index +checksum is accepted only when the pinned index is bound by the durable +local-APFS identity used for raced-input checks. + +The sidecar is installed while the index lock remains held and after +the pinned index is rechecked. Status then rolls back the index lock, so +issuing a sidecar does not itself rewrite the index. With optional locks +disabled, status does not issue a sidecar. + +Validation and races +-------------------- + +For either eligible literal command, status attempts validation before +loading the index entries. It checks the bounded sidecar, pins the named +index, and recreates the configuration, attribute, standard-exclude, +repository, and `HEAD` inputs. It then queries the builtin file system +monitor from the recorded token and accepts only an empty delta +response. + +The attribute and exclude snapshots remain held across that query. +Before returning a clean result, status freshly checks configuration, +`HEAD`, uncached replacement refs, the attribute contents and namespace, +the exclude sources, and both the opened and named index identities. +The sidecar's exclude-source opens pass `O_NONBLOCK` and fail closed +when the source cannot be opened and validated; standard-exclude +symbolic links retain their normal lookup behavior. + +An exact porcelain hit produces no output. A plain-status hit refreshes +branch, tracking, and in-progress-operation metadata and passes the +empty tracked and untracked lists to the normal long-status printer. +Either hit reads only the 12-byte index header and the 20- or 32-byte +checksum trailer; for a null trailer, the durable local-APFS identity +is the physical binding. It does not deserialize cache entries, write +the index, replace the sidecar, or advance its provider token. Any +failed check falls through to ordinary status. + +Resumable history checkpoints +----------------------------- + +The physical clean-proof sidecar above is deliberately tied to one +physical index file. A normal, top-level `git status` has a separate +checkpoint store for resuming clean-status history after another Git +implementation has rewritten or re-encoded the index. The store is +consulted only after the ordinary index entries have been read. It +cannot answer a status command by itself, but a successful restore can +publish a new physical proof for the next unchanged-index plain status. +The exact clean porcelain-v2 producer also refreshes this checkpoint +before publishing its physical proof, so both files name the same +provider boundary after a later foreign index rewrite. + +For an index at ``, a checkpoint is stored as +`.csh1.`. The namespace covers the checkpoint +schema, status configuration, semantic inputs, attribute namespace, and +the canonical worktree, Git directory, and common directory paths. It +does not include a Git executable, build prefix, provider token, or +physical index generation. At most eight regular version-1 checkpoint +files are retained next to an index. Each file is limited to 16 MiB; +readers use `O_NONBLOCK`, do not follow symbolic links, and verify an +outer checksum before parsing any section. Publication currently +requires durable index identities on a local APFS file system. + +Each checkpoint is self-contained and co-temporal. It contains: + +* a canonical digest of the ordered logical index entries; + +* the `FSMN` token and dirty bitmap; + +* the serialized untracked cache and its paired `FSUC` token, when an + untracked cache exists; and + +* the `FSCF` semantic proof bound to the same file-system-monitor token. + +The logical index digest includes entry count, path, stage, object ID, +mode, `CE_VALID`, skip-worktree, and intent-to-add state. It excludes +index format, checksum, file identity, cached stat data, and +`CE_FSMONITOR_VALID`. Split and collapsed sparse indexes and +unrecognized transient flags reject matching. A changed logical digest +misses, while publication additionally rejects logical changes during +the command and racy entries. This lets an index-format-only rewrite +find the same logical state without preserving proof across commands +which can change entry membership or persistent flags. + +On a valid hit, status installs all checkpoint sections together in a +scratch index, rechecks the pinned index, and only then replaces the +in-memory acceleration state. It queries the builtin file-system +monitor from the stored token. If the named index already carries a +different usable builtin token, status first requires the stored token +to return a delta; otherwise it keeps the named boundary for the +forward-baseline fallback. A trivial response, provider restart, +malformed section, token mismatch, namespace mismatch, or index race +falls back to ordinary validation. + +Status publishes a replacement checkpoint only after closing the +provider token and validating the semantic proof and paired untracked +cache. Publication uses the normal index lock followed by a per-slot +lockfile, and atomically replaces one namespace slot. When publication +succeeds, status rolls back the pending acceleration-only index update. +After an external restore, status also leaves the main index untouched +if republication fails, so one proof namespace is not copied over +another implementation's index extensions. Only literal normal status +and the exact clean porcelain-v2 producer enable this lane; commands +capable of logical index changes retain the normal index-writing path. +Optional-lock-free commands neither publish checkpoints nor use this +rollback path. diff --git a/Makefile b/Makefile index fac3e8879c8377..5766c2c9226990 100644 --- a/Makefile +++ b/Makefile @@ -413,6 +413,9 @@ include shared.mak # `compat/fsmonitor/fsm-health-.c` files # that implement the `fsm_listen__*()` and `fsm_health__*()` routines. # +# If a platform supports bulk worktree scans during index preload, set +# PRELOAD_INDEX_BULK_BACKEND to the name of its backend. +# # If your platform has OS-specific ways to tell if a repo is incompatible with # fsmonitor (whether the hook or IPC daemon version), set FSMONITOR_OS_SETTINGS # to the "" of the corresponding `compat/fsmonitor/fsm-settings-.c` @@ -866,6 +869,7 @@ TEST_BUILTINS_OBJS += test-repository.o TEST_BUILTINS_OBJS += test-revision-walking.o TEST_BUILTINS_OBJS += test-run-command.o TEST_BUILTINS_OBJS += test-scrap-cache-tree.o +TEST_BUILTINS_OBJS += test-semantic-verify.o TEST_BUILTINS_OBJS += test-serve-v2.o TEST_BUILTINS_OBJS += test-sha1.o TEST_BUILTINS_OBJS += test-sha256.o @@ -1109,6 +1113,8 @@ LIB_OBJS += archive-tar.o LIB_OBJS += archive-zip.o LIB_OBJS += archive.o LIB_OBJS += attr.o +LIB_OBJS += attr-fingerprint.o +LIB_OBJS += attr-manifest.o LIB_OBJS += base85.o LIB_OBJS += bisect.o LIB_OBJS += blame.o @@ -1122,6 +1128,17 @@ LIB_OBJS += cbtree.o LIB_OBJS += chdir-notify.o LIB_OBJS += checkout.o LIB_OBJS += chunk-format.o +LIB_OBJS += clean-status.o +LIB_OBJS += clean-status-config.o +LIB_OBJS += clean-status-epoch.o +LIB_OBJS += clean-status-history-store.o +LIB_OBJS += clean-status-history.o +LIB_OBJS += clean-status-identity.o +LIB_OBJS += clean-status-index.o +LIB_OBJS += clean-status-manifest.o +LIB_OBJS += clean-status-sidecar.o +LIB_OBJS += clean-status-fast.o +LIB_OBJS += clean-status-sidecar-issue.o LIB_OBJS += color.o LIB_OBJS += column.o LIB_OBJS += combine-diff.o @@ -1167,12 +1184,14 @@ LIB_OBJS += ewah/bitmap.o LIB_OBJS += ewah/ewah_bitmap.o LIB_OBJS += ewah/ewah_io.o LIB_OBJS += ewah/ewah_rlw.o +LIB_OBJS += exclude-source-proof.o LIB_OBJS += exec-cmd.o LIB_OBJS += fetch-negotiator.o LIB_OBJS += fetch-object-info.o LIB_OBJS += fetch-pack.o LIB_OBJS += fmt-merge-msg.o LIB_OBJS += fsck.o +LIB_OBJS += fsmonitor-clean-proof.o LIB_OBJS += fsmonitor.o LIB_OBJS += fsmonitor-ipc.o LIB_OBJS += fsmonitor-settings.o @@ -1255,6 +1274,7 @@ LIB_OBJS += parse-options.o LIB_OBJS += patch-delta.o LIB_OBJS += patch-ids.o LIB_OBJS += path.o +LIB_OBJS += path-namespace.o LIB_OBJS += path-walk.o LIB_OBJS += pathspec.o LIB_OBJS += pkt-line.o @@ -1316,6 +1336,11 @@ LIB_OBJS += resolve-undo.o LIB_OBJS += revision.o LIB_OBJS += run-command.o LIB_OBJS += send-pack.o +LIB_OBJS += semantic-verify-file.o +LIB_OBJS += semantic-verify-path.o +LIB_OBJS += semantic-verify-root.o +LIB_OBJS += semantic-verify-worker.o +LIB_OBJS += semantic-verify.o LIB_OBJS += sequencer.o LIB_OBJS += serve.o LIB_OBJS += server-info.o @@ -1374,9 +1399,20 @@ LIB_OBJS += versioncmp.o LIB_OBJS += walker.o LIB_OBJS += wildmatch.o LIB_OBJS += worktree.o +LIB_OBJS += worktree-attr-manifest.o +LIB_OBJS += worktree-attr-source.o LIB_OBJS += wrapper.o LIB_OBJS += write-or-die.o LIB_OBJS += ws.o +ifdef PRELOAD_INDEX_BULK_BACKEND +BASIC_CFLAGS += -DHAVE_PRELOAD_INDEX_BULK +PRELOAD_INDEX_BULK_OBJS += preload-index-bulk-index.o +PRELOAD_INDEX_BULK_OBJS += preload-index-bulk-thread.o +PRELOAD_INDEX_BULK_OBJS += preload-index-bulk.o +PRELOAD_INDEX_BULK_OBJS += compat/preload-index/bulk-$(PRELOAD_INDEX_BULK_BACKEND).o +PRELOAD_INDEX_BULK_OBJS += $(PRELOAD_INDEX_BULK_PLATFORM_OBJS) +endif +LIB_OBJS += $(PRELOAD_INDEX_BULK_OBJS) LIB_OBJS += wt-status.o LIB_OBJS += xdiff-interface.o LIB_OBJS += xdiff/xdiffi.o @@ -1535,9 +1571,24 @@ THIRD_PARTY_SOURCES += sha1dc/% THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/% THIRD_PARTY_SOURCES += $(UNIT_TEST_DIR)/clar/clar/% +CLAR_TEST_SUITES += u-attr-fingerprint +CLAR_TEST_SUITES += u-attr-manifest +CLAR_TEST_SUITES += u-clean-status-config +CLAR_TEST_SUITES += u-clean-status-history +CLAR_TEST_SUITES += u-clean-status-history-store +CLAR_TEST_SUITES += u-clean-status-identity +CLAR_TEST_SUITES += u-clean-status-index +CLAR_TEST_SUITES += u-clean-status-manifest +CLAR_TEST_SUITES += u-clean-status-progress +CLAR_TEST_SUITES += u-clean-status-sidecar +CLAR_TEST_SUITES += u-clean-status-store CLAR_TEST_SUITES += u-ctype CLAR_TEST_SUITES += u-dir CLAR_TEST_SUITES += u-example-decorate +CLAR_TEST_SUITES += u-exclude-source-proof +CLAR_TEST_SUITES += u-fsmonitor-attributes +CLAR_TEST_SUITES += u-fsmonitor-clean-proof +CLAR_TEST_SUITES += u-fsmonitor-response CLAR_TEST_SUITES += u-hash CLAR_TEST_SUITES += u-hashmap CLAR_TEST_SUITES += u-list-objects-filter-options @@ -1546,7 +1597,9 @@ CLAR_TEST_SUITES += u-odb-inmemory CLAR_TEST_SUITES += u-oid-array CLAR_TEST_SUITES += u-oidmap CLAR_TEST_SUITES += u-oidtree +CLAR_TEST_SUITES += u-path-namespace CLAR_TEST_SUITES += u-prio-queue +CLAR_TEST_SUITES += u-preload-index-bulk-darwin CLAR_TEST_SUITES += u-reftable-basics CLAR_TEST_SUITES += u-reftable-block CLAR_TEST_SUITES += u-reftable-merged @@ -1562,6 +1615,7 @@ CLAR_TEST_SUITES += u-strvec CLAR_TEST_SUITES += u-trailer CLAR_TEST_SUITES += u-urlmatch-normalization CLAR_TEST_SUITES += u-utf8-width +CLAR_TEST_SUITES += u-worktree-attr-source CLAR_TEST_PROG = $(UNIT_TEST_BIN)/unit-tests$(X) CLAR_TEST_OBJS = $(patsubst %,$(UNIT_TEST_DIR)/%.o,$(CLAR_TEST_SUITES)) CLAR_TEST_OBJS += $(UNIT_TEST_DIR)/clar/clar.o diff --git a/add-interactive.c b/add-interactive.c index 3cf8a1dbf85e3f..f22b0c83e73ccc 100644 --- a/add-interactive.c +++ b/add-interactive.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "add-interactive.h" +#include "clean-status.h" #include "color.h" #include "diffcore.h" #include "gettext.h" @@ -1123,6 +1124,7 @@ int run_add_i(struct repository *r, const struct pathspec *ps, _("staged"), _("unstaged"), _("path")); opts.list_opts.header = header.buf; + clean_status_prepare_main_index_history(r); discard_index(r->index); if (repo_read_index(r) < 0 || repo_refresh_and_write_index(r, REFRESH_QUIET, 0, 1, diff --git a/add-patch.c b/add-patch.c index f27edcbe8d4151..d8256e3ba752cc 100644 --- a/add-patch.c +++ b/add-patch.c @@ -4,6 +4,7 @@ #include "git-compat-util.h" #include "add-patch.h" #include "advice.h" +#include "clean-status.h" #include "commit.h" #include "config.h" #include "diff.h" @@ -2080,6 +2081,7 @@ int run_add_p(struct repository *r, enum add_p_mode mode, s.mode = &patch_mode_add; s.revision = revision; + clean_status_prepare_main_index_history(r); discard_index(r->index); if (repo_read_index(r) < 0 || (!s.mode->index_only && diff --git a/apply.c b/apply.c index f00b7ba4d3a7e6..cb058f92390250 100644 --- a/apply.c +++ b/apply.c @@ -13,12 +13,15 @@ #include "git-compat-util.h" #include "abspath.h" #include "base85.h" +#include "clean-status.h" +#include "clean-status-index.h" #include "config.h" #include "odb.h" #include "delta.h" #include "diff.h" #include "dir.h" #include "environment.h" +#include "fsmonitor-settings.h" #include "gettext.h" #include "hex.h" #include "xdiff-interface.h" @@ -31,6 +34,7 @@ #include "path.h" #include "quote.h" #include "read-cache.h" +#include "replace-object.h" #include "repository.h" #include "rerere.h" #include "apply.h" @@ -3539,8 +3543,9 @@ static int verify_index_match(struct apply_state *state, return -1; return 0; } - return ie_match_stat(state->repo->index, ce, st, - CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); + return ie_match_stat_with_content_check( + state->repo->index, ce, st, + CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); } #define SUBMODULE_PATCH_WITHOUT_INDEX 1 @@ -4439,9 +4444,79 @@ static void patch_stats(struct apply_state *state, struct patch *patch) } } +static int patch_preserves_clean_history(struct apply_state *state, + struct patch *patch) +{ + struct index_state *istate = state->repo->index; + const struct cache_entry *old; + int suspended = clean_status_fsmonitor_backoff_suspended(istate); + int pos; + + if (!state->update_index || state->ita_only || state->threeway || + state->apply_with_reject || state->fake_ancestor || + state->index_file || !fstat_is_reliable() || + (getenv(INDEX_ENVIRONMENT) && + !clean_status_index_path_is_main(istate->repo, + istate->repo->index_file)) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || + getenv(DB_ENVIRONMENT) || getenv(ALTERNATE_DB_ENVIRONMENT) || + istate != istate->repo->index || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + repo_config_values(istate->repo)->apply_sparse_checkout || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat || + (fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC && + !suspended) || + repo_has_replace_refs_uncached(istate->repo) || + patch->is_new > 0 || patch->is_delete > 0 || patch->is_copy || + patch->is_rename || patch->conflicted_threeway || + !patch->old_name || !patch->new_name || + strcmp(patch->old_name, patch->new_name) || + !S_ISREG(patch->old_mode) || !S_ISREG(patch->new_mode) || + create_ce_mode(patch->old_mode) != + create_ce_mode(patch->new_mode) || + !clean_status_external_history_enabled(istate) || + !istate->untracked || + !istate->untracked->root) + return 0; + if (suspended) { + /* Keep only the authenticated historical boundary during backoff. */ + if (!clean_status_fsmonitor_semantic_baseline_pending(istate) || + !istate->untracked->root->valid || + !istate->untracked->fsmonitor_revalidation) + return 0; + } else if (!clean_status_has_persistent_fsmonitor_semantic_history(istate) || + !clean_status_revalidated_token_matches(istate) || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_untracked_valid || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + !istate->fsmonitor_last_update || + !istate->fsmonitor_untracked_token || + strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token) || + !istate->untracked->use_fsmonitor) { + return 0; + } + + pos = index_name_pos(istate, patch->old_name, + strlen(patch->old_name)); + if (pos < 0) + return 0; + old = istate->cache[pos]; + return S_ISREG(old->ce_mode) && + old->ce_mode == create_ce_mode(patch->new_mode) && + clean_status_index_entry_is_semantically_safe( + istate, old, old); +} + static int remove_file(struct apply_state *state, struct patch *patch, int rmdir_empty) { - if (state->update_index && !state->ita_only) { + if (state->update_index && !state->ita_only && + !patch_preserves_clean_history(state, patch)) { + if (clean_status_external_history_enabled(state->repo->index)) + clean_status_invalidate_current_proof(state->repo->index); if (remove_file_from_index(state->repo->index, patch->old_name) < 0) return error(_("unable to remove %s from index"), patch->old_name); } @@ -4454,6 +4529,7 @@ static int remove_file(struct apply_state *state, struct patch *patch, int rmdir } static int add_index_file(struct apply_state *state, + struct patch *patch, const char *path, unsigned mode, void *buf, @@ -4462,6 +4538,7 @@ static int add_index_file(struct apply_state *state, struct stat st; struct cache_entry *ce; int namelen = strlen(path); + int options = ADD_CACHE_OK_TO_ADD; ce = make_empty_cache_entry(state->repo->index, namelen); memcpy(ce->name, path, namelen); @@ -4496,7 +4573,13 @@ static int add_index_file(struct apply_state *state, "for newly created file %s"), path); } } - if (add_index_entry(state->repo->index, ce, ADD_CACHE_OK_TO_ADD) < 0) { + if (patch_preserves_clean_history(state, patch)) { + options |= ADD_CACHE_OK_TO_REPLACE | + ADD_CACHE_PRESERVE_CLEAN_HISTORY; + } else if (clean_status_external_history_enabled(state->repo->index)) { + clean_status_invalidate_current_proof(state->repo->index); + } + if (add_index_entry(state->repo->index, ce, options) < 0) { discard_cache_entry(ce); return error(_("unable to add cache entry for %s"), path); } @@ -4696,7 +4779,7 @@ static int create_file(struct apply_state *state, struct patch *patch) if (patch->conflicted_threeway) return add_conflicted_stages_file(state, patch); else if (state->check_index || (state->ita_only && patch->is_new > 0)) - return add_index_file(state, path, mode, buf, size); + return add_index_file(state, patch, path, mode, buf, size); return 0; } @@ -4827,6 +4910,18 @@ static int write_out_results(struct apply_state *state, struct patch *list) struct patch *l; struct string_list cpath = STRING_LIST_INIT_DUP; + if (state->update_index && + clean_status_external_history_enabled(state->repo->index)) { + for (l = list; l; l = l->next) { + if (l->rejected || + !patch_preserves_clean_history(state, l)) { + clean_status_invalidate_current_proof( + state->repo->index); + break; + } + } + } + for (phase = 0; phase < 2; phase++) { l = list; while (l) { diff --git a/attr-fingerprint.c b/attr-fingerprint.c new file mode 100644 index 00000000000000..4db09c16509fc7 --- /dev/null +++ b/attr-fingerprint.c @@ -0,0 +1,382 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "attr-fingerprint.h" +#include "attr.h" +#include "environment.h" +#include "hash-framing.h" +#include "path.h" +#include "path-namespace.h" +#include "repository.h" +#include "strbuf.h" +#include "wrapper.h" + +struct attr_source_snapshot_entry { + char *path; + char *buf; + size_t len; +}; + +struct attr_source_snapshot { + struct attr_fingerprint fingerprint; + struct attr_source_snapshot_entry sources[ATTR_SOURCE_SNAPSHOT_NR]; +}; + +static int open_attr_source(const char *path) +{ +#ifdef O_NONBLOCK + return git_open_cloexec(path, O_RDONLY | O_NONBLOCK); +#else + (void)path; + errno = ENOSYS; + return -1; +#endif +} + +static int hash_source(struct git_hash_ctx *content_ctx, + struct git_hash_ctx *namespace_ctx, + struct git_hash_ctx *portable_namespace_ctx, + const struct attr_fingerprint_source *source, + int *present, + struct attr_source_snapshot_entry *snapshot) +{ + struct path_namespace_snapshot *before = NULL, *after = NULL; + struct stat opened_before, opened_after, named; + struct strbuf normalized = STRBUF_INIT; + char *absolute = NULL; + char *buf = NULL; + ssize_t got; + size_t size; + uint32_t state; + int fd = -1, ret = -1; + char extra; + + hash_optional_cstring(namespace_ctx, source->path); + put_be32(&state, source->enabled); + hash_length_delimited(namespace_ctx, &state, sizeof(state)); + hash_length_delimited(portable_namespace_ctx, &state, sizeof(state)); + *present = 0; + if (!source->enabled || !source->path || !*source->path) { + hash_optional_cstring(content_ctx, NULL); + hash_length_delimited(content_ctx, &state, sizeof(state)); + state = 0; + hash_length_delimited(content_ctx, &state, sizeof(state)); + hash_length_delimited(portable_namespace_ctx, &state, + sizeof(state)); + return 0; + } + + absolute = absolute_pathdup(source->path); + strbuf_addstr(&normalized, absolute); + if (strbuf_normalize_path(&normalized) || + path_namespace_capture(normalized.buf, &before)) + goto done; + *present = path_namespace_target_present(before); + hash_optional_cstring(content_ctx, + *present ? source->path : NULL); + put_be32(&state, source->enabled); + hash_length_delimited(content_ctx, &state, sizeof(state)); + if (!*present) { + if (path_namespace_capture(normalized.buf, &after) || + !path_namespace_equal(before, after)) + goto done; + state = 0; + hash_length_delimited(content_ctx, &state, sizeof(state)); + hash_length_delimited(portable_namespace_ctx, &state, + sizeof(state)); + path_namespace_hash(namespace_ctx, before); + ret = 0; + goto done; + } + + fd = open_attr_source(normalized.buf); + if (fd < 0 || fstat(fd, &opened_before) || + !S_ISREG(opened_before.st_mode) || opened_before.st_nlink != 1 || + opened_before.st_size < 0 || + opened_before.st_size >= ATTR_MAX_FILE_SIZE) + goto done; + size = xsize_t(opened_before.st_size); + buf = xmalloc(size ? size : 1); + got = read_in_full(fd, buf, size); + if (got < 0 || (size_t)got != size || read(fd, &extra, 1) != 0 || + fstat(fd, &opened_after) || stat(normalized.buf, &named) || + !path_namespace_stat_equal(&opened_before, &opened_after) || + !path_namespace_stat_equal(&opened_after, &named) || + path_namespace_capture(normalized.buf, &after) || + !path_namespace_equal(before, after)) + goto done; + state = 1; + hash_length_delimited(content_ctx, &state, sizeof(state)); + hash_length_delimited(portable_namespace_ctx, &state, + sizeof(state)); + hash_optional_cstring(portable_namespace_ctx, source->path); + path_namespace_hash(namespace_ctx, before); + path_namespace_hash(portable_namespace_ctx, before); + path_namespace_hash_stat(namespace_ctx, &opened_after); + path_namespace_hash_stat(portable_namespace_ctx, &opened_after); + hash_length_delimited(content_ctx, buf, size); + hash_length_delimited(portable_namespace_ctx, buf, size); + if (snapshot) { + snapshot->path = xstrdup(source->path); + snapshot->buf = buf; + snapshot->len = size; + buf = NULL; + } + ret = 0; +done: + if (fd >= 0) + close(fd); + free(buf); + free(absolute); + path_namespace_clear(before); + path_namespace_clear(after); + strbuf_release(&normalized); + return ret; +} + +static int fingerprint_sources( + const struct attr_fingerprint_source *sources, size_t nr, + const struct git_hash_algo *algo, struct attr_fingerprint *result, + struct attr_source_snapshot *snapshot) +{ + struct git_hash_ctx content_ctx, namespace_ctx, portable_namespace_ctx; + uint32_t count; + + if (snapshot && nr != ARRAY_SIZE(snapshot->sources)) + BUG("attribute snapshot source count mismatch"); + memset(result, 0, sizeof(*result)); + git_hash_init(&content_ctx, algo); + git_hash_init(&namespace_ctx, algo); + git_hash_init(&portable_namespace_ctx, algo); + hash_optional_cstring(&content_ctx, "attribute-source-content-v1"); + hash_optional_cstring(&namespace_ctx, + "attribute-source-namespace-v1"); + hash_optional_cstring(&portable_namespace_ctx, + "attribute-source-portable-namespace-v1"); + if (nr > UINT32_MAX) + return -1; + put_be32(&count, nr); + hash_length_delimited(&content_ctx, &count, sizeof(count)); + hash_length_delimited(&namespace_ctx, &count, sizeof(count)); + hash_length_delimited(&portable_namespace_ctx, &count, sizeof(count)); + for (size_t i = 0; i < nr; i++) { + int present; + struct attr_source_snapshot_entry *entry = + snapshot ? &snapshot->sources[i] : NULL; + + if (hash_source(&content_ctx, &namespace_ctx, + &portable_namespace_ctx, &sources[i], &present, + entry)) + return -1; + result->sources_present |= present; + } + git_hash_final(result->content_hash, &content_ctx); + git_hash_final(result->namespace_hash, &namespace_ctx); + git_hash_final(result->portable_namespace_hash, + &portable_namespace_ctx); + return 0; +} + +int attr_fingerprint_sources( + const struct attr_fingerprint_source *sources, size_t nr, + const struct git_hash_algo *algo, struct attr_fingerprint *result) +{ + return fingerprint_sources(sources, nr, algo, result, NULL); +} + +static int repository_sources(struct repository *repo, + struct attr_fingerprint_source *sources, + char **info_attributes) +{ + if (getenv(GIT_ATTR_SOURCE_ENVIRONMENT)) + return -1; + sources[0].path = git_attr_system_file(); + sources[0].enabled = git_attr_system_is_enabled(); + sources[1].path = git_attr_global_file(); + sources[1].enabled = 1; + *info_attributes = repo_git_path(repo, INFOATTRIBUTES_FILE); + sources[2].path = *info_attributes; + sources[2].enabled = 1; + return 0; +} + +int attr_fingerprint_repository(struct repository *repo, + struct attr_fingerprint *result) +{ + struct attr_fingerprint_source sources[ATTR_SOURCE_SNAPSHOT_NR]; + char *info_attributes = NULL; + int ret; + + memset(result, 0, sizeof(*result)); + if (repository_sources(repo, sources, &info_attributes)) + return -1; + ret = attr_fingerprint_sources(sources, ARRAY_SIZE(sources), + repo->hash_algo, result); + free(info_attributes); + return ret; +} + +static int legacy_absent_path_is_stable(const char *path) +{ + struct path_namespace_snapshot *before = NULL, *after = NULL; + struct strbuf normalized = STRBUF_INIT; + char *absolute = NULL; + int stable = 0; + + if (!path || !*path) + return 0; + absolute = absolute_pathdup(path); + strbuf_addstr(&normalized, absolute); + if (!strbuf_normalize_path(&normalized) && + !path_namespace_capture(normalized.buf, &before) && + !path_namespace_target_present(before) && + !path_namespace_capture(normalized.buf, &after) && + !path_namespace_target_present(after) && + path_namespace_equal(before, after)) + stable = 1; + free(absolute); + path_namespace_clear(before); + path_namespace_clear(after); + strbuf_release(&normalized); + return stable; +} + +static void legacy_absent_source_hash( + const struct attr_fingerprint_source *sources, + const char *system_path, const struct git_hash_algo *algo, + unsigned char *hash) +{ + struct git_hash_ctx ctx; + uint32_t value; + + git_hash_init(&ctx, algo); + hash_optional_cstring(&ctx, "attribute-source-content-v1"); + put_be32(&value, ATTR_SOURCE_SNAPSHOT_NR); + hash_length_delimited(&ctx, &value, sizeof(value)); + for (size_t i = 0; i < ATTR_SOURCE_SNAPSHOT_NR; i++) { + const char *path = i == ATTR_SOURCE_SNAPSHOT_SYSTEM ? + system_path : sources[i].path; + + hash_optional_cstring(&ctx, path); + put_be32(&value, sources[i].enabled); + hash_length_delimited(&ctx, &value, sizeof(value)); + if (!sources[i].enabled || !path) + continue; + put_be32(&value, 0); + hash_length_delimited(&ctx, &value, sizeof(value)); + } + git_hash_final(hash, &ctx); +} + +int attr_fingerprint_matches_legacy_absent_sources( + struct repository *repo, const unsigned char *expected) +{ + static const char shipped_system_path[] = "//etc/gitattributes"; + struct attr_fingerprint_source sources[ATTR_SOURCE_SNAPSHOT_NR]; + struct attr_fingerprint before, after; + unsigned char legacy[GIT_MAX_RAWSZ]; + char *info_attributes = NULL; + int matches = 0; + + if (!expected || !fstat_is_reliable() || + repository_sources(repo, sources, &info_attributes) || + !sources[ATTR_SOURCE_SNAPSHOT_SYSTEM].enabled || + attr_fingerprint_repository(repo, &before) || + before.sources_present) + goto done; + legacy_absent_source_hash( + sources, sources[ATTR_SOURCE_SNAPSHOT_SYSTEM].path, + repo->hash_algo, legacy); + if (!memcmp(legacy, expected, repo->hash_algo->rawsz)) { + matches = 1; + } else if (legacy_absent_path_is_stable(shipped_system_path)) { + legacy_absent_source_hash( + sources, shipped_system_path, repo->hash_algo, legacy); + matches = !memcmp(legacy, expected, repo->hash_algo->rawsz); + } + if (!matches || attr_fingerprint_repository(repo, &after) || + after.sources_present || + memcmp(before.content_hash, after.content_hash, + repo->hash_algo->rawsz) || + memcmp(before.namespace_hash, after.namespace_hash, + repo->hash_algo->rawsz)) + matches = 0; + +done: + free(info_attributes); + return matches; +} + +int attr_source_snapshot_repository(struct repository *repo, + struct attr_source_snapshot **result) +{ + struct attr_fingerprint_source sources[ATTR_SOURCE_SNAPSHOT_NR]; + struct attr_source_snapshot *snapshot; + char *info_attributes = NULL; + + if (!result) + BUG("attr_source_snapshot_repository requires an output"); + *result = NULL; + if (repository_sources(repo, sources, &info_attributes)) + return -1; + CALLOC_ARRAY(snapshot, 1); + if (fingerprint_sources(sources, ARRAY_SIZE(sources), repo->hash_algo, + &snapshot->fingerprint, snapshot)) { + attr_source_snapshot_free(snapshot); + free(info_attributes); + return -1; + } + free(info_attributes); + *result = snapshot; + return 0; +} + +int attr_source_snapshot_matches_repository( + struct repository *repo, + const struct attr_source_snapshot *snapshot) +{ + struct attr_fingerprint current; + + return snapshot && + !attr_fingerprint_repository(repo, ¤t) && + current.sources_present == + snapshot->fingerprint.sources_present && + !memcmp(current.content_hash, + snapshot->fingerprint.content_hash, + repo->hash_algo->rawsz); +} + +const struct attr_fingerprint *attr_source_snapshot_fingerprint( + const struct attr_source_snapshot *snapshot) +{ + return snapshot ? &snapshot->fingerprint : NULL; +} + +int attr_source_snapshot_read( + const struct attr_source_snapshot *snapshot, + enum attr_source_snapshot_kind kind, + const char **path, const char **buf, size_t *len) +{ + const struct attr_source_snapshot_entry *source; + + if (!snapshot || kind >= ATTR_SOURCE_SNAPSHOT_NR || + !path || !buf || !len) + BUG("invalid attribute snapshot read"); + source = &snapshot->sources[kind]; + if (!source->buf) + return 0; + *path = source->path; + *buf = source->buf; + *len = source->len; + return 1; +} + +void attr_source_snapshot_free(struct attr_source_snapshot *snapshot) +{ + if (!snapshot) + return; + for (size_t i = 0; i < ARRAY_SIZE(snapshot->sources); i++) { + free(snapshot->sources[i].path); + free(snapshot->sources[i].buf); + } + free(snapshot); +} diff --git a/attr-fingerprint.h b/attr-fingerprint.h new file mode 100644 index 00000000000000..5d2bedf93d38a4 --- /dev/null +++ b/attr-fingerprint.h @@ -0,0 +1,49 @@ +#ifndef ATTR_FINGERPRINT_H +#define ATTR_FINGERPRINT_H + +#include "hash.h" + +struct repository; + +struct attr_fingerprint_source { + const char *path; + unsigned int enabled : 1; +}; + +struct attr_fingerprint { + unsigned char content_hash[GIT_MAX_RAWSZ]; + unsigned char namespace_hash[GIT_MAX_RAWSZ]; + unsigned char portable_namespace_hash[GIT_MAX_RAWSZ]; + unsigned int sources_present : 1; +}; + +enum attr_source_snapshot_kind { + ATTR_SOURCE_SNAPSHOT_SYSTEM, + ATTR_SOURCE_SNAPSHOT_GLOBAL, + ATTR_SOURCE_SNAPSHOT_INFO, + ATTR_SOURCE_SNAPSHOT_NR, +}; + +struct attr_source_snapshot; + +int attr_fingerprint_sources( + const struct attr_fingerprint_source *sources, size_t nr, + const struct git_hash_algo *algo, struct attr_fingerprint *result); +int attr_fingerprint_repository(struct repository *repo, + struct attr_fingerprint *result); +int attr_fingerprint_matches_legacy_absent_sources( + struct repository *repo, const unsigned char *expected); +int attr_source_snapshot_repository(struct repository *repo, + struct attr_source_snapshot **result); +int attr_source_snapshot_matches_repository( + struct repository *repo, + const struct attr_source_snapshot *snapshot); +const struct attr_fingerprint *attr_source_snapshot_fingerprint( + const struct attr_source_snapshot *snapshot); +int attr_source_snapshot_read( + const struct attr_source_snapshot *snapshot, + enum attr_source_snapshot_kind kind, + const char **path, const char **buf, size_t *len); +void attr_source_snapshot_free(struct attr_source_snapshot *snapshot); + +#endif /* ATTR_FINGERPRINT_H */ diff --git a/attr-manifest.c b/attr-manifest.c new file mode 100644 index 00000000000000..6c835f26cb410c --- /dev/null +++ b/attr-manifest.c @@ -0,0 +1,313 @@ +#include "git-compat-util.h" +#include "attr.h" +#include "attr-manifest.h" +#include "environment.h" +#include "read-cache-ll.h" +#include "strbuf.h" + +/* + * A manifest begins with a 32-bit entry count. Each entry contains a 32-bit + * path length, four bytes of source metadata, an object-format hash, and the + * unterminated path. Paths are strictly increasing. + */ +static int attr_manifest_path_valid(const unsigned char *path, size_t len) +{ + const char *base; + char *copy; + int valid; + + if (!len || path[0] == '/' || memchr(path, '\0', len)) + return 0; + copy = xmemdupz(path, len); + base = strrchr(copy, '/'); + base = base ? base + 1 : copy; + valid = !strcmp(base, GITATTRIBUTES_FILE) && + verify_path(copy, S_IFREG | 0644); + free(copy); + return valid; +} + +static int attr_manifest_entry_cmp(const struct attr_manifest_entry *a, + const struct attr_manifest_entry *b) +{ + size_t common = a->path_len < b->path_len ? a->path_len : b->path_len; + int cmp = memcmp(a->path, b->path, common); + + if (cmp) + return cmp; + return a->path_len < b->path_len ? -1 : a->path_len > b->path_len; +} + +static int attr_manifest_entry_equal(const struct attr_manifest_entry *a, + const struct attr_manifest_entry *b, + const struct git_hash_algo *algo) +{ + return !attr_manifest_entry_cmp(a, b) && a->source == b->source && + !memcmp(a->hash, b->hash, algo->rawsz); +} + +static void release_parsed_attr(struct match_attr *match) +{ + for (size_t i = 0; i < match->num_attr; i++) { + const char *value = match->state[i].setto; + + if (!ATTR_TRUE(value) && !ATTR_FALSE(value) && + !ATTR_UNSET(value)) + free((char *)value); + } + free(match); +} + +static int normalize_conversion_attributes( + const char *data, size_t len, struct strbuf *normalized) +{ + struct strbuf line = STRBUF_INIT; + size_t offset = 0; + int lineno = 0, ret = -1; + + if ((!data && len) || memchr(data, '\0', len)) + goto done; + while (offset < len) { + const char *start = data + offset; + const char *newline = memchr(start, '\n', len - offset); + const char *trimmed; + struct match_attr *match; + size_t line_len = newline ? + (size_t)(newline - start) + 1 : len - offset; + int display_only; + + strbuf_reset(&line); + strbuf_add(&line, start, line_len); + trimmed = line.buf + strspn(line.buf, " \t\r\n"); + lineno++; + if (!*trimmed || *trimmed == '#') { + strbuf_add(normalized, start, line_len); + offset += line_len; + continue; + } + if (starts_with(trimmed, ATTRIBUTE_MACRO_PREFIX)) + goto done; + match = parse_attr_line(line.buf, GITATTRIBUTES_FILE, lineno, 0); + if (!match || match->is_macro || !match->num_attr) { + if (match) + release_parsed_attr(match); + goto done; + } + display_only = 1; + for (size_t i = 0; i < match->num_attr; i++) + if (strcmp(git_attr_name(match->state[i].attr), + "linguist-generated")) + display_only = 0; + release_parsed_attr(match); + if (!display_only) + strbuf_add(normalized, start, line_len); + offset += line_len; + } + ret = 0; + +done: + strbuf_release(&line); + return ret; +} + +int attr_manifest_only_linguist_generated_changed( + const char *old_data, size_t old_len, + const char *new_data, size_t new_len) +{ + struct strbuf old = STRBUF_INIT, new = STRBUF_INIT; + int equal = 0; + + if (!normalize_conversion_attributes(old_data, old_len, &old) && + !normalize_conversion_attributes(new_data, new_len, &new)) + equal = old.len == new.len && + !memcmp(old.buf, new.buf, old.len); + strbuf_release(&old); + strbuf_release(&new); + return equal; +} + +void attr_manifest_writer_init(struct attr_manifest_writer *writer, + struct strbuf *buf, + const struct git_hash_algo *algo) +{ + uint32_t count; + + if (!algo) + BUG("attribute manifest requires a hash algorithm"); + memset(writer, 0, sizeof(*writer)); + writer->buf = buf; + writer->algo = algo; + strbuf_reset(buf); + put_be32(&count, 0); + strbuf_add(buf, &count, sizeof(count)); +} + +int attr_manifest_writer_add(struct attr_manifest_writer *writer, + const char *path, + enum attr_manifest_source source, + const unsigned char *hash) +{ + struct attr_manifest_entry previous, current; + unsigned char metadata[4] = { source, 0, 0, 0 }; + uint32_t path_len_be; + size_t entry_offset, path_len = strlen(path); + + if (!writer->buf || !writer->algo || !hash || !path_len || + path_len > UINT32_MAX || writer->nr == UINT32_MAX || + (source != ATTR_MANIFEST_WORKTREE && + source != ATTR_MANIFEST_INDEX) || + !attr_manifest_path_valid((const unsigned char *)path, path_len)) + return -1; + + current.path = (const unsigned char *)path; + current.path_len = path_len; + if (writer->nr) { + previous.path = (const unsigned char *)writer->buf->buf + + writer->last_path_offset; + previous.path_len = writer->last_path_len; + if (attr_manifest_entry_cmp(&previous, ¤t) >= 0) + return -1; + } + + entry_offset = writer->buf->len; + put_be32(&path_len_be, path_len); + strbuf_add(writer->buf, &path_len_be, sizeof(path_len_be)); + strbuf_add(writer->buf, metadata, sizeof(metadata)); + strbuf_add(writer->buf, hash, writer->algo->rawsz); + strbuf_add(writer->buf, path, path_len); + writer->last_path_offset = entry_offset + sizeof(path_len_be) + + sizeof(metadata) + writer->algo->rawsz; + writer->last_path_len = path_len; + put_be32(writer->buf->buf, ++writer->nr); + return 0; +} + +int attr_manifest_cursor_init(struct attr_manifest_cursor *cursor, + const void *data, size_t len, + const struct git_hash_algo *algo) +{ + const unsigned char *bytes = data; + size_t minimum_entry_size; + + if (!algo) + BUG("attribute manifest requires a hash algorithm"); + if (len < sizeof(uint32_t)) + return -1; + minimum_entry_size = sizeof(uint32_t) + 4 + algo->rawsz + 1; + cursor->p = bytes + sizeof(uint32_t); + cursor->end = bytes + len; + cursor->last_path = NULL; + cursor->algo = algo; + cursor->last_path_len = 0; + cursor->remaining = get_be32(bytes); + if (cursor->remaining > + (len - sizeof(uint32_t)) / minimum_entry_size) + return -1; + return 0; +} + +int attr_manifest_cursor_next(struct attr_manifest_cursor *cursor, + struct attr_manifest_entry *entry) +{ + struct attr_manifest_entry previous; + uint32_t path_len; + size_t available; + + if (!cursor->remaining) + return cursor->p == cursor->end ? 0 : -1; + available = cursor->end - cursor->p; + if (available < sizeof(uint32_t) + 4 + cursor->algo->rawsz) + return -1; + path_len = get_be32(cursor->p); + cursor->p += sizeof(uint32_t); + entry->source = cursor->p[0]; + if ((entry->source != ATTR_MANIFEST_WORKTREE && + entry->source != ATTR_MANIFEST_INDEX) || + cursor->p[1] || cursor->p[2] || cursor->p[3]) + return -1; + cursor->p += 4; + entry->hash = cursor->p; + cursor->p += cursor->algo->rawsz; + available = cursor->end - cursor->p; + if (!path_len || available < path_len || + !attr_manifest_path_valid(cursor->p, path_len)) + return -1; + entry->path = cursor->p; + entry->path_len = path_len; + if (cursor->last_path) { + previous.path = cursor->last_path; + previous.path_len = cursor->last_path_len; + if (attr_manifest_entry_cmp(&previous, entry) >= 0) + return -1; + } + cursor->last_path = entry->path; + cursor->last_path_len = entry->path_len; + cursor->p += path_len; + cursor->remaining--; + return 1; +} + +int attr_manifest_valid(const void *data, size_t len, + const struct git_hash_algo *algo) +{ + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + int ret; + + if (attr_manifest_cursor_init(&cursor, data, len, algo)) + return 0; + while ((ret = attr_manifest_cursor_next(&cursor, &entry)) > 0) + ; + return !ret; +} + +int attr_manifest_for_each_changed(const void *old_data, size_t old_len, + const void *new_data, size_t new_len, + const struct git_hash_algo *algo, + attr_manifest_change_fn fn, void *data) +{ + struct attr_manifest_cursor old_cursor, new_cursor; + struct attr_manifest_entry old_entry, new_entry; + int old_ret, new_ret; + + /* + * Callers use this as a transactional change set. Validate both + * streams before allowing the callback to observe any entry. + */ + if (!attr_manifest_valid(old_data, old_len, algo) || + !attr_manifest_valid(new_data, new_len, algo)) + return -1; + if (attr_manifest_cursor_init(&old_cursor, old_data, old_len, algo) || + attr_manifest_cursor_init(&new_cursor, new_data, new_len, algo)) + return -1; + old_ret = attr_manifest_cursor_next(&old_cursor, &old_entry); + new_ret = attr_manifest_cursor_next(&new_cursor, &new_entry); + while (old_ret > 0 || new_ret > 0) { + struct attr_manifest_entry changed; + int has_changed = 1; + int cmp; + + if (old_ret <= 0) + cmp = 1; + else if (new_ret <= 0) + cmp = -1; + else + cmp = attr_manifest_entry_cmp(&old_entry, &new_entry); + if (cmp < 0) { + changed = old_entry; + old_ret = attr_manifest_cursor_next(&old_cursor, &old_entry); + } else if (cmp > 0) { + changed = new_entry; + new_ret = attr_manifest_cursor_next(&new_cursor, &new_entry); + } else { + changed = new_entry; + has_changed = !attr_manifest_entry_equal( + &old_entry, &new_entry, algo); + old_ret = attr_manifest_cursor_next(&old_cursor, &old_entry); + new_ret = attr_manifest_cursor_next(&new_cursor, &new_entry); + } + if (has_changed && fn(&changed, data)) + return -1; + } + return old_ret < 0 || new_ret < 0 ? -1 : 0; +} diff --git a/attr-manifest.h b/attr-manifest.h new file mode 100644 index 00000000000000..6718afd5c897f9 --- /dev/null +++ b/attr-manifest.h @@ -0,0 +1,62 @@ +#ifndef ATTR_MANIFEST_H +#define ATTR_MANIFEST_H + +#include "hash.h" + +struct strbuf; + +enum attr_manifest_source { + ATTR_MANIFEST_WORKTREE = 1, + ATTR_MANIFEST_INDEX = 2, +}; + +struct attr_manifest_entry { + const unsigned char *path; + uint32_t path_len; + enum attr_manifest_source source; + const unsigned char *hash; +}; + +struct attr_manifest_cursor { + const unsigned char *p; + const unsigned char *end; + const unsigned char *last_path; + const struct git_hash_algo *algo; + uint32_t last_path_len; + uint32_t remaining; +}; + +struct attr_manifest_writer { + struct strbuf *buf; + const struct git_hash_algo *algo; + size_t last_path_offset; + uint32_t last_path_len; + uint32_t nr; +}; + +typedef int (*attr_manifest_change_fn)(const struct attr_manifest_entry *entry, + void *data); + +void attr_manifest_writer_init(struct attr_manifest_writer *writer, + struct strbuf *buf, + const struct git_hash_algo *algo); +int attr_manifest_writer_add(struct attr_manifest_writer *writer, + const char *path, + enum attr_manifest_source source, + const unsigned char *hash); +int attr_manifest_cursor_init(struct attr_manifest_cursor *cursor, + const void *data, size_t len, + const struct git_hash_algo *algo); +int attr_manifest_cursor_next(struct attr_manifest_cursor *cursor, + struct attr_manifest_entry *entry); +int attr_manifest_valid(const void *data, size_t len, + const struct git_hash_algo *algo); +int attr_manifest_for_each_changed(const void *old_data, size_t old_len, + const void *new_data, size_t new_len, + const struct git_hash_algo *algo, + attr_manifest_change_fn fn, void *data); +int attr_manifest_only_linguist_generated_changed( + const char *old_data, size_t old_len, + const char *new_data, size_t new_len); + +#endif /* ATTR_MANIFEST_H */ diff --git a/attr.c b/attr.c index 0e63f1b6de8f53..04f28e119f2361 100644 --- a/attr.c +++ b/attr.c @@ -14,6 +14,7 @@ #include "environment.h" #include "exec-cmd.h" #include "attr.h" +#include "attr-fingerprint.h" #include "dir.h" #include "gettext.h" #include "path.h" @@ -477,6 +478,8 @@ static struct check_vector { pthread_mutex_t mutex; } check_vector; +static const struct attr_source_snapshot *source_snapshot; + static inline void vector_lock(void) { pthread_mutex_lock(&check_vector.mutex); @@ -536,6 +539,31 @@ static void drop_all_attr_stacks(void) vector_unlock(); } +void git_attr_invalidate_all(void) +{ + drop_all_attr_stacks(); +} + +void git_attr_source_snapshot_begin( + const struct attr_source_snapshot *snapshot) +{ + if (!snapshot) + BUG("cannot begin a NULL attribute source snapshot"); + if (source_snapshot) + BUG("attribute source snapshots cannot be nested"); + drop_all_attr_stacks(); + source_snapshot = snapshot; +} + +void git_attr_source_snapshot_end( + const struct attr_source_snapshot *snapshot) +{ + if (!snapshot || source_snapshot != snapshot) + BUG("ending an inactive attribute source snapshot"); + drop_all_attr_stacks(); + source_snapshot = NULL; +} + struct attr_check *attr_check_alloc(void) { struct attr_check *c = xcalloc(1, sizeof(struct attr_check)); @@ -668,6 +696,15 @@ static struct attr_stack *read_attr_from_array(const char **list) return res; } +static void handle_attr_line_buf(struct attr_stack *res, + struct strbuf *line, const char *path, + int *lineno, unsigned flags) +{ + if (!*lineno && starts_with(line->buf, utf8_bom)) + strbuf_remove(line, 0, strlen(utf8_bom)); + handle_attr_line(res, line->buf, path, ++*lineno, flags); +} + /* * Callers into the attribute system assume there is a single, system-wide * global state where attributes are read from and when the state is flipped by @@ -721,17 +758,48 @@ static struct attr_stack *read_attr_from_file(const char *path, unsigned flags) } CALLOC_ARRAY(res, 1); - while (strbuf_getline(&buf, fp) != EOF) { - if (!lineno && starts_with(buf.buf, utf8_bom)) - strbuf_remove(&buf, 0, strlen(utf8_bom)); - handle_attr_line(res, buf.buf, path, ++lineno, flags); - } + while (strbuf_getline(&buf, fp) != EOF) + handle_attr_line_buf(res, &buf, path, &lineno, flags); fclose(fp); strbuf_release(&buf); return res; } +static struct attr_stack *read_attr_from_snapshot( + enum attr_source_snapshot_kind kind, unsigned flags) +{ + struct attr_stack *res; + struct strbuf line = STRBUF_INIT; + const char *path, *buf; + size_t length; + size_t offset = 0; + int lineno = 0; + + if (!source_snapshot) + BUG("attribute source snapshot is not set"); + if (!attr_source_snapshot_read(source_snapshot, kind, + &path, &buf, &length)) + return NULL; + + CALLOC_ARRAY(res, 1); + while (offset < length) { + const char *start = buf + offset; + const char *newline = memchr(start, '\n', length - offset); + size_t len = newline ? (size_t)(newline - start) : + length - offset; + + if (newline && len && start[len - 1] == '\r') + len--; + strbuf_reset(&line); + strbuf_add(&line, start, len); + handle_attr_line_buf(res, &line, path, &lineno, flags); + offset = newline ? (size_t)(newline - buf) + 1 : length; + } + strbuf_release(&line); + return res; +} + static struct attr_stack *read_attr_from_buf(char *buf, size_t length, const char *path, unsigned flags) { @@ -922,13 +990,21 @@ static void bootstrap_attr_stack(struct index_state *istate, push_stack(stack, e, NULL, 0); /* system-wide frame */ - if (git_attr_system_is_enabled()) { + if (source_snapshot) { + e = read_attr_from_snapshot( + ATTR_SOURCE_SNAPSHOT_SYSTEM, flags); + push_stack(stack, e, NULL, 0); + } else if (git_attr_system_is_enabled()) { e = read_attr_from_file(git_attr_system_file(), flags); push_stack(stack, e, NULL, 0); } /* home directory */ - if (git_attr_global_file()) { + if (source_snapshot) { + e = read_attr_from_snapshot( + ATTR_SOURCE_SNAPSHOT_GLOBAL, flags); + push_stack(stack, e, NULL, 0); + } else if (git_attr_global_file()) { e = read_attr_from_file(git_attr_global_file(), flags); push_stack(stack, e, NULL, 0); } @@ -938,7 +1014,9 @@ static void bootstrap_attr_stack(struct index_state *istate, push_stack(stack, e, xstrdup(""), 0); /* info frame */ - if (startup_info->have_repository) + if (source_snapshot) + e = read_attr_from_snapshot(ATTR_SOURCE_SNAPSHOT_INFO, flags); + else if (startup_info->have_repository) e = read_attr_from_file(git_path_info_attributes(), flags); else e = NULL; diff --git a/attr.h b/attr.h index a04a5210921e22..af2ae096d6642a 100644 --- a/attr.h +++ b/attr.h @@ -129,6 +129,7 @@ struct index_state; * `git_attr_name()`. */ struct git_attr; +struct attr_source_snapshot; /* opaque structures used internally for attribute collection */ struct all_attrs_item; @@ -227,6 +228,20 @@ enum git_attr_direction { }; void git_attr_set_direction(enum git_attr_direction new_direction); +/* Discard cached attributes after a provider-wide invalidation. */ +void git_attr_invalidate_all(void); + +/* + * Read system, global, and info attributes from an immutable snapshot. + * begin() and end() must be strictly paired, cannot nest, and the caller must + * keep the snapshot alive until end(). Readers may run concurrently while a + * snapshot is active, but begin() and end() require that there are no readers. + */ +void git_attr_source_snapshot_begin( + const struct attr_source_snapshot *snapshot); +void git_attr_source_snapshot_end( + const struct attr_source_snapshot *snapshot); + void attr_start(void); /* Return the system gitattributes file. */ diff --git a/builtin.h b/builtin.h index 4e47a4ebd30ba3..512df065158d40 100644 --- a/builtin.h +++ b/builtin.h @@ -177,6 +177,7 @@ int cmd_diagnose(int argc, const char **argv, const char *prefix, struct reposit int cmd_diff_files(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff_index(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff(int argc, const char **argv, const char *prefix, struct repository *repo); +void prepare_diff_external_history(struct repository *repo); int cmd_diff_pairs(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_diff_tree(int argc, const char **argv, const char *prefix, struct repository *repo); int cmd_difftool(int argc, const char **argv, const char *prefix, struct repository *repo); diff --git a/builtin/add.c b/builtin/add.c index 60ffbede2be58a..d2ac83d9f0dbf9 100644 --- a/builtin/add.c +++ b/builtin/add.c @@ -6,6 +6,8 @@ #include "builtin.h" #include "advice.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "lockfile.h" @@ -285,18 +287,20 @@ static struct option builtin_add_options[] = { }; static int add_config(const char *var, const char *value, - const struct config_context *ctx, void *cb) + const struct config_context *ctx, void *data) { + clean_status_config_add(data, var, value, ctx); + if (!strcmp(var, "add.ignoreerrors") || !strcmp(var, "add.ignore-errors")) { ignore_add_errors = git_config_bool(var, value); return 0; } - if (git_color_config(var, value, cb) < 0) + if (git_color_config(var, value, NULL) < 0) return -1; - return git_default_config(var, value, ctx, cb); + return git_default_config(var, value, ctx, NULL); } static const char embedded_advice[] = N_( @@ -384,18 +388,25 @@ int cmd_add(int argc, const char *prefix, struct repository *repo) { + struct clean_status_config_digest clean_digest; int exit_status = 0; struct pathspec pathspec; struct dir_struct dir = DIR_INIT; int flags; int add_new_files; + int preserve_add_history = 0; int require_pathspec; char *seen = NULL; char *ps_matched = NULL; struct lock_file lock_file = LOCK_INIT; struct odb_transaction *transaction; - repo_config(repo, add_config, NULL); + show_usage_with_options_if_asked(argc, argv, + builtin_add_usage, builtin_add_options); + + clean_status_config_init(&clean_digest, repo->hash_algo); + repo_config(repo, add_config, &clean_digest); + clean_status_config_final(&clean_digest); argc = parse_options(argc, argv, prefix, builtin_add_options, builtin_add_usage, PARSE_OPT_KEEP_ARGV0); @@ -492,8 +503,29 @@ int cmd_add(int argc, (!(addremove || take_worktree_changes) ? ADD_CACHE_IGNORE_REMOVAL : 0)); + /* + * The refresh-only path below updates stat data and fsmonitor + * validity, but does not change the logical contents of the index. + * Ordinary add can do the same after an mtime-only change. Ask + * ADD_CACHE_TRACK_CLEAN_HISTORY to invalidate on any persistent + * add/remove decision below. + */ + if (refresh_only) { + clean_status_enable_external_history(repo); + clean_status_set_config_digest(repo, &clean_digest); + } else if (!show_only && !intent_to_add && !add_renormalize && + !chmod_arg && !include_sparse && !ignore_add_errors) { + preserve_add_history = 1; + flags |= ADD_CACHE_TRACK_CLEAN_HISTORY; + clean_status_enable_external_history(repo); + clean_status_set_config_digest(repo, &clean_digest); + } + if (repo_read_index_preload(repo, &pathspec, 0) < 0) die(_("index file corrupt")); + if (preserve_add_history && + (repo->index->split_index || repo->index->sparse_index)) + clean_status_invalidate_current_proof(repo->index); die_in_unpopulated_submodule(repo->index, prefix); die_path_inside_submodule(repo->index, &pathspec); @@ -603,8 +635,12 @@ int cmd_add(int argc, odb_transaction_commit(transaction); finish: - if (write_locked_index(repo->index, &lock_file, - COMMIT_LOCK | SKIP_IF_UNCHANGED)) + if (preserve_add_history && exit_status) + clean_status_invalidate_current_proof(repo->index); + if (show_only) + rollback_lock_file(&lock_file); + else if (write_locked_index(repo->index, &lock_file, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) die(_("unable to write new index file")); free(ps_matched); diff --git a/builtin/am.c b/builtin/am.c index e9623b8307793f..f4735c031022c2 100644 --- a/builtin/am.c +++ b/builtin/am.c @@ -9,6 +9,7 @@ #include "builtin.h" #include "abspath.h" #include "advice.h" +#include "clean-status.h" #include "config.h" #include "editor.h" #include "environment.h" @@ -2464,6 +2465,8 @@ int cmd_am(int argc, /* Ensure a valid committer ident can be constructed */ git_committer_info(IDENT_STRICT); + clean_status_prepare_main_index_history(the_repository); + if (repo_read_index_preload(the_repository, NULL, 0) < 0) die(_("failed to read the index")); diff --git a/builtin/apply.c b/builtin/apply.c index d642a402516f30..99c69f12889696 100644 --- a/builtin/apply.c +++ b/builtin/apply.c @@ -1,5 +1,6 @@ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "clean-status.h" #include "gettext.h" #include "hash.h" #include "apply.h" @@ -43,6 +44,11 @@ int cmd_apply(int argc, if (check_apply_state(&state, force_apply)) exit(128); + if (state.apply && state.check_index && !state.threeway && + !state.apply_with_reject && !state.ita_only && + !state.fake_ancestor && !state.index_file) + clean_status_prepare_main_index_history(the_repository); + ret = apply_all_patches(&state, argc, argv, options); clear_apply_state(&state); diff --git a/builtin/check-attr.c b/builtin/check-attr.c index 217d83ea7d5de0..d000e8d0f221dc 100644 --- a/builtin/check-attr.c +++ b/builtin/check-attr.c @@ -3,6 +3,7 @@ #include "config.h" #include "attr.h" #include "environment.h" +#include "fsmonitor.h" #include "gettext.h" #include "object-name.h" #include "quote.h" @@ -115,6 +116,7 @@ int cmd_check_attr(int argc, struct attr_check *check; struct object_id initialized_oid; int cnt, i, doubledash, filei; + int scoped_bootstrap = 0; if (!is_bare_repository(the_repository)) setup_work_tree(the_repository); @@ -127,13 +129,6 @@ int cmd_check_attr(int argc, prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; - if (repo_read_index(the_repository) < 0) { - die("invalid cache"); - } - - if (cached_attrs) - git_attr_set_direction(GIT_ATTR_INDEX); - doubledash = -1; for (i = 0; doubledash < 0 && i < argc; i++) { if (!strcmp(argv[i], "--")) @@ -176,6 +171,18 @@ int cmd_check_attr(int argc, error_with_usage("No file specified"); } + scoped_bootstrap = !stdin_paths && !source && + argc - filei > 0 && argc - filei <= 64; + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(the_repository->index); + if (repo_read_index(the_repository) < 0) + die("invalid cache"); + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(the_repository->index); + + if (cached_attrs) + git_attr_set_direction(GIT_ATTR_INDEX); + check = attr_check_alloc(); if (!all_attrs) { for (i = 0; i < cnt; i++) { diff --git a/builtin/checkout-index.c b/builtin/checkout-index.c index 311b94ff3174a6..f474bf524da0f6 100644 --- a/builtin/checkout-index.c +++ b/builtin/checkout-index.c @@ -8,9 +8,13 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" +#include "fsmonitor-settings.h" #include "gettext.h" +#include "hook.h" #include "lockfile.h" #include "quote.h" #include "cache-tree.h" @@ -30,6 +34,13 @@ static char topath[4][TEMPORARY_FILENAME_LENGTH + 1]; static struct checkout state = CHECKOUT_INIT; +static int checkout_index_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + static void write_tempfile_record(const char *name, const char *prefix) { int i; @@ -215,6 +226,7 @@ int cmd_checkout_index(int argc, const char *prefix, struct repository *repo) { + struct clean_status_config_digest clean_digest; int i; struct lock_file lock_file = LOCK_INIT; int all = 0; @@ -253,19 +265,21 @@ int cmd_checkout_index(int argc, show_usage_with_options_if_asked(argc, argv, builtin_checkout_index_usage, builtin_checkout_index_options); - repo_config(repo, git_default_config, NULL); + clean_status_config_init(&clean_digest, repo->hash_algo); + repo_config(repo, checkout_index_config, &clean_digest); + clean_status_config_final(&clean_digest); + /* + * checkout-index never changes index contents. Keep closed semantic + * history attached when -u writes fresh stat data. + */ + clean_status_set_config_digest(repo, &clean_digest); prefix_length = prefix ? strlen(prefix) : 0; prepare_repo_settings(repo); repo->settings.command_requires_full_index = 0; - if (repo_read_index(repo) < 0) { - die("invalid cache"); - } - argc = parse_options(argc, argv, prefix, builtin_checkout_index_options, builtin_checkout_index_usage, 0); - state.istate = repo->index; state.force = force; state.quiet = quiet; state.not_new = not_new; @@ -280,6 +294,15 @@ int cmd_checkout_index(int argc, die(_("options '%s' and '%s' cannot be used together"), "--stage=all", "--no-temp"); + if (index_opt && !state.base_dir_len && !to_tempfile && + !checkout_stage && !getenv(INDEX_ENVIRONMENT) && + fstat_is_reliable() && + fsm_settings__get_mode(repo) == FSMONITOR_MODE_IPC) + clean_status_enable_external_history(repo); + if (repo_read_index(repo) < 0) + die("invalid cache"); + state.istate = repo->index; + /* * when --prefix is specified we do not want to update cache. */ @@ -343,8 +366,14 @@ int cmd_checkout_index(int argc, if (err) return 1; - if (is_lock_file_locked(&lock_file) && - write_locked_index(repo->index, &lock_file, COMMIT_LOCK)) - die("Unable to write new index file"); + if (is_lock_file_locked(&lock_file)) { + unsigned int flags = COMMIT_LOCK; + + if (!repo->index->cache_changed && + !hook_exists(repo, "post-index-change")) + flags |= SKIP_IF_UNCHANGED; + if (write_locked_index(repo->index, &lock_file, flags)) + die("Unable to write new index file"); + } return 0; } diff --git a/builtin/checkout.c b/builtin/checkout.c index 55e3a89a852712..fbc324b8b7dee7 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -6,6 +6,8 @@ #include "branch.h" #include "cache-tree.h" #include "checkout.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "commit.h" #include "config.h" #include "diff.h" @@ -46,6 +48,7 @@ #include "add-interactive.h" struct checkout_opts { + struct clean_status_config_digest clean_digest; int patch_mode; int patch_context; int patch_interhunk_context; @@ -142,6 +145,11 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm return run_hooks_opt(the_repository, "post-checkout", &opt); } +struct tree_checkout_context { + int overlay_mode; + int *index_changed; +}; + /* * Handle a tree object and determine if we need to recurse into the * tree (READ_TREE_RECURSIVE) or skip it (0). @@ -149,11 +157,12 @@ static int post_checkout_hook(struct commit *old_commit, struct commit *new_comm static int try_update_sparse_directory(const struct object_id *oid, struct strbuf *base, const char *pathname, - int overlay_mode) + struct tree_checkout_context *context) { struct strbuf dirpath = STRBUF_INIT; struct cache_entry *old; int pos, result = READ_TREE_RECURSIVE; + int overlay_mode = context ? context->overlay_mode : 1; if (!the_repository->index->sparse_index) return result; @@ -180,8 +189,11 @@ static int try_update_sparse_directory(const struct object_id *oid, * sparse directory OID directly since files not present in * the source tree should be removed anyway. */ + if (context && context->index_changed) + *context->index_changed = 1; oidcpy(&old->oid, oid); old->ce_flags |= CE_UPDATE; + the_repository->index->cache_changed |= CE_ENTRY_CHANGED; result = 0; } @@ -196,11 +208,11 @@ static int update_some(const struct object_id *oid, struct strbuf *base, int len; struct cache_entry *ce; int pos; - int overlay_mode = context ? *((int *)context) : 1; + struct tree_checkout_context *checkout_context = context; if (S_ISDIR(mode)) return try_update_sparse_directory(oid, base, pathname, - overlay_mode); + checkout_context); len = base->len + strlen(pathname); ce = make_empty_cache_entry(the_repository->index, len); @@ -228,16 +240,26 @@ static int update_some(const struct object_id *oid, struct strbuf *base, } } + if (checkout_context && checkout_context->index_changed && + !clean_status_index_entry_is_semantically_safe( + the_repository->index, + pos >= 0 ? the_repository->index->cache[pos] : NULL, ce)) + *checkout_context->index_changed = 1; add_index_entry(the_repository->index, ce, ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); return 0; } static int read_tree_some(struct tree *tree, const struct pathspec *pathspec, - int overlay_mode) + int overlay_mode, int *index_changed) { + struct tree_checkout_context context = { + .overlay_mode = overlay_mode, + .index_changed = index_changed, + }; + read_tree(the_repository, tree, - pathspec, update_some, &overlay_mode); + pathspec, update_some, &context); /* update the index with the given tree's info * for all args, expanding wildcards, and exit @@ -420,20 +442,26 @@ static void mark_ce_for_checkout_overlay(struct cache_entry *ce, static void mark_ce_for_checkout_no_overlay(struct cache_entry *ce, char *ps_matched, - const struct checkout_opts *opts) + const struct checkout_opts *opts, + int *index_changed) { ce->ce_flags &= ~CE_MATCHED; if (!opts->ignore_skipworktree && ce_skip_worktree(ce)) return; if (ce_path_match(the_repository->index, ce, &opts->pathspec, ps_matched)) { ce->ce_flags |= CE_MATCHED; - if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) + if (opts->source_tree && !(ce->ce_flags & CE_UPDATE)) { /* - * In overlay mode, but the path is not in + * In no-overlay mode, but the path is not in * tree-ish, which means we should remove it * from the index and the working tree. */ + if (index_changed && + !clean_status_index_entry_is_semantically_safe( + the_repository->index, ce, NULL)) + *index_changed = 1; ce->ce_flags |= CE_REMOVE | CE_WT_REMOVE; + } } } @@ -524,6 +552,8 @@ static int checkout_paths(const struct checkout_opts *opts, int errs = 0; struct lock_file lock_file = LOCK_INIT; int checkout_index; + int preserve_source_tree_history = 0; + int source_tree_index_changed = 0; trace2_cmd_mode(opts->patch_mode ? "patch" : "path"); @@ -628,12 +658,36 @@ static int checkout_paths(const struct checkout_opts *opts, } repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + /* + * A plain worktree checkout from the index only rewrites stat data. + * A source-tree checkout may do the same when every selected entry + * already matches the index; if not, invalidate its proof below. + * Keep written paths fsmonitor-invalid in either case. Do not do + * this for --merge, which may recreate unmerged index entries from + * resolve undo data. + */ + preserve_source_tree_history = + opts->source_tree && opts->checkout_index && + !opts->merge && !opts->writeout_stage; + if ((opts->checkout_worktree && !opts->source_tree && + !opts->merge && !opts->writeout_stage) || + preserve_source_tree_history) { + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, + &opts->clean_digest); + } if (repo_read_index_preload(the_repository, &opts->pathspec, 0) < 0) return error(_("index file corrupt")); + if (preserve_source_tree_history && + (the_repository->index->split_index || + the_repository->index->sparse_index)) + source_tree_index_changed = 1; if (opts->source_tree) read_tree_some(opts->source_tree, &opts->pathspec, - opts->overlay_mode); + opts->overlay_mode, + preserve_source_tree_history ? + &source_tree_index_changed : NULL); if (opts->merge) unmerge_index(the_repository->index, &opts->pathspec, CE_MATCHED); @@ -651,7 +705,10 @@ static int checkout_paths(const struct checkout_opts *opts, else mark_ce_for_checkout_no_overlay(the_repository->index->cache[pos], ps_matched, - opts); + opts, + preserve_source_tree_history ? + &source_tree_index_changed : + NULL); if (report_path_error(ps_matched, &opts->pathspec)) { free(ps_matched); @@ -698,7 +755,16 @@ static int checkout_paths(const struct checkout_opts *opts, checkout_index = opts->checkout_index; if (checkout_index) { - if (write_locked_index(the_repository->index, &lock_file, COMMIT_LOCK)) + unsigned int flags = COMMIT_LOCK; + + if (preserve_source_tree_history && + (source_tree_index_changed || errs)) + clean_status_invalidate_current_proof( + the_repository->index); + if (!the_repository->index->cache_changed && + !hook_exists(the_repository, "post-index-change")) + flags |= SKIP_IF_UNCHANGED; + if (write_locked_index(the_repository->index, &lock_file, flags)) die(_("unable to write new index file")); } else { /* @@ -846,6 +912,17 @@ static int merge_working_tree(const struct checkout_opts *opts, struct tree *new_tree; repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + /* + * A discarding switch may rewrite only worktree/stat state when the + * target tree matches the index. Let unpack_trees() transfer the + * proof only after it proves that the rebuilt index is identical. + */ + if (opts->discard_changes || + (!opts->merge && !opts->new_orphan_branch)) { + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, + &opts->clean_digest); + } if (repo_read_index_preload(the_repository, NULL, 0) < 0) { rollback_lock_file(&lock_file); return error(_("index file corrupt")); @@ -891,6 +968,7 @@ static int merge_working_tree(const struct checkout_opts *opts, /* 2-way merge to the new branch */ init_topts(&topts, opts->show_progress, opts->overwrite_ignore, quiet); + topts.preserve_semantic_history = 1; init_checkout_metadata(&topts.meta, new_branch_info->refname, new_branch_info->commit ? &new_branch_info->commit->object.oid : @@ -1282,6 +1360,8 @@ static int git_checkout_config(const char *var, const char *value, { struct checkout_opts *opts = cb; + clean_status_config_add(&opts->clean_digest, var, value, ctx); + if (!strcmp(var, "diff.ignoresubmodules")) { if (!value) return config_error_nonbool(var); @@ -1879,7 +1959,11 @@ static int checkout_main(int argc, const char **argv, const char *prefix, opts->prefix = prefix; opts->show_progress = -1; + show_usage_with_options_if_asked(argc, argv, usagestr, options); + + clean_status_config_init(&opts->clean_digest, the_repository->hash_algo); repo_config(the_repository, git_checkout_config, opts); + clean_status_config_final(&opts->clean_digest); if (the_repository->gitdir) { prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/commit.c b/builtin/commit.c index 28f61745034506..5e8c425fa13f1d 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -9,16 +9,22 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "abspath.h" #include "advice.h" #include "config.h" #include "lockfile.h" #include "cache-tree.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-sidecar.h" #include "color.h" #include "dir.h" #include "editor.h" #include "environment.h" #include "diff.h" #include "commit.h" +#include "fsmonitor.h" +#include "fsmonitor-settings.h" #include "add-interactive.h" #include "gettext.h" #include "revision.h" @@ -30,8 +36,12 @@ #include "path.h" #include "preload-index.h" #include "read-cache.h" +#include "refs.h" +#include "replace-object.h" #include "repository.h" #include "string-list.h" +#include "submodule.h" +#include "symlinks.h" #include "rerere.h" #include "unpack-trees.h" #include "column.h" @@ -39,10 +49,13 @@ #include "sparse-index.h" #include "mailmap.h" #include "help.h" +#include "hook.h" #include "commit-reach.h" #include "commit-graph.h" #include "pretty.h" +#include "trace2.h" #include "trailer.h" +#include "wrapper.h" static const char * const builtin_commit_usage[] = { N_("git commit [-a | --interactive | --patch] [-s] [-v] [-u[]] [--amend]\n" @@ -105,6 +118,7 @@ static const char *color_status_slots[] = { static const char *use_message_buffer; static struct lock_file index_lock; /* real index */ static struct lock_file false_lock; /* used only for partial commits */ +static struct clean_status_commit_checkpoint *commit_checkpoint; static enum { COMMIT_AS_IS = 1, COMMIT_NORMAL, @@ -205,15 +219,44 @@ static void determine_whence(struct wt_status *s) s->whence = whence; } -static void status_init_config(struct wt_status *s, config_fn_t fn) +struct status_config_callback_data { + struct wt_status *status; + config_fn_t fn; + struct clean_status_config_digest *clean_digest; +}; + +static int status_config_callback(const char *key, const char *value, + const struct config_context *ctx, void *cb) { + struct status_config_callback_data *data = cb; + + clean_status_config_add(data->clean_digest, key, value, ctx); + return data->fn(key, value, ctx, data->status); +} + +static void status_init_config_with_clean_digest( + struct wt_status *s, config_fn_t fn, + struct clean_status_config_digest *clean_digest) +{ + struct status_config_callback_data data = { + .status = s, + .fn = fn, + .clean_digest = clean_digest, + }; + wt_status_prepare(the_repository, s); init_diff_ui_defaults(); - repo_config(the_repository, fn, s); + repo_config(the_repository, status_config_callback, &data); determine_whence(s); s->hints = advice_enabled(ADVICE_STATUS_HINTS); /* must come after repo_config() */ } +static void release_commit_checkpoint(void) +{ + clean_status_release_commit_checkpoint(commit_checkpoint); + commit_checkpoint = NULL; +} + static void rollback_index_files(void) { switch (commit_style) { @@ -227,6 +270,7 @@ static void rollback_index_files(void) rollback_lock_file(&false_lock); break; } + release_commit_checkpoint(); } static int commit_index_files(void) @@ -237,6 +281,8 @@ static int commit_index_files(void) case COMMIT_AS_IS: break; /* nothing to do */ case COMMIT_NORMAL: + restore_locked_index_for_commit(the_repository->index, + &index_lock, commit_checkpoint); err = commit_lock_file(&index_lock); break; case COMMIT_PARTIAL: @@ -244,6 +290,7 @@ static int commit_index_files(void) rollback_lock_file(&false_lock); break; } + release_commit_checkpoint(); return err; } @@ -323,6 +370,9 @@ static void create_base_index(const struct commit *current_head) opts.head_idx = 1; opts.index_only = 1; opts.merge = 1; + opts.preserve_semantic_history = + clean_status_external_history_enabled(the_repository->index) && + clean_status_revalidated_token_matches(the_repository->index); opts.src_index = the_repository->index; opts.dst_index = the_repository->index; @@ -349,7 +399,8 @@ static void refresh_cache_or_die(int refresh_flags) } static const char *prepare_index(const char **argv, const char *prefix, - const struct commit *current_head, int is_status) + const struct commit *current_head, int is_status, + struct wt_status *s) { struct string_list partial = STRING_LIST_INIT_DUP; struct pathspec pathspec; @@ -399,7 +450,10 @@ static const char *prepare_index(const char **argv, const char *prefix, refresh_cache_or_die(refresh_flags); - if (write_locked_index(the_repository->index, &index_lock, 0)) + if (is_status ? + write_locked_index(the_repository->index, &index_lock, 0) : + write_locked_index_for_commit(the_repository->index, &index_lock, + &commit_checkpoint)) die(_("unable to create temporary index")); old_repo_index_file = the_repository->index_file; @@ -428,6 +482,10 @@ static const char *prepare_index(const char **argv, const char *prefix, die(_("unable to update temporary index")); } else warning(_("Failed to update main cache tree")); + if (!is_status && + !clean_status_advance_commit_checkpoint( + commit_checkpoint, the_repository->index, &index_lock)) + release_commit_checkpoint(); commit_style = COMMIT_NORMAL; ret = get_lock_file_path(&index_lock); @@ -462,7 +520,10 @@ static const char *prepare_index(const char **argv, const char *prefix, refresh_cache_or_die(refresh_flags); cache_tree_update(the_repository->index, WRITE_TREE_SILENT); - if (write_locked_index(the_repository->index, &index_lock, 0)) + if (is_status ? + write_locked_index(the_repository->index, &index_lock, 0) : + write_locked_index_for_commit(the_repository->index, &index_lock, + &commit_checkpoint)) die(_("unable to write new index file")); commit_style = COMMIT_NORMAL; ret = get_lock_file_path(&index_lock); @@ -480,13 +541,24 @@ static const char *prepare_index(const char **argv, const char *prefix, * We still need to refresh the index here. */ if (!only && !pathspec.nr) { - repo_hold_locked_index(the_repository, &index_lock, - LOCK_DIE_ON_ERROR); - refresh_cache_or_die(refresh_flags); + int update_index = !is_status || use_optional_locks(); + + if (update_index) + repo_hold_locked_index(the_repository, &index_lock, + LOCK_DIE_ON_ERROR); + if (!fstat_is_reliable() || + the_repository->index->split_index || + fsm_settings__get_mode(the_repository) != + FSMONITOR_MODE_IPC) + refresh_cache_or_die(refresh_flags); + else if (wt_status_refresh_index( + s, refresh_flags | REFRESH_IN_PORCELAIN, 0)) + die_resolve_conflict("commit"); if (the_repository->index->cache_changed || !cache_tree_fully_valid(the_repository->index->cache_tree)) cache_tree_update(the_repository->index, WRITE_TREE_SILENT); - if (write_locked_index(the_repository->index, &index_lock, + if (update_index && + write_locked_index(the_repository->index, &index_lock, COMMIT_LOCK | SKIP_IF_UNCHANGED)) die(_("unable to write new index file")); commit_style = COMMIT_AS_IS; @@ -773,13 +845,29 @@ static int prepare_to_commit(const char *index_file, const char *prefix, int clean_message_contents = (cleanup_mode != COMMIT_MSG_CLEANUP_NONE); int old_display_comment_prefix; int invoked_hook; + int hook_index_matches = 0; + struct clean_status_index_snapshot hook_index = { .fd = -1 }; /* This checks and barfs if author is badly specified */ determine_author_info(author_ident); - if (!no_verify && run_commit_hook(use_editor, index_file, &invoked_hook, - "pre-commit", NULL)) - return 0; + if (!no_verify) { + int hook_failed = run_commit_hook( + use_editor, index_file, &invoked_hook, + "pre-commit", NULL); + + if (invoked_hook && fstat_is_reliable()) + wt_status_invalidate_refresh(s); + if (invoked_hook && fstat_is_reliable() && + commit_style == COMMIT_AS_IS) + hook_index_matches = + !clean_status_index_snapshot_pin( + &hook_index, the_repository->index); + if (hook_failed) { + clean_status_index_snapshot_release(&hook_index); + return 0; + } + } if (squash_message) { /* @@ -1095,10 +1183,29 @@ static int prepare_to_commit(const char *index_file, const char *prefix, else fputs(_(empty_rebase_pick_advice), stderr); } + clean_status_index_snapshot_release(&hook_index); return 0; } if (!no_verify && invoked_hook) { + struct lock_file refresh_lock = LOCK_INIT; + + /* + * Preserve any strong invalidation recorded while status + * closed the post-hook token. The pinned source prevents this + * write from replacing an index updated by the hook. + */ + if (hook_index_matches && + repo_hold_locked_index(the_repository, &refresh_lock, 0) >= 0) { + if (clean_status_index_snapshot_still_matches( + &hook_index, the_repository->index)) + repo_update_index_if_able( + the_repository, &refresh_lock); + else + rollback_lock_file(&refresh_lock); + } + clean_status_index_snapshot_release(&hook_index); + /* * Re-read the index as the pre-commit-commit hook was invoked * and could have updated it. We must do this before we invoke @@ -1431,7 +1538,7 @@ static int dry_run_commit(const char **argv, const char *prefix, int committable; const char *index_file; - index_file = prepare_index(argv, prefix, current_head, 1); + index_file = prepare_index(argv, prefix, current_head, 1, s); committable = run_status(stdout, index_file, prefix, 0, s); rollback_index_files(); @@ -1534,6 +1641,249 @@ static int git_status_config(const char *k, const char *v, return git_diff_ui_config(k, v, ctx, NULL); } +/* + * A clean-proof hit certifies the tracked and untracked lists, but it + * deliberately does not cache status output. Refresh the cheap state which + * the selected printer derives from refs and administrative files before + * printing those empty lists. + */ +static int print_clean_sidecar(struct wt_status *s, const char *prefix) +{ + struct object_id oid; + + if (repo_get_oid(s->repo, s->reference, &oid)) + return 0; + s->is_initial = 0; + oidcpy(&s->oid_commit, &oid); + s->ignore_submodule_arg = ignore_submodule_arg; + s->status_format = status_format; + /* A globally clean proof guarantees that both verbose diffs are empty. */ + s->verbose = 0; + FREE_AND_NULL(s->branch); + s->branch = refs_resolve_refdup(get_main_ref_store(s->repo), + "HEAD", 0, NULL, NULL); + wt_status_get_state(s->repo, &s->state, + s->branch && !strcmp(s->branch, "HEAD")); + if (s->state.merge_in_progress) + s->committable = 1; + if (s->relative_paths) + s->prefix = prefix; + wt_status_print(s); + return 1; +} + +static int clean_status_sidecar_needs_reissue(struct repository *repo, + int repository_inputs_changed) +{ + struct clean_status_sidecar_record record = + CLEAN_STATUS_SIDECAR_RECORD_INIT; + struct clean_status_index_snapshot index = { .fd = -1 }; + char *path = xstrfmt("%s.csts", repo->index_file); + struct stat st; + int safe_existing = !lstat(path, &st) && + S_ISREG(st.st_mode) && st.st_nlink == 1 && + is_path_owned_by_current_user(path, NULL); + int reissue = 0; + + if (!clean_status_sidecar_load( + repo->index_file, repo->hash_algo, &record)) + reissue = safe_existing && + (repository_inputs_changed || + !!clean_status_sidecar_pin_source( + repo->index_file, &record.sidecar, + repo->hash_algo, &index) || + record.sidecar.hardlink_nr > 0); + else { + if (lstat(path, &st) < 0) + reissue = errno == ENOENT; + else + reissue = safe_existing; + } + free(path); + clean_status_index_snapshot_release(&index); + clean_status_sidecar_record_release(&record); + return reissue; +} + +#ifdef __linux__ +static int clean_status_scoped_history_test_barrier(void) +{ + const char *ready = + getenv("GIT_TEST_CLEAN_STATUS_SCOPED_HISTORY_BARRIER_READY"); + const char *resume = + getenv("GIT_TEST_CLEAN_STATUS_SCOPED_HISTORY_BARRIER_RESUME"); + const char *scripted = getenv("GIT_TEST_FSMONITOR_QUERY_SEQUENCE"); + struct stat fifo, opened; + char resumed; + int fd, failed; + + if (!ready && !resume) + return 0; + if (!scripted || !*scripted || !ready || !*ready || + !resume || !*resume || lstat(resume, &fifo) || + !S_ISFIFO(fifo.st_mode) || fifo.st_uid != geteuid()) + return -1; + fd = open(ready, O_WRONLY | O_CREAT | O_EXCL | + O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) + return -1; + failed = fstat(fd, &opened) || !S_ISREG(opened.st_mode) || + opened.st_uid != geteuid() || opened.st_nlink != 1 || + write_in_full(fd, "ready\n", 6) != 6; + if (close(fd) || failed) + return -1; + fd = open(resume, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) + return -1; + failed = fstat(fd, &opened) || !S_ISFIFO(opened.st_mode) || + opened.st_uid != geteuid() || + opened.st_dev != fifo.st_dev || opened.st_ino != fifo.st_ino || + read_in_full(fd, &resumed, 1) != 1; + if (close(fd) || failed) + return -1; + return 0; +} + +static int clean_status_scoped_pathspec_is_bounded( + const struct wt_status *status) +{ + const struct pathspec *pathspec = &status->pathspec; + struct index_state *istate = status->repo->index; + int i; + + if (pathspec->nr <= 0 || pathspec->nr > 64 || + pathspec->has_wildcard || + (pathspec->magic & + (PATHSPEC_GLOB | PATHSPEC_ICASE | + PATHSPEC_EXCLUDE | PATHSPEC_ATTR))) + return 0; + for (i = 0; i < pathspec->nr; i++) { + const struct pathspec_item *item = &pathspec->items[i]; + const struct cache_entry *ce; + struct stat st; + int pos; + + if (!item->match || item->len <= 0 || + item->match[item->len - 1] == '/' || + !strcmp(item->match, ".") || + has_symlink_leading_path(item->match, item->len) || + lstat(item->match, &st) || !S_ISREG(st.st_mode)) + return 0; + pos = index_name_pos(istate, item->match, item->len); + if (pos < 0) + return 0; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_intent_to_add(ce) || ce_skip_worktree(ce) || + (ce->ce_flags & CE_VALID) || + !clean_status_index_entry_is_semantically_safe( + istate, ce, ce)) + return 0; + } + return 1; +} +#endif + +static int clean_status_scoped_provider_is_current( + const struct index_state *istate) +{ + return clean_status_has_persistent_fsmonitor_semantic_history(istate) && + clean_status_has_current_full_fsmonitor_proof(istate) && + istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + !istate->fsmonitor_last_update_pending && + istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + istate->fsmonitor_untracked_valid && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token) && + istate->untracked && istate->untracked->root && + istate->untracked->root->valid && + istate->untracked->root->valid_recursive && + !istate->untracked->root->fsmonitor_dirty && + !istate->untracked->fsmonitor_dirty_paths.len && + !istate->fsmonitor_untracked_must_persist && + !clean_status_worktree_manifest_needs_refresh(istate) && + !clean_status_manifest_global_fallback(istate) && + !clean_status_external_history_was_restored(istate); +} + +static int clean_status_defer_scoped_history_capture( + const struct wt_status *status, + struct clean_status_index_snapshot *snapshot) +{ +#ifdef __linux__ + struct repository *repo = status->repo; + struct index_state *istate = repo->index; + struct stat st; + char *physical, *selected, *canonical; + int eligible; + + /* + * Legacy checkpoints cannot authenticate a deferred source digest. + * Keep their ordinary lock and content verification; only a later + * clean proof may decide that publishing a checkpoint is unnecessary. + */ + if (clean_status_identity_is_durable() || + !untracked_files_arg || strcmp(untracked_files_arg, "no") || + status->show_untracked_files != SHOW_NO_UNTRACKED_FILES || + status->show_ignored_mode || status->submodule_summary || + !clean_status_scoped_pathspec_is_bounded(status) || + getenv(INDEX_ENVIRONMENT) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || + getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + istate != repo->index || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + repo_config_get_split_index(repo) > 0 || + repo_config_values(repo)->apply_sparse_checkout || + !fstat_is_reliable() || + !repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC || + (istate->cache_changed & + ~(FSMONITOR_CHANGED | UNTRACKED_CHANGED)) || + !clean_status_scoped_provider_is_current(istate) || + repo_has_replace_refs_uncached(repo)) + return 0; + + physical = xstrfmt("%s/index", repo_get_git_dir(repo)); + selected = real_pathdup(repo_get_index_file(repo), 0); + canonical = real_pathdup(physical, 0); + eligible = selected && canonical && + !fspathcmp(selected, canonical) && + !lstat(physical, &st) && S_ISREG(st.st_mode) && + st.st_nlink == 1 && + !clean_status_index_snapshot_pin_proof_epoch(snapshot, istate); + if (!eligible) + clean_status_index_snapshot_release(snapshot); + free(canonical); + free(selected); + free(physical); + return eligible; +#else + (void)status; + (void)snapshot; + return 0; +#endif +} + +static int clean_status_scoped_history_can_rollback( + const struct wt_status *status, + const struct clean_status_index_snapshot *snapshot) +{ + struct index_state *istate = status->repo->index; + + return clean_status_scoped_provider_is_current(istate) && + !(istate->cache_changed & + ~(FSMONITOR_CHANGED | UNTRACKED_CHANGED)) && + clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate) && + !has_racy_timestamp(istate); +} + int cmd_status(int argc, const char **argv, const char *prefix, @@ -1542,8 +1892,29 @@ struct repository *repo UNUSED) static int no_renames = -1; static const char *rename_score_arg = (const char *)-1; static struct wt_status s; + struct clean_status_config_digest clean_digest; unsigned int progress_flag = 0; int fd; + int default_status_command = argc == 1 && (!prefix || !*prefix); + int exact_clean_command = argc == 2 && + !strcmp(argv[1], "--porcelain=v2") && (!prefix || !*prefix); + int exact_clean_query; + int normal_clean_query; + int reusable_clean_query; + int normal_has_head; + int reissue_clean_sidecar = 0; + int repository_inputs_changed = 0; + int sidecar_provider_reset = 0; + int reissue_after_write = 0; + int save_history_after_write = 0; + int deferred_scoped_history = 0; + int guarded_scoped_history_source = 0; + int optional_status_writes; + struct clean_status_index_snapshot scoped_history_source = { + .fd = -1, + }; + struct clean_status_index_write_receipt written_index = + CLEAN_STATUS_INDEX_WRITE_RECEIPT_INIT; struct object_id oid; static struct option builtin_status_options[] = { OPT__VERBOSE(&verbose, N_("be verbose")), @@ -1605,15 +1976,22 @@ struct repository *repo UNUSED) prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; - status_init_config(&s, git_status_config); + clean_status_config_init(&clean_digest, the_repository->hash_algo); + status_init_config_with_clean_digest( + &s, git_status_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_set_config_digest(the_repository, &clean_digest); argc = parse_options(argc, argv, prefix, builtin_status_options, builtin_status_usage, 0); finalize_colopts(&s.colopts, -1); finalize_deferred_config(&s); + optional_status_writes = use_optional_locks() && + !fsm_settings__is_watch_limit_backoff(the_repository); handle_untracked_files_arg(&s); handle_ignored_arg(&s); + s.ignore_submodule_arg = ignore_submodule_arg; if (s.show_ignored_mode == SHOW_MATCHING_IGNORED && s.show_untracked_files == SHOW_NO_UNTRACKED_FILES) @@ -1622,25 +2000,121 @@ struct repository *repo UNUSED) parse_pathspec(&s.pathspec, 0, PATHSPEC_PREFER_FULL, prefix, argv); + if (s.ignore_submodule_arg) { + struct diff_options diffopt = { 0 }; + + handle_ignore_submodules_arg(&diffopt, s.ignore_submodule_arg); + } + normal_has_head = !repo_get_oid(the_repository, s.reference, &oid); + exact_clean_query = exact_clean_command && + status_format == STATUS_FORMAT_PORCELAIN_V2 && + !s.pathspec.nr && !s.show_branch && !s.show_stash && + !s.show_ignored_mode && !s.null_termination && !s.verbose && + s.show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES; + normal_clean_query = default_status_command && + status_format == STATUS_FORMAT_NONE && normal_has_head && + !s.pathspec.nr && !s.show_branch && + !s.show_ignored_mode && !s.null_termination && !s.verbose && + !s.submodule_summary && + s.show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES && + !repo_config_values(the_repository)->apply_sparse_checkout; + reusable_clean_query = normal_has_head && + !s.show_ignored_mode && !s.submodule_summary && + /* A clean merge still prints a staged-changes header with -vv. */ + !(verbose > 1 && + file_exists(git_path_merge_head(the_repository))) && + !repo_config_values(the_repository)->apply_sparse_checkout; + s.allow_clean_status_shortcuts = normal_has_head && + !s.submodule_summary && + !repo_config_values(the_repository)->apply_sparse_checkout; + clean_status_enable_external_history(the_repository); + s.certify_clean_status = exact_clean_query; + if (reusable_clean_query && + clean_status_try_sidecar(the_repository, &clean_digest, + &repository_inputs_changed, + &sidecar_provider_reset)) { + if (exact_clean_query || + print_clean_sidecar(&s, prefix)) { + wt_status_collect_free_buffers(&s); + return 0; + } + } + if (normal_clean_query && optional_status_writes && + clean_status_identity_is_durable()) + reissue_clean_sidecar = + clean_status_sidecar_needs_reissue( + the_repository, repository_inputs_changed || + sidecar_provider_reset); if (status_format != STATUS_FORMAT_PORCELAIN && - status_format != STATUS_FORMAT_PORCELAIN_V2) + status_format != STATUS_FORMAT_PORCELAIN_V2) { progress_flag = REFRESH_PROGRESS; + if (isatty(2)) + clean_status_enable_progress(the_repository); + } + if (optional_status_writes) + clean_status_require_external_history_source(the_repository); repo_read_index(the_repository); - refresh_index(the_repository->index, - REFRESH_QUIET|REFRESH_UNMERGED|progress_flag, - &s.pathspec, NULL, NULL); - - if (use_optional_locks()) + if (sidecar_provider_reset) { + /* + * The fast probe already lost the old provider boundary. Even + * if the index reader's second query is empty, it must not + * revive the tracked or untracked proof we just rejected. A + * provider-owned, authenticated stat baseline has already + * invalidated that proof and must retain its closing token. + */ + if (!clean_status_fsmonitor_semantic_baseline_pending( + the_repository->index) || + !fsmonitor_pending_token_from_provider(the_repository->index)) { + clean_status_invalidate_current_manifest(the_repository->index); + fsmonitor_invalidate_semantics(the_repository->index); + untracked_cache_invalidate_all(the_repository->index); + } + trace2_data_intmax("status", the_repository, + "clean-proof/provider-reset-carried", 1); + } + if (optional_status_writes) { + deferred_scoped_history = + clean_status_defer_scoped_history_capture( + &s, &scoped_history_source); + guarded_scoped_history_source = deferred_scoped_history; + if (deferred_scoped_history) { + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-capture-deferred", 1); +#ifdef __linux__ + if (clean_status_scoped_history_test_barrier()) + die("invalid clean status scoped-history test barrier"); +#endif + } else + clean_status_capture_external_history_source( + the_repository->index); + } + if (normal_clean_query && optional_status_writes && + clean_status_identity_is_durable() && + (reissue_clean_sidecar || + clean_status_external_history_was_restored( + the_repository->index))) + s.certify_clean_status = 1; + wt_status_start_untracked_cache_preload(&s); + wt_status_refresh_index( + &s, REFRESH_QUIET | REFRESH_UNMERGED | progress_flag | + REFRESH_DEFER_BULK_DIRTY, + s.show_untracked_files != SHOW_NO_UNTRACKED_FILES && + !s.show_ignored_mode); + + if (optional_status_writes) fd = repo_hold_locked_index(the_repository, &index_lock, 0); else fd = -1; + s.bulk_update_index_stat = + 0 <= fd && + the_repository->index->preload_bulk_tracked_nr == + the_repository->index->cache_nr; s.is_initial = repo_get_oid(the_repository, s.reference, &oid) ? 1 : 0; if (!s.is_initial) oidcpy(&s.oid_commit, &oid); - s.ignore_submodule_arg = ignore_submodule_arg; s.status_format = status_format; s.verbose = verbose; if (no_renames != -1) @@ -1654,8 +2128,146 @@ struct repository *repo UNUSED) wt_status_collect(&s); - if (0 <= fd) - repo_update_index_if_able(the_repository, &index_lock); + if (0 <= fd && guarded_scoped_history_source && + !clean_status_index_snapshot_still_matches_proof_epoch( + &scoped_history_source, the_repository->index)) { + rollback_lock_file(&index_lock); + fd = -1; + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-epoch-mismatch", 1); + } + if (0 <= fd && deferred_scoped_history) { + if (clean_status_scoped_history_can_rollback( + &s, &scoped_history_source)) { + rollback_lock_file(&index_lock); + fd = -1; + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-capture-skipped", 1); + } else { + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-repair-required", 1); + if (!clean_status_capture_external_history_source_from_snapshot( + the_repository->index, &scoped_history_source)) + deferred_scoped_history = 0; + else { + rollback_lock_file(&index_lock); + fd = -1; + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-epoch-mismatch", 1); + } + } + } + if (0 <= fd && !deferred_scoped_history) { + int external_restored = + clean_status_external_history_was_restored( + the_repository->index); + int external_saved = 0; + int persist_restored_boundary = 0; + int preserve_entry_changes = + (!external_restored && + (the_repository->index->cache_changed & CE_ENTRY_CHANGED)) || + the_repository->index->fsmonitor_untracked_must_persist; + int deferred_history = preserve_entry_changes && + !external_restored && + clean_status_has_recovered_tracked_stat( + the_repository->index); + int preserve_history_witness = external_restored && + clean_status_external_history_needs_witness_preservation( + the_repository->index); + + /* + * Publish resumable history before the physical clean proof. + * A later foreign index rewrite can only recover the proof if + * both files name the same provider boundary. + * + * CSH1 carries acceleration state, not refreshed stat data. + * A fresh checkpoint names the pre-repair physical index; do + * not let publishing it roll back the write which makes an + * entry repair durable. Restored checkpoints stay no-spill + * for foreign index writers. + */ + if (!deferred_history && !preserve_history_witness) + external_saved = clean_status_save_external_history( + the_repository->index); + else if (deferred_history && + !hook_exists(the_repository, "post-index-change")) + save_history_after_write = 1; + if (external_restored && !external_saved && + clean_status_external_history_owns_index( + the_repository->index) && + has_racy_timestamp(the_repository->index)) { + persist_restored_boundary = 1; + trace2_data_intmax("fsmonitor", the_repository, + "history/external-racy-index-persisted", 1); + } + reissue_after_write = normal_clean_query && + reissue_clean_sidecar && preserve_entry_changes && + !external_restored && !persist_restored_boundary && + !hook_exists(the_repository, "post-index-change"); + + if (the_repository->index->fsmonitor_legacy_untracked_fallback && + !preserve_entry_changes && !external_saved) { + rollback_lock_file(&index_lock); + fd = -1; + } else if (exact_clean_query) { + if (!preserve_entry_changes && external_saved && + clean_status_issue_sidecar( + &s, &clean_digest, &index_lock, 0)) + fd = -1; + else if (!preserve_entry_changes && + !persist_restored_boundary && + (external_restored || external_saved)) { + rollback_lock_file(&index_lock); + fd = -1; + } + } else if (!preserve_entry_changes && + !persist_restored_boundary && + normal_clean_query && + (external_restored || reissue_clean_sidecar) && + clean_status_issue_sidecar( + &s, &clean_digest, &index_lock, 1)) { + fd = -1; + } else if (!preserve_entry_changes && + !persist_restored_boundary && + (external_restored || external_saved)) { + rollback_lock_file(&index_lock); + fd = -1; + } + } + if (0 <= fd && guarded_scoped_history_source && + !clean_status_index_snapshot_still_matches_proof_epoch( + &scoped_history_source, the_repository->index)) { + rollback_lock_file(&index_lock); + fd = -1; + trace2_data_intmax("fsmonitor", the_repository, + "history/scoped-source-epoch-mismatch", 1); + } + if (0 <= fd) { + repo_update_index_if_able_with_receipt(the_repository, &index_lock, + &written_index); + clean_status_index_adopt_write_receipt(the_repository->index, + &written_index); + if (save_history_after_write && + !hook_exists(the_repository, "post-index-change") && + repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { + if (clean_status_save_external_history( + the_repository->index)) + trace2_data_intmax("fsmonitor", the_repository, + "history/external-postwrite-stored", 1); + rollback_lock_file(&index_lock); + } + if (reissue_after_write && + repo_hold_locked_index(the_repository, &index_lock, 0) >= 0) { + if (clean_status_issue_sidecar( + &s, &clean_digest, &index_lock, 1)) + trace2_data_intmax("status", the_repository, + "clean-proof/postwrite-reissued", 1); + else + rollback_lock_file(&index_lock); + } + } + clean_status_index_write_receipt_release(&written_index); + clean_status_index_snapshot_release(&scoped_history_source); if (s.relative_paths) s.prefix = prefix; @@ -1701,6 +2313,7 @@ int cmd_commit(int argc, struct repository *repo UNUSED) { static struct wt_status s; + struct clean_status_config_digest clean_digest; static const char *cleanup_arg = NULL; static struct option builtin_commit_options[] = { OPT__QUIET(&quiet, N_("suppress summary after successful commit")), @@ -1805,7 +2418,14 @@ int cmd_commit(int argc, prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; - status_init_config(&s, git_commit_config); + clean_status_config_init(&clean_digest, the_repository->hash_algo); + status_init_config_with_clean_digest( + &s, git_commit_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_set_config_digest(the_repository, &clean_digest); + if (!getenv(INDEX_ENVIRONMENT) && fstat_is_reliable() && + fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_IPC) + clean_status_enable_external_history(the_repository); s.commit_template = 1; status_format = STATUS_FORMAT_NONE; /* Ignore status.short */ s.colopts = 0; @@ -1835,7 +2455,7 @@ int cmd_commit(int argc, if (dry_run) return dry_run_commit(argv, prefix, current_head, &s); - index_file = prepare_index(argv, prefix, current_head, 0); + index_file = prepare_index(argv, prefix, current_head, 0, &s); /* Set up everything for writing the commit object. This includes running hooks, writing the trees, and interacting with the user. */ @@ -1845,6 +2465,7 @@ int cmd_commit(int argc, rollback_index_files(); goto cleanup; } + wt_status_collect_free_buffers(&s); /* Determine parents */ reflog_msg = getenv("GIT_REFLOG_ACTION"); @@ -1983,6 +2604,8 @@ int cmd_commit(int argc, NULL, NULL, NULL, NULL); cleanup: + release_commit_checkpoint(); + wt_status_collect_free_buffers(&s); free_commit_extra_headers(extra); commit_list_free(parents); strbuf_release(&author_ident); diff --git a/builtin/describe.c b/builtin/describe.c index c0abc931a5948d..a2d8c60e16e0e5 100644 --- a/builtin/describe.c +++ b/builtin/describe.c @@ -2,6 +2,8 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -598,11 +600,19 @@ static int option_parse_exact_match(const struct option *opt, const char *arg, return 0; } +static int describe_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + int cmd_describe(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED ) { + struct clean_status_config_digest clean_digest; struct refs_for_each_ref_options for_each_ref_opts = { .flags = REFS_FOR_EACH_INCLUDE_BROKEN, }; @@ -647,7 +657,11 @@ int cmd_describe(int argc, OPT_END(), }; - repo_config(the_repository, git_default_config, NULL); + show_usage_with_options_if_asked(argc, argv, describe_usage, options); + + clean_status_config_init(&clean_digest, the_repository->hash_algo); + repo_config(the_repository, describe_config, &clean_digest); + clean_status_config_final(&clean_digest); argc = parse_options(argc, argv, prefix, options, describe_usage, 0); if (abbrev < 0) abbrev = DEFAULT_ABBREV; @@ -727,14 +741,23 @@ int cmd_describe(int argc, if (broken) { struct child_process cp = CHILD_PROCESS_INIT; - strvec_pushv(&cp.args, update_index_args); - cp.git_cmd = 1; - cp.no_stdin = 1; - cp.no_stdout = 1; - run_command(&cp); - - child_process_init(&cp); - strvec_pushv(&cp.args, diff_index_args); + if (use_optional_locks()) { + strvec_pushv(&cp.args, update_index_args); + cp.git_cmd = 1; + cp.no_stdin = 1; + cp.no_stdout = 1; + run_command(&cp); + + child_process_init(&cp); + strvec_pushv(&cp.args, diff_index_args); + } else { + strvec_pushl(&cp.args, "-c", + "diff.autoRefreshIndex=true", + "diff", "--quiet", + "--no-ext-diff", "--no-textconv", + "--ignore-submodules=untracked", + "HEAD", "--", NULL); + } cp.git_cmd = 1; cp.no_stdin = 1; cp.no_stdout = 1; @@ -761,13 +784,23 @@ int cmd_describe(int argc, setup_work_tree(the_repository); prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; + /* + * The in-process dirty check only refreshes stat + * data before comparing the worktree with HEAD. + */ + clean_status_set_config_digest(the_repository, + &clean_digest); + prepare_diff_external_history(the_repository); repo_read_index(the_repository); refresh_index(the_repository->index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, NULL); - fd = repo_hold_locked_index(the_repository, - &index_lock, 0); - if (0 <= fd) - repo_update_index_if_able(the_repository, &index_lock); + if (use_optional_locks()) { + fd = repo_hold_locked_index(the_repository, + &index_lock, 0); + if (0 <= fd) + repo_update_index_if_able(the_repository, + &index_lock); + } repo_init_revisions(the_repository, &revs, prefix); diff --git a/builtin/diff-files.c b/builtin/diff-files.c index ea91347ce23beb..267bbe3a41e6b8 100644 --- a/builtin/diff-files.c +++ b/builtin/diff-files.c @@ -12,6 +12,7 @@ #include "diff.h" #include "diff-merges.h" #include "commit.h" +#include "fsmonitor.h" #include "preload-index.h" #include "revision.h" @@ -27,6 +28,7 @@ int cmd_diff_files(int argc, { struct rev_info rev; int result; + int scoped_bootstrap; unsigned options = 0; show_usage_if_asked(argc, argv, diff_files_usage); @@ -84,8 +86,15 @@ int cmd_diff_files(int argc, (rev.diffopt.output_format & DIFF_FORMAT_PATCH)) diff_merges_set_dense_combined_if_unset(&rev); + prepare_diff_external_history(the_repository); + scoped_bootstrap = + diff_has_bounded_regular_pathspec(&rev.diffopt.pathspec); + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(the_repository->index); if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) die_errno("repo_read_index_preload"); + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(the_repository->index); run_diff_files(&rev, options); result = diff_result_code(&rev); release_revisions(&rev); diff --git a/builtin/diff-index.c b/builtin/diff-index.c index 3db7cffede578c..701e900f78086f 100644 --- a/builtin/diff-index.c +++ b/builtin/diff-index.c @@ -6,6 +6,7 @@ #include "diff.h" #include "diff-merges.h" #include "commit.h" +#include "fsmonitor.h" #include "preload-index.h" #include "revision.h" #include "setup.h" @@ -25,6 +26,7 @@ int cmd_diff_index(int argc, unsigned int option = 0; int i; int result; + int scoped_bootstrap; show_usage_if_asked(argc, argv, diff_cache_usage); @@ -68,16 +70,28 @@ int cmd_diff_index(int argc, if (rev.pending.nr != 1 || rev.max_count != -1 || rev.min_age != -1 || rev.max_age != -1) usage(diff_cache_usage); - if (!(option & DIFF_INDEX_CACHED)) { + prepare_diff_external_history(the_repository); + if (!(option & DIFF_INDEX_CACHED)) setup_work_tree(the_repository); + scoped_bootstrap = (option & DIFF_INDEX_CACHED) || + diff_has_bounded_regular_pathspec(&rev.diffopt.pathspec); + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(the_repository->index); + if (!(option & DIFF_INDEX_CACHED)) { if (repo_read_index_preload(the_repository, &rev.diffopt.pathspec, 0) < 0) { + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(the_repository->index); perror("repo_read_index_preload"); return -1; } } else if (repo_read_index(the_repository) < 0) { + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(the_repository->index); perror("repo_read_index"); return -1; } + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(the_repository->index); run_diff_index(&rev, option); result = diff_result_code(&rev); release_revisions(&rev); diff --git a/builtin/diff.c b/builtin/diff.c index 18b1083e984a35..e1c5d6f1f493a9 100644 --- a/builtin/diff.c +++ b/builtin/diff.c @@ -8,23 +8,36 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "attr-fingerprint.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-sidecar.h" #include "config.h" #include "ewah/ewok.h" #include "lockfile.h" #include "color.h" #include "commit.h" +#include "environment.h" #include "gettext.h" +#include "fsmonitor-ll.h" +#include "fsmonitor.h" +#include "fsmonitor-settings.h" #include "tag.h" +#include "trace2.h" #include "diff.h" #include "diff-merges.h" #include "diffcore.h" +#include "dir.h" #include "preload-index.h" #include "read-cache-ll.h" #include "revision.h" #include "log-tree.h" #include "setup.h" +#include "thread-utils.h" #include "oid-array.h" #include "tree.h" +#include "worktree.h" +#include "wt-status.h" #define DIFF_NO_INDEX_EXPLICIT 1 #define DIFF_NO_INDEX_IMPLICIT 2 @@ -39,6 +52,8 @@ static const char builtin_diff_usage[] = "\n" COMMON_DIFF_OPTIONS_HELP; +static int scoped_diff_bootstrap_used; + static const char *blob_path(struct object_array_entry *entry) { return entry->path ? entry->path : entry->name; @@ -140,6 +155,8 @@ static void builtin_diff_index(struct rev_info *revs, int argc, const char **argv) { unsigned int option = 0; + int scoped_bootstrap; + while (1 < argc) { const char *arg = argv[1]; if (!strcmp(arg, "--cached") || !strcmp(arg, "--staged")) @@ -158,8 +175,13 @@ static void builtin_diff_index(struct rev_info *revs, revs->max_count != -1 || revs->min_age != -1 || revs->max_age != -1) usage(builtin_diff_usage); - if (!(option & DIFF_INDEX_CACHED)) { + if (!(option & DIFF_INDEX_CACHED)) setup_work_tree(the_repository); + scoped_bootstrap = (option & DIFF_INDEX_CACHED) || + diff_has_bounded_regular_pathspec(&revs->diffopt.pathspec); + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(the_repository->index); + if (!(option & DIFF_INDEX_CACHED)) { if (repo_read_index_preload(the_repository, &revs->diffopt.pathspec, 0) < 0) { die_errno("repo_read_index_preload"); @@ -167,6 +189,9 @@ static void builtin_diff_index(struct rev_info *revs, } else if (repo_read_index(the_repository) < 0) { die_errno("repo_read_cache"); } + if (scoped_bootstrap) + scoped_diff_bootstrap_used |= + fsmonitor_end_scoped_bootstrap(the_repository->index); run_diff_index(revs, option); } @@ -234,24 +259,221 @@ static void builtin_diff_combined(struct rev_info *revs, oid_array_clear(&parents); } +static pthread_mutex_t diff_refresh_warning_mutex; +static int diff_refresh_warning_seen; + +static void capture_diff_refresh_warning(const char *message UNUSED, + va_list params UNUSED) +{ + pthread_mutex_lock(&diff_refresh_warning_mutex); + diff_refresh_warning_seen = 1; + pthread_mutex_unlock(&diff_refresh_warning_mutex); +} + +static int can_close_diff_fsmonitor_token(struct index_state *istate) +{ + return fsmonitor_pending_token_from_provider(istate) && + fstat_is_reliable() && + the_repository->config_values_private_.trust_ctime && + the_repository->config_values_private_.check_stat && + !getenv(INDEX_ENVIRONMENT) && + !istate->split_index && istate->sparse_index == INDEX_EXPANDED && + !unmerged_index(istate) && + fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_IPC; +} + +static int reuse_diff_recovery_observations( + struct index_state *istate, + const struct clean_status_index_snapshot *source) +{ + struct attr_source_snapshot *attrs = NULL; + struct clean_status_proof_epoch *epoch = NULL; + int reused = 0; + unsigned int i; + + if (!clean_status_index_snapshot_still_matches_proof_epoch( + source, istate) || + !clean_status_index_can_reuse_source_logical_hash(istate) || + !clean_status_fsmonitor_semantic_baseline_pending(istate) || + clean_status_fsmonitor_strong_mismatch(istate) || + clean_status_filter_scope_needs_validation(istate) || + clean_status_manifest_global_fallback(istate) || + clean_status_worktree_manifest_needs_refresh(istate) || + clean_status_capture_attr_snapshot(istate, &attrs) || !attrs) + goto done; + + epoch = clean_status_capture_proof_epoch(istate, attrs, 0); + if (!epoch || !clean_status_proof_epoch_prime_matches(istate, epoch)) + goto done; + + /* Observations made before the proof epoch cannot certify tracked files. */ + for (i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags &= + ~(CE_UPTODATE | CE_FSMONITOR_VALID); + preload_index_bulk_result_clear(istate); + reused = 1; + trace2_data_intmax("diff", istate->repo, + "recovery/reused-provider-observations", 1); + +done: + clean_status_release_proof_epoch(epoch); + attr_source_snapshot_free(attrs); + return reused; +} + static void refresh_index_quietly(void) { struct lock_file lock_file = LOCK_INIT; + struct index_state *istate = the_repository->index; + int can_close_token; + int preserve_untracked; int fd; + int refreshed; + + if (!use_optional_locks() || + fsm_settings__is_watch_limit_backoff(the_repository)) + return; + can_close_token = can_close_diff_fsmonitor_token(istate); + if (can_close_token && + !repo_config_values(the_repository)->apply_sparse_checkout && + istate->untracked && + istate->untracked->root && + istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + !istate->fsmonitor_untracked_valid) { + struct clean_status_index_snapshot source = { .fd = -1 }; + struct clean_status_config_digest digest; + struct object_id exclude_digest; + struct stat scanned_worktree; + struct wt_status status; + report_fn original_warning; + int proof_complete; + int warning_seen; + + if (clean_status_index_snapshot_open_allow_null_checksum( + &source, repo_get_index_file(the_repository), + the_repository->hash_algo)) + return; + if (clean_status_config_read_repository(the_repository, + &digest)) { + clean_status_index_snapshot_release(&source); + return; + } + clean_status_set_config_digest(the_repository, &digest); + if (!reuse_diff_recovery_observations(istate, &source)) { + discard_index(istate); + repo_read_index(the_repository); + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + &source, istate)) { + clean_status_index_snapshot_release(&source); + return; + } + refresh_fsmonitor(istate); + if (!can_close_diff_fsmonitor_token(istate) || + repo_config_values(the_repository)->apply_sparse_checkout || + !istate->untracked || !istate->untracked->root || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + istate->fsmonitor_untracked_valid) { + clean_status_index_snapshot_release(&source); + return; + } + if (HAVE_THREADS && + pthread_mutex_init(&diff_refresh_warning_mutex, NULL)) { + clean_status_index_snapshot_release(&source); + return; + } + diff_refresh_warning_seen = 0; + original_warning = get_warn_routine(); + set_warn_routine(capture_diff_refresh_warning); + wt_status_prepare(the_repository, &status); + status.certify_clean_status = 1; + status.show_untracked_files = istate->untracked->dir_flags ? + SHOW_NORMAL_UNTRACKED_FILES : SHOW_ALL_UNTRACKED_FILES; + wt_status_start_untracked_cache_preload(&status); + wt_status_refresh_index(&status, + REFRESH_QUIET | REFRESH_UNMERGED | + REFRESH_DEFER_BULK_DIRTY, 1); + proof_complete = !status.certify_untracked_scan_failed && + !wt_status_certified_excludes_digest( + &status, &exclude_digest, &scanned_worktree) && + !fsmonitor_has_pending_token(istate) && + istate->fsmonitor_untracked_valid && + istate->fsmonitor_last_update && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token) && + (!istate->clean_status || + clean_status_revalidated_token_matches(istate)); + wt_status_collect_free_buffers(&status); + set_warn_routine(original_warning); + pthread_mutex_lock(&diff_refresh_warning_mutex); + warning_seen = diff_refresh_warning_seen; + pthread_mutex_unlock(&diff_refresh_warning_mutex); + pthread_mutex_destroy(&diff_refresh_warning_mutex); + string_list_clear(&status.untracked, 0); + string_list_clear(&status.ignored, 0); + free(status.branch); + if (!proof_complete || warning_seen) { + clean_status_index_snapshot_release(&source); + return; + } + fd = repo_hold_locked_index(the_repository, &lock_file, 0); + if (fd < 0) { + clean_status_index_snapshot_release(&source); + return; + } + if (!clean_status_index_snapshot_still_matches_path( + &source, repo_get_index_file(the_repository), + the_repository->hash_algo)) { + rollback_lock_file(&lock_file); + clean_status_index_snapshot_release(&source); + return; + } + repo_update_index_if_able(the_repository, &lock_file); + clean_status_index_snapshot_release(&source); + return; + } fd = repo_hold_locked_index(the_repository, &lock_file, 0); if (fd < 0) return; - discard_index(the_repository->index); + discard_index(istate); repo_read_index(the_repository); - refresh_index(the_repository->index, REFRESH_QUIET|REFRESH_UNMERGED, NULL, NULL, - NULL); + can_close_token = can_close_diff_fsmonitor_token(istate); + refreshed = refresh_index(istate, + REFRESH_QUIET | REFRESH_UNMERGED | + (can_close_token ? REFRESH_IN_PROOF_EPOCH : 0), + NULL, NULL, NULL); + preserve_untracked = istate->untracked && + istate->untracked->fsmonitor_revalidation; + /* A complete tracked refresh cannot also authenticate untracked files. */ + if (!refreshed && can_close_token && + fsmonitor_pending_token_from_provider(istate) && + fsmonitor_query_pending_token(istate, 0) == FSMONITOR_TOKEN_CLEAN) { + clean_status_mark_fsmonitor_config_valid( + istate, istate->fsmonitor_last_update_pending); + fsmonitor_accept_pending_token(istate, 0, 0); + if (preserve_untracked && + clean_status_revalidated_token_matches(istate)) { + /* + * Preserve directory snapshots only as candidates. Their + * pending proof requires a full untracked revalidation. + */ + istate->untracked->fsmonitor_revalidation = 1; + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + clean_status_begin_fsmonitor_semantic_baseline(istate); + } + } repo_update_index_if_able(the_repository, &lock_file); } static void builtin_diff_files(struct rev_info *revs, int argc, const char **argv) { unsigned int options = 0; + int scoped_bootstrap; while (1 < argc && argv[1][0] == '-') { if (!strcmp(argv[1], "--base")) @@ -282,10 +504,17 @@ static void builtin_diff_files(struct rev_info *revs, int argc, const char **arg diff_merges_set_dense_combined_if_unset(revs); setup_work_tree(the_repository); + scoped_bootstrap = + diff_has_bounded_regular_pathspec(&revs->diffopt.pathspec); + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(the_repository->index); if (repo_read_index_preload(the_repository, &revs->diffopt.pathspec, 0) < 0) { die_errno("repo_read_index_preload"); } + if (scoped_bootstrap) + scoped_diff_bootstrap_used |= + fsmonitor_end_scoped_bootstrap(the_repository->index); run_diff_files(revs, options); } @@ -396,6 +625,27 @@ static void symdiff_release(struct symdiff *sdiff) bitmap_free(sdiff->skip); } +void prepare_diff_external_history(struct repository *repo) +{ + struct clean_status_config_digest digest; + struct worktree *worktree = NULL; + + if (!fstat_is_reliable() || getenv(INDEX_ENVIRONMENT) || + is_bare_repository(repo) || !repo_get_work_tree(repo) || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC || + repo_config_values(repo)->apply_sparse_checkout) + goto done; + worktree = get_current_worktree(repo); + if (!worktree || + clean_status_config_read_repository(repo, &digest)) + goto done; + clean_status_set_config_digest(repo, &digest); + clean_status_enable_external_history(repo); + +done: + free_worktree(worktree); +} + int cmd_diff(int argc, const char **argv, const char *prefix, @@ -411,6 +661,8 @@ int cmd_diff(int argc, int result; struct symdiff sdiff; + scoped_diff_bootstrap_used = 0; + /* * We could get N tree-ish in the rev.pending_objects list. * Also there could be M blobs there, and P pathspecs. --cached may @@ -533,6 +785,7 @@ int cmd_diff(int argc, if (nongit) die(_("Not a git repository")); + prepare_diff_external_history(the_repository); argc = setup_revisions(argc, argv, &rev, NULL); if (!rev.diffopt.output_format) { rev.diffopt.output_format = DIFF_FORMAT_PATCH; @@ -646,7 +899,8 @@ int cmd_diff(int argc, ent.objects, ent.nr, first_non_parent); result = diff_result_code(&rev); - if (1 < rev.diffopt.skip_stat_unmatch) + if (1 < rev.diffopt.skip_stat_unmatch && + !scoped_diff_bootstrap_used) refresh_index_quietly(); release_revisions(&rev); object_array_clear(&ent); diff --git a/builtin/fsmonitor--daemon.c b/builtin/fsmonitor--daemon.c index 4161dd82825b4c..1c53a5af4dd6df 100644 --- a/builtin/fsmonitor--daemon.c +++ b/builtin/fsmonitor--daemon.c @@ -42,9 +42,15 @@ static int fsmonitor__start_timeout_sec = 60; #define FSMONITOR__ANNOUNCE_STARTUP "fsmonitor.announcestartup" static int fsmonitor__announce_startup = 0; +struct fsmonitor_config_data { + unsigned int ignore_start_timeout : 1; +}; + static int fsmonitor_config(const char *var, const char *value, const struct config_context *ctx, void *cb) { + struct fsmonitor_config_data *data = cb; + if (!strcmp(var, FSMONITOR__IPC_THREADS)) { int i = git_config_int(var, value, ctx->kvi); if (i < 1) @@ -55,7 +61,12 @@ static int fsmonitor_config(const char *var, const char *value, } if (!strcmp(var, FSMONITOR__START_TIMEOUT)) { - int i = git_config_int(var, value, ctx->kvi); + int i; + + /* The run process does not consume this client-only setting. */ + if (data && data->ignore_start_timeout) + return 0; + i = git_config_int(var, value, ctx->kvi); if (i < 0) return error(_("value of '%s' out of range: %d"), FSMONITOR__START_TIMEOUT, i); @@ -73,7 +84,7 @@ static int fsmonitor_config(const char *var, const char *value, return 0; } - return git_default_config(var, value, ctx, cb); + return git_default_config(var, value, ctx, NULL); } /* @@ -399,6 +410,13 @@ static struct fsmonitor_token_data *fsmonitor_new_token_data(void) if (test_env_value < 0) test_env_value = git_env_bool("GIT_TEST_FSMONITOR_TOKEN", 0); +#ifdef __APPLE__ + strbuf_addstr(&token->token_id, + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX); +#endif + strbuf_addstr(&token->token_id, + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX); + if (!test_env_value) { struct timeval tv; struct tm tm; @@ -694,18 +712,63 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, int do_trivial = 0; int do_flush = 0; int do_cookie = 0; + int invalid_binding = 0; + int hardlink_aware_query = 0; enum fsmonitor_cookie_item_result cookie_result; + if (strcmp(command, "quit") && + strcmp(command, "flush") && + strcmp(command, FSMONITOR_IPC_CAPABILITY_COMMAND) && + !starts_with(command, "builtin:")) { + const char *identity; + const char *query; + + if (skip_prefix(command, FSMONITOR_IPC_HARDLINK_QUERY_PREFIX, + &identity)) + hardlink_aware_query = 1; + else if (!skip_prefix(command, FSMONITOR_IPC_QUERY_PREFIX, + &identity)) + identity = NULL; + + if (!identity || + !(query = strchr(identity, '\n')) || + query - identity != FSMONITOR_IPC_WORKTREE_ID_HEX || + state->worktree_identity.len != FSMONITOR_IPC_WORKTREE_ID_HEX || + memcmp(identity, state->worktree_identity.buf, + FSMONITOR_IPC_WORKTREE_ID_HEX)) { + invalid_binding = 1; + trace2_data_intmax("fsmonitor", the_repository, + "query/worktree-mismatch", 1); + } else { + command = query + 1; + } + } + /* * We expect `command` to be of the form: * - * := quit NUL + * := get-capabilities NUL + * | quit NUL * | flush NUL * | NUL * | NUL */ - if (!strcmp(command, "quit")) { + if (!strcmp(command, FSMONITOR_IPC_CAPABILITY_COMMAND)) { + static const char capabilities[] = + FSMONITOR_IPC_QUERY_VERSION "\n" + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY "\n" +#ifdef __APPLE__ + FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" + FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" +#endif + ; + + return reply(reply_data, capabilities, + sizeof(capabilities) - 1); + + } else if (!strcmp(command, "quit")) { /* * A client has requested over the socket/pipe that the * daemon shutdown. @@ -728,6 +791,11 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, do_flush = 1; do_trivial = 1; + } else if (invalid_binding) { + /* Never trust a token from an unbound or different worktree. */ + do_trivial = 1; + do_cookie = 1; + } else if (!skip_prefix(command, "builtin:", &p)) { /* assume V1 timestamp or garbage */ @@ -787,6 +855,14 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, error(_("fsmonitor: cookie_result '%d' != SEEN"), cookie_result); do_trivial = 1; + /* + * This boundary could not be synchronized. Retire it so + * a later successful cookie cannot make an old client's + * token appear complete again. An aborted cookie already + * belongs to a listener-initiated reset. + */ + if (cookie_result == FCIR_ERROR) + do_flush = 1; } } @@ -896,6 +972,10 @@ static int do_handle_client(struct fsmonitor_daemon_state *state, const char *s = batch->interned_paths[k]; size_t s_len; + if (!hardlink_aware_query && + starts_with(s, FSMONITOR_PATH_HARDLINK_INODE_PREFIX)) + s = FSMONITOR_PATH_GLOBAL_INVALIDATE; + if (!strset_add(&shown, s)) duplicates++; else { @@ -1311,6 +1391,12 @@ static int fsmonitor_run_daemon(void) strbuf_init(&state.path_worktree_watch, 0); strbuf_addstr(&state.path_worktree_watch, absolute_path(repo_get_work_tree(the_repository))); + strbuf_init(&state.worktree_identity, 0); + if (fsmonitor_ipc__get_worktree_identity(the_repository, + &state.worktree_identity)) { + err = error(_("could not identify worktree root")); + goto done; + } state.nr_paths_watching = 1; strbuf_init(&state.alias.alias, 0); @@ -1437,6 +1523,7 @@ static int fsmonitor_run_daemon(void) ipc_server_free(state.ipc_server_data); strbuf_release(&state.path_worktree_watch); + strbuf_release(&state.worktree_identity); strbuf_release(&state.path_gitdir_watch); strbuf_release(&state.path_cookie_prefix); strbuf_release(&state.path_ipc); @@ -1570,6 +1657,9 @@ int cmd_fsmonitor__daemon(int argc, const char *prefix, struct repository *repo UNUSED) { + struct fsmonitor_config_data config_data = { + .ignore_start_timeout = argc > 1 && !strcmp(argv[1], "run"), + }; const char *subcmd; enum fsmonitor_reason reason; int detach_console = 0; @@ -1586,7 +1676,7 @@ int cmd_fsmonitor__daemon(int argc, OPT_END() }; - repo_config(the_repository, fsmonitor_config, NULL); + repo_config(the_repository, fsmonitor_config, &config_data); argc = parse_options(argc, argv, prefix, options, builtin_fsmonitor__daemon_usage, 0); diff --git a/builtin/gc.c b/builtin/gc.c index 49c8474fade8ed..e9572940dcb22d 100644 --- a/builtin/gc.c +++ b/builtin/gc.c @@ -1593,7 +1593,8 @@ static int maintenance_task_geometric_repack(struct maintenance_run_opts *opts, child.odb_to_close = the_repository->objects; strvec_pushl(&child.args, "repack", "-d", "-l", NULL); - if (geometry.split < geometry.pack_nr) + if (geometry.split < geometry.pack_nr || + geometry.promisor_split < geometry.promisor_pack_nr) strvec_pushf(&child.args, "--geometric=%d", geometry.split_factor); else @@ -1648,7 +1649,7 @@ static int geometric_repack_auto_condition(struct gc_config *cfg UNUSED) * When we'd merge at least two packs with one another we always * perform the repack. */ - if (geometry.split) { + if (geometry.split || geometry.promisor_split) { ret = 1; goto out; } diff --git a/builtin/ls-files.c b/builtin/ls-files.c index b044520f9e3c39..2d5b6bbade54e9 100644 --- a/builtin/ls-files.c +++ b/builtin/ls-files.c @@ -12,6 +12,7 @@ #include "config.h" #include "convert.h" #include "environment.h" +#include "fsmonitor.h" #include "quote.h" #include "dir.h" #include "gettext.h" @@ -587,6 +588,20 @@ static int option_parse_exclude_standard(const struct option *opt, return 0; } +static int ls_files_is_index_only(const struct dir_struct *dir, int show_tag) +{ + if (show_deleted || show_others || show_unmerged || + show_resolve_undo || show_modified || show_killed || + show_valid_bit || show_fsmonitor_bit || show_eol || + recurse_submodules || show_tag || debug_mode || + with_tree || format || exc_given || dir->exclude_per_dir || + (dir->flags & DIR_SHOW_IGNORED) || + (pathspec.magic & PATHSPEC_ATTR)) + return 0; + + return 1; +} + int cmd_ls_files(int argc, const char **argv, const char *cmd_prefix, @@ -666,6 +681,7 @@ int cmd_ls_files(int argc, OPT_END() }; int ret = 0; + int scoped_bootstrap; show_usage_with_options_if_asked(argc, argv, ls_files_usage, builtin_ls_files_options); @@ -678,11 +694,17 @@ int cmd_ls_files(int argc, prefix_len = strlen(prefix); repo_config(repo, git_default_config, NULL); + argc = parse_options(argc, argv, prefix, builtin_ls_files_options, + ls_files_usage, 0); + parse_pathspec(&pathspec, 0, PATHSPEC_PREFER_CWD, prefix, argv); + scoped_bootstrap = ls_files_is_index_only(&dir, show_tag); + if (scoped_bootstrap) + fsmonitor_begin_scoped_bootstrap(repo->index); if (repo_read_index(repo) < 0) die("index file corrupt"); + if (scoped_bootstrap) + fsmonitor_end_scoped_bootstrap(repo->index); - argc = parse_options(argc, argv, prefix, builtin_ls_files_options, - ls_files_usage, 0); pl = add_pattern_list(&dir, EXC_CMDL, "--exclude option"); for (i = 0; i < exclude_list.nr; i++) { add_pattern(exclude_list.items[i].string, "", 0, pl, --exclude_args); @@ -729,10 +751,6 @@ int cmd_ls_files(int argc, die("ls-files --recurse-submodules does not support " "--error-unmatch"); - parse_pathspec(&pathspec, 0, - PATHSPEC_PREFER_CWD, - prefix, argv); - /* * Find common prefix for all pathspec's * This is used as a performance optimization which unfortunately cannot diff --git a/builtin/merge.c b/builtin/merge.c index 58d1b7bb07d90f..c7e8a31208a386 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -13,6 +13,8 @@ #include "abspath.h" #include "advice.h" +#include "clean-status-config.h" +#include "clean-status.h" #include "config.h" #include "editor.h" #include "environment.h" @@ -1373,6 +1375,7 @@ int cmd_merge(int argc, struct commit_list *common = NULL; const char *best_strategy = NULL, *wt_strategy = NULL; struct commit_list *remoteheads = NULL, *p; + struct clean_status_config_digest clean_digest; void *branch_to_free; int orig_argc = argc; int merge_log_config = -1; @@ -1469,6 +1472,12 @@ int cmd_merge(int argc, goto done; } + if (fast_forward != FF_NO && !getenv(INDEX_ENVIRONMENT) && + !clean_status_config_read_repository(the_repository, &clean_digest)) { + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } + if (repo_read_index_unmerged(the_repository)) die_resolve_conflict("merge"); diff --git a/builtin/mv.c b/builtin/mv.c index a82fc97a19f6ee..150c77846a65c3 100644 --- a/builtin/mv.c +++ b/builtin/mv.c @@ -10,6 +10,8 @@ #include "builtin.h" #include "abspath.h" #include "advice.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -205,11 +207,20 @@ static int pathmap_cmp(const void *cmp_data UNUSED, return fspathcmp(e1->path, e2->path); } +static int mv_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + int cmd_mv(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; + int preserve_clean_history = !getenv(INDEX_ENVIRONMENT); int i, flags, gitmodules_modified = 0; int verbose = 0, show_only = 0, force = 0, ignore_errors = 0, ignore_sparse = 0; struct option builtin_mv_options[] = { @@ -240,7 +251,19 @@ int cmd_mv(int argc, int ret; struct repo_config_values *cfg = repo_config_values(the_repository); - repo_config(the_repository, git_default_config, NULL); + show_usage_with_options_if_asked(argc, argv, + builtin_mv_usage, builtin_mv_options); + + if (preserve_clean_history) { + clean_status_config_init(&clean_digest, + the_repository->hash_algo); + repo_config(the_repository, mv_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_set_config_digest(the_repository, &clean_digest); + clean_status_enable_external_history(the_repository); + } else { + repo_config(the_repository, git_default_config, NULL); + } argc = parse_options(argc, argv, prefix, builtin_mv_options, builtin_mv_usage, 0); @@ -571,6 +594,28 @@ int cmd_mv(int argc, the_repository->index->cache[pos], &st, 0); + if (preserve_clean_history) { + struct cache_entry *old_entry = + the_repository->index->cache[pos]; + struct cache_entry *new_entry; + size_t dstlen = strlen(dst); + int safe; + + new_entry = make_empty_cache_entry( + the_repository->index, dstlen); + copy_cache_entry(new_entry, old_entry); + new_entry->ce_namelen = dstlen; + new_entry->index = 0; + memcpy(new_entry->name, dst, dstlen + 1); + safe = clean_status_index_entry_is_semantically_safe( + the_repository->index, old_entry, NULL) && + clean_status_index_entry_is_semantically_safe( + the_repository->index, NULL, new_entry); + discard_cache_entry(new_entry); + if (!safe) + clean_status_invalidate_current_proof( + the_repository->index); + } rename_index_entry_at(the_repository->index, pos, dst); if (ignore_sparse && diff --git a/builtin/pull.c b/builtin/pull.c index db3ee0aab3ed91..daaf273f0da9a5 100644 --- a/builtin/pull.c +++ b/builtin/pull.c @@ -10,6 +10,8 @@ #include "builtin.h" #include "advice.h" +#include "clean-status-config.h" +#include "clean-status.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -226,6 +228,9 @@ static enum rebase_type config_get_rebase(int *rebase_unspecified) static int git_pull_config(const char *var, const char *value, const struct config_context *ctx, void *cb) { + if (cb) + clean_status_config_add(cb, var, value, ctx); + if (!strcmp(var, "rebase.autostash")) { /* * run_rebase() also reads this option. The reason we handle it here is @@ -248,7 +253,7 @@ static int git_pull_config(const char *var, const char *value, check_trust_level = 0; } - return git_default_config(var, value, ctx, cb); + return git_default_config(var, value, ctx, NULL); } /** @@ -862,6 +867,7 @@ int cmd_pull(int argc, struct oid_array merge_heads = OID_ARRAY_INIT; struct object_id orig_head, curr_head; struct object_id rebase_fork_point; + struct clean_status_config_digest clean_digest; int rebase_unspecified = 0; int can_ff; int divergent; @@ -1015,7 +1021,16 @@ int cmd_pull(int argc, if (!getenv("GIT_REFLOG_ACTION")) set_reflog_message(argc, argv); - repo_config(the_repository, git_pull_config, NULL); + if (the_repository->gitdir && !getenv(INDEX_ENVIRONMENT)) { + clean_status_config_init(&clean_digest, + the_repository->hash_algo); + repo_config(the_repository, git_pull_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } else { + repo_config(the_repository, git_pull_config, NULL); + } if (the_repository->gitdir) { prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/read-tree.c b/builtin/read-tree.c index 999a82ecdfd737..9a9f2c4a8b7e47 100644 --- a/builtin/read-tree.c +++ b/builtin/read-tree.c @@ -5,6 +5,8 @@ */ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -46,10 +48,11 @@ static const char * const read_tree_usage[] = { NULL }; -static int index_output_cb(const struct option *opt UNUSED, const char *arg, +static int index_output_cb(const struct option *opt, const char *arg, int unset) { BUG_ON_OPT_NEG(unset); + *(int *)opt->value = 1; set_alternate_index_output(arg); return 0; } @@ -100,12 +103,14 @@ static int debug_merge(const struct cache_entry * const *stages, } static int git_read_tree_config(const char *var, const char *value, - const struct config_context *ctx, void *cb) + const struct config_context *ctx, void *data) { + clean_status_config_add(data, var, value, ctx); + if (!strcmp(var, "submodule.recurse")) - return git_default_submodule_config(var, value, cb); + return git_default_submodule_config(var, value, NULL); - return git_default_config(var, value, ctx, cb); + return git_default_config(var, value, ctx, NULL); } int cmd_read_tree(int argc, @@ -113,7 +118,10 @@ int cmd_read_tree(int argc, const char *cmd_prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; int i, stage = 0; + int index_output = 0; + int preserve_history = 0; struct object_id oid; struct tree_desc t[MAX_UNPACK_TREES]; struct unpack_trees_options opts; @@ -121,7 +129,7 @@ int cmd_read_tree(int argc, struct lock_file lock_file = LOCK_INIT; const struct option read_tree_options[] = { OPT__SUPER_PREFIX(&opts.super_prefix), - OPT_CALLBACK_F(0, "index-output", NULL, N_("file"), + OPT_CALLBACK_F(0, "index-output", &index_output, N_("file"), N_("write resulting index to "), PARSE_OPT_NONEG, index_output_cb), OPT_BOOL(0, "empty", &read_empty, @@ -169,7 +177,12 @@ int cmd_read_tree(int argc, opts.src_index = the_repository->index; opts.dst_index = the_repository->index; - repo_config(the_repository, git_read_tree_config, NULL); + show_usage_with_options_if_asked(argc, argv, + read_tree_usage, read_tree_options); + + clean_status_config_init(&clean_digest, the_repository->hash_algo); + repo_config(the_repository, git_read_tree_config, &clean_digest); + clean_status_config_final(&clean_digest); argc = parse_options(argc, argv, cmd_prefix, read_tree_options, read_tree_usage, 0); @@ -190,6 +203,24 @@ int cmd_read_tree(int argc, repo_hold_locked_index(the_repository, &lock_file, LOCK_DIE_ON_ERROR); + /* + * A one-tree merge or reset can rewrite only stat and fsmonitor + * state when its tree matches the existing index. Attach history + * before reading that index; unpack_trees() will transfer it only + * after proving that the result has the same logical entries. + */ + if (argc == 1 && !read_empty && !opts.prefix && + (opts.reset || opts.merge) && + !opts.dry_run && + !opts.skip_sparse_checkout && !opts.internal.debug_unpack && + !opts.trivial_merges_only && !opts.aggressive && + !opts.super_prefix && !index_output && + !should_update_submodules()) { + preserve_history = 1; + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } + /* * NEEDSWORK * @@ -200,10 +231,17 @@ int cmd_read_tree(int argc, */ if (opts.reset || opts.merge || opts.prefix) { - if (repo_read_index_unmerged(the_repository) && (opts.prefix || opts.merge)) + int unmerged = repo_read_index_unmerged(the_repository); + + if (preserve_history && unmerged) + clean_status_invalidate_current_proof( + the_repository->index); + if (unmerged && (opts.prefix || opts.merge)) die(_("You need to resolve your current index first")); stage = opts.merge = 1; } + if (preserve_history && the_repository->index->resolve_undo) + clean_status_invalidate_current_proof(the_repository->index); resolve_undo_clear_index(the_repository->index); for (i = 0; i < argc; i++) { diff --git a/builtin/rebase.c b/builtin/rebase.c index 10a306310cd439..5ca096f48a20c7 100644 --- a/builtin/rebase.c +++ b/builtin/rebase.c @@ -10,6 +10,8 @@ #include "builtin.h" #include "abspath.h" +#include "clean-status-config.h" +#include "clean-status.h" #include "environment.h" #include "gettext.h" #include "hex.h" @@ -135,6 +137,8 @@ struct rebase_options { int config_autosquash; int config_rebase_merges; int config_update_refs; + struct clean_status_config_digest clean_digest; + unsigned clean_history_enabled : 1; }; #define REBASE_OPTIONS_INIT { \ @@ -795,6 +799,9 @@ static int rebase_config(const char *var, const char *value, { struct rebase_options *opts = data; + if (opts->clean_history_enabled) + clean_status_config_add(&opts->clean_digest, var, value, ctx); + if (!strcmp(var, "rebase.stat")) { if (git_config_bool(var, value)) opts->flags |= REBASE_DIFFSTAT; @@ -1261,7 +1268,17 @@ int cmd_rebase(int argc, prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; + options.clean_history_enabled = !getenv(INDEX_ENVIRONMENT); + if (options.clean_history_enabled) + clean_status_config_init(&options.clean_digest, + the_repository->hash_algo); repo_config(the_repository, rebase_config, &options); + if (options.clean_history_enabled) { + clean_status_config_final(&options.clean_digest); + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, + &options.clean_digest); + } /* options.gpg_sign_opt will be either "-S" or NULL */ gpg_sign = options.gpg_sign_opt ? "" : NULL; FREE_AND_NULL(options.gpg_sign_opt); diff --git a/builtin/reset.c b/builtin/reset.c index 78e69bd84ba2c3..c7c3570b3cb81b 100644 --- a/builtin/reset.c +++ b/builtin/reset.c @@ -12,11 +12,14 @@ #include "builtin.h" #include "advice.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" #include "hash.h" #include "hex.h" +#include "hook.h" #include "lockfile.h" #include "object.h" #include "pretty.h" @@ -159,9 +162,17 @@ static void update_index_from_diff(struct diff_queue_struct *q, int pos; struct diff_filespec *one = q->queue[i]->one; int is_in_reset_tree = one->mode && !is_null_oid(&one->oid); + struct cache_entry *old; struct cache_entry *ce; + pos = index_name_pos(the_repository->index, one->path, + strlen(one->path)); + old = pos >= 0 ? the_repository->index->cache[pos] : NULL; if (!is_in_reset_tree && !intent_to_add) { + if (!clean_status_index_entry_is_semantically_safe( + the_repository->index, old, NULL)) + clean_status_invalidate_current_proof( + the_repository->index); remove_file_from_index(the_repository->index, one->path); continue; } @@ -177,7 +188,6 @@ static void update_index_from_diff(struct diff_queue_struct *q, * if this entry is outside the sparse cone - this is necessary * to properly construct the reset sparse directory. */ - pos = index_name_pos(the_repository->index, one->path, strlen(one->path)); if ((pos >= 0 && ce_skip_worktree(the_repository->index->cache[pos])) || (pos < 0 && !path_in_sparse_checkout(one->path, the_repository->index))) ce->ce_flags |= CE_SKIP_WORKTREE; @@ -189,6 +199,10 @@ static void update_index_from_diff(struct diff_queue_struct *q, ce->ce_flags |= CE_INTENT_TO_ADD; set_object_name_for_intent_to_add_entry(ce); } + if (!clean_status_index_entry_is_semantically_safe( + the_repository->index, old, ce)) + clean_status_invalidate_current_proof( + the_repository->index); add_index_entry(the_repository->index, ce, ADD_CACHE_OK_TO_ADD | ADD_CACHE_OK_TO_REPLACE); } @@ -325,12 +339,14 @@ static int reset_refs(const char *rev, const struct object_id *oid) } static int git_reset_config(const char *var, const char *value, - const struct config_context *ctx, void *cb) + const struct config_context *ctx, void *data) { + clean_status_config_add(data, var, value, ctx); + if (!strcmp(var, "submodule.recurse")) - return git_default_submodule_config(var, value, cb); + return git_default_submodule_config(var, value, NULL); - return git_default_config(var, value, ctx, cb); + return git_default_config(var, value, ctx, NULL); } int cmd_reset(int argc, @@ -338,9 +354,11 @@ int cmd_reset(int argc, const char *prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; int reset_type = NONE, update_ref_status = 0, quiet = 0; int no_refresh = 0; int patch_mode = 0, pathspec_file_nul = 0, unborn; + int preserve_mixed_history = 0; const char *rev; char *pathspec_from_file = NULL; struct object_id oid; @@ -382,7 +400,11 @@ int cmd_reset(int argc, OPT_END() }; - repo_config(the_repository, git_reset_config, NULL); + show_usage_with_options_if_asked(argc, argv, git_reset_usage, options); + + clean_status_config_init(&clean_digest, the_repository->hash_algo); + repo_config(the_repository, git_reset_config, &clean_digest); + clean_status_config_final(&clean_digest); argc = parse_options(argc, argv, prefix, options, git_reset_usage, PARSE_OPT_KEEP_DASHDASH); @@ -477,6 +499,25 @@ int cmd_reset(int argc, if (intent_to_add && reset_type != MIXED) die(_("the option '%s' requires '%s'"), "-N", "--mixed"); + /* + * A no-path mixed or hard reset is a candidate for a stat-only + * rewrite even when its target commit differs from HEAD. Attach + * history early enough for the initial index read. Mixed reset + * checks its in-place result below; hard reset lets unpack_trees() + * transfer only an equal logical index. + */ + if ((reset_type == MIXED || reset_type == HARD) && + !pathspec.nr && !intent_to_add && + !unborn) { + preserve_mixed_history = reset_type == MIXED; + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } else if (reset_type == MIXED && pathspec.nr && + !intent_to_add && !unborn) { + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } + if (repo_read_index(the_repository) < 0) die(_("index file corrupt")); @@ -488,6 +529,8 @@ int cmd_reset(int argc, if (reset_type != SOFT) { struct lock_file lock = LOCK_INIT; + unsigned int write_flags = COMMIT_LOCK; + repo_hold_locked_index(the_repository, &lock, LOCK_DIE_ON_ERROR); if (reset_type == MIXED) { @@ -496,6 +539,13 @@ int cmd_reset(int argc, update_ref_status = 1; goto cleanup; } + if (preserve_mixed_history && + (the_repository->index->split_index || + the_repository->index->sparse_index || + (the_repository->index->cache_changed & + RESOLVE_UNDO_CHANGED))) + clean_status_invalidate_current_proof( + the_repository->index); the_repository->index->updated_skipworktree = 1; if (!no_refresh && repo_get_work_tree(the_repository)) { uint64_t t_begin, t_delta_in_ms; @@ -527,7 +577,11 @@ int cmd_reset(int argc, free(ref); } - if (write_locked_index(the_repository->index, &lock, COMMIT_LOCK)) + if (reset_type == MIXED && + !the_repository->index->cache_changed && + !hook_exists(the_repository, "post-index-change")) + write_flags |= SKIP_IF_UNCHANGED; + if (write_locked_index(the_repository->index, &lock, write_flags)) die(_("Could not write new index file.")); } diff --git a/builtin/revert.c b/builtin/revert.c index bedc40f368eccc..ac8968f3558d8d 100644 --- a/builtin/revert.c +++ b/builtin/revert.c @@ -2,9 +2,12 @@ #include "git-compat-util.h" #include "builtin.h" +#include "clean-status-config.h" +#include "clean-status.h" #include "parse-options.h" #include "diff.h" #include "environment.h" +#include "fsmonitor-settings.h" #include "gettext.h" #include "revision.h" #include "rerere.h" @@ -115,6 +118,7 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, const char sentinel_value = 0; /* value not important */ const char *strategy = &sentinel_value; const char *gpg_sign = &sentinel_value; + struct clean_status_config_digest clean_digest; enum empty_action empty_opt = EMPTY_COMMIT_UNSPECIFIED; int cmd = 0; struct option base_options[] = { @@ -172,6 +176,12 @@ static int run_sequencer(int argc, const char **argv, const char *prefix, argc = parse_options(argc, argv, prefix, options, usage_str, PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT); + if (fstat_is_reliable() && !getenv(INDEX_ENVIRONMENT) && + fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_IPC && + !clean_status_config_read_repository(the_repository, &clean_digest)) { + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &clean_digest); + } prepare_repo_settings(the_repository); the_repository->settings.command_requires_full_index = 0; diff --git a/builtin/rm.c b/builtin/rm.c index 081d0bc3754c52..39636abb93aedd 100644 --- a/builtin/rm.c +++ b/builtin/rm.c @@ -8,6 +8,8 @@ #include "builtin.h" #include "advice.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "lockfile.h" @@ -262,17 +264,38 @@ static struct option builtin_rm_options[] = { OPT_END(), }; +static int rm_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + int cmd_rm(int argc, const char **argv, const char *prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; struct lock_file lock_file = LOCK_INIT; + int preserve_clean_history = !getenv(INDEX_ENVIRONMENT); int i, ret = 0; struct pathspec pathspec; char *seen; - repo_config(the_repository, git_default_config, NULL); + show_usage_with_options_if_asked(argc, argv, + builtin_rm_usage, builtin_rm_options); + + if (preserve_clean_history) { + clean_status_config_init(&clean_digest, + the_repository->hash_algo); + repo_config(the_repository, rm_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_set_config_digest(the_repository, &clean_digest); + clean_status_enable_external_history(the_repository); + } else { + repo_config(the_repository, git_default_config, NULL); + } argc = parse_options(argc, argv, prefix, builtin_rm_options, builtin_rm_usage, 0); @@ -392,9 +415,19 @@ int cmd_rm(int argc, */ for (i = 0; i < list.nr; i++) { const char *path = list.entry[i].name; + int pos; if (!quiet) printf("rm '%s'\n", path); + pos = index_name_pos(the_repository->index, + path, strlen(path)); + if (preserve_clean_history && + (pos < 0 || + !clean_status_index_entry_is_semantically_safe( + the_repository->index, + the_repository->index->cache[pos], NULL))) + clean_status_invalidate_current_proof(the_repository->index); + if (remove_file_from_index(the_repository->index, path)) die(_("git rm: unable to remove %s"), path); } diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c index cb4a037b770291..f48d981ec10ccd 100644 --- a/builtin/sparse-checkout.c +++ b/builtin/sparse-checkout.c @@ -7,6 +7,7 @@ #include "dir.h" #include "environment.h" #include "gettext.h" +#include "hook.h" #include "object-file.h" #include "object-name.h" #include "parse-options.h" @@ -243,9 +244,16 @@ static int update_working_directory(struct repository *r, * files in the way or dirty entries that can't be removed. */ result = UPDATE_SPARSITY_SUCCESS; - if (result == UPDATE_SPARSITY_SUCCESS) - write_locked_index(r->index, &lock_file, COMMIT_LOCK); - else + if (result == UPDATE_SPARSITY_SUCCESS) { + unsigned int flags = COMMIT_LOCK; + + if (!r->index->cache_changed && + !r->index->updated_workdir && + !r->index->updated_skipworktree && + !hook_exists(r, "post-index-change")) + flags |= SKIP_IF_UNCHANGED; + write_locked_index(r->index, &lock_file, flags); + } else rollback_lock_file(&lock_file); clean_tracked_sparse_directories(r); diff --git a/builtin/stash.c b/builtin/stash.c index 72c52571f8c06c..60a63ef004435a 100644 --- a/builtin/stash.c +++ b/builtin/stash.c @@ -2,8 +2,11 @@ #include "builtin.h" #include "abspath.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" +#include "fsmonitor-settings.h" #include "gettext.h" #include "hash.h" #include "hex.h" @@ -20,6 +23,7 @@ #include "entry.h" #include "preload-index.h" #include "read-cache.h" +#include "replace-object.h" #include "repository.h" #include "rerere.h" #include "revision.h" @@ -150,6 +154,7 @@ static int show_stat = 1; static int show_patch; static int show_include_untracked; static int use_index; +static struct clean_status_config_digest stash_clean_digest; /* * w_commit is set to the commit containing the working tree @@ -331,7 +336,8 @@ static int clear_stash(int argc, const char **argv, const char *prefix, return do_clear_stash(); } -static int reset_tree(struct object_id *i_tree, int update, int reset) +static int reset_tree(struct object_id *i_tree, int update, int reset, + int preserve_semantic_history) { int nr_trees = 1; struct unpack_trees_options opts; @@ -356,6 +362,25 @@ static int reset_tree(struct object_id *i_tree, int update, int reset) opts.head_idx = 1; opts.src_index = the_repository->index; opts.dst_index = the_repository->index; + opts.preserve_semantic_history = preserve_semantic_history && + !update && !reset && fstat_is_reliable() && + !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(DB_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + !repo_config_values(the_repository)->apply_sparse_checkout && + the_repository->config_values_private_.trust_ctime && + the_repository->config_values_private_.check_stat && + fsm_settings__get_mode(the_repository) == FSMONITOR_MODE_IPC && + !repo_has_replace_refs_uncached(the_repository) && + the_repository->index->fsmonitor_untracked_valid && + clean_status_has_persistent_fsmonitor_semantic_history( + the_repository->index) && + clean_status_revalidated_token_matches(the_repository->index); + opts.preserve_backoff_history = preserve_semantic_history && + !update && !reset && + clean_status_fsmonitor_backoff_suspended(the_repository->index); opts.merge = 1; opts.reset = reset ? UNPACK_RESET_PROTECT_UNTRACKED : 0; opts.update = update; @@ -586,6 +611,7 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) struct stat st; ce = the_repository->index->cache[pos]; + clean_status_invalidate_current_proof(the_repository->index); if (!lstat(ce->name, &st)) { /* Conflicting path present; relocate it */ struct strbuf new_path = STRBUF_INIT; @@ -626,6 +652,12 @@ static void unstage_changes_unless_new(struct object_id *orig_tree) &p->one->oid, p->one->path, 0, 0); + if (!clean_status_index_entry_is_semantically_safe( + the_repository->index, + pos >= 0 ? the_repository->index->cache[pos] : NULL, + ce)) + clean_status_invalidate_current_proof( + the_repository->index); add_index_entry(the_repository->index, ce, option); } } @@ -653,6 +685,8 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, struct tree *head, *merge, *merge_base; struct lock_file lock = LOCK_INIT; + clean_status_prepare_main_index_history(the_repository); + repo_read_index_preload(the_repository, NULL, 0); if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, NULL, NULL, NULL)) @@ -693,6 +727,15 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, } } + /* The distinct-index path already reread after reset_head(). */ + if (!has_index && is_null_oid(&the_repository->index->oid) && + clean_status_fsmonitor_backoff_suspended(the_repository->index)) { + /* The initial refresh may have replaced our source inode. */ + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) + return error(_("could not read index")); + } + init_ui_merge_options(&o, the_repository); o.branch1 = label_ours ? label_ours : "Updated upstream"; @@ -738,7 +781,11 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, } if (has_index) { - if (reset_tree(&index_tree, 0, 0)) + /* The preceding publication replaced a null-checksum source inode. */ + if (is_null_oid(&the_repository->index->oid) && + clean_status_fsmonitor_backoff_suspended(the_repository->index)) + discard_index(the_repository->index); + if (reset_tree(&index_tree, 0, 0, 1)) ret = -1; } else { unstage_changes_unless_new(&c_tree); @@ -758,10 +805,14 @@ static int do_apply_stash(const char *prefix, struct stash_info *info, */ cp.git_cmd = 1; cp.dir = prefix; - strvec_pushf(&cp.env, GIT_WORK_TREE_ENVIRONMENT"=%s", - absolute_path(repo_get_work_tree(the_repository))); - strvec_pushf(&cp.env, GIT_DIR_ENVIRONMENT"=%s", - absolute_path(repo_get_git_dir(the_repository))); + /* Keep discovered config origins stable unless discovery is overridden. */ + if (getenv(GIT_DIR_ENVIRONMENT) || + getenv(GIT_WORK_TREE_ENVIRONMENT)) { + strvec_pushf(&cp.env, GIT_WORK_TREE_ENVIRONMENT"=%s", + absolute_path(repo_get_work_tree(the_repository))); + strvec_pushf(&cp.env, GIT_DIR_ENVIRONMENT"=%s", + absolute_path(repo_get_git_dir(the_repository))); + } strvec_push(&cp.args, "status"); run_command(&cp); } @@ -975,6 +1026,8 @@ static int list_stash(int argc, const char **argv, const char *prefix, static int git_stash_config(const char *var, const char *value, const struct config_context *ctx, void *cb) { + clean_status_config_add(cb, var, value, ctx); + if (!strcmp(var, "stash.showstat")) { show_stat = git_config_bool(var, value); return 0; @@ -991,7 +1044,7 @@ static int git_stash_config(const char *var, const char *value, use_index = git_config_bool(var, value); return 0; } - return git_diff_basic_config(var, value, ctx, cb); + return git_diff_basic_config(var, value, ctx, NULL); } static void diff_include_untracked(const struct stash_info *info, struct diff_options *diff_opt) @@ -1128,7 +1181,7 @@ static int do_store_stash(const struct object_id *w_commit, const char *stash_ms int quiet) { struct stash_info info; - char revision[GIT_MAX_HEXSZ]; + char revision[GIT_MAX_HEXSZ + 1]; oid_to_hex_r(revision, w_commit); assert_stash_like(&info, revision); @@ -1438,7 +1491,8 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps, return ret; } -static int stash_working_tree(struct stash_info *info, const struct pathspec *ps) +static int stash_working_tree(struct stash_info *info, const struct pathspec *ps, + int preserve_clean_history) { int ret = 0; struct rev_info rev; @@ -1450,7 +1504,7 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps copy_pathspec(&rev.prune_data, ps); set_alternate_index_output(stash_index_path.buf); - if (reset_tree(&info->i_tree, 0, 0)) { + if (reset_tree(&info->i_tree, 0, 0, 0)) { ret = -1; goto done; } @@ -1482,8 +1536,13 @@ static int stash_working_tree(struct stash_info *info, const struct pathspec *ps goto done; } - if (write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, 0, - NULL)) { + if (preserve_clean_history) + clean_status_set_config_digest(the_repository, NULL); + ret = write_index_as_tree(&info->w_tree, &istate, stash_index_path.buf, + 0, NULL); + if (preserve_clean_history) + clean_status_set_config_digest(the_repository, &stash_clean_digest); + if (ret) { ret = -1; goto done; } @@ -1600,7 +1659,10 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b goto done; } } else { - if (stash_working_tree(info, ps)) { + if (stash_working_tree(info, ps, + !include_untracked && + clean_status_external_history_enabled( + the_repository->index))) { if (!quiet) fprintf_ln(stderr, _("Cannot save the current " "worktree state")); @@ -1652,6 +1714,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED, strbuf_join_argv(&stash_msg_buf, argc - 1, ++argv, ' '); memset(&ps, 0, sizeof(ps)); + clean_status_enable_external_history(the_repository); + clean_status_set_config_digest(the_repository, &stash_clean_digest); if (!check_changes_tracked_files(&ps)) return 0; @@ -1671,6 +1735,8 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q int include_untracked, int only_staged) { int ret = 0; + int preserve_clean_history = !ps->nr && !include_untracked; + struct lock_file index_lock = LOCK_INIT; struct stash_info info = STASH_INFO_INIT; struct strbuf patch = STRBUF_INIT; struct strbuf stash_msg_buf = STRBUF_INIT; @@ -1698,6 +1764,13 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q goto done; } + /* + * Even a cancelled patch selection can refresh the real index. Attach + * authenticated history before that first read, independently of the + * eventual stash operation. Whole-worktree changes still invalidate + * their proof below, and each writer must validate its own changes. + */ + clean_status_prepare_main_index_history(the_repository); repo_read_index_preload(the_repository, NULL, 0); if (!include_untracked && ps->nr) { char *ps_matched = xcalloc(ps->nr, 1); @@ -1717,17 +1790,31 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q free(ps_matched); } - if (repo_refresh_and_write_index(the_repository, REFRESH_QUIET, 0, 0, - NULL, NULL, NULL)) { + if (repo_hold_locked_index(the_repository, &index_lock, + LOCK_REPORT_ON_ERROR) < 0 || + refresh_index(the_repository->index, REFRESH_QUIET, + NULL, NULL, NULL)) { ret = error(_("could not write index")); goto done; } if (!check_changes(ps, include_untracked, &untracked_files)) { + rollback_lock_file(&index_lock); if (!quiet) printf_ln(_("No local changes to save")); goto done; } + if (preserve_clean_history && !(patch_mode || only_staged)) { + clean_status_invalidate_current_proof(the_repository->index); + if (clean_status_should_write_fsmonitor_config( + the_repository->index)) + the_repository->index->cache_changed |= FSMONITOR_CHANGED; + } + if (write_locked_index(the_repository->index, &index_lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) { + ret = error(_("could not write index")); + goto done; + } if (!refs_reflog_exists(get_main_ref_store(the_repository), ref_stash) && do_clear_stash()) { ret = -1; @@ -1743,6 +1830,33 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q ret = -1; goto done; } + if (preserve_clean_history && (patch_mode || only_staged)) { + /* + * A cancelled selection has not changed the worktree. Invalidate + * only after it succeeds, but before publishing the stash. Patch + * selection may have loaded its private index into repo->index, + * so reread the original selected index under a fresh lock. + */ + if (repo_hold_locked_index(the_repository, &index_lock, + LOCK_REPORT_ON_ERROR) < 0) { + ret = error(_("could not write index")); + goto done; + } + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) { + ret = error(_("could not read index")); + goto done; + } + clean_status_invalidate_current_proof(the_repository->index); + if (clean_status_should_write_fsmonitor_config( + the_repository->index)) + the_repository->index->cache_changed |= FSMONITOR_CHANGED; + if (write_locked_index(the_repository->index, &index_lock, + COMMIT_LOCK | SKIP_IF_UNCHANGED)) { + ret = error(_("could not write index")); + goto done; + } + } if (do_store_stash(&info.w_commit, stash_msg_buf.buf, 1)) { ret = -1; @@ -1892,6 +2006,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q } done: + rollback_lock_file(&index_lock); strbuf_release(&patch); strbuf_release(&out); free_stash_info(&info); @@ -2478,7 +2593,12 @@ int cmd_stash(int argc, const char **args_copy; int ret; - repo_config(the_repository, git_stash_config, NULL); + show_usage_with_options_if_asked(argc, argv, git_stash_usage, options); + + clean_status_config_init(&stash_clean_digest, + the_repository->hash_algo); + repo_config(the_repository, git_stash_config, &stash_clean_digest); + clean_status_config_final(&stash_clean_digest); argc = parse_options(argc, argv, prefix, options, git_stash_usage, PARSE_OPT_SUBCOMMAND_OPTIONAL | diff --git a/builtin/update-index.c b/builtin/update-index.c index 241abd4332dcf9..1582d8ecc74b45 100644 --- a/builtin/update-index.c +++ b/builtin/update-index.c @@ -8,6 +8,8 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "builtin.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" @@ -54,6 +56,59 @@ static int ignore_skip_worktree_entries; #define UNMARK_FLAG 2 static struct strbuf mtime_dir = STRBUF_INIT; +static int update_index_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + +static int is_proof_preserving_rewrite(int argc, const char **argv) +{ + if (argc >= 4 && !strcmp(argv[1], "--refresh") && + !strcmp(argv[2], "--")) { + for (int i = 3; i < argc; i++) { + const char *base = strrchr(argv[i], '/'); + + base = base ? base + 1 : argv[i]; + if (!*base || !strcasecmp(base, ".gitattributes") || + !strcasecmp(base, ".gitignore")) + return 0; + } + return 1; + } + + if (argc == 2) + return !strcmp(argv[1], "--refresh") || + !strcmp(argv[1], "--force-write-index"); + + if (argc != 3) + return 0; + + return (!strcmp(argv[1], "--refresh") && + !strcmp(argv[2], "--force-write-index")) || + (!strcmp(argv[1], "--force-write-index") && + !strcmp(argv[2], "--refresh")) || + (!strcmp(argv[1], "--untracked-cache") && + !strcmp(argv[2], "--force-write-index")) || + (!strcmp(argv[1], "--force-write-index") && + !strcmp(argv[2], "--untracked-cache")); +} + +static int is_fsmonitor_invalidation_rewrite(int argc, const char **argv) +{ + int first_path = 2; + + if (argc < 3 || strcmp(argv[1], "--no-fsmonitor-valid")) + return 0; + if (!strcmp(argv[first_path], "--")) + return argc > ++first_path; + for (int i = first_path; i < argc; i++) + if (argv[i][0] == '-') + return 0; + return 1; +} + /* Untracked cache mode */ enum uc_mode { UC_UNSPECIFIED = -1, @@ -249,6 +304,14 @@ static int mark_ce_flags(const char *path, int flag, int mark) the_repository->index->cache[pos]->ce_flags |= flag; else the_repository->index->cache[pos]->ce_flags &= ~flag; + if (flag == CE_FSMONITOR_VALID && !mark && + clean_status_external_history_enabled( + the_repository->index) && + !the_repository->index->split_index) { + /* The fsmonitor bitmap does not change the indexed tree. */ + the_repository->index->cache_changed |= FSMONITOR_CHANGED; + return 0; + } the_repository->index->cache[pos]->ce_flags |= CE_UPDATE_IN_BASE; cache_tree_invalidate_path(the_repository->index, path); the_repository->index->cache_changed |= CE_ENTRY_CHANGED; @@ -261,6 +324,8 @@ static int remove_one_path(const char *path) { if (!allow_remove) return error("%s: does not exist and --remove not passed", path); + if (clean_status_external_history_enabled(the_repository->index)) + clean_status_invalidate_current_proof(the_repository->index); if (remove_file_from_index(the_repository->index, path)) return error("%s: cannot remove from the index", path); return 0; @@ -303,6 +368,12 @@ static int add_one_path(const struct cache_entry *old, const char *path, int len } option = allow_add ? ADD_CACHE_OK_TO_ADD : 0; option |= allow_replace ? ADD_CACHE_OK_TO_REPLACE : 0; + if (clean_status_external_history_enabled(the_repository->index) && + (!old || old->ce_mode != ce->ce_mode || + !oideq(&old->oid, &ce->oid)) && + !clean_status_index_entry_is_semantically_safe( + the_repository->index, old, ce)) + clean_status_invalidate_current_proof(the_repository->index); if (add_index_entry(the_repository->index, ce, option)) { discard_cache_entry(ce); return error("%s: cannot add to the index - missing --add option?", path); @@ -338,6 +409,9 @@ static int process_directory(const char *path, int len, struct stat *st) struct object_id oid; int pos = index_name_pos(the_repository->index, path, len); + if (clean_status_external_history_enabled(the_repository->index)) + clean_status_invalidate_current_proof(the_repository->index); + /* Exact match: file or existing gitlink */ if (pos >= 0) { const struct cache_entry *ce = the_repository->index->cache[pos]; @@ -917,6 +991,7 @@ int cmd_update_index(int argc, const char *prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; int newfd, entries, has_errors = 0, nul_term_line = 0; enum uc_mode untracked_cache = UC_UNSPECIFIED; int read_from_stdin = 0; @@ -932,6 +1007,11 @@ int cmd_update_index(int argc, struct parse_opt_ctx_t ctx; strbuf_getline_fn getline_fn; int parseopt_state = PARSE_OPT_UNKNOWN; + int preserve_fsmonitor_history = + is_fsmonitor_invalidation_rewrite(argc, argv); + int preserve_clean_history = + is_proof_preserving_rewrite(argc, argv) || + preserve_fsmonitor_history; struct repository *r = the_repository; struct odb_transaction *transaction; struct option options[] = { @@ -1097,7 +1177,21 @@ int cmd_update_index(int argc, show_usage_with_options_if_asked(argc, argv, update_index_usage, options); - repo_config(the_repository, git_default_config, NULL); + if (preserve_clean_history) { + clean_status_config_init(&clean_digest, + the_repository->hash_algo); + repo_config(the_repository, update_index_config, + &clean_digest); + clean_status_config_final(&clean_digest); + /* + * These exact forms can refresh stat data, or no data at all, + * but cannot change the logical contents of the index. + */ + clean_status_set_config_digest(the_repository, &clean_digest); + clean_status_enable_external_history(the_repository); + } else { + repo_config(the_repository, git_default_config, NULL); + } prepare_repo_settings(r); the_repository->settings.command_requires_full_index = 0; @@ -1110,6 +1204,28 @@ int cmd_update_index(int argc, entries = repo_read_index(the_repository); if (entries < 0) die("cache corrupted"); + if (preserve_clean_history && argc >= 4 && + !strcmp(argv[1], "--refresh") && !strcmp(argv[2], "--")) { + if (the_repository->index->split_index || + the_repository->index->sparse_index) + clean_status_invalidate_current_proof( + the_repository->index); + for (int i = 3; i < argc; i++) { + char *path = prefix_path(the_repository, prefix, + prefix_length, argv[i]); + int pos = index_name_pos(the_repository->index, + path, strlen(path)); + const struct cache_entry *ce = pos < 0 ? NULL : + the_repository->index->cache[pos]; + + if (!ce || !S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + clean_status_invalidate_current_proof( + the_repository->index); + free(path); + } + } the_repository->index->updated_skipworktree = 1; diff --git a/builtin/write-tree.c b/builtin/write-tree.c index e3bd1a40dbf389..bb04b01968d068 100644 --- a/builtin/write-tree.c +++ b/builtin/write-tree.c @@ -5,10 +5,14 @@ */ #define USE_THE_REPOSITORY_VARIABLE #include "builtin.h" +#include "abspath.h" +#include "clean-status.h" +#include "clean-status-config.h" #include "config.h" #include "environment.h" #include "gettext.h" #include "hex.h" +#include "strbuf.h" #include "tree.h" #include "cache-tree.h" #include "parse-options.h" @@ -18,11 +22,50 @@ static const char * const write_tree_usage[] = { NULL }; +static int write_tree_config(const char *key, const char *value, + const struct config_context *ctx, void *data) +{ + clean_status_config_add(data, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + +static int write_tree_uses_worktree_index(void) +{ + const char *index_file = getenv(INDEX_ENVIRONMENT); + struct strbuf worktree_index = STRBUF_INIT; + struct stat st; + char *expected = NULL, *expected_lock = NULL, *actual = NULL; + int matches = 0; + + if (!index_file) + return 1; + if (lstat(index_file, &st) || !S_ISREG(st.st_mode)) + return 0; + + strbuf_addf(&worktree_index, "%s/index", + repo_get_git_dir(the_repository)); + expected = real_pathdup(worktree_index.buf, 0); + actual = real_pathdup(index_file, 0); + if (expected && actual) { + expected_lock = xstrfmt("%s.lock", expected); + if (!strcmp(expected, actual) || + !strcmp(expected_lock, actual)) + matches = 1; + } + + free(expected); + free(expected_lock); + free(actual); + strbuf_release(&worktree_index); + return matches; +} + int cmd_write_tree(int argc, const char **argv, const char *cmd_prefix, struct repository *repo UNUSED) { + struct clean_status_config_digest clean_digest; int flags = 0, ret; const char *tree_prefix = NULL; struct object_id oid; @@ -44,7 +87,19 @@ int cmd_write_tree(int argc, OPT_END() }; - repo_config(the_repository, git_default_config, NULL); + show_usage_with_options_if_asked(argc, argv, + write_tree_usage, write_tree_options); + + if (write_tree_uses_worktree_index()) { + clean_status_config_init(&clean_digest, + the_repository->hash_algo); + repo_config(the_repository, write_tree_config, &clean_digest); + clean_status_config_final(&clean_digest); + clean_status_set_config_digest(the_repository, &clean_digest); + clean_status_enable_external_history(the_repository); + } else { + repo_config(the_repository, git_default_config, NULL); + } argc = parse_options(argc, argv, cmd_prefix, write_tree_options, write_tree_usage, 0); diff --git a/cache-tree.c b/cache-tree.c index d92f5132865f13..e26835fb85c1a7 100644 --- a/cache-tree.c +++ b/cache-tree.c @@ -2,6 +2,8 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" +#include "clean-status-index.h" +#include "fsmonitor-settings.h" #include "gettext.h" #include "hex.h" #include "lockfile.h" @@ -676,20 +678,34 @@ static struct cache_tree *read_one(const char **buffer, unsigned long *size_p) /* * Just a heuristic -- we do not add directories that often but * we do not want to have to extend it immediately when we do, - * hence +2. + * hence +2. Avoid a separate allocation for the common leaf case. */ - it->subtree_alloc = subtree_nr + 2; - CALLOC_ARRAY(it->down, it->subtree_alloc); + if (subtree_nr) { + it->subtree_alloc = subtree_nr + 2; + ALLOC_ARRAY(it->down, it->subtree_alloc); + } for (i = 0; i < subtree_nr; i++) { /* read each subtree */ struct cache_tree *sub; struct cache_tree_sub *subtree; const char *name = buf; + int namelen; sub = read_one(&buf, &size); if (!sub) goto free_return; - subtree = cache_tree_sub(it, name); + namelen = strlen(name); + if (!it->subtree_nr || + subtree_name_cmp(it->down[it->subtree_nr - 1]->name, + it->down[it->subtree_nr - 1]->namelen, + name, namelen) < 0) { + FLEX_ALLOC_MEM(subtree, name, name, namelen); + subtree->namelen = namelen; + it->down[it->subtree_nr++] = subtree; + } else { + /* Be liberal in what we accept from older writers. */ + subtree = cache_tree_sub(it, name); + } subtree->cache_tree = sub; } if (subtree_nr != it->subtree_nr) @@ -794,6 +810,16 @@ struct tree *write_in_core_index_as_tree(struct repository *repo, } +static int skip_backoff_cache_tree_write(struct index_state *istate, + const char *index_path) +{ + struct repository *repo = istate->repo; + + return !get_alternate_index_output() && + fsm_settings__is_watch_limit_backoff(repo) && + clean_status_index_path_is_main(repo, index_path); +} + int write_index_as_tree(struct object_id *oid, struct index_state *index_state, const char *index_path, int flags, const char *prefix) { int entries, was_valid; @@ -815,7 +841,8 @@ int write_index_as_tree(struct object_id *oid, struct index_state *index_state, ret = write_index_as_tree_internal(oid, index_state, was_valid, flags, prefix); - if (!ret && !was_valid) { + if (!ret && !was_valid && + !skip_backoff_cache_tree_write(index_state, index_path)) { write_locked_index(index_state, &lock_file, COMMIT_LOCK); /* Not being able to write is fine -- we are only interested * in updating the cache-tree part, and if the next caller diff --git a/clean-status-config.c b/clean-status-config.c new file mode 100644 index 00000000000000..e6523e193ccaaa --- /dev/null +++ b/clean-status-config.c @@ -0,0 +1,602 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "clean-status-config.h" +#include "clean-status-index.h" +#include "config.h" +#include "environment.h" +#include "hash-framing.h" +#include "parse.h" +#include "path-namespace.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "strbuf.h" +#include "wrapper.h" + +#define CLEAN_STATUS_FILTER_PROOF_DOMAIN \ + "clean-status-configured-filter-scope-v1" + +#define CLEAN_STATUS_FILTER_CLEAN (1U << 0) +#define CLEAN_STATUS_FILTER_SMUDGE (1U << 1) +#define CLEAN_STATUS_FILTER_PROCESS (1U << 2) +#define CLEAN_STATUS_FILTER_REQUIRED (1U << 3) +#define CLEAN_STATUS_FILTER_COMPLETE \ + (CLEAN_STATUS_FILTER_CLEAN | CLEAN_STATUS_FILTER_SMUDGE | \ + CLEAN_STATUS_FILTER_PROCESS | CLEAN_STATUS_FILTER_REQUIRED) + +struct clean_status_pending_filter_entry { + char *key; + char *value; + char *filename; + enum config_scope scope; + enum config_origin_type origin_type; +}; + +struct clean_status_pending_filter { + char *driver; + struct clean_status_pending_filter_entry entries[4]; + unsigned nr; + unsigned mask; +}; + +void clean_status_config_init(struct clean_status_config_digest *digest, + const struct git_hash_algo *algo) +{ + if (!algo) + BUG("clean-status config digest requires a hash algorithm"); + memset(digest, 0, sizeof(*digest)); + git_hash_init(&digest->ctx, algo); + git_hash_init(&digest->semantic_ctx, algo); + git_hash_init(&digest->tracked_policy_ctx, algo); + hash_optional_cstring(&digest->tracked_policy_ctx, + "clean-status-tracked-policy-v1"); + /* Invalidate proofs written before multiply-linked files stayed dirty. */ + hash_optional_cstring(&digest->ctx, + "clean-status-config-hardlink-v1"); + digest->initialized = 1; +} + +static void hash_config_entry(struct git_hash_ctx *ctx, + const char *key, const char *value, + const struct config_context *config_ctx) +{ + uint32_t metadata[2] = { 0 }; + + hash_optional_cstring(ctx, key); + hash_optional_cstring(ctx, value); + if (config_ctx && config_ctx->kvi) { + put_be32(&metadata[0], config_ctx->kvi->scope); + put_be32(&metadata[1], config_ctx->kvi->origin_type); + hash_length_delimited(ctx, metadata, sizeof(metadata)); + hash_optional_cstring(ctx, config_ctx->kvi->filename); + } else { + hash_length_delimited(ctx, metadata, sizeof(metadata)); + hash_optional_cstring(ctx, NULL); + } +} + +static void hash_effective_config_entry(struct git_hash_ctx *ctx, + const char *key, + const char *value) +{ + hash_optional_cstring(ctx, key); + hash_optional_cstring(ctx, value); +} + +static int config_is_command_transport(const char *key, + const struct config_context *ctx) +{ + const char *subsection, *subkey; + size_t subsection_len; + + if (!ctx || !ctx->kvi || ctx->kvi->scope != CONFIG_SCOPE_COMMAND) + return 0; + if (starts_with(key, "credential.")) + return 1; + if (parse_config_key(key, "url", &subsection, &subsection_len, + &subkey) || !subsection || !subsection_len) + return 0; + return !strcmp(subkey, "insteadof") || + !strcmp(subkey, "pushinsteadof"); +} + +static int config_is_command_acceleration(const char *key, + const struct config_context *ctx) +{ + return ctx && ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_COMMAND && + (!strcmp(key, "core.preloadindex") || + !strcmp(key, "core.preloadindexbulk")); +} + +/* Clean proofs cache no output; the status printer uses these choices. */ +static int config_is_command_presentation(const char *key, + const struct config_context *ctx) +{ + return ctx && ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_COMMAND && + (!strcmp(key, "status.relativepaths") || + !strcmp(key, "color.ui") || + !strcmp(key, "core.quotepath")); +} + +static int config_is_command_empty_attributes(const char *key, + const char *value, + const struct config_context *ctx, + const struct clean_status_config_digest *digest) +{ + return ctx && ctx->kvi && ctx->kvi->scope == CONFIG_SCOPE_COMMAND && + value && !*value && + (!strcmp(key, "core.attributesfile") || + (!strcmp(key, "attr.tree") && + !digest->attribute_tree_configured)); +} + +static int config_is_command_status_guard( + const char *key, const char *value, + const struct config_context *ctx, + struct clean_status_config_digest *digest) +{ + int command = ctx && ctx->kvi && + ctx->kvi->scope == CONFIG_SCOPE_COMMAND; + + if (!strcmp(key, "core.fsmonitor")) { + int boolean = git_parse_maybe_bool(value); + int redundant = command && boolean >= 0 && + digest->fsmonitor_value_seen && + digest->fsmonitor_value_boolean && + !!boolean == !!digest->fsmonitor_value_enabled; + + digest->fsmonitor_value_seen = 1; + digest->fsmonitor_value_boolean = boolean >= 0; + digest->fsmonitor_value_enabled = boolean > 0; + return redundant; + } + if (!strcmp(key, "submodule.recurse")) { + int boolean = git_parse_maybe_bool(value); + int known_scope = ctx && ctx->kvi && + ctx->kvi->scope > CONFIG_SCOPE_UNKNOWN && + ctx->kvi->scope <= CONFIG_SCOPE_COMMAND; + int redundant = command && !boolean && + (!digest->submodule_recurse_seen || + digest->submodule_recurse_known_false); + + /* Recursion defaults to off; retain every effective change. */ + digest->submodule_recurse_seen = 1; + digest->submodule_recurse_known_false = + known_scope && !boolean; + return redundant; + } + + return command && value && + ((!strcmp(key, "safe.barerepository") && + !strcmp(value, "explicit")) || + (!strcmp(key, "core.hookspath") && + !strcmp(value, "/dev/null")) || + (!strcmp(key, "hook.post-index-change.enabled") && + !strcmp(value, "false"))); +} + +static unsigned config_command_disabled_filter_part( + const char *key, const char *value, + const struct config_context *ctx, + const char **driver, size_t *driver_len) +{ + const char *subkey; + + if (!ctx || !ctx->kvi || ctx->kvi->scope != CONFIG_SCOPE_COMMAND || + !value || parse_config_key(key, "filter", driver, driver_len, + &subkey) || !*driver || !*driver_len) + return 0; + if (!strcmp(subkey, "required")) + return git_parse_maybe_bool(value) == 0 ? + CLEAN_STATUS_FILTER_REQUIRED : 0; + if (*value) + return 0; + if (!strcmp(subkey, "clean")) + return CLEAN_STATUS_FILTER_CLEAN; + if (!strcmp(subkey, "smudge")) + return CLEAN_STATUS_FILTER_SMUDGE; + if (!strcmp(subkey, "process")) + return CLEAN_STATUS_FILTER_PROCESS; + return 0; +} + +static int config_is_tracked_policy(const char *key) +{ + return !strcmp(key, "core.filemode") || + !strcmp(key, "core.trustctime") || + !strcmp(key, "core.checkstat") || + !strcmp(key, "core.symlinks") || + !strcmp(key, "core.ignorecase") || + !strcmp(key, "core.ignorestat") || + !strcmp(key, "core.sparsecheckout") || + !strcmp(key, "core.sparsecheckoutcone") || + !strcmp(key, "core.precomposeunicode") || + !strcmp(key, "core.protecthfs") || + !strcmp(key, "core.protectntfs") || + !strcmp(key, "core.excludesfile") || + !strcmp(key, "core.attributesfile"); +} + +static void hash_retained_config_entry( + struct clean_status_config_digest *digest, + const char *key, const char *value, + const struct config_context *ctx) +{ + const char *suffix; + int semantic; + + hash_config_entry(&digest->ctx, key, value, ctx); + if (config_is_tracked_policy(key)) + hash_effective_config_entry(&digest->tracked_policy_ctx, + key, value); + semantic = !strcmp(key, "core.autocrlf") || + !strcmp(key, "core.eol") || + !strcmp(key, "core.checkroundtripencoding"); + if (skip_prefix(key, "filter.", &suffix) && + (ends_with(suffix, ".clean") || ends_with(suffix, ".process") || + ends_with(suffix, ".required"))) { + digest->filter_configured = 1; + semantic = 1; + } + if (semantic) { + hash_effective_config_entry(&digest->semantic_ctx, key, value); + digest->semantic_config_explicit = 1; + } +} + +static void flush_pending_filter(struct clean_status_config_digest *digest) +{ + struct clean_status_pending_filter *pending = digest->pending_filter; + + if (!pending) + return; + /* Remember command overrides omitted from the authenticated digest. */ + if (pending->mask == CLEAN_STATUS_FILTER_COMPLETE) + digest->normalized_filter_disable = 1; + for (unsigned i = 0; i < pending->nr; i++) { + struct clean_status_pending_filter_entry *entry = + &pending->entries[i]; + + if (pending->mask != CLEAN_STATUS_FILTER_COMPLETE) { + struct key_value_info kvi = KVI_INIT; + struct config_context ctx = { .kvi = &kvi }; + + kvi.scope = entry->scope; + kvi.origin_type = entry->origin_type; + kvi.filename = entry->filename; + hash_retained_config_entry(digest, entry->key, + entry->value, &ctx); + } + free(entry->key); + free(entry->value); + free(entry->filename); + } + free(pending->driver); + free(pending); + digest->pending_filter = NULL; +} + +static void queue_disabled_filter_part( + struct clean_status_config_digest *digest, + const char *key, const char *value, + const struct config_context *ctx, + const char *driver, size_t driver_len, unsigned part) +{ + struct clean_status_pending_filter *pending = digest->pending_filter; + struct clean_status_pending_filter_entry *entry; + + if (pending && + (strlen(pending->driver) != driver_len || + memcmp(pending->driver, driver, driver_len) || + (pending->mask & part))) { + flush_pending_filter(digest); + pending = NULL; + } + if (!pending) { + CALLOC_ARRAY(pending, 1); + pending->driver = xstrndup(driver, driver_len); + digest->pending_filter = pending; + } + entry = &pending->entries[pending->nr++]; + entry->key = xstrdup(key); + entry->value = xstrdup(value); + entry->filename = xstrdup_or_null(ctx->kvi->filename); + entry->scope = ctx->kvi->scope; + entry->origin_type = ctx->kvi->origin_type; + pending->mask |= part; + if (pending->mask == CLEAN_STATUS_FILTER_COMPLETE) + flush_pending_filter(digest); +} + +void clean_status_config_add(struct clean_status_config_digest *digest, + const char *key, const char *value, + const struct config_context *ctx) +{ + const char *driver = NULL; + size_t driver_len = 0; + unsigned filter_part; + + if (!digest->initialized || digest->finalized) + BUG("invalid clean-status config digest state"); + filter_part = config_command_disabled_filter_part( + key, value, ctx, &driver, &driver_len); + if (filter_part) { + queue_disabled_filter_part(digest, key, value, ctx, + driver, driver_len, filter_part); + return; + } + flush_pending_filter(digest); + if (!strcmp(key, "attr.tree") && value && *value) + digest->attribute_tree_configured = 1; + /* Independent attribute fingerprints guard empty source overrides. */ + if (config_is_command_transport(key, ctx) || + config_is_command_acceleration(key, ctx) || + config_is_command_presentation(key, ctx) || + config_is_command_empty_attributes(key, value, ctx, digest) || + config_is_command_status_guard(key, value, ctx, digest)) + return; + hash_retained_config_entry(digest, key, value, ctx); +} + +void clean_status_config_final(struct clean_status_config_digest *digest) +{ + if (!digest->initialized || digest->finalized) + BUG("invalid clean-status config digest state"); + flush_pending_filter(digest); + if (digest->filter_configured) { + /* + * Leave repositories without configured clean filters in their + * existing proof domain. Configured filters require a proof which + * has classified every tracked path before it may be reused. + */ + hash_optional_cstring(&digest->ctx, + CLEAN_STATUS_FILTER_PROOF_DOMAIN); + hash_optional_cstring(&digest->semantic_ctx, + CLEAN_STATUS_FILTER_PROOF_DOMAIN); + } + git_hash_final(digest->hash, &digest->ctx); + git_hash_final(digest->semantic_hash, &digest->semantic_ctx); + git_hash_final(digest->tracked_policy_hash, + &digest->tracked_policy_ctx); + digest->finalized = 1; +} + +static int config_digest_callback(const char *key, const char *value, + const struct config_context *ctx, + void *data) +{ + clean_status_config_add(data, key, value, ctx); + return 0; +} + +int clean_status_config_read_repository( + struct repository *repo, + struct clean_status_config_digest *digest) +{ + struct config_options opts = { 0 }; + + clean_status_config_init(digest, repo->hash_algo); + opts.respect_includes = 1; + opts.commondir = repo->commondir; + opts.git_dir = repo->gitdir; + if (config_with_options(config_digest_callback, digest, NULL, + repo, &opts) < 0) + return -1; + clean_status_config_final(digest); + return 0; +} + +#ifdef __APPLE__ +struct config_epoch_source { + char *path; + struct path_namespace_snapshot *namespace; + struct stat stat; + int fd; +}; + +struct config_epoch_proof { + struct config_epoch_source *sources; + char *system_path; + size_t nr; + size_t alloc; + struct stat index; + int failed; + int system_seen; +}; + +static int config_epoch_command_is_safe( + const char *key, const struct config_context *ctx) +{ + return starts_with(key, "advice.") || + !strcmp(key, "user.name") || !strcmp(key, "user.email") || + config_is_command_acceleration(key, ctx) || + config_is_command_transport(key, ctx); +} + +static int config_epoch_source_precedes_index( + const struct stat *source, const struct stat *index) +{ + return source->st_ctimespec.tv_sec < index->st_birthtimespec.tv_sec || + (source->st_ctimespec.tv_sec == + index->st_birthtimespec.tv_sec && + source->st_ctimespec.tv_nsec < + index->st_birthtimespec.tv_nsec); +} + +static int config_epoch_capture_source( + const char *key, const char *value UNUSED, + const struct config_context *ctx, void *data) +{ + struct config_epoch_proof *proof = data; + struct config_epoch_source *source; + struct path_namespace_snapshot *after = NULL; + struct strbuf normalized = STRBUF_INIT; + struct stat named; + char *absolute = NULL; + int fd = -1; + int allocated = 0; + + if (proof->failed) + return 0; + if (!ctx || !ctx->kvi) + goto fail; + if (ctx->kvi->scope == CONFIG_SCOPE_COMMAND) { + if (!config_epoch_command_is_safe(key, ctx)) + goto fail; + return 0; + } + if (starts_with(key, "includeif.")) + goto fail; + if (ctx->kvi->origin_type != CONFIG_ORIGIN_FILE || + !ctx->kvi->filename || !*ctx->kvi->filename) + goto fail; + for (size_t i = 0; i < proof->nr; i++) + if (!strcmp(proof->sources[i].path, ctx->kvi->filename)) + return 0; + absolute = absolute_pathdup(ctx->kvi->filename); + strbuf_addstr(&normalized, absolute); + if (strbuf_normalize_path(&normalized)) + goto fail; + if (ctx->kvi->scope == CONFIG_SCOPE_SYSTEM && + proof->system_path && strcmp(normalized.buf, proof->system_path)) + goto fail; + fd = open_nofollow(normalized.buf, O_RDONLY | O_CLOEXEC); + if (fd < 0) + goto fail; + ALLOC_GROW(proof->sources, proof->nr + 1, proof->alloc); + source = &proof->sources[proof->nr]; + memset(source, 0, sizeof(*source)); + source->fd = -1; + allocated = 1; + if (fstat(fd, &source->stat) || + !S_ISREG(source->stat.st_mode) || + source->stat.st_nlink != 1 || + (!is_path_owned_by_current_user(normalized.buf, NULL) && + !(source->stat.st_uid == 0 && + ctx->kvi->scope == CONFIG_SCOPE_SYSTEM)) || + !config_epoch_source_precedes_index(&source->stat, &proof->index) || + lstat(normalized.buf, &named) || + !path_namespace_stat_equal(&source->stat, &named) || + path_namespace_capture(normalized.buf, &source->namespace) || + !path_namespace_target_present(source->namespace) || + path_namespace_capture(normalized.buf, &after) || + !path_namespace_equal(source->namespace, after)) + goto fail; + source->path = xstrdup(ctx->kvi->filename); + source->fd = fd; + proof->nr++; + if (ctx->kvi->scope == CONFIG_SCOPE_SYSTEM && proof->system_path) + proof->system_seen = 1; + fd = -1; + path_namespace_clear(after); + strbuf_release(&normalized); + free(absolute); + return 0; + +fail: + if (fd >= 0) + close(fd); + if (allocated) + path_namespace_clear(proof->sources[proof->nr].namespace); + path_namespace_clear(after); + strbuf_release(&normalized); + free(absolute); + proof->failed = 1; + return 0; +} + +static int config_epoch_sources_still_match( + const struct config_epoch_proof *proof) +{ + for (size_t i = 0; i < proof->nr; i++) { + const struct config_epoch_source *source = &proof->sources[i]; + struct path_namespace_snapshot *namespace = NULL; + struct strbuf normalized = STRBUF_INIT; + struct stat held, named; + char *absolute = absolute_pathdup(source->path); + int valid; + + strbuf_addstr(&normalized, absolute); + valid = !strbuf_normalize_path(&normalized) && + !fstat(source->fd, &held) && + !lstat(normalized.buf, &named) && + path_namespace_stat_equal(&source->stat, &held) && + path_namespace_stat_equal(&held, &named) && + config_epoch_source_precedes_index(&held, &proof->index) && + !path_namespace_capture(normalized.buf, &namespace) && + path_namespace_equal(source->namespace, namespace); + path_namespace_clear(namespace); + strbuf_release(&normalized); + free(absolute); + if (!valid) + return 0; + } + return 1; +} +#endif + +int clean_status_config_tracked_sources_predate_index( + struct index_state *istate) +{ +#ifdef __APPLE__ + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + struct config_epoch_proof proof = { 0 }; + struct config_options opts = { 0 }; + const char *system_path = getenv("GIT_CONFIG_SYSTEM"); + int valid = 0; + + /* + * Version-one proofs did not record their tracked-stat policy. The + * shipped writer is trusted not to have used transient tracked-policy + * overrides; stable configuration sources older than its index then + * authenticate the one-time migration. Version-two proofs carry their + * complete policy instead and never use this compatibility exception. + */ + if (!istate || getenv("GIT_CONFIG_GLOBAL") || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || + getenv(INDEX_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + clean_status_index_snapshot_pin(&snapshot, istate) || + fstat(snapshot.fd, &proof.index) || + proof.index.st_birthtimespec.tv_sec <= 0) + goto done; + if (system_path) { + struct strbuf normalized = STRBUF_INIT; + + if (!is_absolute_path(system_path)) + goto done; + strbuf_addstr(&normalized, system_path); + if (strbuf_normalize_path(&normalized)) { + strbuf_release(&normalized); + goto done; + } + proof.system_path = strbuf_detach(&normalized, NULL); + } + opts.respect_includes = 1; + opts.commondir = istate->repo->commondir; + opts.git_dir = istate->repo->gitdir; + if (config_with_options(config_epoch_capture_source, &proof, NULL, + istate->repo, &opts) < 0 || + proof.failed || !proof.nr || + (proof.system_path && !proof.system_seen) || + !config_epoch_sources_still_match(&proof) || + !clean_status_index_snapshot_still_matches_proof_epoch( + &snapshot, istate)) + goto done; + valid = 1; + +done: + free(proof.system_path); + for (size_t i = 0; i < proof.nr; i++) { + close(proof.sources[i].fd); + path_namespace_clear(proof.sources[i].namespace); + free(proof.sources[i].path); + } + free(proof.sources); + clean_status_index_snapshot_release(&snapshot); + return valid; +#else + (void)istate; + return 0; +#endif +} diff --git a/clean-status-config.h b/clean-status-config.h new file mode 100644 index 00000000000000..0c381ecd14a27b --- /dev/null +++ b/clean-status-config.h @@ -0,0 +1,44 @@ +#ifndef CLEAN_STATUS_CONFIG_H +#define CLEAN_STATUS_CONFIG_H + +#include "hash.h" + +struct config_context; +struct index_state; +struct repository; +struct clean_status_pending_filter; + +struct clean_status_config_digest { + struct git_hash_ctx ctx; + struct git_hash_ctx semantic_ctx; + struct git_hash_ctx tracked_policy_ctx; + unsigned char hash[GIT_MAX_RAWSZ]; + unsigned char semantic_hash[GIT_MAX_RAWSZ]; + unsigned char tracked_policy_hash[GIT_MAX_RAWSZ]; + struct clean_status_pending_filter *pending_filter; + unsigned initialized : 1; + unsigned finalized : 1; + unsigned filter_configured : 1; + unsigned normalized_filter_disable : 1; + unsigned semantic_config_explicit : 1; + unsigned attribute_tree_configured : 1; + unsigned fsmonitor_value_seen : 1; + unsigned fsmonitor_value_boolean : 1; + unsigned fsmonitor_value_enabled : 1; + unsigned submodule_recurse_seen : 1; + unsigned submodule_recurse_known_false : 1; +}; + +void clean_status_config_init(struct clean_status_config_digest *digest, + const struct git_hash_algo *algo); +void clean_status_config_add(struct clean_status_config_digest *digest, + const char *key, const char *value, + const struct config_context *ctx); +void clean_status_config_final(struct clean_status_config_digest *digest); +int clean_status_config_read_repository( + struct repository *repo, + struct clean_status_config_digest *digest); +int clean_status_config_tracked_sources_predate_index( + struct index_state *istate); + +#endif /* CLEAN_STATUS_CONFIG_H */ diff --git a/clean-status-epoch.c b/clean-status-epoch.c new file mode 100644 index 00000000000000..f78bf8fb6bd78f --- /dev/null +++ b/clean-status-epoch.c @@ -0,0 +1,222 @@ +#include "git-compat-util.h" +#include "attr-fingerprint.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-ll.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "trace2.h" + +/* + * State captured before a worktree scan. Every recorded input must still + * match after the closing provider query before scan results are accepted. + */ +struct clean_status_proof_epoch { + struct index_state *istate; + struct clean_status_index_snapshot index; + char *scan_start_token; + unsigned char config_hash[GIT_MAX_RAWSZ]; + unsigned char semantic_hash[GIT_MAX_RAWSZ]; + unsigned char attr_hash[GIT_MAX_RAWSZ]; + unsigned char attr_namespace_hash[GIT_MAX_RAWSZ]; + unsigned char manifest_hash[GIT_MAX_RAWSZ]; + uint32_t manifest_flags; + unsigned semantic_explicit : 1; + unsigned attr_sources_present : 1; + unsigned filter_configured : 1; + unsigned filter_scope_valid : 1; + unsigned strong_mismatch : 1; + unsigned config_mismatch : 1; +}; + +static int config_matches_epoch( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch) +{ + struct clean_status_state *state = istate->clean_status; + struct clean_status_config_digest digest; + const struct git_hash_algo *algo = istate->repo->hash_algo; + + if (clean_status_config_read_repository(istate->repo, &digest)) + return 0; + return digest.finalized && + digest.filter_configured == epoch->filter_configured && + digest.semantic_config_explicit == epoch->semantic_explicit && + !memcmp(digest.hash, epoch->config_hash, algo->rawsz) && + !memcmp(digest.semantic_hash, epoch->semantic_hash, algo->rawsz) && + state && state->current_config_valid && + state->current_semantic_valid && + !memcmp(state->current_config_hash, epoch->config_hash, + algo->rawsz) && + !memcmp(state->current_semantic_hash, epoch->semantic_hash, + algo->rawsz); +} + +struct clean_status_proof_epoch *clean_status_capture_proof_epoch( + struct index_state *istate, + const struct attr_source_snapshot *attrs, + int validate_filter_scope) +{ + struct clean_status_state *state = istate->clean_status; + struct clean_status_proof_epoch *epoch; + struct clean_status_config_digest digest; + struct clean_status_index_snapshot index; + const struct attr_fingerprint *fingerprint = + attr_source_snapshot_fingerprint(attrs); + uint32_t manifest_requirements = + FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + if (istate->split_index || !state || !state->current_config_valid || + !state->config_enforced || + !state->current_semantic_valid || !state->current_attr_valid || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.global_fallback || + (clean_status_filter_scope_needs_validation(istate) && + !validate_filter_scope) || + (state->manifest.current_flags & manifest_requirements) != + manifest_requirements || + !fsmonitor_pending_token_from_provider(istate) || + !istate->fsmonitor_last_update_pending || !fingerprint || + memcmp(fingerprint->content_hash, state->current_attr_hash, + istate->repo->hash_algo->rawsz) || + memcmp(fingerprint->namespace_hash, + state->current_attr_namespace_hash, + istate->repo->hash_algo->rawsz) || + fingerprint->sources_present != + state->current_attr_sources_present) + return NULL; + if (clean_status_config_read_repository(istate->repo, &digest) || + !digest.finalized || + digest.filter_configured != state->filter_configured || + digest.semantic_config_explicit != + state->current_semantic_explicit || + memcmp(digest.hash, state->current_config_hash, + istate->repo->hash_algo->rawsz) || + memcmp(digest.semantic_hash, state->current_semantic_hash, + istate->repo->hash_algo->rawsz) || + clean_status_index_snapshot_pin_proof_epoch(&index, istate)) + return NULL; + + CALLOC_ARRAY(epoch, 1); + epoch->istate = istate; + epoch->index = index; + epoch->scan_start_token = xstrdup(istate->fsmonitor_last_update_pending); + memcpy(epoch->config_hash, state->current_config_hash, + istate->repo->hash_algo->rawsz); + memcpy(epoch->semantic_hash, state->current_semantic_hash, + istate->repo->hash_algo->rawsz); + memcpy(epoch->attr_hash, state->current_attr_hash, + istate->repo->hash_algo->rawsz); + memcpy(epoch->attr_namespace_hash, + state->current_attr_namespace_hash, + istate->repo->hash_algo->rawsz); + memcpy(epoch->manifest_hash, state->manifest.current_hash, + istate->repo->hash_algo->rawsz); + epoch->manifest_flags = state->manifest.current_flags; + epoch->semantic_explicit = state->current_semantic_explicit; + epoch->attr_sources_present = state->current_attr_sources_present; + epoch->filter_configured = state->filter_configured; + epoch->filter_scope_valid = state->filter_scope_valid; + epoch->strong_mismatch = state->strong_mismatch; + epoch->config_mismatch = state->config_mismatch; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/proof-epoch-captured", 1); + return epoch; +} + +int clean_status_proof_epoch_start_token_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch) +{ + return epoch && epoch->istate == istate && epoch->scan_start_token && + fsmonitor_pending_token_from_provider(istate) && + istate->fsmonitor_last_update_pending && + !strcmp(epoch->scan_start_token, + istate->fsmonitor_last_update_pending); +} + +static int proof_epoch_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch, + int check_attr_namespace) +{ + struct clean_status_state *state; + struct attr_fingerprint attrs; + const struct git_hash_algo *algo = istate->repo->hash_algo; + int matched = 0; + + if (!epoch || epoch->istate != istate || + !fsmonitor_pending_token_from_provider(istate) || + !istate->fsmonitor_last_update_pending) + goto done; + state = istate->clean_status; + if (!state || !state->current_config_valid || !state->config_enforced || + !state->current_semantic_valid || !state->current_attr_valid || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.global_fallback || + state->manifest.current_flags != epoch->manifest_flags || + state->current_semantic_explicit != epoch->semantic_explicit || + state->current_attr_sources_present != epoch->attr_sources_present || + state->filter_configured != epoch->filter_configured || + state->filter_scope_valid != epoch->filter_scope_valid || + state->strong_mismatch != epoch->strong_mismatch || + state->config_mismatch != epoch->config_mismatch) + goto done; + if (attr_fingerprint_repository(istate->repo, &attrs) || + memcmp(attrs.content_hash, epoch->attr_hash, algo->rawsz) || + (check_attr_namespace && + memcmp(attrs.namespace_hash, epoch->attr_namespace_hash, + algo->rawsz)) || + attrs.sources_present != epoch->attr_sources_present || + memcmp(state->manifest.current_hash, epoch->manifest_hash, + algo->rawsz) || + !config_matches_epoch(istate, epoch) || + !clean_status_index_snapshot_still_matches_proof_epoch( + &epoch->index, istate)) + goto done; + matched = 1; +done: + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/proof-epoch-matched", matched); + return matched; +} + +int clean_status_proof_epoch_prime_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch) +{ + int matched = + clean_status_proof_epoch_start_token_matches(istate, epoch) && + proof_epoch_matches(istate, epoch, 1); + + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/proof-epoch-primed", matched); + return matched; +} + +int clean_status_proof_epoch_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch) +{ + return proof_epoch_matches(istate, epoch, 1); +} + +int clean_status_proof_epoch_content_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch) +{ + return proof_epoch_matches(istate, epoch, 0); +} + +void clean_status_release_proof_epoch( + struct clean_status_proof_epoch *epoch) +{ + if (!epoch) + return; + clean_status_index_snapshot_release(&epoch->index); + free(epoch->scan_start_token); + free(epoch); +} diff --git a/clean-status-fast.c b/clean-status-fast.c new file mode 100644 index 00000000000000..57b6d19d4b0179 --- /dev/null +++ b/clean-status-fast.c @@ -0,0 +1,363 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "attr-fingerprint.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-sidecar.h" +#include "dir.h" +#include "environment.h" +#include "exclude-source-proof.h" +#include "fsmonitor.h" +#include "fsmonitor-settings.h" +#include "object-name.h" +#include "path-namespace.h" +#include "repository.h" +#include "semantic-verify-internal.h" +#include "trace2.h" +#include "worktree.h" +#include "wrapper.h" + +#if !EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN + +int clean_status_try_sidecar( + struct repository *repo UNUSED, + const struct clean_status_config_digest *config UNUSED, + int *repository_inputs_changed, int *provider_reset) +{ + *repository_inputs_changed = 0; + *provider_reset = 0; + return 0; +} + +#else + +struct fast_exclude_context { + int root_fd; +}; + +static void trace_miss(struct repository *repo, const char *reason) +{ + trace2_data_string("status", repo, "clean-proof/miss", reason); +} + +static int open_exclude_parent(void *data, const char *path) +{ + struct fast_exclude_context *context = data; + int flags = O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC; + +#ifdef O_NOFOLLOW + flags |= O_NOFOLLOW; +#endif + if (is_absolute_path(path)) + return open(path, flags); + return openat(context->root_fd, path, flags); +} + +static int capture_standard_excludes( + struct repository *repo, struct fast_exclude_context *context, + struct exclude_source_proof **proof, struct object_id *digest) +{ + struct dir_struct dir = DIR_INIT; + int ret; + + *proof = exclude_source_proof_create( + repo->index, context, open_exclude_parent, + EXCLUDE_SOURCE_PROOF_NONBLOCKING); + dir.internal.exclude_source_proof = *proof; + setup_standard_excludes(&dir); + ret = exclude_source_proof_digest(*proof, repo->hash_algo, digest); + dir_clear(&dir); + return ret; +} + +static int attr_snapshot_still_matches( + struct repository *repo, const struct attr_source_snapshot *snapshot) +{ + const struct attr_fingerprint *expected = + attr_source_snapshot_fingerprint(snapshot); + struct attr_fingerprint current; + + return expected && + !attr_fingerprint_repository(repo, ¤t) && + current.sources_present == expected->sources_present && + !memcmp(current.content_hash, expected->content_hash, + repo->hash_algo->rawsz) && + !memcmp(current.namespace_hash, expected->namespace_hash, + repo->hash_algo->rawsz); +} + +static int hardlink_witnesses_still_match( + struct repository *repo, const struct clean_status_sidecar *sidecar) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && !defined(NO_NSEC) + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + const unsigned char *cursor, *end; + unsigned int namespace_unstable = 0; + int ret = 0; + + if (!sidecar->hardlink_nr) + return 1; + cursor = sidecar->hardlinks; + end = cursor + sidecar->hardlinks_len; + if (!repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + semantic_verify_root_init(repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + for (uint32_t i = 0; i < sidecar->hardlink_nr; i++) { + struct path_stat_identity expected, observed; + const unsigned char *raw_path; + const char *basename; + struct stat held, named; + size_t path_len; + char *name; + int parent_fd, fd; + + if (clean_status_sidecar_next_hardlink( + &cursor, end, &raw_path, &path_len, &expected) || + !path_len || memchr(raw_path, '\0', path_len)) + goto done; + name = xmemdupz(raw_path, path_len); + if (semantic_verify_resolve_parent( + path, name, i, &parent_fd, &basename)) { + free(name); + goto done; + } + fd = semantic_verify_openat( + parent_fd, basename, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0) { + free(name); + goto done; + } + if (fstat(fd, &held) || !S_ISREG(held.st_mode) || + held.st_nlink <= 1 || held.st_dev != root->stat.st_dev || + fstatat(parent_fd, basename, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&held, &named)) { + close(fd); + free(name); + goto done; + } + path_stat_identity_init(&observed, &held); + close(fd); + free(name); + if (!path_stat_identity_equal(&expected, &observed)) + goto done; + } + if (cursor != end || !semantic_verify_root_stable(root)) + goto done; + ret = 1; + +done: + semantic_verify_path_free(path, &namespace_unstable, NULL); + if (namespace_unstable || (root && !semantic_verify_root_stable(root))) + ret = 0; + semantic_verify_root_clear(root); + return ret; +#else + (void)repo; + return !sidecar->hardlink_nr; +#endif +} + +static int fast_path_test_barrier(void) +{ + const char *ready = + getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_BARRIER_READY"); + const char *resume = + getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_BARRIER_RESUME"); + struct strbuf buf = STRBUF_INIT; + int fd; + int ret; + + if (!ready && !resume) + return 0; + if (!ready || !resume) + return -1; + fd = open(resume, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -1; + write_file(ready, "ready"); + ret = strbuf_read(&buf, fd, 1) > 0 ? 0 : -1; + close(fd); + strbuf_release(&buf); + return ret; +} + +static int current_worktree_is_main(struct repository *repo) +{ + struct worktree *worktree = get_current_worktree(repo); + int ret = worktree && is_main_worktree(worktree); + + free_worktree(worktree); + return ret; +} + +int clean_status_try_sidecar( + struct repository *repo, + const struct clean_status_config_digest *config, + int *repository_inputs_changed, int *provider_reset) +{ + struct clean_status_sidecar_record record = + CLEAN_STATUS_SIDECAR_RECORD_INIT; + struct clean_status_index_snapshot index = { .fd = -1 }; + struct attr_source_snapshot *attrs = NULL; + struct exclude_source_proof *excludes = NULL; + struct fast_exclude_context exclude_context = { .root_fd = -1 }; + struct fsmonitor_query_result query = FSMONITOR_QUERY_RESULT_INIT; + struct clean_status_config_digest fresh_config; + struct object_id exclude_digest, head_tree; + struct stat scanned_worktree; + unsigned char repo_hash[GIT_MAX_RAWSZ]; + char *query_token = NULL; + int ret = 0; + + *repository_inputs_changed = 0; + *provider_reset = 0; + if (!config->finalized || + (config->filter_configured && config->normalized_filter_disable) || + getenv(INDEX_ENVIRONMENT) || is_bare_repository(repo) || + !repo_get_work_tree(repo) || + !current_worktree_is_main(repo) || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC) { + trace_miss(repo, "fast-repository-shape"); + goto done; + } + if (clean_status_sidecar_load( + repo->index_file, repo->hash_algo, &record)) { + trace_miss(repo, "fast-sidecar-missing-or-corrupt"); + goto done; + } + if (clean_status_sidecar_pin_source( + repo->index_file, &record.sidecar, repo->hash_algo, + &index)) { + trace_miss(repo, "fast-index-mismatch"); + goto done; + } + if (memcmp(config->hash, record.sidecar.proof.config_hash, + repo->hash_algo->rawsz)) { + *repository_inputs_changed = 1; + trace_miss(repo, "fast-config-changed"); + goto done; + } + if (attr_source_snapshot_repository(repo, &attrs)) { + trace_miss(repo, "fast-attributes"); + goto done; + } + exclude_context.root_fd = open_nofollow( + repo_get_work_tree(repo), + O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC); + if (exclude_context.root_fd < 0 || + fstat(exclude_context.root_fd, &scanned_worktree) || + capture_standard_excludes( + repo, &exclude_context, &excludes, &exclude_digest) || + !oideq(&exclude_digest, + &record.sidecar.proof.exclude_source_digest)) { + trace_miss(repo, "fast-excludes"); + goto done; + } + if (clean_status_repository_fingerprint( + repo, attrs, &index, &scanned_worktree, repo_hash)) { + trace_miss(repo, "fast-repository-unavailable"); + goto done; + } + if (memcmp(repo_hash, record.sidecar.proof.repo_hash, + repo->hash_algo->rawsz)) { + *repository_inputs_changed = 1; + trace_miss(repo, "fast-repository-input"); + goto done; + } + if (repo_get_oid_tree(repo, "HEAD^{tree}", &head_tree) || + !oideq(&head_tree, &record.sidecar.proof.head_tree)) { + trace_miss(repo, "fast-head-changed"); + goto done; + } + if (!hardlink_witnesses_still_match(repo, &record.sidecar)) { + trace_miss(repo, "fast-hardlink-changed"); + goto done; + } + + query_token = xmemdupz( + record.sidecar.token, record.sidecar.token_len); + if (query_builtin_fsmonitor(query_token, &query) != + FSMONITOR_QUERY_DELTA) { + if (query.outcome == FSMONITOR_QUERY_TRIVIAL) + trace2_data_intmax("fsm_client", NULL, + "query/trivial-response", 1); + /* A later successful query cannot erase this lost boundary. */ + *provider_reset = 1; + trace_miss(repo, "fast-provider-changed"); + goto done; + } + if (query.paths.len) { + trace_miss(repo, "fast-provider-changed"); + goto done; + } + if (fast_path_test_barrier()) { + trace_miss(repo, "fast-test-barrier"); + goto done; + } + + if (clean_status_config_read_repository(repo, &fresh_config) || + fresh_config.filter_configured != config->filter_configured || + (fresh_config.filter_configured && + fresh_config.normalized_filter_disable) || + memcmp(fresh_config.hash, config->hash, + repo->hash_algo->rawsz)) { + trace_miss(repo, "fast-config-raced"); + goto done; + } + if (repo_get_oid_tree(repo, "HEAD^{tree}", &head_tree) || + !oideq(&head_tree, &record.sidecar.proof.head_tree)) { + trace_miss(repo, "fast-head-raced"); + goto done; + } + if (clean_status_repository_fingerprint( + repo, attrs, &index, &scanned_worktree, repo_hash) || + memcmp(repo_hash, record.sidecar.proof.repo_hash, + repo->hash_algo->rawsz)) { + trace_miss(repo, "fast-repository-raced"); + goto done; + } + if (!attr_snapshot_still_matches(repo, attrs)) { + trace_miss(repo, "fast-attributes-raced"); + goto done; + } + if (!exclude_source_proof_validate(excludes)) { + trace_miss(repo, "fast-excludes-raced"); + goto done; + } + if (!clean_status_index_snapshot_still_matches_path( + &index, repo->index_file, repo->hash_algo)) { + trace_miss(repo, "fast-index-raced"); + goto done; + } + if (!hardlink_witnesses_still_match(repo, &record.sidecar)) { + trace_miss(repo, "fast-hardlink-raced"); + goto done; + } + + if (record.sidecar.hardlink_nr) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-validated", + record.sidecar.hardlink_nr); + trace2_data_intmax("status", repo, "clean-proof/hit", 1); + ret = 1; + +done: + free(query_token); + fsmonitor_query_result_release(&query); + if (exclude_context.root_fd >= 0) + close(exclude_context.root_fd); + exclude_source_proof_release(excludes); + attr_source_snapshot_free(attrs); + clean_status_index_snapshot_release(&index); + clean_status_sidecar_record_release(&record); + return ret; +} + +#endif /* EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN */ diff --git a/clean-status-history-store.c b/clean-status-history-store.c new file mode 100644 index 00000000000000..ead169a017b6ed --- /dev/null +++ b/clean-status-history-store.c @@ -0,0 +1,612 @@ +#include "git-compat-util.h" + +#ifdef __APPLE__ +#include +#include +#endif + +#include "clean-status-history-store.h" +#include "clean-status-identity.h" +#include "clean-status-index.h" +#include "hash-framing.h" +#include "hex.h" +#include "lockfile.h" +#include "path.h" +#include "strbuf.h" +#include "wrapper.h" + +#define CLEAN_STATUS_HISTORY_CHECKPOINT_MAGIC "CSHS" +#define CLEAN_STATUS_HISTORY_CHECKPOINT_VERSION 2 +#define CLEAN_STATUS_HISTORY_CHECKPOINT_LEGACY_VERSION 1 +#define CLEAN_STATUS_HISTORY_CHECKPOINT_MAX_SIZE (16 * 1024 * 1024) +#define CLEAN_STATUS_HISTORY_STORE_MAX_FILES 8 +#define CLEAN_STATUS_HISTORY_HAS_FSMN (1U << 0) +#define CLEAN_STATUS_HISTORY_HAS_UNTR (1U << 1) +#define CLEAN_STATUS_HISTORY_HAS_FSCF (1U << 2) +#define CLEAN_STATUS_HISTORY_HAS_FSUC (1U << 3) +#define CLEAN_STATUS_FILESYSTEM_ID_SIZE 16 + +struct clean_status_filesystem_id { + unsigned char value[CLEAN_STATUS_FILESYSTEM_ID_SIZE]; +}; + +static int checksum_valid(const void *data, size_t len, + const struct git_hash_algo *algo) +{ + const unsigned char *bytes = data; + unsigned char actual[GIT_MAX_RAWSZ]; + + if (len < algo->rawsz) + return 0; + hash_buffer_digest(algo, data, len - algo->rawsz, actual); + return !memcmp(actual, bytes + len - algo->rawsz, algo->rawsz); +} + +static void proof_namespace_hash(const char *proof_namespace, + const struct git_hash_algo *algo, + unsigned char *out) +{ + static const char domain[] = "git-clean-status-history-namespace-v1"; + struct git_hash_ctx ctx; + + git_hash_init(&ctx, algo); + hash_length_delimited(&ctx, domain, sizeof(domain) - 1); + hash_length_delimited(&ctx, proof_namespace, strlen(proof_namespace)); + git_hash_final(out, &ctx); +} + +static char *history_store_path(const char *index_path, + const char *proof_namespace, + const struct git_hash_algo *algo) +{ + unsigned char hash[GIT_MAX_RAWSZ]; + char hex[GIT_MAX_HEXSZ + 1]; + + proof_namespace_hash(proof_namespace, algo, hash); + hash_to_hex_algop_r(hex, hash, algo); + return xstrfmt("%s.csh1.%s", index_path, hex); +} + +char *clean_status_history_store_witness_path( + const char *index_path, const char *proof_namespace, + const struct git_hash_algo *algo) +{ + unsigned char hash[GIT_MAX_RAWSZ]; + char hex[GIT_MAX_HEXSZ + 1]; + + proof_namespace_hash(proof_namespace, algo, hash); + hash_to_hex_algop_r(hex, hash, algo); + return xstrfmt("%s.cswi.%s", index_path, hex); +} + +struct history_store_file { + char *path; + timestamp_t mtime; + unsigned int mtime_nsec; + unsigned retained : 1; +}; + +static int history_store_file_cmp(const void *va, const void *vb) +{ + const struct history_store_file *a = va; + const struct history_store_file *b = vb; + + if (a->mtime != b->mtime) + return a->mtime < b->mtime ? -1 : 1; + if (a->mtime_nsec != b->mtime_nsec) + return a->mtime_nsec < b->mtime_nsec ? -1 : 1; + return strcmp(a->path, b->path); +} + +/* + * The status caller holds index.lock while publishing a checkpoint. That + * serializes this directory-level retention step with every supported + * publisher, while per-slot lockfiles still make each replacement atomic. + */ +static int prune_history_store(const char *index_path, + const char *retained_path, + const struct git_hash_algo *algo, + size_t limit) +{ + struct history_store_file *files = NULL; + struct strbuf directory = STRBUF_INIT; + struct strbuf prefix = STRBUF_INIT; + struct strbuf candidate = STRBUF_INIT; + const char *slash = find_last_dir_sep(index_path); + const char *base = slash ? slash + 1 : index_path; + const char *retained_slash = find_last_dir_sep(retained_path); + const char *retained_base = retained_slash ? + retained_slash + 1 : retained_path; + DIR *dir = NULL; + struct dirent *de; + size_t nr = 0, alloc = 0, remove_nr; + int ret = -1; + + if (slash) { + if (slash == index_path) + strbuf_addch(&directory, '/'); + else + strbuf_add(&directory, index_path, slash - index_path); + } else { + strbuf_addch(&directory, '.'); + } + strbuf_addf(&prefix, "%s.csh1.", base); + dir = opendir(directory.buf); + if (!dir) + goto done; + while ((de = readdir(dir))) { + const char *suffix; + struct stat st; + + if (!starts_with(de->d_name, prefix.buf)) + continue; + suffix = de->d_name + prefix.len; + if (strlen(suffix) != algo->hexsz || + strspn(suffix, "0123456789abcdef") != algo->hexsz) + continue; + strbuf_reset(&candidate); + strbuf_addf(&candidate, "%s/%s", directory.buf, de->d_name); + if (lstat(candidate.buf, &st) || !S_ISREG(st.st_mode)) + continue; + ALLOC_GROW(files, nr + 1, alloc); + files[nr].path = xstrdup(candidate.buf); + files[nr].mtime = st.st_mtime; + files[nr].mtime_nsec = ST_MTIME_NSEC(st); + files[nr].retained = !strcmp(de->d_name, retained_base); + nr++; + } + if (limit >= nr) { + ret = 0; + goto done; + } + QSORT(files, nr, history_store_file_cmp); + remove_nr = nr - limit; + for (size_t i = 0; i < nr && remove_nr; i++) { + struct stat st; + + if (files[i].retained) + continue; + /* Recheck without following links immediately before removal. */ + if (lstat(files[i].path, &st) || !S_ISREG(st.st_mode) || + unlink(files[i].path)) + goto done; + { + char *witness = xstrdup(files[i].path); + size_t pathlen = strlen(witness); + char *marker = pathlen >= algo->hexsz + 6 ? + witness + pathlen - algo->hexsz - 6 : NULL; + + if (marker && !memcmp(marker, ".csh1.", 6)) + memcpy(marker, ".cswi.", 6); + else + marker = NULL; + if (marker && !lstat(witness, &st) && + S_ISREG(st.st_mode)) + unlink(witness); + free(witness); + } + remove_nr--; + } + ret = remove_nr ? -1 : 0; + +done: + if (dir) + closedir(dir); + for (size_t i = 0; i < nr; i++) + free(files[i].path); + free(files); + strbuf_release(&candidate); + strbuf_release(&prefix); + strbuf_release(&directory); + return ret; +} + +static int open_nofollow_nonblocking(const char *path, int flags) +{ +#ifdef O_NONBLOCK + return open_nofollow(path, flags | O_NONBLOCK); +#else + (void)path; + (void)flags; + errno = ENOSYS; + return -1; +#endif +} + +int clean_status_history_checkpoint_parse( + struct clean_status_history_checkpoint *checkpoint, + const char *proof_namespace, const void *data, size_t len, + const struct git_hash_algo *algo) +{ + const unsigned char *p = data; + const unsigned char *end; + const unsigned char *payload; + unsigned char expected_namespace[GIT_MAX_RAWSZ]; + size_t minimum = 4 + 2 * sizeof(uint32_t) + 2 * algo->rawsz + + 4 * sizeof(uint32_t) + algo->rawsz; + uint32_t version, flags, lengths[4]; + + memset(checkpoint, 0, sizeof(*checkpoint)); + if (!proof_namespace || !*proof_namespace || len < minimum || + len > CLEAN_STATUS_HISTORY_CHECKPOINT_MAX_SIZE || + memcmp(p, CLEAN_STATUS_HISTORY_CHECKPOINT_MAGIC, 4) || + !checksum_valid(data, len, algo)) + return -1; + end = p + len - algo->rawsz; + p += 4; + version = get_be32(p); + if (version != CLEAN_STATUS_HISTORY_CHECKPOINT_VERSION && + version != CLEAN_STATUS_HISTORY_CHECKPOINT_LEGACY_VERSION) + return -1; + p += sizeof(uint32_t); + if (version == CLEAN_STATUS_HISTORY_CHECKPOINT_VERSION) { + minimum += CLEAN_STATUS_IDENTITY_SIZE + + 2 * sizeof(uint32_t) + algo->rawsz; + if (len < minimum) + return -1; + } + flags = get_be32(p); + p += sizeof(uint32_t); + if ((flags & (CLEAN_STATUS_HISTORY_HAS_FSMN | + CLEAN_STATUS_HISTORY_HAS_FSCF)) != + (CLEAN_STATUS_HISTORY_HAS_FSMN | + CLEAN_STATUS_HISTORY_HAS_FSCF) || + !!(flags & CLEAN_STATUS_HISTORY_HAS_UNTR) != + !!(flags & CLEAN_STATUS_HISTORY_HAS_FSUC) || + flags & ~(CLEAN_STATUS_HISTORY_HAS_FSMN | + CLEAN_STATUS_HISTORY_HAS_UNTR | + CLEAN_STATUS_HISTORY_HAS_FSCF | + CLEAN_STATUS_HISTORY_HAS_FSUC)) + return -1; + proof_namespace_hash(proof_namespace, algo, expected_namespace); + if (memcmp(p, expected_namespace, algo->rawsz)) + return -1; + p += algo->rawsz; + memcpy(checkpoint->index_hash, p, algo->rawsz); + p += algo->rawsz; + if (version == CLEAN_STATUS_HISTORY_CHECKPOINT_VERSION) { + if (clean_status_identity_read( + &p, end, &checkpoint->source_identity)) + return -1; + checkpoint->source_version = get_be32(p); + p += sizeof(uint32_t); + checkpoint->source_cache_nr = get_be32(p); + p += sizeof(uint32_t); + oidread(&checkpoint->source_checksum, p, algo); + p += algo->rawsz; + if (checkpoint->source_version < 2 || + checkpoint->source_version > 4) + return -1; + checkpoint->source_alias_valid = 1; + } + for (size_t i = 0; i < ARRAY_SIZE(lengths); i++) { + lengths[i] = get_be32(p); + p += sizeof(uint32_t); + } + payload = p; + if (!!lengths[0] != !!(flags & CLEAN_STATUS_HISTORY_HAS_FSMN) || + !!lengths[1] != !!(flags & CLEAN_STATUS_HISTORY_HAS_UNTR) || + !!lengths[2] != !!(flags & CLEAN_STATUS_HISTORY_HAS_FSCF) || + !!lengths[3] != !!(flags & CLEAN_STATUS_HISTORY_HAS_FSUC)) + return -1; + for (size_t i = 0; i < ARRAY_SIZE(lengths); i++) { + if ((size_t)(end - p) < lengths[i]) + return -1; + p += lengths[i]; + } + if (p != end) + return -1; + p = payload; + if (lengths[0]) { + checkpoint->fsmonitor = p; + checkpoint->fsmonitor_len = lengths[0]; + p += lengths[0]; + } + if (lengths[1]) { + checkpoint->untracked_cache = p; + checkpoint->untracked_cache_len = lengths[1]; + p += lengths[1]; + } + if (lengths[2]) { + checkpoint->fsmonitor_config = p; + checkpoint->fsmonitor_config_len = lengths[2]; + p += lengths[2]; + } + if (lengths[3]) { + checkpoint->fsmonitor_untracked = p; + checkpoint->fsmonitor_untracked_len = lengths[3]; + } + return 0; +} + +int clean_status_history_checkpoint_write( + struct strbuf *out, const char *proof_namespace, + const struct clean_status_history_checkpoint *checkpoint, + const struct git_hash_algo *algo) +{ + unsigned char namespace_hash[GIT_MAX_RAWSZ]; + uint32_t value, flags = 0; + uint32_t version = checkpoint->source_alias_valid ? + CLEAN_STATUS_HISTORY_CHECKPOINT_VERSION : + CLEAN_STATUS_HISTORY_CHECKPOINT_LEGACY_VERSION; + + strbuf_reset(out); + if (!proof_namespace || !*proof_namespace || + checkpoint->fsmonitor_len > UINT32_MAX || + checkpoint->untracked_cache_len > UINT32_MAX || + checkpoint->fsmonitor_config_len > UINT32_MAX || + checkpoint->fsmonitor_untracked_len > UINT32_MAX || + (!!checkpoint->fsmonitor != !!checkpoint->fsmonitor_len) || + (!!checkpoint->untracked_cache != + !!checkpoint->untracked_cache_len) || + (!!checkpoint->fsmonitor_config != + !!checkpoint->fsmonitor_config_len) || + (!!checkpoint->fsmonitor_untracked != + !!checkpoint->fsmonitor_untracked_len) || + (checkpoint->source_alias_valid && + (checkpoint->source_version < 2 || + checkpoint->source_version > 4 || + checkpoint->source_checksum.algo != hash_algo_by_ptr(algo))) || + !checkpoint->fsmonitor_len || !checkpoint->fsmonitor_config_len || + (!!checkpoint->untracked_cache_len != + !!checkpoint->fsmonitor_untracked_len)) + return -1; + flags |= CLEAN_STATUS_HISTORY_HAS_FSMN; + if (checkpoint->untracked_cache_len) + flags |= CLEAN_STATUS_HISTORY_HAS_UNTR; + if (checkpoint->fsmonitor_config_len) + flags |= CLEAN_STATUS_HISTORY_HAS_FSCF; + if (checkpoint->fsmonitor_untracked_len) + flags |= CLEAN_STATUS_HISTORY_HAS_FSUC; + proof_namespace_hash(proof_namespace, algo, namespace_hash); + strbuf_add(out, CLEAN_STATUS_HISTORY_CHECKPOINT_MAGIC, 4); + put_be32(&value, version); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, flags); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, namespace_hash, algo->rawsz); + strbuf_add(out, checkpoint->index_hash, algo->rawsz); + if (checkpoint->source_alias_valid) { + clean_status_identity_write(out, &checkpoint->source_identity); + put_be32(&value, checkpoint->source_version); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, checkpoint->source_cache_nr); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, checkpoint->source_checksum.hash, + algo->rawsz); + } + put_be32(&value, checkpoint->fsmonitor_len); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, checkpoint->untracked_cache_len); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, checkpoint->fsmonitor_config_len); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, checkpoint->fsmonitor_untracked_len); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, checkpoint->fsmonitor, checkpoint->fsmonitor_len); + if (checkpoint->untracked_cache_len) + strbuf_add(out, checkpoint->untracked_cache, + checkpoint->untracked_cache_len); + strbuf_add(out, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len); + if (checkpoint->fsmonitor_untracked_len) + strbuf_add(out, checkpoint->fsmonitor_untracked, + checkpoint->fsmonitor_untracked_len); + hash_append_checksum(out, algo); + if (out->len > CLEAN_STATUS_HISTORY_CHECKPOINT_MAX_SIZE) { + strbuf_reset(out); + return -1; + } + return 0; +} + +int clean_status_history_store_load( + const char *index_path, const char *proof_namespace, + const struct git_hash_algo *algo, + struct clean_status_history_store_record *record) +{ + struct stat st; + char extra; + char *path = history_store_path(index_path, proof_namespace, algo); + int fd = -1, ret = -1; + size_t size; + + memset(&record->checkpoint, 0, sizeof(record->checkpoint)); + strbuf_reset(&record->storage); + fd = open_nofollow_nonblocking(path, O_RDONLY | O_CLOEXEC); + if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || + st.st_size < 0 || + st.st_size > CLEAN_STATUS_HISTORY_CHECKPOINT_MAX_SIZE) + goto done; + size = xsize_t(st.st_size); + strbuf_grow(&record->storage, size); + strbuf_setlen(&record->storage, size); + if ((size_t)read_in_full(fd, record->storage.buf, size) != size || + read(fd, &extra, 1) != 0 || + clean_status_history_checkpoint_parse( + &record->checkpoint, proof_namespace, record->storage.buf, + record->storage.len, algo)) + goto done; + ret = 0; + +done: + if (ret) + strbuf_reset(&record->storage); + if (fd >= 0) + close(fd); + free(path); + return ret; +} + +void clean_status_history_store_record_release( + struct clean_status_history_store_record *record) +{ + strbuf_release(&record->storage); + memset(&record->checkpoint, 0, sizeof(record->checkpoint)); +} + +static int local_apfs_id(int fd MAYBE_UNUSED, + struct clean_status_filesystem_id *id) +{ +#ifdef __APPLE__ + struct statfs fs; +#endif + + memset(id, 0, sizeof(*id)); +#ifdef __APPLE__ + if (fstatfs(fd, &fs) || !(fs.f_flags & MNT_LOCAL) || + strcmp(fs.f_fstypename, "apfs") || + sizeof(fs.f_fsid) > sizeof(id->value)) + return -1; + memcpy(id->value, &fs.f_fsid, sizeof(fs.f_fsid)); + return 0; +#else + return -1; +#endif +} + +static void install_history_witness( + const char *index_path, const char *proof_namespace, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo, int encoded_matches) +{ +#ifdef __APPLE__ + struct clean_status_filesystem_id fsid; + struct clean_status_index_snapshot existing = { .fd = -1 }; + char *witness = NULL, *temporary = NULL; + + if (!snapshot || snapshot->fd < 0 || + local_apfs_id(snapshot->fd, &fsid)) + return; + witness = clean_status_history_store_witness_path( + index_path, proof_namespace, algo); + if (encoded_matches && + !clean_status_index_snapshot_open(&existing, witness, algo) && + existing.version == snapshot->version && + existing.cache_nr == snapshot->cache_nr && + oideq(&existing.checksum, &snapshot->checksum)) { + clean_status_index_snapshot_release(&existing); + free(witness); + return; + } + clean_status_index_snapshot_release(&existing); + temporary = xstrfmt("%s.tmp.%"PRIuMAX, witness, + (uintmax_t)getpid()); + if (!fclonefileat(snapshot->fd, AT_FDCWD, temporary, 0) && + clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo)) + rename(temporary, witness); + unlink(temporary); + free(temporary); + free(witness); +#else + (void)index_path; + (void)proof_namespace; + (void)snapshot; + (void)algo; + (void)encoded_matches; +#endif +} + +int clean_status_history_checkpoint_source_matches( + const char *index_path, + const struct clean_status_history_checkpoint *checkpoint, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo) +{ + struct clean_status_filesystem_id fsid; + + return checkpoint && checkpoint->source_alias_valid && + clean_status_identity_is_durable() && + snapshot && snapshot->fd >= 0 && + !local_apfs_id(snapshot->fd, &fsid) && + clean_status_identity_equal( + &checkpoint->source_identity, &snapshot->identity) && + checkpoint->source_version == snapshot->version && + checkpoint->source_cache_nr == snapshot->cache_nr && + oideq(&checkpoint->source_checksum, &snapshot->checksum) && + clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo); +} + +int clean_status_history_store_install( + const char *index_path, const char *proof_namespace, + const struct clean_status_history_checkpoint *checkpoint, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo) +{ + struct clean_status_filesystem_id fsid; + struct clean_status_history_checkpoint aliased; + struct clean_status_history_store_record current = + CLEAN_STATUS_HISTORY_STORE_RECORD_INIT; + struct strbuf encoded = STRBUF_INIT; + struct lock_file lock = LOCK_INIT; + char *path = history_store_path(index_path, proof_namespace, algo); + struct stat st; + int current_is_regular, encoded_matches = 0; + int checkpoint_fd = -1, ret = -1; + +#ifdef GIT_WINDOWS_NATIVE + /* Preserve the unsupported Windows path's original fail-closed behavior. */ + goto done; +#endif + if (!snapshot || snapshot->fd < 0 || + !clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo)) + goto done; + aliased = *checkpoint; + aliased.source_alias_valid = + clean_status_identity_is_durable() && + !local_apfs_id(snapshot->fd, &fsid); + if (aliased.source_alias_valid) { + aliased.source_identity = snapshot->identity; + aliased.source_version = snapshot->version; + aliased.source_cache_nr = snapshot->cache_nr; + oidcpy(&aliased.source_checksum, &snapshot->checksum); + } + if (clean_status_history_checkpoint_write( + &encoded, proof_namespace, &aliased, algo)) + goto done; + current_is_regular = !lstat(path, &st) && S_ISREG(st.st_mode); + if (!clean_status_history_store_load( + index_path, proof_namespace, algo, ¤t)) + encoded_matches = current.storage.len == encoded.len && + !memcmp(current.storage.buf, encoded.buf, encoded.len); + clean_status_history_store_record_release(¤t); + + /* + * If this namespace is new, make room before the atomic install so a + * successful publication never takes the bounded store above eight + * regular checkpoint slots. No other checkpoint schema is considered. + */ + if (prune_history_store( + index_path, path, algo, + current_is_regular ? CLEAN_STATUS_HISTORY_STORE_MAX_FILES : + CLEAN_STATUS_HISTORY_STORE_MAX_FILES - 1) || + !clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo)) + goto done; + if (aliased.source_alias_valid) + install_history_witness(index_path, proof_namespace, + snapshot, algo, encoded_matches); + if (encoded_matches) { + ret = 0; + goto done; + } + checkpoint_fd = hold_lock_file_for_update(&lock, path, 0); + if (checkpoint_fd < 0 || + (size_t)write_in_full(checkpoint_fd, encoded.buf, encoded.len) != + encoded.len || + !clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo) || + commit_lock_file(&lock)) + goto done; + ret = 0; + +done: + if (ret) + rollback_lock_file(&lock); + free(path); + strbuf_release(&encoded); + return ret; +} diff --git a/clean-status-history-store.h b/clean-status-history-store.h new file mode 100644 index 00000000000000..2e275c87a62afc --- /dev/null +++ b/clean-status-history-store.h @@ -0,0 +1,64 @@ +#ifndef CLEAN_STATUS_HISTORY_STORE_H +#define CLEAN_STATUS_HISTORY_STORE_H + +#include "clean-status-identity.h" +#include "hash.h" +#include "strbuf.h" + +struct clean_status_index_snapshot; + +struct clean_status_history_checkpoint { + unsigned char index_hash[GIT_MAX_RAWSZ]; + unsigned int source_alias_valid : 1; + struct clean_status_identity source_identity; + uint32_t source_version; + uint32_t source_cache_nr; + struct object_id source_checksum; + const unsigned char *fsmonitor; + size_t fsmonitor_len; + const unsigned char *untracked_cache; + size_t untracked_cache_len; + const unsigned char *fsmonitor_config; + size_t fsmonitor_config_len; + const unsigned char *fsmonitor_untracked; + size_t fsmonitor_untracked_len; +}; + +struct clean_status_history_store_record { + struct clean_status_history_checkpoint checkpoint; + struct strbuf storage; +}; + +#define CLEAN_STATUS_HISTORY_STORE_RECORD_INIT { \ + .storage = STRBUF_INIT, \ +} + +int clean_status_history_checkpoint_parse( + struct clean_status_history_checkpoint *checkpoint, + const char *proof_namespace, const void *data, size_t len, + const struct git_hash_algo *algo); +int clean_status_history_checkpoint_write( + struct strbuf *out, const char *proof_namespace, + const struct clean_status_history_checkpoint *checkpoint, + const struct git_hash_algo *algo); +int clean_status_history_store_load( + const char *index_path, const char *proof_namespace, + const struct git_hash_algo *algo, + struct clean_status_history_store_record *record); +void clean_status_history_store_record_release( + struct clean_status_history_store_record *record); +int clean_status_history_store_install( + const char *index_path, const char *proof_namespace, + const struct clean_status_history_checkpoint *checkpoint, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo); +char *clean_status_history_store_witness_path( + const char *index_path, const char *proof_namespace, + const struct git_hash_algo *algo); +int clean_status_history_checkpoint_source_matches( + const char *index_path, + const struct clean_status_history_checkpoint *checkpoint, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo); + +#endif /* CLEAN_STATUS_HISTORY_STORE_H */ diff --git a/clean-status-history.c b/clean-status-history.c new file mode 100644 index 00000000000000..97a501424c0f58 --- /dev/null +++ b/clean-status-history.c @@ -0,0 +1,2823 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "attr-fingerprint.h" +#include "clean-status.h" +#include "clean-status-history-store.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "dir.h" +#include "environment.h" +#include "fsmonitor.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-ll.h" +#include "fsmonitor-settings.h" +#include "hash-framing.h" +#include "hex.h" +#include "lockfile.h" +#include "read-cache-ll.h" +#include "replace-object.h" +#include "repository.h" +#include "semantic-verify-internal.h" +#include "strbuf.h" +#include "trace2.h" +#include "ewah/ewok.h" + +#define CLEAN_STATUS_HISTORY_SCHEMA "builtin-fsmonitor-history-v2" + +static struct repository *external_history_source_repo; + +static void invalidate_disk_history(struct clean_status_state *state) +{ + state->disk_config_seen = 1; + state->disk_config_invalid = 1; + state->disk_config_valid = 0; + state->disk_semantic_valid = 0; + state->disk_tracked_policy_valid = 0; + memset(state->disk_tracked_policy_hash, 0, + sizeof(state->disk_tracked_policy_hash)); + state->disk_attr_valid = 0; + FREE_AND_NULL(state->disk_config_token); + strbuf_reset(&state->disk_config_raw); + state->manifest.disk_valid = 0; + state->manifest.disk_flags = 0; + strbuf_reset(&state->manifest.disk); +} + +int clean_status_read_fsmonitor_config(struct index_state *istate, + const void *data, unsigned long size) +{ + struct clean_status_state *state = clean_status_get_state(istate); + struct fsmonitor_clean_proof proof; + + if (state->disk_config_seen || + fsmonitor_clean_proof_parse(&proof, data, size, + istate->repo->hash_algo) || + clean_status_manifest_load(&state->manifest, + proof.attr_manifest, + proof.attr_manifest_len, + proof.flags, + istate->repo->hash_algo)) { + invalidate_disk_history(state); + trace2_data_intmax("fsmonitor", istate->repo, + "config/invalid-extension", 1); + return 0; + } + + state->disk_config_seen = 1; + state->disk_config_token = xmemdupz(proof.token, proof.token_len); + memcpy(state->disk_config_hash, proof.config_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->disk_semantic_hash, proof.semantic_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->disk_attr_hash, proof.attr_hash, + istate->repo->hash_algo->rawsz); + if (proof.tracked_policy_hash) { + memcpy(state->disk_tracked_policy_hash, + proof.tracked_policy_hash, + istate->repo->hash_algo->rawsz); + state->disk_tracked_policy_valid = 1; + } else { + state->disk_tracked_policy_valid = 0; + memset(state->disk_tracked_policy_hash, 0, + sizeof(state->disk_tracked_policy_hash)); + } + strbuf_add(&state->disk_config_raw, data, size); + state->disk_config_valid = 1; + state->disk_semantic_valid = 1; + state->disk_attr_valid = 1; + return 0; +} + +static int prepare_fsmonitor_config(struct index_state *istate, int trace) +{ + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + int token_coherent, config_coherent, tracked_policy_coherent; + int semantic_changed, attr_changed; + int legacy_empty_attributes = 0; + int manifest_reusable; + int coherent; + + istate->fsmonitor_untracked_revalidation_authenticated = 0; + if (!state || !state->current_config_valid) + return 0; + token_coherent = state->disk_config_valid && + !state->disk_config_invalid && istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && state->disk_config_token && + !strcmp(state->disk_config_token, istate->fsmonitor_last_update); + tracked_policy_coherent = !state->disk_tracked_policy_valid || + (state->current_tracked_policy_valid && + !memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz)); + config_coherent = state->disk_config_valid && + tracked_policy_coherent && + !memcmp(state->disk_config_hash, state->current_config_hash, + algo->rawsz); + semantic_changed = state->disk_semantic_valid && + state->current_semantic_valid && + memcmp(state->disk_semantic_hash, state->current_semantic_hash, + algo->rawsz); + attr_changed = (state->disk_attr_valid && !state->current_attr_valid) || + (state->disk_attr_valid && state->current_attr_valid && + memcmp(state->disk_attr_hash, state->current_attr_hash, + algo->rawsz)); + if (attr_changed && token_coherent && state->disk_semantic_valid && + state->current_semantic_valid && !semantic_changed && + state->disk_attr_valid && state->current_attr_valid && + !state->current_attr_sources_present && !state->filter_configured && + state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + istate == istate->repo->index && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + !repo_has_replace_refs_uncached(istate->repo) && + attr_fingerprint_matches_legacy_absent_sources( + istate->repo, state->disk_attr_hash)) { + attr_changed = 0; + legacy_empty_attributes = 1; + } + coherent = token_coherent && config_coherent && + state->disk_semantic_valid && state->current_semantic_valid && + !semantic_changed && state->disk_attr_valid && + state->current_attr_valid && !attr_changed && + state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; + istate->fsmonitor_untracked_revalidation_authenticated = + token_coherent && config_coherent && + state->disk_semantic_valid && state->current_semantic_valid && + !semantic_changed && state->disk_attr_valid && + state->current_attr_valid && !attr_changed && + state->disk_tracked_policy_valid && + state->current_tracked_policy_valid && + state->manifest.disk_valid && + state->manifest.disk_flags == + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX) && + !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + istate == istate->repo->index && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && fstat_is_reliable() && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + istate->untracked && istate->untracked->root && + istate->untracked->root->valid && + istate->untracked->fsmonitor_revalidation && + istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + !istate->fsmonitor_untracked_valid && + istate->fsmonitor_untracked_token && + starts_with(istate->fsmonitor_last_update, "builtin:") && + istate->fsmonitor_last_update[strlen("builtin:")] && + strcmp(istate->fsmonitor_last_update, "builtin:fake") && + starts_with(istate->fsmonitor_untracked_token, "pending:") && + !strcmp(istate->fsmonitor_last_update + strlen("builtin:"), + istate->fsmonitor_untracked_token + strlen("pending:")); + manifest_reusable = token_coherent && !config_coherent && + state->disk_semantic_valid && state->current_semantic_valid && + !semantic_changed && state->disk_attr_valid && + state->current_attr_valid && !attr_changed && + !state->filter_configured && state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; + state->filter_scope_valid = + (coherent || istate->fsmonitor_untracked_revalidation_authenticated) && + state->filter_configured; + state->config_revalidated = coherent; + state->initial_coherent = coherent; + FREE_AND_NULL(state->config_revalidated_token); + if (coherent) { + state->config_revalidated_token = + xstrdup(istate->fsmonitor_last_update); + clean_status_manifest_adopt_disk(&state->manifest); + } else if (manifest_reusable) { + clean_status_manifest_adopt_disk(&state->manifest); + if (trace) + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-reused", 1); + } + state->config_mismatch = state->config_enforced && !coherent; + state->strong_mismatch = state->config_enforced && + (state->disk_config_invalid || + semantic_changed || attr_changed || + (state->disk_config_valid && !state->current_attr_valid) || + (!state->disk_semantic_valid && + state->current_semantic_explicit) || + (!state->disk_attr_valid && + state->current_attr_sources_present) || + clean_status_filter_scope_needs_validation(istate)); + if (trace) { + if (legacy_empty_attributes) + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/legacy-empty-attributes", 1); + trace2_data_intmax("fsmonitor", istate->repo, + "config/coherent", coherent); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/initial-mismatch", + state->strong_mismatch); + } + return coherent; +} + +void clean_status_prepare_fsmonitor_config(struct index_state *istate) +{ + prepare_fsmonitor_config(istate, 1); +} + +int clean_status_probe_fsmonitor_config(struct index_state *istate) +{ + return prepare_fsmonitor_config(istate, 0); +} + +int clean_status_pending_revalidation_manifest_unchanged( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + const uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + return state && istate->fsmonitor_untracked_revalidation_authenticated && + state->manifest.disk_valid && state->manifest.current_valid && + state->manifest.checked && !state->manifest.current_invalidated && + !state->manifest.global_fallback && !state->manifest.changed && + (state->manifest.disk_flags & required) == required && + (state->manifest.current_flags & required) == required && + !memcmp(state->manifest.disk_hash, state->manifest.current_hash, + istate->repo->hash_algo->rawsz); +} + +int clean_status_try_preserve_tracked_config_epoch( + struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo; + int attr_matches; + + if (!state || !istate->repo || istate != istate->repo->index) + return 0; + algo = istate->repo->hash_algo; + attr_matches = state->disk_attr_valid && + state->current_attr_valid && + (!memcmp(state->disk_attr_hash, state->current_attr_hash, + algo->rawsz) || + attr_fingerprint_matches_legacy_absent_sources( + istate->repo, state->disk_attr_hash)); + if (!state->config_enforced || !state->config_mismatch || + state->strong_mismatch || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->current_config_valid || !state->disk_semantic_valid || + !state->current_semantic_valid || + memcmp(state->disk_semantic_hash, state->current_semantic_hash, + algo->rawsz) || !attr_matches || + (!state->disk_tracked_policy_valid && + state->current_attr_sources_present) || + state->filter_configured || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + state->manifest.global_fallback || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || !state->disk_config_token || + strcmp(state->disk_config_token, istate->fsmonitor_last_update) || + istate->split_index || istate->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + repo_has_replace_refs_uncached(istate->repo) || + !state->current_tracked_policy_valid || + (state->disk_tracked_policy_valid ? + memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz) : + !clean_status_config_tracked_sources_predate_index(istate))) + return 0; + clean_status_mark_fsmonitor_config_valid( + istate, istate->fsmonitor_last_update); + if (!clean_status_revalidated_token_matches(istate)) + return 0; + trace2_data_intmax("fsmonitor", istate->repo, + "config/tracked-epoch-valid", 1); + return 1; +} + +int clean_status_has_persistent_fsmonitor_semantic_history( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->disk_config_valid && + !state->disk_config_invalid && state->disk_semantic_valid && + state->disk_attr_valid && state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + state->disk_config_raw.len; +} + +int clean_status_has_worktree_manifest_history( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + return state && state->disk_config_valid && + !state->disk_config_invalid && state->manifest.disk_valid && + (state->manifest.disk_flags & required) == required; +} + +int clean_status_fsmonitor_semantic_adoption_needed( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + int missing_history; + + if (!state || !state->current_config_valid || !state->config_enforced) + return 0; + if (state->semantic_baseline_pending) + return 0; + missing_history = + !clean_status_has_persistent_fsmonitor_semantic_history(istate) && + !clean_status_has_worktree_manifest_history(istate); + /* + * Keep missing history on the proof path until fsmonitor explicitly + * chooses the narrow forward-baseline lane for a valid legacy token. + */ + return state->strong_mismatch || missing_history || + clean_status_filter_scope_needs_validation(istate); +} + +int clean_status_fsmonitor_semantic_baseline_needed( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + const struct repo_config_values *cfg; + + if (!state || !state->current_config_valid || !state->config_enforced || + state->strong_mismatch || state->disk_config_seen || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !*istate->fsmonitor_last_update) + return 0; + /* + * This helper is exercised by isolated index-state unit fixtures, + * which are not the_repository. The config values are already + * initialized with the repository and need no lazy parsing here. + */ + cfg = &istate->repo->config_values_private_; + if (!cfg->trust_ctime || !cfg->check_stat) + return 0; + return !clean_status_has_persistent_fsmonitor_semantic_history(istate) && + !clean_status_has_worktree_manifest_history(istate); +} + +int clean_status_fsmonitor_semantic_baseline_pending( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->semantic_baseline_pending; +} + +void clean_status_begin_fsmonitor_semantic_baseline( + struct index_state *istate) +{ + struct clean_status_state *state = clean_status_get_state(istate); + + state->semantic_baseline_pending = 1; +} + +static int current_proof_is_writable(const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + state->config_enforced && state->current_config_valid && + state->current_semantic_valid && state->current_attr_valid && + state->manifest.current_valid && + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + !clean_status_filter_scope_needs_validation(istate) && + clean_status_revalidated_token_matches(istate); +} + +int clean_status_has_current_full_fsmonitor_proof( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return current_proof_is_writable(istate) && + state->manifest.checked && + !state->manifest.current_invalidated && + !state->manifest.global_fallback; +} + +void clean_status_advance_fsmonitor_config_token( + struct index_state *istate, const char *next_token) +{ + struct clean_status_state *state = istate->clean_status; + + if (!next_token || !current_proof_is_writable(istate)) + return; + if (strcmp(istate->fsmonitor_last_update, next_token)) + clean_status_clear_authenticated_new_directories(istate); + FREE_AND_NULL(state->config_revalidated_token); + state->config_revalidated_token = xstrdup(next_token); + trace2_data_intmax("fsmonitor", istate->repo, + "config/token-advanced", 1); +} + +int clean_status_should_write_fsmonitor_config( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return current_proof_is_writable(istate) || + (state && state->disk_config_valid && + !state->disk_config_invalid && state->disk_config_raw.len); +} + +void clean_status_write_fsmonitor_config(struct strbuf *out, + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + + if (current_proof_is_writable(istate)) { + struct fsmonitor_clean_proof proof = { + .flags = state->manifest.current_flags, + .token = (const unsigned char *)istate->fsmonitor_last_update, + .token_len = strlen(istate->fsmonitor_last_update), + .config_hash = state->current_config_hash, + .semantic_hash = state->current_semantic_hash, + .attr_hash = state->current_attr_hash, + .tracked_policy_hash = + state->current_tracked_policy_valid ? + state->current_tracked_policy_hash : NULL, + .attr_manifest = + (const unsigned char *)state->manifest.current.buf, + .attr_manifest_len = state->manifest.current.len, + }; + + if (fsmonitor_clean_proof_write(out, &proof, algo)) + BUG("cannot serialize validated fsmonitor clean proof"); + return; + } + if (fsmonitor_clean_proof_copy_without_bindings( + out, state->disk_config_raw.buf, state->disk_config_raw.len, algo)) + BUG("cannot preserve validated fsmonitor clean proof"); +} + +struct clean_status_external_checkpoint { + char proof_namespace[GIT_MAX_HEXSZ + 1]; + struct clean_status_history_checkpoint checkpoint; + struct strbuf fsmonitor; + struct strbuf untracked_cache; + struct strbuf fsmonitor_config; + struct strbuf fsmonitor_untracked; +}; + +static void clean_status_release_external_history( + struct clean_status_external_checkpoint *checkpoint); + +static int external_history_namespace(struct index_state *istate, char *out) +{ + static const char domain[] = "git-clean-status-history-key-v2"; + struct clean_status_state *state = istate->clean_status; + struct git_hash_ctx ctx; + unsigned char hash[GIT_MAX_RAWSZ]; + char *worktree = NULL, *gitdir = NULL, *commondir = NULL; + int ret = -1; + + if (!state || !state->current_config_valid || + !state->current_semantic_valid || !state->current_attr_valid || + !repo_get_work_tree(istate->repo)) + return -1; + worktree = real_pathdup(repo_get_work_tree(istate->repo), 0); + gitdir = real_pathdup(repo_get_git_dir(istate->repo), 0); + commondir = real_pathdup(repo_get_common_dir(istate->repo), 0); + if (!worktree || !gitdir || !commondir) + goto done; + git_hash_init(&ctx, istate->repo->hash_algo); + hash_length_delimited(&ctx, domain, sizeof(domain) - 1); + hash_length_delimited(&ctx, CLEAN_STATUS_HISTORY_SCHEMA, + strlen(CLEAN_STATUS_HISTORY_SCHEMA)); + hash_length_delimited(&ctx, state->current_config_hash, + istate->repo->hash_algo->rawsz); + hash_length_delimited(&ctx, state->current_semantic_hash, + istate->repo->hash_algo->rawsz); + hash_length_delimited(&ctx, + state->current_attr_portable_namespace_hash, + istate->repo->hash_algo->rawsz); + hash_length_delimited(&ctx, worktree, strlen(worktree)); + hash_length_delimited(&ctx, gitdir, strlen(gitdir)); + hash_length_delimited(&ctx, commondir, strlen(commondir)); + git_hash_final(hash, &ctx); + hash_to_hex_algop_r(out, hash, istate->repo->hash_algo); + ret = 0; + +done: + free(worktree); + free(gitdir); + free(commondir); + return ret; +} + +void clean_status_require_external_history_source(struct repository *repo) +{ + external_history_source_repo = repo; +} + +void clean_status_capture_external_history_source( + struct index_state *istate) +{ + struct clean_status_history_store_record record = + CLEAN_STATUS_HISTORY_STORE_RECORD_INIT; + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + struct clean_status_state *state = istate->clean_status; + char proof_namespace[GIT_MAX_HEXSZ + 1]; + + if (!clean_status_external_history_enabled(istate) || + getenv(INDEX_ENVIRONMENT) || istate != istate->repo->index || + !state) + goto done; + if (state->source_logical_hash_valid) + goto done; + if (!clean_status_has_persistent_fsmonitor_semantic_history(istate)) + goto done; + if (clean_status_index_snapshot_pin_proof_epoch(&snapshot, istate)) + goto done; + if (!external_history_namespace(istate, proof_namespace) && + !clean_status_history_store_load( + istate->repo->index_file, proof_namespace, + istate->repo->hash_algo, &record) && + clean_status_index_can_reuse_source_logical_hash(istate) && + clean_status_history_checkpoint_source_matches( + istate->repo->index_file, &record.checkpoint, + &snapshot, istate->repo->hash_algo)) { + memcpy(state->source_logical_hash, + record.checkpoint.index_hash, + istate->repo->hash_algo->rawsz); + } else if (clean_status_index_logical_digest( + istate, state->source_logical_hash)) { + goto done; + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + &snapshot, istate)) + goto done; + state->source_logical_hash_valid = 1; + +done: + clean_status_index_snapshot_release(&snapshot); + clean_status_history_store_record_release(&record); +} + +int clean_status_capture_external_history_source_from_snapshot( + struct index_state *istate, + const struct clean_status_index_snapshot *snapshot) +{ +#ifdef __linux__ + struct index_state original = INDEX_STATE_INIT(istate->repo); + struct clean_status_state *state = istate->clean_status; + unsigned char hash[GIT_MAX_RAWSZ]; + int owned, captured = -1; + + /* + * The selected entry may already contain a legitimate stat repair. + * Recover its original logical source from the still-pinned physical + * descriptor instead of authenticating the mutated in-memory index. + */ + if (!state || + !clean_status_external_history_enabled(istate) || + getenv(INDEX_ENVIRONMENT) || istate != istate->repo->index || + snapshot->fd < 0 || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + if (state->source_logical_hash_valid) { + captured = 0; + goto done; + } + owned = fcntl(snapshot->fd, F_DUPFD_CLOEXEC, 0); + if (owned < 0 || + do_read_index_from_fd(&original, owned, + istate->repo->index_file) < 0 || + original.repo != istate->repo || + original.version != snapshot->version || + original.cache_nr != snapshot->cache_nr || + !oideq(&original.oid, &snapshot->checksum) || + original.split_index || original.sparse_index != INDEX_EXPANDED || + clean_status_index_logical_digest(&original, hash) || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + memcpy(state->source_logical_hash, hash, + istate->repo->hash_algo->rawsz); + state->source_logical_hash_valid = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "history/scoped-original-source-restored", 1); + captured = 0; + +done: + if (original.fsmonitor_dirty) + ewah_free(original.fsmonitor_dirty); + original.fsmonitor_dirty = NULL; + release_index(&original); + return captured; +#else + (void)istate; + (void)snapshot; + return -1; +#endif +} + +static struct clean_status_external_checkpoint * +clean_status_prepare_external_history(struct index_state *istate) +{ + struct clean_status_external_checkpoint *checkpoint; + struct clean_status_state *state = istate->clean_status; + const unsigned int acceleration_changes = + CE_ENTRY_CHANGED | FSMONITOR_CHANGED | UNTRACKED_CHANGED; + + if (!clean_status_external_history_enabled(istate) || + getenv(INDEX_ENVIRONMENT) || istate != istate->repo->index) + return NULL; + if (!state || !state->source_logical_hash_valid) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "missing-source"); + return NULL; + } + if (!current_proof_is_writable(istate)) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "unwritable-proof"); + return NULL; + } + if (istate->cache_changed & ~acceleration_changes) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "logical-flags"); + return NULL; + } + if (has_racy_timestamp(istate)) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "racy-index"); + return NULL; + } + CALLOC_ARRAY(checkpoint, 1); + checkpoint->fsmonitor = (struct strbuf)STRBUF_INIT; + checkpoint->untracked_cache = (struct strbuf)STRBUF_INIT; + checkpoint->fsmonitor_config = (struct strbuf)STRBUF_INIT; + checkpoint->fsmonitor_untracked = (struct strbuf)STRBUF_INIT; + if (external_history_namespace( + istate, checkpoint->proof_namespace)) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "namespace"); + goto fail; + } + if (clean_status_index_can_reuse_source_logical_hash(istate)) { + memcpy(checkpoint->checkpoint.index_hash, + state->source_logical_hash, + istate->repo->hash_algo->rawsz); + } else if (clean_status_index_logical_digest_after_status( + istate, checkpoint->checkpoint.index_hash)) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "logical-flags"); + goto fail; + } + if (memcmp(checkpoint->checkpoint.index_hash, + state->source_logical_hash, + istate->repo->hash_algo->rawsz)) { + trace2_data_string("fsmonitor", istate->repo, + "history/external-save-reject", "logical-change"); + goto fail; + } + snapshot_fsmonitor_extension(&checkpoint->fsmonitor, istate); + clean_status_write_fsmonitor_config( + &checkpoint->fsmonitor_config, istate); + if (istate->untracked) { + if (!istate->fsmonitor_untracked_valid || + !istate->fsmonitor_untracked_token || + strcmp(istate->fsmonitor_untracked_token, + istate->fsmonitor_last_update)) { + trace2_data_string( + "fsmonitor", istate->repo, + "history/external-save-reject", "untracked-token"); + goto fail; + } + write_untracked_extension( + &checkpoint->untracked_cache, istate->untracked); + write_fsmonitor_untracked_extension( + &checkpoint->fsmonitor_untracked, istate); + } + checkpoint->checkpoint.fsmonitor = + (const unsigned char *)checkpoint->fsmonitor.buf; + checkpoint->checkpoint.fsmonitor_len = checkpoint->fsmonitor.len; + checkpoint->checkpoint.untracked_cache = + checkpoint->untracked_cache.len ? + (const unsigned char *)checkpoint->untracked_cache.buf : + NULL; + checkpoint->checkpoint.untracked_cache_len = + checkpoint->untracked_cache.len; + checkpoint->checkpoint.fsmonitor_config = + (const unsigned char *)checkpoint->fsmonitor_config.buf; + checkpoint->checkpoint.fsmonitor_config_len = + checkpoint->fsmonitor_config.len; + checkpoint->checkpoint.fsmonitor_untracked = + checkpoint->fsmonitor_untracked.len ? + (const unsigned char *)checkpoint->fsmonitor_untracked.buf : + NULL; + checkpoint->checkpoint.fsmonitor_untracked_len = + checkpoint->fsmonitor_untracked.len; + return checkpoint; + +fail: + clean_status_release_external_history(checkpoint); + return NULL; +} + +static int clean_status_install_external_history( + struct index_state *istate, + struct clean_status_external_checkpoint *checkpoint) +{ + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + int installed = 0; + + if (!checkpoint || + clean_status_index_snapshot_pin_proof_epoch(&snapshot, istate) || + clean_status_history_store_install( + istate->repo->index_file, checkpoint->proof_namespace, + &checkpoint->checkpoint, &snapshot, + istate->repo->hash_algo)) + goto done; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-stored", 1); + installed = 1; + +done: + clean_status_index_snapshot_release(&snapshot); + return installed; +} + +static void clean_status_release_external_history( + struct clean_status_external_checkpoint *checkpoint) +{ + if (!checkpoint) + return; + strbuf_release(&checkpoint->fsmonitor); + strbuf_release(&checkpoint->untracked_cache); + strbuf_release(&checkpoint->fsmonitor_config); + strbuf_release(&checkpoint->fsmonitor_untracked); + free(checkpoint); +} + +int clean_status_save_external_history(struct index_state *istate) +{ + struct clean_status_external_checkpoint *checkpoint = + clean_status_prepare_external_history(istate); + int saved = clean_status_install_external_history( + istate, checkpoint); + + clean_status_release_external_history(checkpoint); + return saved; +} + +static int on_index_history_is_coherent(struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + + if (!state || !state->disk_config_seen) + return 0; + clean_status_probe_fsmonitor_config(istate); + prepare_fsmonitor_untracked(istate); + return state->initial_coherent && + (!istate->untracked || istate->fsmonitor_untracked_valid); +} + +static int has_usable_on_index_builtin_token( + const struct index_state *istate) +{ + return istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + *istate->fsmonitor_last_update && + starts_with(istate->fsmonitor_last_update, "builtin:") && + strcmp(istate->fsmonitor_last_update, "builtin:fake"); +} + +static enum fsmonitor_query_outcome external_token_query_outcome( + const char *token) +{ + struct fsmonitor_query_result result = + FSMONITOR_QUERY_RESULT_INIT; + enum fsmonitor_query_outcome outcome = + query_builtin_fsmonitor(token, &result); + + fsmonitor_query_result_release(&result); + return outcome; +} + +static int missing_fsmonitor_token_is_replayable( + struct index_state *istate, struct index_state *parsed) +{ + struct fsmonitor_query_result result = + FSMONITOR_QUERY_RESULT_INIT; + const char *token = parsed->fsmonitor_last_update; + const char *path, *end; + int replayable = 0; + + if (!token || !starts_with(token, "builtin:") || + !strcmp(token, "builtin:fake") || + query_builtin_fsmonitor(token, &result) != + FSMONITOR_QUERY_DELTA) + goto done; + path = result.paths.buf; + end = result.paths.buf + result.paths.len; + while (path < end) { + size_t len = strlen(path); + const char *base = find_last_dir_sep(path); + + base = base ? base + 1 : path; + if (!strcmp(path, FSMONITOR_PATH_GLOBAL_INVALIDATE) || + starts_with(path, FSMONITOR_PATH_HARDLINK_INODE_PREFIX)) + goto done; + if (!fspathcmp(base, ".gitattributes")) { + struct index_state witness = *istate; + + witness.clean_status = parsed->clean_status; + witness.fsmonitor_last_update = + parsed->fsmonitor_last_update; + witness.fsmonitor_token_valid = + parsed->fsmonitor_token_valid; + if (!clean_status_manifest_reconcile_deleted_attribute( + &witness, path)) { + struct clean_status_state *state = + parsed->clean_status; + struct strbuf proof = STRBUF_INIT; + + if (!clean_status_manifest_reconcile_display_only_attribute( + &witness, path)) + goto done; + clean_status_write_fsmonitor_config( + &proof, parsed); + strbuf_reset(&state->disk_config_raw); + strbuf_addbuf(&state->disk_config_raw, &proof); + strbuf_reset(&state->manifest.disk); + strbuf_addbuf(&state->manifest.disk, + &state->manifest.current); + memcpy(state->manifest.disk_hash, + state->manifest.current_hash, + parsed->repo->hash_algo->rawsz); + state->manifest.disk_flags = + state->manifest.current_flags; + strbuf_release(&proof); + } + } + path += len + 1; + } + replayable = path == end; + +done: + fsmonitor_query_result_release(&result); + return replayable; +} + +static void invalidate_unwatched_recovered_entry(size_t pos, void *data) +{ + struct index_state *istate = data; + + if (pos >= istate->cache_nr) + BUG("recovered fsmonitor entry is outside the index"); + fsmonitor_invalidate_cache_entry(istate->cache[pos]); +} + +#ifdef __APPLE__ +static int external_semantic_delta_is_safe( + const struct strbuf *paths, struct index_state *old_index, + struct index_state *new_index) +{ + const char *path = paths->buf; + const char *end = paths->buf + paths->len; + + while (path < end) { + size_t len = strlen(path); + const char *base = find_last_dir_sep(path); + + base = base ? base + 1 : path; + if (!len || + starts_with(path, FSMONITOR_PATH_HARDLINK_INODE_PREFIX) || + !fspathcmp(base, ".gitattributes") || + !fspathcmp(base, ".gitignore")) + return 0; + if (path[len - 1] == '/') { + int old_pos = index_name_pos(old_index, path, len); + int new_pos = index_name_pos(new_index, path, len); + const struct cache_entry *entry; + + old_pos = old_pos < 0 ? -old_pos - 1 : old_pos; + new_pos = new_pos < 0 ? -new_pos - 1 : new_pos; + if ((unsigned int)old_pos < old_index->cache_nr && + starts_with(old_index->cache[old_pos]->name, path)) + return 0; + if ((unsigned int)new_pos >= new_index->cache_nr || + !starts_with(new_index->cache[new_pos]->name, path)) { + path += len + 1; + continue; + } + entry = new_index->cache[new_pos]; + if (!clean_status_index_entry_is_semantically_safe( + old_index, NULL, entry)) + return 0; + } + path += len + 1; + } + return path == end; +} + +static void invalidate_external_checkpoint_entry(size_t pos, void *data) +{ + struct index_state *istate = data; + + if (pos < istate->cache_nr) + istate->cache[pos]->ce_flags &= ~CE_FSMONITOR_VALID; +} + +static int external_checkpoint_path_was_replayed( + const char *name, const struct strbuf *paths) +{ + const char *path = paths->buf; + const char *end = paths->buf + paths->len; + + while (path < end) { + size_t len = strlen(path); + + if (!fspathcmp(name, path) || + (path[len - 1] == '/' && !fspathncmp(name, path, len))) + return 1; + path += len + 1; + } + return 0; +} + +static void restore_external_tracked_history( + struct index_state *istate, struct index_state *witness, + const struct clean_status_history_checkpoint *checkpoint, + const struct strbuf *paths, const struct fsmonitor_clean_proof *proof) +{ + struct index_state parsed = INDEX_STATE_INIT(istate->repo); + const struct stat_data empty_stat = { 0 }; + unsigned int old_pos = 0, new_pos = 0, restored = 0, i; + const unsigned int unsafe_flags = CE_VALID | CE_SKIP_WORKTREE | + CE_INTENT_TO_ADD | CE_CONTENT_CHECK_REQUIRED | CE_STAGEMASK; + + if (!checkpoint->fsmonitor_len || !istate->fsmonitor_dirty) + return; + parsed.cache_nr = witness->cache_nr; + if (read_fsmonitor_extension(&parsed, checkpoint->fsmonitor, + checkpoint->fsmonitor_len) || + !parsed.fsmonitor_token_valid || !parsed.fsmonitor_dirty || + !parsed.fsmonitor_last_update || + strlen(parsed.fsmonitor_last_update) != proof->token_len || + memcmp(parsed.fsmonitor_last_update, proof->token, + proof->token_len)) + goto done; + for (i = 0; i < witness->cache_nr; i++) + if (!S_ISGITLINK(witness->cache[i]->ce_mode)) + witness->cache[i]->ce_flags |= CE_FSMONITOR_VALID; + ewah_each_bit(parsed.fsmonitor_dirty, + invalidate_external_checkpoint_entry, witness); + for (i = 0; i < istate->cache_nr; i++) + if (!S_ISGITLINK(istate->cache[i]->ce_mode)) + istate->cache[i]->ce_flags |= CE_FSMONITOR_VALID; + ewah_each_bit(istate->fsmonitor_dirty, + invalidate_external_checkpoint_entry, istate); + while (old_pos < witness->cache_nr && new_pos < istate->cache_nr) { + const struct cache_entry *old_entry = witness->cache[old_pos]; + struct cache_entry *new_entry = istate->cache[new_pos]; + int cmp = strcmp(old_entry->name, new_entry->name); + int recover_stat; + + if (cmp < 0) { + old_pos++; + continue; + } + if (cmp > 0) { + new_pos++; + continue; + } + old_pos++; + new_pos++; + if ((new_entry->ce_flags & CE_FSMONITOR_VALID) || + !(old_entry->ce_flags & CE_FSMONITOR_VALID) || + ((old_entry->ce_flags | new_entry->ce_flags) & unsafe_flags) || + (!S_ISREG(new_entry->ce_mode) && + !S_ISLNK(new_entry->ce_mode)) || + old_entry->ce_mode != new_entry->ce_mode || + !oideq(&old_entry->oid, &new_entry->oid) || + external_checkpoint_path_was_replayed( + new_entry->name, paths)) + continue; + recover_stat = !memcmp(&new_entry->ce_stat_data, &empty_stat, + sizeof(empty_stat)); + if (memcmp(&old_entry->ce_stat_data, &new_entry->ce_stat_data, + sizeof(old_entry->ce_stat_data)) && + (!recover_stat || + !memcmp(&old_entry->ce_stat_data, &empty_stat, + sizeof(empty_stat)) || + is_racy_timestamp(witness, old_entry))) + continue; + if (recover_stat) + new_entry->ce_stat_data = old_entry->ce_stat_data; + if (is_racy_timestamp(istate, new_entry)) { + if (recover_stat) + new_entry->ce_stat_data = empty_stat; + continue; + } + if (recover_stat) { + new_entry->ce_flags |= CE_UPDATE_IN_BASE; + istate->cache_changed |= CE_ENTRY_CHANGED; + istate->clean_status->recovered_tracked_stat = 1; + } + new_entry->ce_flags |= CE_FSMONITOR_VALID; + restored++; + } + if (restored) { + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; + fill_fsmonitor_bitmap(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-tracked-restored", restored); + } + +done: + if (parsed.fsmonitor_dirty) + ewah_free(parsed.fsmonitor_dirty); + parsed.fsmonitor_dirty = NULL; + parsed.cache_nr = 0; + release_index(&parsed); +} + +static int external_index_has_other_tracked_sibling( + struct index_state *istate, const char *name, size_t parent_len) +{ + const unsigned int unsafe_flags = + CE_STAGEMASK | CE_SKIP_WORKTREE | CE_INTENT_TO_ADD; + int pos = index_name_pos(istate, name, parent_len); + + if (pos < 0) + pos = -pos - 1; + for (; (unsigned int)pos < istate->cache_nr; pos++) { + const struct cache_entry *entry = istate->cache[pos]; + + if (ce_namelen(entry) <= parent_len || + memcmp(entry->name, name, parent_len)) + break; + if (!strcmp(entry->name, name)) + continue; + if ((entry->ce_flags & unsafe_flags) || + (!S_ISREG(entry->ce_mode) && !S_ISLNK(entry->ce_mode))) + continue; + return 1; + } + return 0; +} + +static int external_untracked_membership_needs_root_invalidation( + struct index_state *istate, struct index_state *witness, + const char *name) +{ + const char *slash = find_last_dir_sep(name); + size_t parent_len; + + if (!slash || istate->sparse_index || witness->sparse_index) + return 1; + parent_len = slash - name + 1; + return !external_index_has_other_tracked_sibling( + witness, name, parent_len) || + !external_index_has_other_tracked_sibling( + istate, name, parent_len); +} + +static void restore_external_untracked_history( + struct index_state *istate, struct index_state *witness, + const struct clean_status_history_checkpoint *checkpoint, + const struct strbuf *paths, const struct fsmonitor_clean_proof *proof, + int persist_recovered_proof) +{ + struct index_state parsed = INDEX_STATE_INIT(istate->repo); + const char *path = paths->buf; + const char *end = paths->buf + paths->len; + unsigned int old_pos = 0, new_pos = 0; + unsigned int targeted_membership = 0, rooted_membership = 0; + if (istate->fsmonitor_untracked_valid || + !checkpoint->untracked_cache_len || + !checkpoint->fsmonitor_untracked_len) + return; + parsed.untracked = read_untracked_extension( + checkpoint->untracked_cache, + checkpoint->untracked_cache_len); + if (!parsed.untracked || + read_fsmonitor_untracked_extension( + &parsed, checkpoint->fsmonitor_untracked, + checkpoint->fsmonitor_untracked_len) || + parsed.fsmonitor_untracked_extension_invalid || + !parsed.fsmonitor_untracked_token || + strlen(parsed.fsmonitor_untracked_token) != proof->token_len || + memcmp(parsed.fsmonitor_untracked_token, + proof->token, proof->token_len)) + goto done; + free_untracked_cache(istate->untracked); + istate->untracked = parsed.untracked; + parsed.untracked = NULL; + istate->untracked->use_fsmonitor = 1; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + istate->fsmonitor_untracked_extension_seen = 1; + istate->fsmonitor_untracked_extension_invalid = 0; + istate->fsmonitor_untracked_valid = 1; + if (persist_recovered_proof) { + istate->cache_changed |= UNTRACKED_CHANGED; + istate->fsmonitor_untracked_must_persist = 1; + } + while (old_pos < witness->cache_nr || new_pos < istate->cache_nr) { + const struct cache_entry *old_entry = + old_pos < witness->cache_nr ? + witness->cache[old_pos] : NULL; + const struct cache_entry *new_entry = + new_pos < istate->cache_nr ? + istate->cache[new_pos] : NULL; + int cmp = !old_entry ? 1 : !new_entry ? -1 : + strcmp(old_entry->name, new_entry->name); + + if (cmp < 0) { + int rooted = + external_untracked_membership_needs_root_invalidation( + istate, witness, old_entry->name); + + untracked_cache_invalidate_path( + istate, old_entry->name, rooted); + rooted ? rooted_membership++ : targeted_membership++; + old_pos++; + } else if (cmp > 0) { + int rooted = + external_untracked_membership_needs_root_invalidation( + istate, witness, new_entry->name); + + untracked_cache_invalidate_path( + istate, new_entry->name, rooted); + rooted ? rooted_membership++ : targeted_membership++; + new_pos++; + } else { + old_pos++; + new_pos++; + } + } + while (path < end) { + size_t len = strlen(path); + + untracked_cache_invalidate_trimmed_path(istate, path, 0); + path += len + 1; + } + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-untracked-restored", 1); + if (targeted_membership) + trace2_data_intmax("fsmonitor", istate->repo, + "history/untracked-membership-targeted", + targeted_membership); + if (rooted_membership) + trace2_data_intmax("fsmonitor", istate->repo, + "history/untracked-membership-rooted", + rooted_membership); + +done: + release_index(&parsed); +} +#endif + +#if defined(__APPLE__) || SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +static int open_external_history_witness(const char *path) +{ +#ifdef O_NONBLOCK + return open_nofollow(path, O_RDONLY | O_CLOEXEC | O_NONBLOCK); +#else + (void)path; + errno = ENOSYS; + return -1; +#endif +} +#endif + +static int restore_external_semantic_history( + struct index_state *istate, + const struct clean_status_history_checkpoint *checkpoint, + const char *proof_namespace, + const struct clean_status_index_snapshot *snapshot) +{ +#ifdef __APPLE__ + struct index_state witness = INDEX_STATE_INIT(istate->repo); + struct fsmonitor_query_result old = FSMONITOR_QUERY_RESULT_INIT; + struct fsmonitor_query_result current = FSMONITOR_QUERY_RESULT_INIT; + struct fsmonitor_clean_proof proof; + struct clean_status_identity before_identity, after_identity; + struct stat before, after; + unsigned char witness_hash[GIT_MAX_RAWSZ]; + struct clean_status_state *state = istate->clean_status; + char *path = NULL; + int fd = -1, transferred = 0; + int missing_current = 0, seeded_current = 0; + int persist_recovered_proof = + !istate->fsmonitor_untracked_extension_seen && state && + state->initial_coherent && + clean_status_has_persistent_fsmonitor_semantic_history(istate); + + if (!checkpoint->source_alias_valid || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC) + goto done; + if (!has_usable_on_index_builtin_token(istate)) { + if (!state || istate != istate->repo->index || + istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + istate->fsmonitor_extension_seen || + istate->fsmonitor_token_valid || + istate->fsmonitor_last_update || + istate->fsmonitor_last_update_pending || + istate->fsmonitor_dirty || + istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_token || + istate->fsmonitor_untracked_valid || + state->disk_config_seen || state->disk_config_invalid || + state->filter_configured || + state->current_attr_sources_present || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat) + goto done; + missing_current = 1; + } + path = clean_status_history_store_witness_path( + istate->repo->index_file, proof_namespace, + istate->repo->hash_algo); + fd = open_external_history_witness(path); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_nlink != 1 || before.st_uid != geteuid() || + clean_status_identity_from_stat(&before_identity, &before) || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino) + goto done; + if (read_index_entries_from_fd(&witness, fd)) + goto done; + if (fstat(fd, &after) || after.st_nlink != 1 || + after.st_uid != geteuid() || + clean_status_identity_from_stat(&after_identity, &after) || + !clean_status_identity_equal(&before_identity, &after_identity) || + before.st_size != after.st_size || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino || + witness.version != checkpoint->source_version || + witness.cache_nr != checkpoint->source_cache_nr || + !oideq(&witness.oid, &checkpoint->source_checksum) || + clean_status_index_logical_digest(&witness, witness_hash) || + memcmp(witness_hash, checkpoint->index_hash, + istate->repo->hash_algo->rawsz) || + fsmonitor_clean_proof_parse( + &proof, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len, + istate->repo->hash_algo)) + goto done; + if (missing_current && + ((proof.flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || !checkpoint->fsmonitor_len || + !checkpoint->untracked_cache_len || + !checkpoint->fsmonitor_untracked_len)) + goto done; + clean_status_release(&witness); + clean_status_attach_config(&witness); + clean_status_read_fsmonitor_config( + &witness, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len); + free(witness.fsmonitor_last_update); + witness.fsmonitor_last_update = + xmemdupz(proof.token, proof.token_len); + witness.fsmonitor_token_valid = 1; + clean_status_prepare_fsmonitor_config(&witness); + if (!current_proof_is_writable(&witness) || + query_builtin_fsmonitor(witness.fsmonitor_last_update, &old) != + FSMONITOR_QUERY_DELTA) + goto done; + if (!missing_current) { + if (query_builtin_fsmonitor(istate->fsmonitor_last_update, + ¤t) != FSMONITOR_QUERY_DELTA || + strcmp(old.token.buf, current.token.buf) || + !external_semantic_delta_is_safe(&old.paths, + &witness, istate) || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + } + if (missing_current) { + const char *changed = old.paths.buf; + const char *end = old.paths.buf + old.paths.len; + + if (!has_usable_on_index_builtin_token(&witness) || + !old.token.len || + !starts_with(old.token.buf, "builtin:") || + !strcmp(old.token.buf, "builtin:fake") || + !external_semantic_delta_is_safe(&old.paths, + &witness, istate) || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + while (changed < end) { + if (!strcmp(changed, FSMONITOR_PATH_GLOBAL_INVALIDATE)) + goto done; + changed += strlen(changed) + 1; + } + if (changed != end) + goto done; + for (size_t i = 0; i < istate->cache_nr; i++) + if (istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) + goto done; + istate->fsmonitor_last_update = + xstrdup(witness.fsmonitor_last_update); + istate->fsmonitor_token_valid = 1; + istate->fsmonitor_extension_seen = 1; + fill_fsmonitor_bitmap(istate); + seeded_current = 1; + } + if (strcmp(witness.fsmonitor_last_update, + istate->fsmonitor_last_update)) { + clean_status_advance_fsmonitor_config_token( + &witness, istate->fsmonitor_last_update); + free(witness.fsmonitor_last_update); + witness.fsmonitor_last_update = + xstrdup(istate->fsmonitor_last_update); + } + transferred = + clean_status_transfer_current_proof_if_semantically_same_index( + istate, &witness); + if (transferred) { + clean_status_set_authenticated_new_directories( + istate, &witness, &old.paths); + restore_external_tracked_history( + istate, &witness, checkpoint, &old.paths, &proof); + if (missing_current && istate->fsmonitor_dirty) + ewah_each_bit(istate->fsmonitor_dirty, + invalidate_unwatched_recovered_entry, istate); + restore_external_untracked_history( + istate, &witness, checkpoint, &old.paths, &proof, + persist_recovered_proof); + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-semantic-restored", 1); + if (missing_current) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-fsmn-recovered", 1); + } + +done: + if (seeded_current && !transferred) { + FREE_AND_NULL(istate->fsmonitor_last_update); + istate->fsmonitor_token_valid = 0; + istate->fsmonitor_extension_seen = 0; + if (istate->fsmonitor_dirty) + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; + for (size_t i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID; + clean_status_release(istate); + clean_status_attach_config(istate); + } + if (fd >= 0) + close(fd); + free(path); + fsmonitor_query_result_release(&old); + fsmonitor_query_result_release(¤t); + release_index(&witness); + return transferred; +#else + (void)istate; + (void)checkpoint; + (void)proof_namespace; + (void)snapshot; + return 0; +#endif +} + +static int restore_external_bootstrap_manifest( + struct index_state *istate, + const struct clean_status_history_checkpoint *checkpoint, + const char *proof_namespace, + const struct clean_status_index_snapshot *snapshot) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + const unsigned int semantic_flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + const unsigned int transient_flags = + CE_UPDATE | CE_REMOVE | CE_ADDED | CE_WT_REMOVE | + CE_CONFLICTED | CE_UNPACKED | CE_NEW_SKIP_WORKTREE | + CE_MATCHED | CE_STRIP_NAME | CE_CONTENT_CHECK_REQUIRED; + struct clean_status_state *state = istate->clean_status; + struct index_state witness = INDEX_STATE_INIT(istate->repo); + struct clean_status_identity before_identity, after_identity; + struct fsmonitor_query_result changes = + FSMONITOR_QUERY_RESULT_INIT; + struct fsmonitor_clean_proof proof; + struct strbuf rewritten = STRBUF_INIT; + struct stat before, after; + unsigned char witness_hash[GIT_MAX_RAWSZ]; + char *path = NULL; + int fd = -1, attr_pos = -1, transferred = 0; + + if (!checkpoint->source_alias_valid || !state || + state->disk_config_invalid || state->filter_configured || + state->current_attr_sources_present || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + fsmonitor_clean_proof_parse(&proof, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len, + istate->repo->hash_algo) || + (proof.flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL) + goto done; + path = clean_status_history_store_witness_path( + istate->repo->index_file, proof_namespace, + istate->repo->hash_algo); + fd = open_external_history_witness(path); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_nlink != 1 || before.st_uid != geteuid() || + clean_status_identity_from_stat(&before_identity, &before) || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino) + goto done; + if (read_index_entries_from_fd(&witness, fd)) + goto done; + if (fstat(fd, &after) || after.st_nlink != 1 || + after.st_uid != geteuid() || + clean_status_identity_from_stat(&after_identity, &after) || + !clean_status_identity_equal(&before_identity, &after_identity) || + before.st_size != after.st_size || + lstat(path, &after) || before.st_dev != after.st_dev || + before.st_ino != after.st_ino || + witness.version != checkpoint->source_version || + witness.cache_nr != checkpoint->source_cache_nr || + witness.cache_nr != istate->cache_nr || + !oideq(&witness.oid, &checkpoint->source_checksum) || + clean_status_index_logical_digest(&witness, witness_hash) || + memcmp(witness_hash, checkpoint->index_hash, + istate->repo->hash_algo->rawsz)) + goto done; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *old = witness.cache[i]; + const struct cache_entry *current = istate->cache[i]; + + if (ce_namelen(old) != ce_namelen(current) || + memcmp(old->name, current->name, ce_namelen(old) + 1) || + old->ce_mode != current->ce_mode || + ((old->ce_flags ^ current->ce_flags) & semantic_flags) || + ((old->ce_flags | current->ce_flags) & transient_flags)) + goto done; + if (oideq(&old->oid, ¤t->oid)) + continue; + if (attr_pos >= 0 || strcmp(current->name, ".gitattributes") || + !S_ISREG(current->ce_mode)) + goto done; + attr_pos = i; + } + if (attr_pos < 0 || + !clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + clean_status_release(&witness); + clean_status_attach_config(&witness); + clean_status_read_fsmonitor_config( + &witness, checkpoint->fsmonitor_config, + checkpoint->fsmonitor_config_len); + free(witness.fsmonitor_last_update); + witness.fsmonitor_last_update = + xmemdupz(proof.token, proof.token_len); + witness.fsmonitor_token_valid = 1; + clean_status_prepare_fsmonitor_config(&witness); + if (!current_proof_is_writable(&witness)) + goto done; + { + struct index_state current = *istate; + + current.clean_status = witness.clean_status; + current.fsmonitor_last_update = + witness.fsmonitor_last_update; + current.fsmonitor_token_valid = 1; + if (!clean_status_manifest_reconcile_display_only_attribute( + ¤t, ".gitattributes")) + goto done; + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + if (query_builtin_fsmonitor(witness.fsmonitor_last_update, + &changes) != FSMONITOR_QUERY_DELTA) + goto done; + for (const char *changed = changes.paths.buf, + *end = changes.paths.buf + changes.paths.len; + changed < end; changed += strlen(changed) + 1) { + size_t len = strlen(changed); + const char *base = find_last_dir_sep(changed); + + base = base ? base + 1 : changed; + if (!len || + !strcmp(changed, FSMONITOR_PATH_GLOBAL_INVALIDATE) || + starts_with(changed, FSMONITOR_PATH_HARDLINK_INODE_PREFIX) || + changed[len - 1] == '/' || + (!fspathcmp(base, ".gitattributes") && + strcmp(changed, ".gitattributes"))) + goto done; + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + snapshot, istate)) + goto done; + clean_status_write_fsmonitor_config(&rewritten, &witness); + clean_status_release(istate); + clean_status_attach_config(istate); + clean_status_read_fsmonitor_config( + istate, rewritten.buf, rewritten.len); + state = istate->clean_status; + state->config_mismatch = 0; + state->strong_mismatch = 0; + state->initial_coherent = 0; + state->config_revalidated = 0; + clean_status_manifest_adopt_disk(&state->manifest); + state->manifest.checked = 1; + state->manifest.global_fallback = 0; + state->manifest.current_invalidated = 0; + state->authenticated_bootstrap_manifest = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-bootstrap-manifest", 1); + transferred = 1; + +done: + if (fd >= 0) + close(fd); + free(path); + fsmonitor_query_result_release(&changes); + strbuf_release(&rewritten); + release_index(&witness); + return transferred; +#else + (void)istate; + (void)checkpoint; + (void)proof_namespace; + (void)snapshot; + return 0; +#endif +} + +int clean_status_restore_external_history(struct index_state *istate) +{ + struct clean_status_history_store_record record = + CLEAN_STATUS_HISTORY_STORE_RECORD_INIT; + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + struct clean_status_state *state = istate->clean_status; + struct index_state parsed = INDEX_STATE_INIT(istate->repo); + unsigned char index_hash[GIT_MAX_RAWSZ]; + char proof_namespace[GIT_MAX_HEXSZ + 1]; + int record_loaded = 0; + int missing_fsmonitor_recovery = 0; + int owned_index = 0; + int preserve_witness = 0; + int provider_reset_recovery = 0; + int restored = 0; + + /* Split and sparse representations are known only after parsing. */ + if (fsmonitor_scoped_bootstrap_is_active(istate) || + !clean_status_external_history_enabled(istate) || !state || + state->disk_config_invalid || + !state->config_enforced || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_attr_valid || getenv(INDEX_ENVIRONMENT) || + istate != istate->repo->index || + on_index_history_is_coherent(istate) || + clean_status_index_snapshot_pin_proof_epoch(&snapshot, istate)) + goto done; + /* + * An unbound proof for the current configuration records deliberate + * invalidation. A legacy writer removes FSCF entirely, while a proof + * from another configuration must not hide this namespace's checkpoint. + */ + if (state->disk_config_valid && + !memcmp(state->disk_config_hash, state->current_config_hash, + istate->repo->hash_algo->rawsz) && + !clean_status_has_persistent_fsmonitor_semantic_history(istate)) { + missing_fsmonitor_recovery = + !istate->fsmonitor_extension_seen && + !istate->fsmonitor_token_valid && + !istate->fsmonitor_last_update && + fsm_settings__get_mode(istate->repo) == + FSMONITOR_MODE_IPC && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat; + if (!missing_fsmonitor_recovery) { + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } + } + if (external_history_namespace(istate, proof_namespace)) + goto done; + if (!clean_status_history_store_load( + istate->repo->index_file, proof_namespace, + istate->repo->hash_algo, &record)) { + record_loaded = 1; + if (clean_status_index_can_reuse_source_logical_hash(istate) && + clean_status_history_checkpoint_source_matches( + istate->repo->index_file, &record.checkpoint, + &snapshot, istate->repo->hash_algo)) { + memcpy(index_hash, record.checkpoint.index_hash, + istate->repo->hash_algo->rawsz); + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-physical-alias", 1); + goto have_index_hash; + } + } + if (!record_loaded && external_history_source_repo != istate->repo) { + if (missing_fsmonitor_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } + if (clean_status_index_logical_digest(istate, index_hash)) + goto done; + +have_index_hash: + memcpy(state->source_logical_hash, index_hash, + istate->repo->hash_algo->rawsz); + state->source_logical_hash_valid = 1; + if (!record_loaded) { + if (missing_fsmonitor_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } + if (memcmp(index_hash, record.checkpoint.index_hash, + istate->repo->hash_algo->rawsz)) { + if (missing_fsmonitor_recovery) { + if (restore_external_bootstrap_manifest( + istate, &record.checkpoint, proof_namespace, + &snapshot)) + goto done; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } + restored = restore_external_semantic_history( + istate, &record.checkpoint, proof_namespace, &snapshot); + goto done; + } + parsed.cache_nr = istate->cache_nr; + if (read_fsmonitor_extension( + &parsed, record.checkpoint.fsmonitor, + record.checkpoint.fsmonitor_len) || + !parsed.fsmonitor_token_valid || !parsed.fsmonitor_last_update || + !parsed.fsmonitor_dirty) + goto done; + if (record.checkpoint.untracked_cache_len) { + parsed.untracked = read_untracked_extension( + record.checkpoint.untracked_cache, + record.checkpoint.untracked_cache_len); + read_fsmonitor_untracked_extension( + &parsed, record.checkpoint.fsmonitor_untracked, + record.checkpoint.fsmonitor_untracked_len); + if (!parsed.untracked || + parsed.fsmonitor_untracked_extension_invalid || + !parsed.fsmonitor_untracked_token || + strcmp(parsed.fsmonitor_untracked_token, + parsed.fsmonitor_last_update)) + goto done; + } + clean_status_attach_config(&parsed); + clean_status_read_fsmonitor_config( + &parsed, record.checkpoint.fsmonitor_config, + record.checkpoint.fsmonitor_config_len); + prepare_fsmonitor_untracked(&parsed); + clean_status_probe_fsmonitor_config(&parsed); + if (!current_proof_is_writable(&parsed) || + (!!parsed.untracked && !parsed.fsmonitor_untracked_valid)) + goto done; + if (missing_fsmonitor_recovery && + (!parsed.untracked || + !missing_fsmonitor_token_is_replayable(istate, &parsed))) { + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-proof-invalidated", 1); + goto done; + } + /* + * Provider tokens are opaque. A logical-index match says that the + * checkpoint names the same staged entries; it does not say that its + * token can still replay the interval which the named index already + * crossed. Probe a differing checkpoint token before replacing a + * usable on-index boundary when builtin IPC can answer that question. + * A successful delta is queried again by the normal refresh path; a + * failed probe leaves the named index intact. If both tokens receive a + * trivial response, neither boundary can be replayed: a complete, + * authenticated checkpoint can still seed the existing forward + * baseline and parallel untracked-directory revalidation. + */ + if (fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + has_usable_on_index_builtin_token(istate) && + starts_with(parsed.fsmonitor_last_update, "builtin:") && + strcmp(istate->fsmonitor_last_update, + parsed.fsmonitor_last_update)) { + enum fsmonitor_query_outcome outcome = + external_token_query_outcome( + parsed.fsmonitor_last_update); + + if (outcome != FSMONITOR_QUERY_DELTA) { + if (outcome != FSMONITOR_QUERY_TRIVIAL || + !fstat_is_reliable() || + !record.checkpoint.source_alias_valid || + istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + !parsed.untracked || !parsed.untracked->root || + !parsed.untracked->root->valid || + !parsed.untracked->root->valid_recursive || + !parsed.fsmonitor_untracked_valid || + !parsed.fsmonitor_untracked_extension_seen || + parsed.fsmonitor_untracked_extension_invalid || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat || + external_token_query_outcome( + istate->fsmonitor_last_update) != + FSMONITOR_QUERY_TRIVIAL) { + trace2_data_intmax( + "fsmonitor", istate->repo, + "history/external-token-unreplayable", 1); + goto done; + } + provider_reset_recovery = 1; + } + } + if (!clean_status_index_snapshot_still_matches_proof_epoch( + &snapshot, istate)) + goto done; + owned_index = !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !istate->split_index && + !state->current_attr_sources_present && + istate->sparse_index == INDEX_EXPANDED && + !state->disk_config_invalid && + ((!state->disk_config_seen && !state->disk_config_valid && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + has_usable_on_index_builtin_token(istate) && + has_usable_on_index_builtin_token(&parsed) && + !strcmp(istate->fsmonitor_last_update, + parsed.fsmonitor_last_update)) || + (state->disk_config_seen && state->disk_config_valid && + state->disk_semantic_valid && state->disk_attr_valid && + state->manifest.disk_valid && + (((state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL) || + (missing_fsmonitor_recovery && + clean_status_has_worktree_manifest_history(istate))) && + !memcmp(state->disk_config_hash, + state->current_config_hash, + istate->repo->hash_algo->rawsz) && + !memcmp(state->disk_semantic_hash, + state->current_semantic_hash, + istate->repo->hash_algo->rawsz) && + !memcmp(state->disk_attr_hash, + state->current_attr_hash, + istate->repo->hash_algo->rawsz))); + preserve_witness = !state->disk_config_seen && + !istate->fsmonitor_untracked_valid && + has_usable_on_index_builtin_token(istate); + clean_status_invalidate_current_proof(istate); + clean_status_copy_fsmonitor_history(istate, &parsed); + FREE_AND_NULL(istate->fsmonitor_last_update); + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_pending_token_from_provider = 0; + if (istate->fsmonitor_dirty) + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_last_update = parsed.fsmonitor_last_update; + parsed.fsmonitor_last_update = NULL; + istate->fsmonitor_dirty = parsed.fsmonitor_dirty; + parsed.fsmonitor_dirty = NULL; + if (missing_fsmonitor_recovery) + ewah_each_bit(istate->fsmonitor_dirty, + invalidate_unwatched_recovered_entry, istate); + istate->fsmonitor_token_valid = 1; + istate->fsmonitor_extension_seen = 1; + free_untracked_cache(istate->untracked); + istate->untracked = parsed.untracked; + parsed.untracked = NULL; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + parsed.fsmonitor_untracked_token; + parsed.fsmonitor_untracked_token = NULL; + istate->fsmonitor_untracked_extension_seen = + parsed.fsmonitor_untracked_extension_seen; + istate->fsmonitor_untracked_extension_invalid = 0; + istate->fsmonitor_untracked_valid = + parsed.fsmonitor_untracked_valid; + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-restored", 1); + if (provider_reset_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-reset-restored", 1); + if (missing_fsmonitor_recovery) + trace2_data_intmax("fsmonitor", istate->repo, + "history/external-fsmn-recovered", 1); + state->external_history_restored = 1; + state->external_history_owned_index = owned_index; + state->external_history_preserve_witness = preserve_witness; + restored = 1; + +done: + if (parsed.fsmonitor_dirty) + ewah_free(parsed.fsmonitor_dirty); + parsed.fsmonitor_dirty = NULL; + parsed.cache_nr = 0; + release_index(&parsed); + clean_status_index_snapshot_release(&snapshot); + clean_status_history_store_record_release(&record); + return restored; +} + +int clean_status_external_history_was_restored( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->external_history_restored; +} + +int clean_status_external_history_needs_witness_preservation( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->external_history_restored && + state->external_history_preserve_witness && + !state->external_history_owned_index && + istate == istate->repo->index && + current_proof_is_writable(istate); +} + +int clean_status_has_recovered_tracked_stat( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->recovered_tracked_stat && + (istate->cache_changed & CE_ENTRY_CHANGED) && + istate == istate->repo->index && + current_proof_is_writable(istate); +} + +int clean_status_has_authenticated_bootstrap_manifest( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->authenticated_bootstrap_manifest && + state->manifest.current_valid && state->manifest.checked && + !state->manifest.current_invalidated && + (state->manifest.current_flags & + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) == + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX); +} + +int clean_status_external_history_owns_index( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->external_history_restored && + state->external_history_owned_index && + istate == istate->repo->index && + !getenv(INDEX_ENVIRONMENT) && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED; +} + + +void clean_status_copy_fsmonitor_history(struct index_state *dst, + const struct index_state *src) +{ + const struct clean_status_state *src_state = src->clean_status; + struct clean_status_state *dst_state; + + if (!src_state || !src_state->disk_config_valid || + src_state->disk_config_invalid || !src_state->disk_config_raw.len) + return; + dst_state = clean_status_get_state(dst); + dst_state->backoff_suspended = 0; + FREE_AND_NULL(dst_state->backoff_token); + FREE_AND_NULL(dst_state->disk_config_token); + strbuf_reset(&dst_state->disk_config_raw); + dst_state->disk_config_token = + xstrdup_or_null(src_state->disk_config_token); + strbuf_addbuf(&dst_state->disk_config_raw, + &src_state->disk_config_raw); + memcpy(dst_state->disk_config_hash, src_state->disk_config_hash, + dst->repo->hash_algo->rawsz); + memcpy(dst_state->disk_semantic_hash, src_state->disk_semantic_hash, + dst->repo->hash_algo->rawsz); + memcpy(dst_state->disk_tracked_policy_hash, + src_state->disk_tracked_policy_hash, + dst->repo->hash_algo->rawsz); + memcpy(dst_state->disk_attr_hash, src_state->disk_attr_hash, + dst->repo->hash_algo->rawsz); + if (clean_status_manifest_load( + &dst_state->manifest, src_state->manifest.disk.buf, + src_state->manifest.disk.len, src_state->manifest.disk_flags, + dst->repo->hash_algo)) + BUG("cannot copy validated clean-status manifest"); + dst_state->disk_config_seen = 1; + dst_state->disk_config_valid = 1; + dst_state->disk_semantic_valid = src_state->disk_semantic_valid; + dst_state->disk_tracked_policy_valid = + src_state->disk_tracked_policy_valid; + dst_state->disk_attr_valid = src_state->disk_attr_valid; + dst_state->disk_config_invalid = 0; +} + +static int same_persistent_index_contents(const struct index_state *a, + const struct index_state *b) +{ + const unsigned int semantic_flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + const unsigned int transient_flags = + CE_UPDATE | CE_REMOVE | CE_ADDED | CE_WT_REMOVE | + CE_CONFLICTED | CE_UNPACKED | CE_NEW_SKIP_WORKTREE | + CE_MATCHED | CE_STRIP_NAME; + unsigned int i; + + if (a->repo != b->repo || a->split_index || b->split_index || + a->sparse_index || b->sparse_index || + a->cache_nr != b->cache_nr) + return 0; + + for (i = 0; i < a->cache_nr; i++) { + const struct cache_entry *ce_a = a->cache[i]; + const struct cache_entry *ce_b = b->cache[i]; + + if (ce_namelen(ce_a) != ce_namelen(ce_b) || + memcmp(ce_a->name, ce_b->name, ce_namelen(ce_a) + 1) || + ce_a->ce_mode != ce_b->ce_mode || + !oideq(&ce_a->oid, &ce_b->oid) || + ((ce_a->ce_flags ^ ce_b->ce_flags) & semantic_flags) || + ((ce_a->ce_flags | ce_b->ce_flags) & transient_flags)) + return 0; + } + + return 1; +} + +static int replace_current_fsmonitor_proof(struct index_state *dst, + const struct index_state *src) +{ + struct index_state replacement = INDEX_STATE_INIT(dst->repo); + struct strbuf proof = STRBUF_INIT; + int transferred = 0; + + /* Preserve the destination's source identity and retained index fd. */ + clean_status_write_fsmonitor_config(&proof, src); + clean_status_read_fsmonitor_config(&replacement, proof.buf, proof.len); + if (replacement.clean_status && + replacement.clean_status->disk_config_valid && + !replacement.clean_status->disk_config_invalid) { + dst->fsmonitor_token_valid = src->fsmonitor_token_valid; + clean_status_copy_fsmonitor_history(dst, &replacement); + clean_status_attach_config(dst); + clean_status_prepare_fsmonitor_config(dst); + transferred = current_proof_is_writable(dst); + } + clean_status_release(&replacement); + strbuf_release(&proof); + + return transferred; +} + +int clean_status_transfer_current_proof_if_same_index( + struct index_state *dst, const struct index_state *src) +{ + if (!current_proof_is_writable(src) || + !src->fsmonitor_last_update || + !dst->fsmonitor_last_update || + strcmp(src->fsmonitor_last_update, dst->fsmonitor_last_update) || + !same_persistent_index_contents(dst, src)) + return 0; + + /* + * Reparse the current proof as the destination's disk proof, then + * reattach the current command's digest. This copies only a proof + * which the destination's identical logical entries can support. + */ + return replace_current_fsmonitor_proof(dst, src); +} + +int clean_status_transfer_current_proof_if_semantically_same_index( + struct index_state *dst, const struct index_state *src) +{ + const unsigned int semantic_flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + unsigned int src_pos = 0, dst_pos = 0; + int transferred; + + if (!current_proof_is_writable(src) || + src->repo != dst->repo || src->split_index || dst->split_index || + src->sparse_index || dst->sparse_index || + (src->cache_changed & RESOLVE_UNDO_CHANGED) || + src->resolve_undo || + !src->fsmonitor_last_update || !dst->fsmonitor_last_update || + strcmp(src->fsmonitor_last_update, dst->fsmonitor_last_update)) + return 0; + + while (src_pos < src->cache_nr || dst_pos < dst->cache_nr) { + const struct cache_entry *old = src_pos < src->cache_nr ? + src->cache[src_pos] : NULL; + const struct cache_entry *new_entry = dst_pos < dst->cache_nr ? + dst->cache[dst_pos] : NULL; + int cmp; + + if (!old) + cmp = 1; + else if (!new_entry) + cmp = -1; + else + cmp = strcmp(old->name, new_entry->name); + if (cmp < 0) { + if (!clean_status_index_entry_is_semantically_safe( + src, old, NULL)) + return 0; + src_pos++; + } else if (cmp > 0) { + if (!clean_status_index_entry_is_semantically_safe( + src, NULL, new_entry)) + return 0; + dst_pos++; + } else { + if ((old->ce_mode != new_entry->ce_mode || + !oideq(&old->oid, &new_entry->oid) || + ((old->ce_flags ^ new_entry->ce_flags) & semantic_flags)) && + !clean_status_index_entry_is_semantically_safe( + src, old, new_entry)) + return 0; + src_pos++; + dst_pos++; + } + } + + if (current_proof_is_writable(dst)) { + const struct clean_status_state *src_state = src->clean_status; + const struct clean_status_state *dst_state = dst->clean_status; + size_t rawsz = dst->repo->hash_algo->rawsz; + + if (memcmp(src_state->current_config_hash, + dst_state->current_config_hash, rawsz) || + memcmp(src_state->current_semantic_hash, + dst_state->current_semantic_hash, rawsz) || + memcmp(src_state->current_attr_hash, + dst_state->current_attr_hash, rawsz) || + src_state->manifest.current_flags != + dst_state->manifest.current_flags || + src_state->manifest.current.len != + dst_state->manifest.current.len || + memcmp(src_state->manifest.current.buf, + dst_state->manifest.current.buf, + src_state->manifest.current.len)) + return 0; + trace2_data_intmax("fsmonitor", dst->repo, + "history/semantic-transferred", 1); + return 1; + } + + transferred = replace_current_fsmonitor_proof(dst, src); + if (transferred) + trace2_data_intmax("fsmonitor", dst->repo, + "history/semantic-transferred", 1); + + return transferred; +} + +struct clean_status_commit_checkpoint { + struct repository *repo; + struct lock_file *lock; + struct clean_status_index_snapshot source; + struct clean_status_index_snapshot written; + struct attr_source_snapshot *attrs; + struct strbuf config; + struct strbuf untracked; + char *main_path; + char *token; + unsigned char config_hash[GIT_MAX_RAWSZ]; + unsigned char semantic_hash[GIT_MAX_RAWSZ]; + unsigned char tracked_policy_hash[GIT_MAX_RAWSZ]; + unsigned char logical_hash[GIT_MAX_RAWSZ]; + uint64_t written_dev; + uint64_t written_ino; + int writer_fd; + unsigned sealed : 1; + unsigned needs_restore : 1; +}; + +static int commit_checkpoint_owner_matches(const struct stat *st) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + return st->st_uid == geteuid(); +#else + (void)st; + return 0; +#endif +} + +struct clean_status_backoff_transfer { + struct index_state *src; + struct clean_status_state *state; + struct untracked_cache *untracked; + struct clean_status_index_snapshot source; + struct attr_source_snapshot *attrs; + char *main_path; + char *token; + unsigned char config_hash[GIT_MAX_RAWSZ]; + unsigned char semantic_hash[GIT_MAX_RAWSZ]; + unsigned char tracked_policy_hash[GIT_MAX_RAWSZ]; +}; + +static int backoff_transfer_state_is_eligible( + const struct index_state *src, const struct untracked_cache *uc) +{ + const struct clean_status_state *state = src ? src->clean_status : NULL; + const struct git_hash_algo *algo; + const char *suffix, *pending; + const uint32_t historical = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + if (!state || !src->repo || src != src->repo->index || + !clean_status_fsmonitor_backoff_suspended(src) || + !fstat_is_reliable() || get_alternate_index_output() || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + src->split_index || src->sparse_index != INDEX_EXPANDED || + src->resolve_undo || + (src->cache_changed & + (CE_ENTRY_ADDED | CE_ENTRY_REMOVED | RESOLVE_UNDO_CHANGED)) || + repo_config_values(src->repo)->apply_sparse_checkout || + !src->repo->config_values_private_.trust_ctime || + !src->repo->config_values_private_.check_stat || + !state->config_enforced || !state->current_config_valid || + !state->current_semantic_valid || !state->current_attr_valid || + !state->current_tracked_policy_valid || + state->external_history_restored || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->disk_semantic_valid || !state->disk_attr_valid || + !state->disk_tracked_policy_valid || !state->manifest.disk_valid || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || state->manifest.global_fallback || + state->manifest.current_flags != historical || + !state->semantic_baseline_pending || + !src->fsmonitor_extension_seen || !src->fsmonitor_token_valid || + src->fsmonitor_last_update_pending || + src->fsmonitor_pending_token_from_provider || + src->fsmonitor_untracked_valid || + src->fsmonitor_untracked_revalidation_authenticated || + src->fsmonitor_legacy_untracked_fallback || + !src->fsmonitor_untracked_extension_seen || + src->fsmonitor_untracked_extension_invalid || + !src->fsmonitor_untracked_token || !state->disk_config_token || + strcmp(state->disk_config_token, src->fsmonitor_last_update) || + !skip_prefix(src->fsmonitor_last_update, "builtin:", &suffix) || + !*suffix || !strcmp(suffix, "fake") || + !uc || !uc->root || !uc->root->valid || + uc->root->valid_recursive || uc->use_fsmonitor || + !uc->fsmonitor_revalidation || uc->fsmonitor_dirty_paths.len) + return 0; + if (strcmp(src->fsmonitor_last_update, src->fsmonitor_untracked_token) && + (!skip_prefix(src->fsmonitor_untracked_token, "pending:", &pending) || + strcmp(suffix, pending))) + return 0; + algo = src->repo->hash_algo; + return !memcmp(state->disk_config_hash, state->current_config_hash, + algo->rawsz) && + !memcmp(state->disk_semantic_hash, state->current_semantic_hash, + algo->rawsz) && + !memcmp(state->disk_attr_hash, state->current_attr_hash, algo->rawsz) && + !memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz); +} + +static int backoff_transfer_source_matches( + const struct clean_status_backoff_transfer *transfer, + const struct untracked_cache *uc) +{ + const struct index_state *src = transfer->src; + const struct clean_status_state *state = src->clean_status; + struct clean_status_config_digest digest; + const struct git_hash_algo *algo = src->repo->hash_algo; + + return state == transfer->state && uc == transfer->untracked && + backoff_transfer_state_is_eligible(src, uc) && + !strcmp(transfer->token, src->fsmonitor_last_update) && + !memcmp(state->current_config_hash, transfer->config_hash, algo->rawsz) && + !memcmp(state->current_semantic_hash, transfer->semantic_hash, + algo->rawsz) && + !memcmp(state->current_tracked_policy_hash, + transfer->tracked_policy_hash, algo->rawsz) && + clean_status_index_path_is_main(src->repo, transfer->main_path) && + !repo_has_replace_refs_uncached(src->repo) && + clean_status_index_snapshot_still_matches_proof_epoch( + &transfer->source, src) && + clean_status_index_snapshot_still_matches_path( + &transfer->source, transfer->main_path, algo) && + attr_source_snapshot_matches_repository(src->repo, transfer->attrs) && + !clean_status_config_read_repository(src->repo, &digest) && + digest.finalized && + !memcmp(digest.hash, transfer->config_hash, algo->rawsz) && + !memcmp(digest.semantic_hash, transfer->semantic_hash, algo->rawsz) && + !memcmp(digest.tracked_policy_hash, + transfer->tracked_policy_hash, algo->rawsz); +} + +void clean_status_release_backoff_transfer( + struct clean_status_backoff_transfer *transfer) +{ + if (!transfer) + return; + clean_status_index_snapshot_release(&transfer->source); + attr_source_snapshot_free(transfer->attrs); + free(transfer->main_path); + free(transfer->token); + free(transfer); +} + +struct clean_status_backoff_transfer *clean_status_capture_backoff_transfer( + struct index_state *src) +{ + struct clean_status_backoff_transfer *transfer; + const struct clean_status_state *state; + const struct attr_fingerprint *attrs; + struct stat st; + const unsigned int unsafe_flags = CE_STAGEMASK | CE_VALID | + CE_EXTENDED_FLAGS | CE_UPDATE | CE_REMOVE | CE_ADDED | + CE_WT_REMOVE | CE_CONFLICTED | CE_UNPACKED | + CE_NEW_SKIP_WORKTREE | CE_STRIP_NAME; + + if (!src || !backoff_transfer_state_is_eligible(src, src->untracked) || + !clean_status_index_path_is_main(src->repo, src->repo->index_file)) + return NULL; + for (size_t i = 0; i < src->cache_nr; i++) + if (src->cache[i]->ce_flags & unsafe_flags) + return NULL; + state = src->clean_status; + CALLOC_ARRAY(transfer, 1); + transfer->src = src; + transfer->state = src->clean_status; + transfer->untracked = src->untracked; + transfer->source.fd = -1; + transfer->main_path = xstrfmt("%s/index", repo_get_git_dir(src->repo)); + transfer->token = xstrdup(src->fsmonitor_last_update); + memcpy(transfer->config_hash, state->current_config_hash, + src->repo->hash_algo->rawsz); + memcpy(transfer->semantic_hash, state->current_semantic_hash, + src->repo->hash_algo->rawsz); + memcpy(transfer->tracked_policy_hash, state->current_tracked_policy_hash, + src->repo->hash_algo->rawsz); + if (clean_status_index_snapshot_pin_proof_epoch(&transfer->source, src) || + fstat(transfer->source.fd, &st) || + !commit_checkpoint_owner_matches(&st) || + attr_source_snapshot_repository(src->repo, &transfer->attrs)) + goto fail; + attrs = attr_source_snapshot_fingerprint(transfer->attrs); + if (!attrs || + attrs->sources_present != state->current_attr_sources_present || + memcmp(attrs->content_hash, state->current_attr_hash, + src->repo->hash_algo->rawsz) || + !backoff_transfer_source_matches(transfer, src->untracked)) + goto fail; + return transfer; +fail: + clean_status_release_backoff_transfer(transfer); + return NULL; +} + +static int backoff_transfer_entries_match( + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + const unsigned int persistent_flags = + CE_STAGEMASK | CE_EXTENDED | CE_VALID | CE_EXTENDED_FLAGS; + const unsigned int unsafe_flags = CE_STAGEMASK | CE_VALID | + CE_EXTENDED_FLAGS | CE_REMOVE | CE_ADDED | CE_WT_REMOVE | + CE_CONFLICTED | CE_NEW_SKIP_WORKTREE | CE_STRIP_NAME; + + return old && new_entry && + !((old->ce_flags | new_entry->ce_flags) & unsafe_flags) && + ce_namelen(old) == ce_namelen(new_entry) && + !memcmp(old->name, new_entry->name, ce_namelen(old) + 1) && + old->ce_mode == new_entry->ce_mode && + !((old->ce_flags ^ new_entry->ce_flags) & persistent_flags); +} + +int clean_status_backoff_transfer_entry_is_safe( + const struct clean_status_backoff_transfer *transfer, + const struct cache_entry *old, const struct cache_entry *new_entry) +{ + return transfer && transfer->src->clean_status == transfer->state && + transfer->src->untracked == transfer->untracked && + backoff_transfer_state_is_eligible(transfer->src, transfer->untracked) && + backoff_transfer_entries_match(old, new_entry) && + S_ISREG(old->ce_mode) && S_ISREG(new_entry->ce_mode) && + clean_status_index_entry_is_semantically_safe( + transfer->src, old, new_entry); +} + +int clean_status_transfer_backoff_history( + struct clean_status_backoff_transfer *transfer, + struct index_state *dst, struct index_state *src) +{ + struct clean_status_state *state; + const char *suffix; + int changed = 0; + + /* move_index_extensions() must already have moved this exact pending UC. */ + if (!transfer || src != transfer->src || src == dst || + src->repo != dst->repo || src->untracked || + dst->untracked != transfer->untracked || + dst->split_index || dst->sparse_index != INDEX_EXPANDED || + dst->resolve_undo || (dst->cache_changed & RESOLVE_UNDO_CHANGED) || + src->cache_nr != dst->cache_nr || + !dst->fsmonitor_last_update || + strcmp(transfer->token, dst->fsmonitor_last_update) || + !backoff_transfer_source_matches(transfer, dst->untracked)) + return 0; + for (size_t i = 0; i < src->cache_nr; i++) { + const struct cache_entry *old = src->cache[i]; + const struct cache_entry *new_entry = dst->cache[i]; + + if (!backoff_transfer_entries_match(old, new_entry)) + return 0; + if (!oideq(&old->oid, &new_entry->oid)) { + if (!S_ISREG(old->ce_mode) || !S_ISREG(new_entry->ce_mode) || + !clean_status_index_entry_is_semantically_safe( + src, old, new_entry)) + return 0; + changed = 1; + } + } + if (!backoff_transfer_source_matches(transfer, dst->untracked) || + !skip_prefix(transfer->token, "builtin:", &suffix)) + return 0; + + /* + * Keep the source's owned descriptor and identity together. The generic + * extension copier intentionally carries no suspended authority; only + * this same-repository, source-bound ownership move may retain it. + */ + clean_status_release(dst); + state = dst->clean_status = src->clean_status; + src->clean_status = NULL; + if (changed) + state->source_logical_hash_valid = 0; + clean_status_clear_authenticated_new_directories(dst); + state->authenticated_bootstrap_manifest = 0; + state->config_revalidated = 0; + state->initial_coherent = 0; + state->filter_scope_valid = 0; + state->config_mismatch = 1; + FREE_AND_NULL(state->config_revalidated_token); + dst->fsmonitor_extension_seen = 1; + dst->fsmonitor_token_valid = 1; + dst->fsmonitor_untracked_valid = 0; + dst->fsmonitor_untracked_revalidation_authenticated = 0; + dst->fsmonitor_untracked_extension_seen = 1; + dst->fsmonitor_untracked_extension_invalid = 0; + dst->fsmonitor_legacy_untracked_fallback = 0; + dst->fsmonitor_pending_token_from_provider = 0; + FREE_AND_NULL(dst->fsmonitor_last_update_pending); + ewah_free(dst->fsmonitor_dirty); + dst->fsmonitor_dirty = NULL; + FREE_AND_NULL(dst->fsmonitor_untracked_token); + dst->fsmonitor_untracked_token = xstrfmt("pending:%s", suffix); + dst->untracked->use_fsmonitor = 0; + dst->untracked->fsmonitor_revalidation = 1; + for (size_t i = 0; i < dst->cache_nr; i++) { + struct cache_entry *ce = dst->cache[i]; + + if (!oideq(&src->cache[i]->oid, &ce->oid)) + fsmonitor_invalidate_cache_entry(ce); + ce->ce_flags &= ~(CE_FSMONITOR_VALID | CE_UPTODATE); + } + /* Only the exhaustive membership proof cancels builder bookkeeping. */ + dst->cache_changed &= ~(CE_ENTRY_ADDED | CE_ENTRY_REMOVED); + dst->cache_changed |= FSMONITOR_CHANGED | UNTRACKED_CHANGED; + if (changed) + dst->cache_changed |= CE_ENTRY_CHANGED; + trace2_data_intmax("fsmonitor", dst->repo, + "history/backoff-transferred", 1); + return 1; +} + +static int commit_checkpoint_source_matches( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock) +{ + struct clean_status_config_digest digest; + struct repository *repo; + char *destination; + int matches; + + if (!checkpoint || !lock || checkpoint->lock != lock || + !is_lock_file_locked(lock)) + return 0; + repo = checkpoint->repo; + if (!fsm_settings__is_watch_limit_backoff(repo) || + get_alternate_index_output() || + !clean_status_index_path_is_main(repo, checkpoint->main_path) || + repo_has_replace_refs_uncached(repo) || + !clean_status_index_snapshot_still_matches_path( + &checkpoint->source, checkpoint->main_path, repo->hash_algo) || + !attr_source_snapshot_matches_repository(repo, checkpoint->attrs) || + clean_status_config_read_repository(repo, &digest) || + !digest.finalized || + memcmp(digest.hash, checkpoint->config_hash, repo->hash_algo->rawsz) || + memcmp(digest.semantic_hash, checkpoint->semantic_hash, + repo->hash_algo->rawsz) || + memcmp(digest.tracked_policy_hash, checkpoint->tracked_policy_hash, + repo->hash_algo->rawsz)) + return 0; + destination = get_locked_file_path(lock); + matches = !fspathcmp(destination, checkpoint->main_path); + free(destination); + return matches; +} + +void clean_status_release_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint) +{ + if (!checkpoint) + return; + clean_status_index_snapshot_release(&checkpoint->source); + clean_status_index_snapshot_release(&checkpoint->written); + if (checkpoint->writer_fd >= 0) + close(checkpoint->writer_fd); + attr_source_snapshot_free(checkpoint->attrs); + strbuf_release(&checkpoint->config); + strbuf_release(&checkpoint->untracked); + free(checkpoint->main_path); + free(checkpoint->token); + free(checkpoint); +} + +struct clean_status_commit_checkpoint *clean_status_capture_commit_checkpoint( + struct index_state *istate, struct lock_file *lock) +{ +#if defined(F_DUPFD_CLOEXEC) && defined(F_GETFL) && defined(O_ACCMODE) + struct clean_status_commit_checkpoint *checkpoint; + const struct clean_status_state *state; + const struct attr_fingerprint *attrs; + struct fsmonitor_clean_proof proof; + struct stat st; + const char *suffix, *pending; + int fd, flags; + const uint32_t historical = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + if (!istate || !istate->repo || !lock) + return NULL; + state = istate->clean_status; + if (!clean_status_fsmonitor_backoff_suspended(istate) || + !clean_status_fsmonitor_semantic_baseline_pending(istate) || + !is_lock_file_locked(lock) || get_alternate_index_output() || + !fstat_is_reliable() || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + istate != istate->repo->index || istate->resolve_undo || + istate->split_index || istate->sparse_index != INDEX_EXPANDED || + repo_config_values(istate->repo)->apply_sparse_checkout || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat || + (istate->cache_changed & (CE_ENTRY_ADDED | CE_ENTRY_REMOVED)) || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + istate->fsmonitor_legacy_untracked_fallback || + !istate->fsmonitor_untracked_token || + !skip_prefix(istate->fsmonitor_last_update, "builtin:", &suffix) || + !*suffix || !strcmp(suffix, "fake") || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || state->manifest.global_fallback || + state->manifest.current_flags != historical || + !istate->untracked || !istate->untracked->root || + !istate->untracked->root->valid || + !istate->untracked->fsmonitor_revalidation || + istate->untracked->fsmonitor_dirty_paths.len) + return NULL; + if (strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token) && + (!skip_prefix(istate->fsmonitor_untracked_token, "pending:", &pending) || + strcmp(suffix, pending))) + return NULL; + fd = get_lock_file_fd(lock); + flags = fd < 0 ? -1 : fcntl(fd, F_GETFL); + if (flags < 0 || (flags & O_ACCMODE) != O_RDWR || + fstat(fd, &st) || !S_ISREG(st.st_mode) || + st.st_nlink != 1 || !commit_checkpoint_owner_matches(&st)) + return NULL; + + CALLOC_ARRAY(checkpoint, 1); + checkpoint->repo = istate->repo; + checkpoint->lock = lock; + checkpoint->source.fd = -1; + checkpoint->written.fd = -1; + checkpoint->writer_fd = -1; + strbuf_init(&checkpoint->config, 0); + strbuf_init(&checkpoint->untracked, 0); + checkpoint->main_path = get_locked_file_path(lock); + checkpoint->token = xstrdup(istate->fsmonitor_last_update); + memcpy(checkpoint->config_hash, state->current_config_hash, + istate->repo->hash_algo->rawsz); + memcpy(checkpoint->semantic_hash, state->current_semantic_hash, + istate->repo->hash_algo->rawsz); + memcpy(checkpoint->tracked_policy_hash, state->current_tracked_policy_hash, + istate->repo->hash_algo->rawsz); + + /* The first write replaces istate->oid, so pin its canonical source now. */ + if (clean_status_index_snapshot_pin_proof_epoch(&checkpoint->source, istate) || + fstat(checkpoint->source.fd, &st) || + !commit_checkpoint_owner_matches(&st) || + attr_source_snapshot_repository(istate->repo, &checkpoint->attrs)) + goto fail; + attrs = attr_source_snapshot_fingerprint(checkpoint->attrs); + if (!attrs || + attrs->sources_present != state->current_attr_sources_present || + memcmp(attrs->content_hash, state->current_attr_hash, + istate->repo->hash_algo->rawsz) || + !commit_checkpoint_source_matches(checkpoint, lock)) + goto fail; + clean_status_write_fsmonitor_config(&checkpoint->config, istate); + if (fsmonitor_clean_proof_parse(&proof, checkpoint->config.buf, + checkpoint->config.len, istate->repo->hash_algo) || + proof.flags != historical || + proof.token_len != strlen(checkpoint->token) || + memcmp(proof.token, checkpoint->token, proof.token_len)) + goto fail; + write_untracked_extension(&checkpoint->untracked, istate->untracked); + checkpoint->writer_fd = fcntl(fd, F_DUPFD_CLOEXEC, 0); + if (checkpoint->writer_fd < 0) + goto fail; + return checkpoint; +fail: + clean_status_release_commit_checkpoint(checkpoint); + return NULL; +#else + (void)istate; + (void)lock; + return NULL; +#endif +} + +void clean_status_record_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint, + struct index_state *istate, struct lock_file *lock) +{ + struct index_state written = INDEX_STATE_INIT(istate->repo); + struct clean_status_identity identity; + struct stat st; + + if (!checkpoint || checkpoint->sealed || checkpoint->writer_fd < 0) + return; + /* Run before post-index-change can replace the close-only output. */ + if (checkpoint->repo != istate->repo || + !clean_status_fsmonitor_backoff_suspended(istate) || + !commit_checkpoint_source_matches(checkpoint, lock) || + fstat(checkpoint->writer_fd, &st) || + !commit_checkpoint_owner_matches(&st) || + clean_status_identity_from_stat(&identity, &st) || + clean_status_index_snapshot_open_allow_null_checksum( + &checkpoint->written, get_lock_file_path(lock), + istate->repo->hash_algo) || + !clean_status_identity_equal(&identity, &checkpoint->written.identity) || + checkpoint->written.version != istate->version || + checkpoint->written.cache_nr != istate->cache_nr || + !oideq(&checkpoint->written.checksum, &istate->oid) || + read_index_entries_from_fd(&written, checkpoint->writer_fd) || + clean_status_index_logical_digest(&written, checkpoint->logical_hash) || + !clean_status_index_snapshot_still_matches_path( + &checkpoint->written, get_lock_file_path(lock), + istate->repo->hash_algo)) + goto done; + checkpoint->written_dev = st.st_dev; + checkpoint->written_ino = st.st_ino; + checkpoint->sealed = 1; +done: + close(checkpoint->writer_fd); + checkpoint->writer_fd = -1; + if (!checkpoint->sealed) + clean_status_index_snapshot_release(&checkpoint->written); + release_index(&written); +} + +int clean_status_commit_checkpoint_changed( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock) +{ + return checkpoint && checkpoint->sealed && lock && + checkpoint->lock == lock && + is_lock_file_locked(lock) && + (checkpoint->needs_restore || + !clean_status_index_snapshot_still_matches_path( + &checkpoint->written, get_lock_file_path(lock), + checkpoint->repo->hash_algo)); +} + +int clean_status_commit_checkpoint_still_valid( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock) +{ + return checkpoint && checkpoint->sealed && + commit_checkpoint_source_matches(checkpoint, lock); +} + +static int attach_commit_checkpoint_history( + const struct clean_status_commit_checkpoint *checkpoint, + struct index_state *replacement) +{ + struct clean_status_state *state; + const char *suffix; + + replacement->untracked = read_untracked_extension( + checkpoint->untracked.buf, checkpoint->untracked.len); + if (!replacement->untracked || !replacement->untracked->root || + !replacement->untracked->root->valid || + !skip_prefix(checkpoint->token, "builtin:", &suffix) || !*suffix) + return 0; + clean_status_read_fsmonitor_config(replacement, checkpoint->config.buf, + checkpoint->config.len); + clean_status_attach_config(replacement); + state = replacement->clean_status; + if (!state || !state->disk_config_valid || state->disk_config_invalid || + !state->current_config_valid || !state->current_attr_valid) + return 0; + clean_status_manifest_adopt_disk(&state->manifest); + state->backoff_token = xstrdup(checkpoint->token); + state->backoff_suspended = 1; + state->semantic_baseline_pending = 1; + state->config_mismatch = 1; + state->filter_scope_valid = 0; + replacement->fsmonitor_extension_seen = 1; + replacement->fsmonitor_token_valid = 1; + replacement->fsmonitor_last_update = xstrdup(checkpoint->token); + replacement->fsmonitor_untracked_extension_seen = 1; + replacement->fsmonitor_untracked_token = xstrfmt("pending:%s", suffix); + replacement->untracked->fsmonitor_revalidation = 1; + replacement->untracked->use_fsmonitor = 0; + replacement->cache_changed |= FSMONITOR_CHANGED | UNTRACKED_CHANGED; + for (size_t i = 0; i < replacement->cache_nr; i++) + replacement->cache[i]->ce_flags &= + ~(CE_FSMONITOR_VALID | CE_UPTODATE); + return 1; +} + +static int read_commit_checkpoint_written( + const struct clean_status_commit_checkpoint *checkpoint, + struct index_state *written) +{ + struct stat before, after; + unsigned char hash[GIT_MAX_RAWSZ]; + + /* + * The child may have replaced the name, leaving this owned descriptor + * unlinked. Its original inode and sealed logical contents still bind + * the baseline; neither the old pathname nor unlink's ctime is authority. + */ + return checkpoint->written.fd >= 0 && + !fstat(checkpoint->written.fd, &before) && + S_ISREG(before.st_mode) && before.st_nlink <= 1 && + commit_checkpoint_owner_matches(&before) && + (uint64_t)before.st_dev == checkpoint->written_dev && + (uint64_t)before.st_ino == checkpoint->written_ino && + !read_index_entries_from_fd(written, checkpoint->written.fd) && + written->version == checkpoint->written.version && + written->cache_nr == checkpoint->written.cache_nr && + oideq(&written->oid, &checkpoint->written.checksum) && + !clean_status_index_logical_digest(written, hash) && + !memcmp(hash, checkpoint->logical_hash, + checkpoint->repo->hash_algo->rawsz) && + !fstat(checkpoint->written.fd, &after) && + path_namespace_stat_equal(&before, &after); +} + +int clean_status_advance_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint, + const struct index_state *current, struct lock_file *lock) +{ + struct index_state before = INDEX_STATE_INIT(NULL); + struct index_state after = INDEX_STATE_INIT(NULL); + struct clean_status_index_snapshot selected = { .fd = -1 }; + const struct clean_status_state *state; + const struct git_hash_algo *algo; + struct stat st; + unsigned char current_hash[GIT_MAX_RAWSZ]; + unsigned char selected_hash[GIT_MAX_RAWSZ]; + const unsigned int persistent_flags = + CE_STAGEMASK | CE_EXTENDED | CE_VALID | CE_EXTENDED_FLAGS; + const uint32_t historical = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + int advanced = 0; + + if (!current || !checkpoint || checkpoint->needs_restore || + !clean_status_commit_checkpoint_still_valid(checkpoint, lock) || + current->repo != checkpoint->repo || current != current->repo->index || + current->resolve_undo) + return 0; + algo = current->repo->hash_algo; + before.repo = after.repo = current->repo; + if (!read_commit_checkpoint_written(checkpoint, &before) || + clean_status_index_snapshot_open_allow_null_checksum( + &selected, get_lock_file_path(lock), algo) || + fstat(selected.fd, &st) || !commit_checkpoint_owner_matches(&st) || + read_index_entries_from_fd(&after, selected.fd) || + after.version != selected.version || + after.cache_nr != selected.cache_nr || + !oideq(&after.oid, &selected.checksum) || + clean_status_index_logical_digest(&after, selected_hash) || + clean_status_index_logical_digest(current, current_hash) || + memcmp(current_hash, selected_hash, algo->rawsz) || + before.cache_nr != after.cache_nr || + !attach_commit_checkpoint_history(checkpoint, &before)) + goto done; + + /* The old manifest is a historical path witness, never a current proof. */ + state = before.clean_status; + if (state->manifest.current_flags != historical || + !state->disk_semantic_valid || !state->disk_tracked_policy_valid || + !state->disk_attr_valid || + memcmp(state->disk_config_hash, checkpoint->config_hash, algo->rawsz) || + memcmp(state->disk_semantic_hash, checkpoint->semantic_hash, + algo->rawsz) || + memcmp(state->disk_tracked_policy_hash, + checkpoint->tracked_policy_hash, algo->rawsz) || + memcmp(state->disk_attr_hash, state->current_attr_hash, algo->rawsz)) + goto done; + for (size_t i = 0; i < before.cache_nr; i++) { + const struct cache_entry *old = before.cache[i]; + const struct cache_entry *new_entry = after.cache[i]; + + if (ce_namelen(old) != ce_namelen(new_entry) || + memcmp(old->name, new_entry->name, ce_namelen(old)) || + old->ce_mode != new_entry->ce_mode || + ((old->ce_flags ^ new_entry->ce_flags) & persistent_flags) || + (!oideq(&old->oid, &new_entry->oid) && + (!S_ISREG(old->ce_mode) || !S_ISREG(new_entry->ce_mode) || + !clean_status_index_entry_is_semantically_safe( + &before, old, new_entry)))) + goto done; + } + if (!clean_status_index_snapshot_still_matches_path( + &selected, get_lock_file_path(lock), algo) || + !commit_checkpoint_source_matches(checkpoint, lock)) + goto done; + + /* Subsequent hooks must leave these selected logical entries unchanged. */ + clean_status_index_snapshot_release(&checkpoint->written); + checkpoint->written = selected; + selected.fd = -1; + checkpoint->written_dev = st.st_dev; + checkpoint->written_ino = st.st_ino; + memcpy(checkpoint->logical_hash, selected_hash, algo->rawsz); + checkpoint->needs_restore = 1; + advanced = 1; + trace2_data_intmax("fsmonitor", current->repo, + "history/commit-backoff-advanced", 1); +done: + clean_status_index_snapshot_release(&selected); + release_index(&before); + release_index(&after); + return advanced; +} + +int clean_status_prepare_commit_checkpoint_restore( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock, const struct index_state *current, + struct index_state *replacement, int fd) +{ + struct stat st; + unsigned char hash[GIT_MAX_RAWSZ]; + + if (!current || !replacement || + !clean_status_commit_checkpoint_still_valid(checkpoint, lock) || + current->repo != checkpoint->repo || current != current->repo->index || + current->resolve_undo || + fstat(fd, &st) || !commit_checkpoint_owner_matches(&st) || + clean_status_index_logical_digest(current, hash) || + memcmp(hash, checkpoint->logical_hash, current->repo->hash_algo->rawsz) || + read_index_entries_from_fd(replacement, fd) || + clean_status_index_logical_digest(replacement, hash) || + memcmp(hash, checkpoint->logical_hash, current->repo->hash_algo->rawsz)) + return 0; + + return attach_commit_checkpoint_history(checkpoint, replacement); +} diff --git a/clean-status-identity.c b/clean-status-identity.c new file mode 100644 index 00000000000000..98d5a7e51b9a73 --- /dev/null +++ b/clean-status-identity.c @@ -0,0 +1,56 @@ +#include "git-compat-util.h" +#include "clean-status-identity.h" +#include "strbuf.h" + +int clean_status_identity_from_stat(struct clean_status_identity *identity, + const struct stat *st) +{ + memset(identity, 0, sizeof(*identity)); + if (!S_ISREG(st->st_mode) || st->st_nlink != 1) + return -1; + path_stat_identity_init(&identity->stat, st); + return 0; +} + +int clean_status_identity_is_durable(void) +{ +#ifdef __APPLE__ + return 1; +#else + return 0; +#endif +} + +int clean_status_identity_equal(const struct clean_status_identity *a, + const struct clean_status_identity *b) +{ + return path_stat_identity_equal(&a->stat, &b->stat); +} + +void clean_status_identity_write(struct strbuf *out, + const struct clean_status_identity *identity) +{ + uint64_t value; + size_t i; + + for (i = 0; i < ARRAY_SIZE(identity->stat.fields); i++) { + put_be64(&value, identity->stat.fields[i]); + strbuf_add(out, &value, sizeof(value)); + } +} + +int clean_status_identity_read(const unsigned char **p, + const unsigned char *end, + struct clean_status_identity *identity) +{ + size_t i; + + memset(identity, 0, sizeof(*identity)); + for (i = 0; i < ARRAY_SIZE(identity->stat.fields); i++) { + if ((size_t)(end - *p) < sizeof(uint64_t)) + return -1; + identity->stat.fields[i] = get_be64(*p); + *p += sizeof(uint64_t); + } + return 0; +} diff --git a/clean-status-identity.h b/clean-status-identity.h new file mode 100644 index 00000000000000..a22f8438f50874 --- /dev/null +++ b/clean-status-identity.h @@ -0,0 +1,27 @@ +#ifndef CLEAN_STATUS_IDENTITY_H +#define CLEAN_STATUS_IDENTITY_H + +#include "path-namespace.h" + +struct strbuf; +struct stat; + +struct clean_status_identity { + struct path_stat_identity stat; +}; + +#define CLEAN_STATUS_IDENTITY_SIZE \ + (PATH_STAT_IDENTITY_FIELDS * sizeof(uint64_t)) + +int clean_status_identity_from_stat(struct clean_status_identity *identity, + const struct stat *st); +int clean_status_identity_is_durable(void); +int clean_status_identity_equal(const struct clean_status_identity *a, + const struct clean_status_identity *b); +void clean_status_identity_write(struct strbuf *out, + const struct clean_status_identity *identity); +int clean_status_identity_read(const unsigned char **p, + const unsigned char *end, + struct clean_status_identity *identity); + +#endif /* CLEAN_STATUS_IDENTITY_H */ diff --git a/clean-status-index.c b/clean-status-index.c new file mode 100644 index 00000000000000..50cb6d870d8ecc --- /dev/null +++ b/clean-status-index.c @@ -0,0 +1,678 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "clean-status-sidecar.h" +#include "dir.h" +#include "environment.h" +#include "hash-framing.h" +#include "object.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "trace2.h" +#include "wrapper.h" + +#define LOGICAL_INDEX_PERSISTENT_FLAGS \ + (CE_STAGEMASK | CE_EXTENDED | CE_VALID | CE_EXTENDED_FLAGS) +#define LOGICAL_INDEX_BENIGN_FLAGS \ + (CE_UPTODATE | CE_HASHED | CE_FSMONITOR_VALID) + +static int index_path_matches_main( + const char *path, const char *canonical, + const struct clean_status_identity *main_identity) +{ + struct clean_status_identity identity; + struct stat st; + char *resolved; + int matches; + + if (!path || !*path || lstat(path, &st) || + clean_status_identity_from_stat(&identity, &st) || + !clean_status_identity_equal(&identity, main_identity)) + return 0; + resolved = real_pathdup(path, 0); + matches = resolved && !fspathcmp(resolved, canonical); + free(resolved); + return matches; +} + +int clean_status_index_path_is_main(struct repository *repo, const char *path) +{ + struct clean_status_identity main_identity; + struct stat st; + const char *selected = getenv(INDEX_ENVIRONMENT); + char *main_index, *canonical = NULL; + int matches = 0; + + if (!repo || !repo->initialized || !repo->gitdir || !*repo->gitdir || + !repo->index_file || !*repo->index_file || !path || !*path) + return 0; + /* repo_git_path("index") follows GIT_INDEX_FILE, including lockfiles. */ + main_index = xstrfmt("%s/index", repo_get_git_dir(repo)); + if (lstat(main_index, &st) || + clean_status_identity_from_stat(&main_identity, &st)) + goto done; + canonical = real_pathdup(main_index, 0); + if (!canonical || + !index_path_matches_main(repo->index_file, canonical, &main_identity) || + !index_path_matches_main(path, canonical, &main_identity) || + (selected && + !index_path_matches_main(selected, canonical, &main_identity)) || + !index_path_matches_main(main_index, canonical, &main_identity)) + goto done; + matches = 1; +done: + free(canonical); + free(main_index); + return matches; +} + +static int snapshot_read( + int fd, const struct stat *st, const struct git_hash_algo *algo, + uint32_t *version, uint32_t *cache_nr, struct object_id *checksum) +{ + unsigned char header[12]; + unsigned char trailer[GIT_MAX_RAWSZ]; + + if (st->st_size < 0 || + (uintmax_t)st->st_size < sizeof(header) + algo->rawsz || + (size_t)pread_in_full(fd, header, sizeof(header), 0) != + sizeof(header) || + memcmp(header, "DIRC", 4) || + (size_t)pread_in_full(fd, trailer, algo->rawsz, + st->st_size - (off_t)algo->rawsz) != + algo->rawsz) + return -1; + *version = get_be32(header + 4); + *cache_nr = get_be32(header + 8); + if (*version < 2 || *version > 4) + return -1; + oidread(checksum, trailer, algo); + return 0; +} + +static int snapshot_matches( + int fd, const struct stat *st, uint32_t expected_version, + uint32_t expected_cache_nr, const struct object_id *expected_checksum, + const struct git_hash_algo *algo) +{ + struct object_id checksum; + uint32_t version, cache_nr; + + return !snapshot_read(fd, st, algo, &version, &cache_nr, &checksum) && + version == expected_version && cache_nr == expected_cache_nr && + oideq(&checksum, expected_checksum); +} + +static int snapshot_open( + struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo, int allow_null_checksum) +{ + struct clean_status_identity named; + struct stat fd_st, named_st; + int fd, flags = O_RDONLY | O_CLOEXEC; + + memset(snapshot, 0, sizeof(*snapshot)); + snapshot->fd = -1; +#ifdef O_NONBLOCK + flags |= O_NONBLOCK; +#endif + fd = open_nofollow(path, flags); + if (fd < 0 || + fstat(fd, &fd_st) || + !S_ISREG(fd_st.st_mode) || + lstat(path, &named_st) || + clean_status_identity_from_stat(&snapshot->identity, &fd_st) || + clean_status_identity_from_stat(&named, &named_st) || + !clean_status_identity_equal(&snapshot->identity, &named) || + snapshot_read(fd, &fd_st, algo, &snapshot->version, + &snapshot->cache_nr, &snapshot->checksum) || + (!allow_null_checksum && is_null_oid(&snapshot->checksum))) + goto fail; + snapshot->fd = fd; + return 0; + +fail: + if (fd >= 0) + close(fd); + return -1; +} + +int clean_status_index_snapshot_open( + struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo) +{ + return snapshot_open(snapshot, path, algo, 0); +} + +int clean_status_index_snapshot_open_allow_null_checksum( + struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo) +{ + return snapshot_open(snapshot, path, algo, 1); +} + +int clean_status_index_snapshot_still_matches_path( + const struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo) +{ + struct clean_status_identity fd_identity, named_identity; + struct stat fd_st, named_st; + + return snapshot->fd >= 0 && + !fstat(snapshot->fd, &fd_st) && + !lstat(path, &named_st) && + !clean_status_identity_from_stat(&fd_identity, &fd_st) && + !clean_status_identity_from_stat(&named_identity, &named_st) && + clean_status_identity_equal(&fd_identity, &snapshot->identity) && + clean_status_identity_equal(&named_identity, + &snapshot->identity) && + snapshot_matches(snapshot->fd, &fd_st, snapshot->version, + snapshot->cache_nr, &snapshot->checksum, algo); +} + +static int source_index_matches_snapshot( + const struct clean_status_index_snapshot *snapshot, + const struct clean_status_state *state, + const struct git_hash_algo *algo) +{ + struct clean_status_identity identity; + struct stat st; + + return state && state->source_index_fd >= 0 && + state->source_index_identity_valid && + !fstat(state->source_index_fd, &st) && + !clean_status_identity_from_stat(&identity, &st) && + clean_status_identity_equal( + &identity, &state->source_index_identity) && + clean_status_identity_equal( + &snapshot->identity, &state->source_index_identity) && + snapshot_matches(state->source_index_fd, &st, + snapshot->version, snapshot->cache_nr, + &snapshot->checksum, algo); +} + +static int snapshot_matches_index_state( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate, int allow_process_local_source) +{ + const struct clean_status_state *state = istate->clean_status; + + if (istate->version != snapshot->version || + istate->cache_nr != snapshot->cache_nr || + !oideq(&istate->oid, &snapshot->checksum)) + return 0; + if (!is_null_oid(&snapshot->checksum)) + return 1; + if (clean_status_identity_is_durable() && state && + state->source_identity_valid && + clean_status_identity_equal(&snapshot->identity, + &state->source_identity)) + return 1; + return allow_process_local_source && + source_index_matches_snapshot( + snapshot, state, istate->repo->hash_algo); +} + +static int snapshot_pin( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate, int allow_process_local_source) +{ + if (snapshot_open(snapshot, istate->repo->index_file, + istate->repo->hash_algo, 1)) + return -1; + if (snapshot_matches_index_state( + snapshot, istate, allow_process_local_source)) + return 0; + clean_status_index_snapshot_release(snapshot); + return -1; +} + +int clean_status_index_snapshot_pin( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate) +{ + return snapshot_pin(snapshot, istate, 0); +} + +int clean_status_index_snapshot_pin_proof_epoch( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate) +{ + /* + * A proof epoch is process-local and dies with its index state. It may + * therefore use the descriptor for the file which populated that state. + * Persisted history and sidecars continue to use the generic pin above. + */ + return snapshot_pin(snapshot, istate, 1); +} + +static int snapshot_still_matches( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate, int allow_process_local_source) +{ + return snapshot_matches_index_state( + snapshot, istate, allow_process_local_source) && + clean_status_index_snapshot_still_matches_path( + snapshot, istate->repo->index_file, + istate->repo->hash_algo); +} + +int clean_status_index_snapshot_still_matches( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate) +{ + return snapshot_still_matches(snapshot, istate, 0); +} + +int clean_status_index_snapshot_still_matches_proof_epoch( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate) +{ + return snapshot_still_matches(snapshot, istate, 1); +} + +void clean_status_index_snapshot_release( + struct clean_status_index_snapshot *snapshot) +{ + if (snapshot->fd >= 0) + close(snapshot->fd); + snapshot->fd = -1; +} + +static int write_receipt_owner_matches(const struct stat *st) +{ +#ifdef __APPLE__ + return st->st_uid == geteuid(); +#else + (void)st; + return 0; +#endif +} + +static int write_receipt_is_eligible(const struct index_state *istate) +{ + const struct clean_status_state *state; + + if (!istate || !istate->initialized || !istate->repo || + !istate->repo->initialized) + return 0; + state = istate->clean_status; + return clean_status_identity_is_durable() && fstat_is_reliable() && + state && state->config_enforced && state->current_config_valid && + state->source_identity_valid && + clean_status_external_history_enabled(istate) && + !getenv(INDEX_ENVIRONMENT) && istate == istate->repo->index && + !istate->split_index && istate->sparse_index == INDEX_EXPANDED && + !repo_config_values(istate->repo)->apply_sparse_checkout && + is_null_oid(&istate->oid); +} + +int clean_status_index_prepare_write_receipt( + struct index_state *istate, int lock_fd, + struct clean_status_index_write_receipt *receipt) +{ +#if defined(__APPLE__) && defined(F_DUPFD_CLOEXEC) && \ + defined(F_GETFL) && defined(O_ACCMODE) + struct clean_status_identity initial, held; + struct stat before, after; + int owned, flags; + + if (!receipt || receipt->snapshot.fd >= 0 || receipt->istate || + receipt->recorded || !write_receipt_is_eligible(istate) || + lock_fd < 0) + return -1; + flags = fcntl(lock_fd, F_GETFL); + if (flags < 0 || (flags & O_ACCMODE) != O_RDWR || + fstat(lock_fd, &before) || !write_receipt_owner_matches(&before) || + clean_status_identity_from_stat(&initial, &before)) + return -1; + /* The writer closes its descriptor before committing the lockfile. */ + owned = fcntl(lock_fd, F_DUPFD_CLOEXEC, 0); + if (owned < 0) + return -1; + if (fstat(owned, &after) || + clean_status_identity_from_stat(&held, &after) || + !clean_status_identity_equal(&initial, &held)) { + close(owned); + return -1; + } + receipt->snapshot.fd = owned; + receipt->source_identity = istate->clean_status->source_identity; + receipt->istate = istate; + return 0; +#else + (void)istate; + (void)lock_fd; + (void)receipt; + return -1; +#endif +} + +void clean_status_index_record_write_receipt( + struct index_state *istate, + struct clean_status_index_write_receipt *receipt) +{ + struct clean_status_index_snapshot snapshot; + struct stat st; + + if (!receipt || receipt->snapshot.fd < 0) + return; + if (receipt->recorded || receipt->istate != istate || + !write_receipt_is_eligible(istate) || + !clean_status_identity_equal( + &receipt->source_identity, + &istate->clean_status->source_identity)) + goto fail; + + /* Capture ctime after our rename, but before any hook can change it. */ + snapshot = receipt->snapshot; + if (fstat(snapshot.fd, &st) || !write_receipt_owner_matches(&st) || + clean_status_identity_from_stat(&snapshot.identity, &st) || + snapshot_read(snapshot.fd, &st, istate->repo->hash_algo, + &snapshot.version, &snapshot.cache_nr, + &snapshot.checksum) || + snapshot.version != istate->version || + snapshot.cache_nr != istate->cache_nr || + !oideq(&snapshot.checksum, &istate->oid) || + !clean_status_index_snapshot_still_matches_path( + &snapshot, istate->repo->index_file, + istate->repo->hash_algo)) + goto fail; + receipt->snapshot = snapshot; + receipt->recorded = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "history/own-write-source-recorded", 1); + return; + +fail: + clean_status_index_write_receipt_release(receipt); +} + +int clean_status_index_adopt_write_receipt( + struct index_state *istate, + struct clean_status_index_write_receipt *receipt) +{ + struct clean_status_state *state; + struct stat st; + int adopted = 0; + + if (!receipt || !receipt->recorded || receipt->istate != istate || + !write_receipt_is_eligible(istate)) + goto done; + state = istate->clean_status; + if (!clean_status_identity_equal(&receipt->source_identity, + &state->source_identity) || + receipt->snapshot.version != istate->version || + receipt->snapshot.cache_nr != istate->cache_nr || + !oideq(&receipt->snapshot.checksum, &istate->oid) || + fstat(receipt->snapshot.fd, &st) || + !write_receipt_owner_matches(&st) || + !clean_status_index_snapshot_still_matches_path( + &receipt->snapshot, istate->repo->index_file, + istate->repo->hash_algo)) + goto done; + + /* The original descriptor and logical digest still name the old source. */ + state->source_identity = receipt->snapshot.identity; + trace2_data_intmax("fsmonitor", istate->repo, + "history/own-write-source-adopted", 1); + adopted = 1; + +done: + clean_status_index_write_receipt_release(receipt); + return adopted; +} + +void clean_status_index_write_receipt_release( + struct clean_status_index_write_receipt *receipt) +{ + if (!receipt) + return; + clean_status_index_snapshot_release(&receipt->snapshot); + memset(receipt, 0, sizeof(*receipt)); + receipt->snapshot.fd = -1; +} + +int clean_status_index_entries_are_certifiable( + const struct index_state *istate) +{ + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (S_ISGITLINK(ce->ce_mode) || ce_stage(ce) || + ce_intent_to_add(ce) || ce_skip_worktree(ce) || + (ce->ce_flags & CE_VALID) || + !(ce->ce_flags & CE_FSMONITOR_VALID) || + (ce->ce_flags & ~(CE_UPTODATE | CE_HASHED | + CE_FSMONITOR_VALID | + CE_UPDATE_IN_BASE))) + return 0; + } + return 1; +} + +int clean_status_index_is_certifiable(const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + int checksum_is_bound = + !is_null_oid(&istate->oid) || + (clean_status_identity_is_durable() && state && + state->source_identity_valid); + + return checksum_is_bound && + clean_status_index_entries_are_certifiable(istate); +} + +int clean_status_index_is_certifiable_with_hardlinks( + const struct index_state *istate, uint32_t *hardlink_nr) +{ + const struct clean_status_state *state = istate->clean_status; + uint32_t nr = 0; + int checksum_is_bound = + !is_null_oid(&istate->oid) || + (clean_status_identity_is_durable() && state && + state->source_identity_valid); + + if (!hardlink_nr || !checksum_is_bound) + return 0; + *hardlink_nr = 0; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (S_ISGITLINK(ce->ce_mode) || ce_stage(ce) || + ce_intent_to_add(ce) || ce_skip_worktree(ce) || + (ce->ce_flags & CE_VALID) || + (ce->ce_flags & ~(CE_UPTODATE | CE_HASHED | + CE_FSMONITOR_VALID | + CE_UPDATE_IN_BASE))) + return 0; + if (ce->ce_flags & CE_FSMONITOR_VALID) + continue; + if (!S_ISREG(ce->ce_mode) || + !(ce->ce_flags & CE_UPTODATE) || + nr == CLEAN_STATUS_HARDLINK_WITNESS_MAX) + return 0; + nr++; + } + if (nr && (!istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat)) + return 0; + *hardlink_nr = nr; + return 1; +} + +static int index_entry_logical_state_is_supported( + const struct cache_entry *ce, unsigned int extra_benign_flags) +{ + return !(ce->ce_flags & ~(LOGICAL_INDEX_PERSISTENT_FLAGS | + LOGICAL_INDEX_BENIGN_FLAGS | + extra_benign_flags)); +} + +static int index_logical_state_is_supported( + const struct index_state *istate, unsigned int extra_benign_flags) +{ + if (!istate->repo || !istate->repo->hash_algo || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + istate->cache_nr > UINT32_MAX) + return 0; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (!index_entry_logical_state_is_supported( + ce, extra_benign_flags)) + return 0; + } + return 1; +} + +int clean_status_index_can_reuse_source_logical_hash( + const struct index_state *istate) +{ + const unsigned int acceleration_changes = + FSMONITOR_CHANGED | UNTRACKED_CHANGED; + + /* + * Reading or refreshing acceleration extensions may mark only FSMN/UNTR + * state dirty. Reject any cache-entry change, while the flag walk + * preserves every reject condition which the logical digest enforced + * before a physical alias could skip it. Sparse-checkout post-processing + * may clear CE_SKIP_WORKTREE without setting cache_changed, so leave that + * mode on the digest path. + */ + return istate->repo && istate->repo->initialized && + !repo_config_values(istate->repo)->apply_sparse_checkout && + !(istate->cache_changed & ~acceleration_changes) && + index_logical_state_is_supported(istate, 0); +} + +static int index_logical_digest(const struct index_state *istate, + unsigned int extra_benign_flags, + unsigned char *out) +{ + static const char domain[] = "git-clean-status-logical-index-v1"; + struct git_hash_ctx ctx; + uint32_t value; + int initialized = 0, ret = -1; + + if (!istate->repo || !istate->repo->hash_algo || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + istate->cache_nr > UINT32_MAX) + return -1; + trace2_region_enter("fsmonitor", "history_logical_digest", + istate->repo); + git_hash_init(&ctx, istate->repo->hash_algo); + initialized = 1; + hash_length_delimited(&ctx, domain, sizeof(domain) - 1); + put_be32(&value, istate->cache_nr); + hash_length_delimited(&ctx, &value, sizeof(value)); + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + /* + * Every in-memory flag not explicitly known to be an + * acceleration hint may describe work which must be completed + * before the entry is safe to externalize. In particular, + * CE_CONTENT_CHECK_REQUIRED must not disappear with the process + * which raised it. + */ + if (!index_entry_logical_state_is_supported( + ce, extra_benign_flags)) + goto done; + put_be32(&value, ce->ce_mode); + hash_length_delimited(&ctx, &value, sizeof(value)); + put_be32(&value, + ce->ce_flags & LOGICAL_INDEX_PERSISTENT_FLAGS); + hash_length_delimited(&ctx, &value, sizeof(value)); + hash_length_delimited(&ctx, ce->oid.hash, + istate->repo->hash_algo->rawsz); + hash_length_delimited(&ctx, ce->name, ce_namelen(ce)); + } + git_hash_final(out, &ctx); + initialized = 0; + ret = 0; + +done: + if (initialized) + git_hash_discard(&ctx); + trace2_region_leave("fsmonitor", "history_logical_digest", + istate->repo); + return ret; +} + +int clean_status_index_logical_digest(const struct index_state *istate, + unsigned char *out) +{ + return index_logical_digest(istate, 0, out); +} + +int clean_status_index_logical_digest_after_status( + const struct index_state *istate, unsigned char *out) +{ + const unsigned int acceleration_changes = + CE_ENTRY_CHANGED | FSMONITOR_CHANGED | UNTRACKED_CHANGED; + + /* + * CE_UPDATE_IN_BASE has no independent meaning for a full index; status + * uses it as stat-refresh bookkeeping. Keep that exception confined + * to the main, expanded, acceleration-only status result. The common + * digest still rejects split/sparse indexes and every other transient + * flag, and hashes every persistent logical field. + */ + if (!istate->repo || + !clean_status_external_history_enabled(istate) || + istate != istate->repo->index || + (istate->cache_changed & ~acceleration_changes)) + return -1; + return index_logical_digest(istate, CE_UPDATE_IN_BASE, out); +} + +void clean_status_record_source_identity(struct index_state *istate, + const struct stat *st) +{ + struct clean_status_state *state = istate->clean_status; + + if (!state || state->source_identity_valid || + !clean_status_identity_is_durable()) + return; + if (!clean_status_identity_from_stat(&state->source_identity, st)) + state->source_identity_valid = 1; +} + +int clean_status_retain_source_index_fd(struct index_state *istate, int fd, + const struct stat *st) +{ + struct clean_status_state *state = istate->clean_status; + struct clean_status_identity identity, current_identity; + struct stat current; + + if (!fstat_is_reliable() || fd < 0 || !state || + !state->config_enforced || istate->split_index || + !is_null_oid(&istate->oid) || + state->source_index_fd >= 0 || + clean_status_identity_from_stat(&identity, st) || + fstat(fd, ¤t) || + clean_status_identity_from_stat(¤t_identity, ¤t) || + !clean_status_identity_equal(&identity, ¤t_identity)) + return 0; + state->source_index_fd = fd; + state->source_index_identity = identity; + state->source_index_identity_valid = 1; + /* Ownership transfers only after every fail-closed check succeeds. */ + return 1; +} + +int clean_status_verify_null_index(const struct index_state *istate, + const struct stat *st) +{ + const struct clean_status_state *state = istate->clean_status; + struct clean_status_identity identity; + + if (!state || !clean_status_identity_is_durable()) + return 1; + return state->source_identity_valid && + !clean_status_identity_from_stat(&identity, st) && + clean_status_identity_equal(&identity, &state->source_identity); +} diff --git a/clean-status-index.h b/clean-status-index.h new file mode 100644 index 00000000000000..ddc177cef5436e --- /dev/null +++ b/clean-status-index.h @@ -0,0 +1,94 @@ +#ifndef CLEAN_STATUS_INDEX_H +#define CLEAN_STATUS_INDEX_H + +#include "clean-status-identity.h" +#include "hash.h" + +struct index_state; +struct repository; + +/* + * Check the physical worktree-specific main index, including any selected + * GIT_INDEX_FILE. Accept normalized names, but not leaf symlinks, hardlinks, + * private indexes, or lockfiles. This grants no clean-proof authority. + */ +int clean_status_index_path_is_main(struct repository *repo, const char *path); + +struct clean_status_index_snapshot { + struct clean_status_identity identity; + uint32_t version; + uint32_t cache_nr; + struct object_id checksum; + int fd; +}; + +/* + * An opt-in receipt for a canonical index write. Only the index writer may + * record it, after committing its lockfile and before running hooks. The + * caller must initialize and release it, even if no write was performed. + */ +struct clean_status_index_write_receipt { + struct clean_status_index_snapshot snapshot; + struct clean_status_identity source_identity; + const struct index_state *istate; + unsigned int recorded : 1; +}; + +#define CLEAN_STATUS_INDEX_WRITE_RECEIPT_INIT \ + { .snapshot = { .fd = -1 } } + +/* Writer-only lifecycle: prepare duplicates lock_fd; record fails closed. */ +int clean_status_index_prepare_write_receipt( + struct index_state *istate, int lock_fd, + struct clean_status_index_write_receipt *receipt); +void clean_status_index_record_write_receipt( + struct index_state *istate, + struct clean_status_index_write_receipt *receipt); + +/* Consumes the receipt and returns whether the written source was adopted. */ +int clean_status_index_adopt_write_receipt( + struct index_state *istate, + struct clean_status_index_write_receipt *receipt); +void clean_status_index_write_receipt_release( + struct clean_status_index_write_receipt *receipt); + +int clean_status_index_snapshot_open( + struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo); +/* + * Callers which accept a null trailer must separately require a durable + * source identity before trusting the snapshot. + */ +int clean_status_index_snapshot_open_allow_null_checksum( + struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo); +int clean_status_index_snapshot_still_matches_path( + const struct clean_status_index_snapshot *snapshot, const char *path, + const struct git_hash_algo *algo); +int clean_status_index_snapshot_pin( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate); +int clean_status_index_snapshot_pin_proof_epoch( + struct clean_status_index_snapshot *snapshot, + struct index_state *istate); +int clean_status_index_snapshot_still_matches( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate); +int clean_status_index_snapshot_still_matches_proof_epoch( + const struct clean_status_index_snapshot *snapshot, + const struct index_state *istate); +void clean_status_index_snapshot_release( + struct clean_status_index_snapshot *snapshot); +int clean_status_index_entries_are_certifiable( + const struct index_state *istate); +int clean_status_index_is_certifiable(const struct index_state *istate); +int clean_status_index_is_certifiable_with_hardlinks( + const struct index_state *istate, uint32_t *hardlink_nr); +int clean_status_index_logical_digest(const struct index_state *istate, + unsigned char *out); +int clean_status_index_logical_digest_after_status( + const struct index_state *istate, unsigned char *out); +int clean_status_index_can_reuse_source_logical_hash( + const struct index_state *istate); + +#endif /* CLEAN_STATUS_INDEX_H */ diff --git a/clean-status-internal.h b/clean-status-internal.h new file mode 100644 index 00000000000000..aeab1730b84ea3 --- /dev/null +++ b/clean-status-internal.h @@ -0,0 +1,64 @@ +#ifndef CLEAN_STATUS_INTERNAL_H +#define CLEAN_STATUS_INTERNAL_H + +#include "clean-status-identity.h" +#include "clean-status-manifest.h" + +struct index_state; + +struct clean_status_state { + struct clean_status_identity source_identity; + struct clean_status_identity source_index_identity; + struct clean_status_manifest_state manifest; + struct strbuf disk_config_raw; + struct strbuf authenticated_new_directories; + char *disk_config_token; + char *config_revalidated_token; + char *backoff_token; + char *authenticated_new_directories_token; + int source_index_fd; + unsigned char current_config_hash[GIT_MAX_RAWSZ]; + unsigned char disk_config_hash[GIT_MAX_RAWSZ]; + unsigned char current_semantic_hash[GIT_MAX_RAWSZ]; + unsigned char disk_semantic_hash[GIT_MAX_RAWSZ]; + unsigned char current_tracked_policy_hash[GIT_MAX_RAWSZ]; + unsigned char disk_tracked_policy_hash[GIT_MAX_RAWSZ]; + unsigned char current_attr_hash[GIT_MAX_RAWSZ]; + unsigned char current_attr_namespace_hash[GIT_MAX_RAWSZ]; + unsigned char current_attr_portable_namespace_hash[GIT_MAX_RAWSZ]; + unsigned char disk_attr_hash[GIT_MAX_RAWSZ]; + unsigned char source_logical_hash[GIT_MAX_RAWSZ]; + unsigned current_config_valid : 1; + unsigned current_semantic_valid : 1; + unsigned current_tracked_policy_valid : 1; + unsigned current_attr_valid : 1; + unsigned current_semantic_explicit : 1; + unsigned current_attr_sources_present : 1; + unsigned config_enforced : 1; + unsigned filter_configured : 1; + unsigned filter_scope_valid : 1; + unsigned config_mismatch : 1; + unsigned strong_mismatch : 1; + unsigned config_revalidated : 1; + unsigned initial_coherent : 1; + unsigned source_identity_valid : 1; + unsigned source_index_identity_valid : 1; + unsigned source_logical_hash_valid : 1; + unsigned external_history_restored : 1; + unsigned external_history_owned_index : 1; + unsigned external_history_preserve_witness : 1; + unsigned recovered_tracked_stat : 1; + unsigned authenticated_bootstrap_manifest : 1; + unsigned disk_config_valid : 1; + unsigned disk_semantic_valid : 1; + unsigned disk_tracked_policy_valid : 1; + unsigned disk_attr_valid : 1; + unsigned disk_config_seen : 1; + unsigned disk_config_invalid : 1; + unsigned semantic_baseline_pending : 1; + unsigned backoff_suspended : 1; +}; + +struct clean_status_state *clean_status_get_state(struct index_state *istate); + +#endif /* CLEAN_STATUS_INTERNAL_H */ diff --git a/clean-status-manifest.c b/clean-status-manifest.c new file mode 100644 index 00000000000000..8c7e79e0b481ed --- /dev/null +++ b/clean-status-manifest.c @@ -0,0 +1,1207 @@ +#include "git-compat-util.h" +#include "attr-fingerprint.h" +#include "attr.h" +#include "attr-manifest.h" +#include "bloom.h" +#include "clean-status.h" +#include "clean-status-config.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "clean-status-manifest.h" +#include "commit.h" +#include "commit-graph.h" +#include "dir.h" +#include "environment.h" +#include "fsmonitor.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-ll.h" +#include "hash-framing.h" +#include "object.h" +#include "object-name.h" +#include "odb.h" +#include "path-namespace.h" +#include "read-cache-ll.h" +#include "replace-object.h" +#include "repository.h" +#include "semantic-verify.h" +#include "semantic-verify-internal.h" +#include "sparse-index.h" +#include "string-list.h" +#include "trace2.h" +#include "tree.h" +#include "tree-walk.h" +#include "worktree-attr-manifest.h" +#include "worktree-attr-source.h" +#include "wrapper.h" + +struct invalidate_manifest_data { + struct index_state *istate; + const struct strbuf *baseline; + const struct strbuf *current; + int invalidated; +}; + +/* + * Only the second provider query after a closed semantic proof may reuse its + * manifest. A directory delta must authenticate every affected attribute + * source, hash, and absence; its tracked and untracked entries remain dirty. + * Directory timestamps never establish tracked-file content. + */ +static struct { + struct index_state *index; + const struct semantic_verify_proof *proof; + unsigned reused : 1; +} manifest_directory_delta; + +static int build_manifest(struct index_state *istate, + struct strbuf *manifest, + unsigned char *manifest_hash, + struct worktree_attr_manifest_stats *stats) +{ + struct clean_status_index_snapshot snapshot; + struct index_state scratch = INDEX_STATE_INIT(istate->repo); + int ret = -1; + + if (istate->sparse_index == INDEX_EXPANDED) + return worktree_attr_manifest_build( + istate, manifest, manifest_hash, stats); + if (clean_status_index_snapshot_pin(&snapshot, istate)) + return -1; + scratch.fsmonitor_has_run_once = 1; + if (read_index_from(&scratch, istate->repo->index_file, + istate->repo->gitdir) < 0 || + !clean_status_index_snapshot_still_matches(&snapshot, &scratch)) + goto done; + ensure_full_index(&scratch); + ret = worktree_attr_manifest_build( + &scratch, manifest, manifest_hash, stats); + if (ret || + !clean_status_index_snapshot_still_matches(&snapshot, istate)) { + strbuf_reset(manifest); + ret = -1; + } + +done: + release_index(&scratch); + clean_status_index_snapshot_release(&snapshot); + return ret; +} + +void clean_status_manifest_init(struct clean_status_manifest_state *state) +{ + memset(state, 0, sizeof(*state)); + strbuf_init(&state->disk, 0); + strbuf_init(&state->current, 0); +} + +void clean_status_manifest_release(struct clean_status_manifest_state *state) +{ + strbuf_release(&state->disk); + strbuf_release(&state->current); +} + +int clean_status_manifest_load(struct clean_status_manifest_state *state, + const void *data, size_t len, uint32_t flags, + const struct git_hash_algo *algo) +{ + state->disk_valid = 0; + state->disk_flags = 0; + strbuf_reset(&state->disk); + if (flags & ~FSMONITOR_CLEAN_PROOF_ALL || + !attr_manifest_valid(data, len, algo)) + return -1; + strbuf_add(&state->disk, data, len); + hash_buffer_digest(algo, data, len, state->disk_hash); + state->disk_flags = flags; + state->disk_valid = 1; + return 0; +} + +void clean_status_manifest_adopt_disk( + struct clean_status_manifest_state *state) +{ + if (!state->disk_valid) + BUG("cannot adopt an invalid clean-status manifest"); + strbuf_reset(&state->current); + strbuf_addbuf(&state->current, &state->disk); + memcpy(state->current_hash, state->disk_hash, + sizeof(state->current_hash)); + state->current_flags = state->disk_flags; + state->current_valid = 1; + state->checked = 1; + state->current_invalidated = 0; +} + +static int find_manifest_entry( + const struct strbuf *manifest, const char *path, + const struct git_hash_algo *algo, struct attr_manifest_entry *found) +{ + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + size_t path_len = strlen(path); + int ret; + + if (attr_manifest_cursor_init(&cursor, manifest->buf, + manifest->len, algo)) + return -1; + while ((ret = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + if (entry.path_len == path_len && + !memcmp(entry.path, path, path_len)) { + *found = entry; + return 0; + } + } + return -1; +} + +void clean_status_manifest_begin_directory_delta( + struct index_state *istate, const struct semantic_verify_proof *proof) +{ + if (manifest_directory_delta.index) + BUG("nested clean-status directory delta"); + if (!proof || !semantic_verify_proof_is_current(istate, proof)) + return; + manifest_directory_delta.index = istate; + manifest_directory_delta.proof = proof; + manifest_directory_delta.reused = 0; +} + +int clean_status_manifest_end_directory_delta(struct index_state *istate) +{ + int reused; + + if (manifest_directory_delta.index != istate) + return 0; + reused = manifest_directory_delta.reused; + manifest_directory_delta.index = NULL; + manifest_directory_delta.proof = NULL; + manifest_directory_delta.reused = 0; + return reused; +} + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +static int directory_manifest_entry_path_compare( + const struct attr_manifest_entry *entry, const char *name) +{ + size_t len = strlen(name); + size_t common = entry->path_len < len ? entry->path_len : len; + int cmp = memcmp(entry->path, name, common); + + if (cmp) + return cmp; + return entry->path_len < len ? -1 : entry->path_len > len; +} + +static int directory_attribute_source_matches( + struct index_state *istate, struct semantic_verify_path *path, + const char *name, const struct attr_manifest_entry *entry, + size_t position) +{ + const struct cache_entry *indexed; + const struct git_hash_algo *algo = istate->repo->hash_algo; + const char *basename; + unsigned char observed[GIT_MAX_RAWSZ]; + struct stat st; + int parent_fd, found, pos; + + if (semantic_verify_resolve_parent(path, name, position, + &parent_fd, &basename)) + return 0; + if (fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW)) { + if (errno != ENOENT) + return 0; + } else if (!S_ISREG(st.st_mode) || st.st_nlink != 1) { + return 0; + } + if (worktree_attr_source_read(path, name, position, algo, + observed, &found)) + return 0; + if (found) + return entry && entry->source == ATTR_MANIFEST_WORKTREE && + !memcmp(entry->hash, observed, algo->rawsz); + if (!fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW) || + errno != ENOENT) + return 0; + pos = index_name_pos(istate, name, strlen(name)); + if (pos < 0) + return !entry; + indexed = istate->cache[pos]; + if (!S_ISREG(indexed->ce_mode) || ce_stage(indexed) || + ce_skip_worktree(indexed) || ce_intent_to_add(indexed) || + (indexed->ce_flags & CE_VALID)) + return 0; + return entry && entry->source == ATTR_MANIFEST_INDEX && + !memcmp(entry->hash, indexed->oid.hash, algo->rawsz); +} +#endif + +int clean_status_manifest_path_attributes_unchanged( + const struct index_state *istate, const char *name) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + const struct clean_status_state *state = + istate ? istate->clean_status : NULL; + const struct git_hash_algo *algo; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct strbuf candidate = STRBUF_INIT; + const char *slash = name; + unsigned int namespace_unstable = 0; + size_t position = 0; + uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + int safe = 0; + + if (!istate || !istate->repo || !name || !*name || + !verify_path(name, 0) || + !clean_status_fsmonitor_backoff_suspended(istate) || + istate->split_index || istate->sparse_index != INDEX_EXPANDED || + istate->cache_nr > INT_MAX || !state || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || + state->manifest.global_fallback || + (state->manifest.current_flags & required) != required) + return 0; + algo = istate->repo->hash_algo; + if (!algo || !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo) || + semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + + /* The root source applies even to a path without any slash. */ + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + for (;;) { + struct attr_manifest_entry entry; + const struct attr_manifest_entry *historical = NULL; + int pos; + + if (candidate.len > INT_MAX) + goto done; + if (!find_manifest_entry(&state->manifest.current, + candidate.buf, algo, &entry)) + historical = &entry; + /* + * Attribute fallback can read stage #2 of an unmerged path. + * The shared matcher accepts only stage #0, so do not mistake + * an unmerged source for historical absence. The expanded-index + * guard above makes this lookup non-mutating. + */ + pos = index_name_pos((struct index_state *)istate, + candidate.buf, candidate.len); + if (pos < 0) { + unsigned int first = -(pos + 1); + + if (first < istate->cache_nr && + !strcmp(istate->cache[first]->name, candidate.buf)) + goto done; + } + if (!directory_attribute_source_matches( + (struct index_state *)istate, path, candidate.buf, + historical, position++)) + goto done; + slash = strchr(slash, '/'); + if (!slash) + break; + strbuf_reset(&candidate); + strbuf_add(&candidate, name, slash - name + 1); + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + slash++; + } + + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + safe = !namespace_unstable && semantic_verify_root_stable(root); + +done: + if (path) + semantic_verify_path_free(path, NULL, NULL); + semantic_verify_root_clear(root); + strbuf_release(&candidate); + return safe; +#else + (void)istate; + (void)name; + return 0; +#endif +} + +int clean_status_manifest_directory_unchanged( + struct index_state *istate, const char *directory) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct clean_status_index_snapshot snapshot; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_cursor manifest_cursor; + struct attr_manifest_entry manifest_entry; + struct string_list candidates = STRING_LIST_INIT_DUP; + struct strbuf candidate = STRBUF_INIT; + const struct git_hash_algo *algo = istate->repo->hash_algo; + const char *previous = NULL; + unsigned int first, namespace_unstable = 0; + size_t len, previous_len = 0; + int pos, manifest_ret, pinned = 0, safe = 0; + uint32_t required = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + + if (manifest_directory_delta.index != istate || + !manifest_directory_delta.proof || + !fsmonitor_pending_token_from_provider(istate) || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + !fstat_is_reliable() || getenv(INDEX_ENVIRONMENT) || + istate != istate->repo->index || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + repo_config_values(istate->repo)->apply_sparse_checkout || !state || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_tracked_policy_valid || !state->current_attr_valid || + !state->config_enforced || + (state->filter_configured && !state->filter_scope_valid) || + !state->manifest.scan_count || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + state->manifest.global_fallback || + (state->manifest.current_flags & required) != required || + !semantic_verify_proof_is_current( + istate, manifest_directory_delta.proof) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || + config.filter_configured != state->filter_configured || + config.semantic_config_explicit != state->current_semantic_explicit || + memcmp(config.hash, state->current_config_hash, algo->rawsz) || + memcmp(config.semantic_hash, state->current_semantic_hash, + algo->rawsz) || + memcmp(config.tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz) || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present != state->current_attr_sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz) || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + + len = strlen(directory); + if (!len || directory[len - 1] != '/') + goto done; + pos = index_name_pos(istate, directory, len); + if (pos >= 0) + goto done; + first = -pos - 1; + if (first >= istate->cache_nr || + !starts_with(istate->cache[first]->name, directory)) + goto done; + if (clean_status_index_snapshot_pin_proof_epoch(&snapshot, istate)) + goto done; + pinned = 1; + if (semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + + strbuf_addstr(&candidate, directory); + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + string_list_append(&candidates, candidate.buf); + for (unsigned int i = first; i < istate->cache_nr && + starts_with(istate->cache[i]->name, directory); i++) { + const struct cache_entry *ce = istate->cache[i]; + const char *slash = ce->name + len; + + if (ce_stage(ce) || ce_skip_worktree(ce) || + ce_intent_to_add(ce) || (ce->ce_flags & CE_VALID) || + S_ISSPARSEDIR(ce->ce_mode)) + goto done; + while ((slash = strchr(slash, '/')) != NULL) { + size_t parent_len = slash - ce->name; + + if (!previous || previous_len <= parent_len || + previous[parent_len] != '/' || + memcmp(previous, ce->name, parent_len)) { + strbuf_reset(&candidate); + strbuf_add(&candidate, ce->name, parent_len + 1); + strbuf_addstr(&candidate, GITATTRIBUTES_FILE); + string_list_append(&candidates, candidate.buf); + } + slash++; + } + previous = ce->name; + previous_len = ce_namelen(ce); + } + string_list_sort(&candidates); + string_list_remove_duplicates(&candidates, 0); + if (attr_manifest_cursor_init(&manifest_cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + manifest_ret = attr_manifest_cursor_next(&manifest_cursor, + &manifest_entry); + for (size_t i = 0; i < candidates.nr; i++) { + const char *name = candidates.items[i].string; + const struct attr_manifest_entry *entry = NULL; + + while (manifest_ret > 0 && + directory_manifest_entry_path_compare( + &manifest_entry, name) < 0) + manifest_ret = attr_manifest_cursor_next( + &manifest_cursor, &manifest_entry); + if (manifest_ret < 0) + goto done; + if (manifest_ret > 0 && + !directory_manifest_entry_path_compare( + &manifest_entry, name)) + entry = &manifest_entry; + if (!directory_attribute_source_matches( + istate, path, name, entry, + first + i)) + goto done; + } + + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + if (namespace_unstable || !semantic_verify_root_stable(root) || + !clean_status_index_snapshot_still_matches_proof_epoch( + &snapshot, istate) || + !semantic_verify_proof_is_current( + istate, manifest_directory_delta.proof) || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present != state->current_attr_sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz)) + goto done; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-directory-reused", 1); + manifest_directory_delta.reused = 1; + safe = 1; + +done: + if (path) + semantic_verify_path_free(path, NULL, NULL); + semantic_verify_root_clear(root); + if (pinned) + clean_status_index_snapshot_release(&snapshot); + string_list_clear(&candidates, 0); + strbuf_release(&candidate); + return safe; +#else + (void)istate; + (void)directory; + return 0; +#endif +} + +int clean_status_manifest_reconcile_deleted_attribute( + struct index_state *istate, const char *name) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct attr_fingerprint attrs; + struct attr_manifest_cursor cursor; + struct attr_manifest_writer writer; + struct attr_manifest_entry old, entry; + struct strbuf next = STRBUF_INIT; + const struct cache_entry *ce; + const char *base, *basename; + unsigned char hash[GIT_MAX_RAWSZ]; + unsigned char indexed_hash[GIT_MAX_RAWSZ]; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + unsigned char observed_hash[GIT_MAX_RAWSZ]; + unsigned int namespace_unstable = 0; + enum object_type type; + struct stat st; + void *content = NULL; + size_t size; + int pos, parent_fd, found = 0, next_entry, indexed, safe = 0; + int worktree_found, observed_found, changed; + + if (!name) + goto done; + base = find_last_dir_sep(name); + base = base ? base + 1 : name; + if (fspathcmp(base, GITATTRIBUTES_FILE) || + !state || !state->config_revalidated || + !state->current_attr_valid || state->filter_configured || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update)) + goto done; + if (repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + name, algo, &old) || + (old.source != ATTR_MANIFEST_WORKTREE && + old.source != ATTR_MANIFEST_INDEX) || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present != state->current_attr_sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, name, strlen(name)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + indexed = old.source == ATTR_MANIFEST_INDEX; + if (indexed && memcmp(old.hash, ce->oid.hash, algo->rawsz)) + goto done; + content = odb_read_object(istate->repo->objects, + &ce->oid, &type, &size); + if (!content || type != OBJ_BLOB || size >= ATTR_MAX_FILE_SIZE) + goto done; + hash_buffer_digest(algo, content, size, indexed_hash); + if (!indexed && memcmp(old.hash, indexed_hash, algo->rawsz)) + goto done; + if (semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path || worktree_attr_source_read( + path, name, pos, algo, worktree_hash, &worktree_found) || + semantic_verify_resolve_parent( + path, name, pos, &parent_fd, &basename) || + (!worktree_found && + (!fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW) || + errno != ENOENT)) || + (worktree_found && + memcmp(worktree_hash, indexed_hash, algo->rawsz)) || + !semantic_verify_root_stable(root) || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo) || + attr_manifest_cursor_init(&cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + attr_manifest_writer_init(&writer, &next, algo); + while ((next_entry = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + char *entry_name = xmemdupz(entry.path, entry.path_len); + int matches = !strcmp(entry_name, name); + int invalid = attr_manifest_writer_add( + &writer, entry_name, + matches ? + (worktree_found ? ATTR_MANIFEST_WORKTREE : + ATTR_MANIFEST_INDEX) : + entry.source, + matches ? + (worktree_found ? worktree_hash : ce->oid.hash) : + entry.hash); + + free(entry_name); + if (invalid) + goto done; + found += matches; + } + if (next_entry < 0 || found != 1 || + worktree_attr_source_read( + path, name, pos, algo, observed_hash, &observed_found) || + observed_found != worktree_found || + (!observed_found && + (!fstatat(parent_fd, basename, &st, AT_SYMLINK_NOFOLLOW) || + errno != ENOENT)) || + (observed_found && + memcmp(observed_hash, worktree_hash, algo->rawsz))) + goto done; + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + if (namespace_unstable || !semantic_verify_root_stable(root)) + goto done; + changed = indexed == worktree_found; + if (changed) { + hash_buffer_digest(algo, next.buf, next.len, hash); + strbuf_swap(&state->manifest.current, &next); + memcpy(state->manifest.current_hash, hash, algo->rawsz); + state->manifest.changed = 1; + state->manifest.global_fallback = 0; + } + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-reconciled", 1); + safe = 1; + +done: + if (path) + semantic_verify_path_free(path, NULL, NULL); + semantic_verify_root_clear(root); + strbuf_release(&next); + free(content); + return safe; +#else + (void)istate; + (void)name; + return 0; +#endif +} + +static int read_root_worktree_attributes( + struct repository *repo, struct strbuf *out) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct semantic_verify_root *root = NULL; + struct stat before, after, named; + size_t size; + int fd = -1, ret = -1; + char extra; + + if (semantic_verify_root_init(repo, &root)) + goto done; + fd = semantic_verify_openat(root->fd, GITATTRIBUTES_FILE, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_nlink != 1 || before.st_dev != root->stat.st_dev || + before.st_size < 0 || before.st_size >= ATTR_MAX_FILE_SIZE) + goto done; + size = xsize_t(before.st_size); + strbuf_grow(out, size); + strbuf_setlen(out, size); + if ((size_t)read_in_full(fd, out->buf, size) != size || + read(fd, &extra, 1) != 0 || fstat(fd, &after) || + fstatat(root->fd, GITATTRIBUTES_FILE, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&before, &after) || + !path_namespace_stat_equal(&after, &named) || + !semantic_verify_root_stable(root)) + goto done; + ret = 0; + +done: + if (fd >= 0) + close(fd); + semantic_verify_root_clear(root); + if (ret) + strbuf_reset(out); + return ret; +#else + (void)repo; + (void)out; + return -1; +#endif +} + +static int find_previous_root_attributes( + struct repository *repo, struct object_id *oid) +{ + struct bloom_filter_settings *settings; + struct bloom_key key = { 0 }; + struct object_id head_oid; + struct commit *commit; + unsigned int visited = 0, bloom_hits = 0, tree_inspections = 0, limit; + int found = 0; + + if (repo_get_oid(repo, "HEAD", &head_oid) || + !(commit = lookup_commit_reference_gently( + repo, &head_oid, 1))) + return -1; + settings = get_bloom_filter_settings(repo); + limit = settings ? 8192 : 128; + if (settings) + bloom_key_fill(&key, GITATTRIBUTES_FILE, + strlen(GITATTRIBUTES_FILE), settings); + while (commit && visited < limit) { + struct bloom_filter *filter = NULL; + struct commit *parent; + struct tree *current_tree, *parent_tree; + struct object_id current_oid, parent_oid; + unsigned short current_mode, parent_mode; + + visited++; + if (repo_parse_commit_gently(repo, commit, 1) || + !commit->parents) + break; + parent = commit->parents->item; + if (settings) + filter = get_bloom_filter(repo, commit); + if (filter && filter->version >= 0 && + (uint32_t)filter->version == settings->hash_version && + bloom_filter_contains(filter, &key, settings) == 0) { + bloom_hits++; + commit = parent; + continue; + } + if (tree_inspections >= 512) + break; + tree_inspections++; + if (repo_parse_commit_gently(repo, parent, 1) || + !(current_tree = repo_get_commit_tree(repo, commit)) || + !(parent_tree = repo_get_commit_tree(repo, parent)) || + get_tree_entry(repo, ¤t_tree->object.oid, + GITATTRIBUTES_FILE, + ¤t_oid, ¤t_mode) || + get_tree_entry(repo, &parent_tree->object.oid, + GITATTRIBUTES_FILE, + &parent_oid, &parent_mode) || + !S_ISREG(current_mode) || !S_ISREG(parent_mode)) + break; + if (!oideq(¤t_oid, &parent_oid)) { + oidcpy(oid, &parent_oid); + found = 1; + break; + } + commit = parent; + } + if (settings) + bloom_key_clear(&key); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-commits", visited); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-bloom-skips", bloom_hits); + trace2_data_intmax("fsmonitor", repo, + "semantic/attribute-history-tree-inspections", + tree_inspections); + return found ? 0 : -1; +} + +static void *read_authenticated_attribute_blob( + struct index_state *istate, + const struct attr_manifest_entry *old, + const struct object_id *oid, size_t *size) +{ + const struct git_hash_algo *algo = istate->repo->hash_algo; + unsigned char hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *content; + + if (old->source == ATTR_MANIFEST_INDEX && + memcmp(old->hash, oid->hash, algo->rawsz)) + return NULL; + content = odb_read_object(istate->repo->objects, + oid, &type, size); + if (!content || type != OBJ_BLOB || + *size >= ATTR_MAX_FILE_SIZE) { + free(content); + return NULL; + } + if (old->source == ATTR_MANIFEST_WORKTREE) { + hash_buffer_digest(algo, content, *size, hash); + if (memcmp(old->hash, hash, algo->rawsz)) { + free(content); + return NULL; + } + } else if (old->source != ATTR_MANIFEST_INDEX) { + free(content); + return NULL; + } + return content; +} + +static void *read_authenticated_old_attributes( + struct index_state *istate, + const struct attr_manifest_entry *old, + const struct cache_entry *current, size_t *size) +{ + struct object_id parent_oid, historical_oid; + const struct object_id *candidates[2]; + void *content; + size_t nr = 1; + + candidates[0] = ¤t->oid; + if (!repo_get_oid_blob(istate->repo, + "HEAD^:" GITATTRIBUTES_FILE, &parent_oid) && + !oideq(¤t->oid, &parent_oid)) + candidates[nr++] = &parent_oid; + for (size_t i = 0; i < nr; i++) { + content = read_authenticated_attribute_blob( + istate, old, candidates[i], size); + if (content) + return content; + } + if (find_previous_root_attributes(istate->repo, &historical_oid)) + return NULL; + return read_authenticated_attribute_blob( + istate, old, &historical_oid, size); +} + +int clean_status_manifest_reconcile_display_only_attribute( + struct index_state *istate, const char *path) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_cursor cursor; + struct attr_manifest_writer writer; + struct attr_manifest_entry old, entry; + struct strbuf worktree = STRBUF_INIT; + struct strbuf observed = STRBUF_INIT; + struct strbuf next = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + unsigned char indexed_hash[GIT_MAX_RAWSZ]; + unsigned char manifest_hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *previous = NULL, *indexed = NULL; + size_t previous_len, indexed_len; + int pos, found = 0, next_entry, safe = 0; + + if (!path || strcmp(path, GITATTRIBUTES_FILE) || !state || + !state->config_enforced || !state->config_revalidated || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_attr_valid || state->current_attr_sources_present || + state->filter_configured || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update) || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + path, algo, &old) || + old.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured || + memcmp(config.hash, state->current_config_hash, algo->rawsz) || + memcmp(config.semantic_hash, + state->current_semantic_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + previous = read_authenticated_old_attributes( + istate, &old, ce, &previous_len); + if (!previous || + read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + indexed = odb_read_object(istate->repo->objects, + &ce->oid, &type, &indexed_len); + if (!indexed || type != OBJ_BLOB || + indexed_len >= ATTR_MAX_FILE_SIZE) + goto done; + hash_buffer_digest(algo, indexed, indexed_len, indexed_hash); + hash_buffer_digest(algo, worktree.buf, worktree.len, worktree_hash); + if ((memcmp(indexed_hash, worktree_hash, algo->rawsz) && + (indexed_len != previous_len || + memcmp(indexed, previous, indexed_len))) || + !attr_manifest_only_linguist_generated_changed( + previous, previous_len, worktree.buf, worktree.len) || + !attr_manifest_valid(state->manifest.current.buf, + state->manifest.current.len, algo) || + attr_manifest_cursor_init(&cursor, + state->manifest.current.buf, + state->manifest.current.len, algo)) + goto done; + attr_manifest_writer_init(&writer, &next, algo); + while ((next_entry = attr_manifest_cursor_next(&cursor, &entry)) > 0) { + char *entry_path = xmemdupz(entry.path, entry.path_len); + int matches = !strcmp(entry_path, path); + int invalid = attr_manifest_writer_add( + &writer, entry_path, + matches ? ATTR_MANIFEST_WORKTREE : entry.source, + matches ? worktree_hash : entry.hash); + + free(entry_path); + if (invalid) + goto done; + found += matches; + } + if (next_entry < 0 || found != 1 || + read_root_worktree_attributes(istate->repo, &observed) || + observed.len != worktree.len || + memcmp(observed.buf, worktree.buf, worktree.len)) + goto done; + hash_buffer_digest(algo, next.buf, next.len, manifest_hash); + strbuf_swap(&state->manifest.current, &next); + memcpy(state->manifest.current_hash, + manifest_hash, algo->rawsz); + state->manifest.changed = 1; + state->manifest.global_fallback = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/nonconversion-attributes", 1); + safe = 1; + +done: + free(previous); + free(indexed); + strbuf_release(&worktree); + strbuf_release(&observed); + strbuf_release(&next); + return safe; +#else + (void)istate; + (void)path; + return 0; +#endif +} + +int clean_status_manifest_accept_current_display_only_attribute( + struct index_state *istate, const char *path) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_entry current; + struct strbuf worktree = STRBUF_INIT; + struct strbuf observed = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char worktree_hash[GIT_MAX_RAWSZ]; + enum object_type type; + void *indexed = NULL; + size_t indexed_len; + int pos, safe = 0; + + if (!path || strcmp(path, GITATTRIBUTES_FILE) || !state || + !state->config_enforced || !state->config_revalidated || + !state->current_config_valid || !state->current_semantic_valid || + !state->current_attr_valid || state->current_attr_sources_present || + state->filter_configured || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->manifest.disk_valid || !state->manifest.current_valid || + !state->manifest.checked || state->manifest.current_invalidated || + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !state->config_revalidated_token || + strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update) || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(&state->manifest.current, + path, algo, ¤t) || + current.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + memcmp(attrs.content_hash, state->current_attr_hash, + algo->rawsz) || + memcmp(attrs.namespace_hash, + state->current_attr_namespace_hash, algo->rawsz) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured || + memcmp(config.hash, state->current_config_hash, algo->rawsz) || + memcmp(config.semantic_hash, + state->current_semantic_hash, algo->rawsz)) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID) || + read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + hash_buffer_digest(algo, worktree.buf, worktree.len, worktree_hash); + if (memcmp(current.hash, worktree_hash, algo->rawsz)) + goto done; + indexed = odb_read_object(istate->repo->objects, + &ce->oid, &type, &indexed_len); + if (!indexed || type != OBJ_BLOB || + indexed_len >= ATTR_MAX_FILE_SIZE || + !attr_manifest_only_linguist_generated_changed( + indexed, indexed_len, worktree.buf, worktree.len) || + read_root_worktree_attributes(istate->repo, &observed) || + observed.len != worktree.len || + memcmp(observed.buf, worktree.buf, worktree.len)) + goto done; + safe = 1; + +done: + free(indexed); + strbuf_release(&worktree); + strbuf_release(&observed); + return safe; +#else + (void)istate; + (void)path; + return 0; +#endif +} + +static int root_attributes_only_affect_display( + const struct invalidate_manifest_data *data, const char *path, + int *index_pos) +{ + struct index_state *istate = data->istate; + struct clean_status_state *state = istate->clean_status; + const struct git_hash_algo *algo = istate->repo->hash_algo; + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_entry old, current; + struct strbuf worktree = STRBUF_INIT; + const struct cache_entry *ce; + unsigned char hash[GIT_MAX_RAWSZ]; + void *staged = NULL; + size_t staged_len; + int pos, safe = 0; + + if (strcmp(path, GITATTRIBUTES_FILE) || !data->baseline || + !data->current || !state || + (state->current_attr_valid && state->current_attr_sources_present) || + state->filter_configured || + repo_has_replace_refs_uncached(istate->repo) || + find_manifest_entry(data->baseline, path, algo, &old) || + find_manifest_entry(data->current, path, algo, ¤t) || + current.source != ATTR_MANIFEST_WORKTREE || + attr_fingerprint_repository(istate->repo, &attrs) || + attrs.sources_present || + (state->current_attr_valid && + memcmp(attrs.content_hash, state->current_attr_hash, algo->rawsz)) || + clean_status_config_read_repository(istate->repo, &config) || + !config.finalized || config.filter_configured) + goto done; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + goto done; + ce = istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || ce_stage(ce) || + ce_skip_worktree(ce) || ce_intent_to_add(ce) || + (ce->ce_flags & CE_VALID)) + goto done; + staged = read_authenticated_old_attributes( + istate, &old, ce, &staged_len); + if (!staged) + goto done; + if (read_root_worktree_attributes(istate->repo, &worktree)) + goto done; + hash_buffer_digest(algo, worktree.buf, worktree.len, hash); + if (memcmp(current.hash, hash, algo->rawsz) || + !attr_manifest_only_linguist_generated_changed( + staged, staged_len, worktree.buf, worktree.len)) + goto done; + *index_pos = pos; + safe = 1; + +done: + free(staged); + strbuf_release(&worktree); + return safe; +} + +static int invalidate_manifest_path(const struct attr_manifest_entry *entry, + void *cb_data) +{ + struct invalidate_manifest_data *data = cb_data; + char *path = xmemdupz(entry->path, entry->path_len); + int pos; + + untracked_cache_invalidate_trimmed_path(data->istate, path, 0); + if (root_attributes_only_affect_display(data, path, &pos)) { + git_attr_invalidate_all(); + fsmonitor_invalidate_cache_entry(data->istate->cache[pos]); + data->istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", data->istate->repo, + "semantic/nonconversion-attributes", 1); + } else { + data->invalidated += + fsmonitor_invalidate_attributes_path(data->istate, path); + } + free(path); + return 0; +} + +int clean_status_manifest_refresh(struct index_state *istate, + struct clean_status_manifest_state *state) +{ + struct worktree_attr_manifest_stats stats; + struct invalidate_manifest_data invalidation = { .istate = istate }; + const struct git_hash_algo *algo = istate->repo->hash_algo; + const struct strbuf *baseline = NULL; + struct strbuf next = STRBUF_INIT; + unsigned char next_hash[GIT_MAX_RAWSZ]; + + state->scan_count++; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-scan-count", state->scan_count); + if (attr_manifest_valid(state->current.buf, state->current.len, algo)) + baseline = &state->current; + else if (state->disk_valid) + baseline = &state->disk; + state->checked = 1; + state->changed = 0; + state->global_fallback = 0; + state->current_valid = 0; + state->current_flags = 0; + if (build_manifest(istate, &next, next_hash, &stats)) { + state->global_fallback = !!baseline; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-scan-failed", 1); + strbuf_release(&next); + return -1; + } + if (baseline) { + invalidation.baseline = baseline; + invalidation.current = &next; + if (attr_manifest_for_each_changed( + baseline->buf, baseline->len, + next.buf, next.len, algo, + invalidate_manifest_path, &invalidation)) { + state->global_fallback = 1; + strbuf_release(&next); + return -1; + } + state->changed = baseline->len != next.len || + memcmp(baseline->buf, next.buf, next.len); + } + strbuf_swap(&state->current, &next); + strbuf_release(&next); + memcpy(state->current_hash, next_hash, algo->rawsz); + state->current_valid = 1; + state->current_flags = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + state->current_invalidated = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-candidates", stats.candidates); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-threads", stats.threads); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-thread-failures", + stats.thread_failures); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-worktree-sources", + stats.worktree_sources); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-index-sources", stats.index_sources); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-bytes", state->current.len); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-changed", state->changed); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/manifest-invalidated", + invalidation.invalidated); + return invalidation.invalidated; +} + +void clean_status_manifest_invalidate( + struct clean_status_manifest_state *state) +{ + if (state->current_valid) + state->current_invalidated = 1; + state->current_valid = 0; + state->current_flags = 0; +} diff --git a/clean-status-manifest.h b/clean-status-manifest.h new file mode 100644 index 00000000000000..8bc4f1ac28ad28 --- /dev/null +++ b/clean-status-manifest.h @@ -0,0 +1,52 @@ +#ifndef CLEAN_STATUS_MANIFEST_H +#define CLEAN_STATUS_MANIFEST_H + +#include "hash.h" +#include "strbuf.h" + +struct index_state; +struct semantic_verify_proof; + +struct clean_status_manifest_state { + struct strbuf disk; + struct strbuf current; + unsigned char disk_hash[GIT_MAX_RAWSZ]; + unsigned char current_hash[GIT_MAX_RAWSZ]; + uint32_t disk_flags; + uint32_t current_flags; + uint32_t scan_count; + unsigned disk_valid : 1; + unsigned current_valid : 1; + unsigned checked : 1; + unsigned changed : 1; + unsigned global_fallback : 1; + unsigned current_invalidated : 1; +}; + +void clean_status_manifest_init(struct clean_status_manifest_state *state); +void clean_status_manifest_release(struct clean_status_manifest_state *state); +int clean_status_manifest_load(struct clean_status_manifest_state *state, + const void *data, size_t len, uint32_t flags, + const struct git_hash_algo *algo); +void clean_status_manifest_adopt_disk( + struct clean_status_manifest_state *state); +int clean_status_manifest_refresh(struct index_state *istate, + struct clean_status_manifest_state *state); +void clean_status_manifest_begin_directory_delta( + struct index_state *istate, const struct semantic_verify_proof *proof); +int clean_status_manifest_end_directory_delta(struct index_state *istate); +/* Recheck one path's attribute ancestry for suspended backoff history. */ +int clean_status_manifest_path_attributes_unchanged( + const struct index_state *istate, const char *path); +int clean_status_manifest_directory_unchanged( + struct index_state *istate, const char *directory); +int clean_status_manifest_reconcile_deleted_attribute( + struct index_state *istate, const char *path); +int clean_status_manifest_reconcile_display_only_attribute( + struct index_state *istate, const char *path); +int clean_status_manifest_accept_current_display_only_attribute( + struct index_state *istate, const char *path); +void clean_status_manifest_invalidate( + struct clean_status_manifest_state *state); + +#endif /* CLEAN_STATUS_MANIFEST_H */ diff --git a/clean-status-sidecar-issue.c b/clean-status-sidecar-issue.c new file mode 100644 index 00000000000000..91cd4bc9960d25 --- /dev/null +++ b/clean-status-sidecar-issue.c @@ -0,0 +1,307 @@ +#include "git-compat-util.h" +#include "cache-tree.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "clean-status-sidecar.h" +#include "environment.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-ll.h" +#include "fsmonitor-settings.h" +#include "lockfile.h" +#include "object-file.h" +#include "object-name.h" +#include "path-namespace.h" +#include "preload-index.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify-internal.h" +#include "trace2.h" +#include "wt-status.h" + +static void trace_miss(struct repository *repo, const char *reason) +{ + trace2_data_string("status", repo, "clean-proof/miss", reason); +} + +static int issue_test_barrier(void) +{ + const char *ready = + getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_READY"); + const char *resume = + getenv("GIT_TEST_STATUS_CLEAN_SIDECAR_ISSUE_BARRIER_RESUME"); + struct strbuf buf = STRBUF_INIT; + int ret; + + if (!ready && !resume) + return 0; + if (!ready || !resume) + return -1; + write_file(ready, "ready"); + ret = strbuf_read_file(&buf, resume, 1) > 0 ? 0 : -1; + strbuf_release(&buf); + return ret; +} + +static int output_is_certifiable(const struct wt_status *status, + int normal_clean_query) +{ + return (status->status_format == STATUS_FORMAT_PORCELAIN_V2 || + (normal_clean_query && + status->status_format == STATUS_FORMAT_NONE)) && + !status->pathspec.nr && !status->show_branch && + (!status->show_stash || normal_clean_query) && + !status->show_ignored_mode && + !status->null_termination && !status->verbose && + status->show_untracked_files == SHOW_NORMAL_UNTRACKED_FILES && + !status->change.nr && !status->untracked.nr && + !status->ignored.nr; +} + +static int history_is_certifiable( + const struct index_state *istate, + const struct clean_status_config_digest *config) +{ + const struct clean_status_state *state = istate->clean_status; + + /* + * The configured-filter proof domain requires an authenticated, + * fully classified inactive scope. A normalized disabled-filter + * override shares that digest and is never certifiable. + */ + return state && + state->filter_configured == config->filter_configured && + (!config->filter_configured || + (!config->normalized_filter_disable && + state->current_config_valid && + state->current_semantic_valid && + !memcmp(state->current_config_hash, config->hash, + istate->repo->hash_algo->rawsz) && + !memcmp(state->current_semantic_hash, config->semantic_hash, + istate->repo->hash_algo->rawsz) && + state->filter_scope_valid && + !clean_status_filter_scope_needs_validation(istate))) && + clean_status_has_persistent_fsmonitor_semantic_history(istate) && + clean_status_revalidated_token_matches(istate) && + state->manifest.current_valid && + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; +} + +static int fsmonitor_state_is_certifiable( + struct repository *repo, const struct index_state *istate, + uint32_t *hardlink_nr) +{ + return !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + fsm_settings__get_mode(repo) == FSMONITOR_MODE_IPC && + !fsmonitor_has_pending_token(istate) && + istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + strlen(istate->fsmonitor_last_update) <= + FSMONITOR_CLEAN_PROOF_TOKEN_MAX && + clean_status_index_is_certifiable_with_hardlinks( + istate, hardlink_nr); +} + +static int capture_hardlink_witnesses( + struct repository *repo, const struct index_state *istate, + uint32_t expected, struct strbuf *witnesses) +{ +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && !defined(NO_NSEC) + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + unsigned int namespace_unstable = 0; + uint32_t captured = 0, verified = 0; + int ret = -1; + + if (!expected) + return 0; + if (!repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + semantic_verify_root_init(repo, &root)) + goto done; + path = semantic_verify_path_new(root); + if (!path) + goto done; + for (size_t i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + struct path_stat_identity identity; + struct stat held, named; + const char *basename; + int parent_fd, fd; + + if (ce->ce_flags & CE_FSMONITOR_VALID) + continue; + if (semantic_verify_resolve_parent( + path, ce->name, i, &parent_fd, &basename)) + goto done; + fd = semantic_verify_openat( + parent_fd, basename, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0) + goto done; + if (fstat(fd, &held) || !S_ISREG(held.st_mode) || + held.st_nlink <= 1 || held.st_dev != root->stat.st_dev || + match_stat_data(&ce->ce_stat_data, &held)) { + close(fd); + goto done; + } + if (ce->ce_stat_data.sd_ctime.nsec != ST_CTIME_NSEC(held) || + ce->ce_stat_data.sd_mtime.nsec != ST_MTIME_NSEC(held)) { + struct object_id observed; + struct stat after; + + if (index_fd(repo->index, &observed, xdup(fd), &held, + OBJ_BLOB, ce->name, 0) || + !oideq(&observed, &ce->oid) || fstat(fd, &after) || + !path_namespace_stat_equal(&held, &after)) { + close(fd); + goto done; + } + verified++; + } + if (fstatat(parent_fd, basename, &named, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&held, &named)) { + close(fd); + goto done; + } + path_stat_identity_init(&identity, &held); + close(fd); + if (clean_status_sidecar_append_hardlink( + witnesses, ce->name, &identity)) + goto done; + captured++; + } + if (captured != expected || !semantic_verify_root_stable(root)) + goto done; + if (verified) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-content-verified", verified); + ret = 0; + +done: + semantic_verify_path_free(path, &namespace_unstable, NULL); + if (namespace_unstable || (root && !semantic_verify_root_stable(root))) + ret = -1; + semantic_verify_root_clear(root); + if (ret) + strbuf_reset(witnesses); + return ret; +#else + (void)repo; + (void)istate; + (void)witnesses; + return expected ? -1 : 0; +#endif +} + +static int untracked_scan_is_certifiable( + struct wt_status *status, struct object_id *exclude_digest, + struct stat *scanned_worktree) +{ + if (status->untracked_from_preload) + return !preload_index_bulk_standard_excludes_digest( + status->repo->index, exclude_digest, + scanned_worktree); + if (status->untracked_from_token_closure) + return !wt_status_certified_excludes_digest( + status, exclude_digest, scanned_worktree); + return 0; +} + +int clean_status_issue_sidecar( + struct wt_status *status, + const struct clean_status_config_digest *config, + struct lock_file *index_lock, + int normal_clean_query) +{ + struct repository *repo = status->repo; + struct index_state *istate = repo->index; + struct clean_status_index_snapshot index = { .fd = -1 }; + struct clean_status_sidecar sidecar = { 0 }; + struct strbuf hardlinks = STRBUF_INIT; + struct object_id exclude_digest, head_tree; + struct stat scanned_worktree; + unsigned char repo_hash[GIT_MAX_RAWSZ]; + uint32_t hardlink_nr = 0; + int installed = 0; + + if (!is_lock_file_locked(index_lock) || + !config->finalized || + !output_is_certifiable(status, normal_clean_query)) { + trace_miss(repo, "issue-command-or-output"); + goto done; + } + if (!history_is_certifiable(istate, config)) { + trace_miss(repo, "issue-coherent-history"); + goto done; + } + if (getenv(INDEX_ENVIRONMENT) || + !fsmonitor_state_is_certifiable(repo, istate, &hardlink_nr) || + !untracked_scan_is_certifiable( + status, &exclude_digest, &scanned_worktree)) { + trace_miss(repo, "issue-scan-or-index-shape"); + goto done; + } + if (issue_test_barrier()) { + trace_miss(repo, "issue-test-barrier"); + goto done; + } + if (!status->attr_source_snapshot || + clean_status_index_snapshot_pin(&index, istate) || + clean_status_repository_fingerprint( + repo, status->attr_source_snapshot, &index, + &scanned_worktree, repo_hash)) { + trace_miss(repo, "issue-pinned-inputs"); + goto done; + } + if (repo_get_oid_tree(repo, "HEAD^{tree}", &head_tree) || + ((!istate->cache_tree || istate->cache_tree->entry_count < 0) ? + !status->index_tree_verified : + !oideq(&head_tree, &istate->cache_tree->oid))) { + trace_miss(repo, "issue-head-cache-tree"); + goto done; + } + + sidecar.identity = index.identity; + sidecar.proof.index_version = index.version; + sidecar.proof.cache_nr = index.cache_nr; + oidcpy(&sidecar.proof.index_checksum, &index.checksum); + oidcpy(&sidecar.proof.head_tree, &head_tree); + memcpy(sidecar.proof.config_hash, config->hash, + repo->hash_algo->rawsz); + memcpy(sidecar.proof.repo_hash, repo_hash, + repo->hash_algo->rawsz); + oidcpy(&sidecar.proof.exclude_source_digest, &exclude_digest); + sidecar.token = (const unsigned char *)istate->fsmonitor_last_update; + sidecar.token_len = strlen(istate->fsmonitor_last_update); + if (capture_hardlink_witnesses( + repo, istate, hardlink_nr, &hardlinks)) { + trace_miss(repo, "issue-hardlink-witness"); + goto done; + } + if (hardlink_nr) { + sidecar.hardlinks = (const unsigned char *)hardlinks.buf; + sidecar.hardlinks_len = hardlinks.len; + sidecar.hardlink_nr = hardlink_nr; + } + + if (clean_status_sidecar_install( + repo->index_file, &sidecar, &index, repo->hash_algo)) { + trace_miss(repo, "issue-sidecar-write"); + goto done; + } + rollback_lock_file(index_lock); + if (hardlink_nr) + trace2_data_intmax("status", repo, + "clean-proof/hardlink-witnesses", hardlink_nr); + trace2_data_intmax("status", repo, "clean-proof/sidecar", 1); + installed = 1; + +done: + strbuf_release(&hardlinks); + clean_status_index_snapshot_release(&index); + return installed; +} diff --git a/clean-status-sidecar.c b/clean-status-sidecar.c new file mode 100644 index 00000000000000..b40959bcd301ee --- /dev/null +++ b/clean-status-sidecar.c @@ -0,0 +1,533 @@ +#include "git-compat-util.h" + +#ifdef __APPLE__ +#include +#endif + +#include "abspath.h" +#include "attr-fingerprint.h" +#include "clean-status-index.h" +#include "clean-status-sidecar.h" +#include "fsmonitor-clean-proof.h" +#include "hash-framing.h" +#include "lockfile.h" +#include "path.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "replace-object.h" +#include "strbuf.h" +#include "worktree.h" +#include "wrapper.h" + +#define CLEAN_STATUS_SIDECAR_MAGIC "CSTS" +#define CLEAN_STATUS_HARDLINK_PATH_MAX 4096 +#define CLEAN_STATUS_FILESYSTEM_ID_SIZE 16 + +struct clean_status_filesystem_id { + unsigned char value[CLEAN_STATUS_FILESYSTEM_ID_SIZE]; +}; + +static int checksum_valid(const void *data, size_t len, + const struct git_hash_algo *algo) +{ + const unsigned char *bytes = data; + unsigned char actual[GIT_MAX_RAWSZ]; + + if (len < algo->rawsz) + return 0; + hash_buffer_digest(algo, data, len - algo->rawsz, actual); + return !memcmp(actual, bytes + len - algo->rawsz, algo->rawsz); +} + +static int token_valid(const unsigned char *token, size_t token_len) +{ + static const char prefix[] = "builtin:"; + + return token && token_len && + token_len <= FSMONITOR_CLEAN_PROOF_TOKEN_MAX && + !memchr(token, '\0', token_len) && + token_len >= sizeof(prefix) - 1 && + !memcmp(token, prefix, sizeof(prefix) - 1); +} + +static int proof_valid(const struct clean_status_proof *proof, + const struct git_hash_algo *algo) +{ + return proof->index_version >= 2 && proof->index_version <= 4 && + (!is_null_oid(&proof->index_checksum) || + clean_status_identity_is_durable()) && + !is_null_oid(&proof->head_tree) && + !is_null_oid(&proof->exclude_source_digest) && + proof->index_checksum.algo == hash_algo_by_ptr(algo) && + proof->head_tree.algo == hash_algo_by_ptr(algo) && + proof->exclude_source_digest.algo == hash_algo_by_ptr(algo); +} + +static int hardlink_path_valid(const unsigned char *path, size_t len, + const struct path_stat_identity *identity) +{ + char *name; + int valid; + + if (!path || !len || len > CLEAN_STATUS_HARDLINK_PATH_MAX || + memchr(path, '\0', len) || + identity->fields[2] > UINT32_MAX || + !S_ISREG((mode_t)identity->fields[2]) || + identity->fields[3] <= 1) + return 0; + name = xmemdupz(path, len); + valid = verify_path(name, (unsigned)identity->fields[2]); + free(name); + return valid; +} + +int clean_status_sidecar_append_hardlink( + struct strbuf *out, const char *path, + const struct path_stat_identity *identity) +{ + uint32_t path_len; + uint64_t field; + size_t len; + + if (!out || !path || !identity) + return -1; + len = strlen(path); + if (!hardlink_path_valid((const unsigned char *)path, len, identity) || + out->len > CLEAN_STATUS_SIDECAR_MAX_SIZE - + (sizeof(path_len) + len + CLEAN_STATUS_IDENTITY_SIZE)) + return -1; + put_be32(&path_len, (uint32_t)len); + strbuf_add(out, &path_len, sizeof(path_len)); + strbuf_add(out, path, len); + for (size_t i = 0; i < PATH_STAT_IDENTITY_FIELDS; i++) { + put_be64(&field, identity->fields[i]); + strbuf_add(out, &field, sizeof(field)); + } + return 0; +} + +int clean_status_sidecar_next_hardlink( + const unsigned char **cursor, const unsigned char *end, + const unsigned char **path, size_t *path_len, + struct path_stat_identity *identity) +{ + const unsigned char *p; + size_t len; + + if (!cursor || !*cursor || !end || !path || !path_len || !identity || + *cursor > end || (size_t)(end - *cursor) < sizeof(uint32_t)) + return -1; + p = *cursor; + len = get_be32(p); + p += sizeof(uint32_t); + if (len > (size_t)(end - p) || + (size_t)(end - p) - len < CLEAN_STATUS_IDENTITY_SIZE) + return -1; + *path = p; + *path_len = len; + p += len; + for (size_t i = 0; i < PATH_STAT_IDENTITY_FIELDS; i++) { + identity->fields[i] = get_be64(p); + p += sizeof(uint64_t); + } + if (!hardlink_path_valid(*path, *path_len, identity)) + return -1; + *cursor = p; + return 0; +} + +static int hardlink_block_valid(const unsigned char *block, size_t len, + uint32_t nr) +{ + struct path_stat_identity identity; + const unsigned char *cursor = block, *previous = NULL; + const unsigned char *path; + size_t path_len, previous_len = 0; + + if (!nr || nr > CLEAN_STATUS_HARDLINK_WITNESS_MAX || !block || + len > CLEAN_STATUS_SIDECAR_MAX_SIZE) + return 0; + for (uint32_t i = 0; i < nr; i++) { + if (clean_status_sidecar_next_hardlink( + &cursor, block + len, &path, &path_len, &identity)) + return 0; + if (previous) { + size_t common = previous_len < path_len ? + previous_len : path_len; + int order = memcmp(previous, path, common); + + if (order > 0 || (!order && previous_len >= path_len)) + return 0; + } + previous = path; + previous_len = path_len; + } + return cursor == block + len; +} + +int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, + const void *data, size_t len, + const struct git_hash_algo *algo) +{ + const unsigned char *p = data; + const unsigned char *end; + size_t minimum = 4 + 2 * sizeof(uint32_t) + + CLEAN_STATUS_IDENTITY_SIZE + 3 * sizeof(uint32_t) + + 6 * algo->rawsz + 1; + uint32_t flags, token_len, version; + + memset(sidecar, 0, sizeof(*sidecar)); + if (len < minimum || len > CLEAN_STATUS_SIDECAR_MAX_SIZE || + memcmp(p, CLEAN_STATUS_SIDECAR_MAGIC, 4) || + !checksum_valid(data, len, algo)) + return -1; + end = p + len - algo->rawsz; + p += 4; + version = get_be32(p); + if (version != CLEAN_STATUS_SIDECAR_VERSION && + version != CLEAN_STATUS_SIDECAR_HARDLINK_VERSION) + return -1; + p += sizeof(uint32_t); + flags = get_be32(p); + p += sizeof(uint32_t); + if (flags) + return -1; + if (clean_status_identity_read(&p, end, &sidecar->identity)) + return -1; + sidecar->proof.index_version = get_be32(p); + p += sizeof(uint32_t); + sidecar->proof.cache_nr = get_be32(p); + p += sizeof(uint32_t); + oidread(&sidecar->proof.index_checksum, p, algo); + p += algo->rawsz; + oidread(&sidecar->proof.head_tree, p, algo); + p += algo->rawsz; + memcpy(sidecar->proof.config_hash, p, algo->rawsz); + p += algo->rawsz; + memcpy(sidecar->proof.repo_hash, p, algo->rawsz); + p += algo->rawsz; + oidread(&sidecar->proof.exclude_source_digest, p, algo); + p += algo->rawsz; + token_len = get_be32(p); + p += sizeof(uint32_t); + if (!proof_valid(&sidecar->proof, algo) || + (size_t)(end - p) < token_len || + !token_valid(p, token_len)) + return -1; + sidecar->token = p; + sidecar->token_len = token_len; + p += token_len; + if (version == CLEAN_STATUS_SIDECAR_VERSION) + return p == end ? 0 : -1; + if ((size_t)(end - p) < sizeof(uint32_t)) + return -1; + sidecar->hardlink_nr = get_be32(p); + p += sizeof(uint32_t); + sidecar->hardlinks = p; + sidecar->hardlinks_len = end - p; + if (!hardlink_block_valid(sidecar->hardlinks, + sidecar->hardlinks_len, + sidecar->hardlink_nr)) + return -1; + return 0; +} + +int clean_status_sidecar_write(struct strbuf *out, + const struct clean_status_sidecar *sidecar, + const struct git_hash_algo *algo) +{ + uint32_t value; + + strbuf_reset(out); + if (!proof_valid(&sidecar->proof, algo) || + sidecar->token_len > UINT32_MAX || + !token_valid(sidecar->token, sidecar->token_len) || + (sidecar->hardlink_nr ? + !hardlink_block_valid(sidecar->hardlinks, + sidecar->hardlinks_len, + sidecar->hardlink_nr) : + (sidecar->hardlinks || sidecar->hardlinks_len))) + return -1; + + strbuf_add(out, CLEAN_STATUS_SIDECAR_MAGIC, 4); + put_be32(&value, sidecar->hardlink_nr ? + CLEAN_STATUS_SIDECAR_HARDLINK_VERSION : + CLEAN_STATUS_SIDECAR_VERSION); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, 0); + strbuf_add(out, &value, sizeof(value)); + clean_status_identity_write(out, &sidecar->identity); + put_be32(&value, sidecar->proof.index_version); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, sidecar->proof.cache_nr); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, sidecar->proof.index_checksum.hash, algo->rawsz); + strbuf_add(out, sidecar->proof.head_tree.hash, algo->rawsz); + strbuf_add(out, sidecar->proof.config_hash, algo->rawsz); + strbuf_add(out, sidecar->proof.repo_hash, algo->rawsz); + strbuf_add(out, sidecar->proof.exclude_source_digest.hash, + algo->rawsz); + put_be32(&value, sidecar->token_len); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, sidecar->token, sidecar->token_len); + if (sidecar->hardlink_nr) { + put_be32(&value, sidecar->hardlink_nr); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, sidecar->hardlinks, sidecar->hardlinks_len); + } + if (out->len > CLEAN_STATUS_SIDECAR_MAX_SIZE - algo->rawsz) { + strbuf_reset(out); + return -1; + } + hash_append_checksum(out, algo); + return 0; +} + +static char *sidecar_path(const char *index_path) +{ + return xstrfmt("%s.csts", index_path); +} + +static int open_nofollow_nonblocking(const char *path, int flags) +{ +#ifdef O_NONBLOCK + return open_nofollow(path, flags | O_NONBLOCK); +#else + (void)path; + (void)flags; + errno = ENOSYS; + return -1; +#endif +} + +int clean_status_sidecar_load( + const char *index_path, const struct git_hash_algo *algo, + struct clean_status_sidecar_record *record) +{ + struct stat st; + char extra; + char *path = sidecar_path(index_path); + int fd = -1, ret = -1; + size_t size; + + memset(&record->sidecar, 0, sizeof(record->sidecar)); + strbuf_reset(&record->storage); + fd = open_nofollow_nonblocking(path, O_RDONLY | O_CLOEXEC); + if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || + st.st_size < 0 || st.st_size > CLEAN_STATUS_SIDECAR_MAX_SIZE) + goto done; + size = xsize_t(st.st_size); + strbuf_grow(&record->storage, size); + strbuf_setlen(&record->storage, size); + if ((size_t)read_in_full(fd, record->storage.buf, size) != size || + read(fd, &extra, 1) != 0 || + clean_status_sidecar_parse(&record->sidecar, + record->storage.buf, + record->storage.len, algo)) + goto done; + ret = 0; + +done: + if (ret) + strbuf_reset(&record->storage); + if (fd >= 0) + close(fd); + free(path); + return ret; +} + +void clean_status_sidecar_record_release( + struct clean_status_sidecar_record *record) +{ + strbuf_release(&record->storage); + memset(&record->sidecar, 0, sizeof(record->sidecar)); +} + +static int local_apfs_id(int fd MAYBE_UNUSED, + struct clean_status_filesystem_id *id) +{ +#ifdef __APPLE__ + struct statfs fs; +#endif + + memset(id, 0, sizeof(*id)); +#ifdef __APPLE__ + if (fstatfs(fd, &fs) || !(fs.f_flags & MNT_LOCAL) || + strcmp(fs.f_fstypename, "apfs") || + sizeof(fs.f_fsid) > sizeof(id->value)) + return -1; + memcpy(id->value, &fs.f_fsid, sizeof(fs.f_fsid)); + return 0; +#else + return -1; +#endif +} + +static int sidecar_matches_snapshot( + const char *index_path, const struct clean_status_sidecar *sidecar, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo) +{ + struct clean_status_filesystem_id fsid; + + return clean_status_identity_is_durable() && + snapshot && snapshot->fd >= 0 && + !local_apfs_id(snapshot->fd, &fsid) && + clean_status_identity_equal(&snapshot->identity, + &sidecar->identity) && + snapshot->version == sidecar->proof.index_version && + snapshot->cache_nr == sidecar->proof.cache_nr && + oideq(&snapshot->checksum, + &sidecar->proof.index_checksum) && + clean_status_index_snapshot_still_matches_path( + snapshot, index_path, algo); +} + +int clean_status_sidecar_pin_source( + const char *index_path, const struct clean_status_sidecar *sidecar, + const struct git_hash_algo *algo, + struct clean_status_index_snapshot *snapshot) +{ + if (clean_status_index_snapshot_open_allow_null_checksum( + snapshot, index_path, algo)) + return -1; + if (sidecar_matches_snapshot( + index_path, sidecar, snapshot, algo)) + return 0; + clean_status_index_snapshot_release(snapshot); + return -1; +} + +int clean_status_sidecar_install( + const char *index_path, const struct clean_status_sidecar *sidecar, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo) +{ + struct strbuf encoded = STRBUF_INIT; + struct lock_file lock = LOCK_INIT; + char *path = sidecar_path(index_path); + int sidecar_fd = -1, ret = -1; + + if (!sidecar_matches_snapshot( + index_path, sidecar, snapshot, algo) || + clean_status_sidecar_write(&encoded, sidecar, algo)) + goto done; + sidecar_fd = hold_lock_file_for_update(&lock, path, LOCK_NO_DEREF); + if (sidecar_fd < 0 || + (size_t)write_in_full(sidecar_fd, encoded.buf, encoded.len) != + encoded.len || + !sidecar_matches_snapshot( + index_path, sidecar, snapshot, algo) || + commit_lock_file(&lock)) + goto done; + ret = 0; + +done: + if (ret) + rollback_lock_file(&lock); + free(path); + strbuf_release(&encoded); + return ret; +} + +static int current_worktree_is_main(struct repository *repo) +{ + struct worktree *worktree = get_current_worktree(repo); + int ret = worktree && is_main_worktree(worktree); + + free_worktree(worktree); + return ret; +} + +static int worktree_root_identity( + const struct stat *st, uint64_t *identity MAYBE_UNUSED) +{ + if (!S_ISDIR(st->st_mode)) + return -1; +#ifdef __APPLE__ + identity[0] = st->st_dev; + identity[1] = st->st_ino; + identity[2] = st->st_birthtimespec.tv_sec; + identity[3] = st->st_birthtimespec.tv_nsec; + identity[4] = st->st_gen; + return 0; +#else + return -1; +#endif +} + +int clean_status_repository_fingerprint( + struct repository *repo, + const struct attr_source_snapshot *attrs, + const struct clean_status_index_snapshot *index, + const struct stat *scanned_worktree, + unsigned char *out) +{ + static const char domain[] = "git-clean-status-repository-v1"; + const struct attr_fingerprint *attr_fingerprint = + attr_source_snapshot_fingerprint(attrs); + struct clean_status_filesystem_id index_fsid, worktree_fsid; + struct git_hash_ctx ctx; + struct stat st; + char *worktree = NULL, *gitdir = NULL, *commondir = NULL; + uint64_t root_identity[5]; + uint64_t scanned_root_identity[5]; + uint64_t value; + int worktree_fd = -1, ret = -1; + + if (!attr_fingerprint || attr_fingerprint->sources_present || + !index || index->fd < 0 || !scanned_worktree || + is_bare_repository(repo) || + !repo_get_work_tree(repo) || + !current_worktree_is_main(repo) || + repo_has_replace_refs_uncached(repo)) + goto done; + + worktree = real_pathdup(repo_get_work_tree(repo), 0); + gitdir = real_pathdup(repo_get_git_dir(repo), 0); + commondir = real_pathdup(repo_get_common_dir(repo), 0); + if (!worktree || !gitdir || !commondir) + goto done; + worktree_fd = open_nofollow_nonblocking( + worktree, O_RDONLY | O_CLOEXEC); + if (worktree_fd < 0 || + local_apfs_id(worktree_fd, &worktree_fsid) || + local_apfs_id(index->fd, &index_fsid) || + fstat(worktree_fd, &st) || + worktree_root_identity(&st, root_identity) || + worktree_root_identity( + scanned_worktree, scanned_root_identity) || + memcmp(root_identity, scanned_root_identity, + sizeof(root_identity))) + goto done; + + git_hash_init(&ctx, repo->hash_algo); + hash_length_delimited(&ctx, domain, sizeof(domain) - 1); + hash_length_delimited(&ctx, worktree, strlen(worktree)); + hash_length_delimited(&ctx, gitdir, strlen(gitdir)); + hash_length_delimited(&ctx, commondir, strlen(commondir)); + for (size_t i = 0; i < ARRAY_SIZE(root_identity); i++) { + put_be64(&value, root_identity[i]); + hash_length_delimited(&ctx, &value, sizeof(value)); + } + hash_length_delimited(&ctx, worktree_fsid.value, + sizeof(worktree_fsid.value)); + hash_length_delimited(&ctx, index_fsid.value, + sizeof(index_fsid.value)); + hash_length_delimited(&ctx, attr_fingerprint->content_hash, + repo->hash_algo->rawsz); + hash_optional_cstring(&ctx, setlocale(LC_CTYPE, NULL)); + hash_optional_cstring(&ctx, getenv("LC_ALL")); + hash_optional_cstring(&ctx, getenv("LC_CTYPE")); + hash_optional_cstring(&ctx, getenv("LANG")); + git_hash_final(out, &ctx); + ret = 0; + +done: + if (worktree_fd >= 0) + close(worktree_fd); + free(worktree); + free(gitdir); + free(commondir); + return ret; +} diff --git a/clean-status-sidecar.h b/clean-status-sidecar.h new file mode 100644 index 00000000000000..a496246c28ddff --- /dev/null +++ b/clean-status-sidecar.h @@ -0,0 +1,80 @@ +#ifndef CLEAN_STATUS_SIDECAR_H +#define CLEAN_STATUS_SIDECAR_H + +#include "clean-status-identity.h" +#include "hash.h" +#include "strbuf.h" + +struct clean_status_index_snapshot; +struct attr_source_snapshot; +struct repository; +struct stat; + +#define CLEAN_STATUS_SIDECAR_VERSION 1 +#define CLEAN_STATUS_SIDECAR_HARDLINK_VERSION 2 +#define CLEAN_STATUS_HARDLINK_WITNESS_MAX 4096 +#define CLEAN_STATUS_SIDECAR_MAX_SIZE (1024 * 1024) + +struct clean_status_proof { + uint32_t index_version; + uint32_t cache_nr; + struct object_id index_checksum; + struct object_id head_tree; + unsigned char config_hash[GIT_MAX_RAWSZ]; + unsigned char repo_hash[GIT_MAX_RAWSZ]; + struct object_id exclude_source_digest; +}; + +struct clean_status_sidecar { + struct clean_status_identity identity; + struct clean_status_proof proof; + const unsigned char *token; + size_t token_len; + const unsigned char *hardlinks; + size_t hardlinks_len; + uint32_t hardlink_nr; +}; + +struct clean_status_sidecar_record { + struct clean_status_sidecar sidecar; + struct strbuf storage; +}; + +#define CLEAN_STATUS_SIDECAR_RECORD_INIT { \ + .storage = STRBUF_INIT, \ +} + +int clean_status_sidecar_parse(struct clean_status_sidecar *sidecar, + const void *data, size_t len, + const struct git_hash_algo *algo); +int clean_status_sidecar_write(struct strbuf *out, + const struct clean_status_sidecar *sidecar, + const struct git_hash_algo *algo); +int clean_status_sidecar_append_hardlink( + struct strbuf *out, const char *path, + const struct path_stat_identity *identity); +int clean_status_sidecar_next_hardlink( + const unsigned char **cursor, const unsigned char *end, + const unsigned char **path, size_t *path_len, + struct path_stat_identity *identity); +int clean_status_sidecar_load( + const char *index_path, const struct git_hash_algo *algo, + struct clean_status_sidecar_record *record); +void clean_status_sidecar_record_release( + struct clean_status_sidecar_record *record); +int clean_status_sidecar_pin_source( + const char *index_path, const struct clean_status_sidecar *sidecar, + const struct git_hash_algo *algo, + struct clean_status_index_snapshot *snapshot); +int clean_status_sidecar_install( + const char *index_path, const struct clean_status_sidecar *sidecar, + const struct clean_status_index_snapshot *snapshot, + const struct git_hash_algo *algo); +int clean_status_repository_fingerprint( + struct repository *repo, + const struct attr_source_snapshot *attrs, + const struct clean_status_index_snapshot *index, + const struct stat *scanned_worktree, + unsigned char *out); + +#endif /* CLEAN_STATUS_SIDECAR_H */ diff --git a/clean-status.c b/clean-status.c new file mode 100644 index 00000000000000..6c0dd8f1bebac8 --- /dev/null +++ b/clean-status.c @@ -0,0 +1,865 @@ +#include "git-compat-util.h" +#include "attr-fingerprint.h" +#include "attr-manifest.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" +#include "convert.h" +#include "dir.h" +#include "environment.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-settings.h" +#include "progress.h" +#include "read-cache-ll.h" +#include "replace-object.h" +#include "repository.h" +#include "semantic-verify-internal.h" +#include "worktree-attr-source.h" +#include "thread-utils.h" +#include "trace2.h" + +static struct repository *configured_repo; +static unsigned char configured_hash[GIT_MAX_RAWSZ]; +static unsigned char configured_semantic_hash[GIT_MAX_RAWSZ]; +static unsigned char configured_tracked_policy_hash[GIT_MAX_RAWSZ]; +static struct repository *external_history_repo; +static struct repository *progress_repo; +static int configured_hash_valid; +static int configured_filter_configured; +static int configured_semantic_explicit; + +struct clean_status_progress { + struct progress *display; + pthread_mutex_t mutex; + uint64_t completed; +}; + +void clean_status_enable_external_history(struct repository *repo) +{ + external_history_repo = repo; +} + +void clean_status_prepare_main_index_history(struct repository *repo) +{ + struct clean_status_config_digest digest; + + /* + * Attach the command's configuration before its first index read. This + * does not grant a proof: the reader must still authenticate the on-disk + * epoch, and each writer must validate its own logical changes. + */ + if (!repo || !repo->worktree || + !clean_status_index_path_is_main(repo, repo->index_file) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || !fstat_is_reliable() || + repo_config_values(repo)->apply_sparse_checkout || + !repo->config_values_private_.trust_ctime || + !repo->config_values_private_.check_stat || + (fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC && + !fsm_settings__is_watch_limit_backoff(repo)) || + repo_has_replace_refs_uncached(repo) || + clean_status_config_read_repository(repo, &digest)) + return; + clean_status_enable_external_history(repo); + clean_status_set_config_digest(repo, &digest); +} + +int clean_status_external_history_enabled(const struct index_state *istate) +{ + return istate && istate->repo == external_history_repo; +} + +void clean_status_enable_progress(struct repository *repo) +{ + progress_repo = repo; +} + +struct clean_status_progress *clean_status_start_progress( + struct repository *repo, const char *title, uint64_t total) +{ + struct clean_status_progress *progress; + + if (repo != progress_repo) + return NULL; + CALLOC_ARRAY(progress, 1); + if (HAVE_THREADS && pthread_mutex_init(&progress->mutex, NULL)) + BUG("could not initialize clean status progress mutex"); + progress->display = start_delayed_progress(repo, title, total); + return progress; +} + +void clean_status_update_progress(struct clean_status_progress *progress, + uint64_t completed) +{ + if (!progress || !completed) + return; + pthread_mutex_lock(&progress->mutex); + progress->completed += completed; + display_progress(progress->display, progress->completed); + pthread_mutex_unlock(&progress->mutex); +} + +void clean_status_stop_progress(struct clean_status_progress **progress) +{ + if (!progress || !*progress) + return; + stop_progress(&(*progress)->display); + pthread_mutex_destroy(&(*progress)->mutex); + FREE_AND_NULL(*progress); +} + +struct clean_status_state *clean_status_get_state(struct index_state *istate) +{ + if (!istate->clean_status) { + CALLOC_ARRAY(istate->clean_status, 1); + istate->clean_status->source_index_fd = -1; + clean_status_manifest_init(&istate->clean_status->manifest); + strbuf_init(&istate->clean_status->disk_config_raw, 0); + strbuf_init(&istate->clean_status->authenticated_new_directories, + 0); + } + return istate->clean_status; +} + +void clean_status_set_config_digest( + struct repository *repo, + const struct clean_status_config_digest *digest) +{ + configured_repo = repo; + configured_hash_valid = digest && digest->finalized; + configured_filter_configured = configured_hash_valid && + digest->filter_configured; + configured_semantic_explicit = configured_hash_valid && + digest->semantic_config_explicit; + if (!configured_hash_valid) + return; + memcpy(configured_hash, digest->hash, repo->hash_algo->rawsz); + memcpy(configured_semantic_hash, digest->semantic_hash, + repo->hash_algo->rawsz); + memcpy(configured_tracked_policy_hash, + digest->tracked_policy_hash, repo->hash_algo->rawsz); +} + +void clean_status_attach_config(struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + struct attr_fingerprint attrs; + + if (state && state->current_config_valid) + return; + if (!configured_hash_valid || configured_repo != istate->repo) + return; + state = clean_status_get_state(istate); + memcpy(state->current_config_hash, configured_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_semantic_hash, configured_semantic_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_tracked_policy_hash, + configured_tracked_policy_hash, + istate->repo->hash_algo->rawsz); + state->current_config_valid = 1; + state->current_semantic_valid = 1; + state->current_tracked_policy_valid = 1; + state->current_semantic_explicit = configured_semantic_explicit; + state->config_enforced = 1; + state->filter_configured = configured_filter_configured; + if (!attr_fingerprint_repository(istate->repo, &attrs)) { + memcpy(state->current_attr_hash, attrs.content_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_attr_namespace_hash, attrs.namespace_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_attr_portable_namespace_hash, + attrs.portable_namespace_hash, + istate->repo->hash_algo->rawsz); + state->current_attr_valid = 1; + state->current_attr_sources_present = attrs.sources_present; + } +} + +int clean_status_filter_scope_needs_validation( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->current_config_valid && state->config_enforced && + state->filter_configured && !state->filter_scope_valid; +} + +void clean_status_mark_filter_scope_valid(struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + + if (!state || !state->current_config_valid || !state->config_enforced || + !state->filter_configured) + return; + state->filter_scope_valid = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "filter-scope/valid", 1); +} + +int clean_status_revalidated_token_matches(const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && !state->backoff_token && state->config_revalidated && + state->config_revalidated_token && + istate->fsmonitor_last_update && + !strcmp(state->config_revalidated_token, + istate->fsmonitor_last_update); +} + +void clean_status_invalidate_current_proof(struct index_state *istate) +{ + if (!istate->clean_status) + return; + clean_status_clear_authenticated_new_directories(istate); + istate->clean_status->config_revalidated = 0; + istate->clean_status->initial_coherent = 0; + istate->clean_status->filter_scope_valid = 0; + istate->clean_status->semantic_baseline_pending = 0; + istate->clean_status->backoff_suspended = 0; +} + +int clean_status_fsmonitor_backoff_suspended( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->backoff_suspended && state->backoff_token && + istate->fsmonitor_token_valid && istate->fsmonitor_last_update && + !strcmp(state->backoff_token, istate->fsmonitor_last_update) && + fsm_settings__is_watch_limit_backoff(istate->repo); +} + +int clean_status_suspend_fsmonitor_for_backoff(struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + struct clean_status_index_snapshot source = { .fd = -1 }; + const struct git_hash_algo *algo; + const struct untracked_cache *uc = istate->untracked; + const char *suffix, *pending; + char *main_index; + const uint32_t historical = FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX; + int paired, suspended = 0; + + /* Never revive an epoch which an earlier mutation has invalidated. */ + if (state && state->backoff_token) + return clean_status_fsmonitor_backoff_suspended(istate); + if (!state || !istate->repo || !istate->repo->worktree || + !fsm_settings__is_watch_limit_backoff(istate->repo) || + !fstat_is_reliable() || istate != istate->repo->index || + !clean_status_index_path_is_main(istate->repo, + istate->repo->index_file) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + repo_config_values(istate->repo)->apply_sparse_checkout || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat || + repo_has_replace_refs_uncached(istate->repo) || + !state->config_enforced || !state->current_config_valid || + !state->current_semantic_valid || !state->current_attr_valid || + !state->current_tracked_policy_valid || + state->external_history_restored || !state->disk_config_valid || + state->disk_config_invalid || !state->disk_config_raw.len || + !state->disk_semantic_valid || !state->disk_attr_valid || + !state->disk_tracked_policy_valid || !state->manifest.disk_valid || + !istate->fsmonitor_extension_seen || !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + !skip_prefix(istate->fsmonitor_last_update, "builtin:", &suffix) || + !*suffix || !strcmp(suffix, "fake") || + !state->disk_config_token || + strcmp(state->disk_config_token, istate->fsmonitor_last_update) || + istate->fsmonitor_last_update_pending || + istate->fsmonitor_pending_token_from_provider || + istate->fsmonitor_legacy_untracked_fallback || + !uc || !uc->root || !uc->root->valid || + uc->fsmonitor_dirty_paths.len || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + !istate->fsmonitor_untracked_token) + return 0; + algo = istate->repo->hash_algo; + if (memcmp(state->disk_config_hash, state->current_config_hash, + algo->rawsz) || + memcmp(state->disk_semantic_hash, state->current_semantic_hash, + algo->rawsz) || + memcmp(state->disk_attr_hash, state->current_attr_hash, algo->rawsz) || + memcmp(state->disk_tracked_policy_hash, + state->current_tracked_policy_hash, algo->rawsz)) + return 0; + + paired = state->manifest.disk_flags == FSMONITOR_CLEAN_PROOF_ALL && + clean_status_has_current_full_fsmonitor_proof(istate) && + !memcmp(state->manifest.disk_hash, state->manifest.current_hash, + algo->rawsz) && + istate->fsmonitor_untracked_valid && uc->root->valid_recursive && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token); + if (!paired && + !(state->manifest.disk_flags == historical && + !istate->fsmonitor_untracked_valid && uc->fsmonitor_revalidation && + skip_prefix(istate->fsmonitor_untracked_token, "pending:", &pending) && + !strcmp(suffix, pending))) + return 0; + if (clean_status_index_snapshot_pin_proof_epoch(&source, istate)) + return 0; + main_index = xstrfmt("%s/index", repo_get_git_dir(istate->repo)); + if (!clean_status_index_snapshot_still_matches_path( + &source, main_index, algo) || + !untracked_cache_preserve_for_revalidation(istate) || + !clean_status_index_snapshot_still_matches_proof_epoch(&source, istate) || + !clean_status_index_snapshot_still_matches_path( + &source, main_index, algo)) + goto done; + + /* Only historical path semantics survive. Nothing is currently clean. */ + clean_status_manifest_adopt_disk(&state->manifest); + state->manifest.current_flags = historical; + clean_status_clear_authenticated_new_directories(istate); + state->authenticated_bootstrap_manifest = 0; + state->config_revalidated = 0; + state->initial_coherent = 0; + state->config_mismatch = 1; + /* Replacements check their attributes; no current filter scope survives. */ + state->filter_scope_valid = 0; + FREE_AND_NULL(state->config_revalidated_token); + state->backoff_token = xstrdup(istate->fsmonitor_last_update); + state->backoff_suspended = 1; + state->semantic_baseline_pending = 1; + suspended = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "history/watch-limit-suspended", 1); +done: + free(main_index); + clean_status_index_snapshot_release(&source); + return suspended; +} + +static int path_has_no_new_attribute_sources( + const struct index_state *istate, const char *name, + int allow_removed_parent) +{ + const struct clean_status_manifest_state *manifest = + &istate->clean_status->manifest; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct strbuf candidate = STRBUF_INIT; + const char *slash = name; + unsigned char hash[GIT_MAX_RAWSZ]; + unsigned int namespace_unstable = 0; + size_t position = 0; + int safe = 0; + + if (!strchr(name, '/')) + return 1; + if (semantic_verify_root_init(istate->repo, &root)) + goto done; + path = semantic_verify_path_new(root); + while ((slash = strchr(slash, '/')) != NULL) { + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + const struct cache_entry *source = NULL; + const char *basename; + unsigned int low = 0, high = istate->cache_nr; + int parent_fd, found, matched = 0, missing_parent = 0; + + strbuf_reset(&candidate); + strbuf_add(&candidate, name, slash - name + 1); + strbuf_addstr(&candidate, ".gitattributes"); + if (semantic_verify_resolve_parent(path, candidate.buf, + position, &parent_fd, + &basename)) { + if (!allow_removed_parent || errno != ENOENT) + goto done; + missing_parent = 1; + found = 0; + } else if (worktree_attr_source_read(path, candidate.buf, + position, + istate->repo->hash_algo, + hash, &found)) { + goto done; + } + position++; + while (low < high) { + unsigned int middle = low + (high - low) / 2; + const struct cache_entry *ce = istate->cache[middle]; + int cmp = strcmp(ce->name, candidate.buf); + + if (!cmp) { + source = ce; + break; + } + if (cmp < 0) + low = middle + 1; + else + high = middle; + } + if (!source && !found && !missing_parent) { + slash++; + continue; + } + if (!manifest->current_valid || manifest->current_invalidated || + (manifest->current_flags & + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) != + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) + goto done; + if (attr_manifest_cursor_init(&cursor, manifest->current.buf, + manifest->current.len, + istate->repo->hash_algo)) + goto done; + if (missing_parent) { + if (source) + goto done; + while (attr_manifest_cursor_next(&cursor, &entry) > 0) + if (entry.path_len == candidate.len && + !memcmp(entry.path, candidate.buf, candidate.len)) + goto done; + slash++; + continue; + } + if (!source || !S_ISREG(source->ce_mode) || ce_stage(source) || + ce_skip_worktree(source) || ce_intent_to_add(source) || + (source->ce_flags & CE_VALID)) + goto done; + while (attr_manifest_cursor_next(&cursor, &entry) > 0) { + if (entry.path_len != candidate.len || + memcmp(entry.path, candidate.buf, candidate.len)) + continue; + matched = found ? + entry.source == ATTR_MANIFEST_WORKTREE && + !memcmp(entry.hash, hash, + istate->repo->hash_algo->rawsz) : + entry.source == ATTR_MANIFEST_INDEX && + !memcmp(entry.hash, source->oid.hash, + istate->repo->hash_algo->rawsz); + break; + } + if (!matched) + goto done; + slash++; + } + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + safe = !namespace_unstable && semantic_verify_root_stable(root); + +done: + if (path) + semantic_verify_path_free(path, &namespace_unstable, NULL); + semantic_verify_root_clear(root); + strbuf_release(&candidate); + return safe; +} + +int clean_status_index_entry_is_semantically_safe( + const struct index_state *istate, + const struct cache_entry *old, + const struct cache_entry *new_entry) +{ + const struct clean_status_state *state = istate->clean_status; + const struct cache_entry *entry = old ? old : new_entry; + struct conv_attrs attrs; + const char *base; + int suspended = clean_status_fsmonitor_backoff_suspended(istate); + + if (!state || + (!suspended && !clean_status_revalidated_token_matches(istate)) || + (!suspended && state->filter_configured && + !state->filter_scope_valid) || + istate->split_index || + istate->sparse_index || !entry) + return 0; + if ((old && (!S_ISREG(old->ce_mode) && !S_ISLNK(old->ce_mode))) || + (new_entry && (!S_ISREG(new_entry->ce_mode) && + !S_ISLNK(new_entry->ce_mode)))) + return 0; + if ((old && (ce_stage(old) || ce_skip_worktree(old) || + ce_intent_to_add(old) || (old->ce_flags & CE_VALID))) || + (new_entry && (ce_stage(new_entry) || + ce_skip_worktree(new_entry) || + ce_intent_to_add(new_entry) || + (new_entry->ce_flags & CE_VALID)))) + return 0; + base = strrchr(entry->name, '/'); + base = base ? base + 1 : entry->name; + if (!fspathcmp(base, ".gitattributes") || + !fspathcmp(base, ".gitignore")) + return 0; + if (suspended && + (!old || !new_entry || !S_ISREG(old->ce_mode) || + !S_ISREG(new_entry->ce_mode) || + !clean_status_manifest_path_attributes_unchanged(istate, entry->name))) + return 0; + if (state->filter_configured) { + convert_attrs((struct index_state *)istate, &attrs, entry->name); + if (convert_attrs_has_clean_filter(&attrs)) + return 0; + } + if (!old || !new_entry) + return path_has_no_new_attribute_sources(istate, entry->name, + old && !new_entry); + return ce_namelen(old) == ce_namelen(new_entry) && + !memcmp(old->name, new_entry->name, ce_namelen(old)) && + old->ce_mode == new_entry->ce_mode; +} + +void clean_status_clear_authenticated_new_directories( + struct index_state *istate) +{ + struct clean_status_state *state = istate->clean_status; + + if (!state) + return; + strbuf_reset(&state->authenticated_new_directories); + FREE_AND_NULL(state->authenticated_new_directories_token); +} + +static unsigned int clean_status_directory_lower_bound( + const struct index_state *istate, const char *name) +{ + unsigned int low = 0, high = istate->cache_nr; + + while (low < high) { + unsigned int middle = low + (high - low) / 2; + + if (strcmp(istate->cache[middle]->name, name) < 0) + low = middle + 1; + else + high = middle; + } + return low; +} + +static int clean_status_changed_directory_is_semantically_safe( + const struct index_state *istate, const char *name) +{ + const struct clean_status_state *state = istate->clean_status; + struct semantic_verify_root *root = NULL; + struct semantic_verify_path *path = NULL; + struct attr_manifest_cursor cursor; + struct attr_manifest_entry entry; + struct strbuf candidate = STRBUF_INIT; + const char *basename; + unsigned int first, i, namespace_unstable = 0; + size_t len; + int parent_fd, next, removed, safe = 0; + + if (!state || !fstat_is_reliable() || + !state->current_config_valid || !state->current_attr_valid || + !state->config_enforced || !state->config_revalidated || + !clean_status_revalidated_token_matches(istate) || + !state->manifest.current_valid || !state->manifest.checked || + state->manifest.current_invalidated || + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) != + FSMONITOR_CLEAN_PROOF_ALL || + (state->filter_configured && !state->filter_scope_valid) || + istate->split_index || istate->sparse_index || + !istate->repo->config_values_private_.trust_ctime || + !istate->repo->config_values_private_.check_stat) + return 0; + + len = strlen(name); + if (!len || name[len - 1] != '/') + return 0; + first = clean_status_directory_lower_bound(istate, name); + if (first >= istate->cache_nr || + !starts_with(istate->cache[first]->name, name)) + return 0; + /* Each descendant independently authenticates its attribute ancestry. */ + if (istate->cache_nr - first > 64 && + starts_with(istate->cache[first + 64]->name, name)) + return 0; + + if (attr_manifest_cursor_init(&cursor, + state->manifest.current.buf, + state->manifest.current.len, + istate->repo->hash_algo)) + return 0; + while ((next = attr_manifest_cursor_next(&cursor, &entry)) > 0) + if (entry.path_len >= len && + !memcmp(entry.path, name, len)) + return 0; + if (next < 0 || semantic_verify_root_init(istate->repo, &root)) + return 0; + + path = semantic_verify_path_new(root); + strbuf_addstr(&candidate, name); + strbuf_addstr(&candidate, ".gitattributes"); + if (semantic_verify_resolve_parent(path, candidate.buf, 0, + &parent_fd, &basename)) { + if (errno != ENOENT) + goto done; + removed = 1; + } else { + removed = 0; + } + + for (i = first; i < istate->cache_nr && + starts_with(istate->cache[i]->name, name); i++) + if (!clean_status_index_entry_is_semantically_safe( + istate, removed ? istate->cache[i] : NULL, + removed ? NULL : istate->cache[i])) + goto done; + + semantic_verify_path_free(path, &namespace_unstable, NULL); + path = NULL; + safe = !namespace_unstable && semantic_verify_root_stable(root); + if (safe) + trace2_data_intmax("fsmonitor", istate->repo, + removed ? + "semantic/authenticated-removed-directory" : + "semantic/authenticated-restored-directory", 1); + +done: + if (path) + semantic_verify_path_free(path, &namespace_unstable, NULL); + semantic_verify_root_clear(root); + strbuf_release(&candidate); + return safe; +} + +void clean_status_set_authenticated_new_directories( + struct index_state *istate, const struct index_state *old_index, + const struct strbuf *paths) +{ + struct clean_status_state *state = istate->clean_status; + const char *path = paths->buf, *end = paths->buf + paths->len; + + clean_status_clear_authenticated_new_directories(istate); + if (!state || !state->manifest.current_valid || + state->manifest.current_invalidated || + (state->manifest.current_flags & + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) != + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX) || + !clean_status_revalidated_token_matches(istate)) + return; + while (path < end) { + size_t len = strlen(path); + unsigned int old_pos, new_pos; + + if (!len || path[len - 1] != '/') + goto next; + old_pos = clean_status_directory_lower_bound(old_index, path); + new_pos = clean_status_directory_lower_bound(istate, path); + if ((old_pos < old_index->cache_nr && + starts_with(old_index->cache[old_pos]->name, path)) || + new_pos >= istate->cache_nr || + !starts_with(istate->cache[new_pos]->name, path) || + !clean_status_index_entry_is_semantically_safe( + old_index, NULL, istate->cache[new_pos])) + goto next; + strbuf_add(&state->authenticated_new_directories, path, + len + 1); +next: + path += len + 1; + } + if (state->authenticated_new_directories.len) + state->authenticated_new_directories_token = + xstrdup(istate->fsmonitor_last_update); +} + +int clean_status_directory_event_is_semantically_safe( + const struct index_state *istate, const char *name) +{ + const struct clean_status_state *state = istate->clean_status; + const char *path, *end; + + if (!state || !clean_status_revalidated_token_matches(istate)) + return 0; + if (state->authenticated_new_directories_token && + !strcmp(state->authenticated_new_directories_token, + istate->fsmonitor_last_update)) { + path = state->authenticated_new_directories.buf; + end = path + state->authenticated_new_directories.len; + while (path < end) { + if (!strcmp(path, name)) { + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/authenticated-new-directory", 1); + return 1; + } + path += strlen(path) + 1; + } + } + return clean_status_changed_directory_is_semantically_safe( + istate, name); +} + +int clean_status_capture_attr_snapshot( + struct index_state *istate, + struct attr_source_snapshot **snapshot) +{ + struct clean_status_state *state = istate->clean_status; + struct attr_source_snapshot *captured = NULL; + const struct attr_fingerprint *attrs; + int valid, changed = 0; + + if (!snapshot) + BUG("clean_status_capture_attr_snapshot requires an output"); + *snapshot = NULL; + if (!fstat_is_reliable() || !state || !state->current_config_valid || + !state->config_enforced) + return 0; + valid = !attr_source_snapshot_repository(istate->repo, &captured); + attrs = attr_source_snapshot_fingerprint(captured); + if (!valid || !state->current_attr_valid) { + changed = CLEAN_STATUS_ATTR_CONTENT_CHANGED | + CLEAN_STATUS_ATTR_NAMESPACE_CHANGED; + } else { + if (memcmp(attrs->content_hash, state->current_attr_hash, + istate->repo->hash_algo->rawsz)) + changed |= CLEAN_STATUS_ATTR_CONTENT_CHANGED; + if (memcmp(attrs->namespace_hash, + state->current_attr_namespace_hash, + istate->repo->hash_algo->rawsz)) + changed |= CLEAN_STATUS_ATTR_NAMESPACE_CHANGED; + } + if (valid) { + memcpy(state->current_attr_hash, attrs->content_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_attr_namespace_hash, attrs->namespace_hash, + istate->repo->hash_algo->rawsz); + memcpy(state->current_attr_portable_namespace_hash, + attrs->portable_namespace_hash, + istate->repo->hash_algo->rawsz); + state->current_attr_valid = 1; + state->current_attr_sources_present = attrs->sources_present; + } else { + state->current_attr_valid = 0; + } + if (changed) { + clean_status_invalidate_current_proof(istate); + state->config_mismatch = 1; + state->strong_mismatch = 1; + } + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/rechecked", 1); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/mismatch", state->strong_mismatch); + if (!valid) + return -1; + *snapshot = captured; + return changed; +} + +int clean_status_fsmonitor_config_mismatch(const struct index_state *istate) +{ + return istate->clean_status && + istate->clean_status->current_config_valid && + (istate->clean_status->config_mismatch || + !istate->clean_status->config_revalidated); +} + +int clean_status_fsmonitor_strong_mismatch(const struct index_state *istate) +{ + return istate->clean_status && + istate->clean_status->current_config_valid && + istate->clean_status->strong_mismatch; +} + +int clean_status_refresh_worktree_manifest(struct index_state *istate) +{ + struct clean_status_state *state = clean_status_get_state(istate); + + return clean_status_manifest_refresh(istate, &state->manifest); +} + +int clean_status_manifest_global_fallback(const struct index_state *istate) +{ + return istate->clean_status && + istate->clean_status->manifest.global_fallback; +} + +int clean_status_has_authenticated_worktree_manifest( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->disk_config_valid && + !state->disk_config_invalid && istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && state->disk_config_token && + !strcmp(state->disk_config_token, + istate->fsmonitor_last_update) && + state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + state->manifest.current_valid && state->manifest.checked && + !state->manifest.current_invalidated && + (state->manifest.current_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL; +} + +int clean_status_worktree_manifest_needs_refresh( + const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->config_enforced && + state->manifest.current_invalidated; +} + +void clean_status_invalidate_current_manifest(struct index_state *istate) +{ + if (!istate->clean_status) + return; + clean_status_manifest_invalidate(&istate->clean_status->manifest); + clean_status_invalidate_current_proof(istate); +} + +void clean_status_mark_fsmonitor_config_valid(struct index_state *istate, + const char *closed_token) +{ + struct clean_status_state *state = istate->clean_status; + + if (!state || !state->current_config_valid) + return; + if (!closed_token || clean_status_filter_scope_needs_validation(istate) || + !state->manifest.current_valid || !state->manifest.checked || + (state->manifest.current_flags & + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) != + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | + FSMONITOR_CLEAN_PROOF_FULL_INDEX)) { + clean_status_invalidate_current_proof(istate); + FREE_AND_NULL(state->config_revalidated_token); + trace2_data_intmax("fsmonitor", istate->repo, + "config/manifest-unbound", 1); + return; + } + state->config_mismatch = 0; + state->strong_mismatch = 0; + state->semantic_baseline_pending = 0; + state->backoff_suspended = 0; + FREE_AND_NULL(state->backoff_token); + state->manifest.current_flags = FSMONITOR_CLEAN_PROOF_ALL; + state->config_revalidated = state->current_semantic_valid && + state->current_attr_valid && state->manifest.current_valid; + state->initial_coherent = state->config_revalidated; + FREE_AND_NULL(state->config_revalidated_token); + if (state->config_revalidated) + state->config_revalidated_token = xstrdup(closed_token); + trace2_data_intmax("fsmonitor", istate->repo, + "config/revalidated", 1); +} + +void clean_status_release(struct index_state *istate) +{ + if (!istate->clean_status) + return; + if (istate->clean_status->source_index_fd >= 0) + close(istate->clean_status->source_index_fd); + clean_status_manifest_release(&istate->clean_status->manifest); + strbuf_release(&istate->clean_status->disk_config_raw); + strbuf_release(&istate->clean_status->authenticated_new_directories); + free(istate->clean_status->disk_config_token); + free(istate->clean_status->config_revalidated_token); + free(istate->clean_status->backoff_token); + free(istate->clean_status->authenticated_new_directories_token); + FREE_AND_NULL(istate->clean_status); +} diff --git a/clean-status.h b/clean-status.h new file mode 100644 index 00000000000000..1d17885cbc6d19 --- /dev/null +++ b/clean-status.h @@ -0,0 +1,213 @@ +#ifndef CLEAN_STATUS_H +#define CLEAN_STATUS_H + +#include "clean-status-config.h" + +struct index_state; +struct cache_entry; +struct attr_source_snapshot; +struct clean_status_progress; +struct clean_status_proof_epoch; +struct clean_status_index_snapshot; +struct clean_status_commit_checkpoint; +struct clean_status_backoff_transfer; +struct lock_file; +struct repository; +struct stat; +struct strbuf; +struct wt_status; + +enum clean_status_attr_change { + CLEAN_STATUS_ATTR_CONTENT_CHANGED = 1 << 0, + CLEAN_STATUS_ATTR_NAMESPACE_CHANGED = 1 << 1, +}; + +void clean_status_set_config_digest( + struct repository *repo, + const struct clean_status_config_digest *digest); +void clean_status_enable_external_history(struct repository *repo); +void clean_status_prepare_main_index_history(struct repository *repo); +int clean_status_external_history_enabled(const struct index_state *istate); +void clean_status_enable_progress(struct repository *repo); +struct clean_status_progress *clean_status_start_progress( + struct repository *repo, const char *title, uint64_t total); +void clean_status_update_progress(struct clean_status_progress *progress, + uint64_t completed); +void clean_status_stop_progress(struct clean_status_progress **progress); +void clean_status_attach_config(struct index_state *istate); +int clean_status_filter_scope_needs_validation( + const struct index_state *istate); +void clean_status_mark_filter_scope_valid(struct index_state *istate); + +int clean_status_capture_attr_snapshot( + struct index_state *istate, + struct attr_source_snapshot **snapshot); +struct clean_status_proof_epoch *clean_status_capture_proof_epoch( + struct index_state *istate, + const struct attr_source_snapshot *attrs, + int validate_filter_scope); +int clean_status_proof_epoch_start_token_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch); +int clean_status_proof_epoch_prime_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch); +int clean_status_proof_epoch_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch); +int clean_status_proof_epoch_content_matches( + struct index_state *istate, + const struct clean_status_proof_epoch *epoch); +void clean_status_release_proof_epoch( + struct clean_status_proof_epoch *epoch); + +int clean_status_fsmonitor_config_mismatch(const struct index_state *istate); +int clean_status_fsmonitor_strong_mismatch(const struct index_state *istate); +int clean_status_try_preserve_tracked_config_epoch( + struct index_state *istate); +int clean_status_revalidated_token_matches( + const struct index_state *istate); +int clean_status_suspend_fsmonitor_for_backoff(struct index_state *istate); +int clean_status_fsmonitor_backoff_suspended( + const struct index_state *istate); + +int clean_status_has_persistent_fsmonitor_semantic_history( + const struct index_state *istate); +int clean_status_has_current_full_fsmonitor_proof( + const struct index_state *istate); +int clean_status_has_worktree_manifest_history( + const struct index_state *istate); +int clean_status_fsmonitor_semantic_adoption_needed( + const struct index_state *istate); +int clean_status_fsmonitor_semantic_baseline_needed( + const struct index_state *istate); +int clean_status_fsmonitor_semantic_baseline_pending( + const struct index_state *istate); +void clean_status_begin_fsmonitor_semantic_baseline( + struct index_state *istate); + +int clean_status_refresh_worktree_manifest(struct index_state *istate); +int clean_status_manifest_global_fallback(const struct index_state *istate); +int clean_status_pending_revalidation_manifest_unchanged( + const struct index_state *istate); +int clean_status_has_authenticated_worktree_manifest( + const struct index_state *istate); +int clean_status_has_authenticated_bootstrap_manifest( + const struct index_state *istate); +int clean_status_worktree_manifest_needs_refresh( + const struct index_state *istate); +void clean_status_invalidate_current_manifest(struct index_state *istate); +void clean_status_mark_fsmonitor_config_valid( + struct index_state *istate, const char *closed_token); + +void clean_status_record_source_identity(struct index_state *istate, + const struct stat *st); +/* Takes ownership of fd only when it returns 1. */ +int clean_status_retain_source_index_fd(struct index_state *istate, int fd, + const struct stat *st); +int clean_status_verify_null_index(const struct index_state *istate, + const struct stat *st); + +int clean_status_issue_sidecar( + struct wt_status *status, + const struct clean_status_config_digest *config, + struct lock_file *index_lock, + int normal_clean_query); +int clean_status_try_sidecar( + struct repository *repo, + const struct clean_status_config_digest *config, + int *repository_inputs_changed, int *provider_reset); + +int clean_status_read_fsmonitor_config(struct index_state *istate, + const void *data, unsigned long size); +void clean_status_prepare_fsmonitor_config(struct index_state *istate); +int clean_status_probe_fsmonitor_config(struct index_state *istate); +void clean_status_invalidate_current_proof(struct index_state *istate); +int clean_status_index_entry_is_semantically_safe( + const struct index_state *istate, + const struct cache_entry *old, + const struct cache_entry *new_entry); +void clean_status_set_authenticated_new_directories( + struct index_state *istate, const struct index_state *old_index, + const struct strbuf *paths); +void clean_status_clear_authenticated_new_directories( + struct index_state *istate); +int clean_status_directory_event_is_semantically_safe( + const struct index_state *istate, const char *name); +void clean_status_advance_fsmonitor_config_token( + struct index_state *istate, const char *next_token); +int clean_status_should_write_fsmonitor_config( + const struct index_state *istate); +void clean_status_write_fsmonitor_config(struct strbuf *out, + const struct index_state *istate); +int clean_status_restore_external_history(struct index_state *istate); +int clean_status_external_history_was_restored( + const struct index_state *istate); +int clean_status_external_history_needs_witness_preservation( + const struct index_state *istate); +int clean_status_has_recovered_tracked_stat( + const struct index_state *istate); +int clean_status_external_history_owns_index( + const struct index_state *istate); +void clean_status_require_external_history_source(struct repository *repo); +void clean_status_capture_external_history_source( + struct index_state *istate); +int clean_status_capture_external_history_source_from_snapshot( + struct index_state *istate, + const struct clean_status_index_snapshot *snapshot); +int clean_status_save_external_history(struct index_state *istate); +void clean_status_copy_fsmonitor_history(struct index_state *dst, + const struct index_state *src); +int clean_status_transfer_current_proof_if_same_index( + struct index_state *dst, const struct index_state *src); +int clean_status_transfer_current_proof_if_semantically_same_index( + struct index_state *dst, const struct index_state *src); + +/* + * A canonical main-index source may lend suspended historical state to an + * in-process replacement. The caller must abandon the capture on any unsafe + * mutation, move the original extensions, and transfer before discarding src. + * This never grants a current tracked or untracked proof. + */ +struct clean_status_backoff_transfer *clean_status_capture_backoff_transfer( + struct index_state *src); +int clean_status_backoff_transfer_entry_is_safe( + const struct clean_status_backoff_transfer *transfer, + const struct cache_entry *old, const struct cache_entry *new_entry); +int clean_status_transfer_backoff_history( + struct clean_status_backoff_transfer *transfer, + struct index_state *dst, struct index_state *src); +void clean_status_release_backoff_transfer( + struct clean_status_backoff_transfer *transfer); + +/* + * Historical-only state for a parent-owned, uncommitted main-index write. + * Capture before the first write; record its closed output before hooks. + * The caller releases the checkpoint and any replacement index state. The + * entries-only restore reader borrows fd and grants no current clean proof. + */ +struct clean_status_commit_checkpoint *clean_status_capture_commit_checkpoint( + struct index_state *istate, struct lock_file *lock); +void clean_status_record_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint, + struct index_state *istate, struct lock_file *lock); +/* Seal safe same-path interactive changes before running ordinary hooks. */ +int clean_status_advance_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint, + const struct index_state *current, struct lock_file *lock); +int clean_status_commit_checkpoint_changed( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock); +int clean_status_commit_checkpoint_still_valid( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock); +int clean_status_prepare_commit_checkpoint_restore( + const struct clean_status_commit_checkpoint *checkpoint, + struct lock_file *lock, const struct index_state *current, + struct index_state *replacement, int fd); +void clean_status_release_commit_checkpoint( + struct clean_status_commit_checkpoint *checkpoint); + +void clean_status_release(struct index_state *istate); + +#endif /* CLEAN_STATUS_H */ diff --git a/compat/fsmonitor/fsm-darwin-gcc.h b/compat/fsmonitor/fsm-darwin-gcc.h index 3496e29b3a1f1b..959bc88f8f765a 100644 --- a/compat/fsmonitor/fsm-darwin-gcc.h +++ b/compat/fsmonitor/fsm-darwin-gcc.h @@ -40,9 +40,13 @@ typedef const FSEventStreamRef ConstFSEventStreamRef; typedef unsigned int CFStringEncoding; #define kCFStringEncodingUTF8 0x08000100 +typedef long CFIndex; typedef const struct __CFString *CFStringRef; typedef const struct __CFArray *CFArrayRef; +typedef const struct __CFDictionary *CFDictionaryRef; +typedef const struct __CFNumber *CFNumberRef; typedef const struct __CFRunLoop *CFRunLoopRef; +#define kCFNumberSInt64Type 4 struct FSEventStreamContext { long long version; @@ -51,9 +55,11 @@ struct FSEventStreamContext { typedef struct FSEventStreamContext FSEventStreamContext; typedef unsigned int FSEventStreamEventFlags; +#define kFSEventStreamCreateFlagUseCFTypes 0x01 #define kFSEventStreamCreateFlagNoDefer 0x02 #define kFSEventStreamCreateFlagWatchRoot 0x04 #define kFSEventStreamCreateFlagFileEvents 0x10 +#define kFSEventStreamCreateFlagUseExtendedData 0x40 typedef unsigned long long FSEventStreamEventId; #define kFSEventStreamEventIdSinceNow 0xFFFFFFFFFFFFFFFFULL @@ -74,8 +80,17 @@ FSEventStreamRef FSEventStreamCreate(void *allocator, FSEventStreamCreateFlags flags); CFStringRef CFStringCreateWithCString(void *allocator, const char *string, CFStringEncoding encoding); +CFIndex CFStringGetLength(CFStringRef string); +CFIndex CFStringGetMaximumSizeForEncoding(CFIndex length, + CFStringEncoding encoding); +unsigned char CFStringGetCString(CFStringRef string, char *buffer, + CFIndex buffer_size, + CFStringEncoding encoding); CFArrayRef CFArrayCreate(void *allocator, const void **items, long long count, void *callbacks); +const void *CFArrayGetValueAtIndex(CFArrayRef array, CFIndex index); +const void *CFDictionaryGetValue(CFDictionaryRef dictionary, const void *key); +unsigned char CFNumberGetValue(CFNumberRef number, CFIndex type, void *value); void CFRunLoopRun(void); void CFRunLoopStop(CFRunLoopRef run_loop); CFRunLoopRef CFRunLoopGetCurrent(void); diff --git a/compat/fsmonitor/fsm-listen-darwin.c b/compat/fsmonitor/fsm-listen-darwin.c index 43c3a915a0edfc..f25d7cdd907af9 100644 --- a/compat/fsmonitor/fsm-listen-darwin.c +++ b/compat/fsmonitor/fsm-listen-darwin.c @@ -24,7 +24,7 @@ #endif #include "git-compat-util.h" -#include "fsmonitor-ll.h" +#include "fsmonitor.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" #include "fsmonitor-path-utils.h" @@ -37,6 +37,8 @@ struct fsm_listen_data { CFStringRef cfsr_worktree_path; CFStringRef cfsr_gitdir_path; + CFStringRef cfsr_event_path_key; + CFStringRef cfsr_event_inode_key; CFArrayRef cfar_paths_to_watch; int nr_paths_watching; @@ -138,6 +140,26 @@ static int ef_is_dropped(const FSEventStreamEventFlags ef) ef & kFSEventStreamEventFlagUserDropped); } +static int ef_is_hardlink(const FSEventStreamEventFlags ef) +{ + return ef & (kFSEventStreamEventFlagItemIsHardlink | + kFSEventStreamEventFlagItemIsLastHardlink); +} + +static int ef_ignore_dir_metadata(const FSEventStreamEventFlags ef) +{ + static const FSEventStreamEventFlags required = + kFSEventStreamEventFlagItemIsDir | + kFSEventStreamEventFlagItemInodeMetaMod; + static const FSEventStreamEventFlags allowed = + kFSEventStreamEventFlagItemIsDir | + kFSEventStreamEventFlagItemInodeMetaMod | + kFSEventStreamEventFlagItemCreated | + kFSEventStreamEventFlagItemXattrMod; + + return (ef & required) == required && !(ef & ~allowed); +} + /* * If an `xattr` change is the only reason we received this event, * then silently ignore it. Git doesn't care about xattr's. We @@ -201,28 +223,54 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, { struct fsmonitor_daemon_state *state = ctx; struct fsm_listen_data *data = state->listen_data; - char **paths = (char **)event_paths; + CFArrayRef events = event_paths; struct fsmonitor_batch *batch = NULL; struct string_list cookie_list = STRING_LIST_INIT_DUP; const char *path_k; const char *slash; char *resolved = NULL; struct strbuf tmp = STRBUF_INIT; + struct strbuf event_path = STRBUF_INIT; + enum fsmonitor_path_type path_type; /* * Build a list of all filesystem changes into a private/local * list and without holding any locks. */ for (size_t k = 0; k < num_of_events; k++) { + CFDictionaryRef event = CFArrayGetValueAtIndex(events, k); + CFStringRef path = event ? + CFDictionaryGetValue(event, data->cfsr_event_path_key) : + NULL; + CFNumberRef inode = event ? + CFDictionaryGetValue(event, data->cfsr_event_inode_key) : + NULL; + CFIndex path_size; + int64_t file_id = 0; + /* - * On Mac, we receive an array of absolute paths. + * Extended events retain their inode even when their pathname has + * already been removed by the time this callback runs. */ + if (!path) + goto invalid_event; + path_size = CFStringGetMaximumSizeForEncoding( + CFStringGetLength(path), kCFStringEncodingUTF8); + if (path_size < 0) + goto invalid_event; + strbuf_reset(&event_path); + strbuf_grow(&event_path, path_size + 1); + if (!CFStringGetCString(path, event_path.buf, path_size + 1, + kCFStringEncodingUTF8)) + goto invalid_event; + strbuf_setlen(&event_path, strlen(event_path.buf)); + free(resolved); - resolved = fsmonitor__resolve_alias(paths[k], &state->alias); + resolved = fsmonitor__resolve_alias(event_path.buf, &state->alias); if (resolved) path_k = resolved; else - path_k = paths[k]; + path_k = event_path.buf; /* * If you want to debug FSEvents, log them to GIT_TRACE_FSMONITOR. @@ -290,7 +338,31 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, continue; } - switch (fsmonitor_classify_path_absolute(state, path_k)) { + path_type = fsmonitor_classify_path_absolute(state, path_k); + if (ef_is_hardlink(event_flags[k]) && + path_type == IS_WORKDIR_PATH) { + /* + * The daemon never reads the index. Let an inode-aware client + * invalidate every tracked alias without disturbing unrelated + * ignored hardlinks. Missing inode data must remain fail-closed. + */ + if (trace_pass_fl(&trace_fsmonitor)) + log_flags_set(path_k, event_flags[k]); + if (!batch) + batch = fsmonitor_batch__new(); + if (!inode || !CFNumberGetValue(inode, kCFNumberSInt64Type, + &file_id) || !file_id) { + my_add_path(batch, FSMONITOR_PATH_GLOBAL_INVALIDATE); + } else { + strbuf_reset(&tmp); + strbuf_addf(&tmp, "%s%016"PRIx64, + FSMONITOR_PATH_HARDLINK_INODE_PREFIX, + (uint64_t)file_id); + my_add_path(batch, tmp.buf); + } + } + + switch (path_type) { case IS_INSIDE_DOT_GIT_WITH_COOKIE_PREFIX: case IS_INSIDE_GITDIR_WITH_COOKIE_PREFIX: @@ -330,6 +402,12 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, if (trace_pass_fl(&trace_fsmonitor)) log_flags_set(path_k, event_flags[k]); + if (ef_ignore_dir_metadata(event_flags[k])) { + trace_printf_key(&trace_fsmonitor, + "ignore-dir-metadata: '%s', flags=0x%x", + path_k, event_flags[k]); + break; + } /* * Because of the implicit "binning" (the @@ -342,26 +420,19 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, * know how much to invalidate/refresh. */ - if (event_flags[k] & (kFSEventStreamEventFlagItemIsFile | kFSEventStreamEventFlagItemIsSymlink)) { - const char *rel = path_k + - state->path_worktree_watch.len + 1; - + fsmonitor_format_worktree_paths( + &tmp, path_k, state->path_worktree_watch.len, + !!(event_flags[k] & + (kFSEventStreamEventFlagItemIsFile | + kFSEventStreamEventFlagItemIsSymlink)), + !!(event_flags[k] & + kFSEventStreamEventFlagItemIsDir)); + for (const char *relative = tmp.buf; + relative < tmp.buf + tmp.len; + relative += strlen(relative) + 1) { if (!batch) batch = fsmonitor_batch__new(); - my_add_path(batch, rel); - } - - if (event_flags[k] & kFSEventStreamEventFlagItemIsDir) { - const char *rel = path_k + - state->path_worktree_watch.len + 1; - - strbuf_reset(&tmp); - strbuf_addstr(&tmp, rel); - strbuf_addch(&tmp, '/'); - - if (!batch) - batch = fsmonitor_batch__new(); - my_add_path(batch, tmp.buf); + my_add_path(batch, relative); } break; @@ -372,18 +443,28 @@ static void fsevent_callback(ConstFSEventStreamRef streamRef UNUSED, "ignoring '%s'", path_k); break; } + continue; + +invalid_event: + fsmonitor_force_resync(state); + fsmonitor_batch__free_list(batch); + string_list_clear(&cookie_list, 0); + batch = NULL; } free(resolved); fsmonitor_publish(state, batch, &cookie_list); string_list_clear(&cookie_list, 0); strbuf_release(&tmp); + strbuf_release(&event_path); return; force_shutdown: free(resolved); fsmonitor_batch__free_list(batch); string_list_clear(&cookie_list, 0); + strbuf_release(&tmp); + strbuf_release(&event_path); pthread_mutex_lock(&data->dq_lock); data->shutdown_style = FORCE_SHUTDOWN; @@ -415,7 +496,9 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) { FSEventStreamCreateFlags flags = kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagWatchRoot | - kFSEventStreamCreateFlagFileEvents; + kFSEventStreamCreateFlagFileEvents | + kFSEventStreamCreateFlagUseCFTypes | + kFSEventStreamCreateFlagUseExtendedData; FSEventStreamContext ctx = { 0, state, @@ -429,6 +512,13 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) CALLOC_ARRAY(data, 1); state->listen_data = data; + data->cfsr_event_path_key = CFStringCreateWithCString( + NULL, "path", kCFStringEncodingUTF8); + data->cfsr_event_inode_key = CFStringCreateWithCString( + NULL, "fileID", kCFStringEncodingUTF8); + if (!data->cfsr_event_path_key || !data->cfsr_event_inode_key) + goto failed; + data->cfsr_worktree_path = CFStringCreateWithCString( NULL, state->path_worktree_watch.buf, kCFStringEncodingUTF8); dir_array[data->nr_paths_watching++] = data->cfsr_worktree_path; diff --git a/compat/fsmonitor/fsm-listen-linux.c b/compat/fsmonitor/fsm-listen-linux.c index e3dca14b620ee3..6181dcba51472d 100644 --- a/compat/fsmonitor/fsm-listen-linux.c +++ b/compat/fsmonitor/fsm-listen-linux.c @@ -1,5 +1,6 @@ #include "git-compat-util.h" #include "dir.h" +#include "fsmonitor-ipc.h" #include "fsmonitor-ll.h" #include "fsm-listen.h" #include "fsmonitor--daemon.h" @@ -42,6 +43,7 @@ struct rename_entry { struct fsm_listen_data { int fd_inotify; + const char *worktree_identity; enum shutdown_reason shutdown; struct hashmap watches; struct hashmap renames; @@ -102,9 +104,12 @@ static int add_watch(const char *path, struct fsm_listen_data *data) return 0; /* directory was deleted or is not a directory */ if (errno == EEXIST) return 0; /* watch already exists, no action needed */ - if (errno == ENOSPC) + if (errno == ENOSPC) { + fsmonitor_ipc__record_watch_limit_failure( + data->worktree_identity); return error(_("inotify watch limit reached; " "increase fs.inotify.max_user_watches")); + } return error_errno(_("inotify_add_watch('%s') failed"), interned); } @@ -409,6 +414,7 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) state->listen_data = data; state->listen_error_code = -1; data->fd_inotify = -1; + data->worktree_identity = state->worktree_identity.buf; data->shutdown = SHUTDOWN_ERROR; fd = inotify_init1(O_NONBLOCK); @@ -435,6 +441,7 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) } if (!ret) { + fsmonitor_ipc__clear_watch_limit_failure(); state->listen_error_code = 0; data->shutdown = SHUTDOWN_CONTINUE; } @@ -445,11 +452,6 @@ int fsm_listen__ctor(struct fsmonitor_daemon_state *state) void fsm_listen__dtor(struct fsmonitor_daemon_state *state) { struct fsm_listen_data *data; - struct hashmap_iter iter; - struct watch_entry *w; - struct watch_entry **to_remove; - size_t nr_to_remove = 0, alloc_to_remove = 0; - size_t i; int fd; if (!state || !state->listen_data) @@ -459,31 +461,17 @@ void fsm_listen__dtor(struct fsmonitor_daemon_state *state) fd = data->fd_inotify; /* - * Collect all entries first, then remove them. - * We can't modify the hashmap while iterating over it. + * Closing the inotify instance releases every kernel watch at once. + * The forward and reverse maps own separate watch_entry allocations. */ - to_remove = NULL; - hashmap_for_each_entry(&data->watches, &iter, w, ent) { - ALLOC_GROW(to_remove, nr_to_remove + 1, alloc_to_remove); - to_remove[nr_to_remove++] = w; - } - - for (i = 0; i < nr_to_remove; i++) { - to_remove[i]->cookie = 0; /* ignore any pending renames */ - remove_watch(to_remove[i], data); - } - free(to_remove); - - hashmap_clear(&data->watches); - - hashmap_clear(&data->revwatches); /* remove_watch freed the entries */ - + data->fd_inotify = -1; + if (fd >= 0 && close(fd) < 0) + error_errno(_("closing inotify file descriptor failed")); + hashmap_clear_and_free(&data->watches, struct watch_entry, ent); + hashmap_clear_and_free(&data->revwatches, struct watch_entry, ent); hashmap_clear_and_free(&data->renames, struct rename_entry, ent); FREE_AND_NULL(state->listen_data); - - if (fd >= 0 && (close(fd) < 0)) - error_errno(_("closing inotify file descriptor failed")); } void fsm_listen__stop_async(struct fsmonitor_daemon_state *state) diff --git a/compat/precompose_utf8.c b/compat/precompose_utf8.c index 8077f6235b0cae..2be9f7577f9519 100644 --- a/compat/precompose_utf8.c +++ b/compat/precompose_utf8.c @@ -72,19 +72,32 @@ void probe_utf8_pathname_composition(void) strbuf_release(&path); } -const char *precompose_string_if_needed(const char *in) +void repo_precompose_utf8_prepare(struct repository *repo) +{ + struct repo_config_values *cfg = repo_config_values(repo); + + if (cfg->precomposed_unicode < 0 && + repo_config_get_bool(repo, "core.precomposeunicode", + &cfg->precomposed_unicode)) + cfg->precomposed_unicode = 0; +} + +const char *repo_precompose_string_if_needed(struct repository *repo, + const char *in) { size_t inlen; size_t outlen; - struct repo_config_values *cfg = repo_config_values(the_repository); + struct repo_config_values *cfg = repo_config_values(repo); if (!in) return NULL; if (has_non_ascii(in, (size_t)-1, &inlen)) { iconv_t ic_prec; char *out; + if (cfg->precomposed_unicode < 0) - repo_config_get_bool(the_repository, "core.precomposeunicode", &cfg->precomposed_unicode); + repo_config_get_bool(repo, "core.precomposeunicode", + &cfg->precomposed_unicode); if (cfg->precomposed_unicode != 1) return in; ic_prec = iconv_open(repo_encoding, path_encoding); @@ -104,6 +117,11 @@ const char *precompose_string_if_needed(const char *in) return in; } +const char *precompose_string_if_needed(const char *in) +{ + return repo_precompose_string_if_needed(the_repository, in); +} + const char *precompose_argv_prefix(int argc, const char **argv, const char *prefix) { int i = 0; diff --git a/compat/precompose_utf8.h b/compat/precompose_utf8.h index c7c3cc211e5031..6ec1fa973b7db9 100644 --- a/compat/precompose_utf8.h +++ b/compat/precompose_utf8.h @@ -29,8 +29,13 @@ typedef struct { struct dirent_prec_psx *dirent_nfc; } PREC_DIR; +struct repository; + const char *precompose_argv_prefix(int argc, const char **argv, const char *prefix); const char *precompose_string_if_needed(const char *in); +const char *repo_precompose_string_if_needed(struct repository *repo, + const char *in); +void repo_precompose_utf8_prepare(struct repository *repo); void probe_utf8_pathname_composition(void); PREC_DIR *precompose_utf8_opendir(const char *dirname); diff --git a/compat/preload-index/bulk-darwin-root.c b/compat/preload-index/bulk-darwin-root.c new file mode 100644 index 00000000000000..f12f730761c99c --- /dev/null +++ b/compat/preload-index/bulk-darwin-root.c @@ -0,0 +1,100 @@ +#include "git-compat-util.h" + +#include + +#include "compat/preload-index/bulk-darwin.h" +#include "path-namespace.h" +#include "repository.h" +#include "preload-index-bulk.h" + +static int same_fsid(const fsid_t *a, const fsid_t *b) +{ + return !memcmp(a, b, sizeof(*a)); +} + +static int stat_local_apfs(int fd, struct stat *st, struct statfs *fs) +{ + if (fstat(fd, st) || fstatfs(fd, fs)) + return -1; + if (!S_ISDIR(st->st_mode) || !(fs->f_flags & MNT_LOCAL) || + strcmp(fs->f_fstypename, "apfs")) { + errno = EXDEV; + return -1; + } + return 0; +} + +int preload_bulk_darwin_fd_on_root_mount(struct preload_bulk_scan *scan, + int fd, struct stat *st_out) +{ + struct preload_bulk_darwin_data *data = scan->platform_data; + struct statfs fs; + struct stat st; + + if (stat_local_apfs(fd, &st, &fs)) + return -1; + if (st.st_dev != data->root_stat.st_dev || + !same_fsid(&fs.f_fsid, &data->root_fsid)) { + errno = EXDEV; + return -1; + } + if (st_out) + *st_out = st; + return 0; +} + +const char *preload_bulk_darwin_open_root(struct preload_bulk_scan *scan) +{ + struct preload_bulk_darwin_data *data; + struct statfs fs; + struct stat st; + + CALLOC_ARRAY(data, 1); + scan->platform_data = data; + scan->root_fd = open(repo_get_work_tree(scan->repo), + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (scan->root_fd < 0 || + stat_local_apfs(scan->root_fd, &st, &fs)) + return "unsupported-filesystem"; + return NULL; +} + +const char *preload_bulk_darwin_snapshot_root(struct preload_bulk_scan *scan) +{ + struct preload_bulk_darwin_data *data = scan->platform_data; + struct statfs fs; + struct stat st; + + if (stat_local_apfs(scan->root_fd, &st, &fs)) + return "unsupported-filesystem"; + data->root_stat = st; + data->root_fsid = fs.f_fsid; + return NULL; +} + +const char *preload_bulk_darwin_validate_root(struct preload_bulk_scan *scan) +{ + struct preload_bulk_darwin_data *data = scan->platform_data; + struct stat root_after; + int fd = open(repo_get_work_tree(scan->repo), + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + + if (fd < 0 || + preload_bulk_darwin_fd_on_root_mount(scan, fd, &root_after) || + !path_namespace_stat_equal(&data->root_stat, &root_after)) { + if (fd >= 0) + close(fd); + return "namespace-race"; + } + close(fd); + return NULL; +} + +void preload_bulk_darwin_release(struct preload_bulk_scan *scan) +{ + if (scan->root_fd >= 0) { + close(scan->root_fd); + scan->root_fd = -1; + } + FREE_AND_NULL(scan->platform_data); +} diff --git a/compat/preload-index/bulk-darwin.c b/compat/preload-index/bulk-darwin.c new file mode 100644 index 00000000000000..8aef3a12a61413 --- /dev/null +++ b/compat/preload-index/bulk-darwin.c @@ -0,0 +1,620 @@ +#include "git-compat-util.h" + +#include +#include +#include + +#include "compat/precompose_utf8.h" +#include "compat/preload-index/bulk-darwin.h" +#include "dir.h" +#include "path-namespace.h" +#include "preload-index-bulk.h" + +#ifndef SF_FIRMLINK +#define SF_FIRMLINK 0x00800000 +#endif + +#define PRELOAD_INDEX_BULK_BUFFER_SIZE (1024 * 1024) + +static const attrgroup_t required_common = + ATTR_CMN_RETURNED_ATTRS | ATTR_CMN_ERROR | ATTR_CMN_NAME | + ATTR_CMN_DEVID | ATTR_CMN_OBJTYPE | + ATTR_CMN_CRTIME | ATTR_CMN_MODTIME | ATTR_CMN_CHGTIME | + ATTR_CMN_OWNERID | ATTR_CMN_GRPID | ATTR_CMN_ACCESSMASK | + ATTR_CMN_FLAGS | ATTR_CMN_FILEID; +static const attrgroup_t required_dir = ATTR_DIR_MOUNTSTATUS; +static const attrgroup_t required_file = + ATTR_FILE_LINKCOUNT | ATTR_FILE_DATALENGTH; + +static int valid_component(const char *component, size_t len) +{ + return len && + !(len == 1 && component[0] == '.') && + !(len == 2 && component[0] == '.' && component[1] == '.'); +} + +static int valid_relative_path(const char *path) +{ + const char *component = path; + + if (!strcmp(path, ".")) + return 1; + if (!*path || *path == '/') + return 0; + for (;;) { + const char *slash = strchr(component, '/'); + size_t len = slash ? (size_t)(slash - component) : + strlen(component); + + if (!valid_component(component, len)) + return 0; + if (!slash) + return 1; + component = slash + 1; + } +} + +static int preload_bulk_darwin_open_dir_at( + struct preload_bulk_worker *worker UNUSED, + int parent_fd, const char *name) +{ + if (!valid_component(name, strlen(name)) || strchr(name, '/')) { + errno = EINVAL; + return -1; + } + return openat(parent_fd, name, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); +} + +int preload_bulk_darwin_supports_nofollow_any(void) +{ +#ifdef O_NOFOLLOW_ANY + struct utsname uts; + char *end; + unsigned long major; + + /* + * O_NOFOLLOW_ANY arrived in Darwin 20. Older kernels accept the + * same bit as O_ALERT without enforcing no-follow semantics. + */ + if (uname(&uts) || !isdigit((unsigned char)uts.release[0])) + return 0; + errno = 0; + major = strtoul(uts.release, &end, 10); + return !errno && end != uts.release && *end == '.' && major >= 20; +#else + return 0; +#endif +} + +static int preload_bulk_darwin_open_relative(struct preload_bulk_scan *scan, + const char *path) +{ + if (!valid_relative_path(path)) { + errno = EINVAL; + return -1; + } + +#ifdef O_NOFOLLOW_ANY + return openat(scan->root_fd, path, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW_ANY | O_CLOEXEC); +#else + errno = ENOTSUP; + return -1; +#endif +} + +static mode_t vnode_mode(fsobj_type_t type) +{ + switch (type) { + case VREG: + return S_IFREG; + case VLNK: + return S_IFLNK; + default: + return 0; + } +} + +static int fill_file_stat(struct stat *st, dev_t dev, uint64_t fileid, + fsobj_type_t type, struct timespec mtime, + struct timespec ctime, uid_t uid, gid_t gid, + uint32_t access, uint32_t linkcount, off_t size) +{ + mode_t mode = vnode_mode(type); + + if (!mode || size < 0 || + ((access & S_IFMT) && (access & S_IFMT) != mode) || + (access & ~(S_IFMT | 07777))) + return -1; + memset(st, 0, sizeof(*st)); + st->st_dev = dev; + st->st_ino = fileid; + st->st_mode = mode | (access & 07777); + st->st_uid = uid; + st->st_gid = gid; + st->st_nlink = linkcount; + st->st_size = size; + st->st_mtimespec = mtime; + st->st_ctimespec = ctime; + return 0; +} + +struct preload_bulk_darwin_entry { + const char *name; + uint32_t record_len; + dev_t dev; + fsobj_type_t type; + struct timespec birthtime; + struct timespec mtime; + struct timespec ctime; + uid_t uid; + gid_t gid; + uint32_t access; + uint32_t flags; + uint32_t linkcount; + uint32_t mountstatus; + uint64_t fileid; + off_t size; +}; + +static int decode_entry(const char *record, size_t remaining, + struct preload_bulk_darwin_entry *entry) +{ + uint32_t entry_error = 0; + attribute_set_t returned; + attrreference_t name_ref; + const char *p, *end, *name_ref_at; + size_t name_ref_offset, name_offset, name_remaining; + + if (remaining < sizeof(entry->record_len) + sizeof(returned)) + return -1; + memcpy(&entry->record_len, record, sizeof(entry->record_len)); + if ((entry->record_len % sizeof(uint64_t)) || + entry->record_len < sizeof(entry->record_len) + sizeof(returned) || + entry->record_len > remaining) + return -1; + + p = record + sizeof(entry->record_len); + end = record + entry->record_len; + memcpy(&returned, p, sizeof(returned)); + p += sizeof(returned); + if (returned.commonattr != required_common || + returned.volattr || returned.forkattr) + return -1; + +#define TAKE_ATTR(value) do { \ + if ((size_t)(end - p) < sizeof(value)) \ + return -1; \ + memcpy(&(value), p, sizeof(value)); \ + p += sizeof(value); \ +} while (0) + TAKE_ATTR(entry_error); + if (entry_error) + return -1; + name_ref_at = p; + TAKE_ATTR(name_ref); + TAKE_ATTR(entry->dev); + TAKE_ATTR(entry->type); + TAKE_ATTR(entry->birthtime); + TAKE_ATTR(entry->mtime); + TAKE_ATTR(entry->ctime); + TAKE_ATTR(entry->uid); + TAKE_ATTR(entry->gid); + TAKE_ATTR(entry->access); + TAKE_ATTR(entry->flags); + TAKE_ATTR(entry->fileid); + + if (entry->type == VDIR) { + if (returned.dirattr != required_dir || + returned.fileattr) + return -1; + TAKE_ATTR(entry->mountstatus); + } else { + if (returned.dirattr || + (returned.fileattr & ~required_file)) + return -1; + TAKE_ATTR(entry->linkcount); + TAKE_ATTR(entry->size); + if ((entry->type == VREG || entry->type == VLNK) && + returned.fileattr != required_file) + return -1; + } +#undef TAKE_ATTR + + if (name_ref.attr_dataoffset < 0 || + (name_ref.attr_dataoffset % (int32_t)sizeof(uint32_t))) + return -1; + name_ref_offset = name_ref_at - record; + if ((uint32_t)name_ref.attr_dataoffset > + entry->record_len - name_ref_offset) + return -1; + name_offset = name_ref_offset + name_ref.attr_dataoffset; + name_remaining = entry->record_len - name_offset; + entry->name = record + name_offset; + if (!name_ref.attr_length || + name_ref.attr_length > name_remaining || + entry->name < p) + return -1; + if (entry->name[name_ref.attr_length - 1] || + memchr(entry->name, '\0', name_ref.attr_length - 1) || + !valid_component(entry->name, name_ref.attr_length - 1) || + memchr(entry->name, '/', name_ref.attr_length - 1)) + return -1; + return 0; +} + +int preload_bulk_darwin_decode_record(const char *record, size_t len) +{ + struct preload_bulk_darwin_entry entry; + + return decode_entry(record, len, &entry); +} + +static struct preload_bulk_dir_identity directory_identity( + const struct stat *st) +{ + struct preload_bulk_dir_identity result = { + .stat = *st, + .complete = 1, + }; + + return result; +} + +static int directory_identity_matches( + const struct preload_bulk_dir_identity *before, + const struct stat *after) +{ + if (before->complete) + return path_namespace_stat_equal(&before->stat, after); + return S_ISDIR(after->st_mode) && + before->stat.st_dev == after->st_dev && + before->stat.st_ino == after->st_ino && + before->stat.st_birthtimespec.tv_sec == + after->st_birthtimespec.tv_sec && + before->stat.st_birthtimespec.tv_nsec == + after->st_birthtimespec.tv_nsec && + before->stat.st_mtimespec.tv_sec == after->st_mtimespec.tv_sec && + before->stat.st_mtimespec.tv_nsec == + after->st_mtimespec.tv_nsec && + before->stat.st_ctimespec.tv_sec == after->st_ctimespec.tv_sec && + before->stat.st_ctimespec.tv_nsec == + after->st_ctimespec.tv_nsec; +} + +static int enumerate_directory(struct preload_bulk_worker *worker, + const struct preload_bulk_task *task, int fd, + const struct preload_bulk_dir_identity *parent_identity) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_darwin_data *data = scan->platform_data; + struct attrlist attrs = { 0 }; + char *buf = worker->buffer; + size_t path_prefix_len; + + if (!buf) { + buf = xmalloc(PRELOAD_INDEX_BULK_BUFFER_SIZE); + worker->buffer = buf; + } + + attrs.bitmapcount = ATTR_BIT_MAP_COUNT; + attrs.commonattr = required_common; + attrs.dirattr = required_dir; + attrs.fileattr = required_file; + worker->dirs++; + strbuf_reset(&worker->path); + if (strcmp(task->path, ".")) { + strbuf_addstr(&worker->path, task->path); + strbuf_addch(&worker->path, '/'); + } + path_prefix_len = worker->path.len; + + for (;;) { + int nr = getattrlistbulk(fd, &attrs, buf, + PRELOAD_INDEX_BULK_BUFFER_SIZE, + FSOPT_NOFOLLOW | + FSOPT_PACK_INVAL_ATTRS); + char *record = buf; + + worker->bulk_calls++; + if (nr < 0) + return -1; + if (!nr) + return 0; + + for (int i = 0; i < nr; i++) { + struct preload_bulk_darwin_entry entry; + struct stat st; + const char *path_name; + size_t remaining; + int pos; + + if (scan->collect_untracked && + preload_bulk_untracked_root_is_visible( + worker, task->untracked_root)) + return 0; + remaining = buf + PRELOAD_INDEX_BULK_BUFFER_SIZE - record; + if (decode_entry(record, remaining, &entry)) + goto malformed; + worker->entries++; + + /* + * The caller prepares the repository's Unicode policy + * before starting workers, so this is read-only here. + */ + path_name = repo_precompose_string_if_needed(scan->repo, + entry.name); + strbuf_setlen(&worker->path, path_prefix_len); + strbuf_addstr(&worker->path, path_name); + if (path_name != entry.name) + free((char *)path_name); + if (scan->collect_untracked) { + if (!strcmp(task->path, ".") && + !fspathcmp(worker->path.buf, ".git")) + goto next_record; + if (strcmp(task->path, ".") && + !fspathcmp(entry.name, ".git")) { + preload_bulk_invalidate_untracked( + worker); + goto next_record; + } + } + + pos = preload_bulk_index_position(scan, worker->path.buf, + worker->path.len); + if (entry.type == VDIR) { + struct preload_bulk_dir_identity child_identity = { + .stat = { + .st_dev = entry.dev, + .st_ino = entry.fileid, + .st_birthtimespec = + entry.birthtime, + .st_mtimespec = entry.mtime, + .st_ctimespec = entry.ctime, + }, + }; + struct preload_bulk_untracked_root *untracked_root = + task->untracked_root; + int has_tracked_descendants; + + if (pos >= 0) { + if (scan->collect_untracked && + preload_bulk_index_entry_is_gitlink( + scan, pos)) + goto next_record; + preload_bulk_record_tracked_fallback( + worker, pos); + goto next_record; + } + has_tracked_descendants = + preload_bulk_index_pos_has_tracked_descendants( + scan, worker->path.buf, + worker->path.len, pos); + if (!has_tracked_descendants && + preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, + worker->path.len)) { + if (scan->collect_untracked) + preload_bulk_invalidate_untracked( + worker); + goto next_record; + } + if (!has_tracked_descendants) { + if (!scan->collect_untracked || + preload_bulk_untracked_is_invalid( + worker)) + goto next_record; + if (preload_bulk_untracked_root_is_visible( + worker, untracked_root)) + goto next_record; + if (preload_bulk_path_is_excluded( + worker, worker->path.buf, + DT_DIR)) + goto next_record; + if (!untracked_root) + untracked_root = + preload_bulk_untracked_root_new( + worker, + worker->path.buf, + worker->path.len); + } + if (((entry.access & S_IFMT) && + (entry.access & S_IFMT) != S_IFDIR) || + (entry.access & ~(S_IFMT | 07777))) + goto malformed_record; + if (entry.dev != data->root_stat.st_dev || + entry.mountstatus || + (entry.flags & SF_FIRMLINK)) { + preload_bulk_record_tracked_descendants_fallback( + worker, worker->path.buf, + worker->path.len); + if (scan->collect_untracked) + preload_bulk_invalidate_untracked( + worker); + goto next_record; + } + preload_bulk_schedule_directory( + worker, fd, parent_identity, + &child_identity, untracked_root, + entry.name, + worker->path.buf, + worker->path.len); + goto next_record; + } + + if (pos < 0) { + int found_alias = + preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, + worker->path.len); + + if (scan->collect_untracked && + !preload_bulk_untracked_is_invalid( + worker)) { + int dtype; + + if (found_alias) { + preload_bulk_invalidate_untracked( + worker); + goto next_record; + } + if (entry.type == VREG) + dtype = DT_REG; + else if (entry.type == VLNK) + dtype = DT_LNK; + else + goto next_record; + if (!preload_bulk_path_is_excluded( + worker, worker->path.buf, + dtype)) + preload_bulk_record_untracked( + worker, + task->untracked_root, + worker->path.buf); + } + goto next_record; + } + if (entry.dev != data->root_stat.st_dev) { + preload_bulk_record_tracked_fallback( + worker, pos); + goto next_record; + } + if (entry.type != VREG && entry.type != VLNK) { + preload_bulk_record_tracked_fallback( + worker, pos); + goto next_record; + } + if (entry.linkcount != 1) { + preload_bulk_record_tracked_fallback( + worker, pos); + goto next_record; + } + if (fill_file_stat(&st, entry.dev, entry.fileid, + entry.type, entry.mtime, entry.ctime, + entry.uid, entry.gid, entry.access, + entry.linkcount, entry.size)) + goto malformed_record; + preload_bulk_record_tracked( + worker, pos, fd, entry.name, &st, 0); + +next_record: + record += entry.record_len; + continue; + +malformed_record: + worker->malformed++; + goto next_record; + } + } + +malformed: + worker->malformed++; + return -1; +} + +static int scan_directory(struct preload_bulk_worker *worker, + struct preload_bulk_task *task) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_dir_identity before_identity; + struct stat before, after; + size_t path_len; + int fd = task->fd; + int ret = -1; + + if (fd < 0) + fd = preload_bulk_darwin_open_relative(scan, task->path); + if (fd < 0) + goto out; + if (preload_bulk_test_barrier(scan, task->path)) + goto out; + if (preload_bulk_darwin_fd_on_root_mount(scan, fd, &before)) { + if (errno != EXDEV) + goto out; + path_len = strlen(task->path); + preload_bulk_record_tracked_descendants_fallback( + worker, task->path, path_len); + if (scan->collect_untracked) + preload_bulk_invalidate_untracked(worker); + ret = 0; + goto out; + } + /* + * A child may have been replaced after its parent returned the bulk + * record, or while this task waited in the queue. + */ + if (task->has_child_identity && + !directory_identity_matches(&task->child_identity, &before)) { + worker->changed_dirs++; + ret = 0; + goto out; + } + before_identity = directory_identity(&before); + if ((!scan->collect_untracked || + !preload_bulk_untracked_root_is_visible( + worker, task->untracked_root)) && + enumerate_directory(worker, task, fd, &before_identity)) + goto out; + if (fstat(fd, &after)) + goto out; + if (!directory_identity_matches(&before_identity, &after)) + worker->changed_dirs++; + ret = 0; + +out: + if (task->has_parent_identity) { + struct stat parent_after; + int parent_changed = fd < 0; + + /* + * Resolve ".." through the child descriptor, not the worktree + * path, so a rename cannot redirect this parent check. + */ + if (!parent_changed) + parent_changed = fstatat(fd, "..", &parent_after, + AT_SYMLINK_NOFOLLOW); + if (parent_changed || + !directory_identity_matches(&task->parent_identity, + &parent_after)) + worker->changed_dirs++; + } + if (fd >= 0) + close(fd); + return ret; +} + +static const char *start_scan(struct preload_bulk_scan *scan) +{ + const char *error; + + repo_precompose_utf8_prepare(scan->repo); + error = preload_bulk_darwin_open_root(scan); + if (error) + return error; + return preload_bulk_darwin_snapshot_root(scan); +} + +static const char *finish_scan(struct preload_bulk_scan *scan) +{ + return preload_bulk_darwin_validate_root(scan); +} + +static const struct preload_bulk_backend darwin_backend = { + .collects_untracked = 1, + .start = start_scan, + .finish = finish_scan, + .release = preload_bulk_darwin_release, + .open_proof_parent = preload_bulk_darwin_open_relative, + .open_dir_at = preload_bulk_darwin_open_dir_at, + .scan_directory = scan_directory, +}; + +const struct preload_bulk_backend *preload_bulk_platform_backend(void) +{ +#ifdef O_NOFOLLOW_ANY + if (preload_bulk_darwin_supports_nofollow_any()) + return &darwin_backend; +#endif + return NULL; +} diff --git a/compat/preload-index/bulk-darwin.h b/compat/preload-index/bulk-darwin.h new file mode 100644 index 00000000000000..7c5c45ee947651 --- /dev/null +++ b/compat/preload-index/bulk-darwin.h @@ -0,0 +1,29 @@ +#ifndef PRELOAD_INDEX_BULK_DARWIN_H +#define PRELOAD_INDEX_BULK_DARWIN_H + +#ifdef __APPLE__ + +#include + +struct preload_bulk_scan; + +struct preload_bulk_darwin_data { + struct stat root_stat; + fsid_t root_fsid; +}; + +int preload_bulk_darwin_supports_nofollow_any(void); +/* + * Exposed so that tests can validate kernel-supplied records directly. + */ +int preload_bulk_darwin_decode_record(const char *record, size_t len); +int preload_bulk_darwin_fd_on_root_mount(struct preload_bulk_scan *scan, + int fd, struct stat *st_out); +const char *preload_bulk_darwin_open_root(struct preload_bulk_scan *scan); +const char *preload_bulk_darwin_snapshot_root(struct preload_bulk_scan *scan); +const char *preload_bulk_darwin_validate_root(struct preload_bulk_scan *scan); +void preload_bulk_darwin_release(struct preload_bulk_scan *scan); + +#endif /* __APPLE__ */ + +#endif /* PRELOAD_INDEX_BULK_DARWIN_H */ diff --git a/compat/preload-index/bulk-linux-entry.c b/compat/preload-index/bulk-linux-entry.c new file mode 100644 index 00000000000000..02cb27882bece3 --- /dev/null +++ b/compat/preload-index/bulk-linux-entry.c @@ -0,0 +1,268 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include +#include + +#include "compat/preload-index/bulk-linux.h" +#include "dir.h" +#include "preload-index-bulk.h" + +#define PRELOAD_INDEX_BULK_LINUX_BUFFER_SIZE (1024 * 1024) + +struct preload_linux_dirent64 { + uint64_t ino; + int64_t off; + uint16_t reclen; + uint8_t type; + char name[FLEX_ARRAY]; +}; + +#if defined(SYS_getdents64) && defined(SYS_statx) + +static void record_foreign_entry(struct preload_bulk_worker *worker, + const char *path, size_t path_len, + int pos, mode_t mode) +{ + if (pos >= 0) + preload_bulk_record_tracked_fallback(worker, pos); + if (S_ISDIR(mode)) + preload_bulk_record_tracked_descendants_fallback( + worker, path, path_len); + if (worker->scan->collect_untracked) + preload_bulk_invalidate_untracked(worker); +} + +static void handle_directory( + struct preload_bulk_worker *worker, + const struct preload_bulk_task *task, int fd, + const struct preload_bulk_dir_identity *parent_identity, + const char *name, int pos) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_untracked_root *untracked_root = + task->untracked_root; + int has_tracked_descendants; + + if (pos >= 0) { + if (scan->collect_untracked && + preload_bulk_index_entry_is_gitlink(scan, pos)) + return; + preload_bulk_record_tracked_fallback(worker, pos); + return; + } + has_tracked_descendants = + preload_bulk_index_pos_has_tracked_descendants( + scan, worker->path.buf, worker->path.len, pos); + if (!has_tracked_descendants && + preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, worker->path.len)) { + if (scan->collect_untracked) + preload_bulk_invalidate_untracked(worker); + return; + } + if (!has_tracked_descendants) { + if (!scan->collect_untracked || + preload_bulk_untracked_is_invalid(worker) || + preload_bulk_untracked_root_is_visible( + worker, untracked_root) || + preload_bulk_path_is_excluded( + worker, worker->path.buf, DT_DIR)) + return; + if (!untracked_root) + untracked_root = preload_bulk_untracked_root_new( + worker, worker->path.buf, worker->path.len); + } + preload_bulk_schedule_directory( + worker, fd, parent_identity, NULL, untracked_root, + name, worker->path.buf, worker->path.len); +} + +static void record_untracked(struct preload_bulk_worker *worker, + const struct preload_bulk_task *task, + int dtype) +{ + struct preload_bulk_scan *scan = worker->scan; + + if (!scan->collect_untracked || + preload_bulk_untracked_is_invalid(worker)) + return; + if (preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, worker->path.len)) { + preload_bulk_invalidate_untracked(worker); + return; + } + if (!preload_bulk_path_is_excluded( + worker, worker->path.buf, dtype)) + preload_bulk_record_untracked( + worker, task->untracked_root, worker->path.buf); +} + +int preload_bulk_linux_enumerate( + struct preload_bulk_worker *worker, + struct preload_bulk_task *task, int fd, + const struct preload_bulk_dir_identity *parent_identity) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_linux_data *data = scan->platform_data; + char *buf = worker->buffer; + size_t path_prefix_len; + + if (!buf) { + buf = xmalloc(PRELOAD_INDEX_BULK_LINUX_BUFFER_SIZE); + worker->buffer = buf; + } + worker->dirs++; + strbuf_reset(&worker->path); + if (strcmp(task->path, ".")) { + strbuf_addstr(&worker->path, task->path); + strbuf_addch(&worker->path, '/'); + } + path_prefix_len = worker->path.len; + + for (;;) { + long bytes = syscall(SYS_getdents64, fd, buf, + PRELOAD_INDEX_BULK_LINUX_BUFFER_SIZE); + size_t offset = 0; + + worker->bulk_calls++; + if (bytes < 0) + return -1; + if (!bytes) + return 0; + while (offset < (size_t)bytes) { + struct preload_linux_dirent64 *de = + (void *)(buf + offset); + size_t minimum = + offsetof(struct preload_linux_dirent64, name) + 1; + size_t name_space; + struct preload_linux_statx stx; + struct stat st; + char *nul; + unsigned char dtype; + int has_tracked_descendants = 0, pos; + + if (scan->collect_untracked && + preload_bulk_untracked_root_is_visible( + worker, task->untracked_root)) + return 0; + if ((size_t)bytes - offset < minimum || + de->reclen < minimum || + de->reclen > (size_t)bytes - offset) + goto malformed; + name_space = de->reclen - + offsetof(struct preload_linux_dirent64, name); + nul = memchr(de->name, '\0', name_space); + if (!nul || nul == de->name || + memchr(de->name, '/', nul - de->name)) + goto malformed; + offset += de->reclen; + if (is_dot_or_dotdot(de->name)) + continue; + worker->entries++; + if (!fspathcmp(de->name, ".git")) { + if (strcmp(task->path, ".") && + scan->collect_untracked) + preload_bulk_invalidate_untracked( + worker); + continue; + } + + strbuf_setlen(&worker->path, path_prefix_len); + strbuf_addstr(&worker->path, de->name); + if (worker->path.len > INT_MAX) + goto malformed; + pos = preload_bulk_index_position( + scan, worker->path.buf, worker->path.len); + dtype = de->type; + if (data->test_dirent_path && + !strcmp(data->test_dirent_path, worker->path.buf)) + dtype = data->test_dirent_type; + + /* + * Exact tracked paths always reach statx. A directory + * which may contain tracked descendants can be + * scheduled directly: the O_DIRECTORY open and + * descriptor statx remain authoritative. + * + * A hint cannot classify a wholly untracked entry: + * file-versus-directory changes exclude matching and + * result shape. Force those entries through statx before + * taking either shortcut. + */ + if (pos < 0 && dtype != DT_UNKNOWN) + has_tracked_descendants = + preload_bulk_index_pos_has_tracked_descendants( + scan, worker->path.buf, + worker->path.len, pos); + if (scan->collect_untracked && pos < 0 && + !has_tracked_descendants) + dtype = DT_UNKNOWN; + if (dtype == DT_DIR && pos < 0) { + handle_directory(worker, task, fd, + parent_identity, + de->name, pos); + continue; + } + if (pos < 0 && !has_tracked_descendants && + (dtype == DT_REG || dtype == DT_LNK)) { + record_untracked( + worker, task, + dtype == DT_LNK ? DT_LNK : DT_REG); + continue; + } + if (pos < 0 && !has_tracked_descendants && + dtype != DT_UNKNOWN) { + preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, + worker->path.len); + continue; + } + if (preload_bulk_linux_entry_stat( + worker, fd, de->name, &stx, &st)) { + if (errno == EXDEV) { + record_foreign_entry( + worker, worker->path.buf, + worker->path.len, pos, + stx.mode); + continue; + } + goto malformed; + } + if (S_ISDIR(st.st_mode)) { + handle_directory(worker, task, fd, + parent_identity, + de->name, pos); + continue; + } + if (pos < 0) { + if (S_ISREG(st.st_mode)) + record_untracked(worker, task, DT_REG); + else if (S_ISLNK(st.st_mode)) + record_untracked(worker, task, DT_LNK); + else + preload_bulk_record_tracked_alias_fallback( + worker, worker->path.buf, + worker->path.len); + continue; + } + if ((!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)) || + st.st_nlink != 1) { + preload_bulk_record_tracked_fallback( + worker, pos); + continue; + } + preload_bulk_record_tracked( + worker, pos, fd, de->name, &st, 0); + } + } + +malformed: + worker->malformed++; + return -1; +} + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux-open.c b/compat/preload-index/bulk-linux-open.c new file mode 100644 index 00000000000000..02cd5b55b87f43 --- /dev/null +++ b/compat/preload-index/bulk-linux-open.c @@ -0,0 +1,149 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include + +#include "compat/preload-index/bulk-linux.h" +#include "preload-index-bulk.h" + +#if defined(SYS_getdents64) && defined(SYS_statx) + +static int valid_component(const char *component, size_t len) +{ + return len && + !(len == 1 && component[0] == '.') && + !(len == 2 && component[0] == '.' && component[1] == '.'); +} + +static int verify_mount(struct preload_bulk_scan *scan, int fd) +{ + struct preload_bulk_linux_data *data = scan->platform_data; + struct preload_linux_statx stx; + + if (preload_bulk_linux_statx_raw(fd, "", PRELOAD_AT_EMPTY_PATH, + &stx)) + return -1; + if (!preload_bulk_linux_statx_complete(&stx)) { + errno = EOPNOTSUPP; + return -1; + } + if (stx.mnt_id != data->root_mnt_id) { + errno = EXDEV; + return -1; + } + return 0; +} + +#ifdef SYS_openat2 +int preload_bulk_linux_openat2_raw(int dirfd, const char *path) +{ + struct preload_linux_open_how how = { + .flags = O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC, + .resolve = PRELOAD_RESOLVE_BENEATH | + PRELOAD_RESOLVE_NO_SYMLINKS | + PRELOAD_RESOLVE_NO_MAGICLINKS | + PRELOAD_RESOLVE_NO_XDEV, + }; + + return syscall(SYS_openat2, dirfd, path, &how, sizeof(how)); +} +#endif + +static int open_one_fallback(struct preload_bulk_scan *scan, int parent_fd, + const char *name, int check_mount) +{ + int fd = openat(parent_fd, name, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + + if (fd < 0) + return -1; + if (check_mount && verify_mount(scan, fd)) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return -1; + } + return fd; +} + +int preload_bulk_linux_open_dir_at( + struct preload_bulk_worker *worker, int parent_fd, + const char *name) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_linux_data *data = scan->platform_data; + + if (!valid_component(name, strlen(name)) || strchr(name, '/')) { + errno = EINVAL; + return -1; + } +#ifdef SYS_openat2 + if (data->use_openat2) + return preload_bulk_linux_openat2_raw(parent_fd, name); +#else + (void)data; +#endif + /* scan_directory() verifies the opened descriptor's mount ID. */ + return open_one_fallback(scan, parent_fd, name, 0); +} + +int preload_bulk_linux_open_relative(struct preload_bulk_scan *scan, + const char *path) +{ + struct preload_bulk_linux_data *data = scan->platform_data; + const char *component = path; + int fd; + + if (!*path || *path == '/' || path[strlen(path) - 1] == '/') { + errno = EINVAL; + return -1; + } +#ifdef SYS_openat2 + if (data->use_openat2) + return preload_bulk_linux_openat2_raw(scan->root_fd, path); +#else + (void)data; +#endif + fd = fcntl(scan->root_fd, F_DUPFD_CLOEXEC, 0); + if (fd < 0) + return -1; + if (verify_mount(scan, fd)) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return -1; + } + if (!strcmp(path, ".")) + return fd; + while (*component) { + const char *slash = strchr(component, '/'); + size_t len = slash ? (size_t)(slash - component) : + strlen(component); + char *name; + int next; + + if (!valid_component(component, len)) { + close(fd); + errno = EINVAL; + return -1; + } + name = xmemdupz(component, len); + next = open_one_fallback(scan, fd, name, 1); + free(name); + close(fd); + if (next < 0) + return -1; + fd = next; + if (!slash) + break; + component = slash + 1; + } + return fd; +} + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux-scan.c b/compat/preload-index/bulk-linux-scan.c new file mode 100644 index 00000000000000..c6da94aa41564b --- /dev/null +++ b/compat/preload-index/bulk-linux-scan.c @@ -0,0 +1,107 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include + +#include "compat/preload-index/bulk-linux.h" +#include "path-namespace.h" +#include "preload-index-bulk.h" + +#if defined(SYS_getdents64) && defined(SYS_statx) + +static struct preload_bulk_dir_identity directory_identity( + const struct preload_linux_statx *stx, const struct stat *st) +{ + struct preload_bulk_dir_identity result = { + .stat = *st, + .platform_id = stx->mnt_id, + .complete = 1, + }; + + return result; +} + +static int directory_identity_matches( + const struct preload_bulk_dir_identity *before, + const struct preload_linux_statx *stx, const struct stat *after) +{ + return S_ISDIR(after->st_mode) && + path_namespace_stat_equal(&before->stat, after) && + before->platform_id == stx->mnt_id; +} + +int preload_bulk_linux_scan_directory(struct preload_bulk_worker *worker, + struct preload_bulk_task *task) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_linux_statx before_stx, after_stx; + struct preload_bulk_dir_identity before_identity; + struct stat before, after; + size_t path_len; + int fd = task->fd; + int ret = -1; + + if (fd < 0) + fd = preload_bulk_linux_open_relative(scan, task->path); + if (fd < 0) + goto out; + if (preload_bulk_test_barrier(scan, task->path)) + goto out; + if (preload_bulk_linux_fd_stat( + worker, fd, &before_stx, &before) || + !S_ISDIR(before.st_mode)) { + if (errno != EXDEV) + goto out; + path_len = strlen(task->path); + preload_bulk_record_tracked_descendants_fallback( + worker, task->path, path_len); + if (scan->collect_untracked) + preload_bulk_invalidate_untracked(worker); + ret = 0; + goto out; + } + before_identity = directory_identity(&before_stx, &before); + if ((!scan->collect_untracked || + !preload_bulk_untracked_root_is_visible( + worker, task->untracked_root)) && + preload_bulk_linux_enumerate( + worker, task, fd, &before_identity)) + goto out; + if (preload_bulk_linux_fd_stat( + worker, fd, &after_stx, &after)) + goto out; + if (!preload_bulk_linux_statx_same(&before_stx, &after_stx) || + !directory_identity_matches( + &before_identity, &after_stx, &after)) + worker->changed_dirs++; + ret = 0; + +out: + if (task->has_parent_identity) { + struct preload_linux_statx parent_stx; + struct stat parent_after; + int parent_changed = fd < 0; + + /* + * Resolve ".." through the held child descriptor so a move + * cannot redirect the parent check to the old path. + */ + if (!parent_changed) + parent_changed = preload_bulk_linux_entry_stat( + worker, fd, "..", &parent_stx, + &parent_after); + if (parent_changed || + !directory_identity_matches( + &task->parent_identity, &parent_stx, + &parent_after)) + worker->changed_dirs++; + } + if (fd >= 0) + close(fd); + return ret; +} + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux-stat.c b/compat/preload-index/bulk-linux-stat.c new file mode 100644 index 00000000000000..a8594d7230f9d0 --- /dev/null +++ b/compat/preload-index/bulk-linux-stat.c @@ -0,0 +1,140 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include +#include + +#include "compat/preload-index/bulk-linux.h" +#include "preload-index-bulk.h" + +#if defined(SYS_getdents64) && defined(SYS_statx) + +int preload_bulk_linux_statx_raw(int dirfd, const char *path, int flags, + struct preload_linux_statx *stx) +{ + memset(stx, 0, sizeof(*stx)); + return syscall(SYS_statx, dirfd, path, flags, + PRELOAD_STATX_BASIC_STATS | PRELOAD_STATX_MNT_ID, stx); +} + +int preload_bulk_linux_statx_complete( + const struct preload_linux_statx *stx) +{ + return (stx->mask & + (PRELOAD_STATX_BASIC_STATS | PRELOAD_STATX_MNT_ID)) == + (PRELOAD_STATX_BASIC_STATS | PRELOAD_STATX_MNT_ID) && + stx->mtime.tv_nsec < 1000000000 && + stx->ctime.tv_nsec < 1000000000; +} + +int preload_bulk_linux_statx_same(const struct preload_linux_statx *a, + const struct preload_linux_statx *b) +{ + return preload_bulk_linux_statx_complete(a) && + preload_bulk_linux_statx_complete(b) && + a->mnt_id == b->mnt_id && + a->dev_major == b->dev_major && + a->dev_minor == b->dev_minor && + a->ino == b->ino && a->mode == b->mode && + a->nlink == b->nlink && a->uid == b->uid && + a->gid == b->gid && a->size == b->size && + a->mtime.tv_sec == b->mtime.tv_sec && + a->mtime.tv_nsec == b->mtime.tv_nsec && + a->ctime.tv_sec == b->ctime.tv_sec && + a->ctime.tv_nsec == b->ctime.tv_nsec; +} + +static int statx_to_stat(const struct preload_linux_statx *stx, + struct stat *st) +{ + dev_t dev; + + if (!preload_bulk_linux_statx_complete(stx)) + return -1; + memset(st, 0, sizeof(*st)); + dev = makedev(stx->dev_major, stx->dev_minor); + if (major(dev) != stx->dev_major || minor(dev) != stx->dev_minor) + return -1; + st->st_dev = dev; + st->st_ino = stx->ino; + if ((uint64_t)st->st_ino != stx->ino) + return -1; + st->st_mode = stx->mode; + st->st_nlink = stx->nlink; + if ((uint64_t)st->st_nlink != stx->nlink) + return -1; + st->st_uid = stx->uid; + st->st_gid = stx->gid; + if ((uint64_t)st->st_uid != stx->uid || + (uint64_t)st->st_gid != stx->gid) + return -1; + st->st_size = stx->size; + if (st->st_size < 0 || (uint64_t)st->st_size != stx->size) + return -1; + st->st_mtim.tv_sec = stx->mtime.tv_sec; + st->st_mtim.tv_nsec = stx->mtime.tv_nsec; + st->st_ctim.tv_sec = stx->ctime.tv_sec; + st->st_ctim.tv_nsec = stx->ctime.tv_nsec; + if ((int64_t)st->st_mtim.tv_sec != stx->mtime.tv_sec || + (int64_t)st->st_ctim.tv_sec != stx->ctime.tv_sec) + return -1; + return 0; +} + +int preload_bulk_linux_entry_stat(struct preload_bulk_worker *worker, + int dirfd, const char *name, + struct preload_linux_statx *stx, + struct stat *st) +{ + struct preload_bulk_linux_data *data = + worker->scan->platform_data; + + if (preload_bulk_linux_statx_raw( + dirfd, name, + PRELOAD_AT_SYMLINK_NOFOLLOW | PRELOAD_AT_NO_AUTOMOUNT, + stx)) + return -1; + if (!preload_bulk_linux_statx_complete(stx)) { + errno = EOPNOTSUPP; + return -1; + } + if (stx->mnt_id != data->root_mnt_id) { + errno = EXDEV; + return -1; + } + if (statx_to_stat(stx, st)) { + errno = EOVERFLOW; + return -1; + } + return 0; +} + +int preload_bulk_linux_fd_stat(struct preload_bulk_worker *worker, int fd, + struct preload_linux_statx *stx, + struct stat *st) +{ + struct preload_bulk_linux_data *data = + worker->scan->platform_data; + + if (preload_bulk_linux_statx_raw(fd, "", PRELOAD_AT_EMPTY_PATH, + stx)) + return -1; + if (!preload_bulk_linux_statx_complete(stx)) { + errno = EOPNOTSUPP; + return -1; + } + if (stx->mnt_id != data->root_mnt_id) { + errno = EXDEV; + return -1; + } + if (statx_to_stat(stx, st)) { + errno = EOVERFLOW; + return -1; + } + return 0; +} + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux-topology.c b/compat/preload-index/bulk-linux-topology.c new file mode 100644 index 00000000000000..523e6c08b909b3 --- /dev/null +++ b/compat/preload-index/bulk-linux-topology.c @@ -0,0 +1,166 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include +#include +#include + +#include "compat/preload-index/bulk-linux.h" +#include "parse.h" +#include "preload-index-bulk.h" +#include "repository.h" +#include "trace2.h" + +#ifndef EXT_FAMILY_SUPER_MAGIC +#define EXT_FAMILY_SUPER_MAGIC 0xef53 +#endif +#ifndef XFS_SUPER_MAGIC +#define XFS_SUPER_MAGIC 0x58465342 +#endif + +#if defined(SYS_getdents64) && defined(SYS_statx) + +static void load_test_dirent_type(struct preload_bulk_linux_data *data) +{ + const char *path, *value; + + if (!git_env_bool("GIT_TEST_PRELOAD_INDEX_BULK", 0)) + return; + value = getenv("GIT_TEST_PRELOAD_INDEX_BULK_DIRENT_TYPE"); + if (!value) + return; + if (skip_prefix(value, "dir:", &path)) + data->test_dirent_type = DT_DIR; + else if (skip_prefix(value, "reg:", &path)) + data->test_dirent_type = DT_REG; + else + die("invalid GIT_TEST_PRELOAD_INDEX_BULK_DIRENT_TYPE"); + if (!*path) + die("GIT_TEST_PRELOAD_INDEX_BULK_DIRENT_TYPE needs a path"); + data->test_dirent_path = xstrdup(path); +} + +static int read_mountinfo(struct strbuf *out) +{ + int fd = open("/proc/self/mountinfo", O_RDONLY | O_CLOEXEC); + int ret = -1; + + if (fd < 0) + return -1; + strbuf_reset(out); + if (strbuf_read(out, fd, 0) >= 0) + ret = 0; + if (close(fd)) + ret = -1; + return ret; +} + +const char *preload_bulk_linux_start(struct preload_bulk_scan *scan) +{ + struct preload_bulk_linux_data *data; + struct statfs fs; + const char *fs_name; + + CALLOC_ARRAY(data, 1); + strbuf_init(&data->mountinfo, 0); + load_test_dirent_type(data); + scan->platform_data = data; + scan->root_fd = open(repo_get_work_tree(scan->repo), + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (scan->root_fd < 0 || fstatfs(scan->root_fd, &fs)) + return "unsupported-filesystem"; + if ((unsigned long)fs.f_type == EXT_FAMILY_SUPER_MAGIC) + fs_name = "ext-family"; + else if ((unsigned long)fs.f_type == XFS_SUPER_MAGIC) + fs_name = "xfs"; + else + return "unsupported-filesystem"; + trace2_data_string("index", scan->repo, "preload/bulk_filesystem", + fs_name); + if (preload_bulk_linux_statx_raw( + scan->root_fd, "", PRELOAD_AT_EMPTY_PATH, + &data->root_statx) || + !preload_bulk_linux_statx_complete(&data->root_statx) || + !S_ISDIR(data->root_statx.mode)) + return "statx-unavailable"; + data->root_mnt_id = data->root_statx.mnt_id; + if (read_mountinfo(&data->mountinfo)) + return "namespace-check-unavailable"; +#ifdef SYS_openat2 + { + int fd = preload_bulk_linux_openat2_raw(scan->root_fd, "."); + + if (fd >= 0) { + struct preload_linux_statx probe; + + if (!preload_bulk_linux_statx_raw( + fd, "", PRELOAD_AT_EMPTY_PATH, &probe) && + preload_bulk_linux_statx_complete(&probe) && + probe.mnt_id == data->root_mnt_id) + data->use_openat2 = 1; + close(fd); + } + } +#endif + trace2_data_intmax("index", scan->repo, "preload/bulk_openat2", + data->use_openat2); + return NULL; +} + +const char *preload_bulk_linux_finish(struct preload_bulk_scan *scan) +{ + struct preload_bulk_linux_data *data = scan->platform_data; + struct strbuf after = STRBUF_INIT; + struct preload_linux_statx root_after; + const char *result = NULL; + int fd; + + if (read_mountinfo(&after)) { + result = "namespace-check-unavailable"; + goto out; + } + if (strbuf_cmp(&data->mountinfo, &after)) { + trace2_data_intmax( + "index", scan->repo, + "preload/bulk_namespace_churn", 1); + result = "namespace-churn"; + goto out; + } + fd = open(repo_get_work_tree(scan->repo), + O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { + result = "namespace-race"; + goto out; + } + if (preload_bulk_linux_statx_raw( + fd, "", PRELOAD_AT_EMPTY_PATH, &root_after) || + !preload_bulk_linux_statx_same( + &data->root_statx, &root_after)) + result = "namespace-race"; + close(fd); + +out: + strbuf_release(&after); + return result; +} + +void preload_bulk_linux_release(struct preload_bulk_scan *scan) +{ + struct preload_bulk_linux_data *data = scan->platform_data; + + if (scan->root_fd >= 0) { + close(scan->root_fd); + scan->root_fd = -1; + } + if (!data) + return; + strbuf_release(&data->mountinfo); + free(data->test_dirent_path); + free(data); + scan->platform_data = NULL; +} + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux.c b/compat/preload-index/bulk-linux.c new file mode 100644 index 00000000000000..5c4c120a61840c --- /dev/null +++ b/compat/preload-index/bulk-linux.c @@ -0,0 +1,37 @@ +#include "git-compat-util.h" + +#ifdef __linux__ + +#include + +#include "compat/preload-index/bulk-linux.h" +#include "preload-index-bulk.h" + +#if defined(SYS_getdents64) && defined(SYS_statx) + +static const struct preload_bulk_backend linux_backend = { + .collects_untracked = 1, + .max_threads = 16, + .start = preload_bulk_linux_start, + .finish = preload_bulk_linux_finish, + .release = preload_bulk_linux_release, + .open_proof_parent = preload_bulk_linux_open_relative, + .open_dir_at = preload_bulk_linux_open_dir_at, + .scan_directory = preload_bulk_linux_scan_directory, +}; + +const struct preload_bulk_backend *preload_bulk_platform_backend(void) +{ + return &linux_backend; +} + +#else /* !SYS_getdents64 || !SYS_statx */ + +const struct preload_bulk_backend *preload_bulk_platform_backend(void) +{ + return NULL; +} + +#endif + +#endif /* __linux__ */ diff --git a/compat/preload-index/bulk-linux.h b/compat/preload-index/bulk-linux.h new file mode 100644 index 00000000000000..e25db006115584 --- /dev/null +++ b/compat/preload-index/bulk-linux.h @@ -0,0 +1,112 @@ +#ifndef PRELOAD_INDEX_BULK_LINUX_H +#define PRELOAD_INDEX_BULK_LINUX_H + +#ifdef __linux__ + +#include + +#include "strbuf.h" + +#define PRELOAD_AT_NO_AUTOMOUNT 0x800 +#define PRELOAD_AT_EMPTY_PATH 0x1000 +#define PRELOAD_AT_SYMLINK_NOFOLLOW 0x100 +#define PRELOAD_STATX_BASIC_STATS 0x000007ffU +#define PRELOAD_STATX_MNT_ID 0x00001000U +#define PRELOAD_RESOLVE_NO_XDEV 0x01 +#define PRELOAD_RESOLVE_NO_MAGICLINKS 0x02 +#define PRELOAD_RESOLVE_NO_SYMLINKS 0x04 +#define PRELOAD_RESOLVE_BENEATH 0x08 + +struct preload_linux_statx_timestamp { + int64_t tv_sec; + uint32_t tv_nsec; + int32_t reserved; +}; + +struct preload_linux_statx { + uint32_t mask; + uint32_t blksize; + uint64_t attributes; + uint32_t nlink; + uint32_t uid; + uint32_t gid; + uint16_t mode; + uint16_t spare0; + uint64_t ino; + uint64_t size; + uint64_t blocks; + uint64_t attributes_mask; + struct preload_linux_statx_timestamp atime; + struct preload_linux_statx_timestamp btime; + struct preload_linux_statx_timestamp ctime; + struct preload_linux_statx_timestamp mtime; + uint32_t rdev_major; + uint32_t rdev_minor; + uint32_t dev_major; + uint32_t dev_minor; + uint64_t mnt_id; + uint32_t dio_mem_align; + uint32_t dio_offset_align; + uint64_t spare3[12]; +}; + +struct preload_linux_open_how { + uint64_t flags; + uint64_t mode; + uint64_t resolve; +}; + +struct preload_bulk_linux_data { + struct preload_linux_statx root_statx; + struct strbuf mountinfo; + uint64_t root_mnt_id; + char *test_dirent_path; + unsigned char test_dirent_type; + int use_openat2; +}; + +struct preload_bulk_scan; +struct preload_bulk_task; +struct preload_bulk_worker; +struct preload_bulk_dir_identity; + +#if defined(SYS_getdents64) && defined(SYS_statx) + +int preload_bulk_linux_statx_raw(int dirfd, const char *path, int flags, + struct preload_linux_statx *stx); +int preload_bulk_linux_statx_complete( + const struct preload_linux_statx *stx); +int preload_bulk_linux_statx_same(const struct preload_linux_statx *a, + const struct preload_linux_statx *b); +int preload_bulk_linux_entry_stat(struct preload_bulk_worker *worker, + int dirfd, const char *name, + struct preload_linux_statx *stx, + struct stat *st); +int preload_bulk_linux_fd_stat(struct preload_bulk_worker *worker, int fd, + struct preload_linux_statx *stx, + struct stat *st); + +#ifdef SYS_openat2 +int preload_bulk_linux_openat2_raw(int dirfd, const char *path); +#endif +int preload_bulk_linux_open_dir_at(struct preload_bulk_worker *worker, + int parent_fd, const char *name); +int preload_bulk_linux_open_relative(struct preload_bulk_scan *scan, + const char *path); + +int preload_bulk_linux_enumerate( + struct preload_bulk_worker *worker, + struct preload_bulk_task *task, int fd, + const struct preload_bulk_dir_identity *parent_identity); +int preload_bulk_linux_scan_directory(struct preload_bulk_worker *worker, + struct preload_bulk_task *task); + +const char *preload_bulk_linux_start(struct preload_bulk_scan *scan); +const char *preload_bulk_linux_finish(struct preload_bulk_scan *scan); +void preload_bulk_linux_release(struct preload_bulk_scan *scan); + +#endif /* SYS_getdents64 && SYS_statx */ + +#endif /* __linux__ */ + +#endif /* PRELOAD_INDEX_BULK_LINUX_H */ diff --git a/compat/simple-ipc/ipc-unix-socket.c b/compat/simple-ipc/ipc-unix-socket.c index 7db3b2a89755c6..d27747bc1d0b63 100644 --- a/compat/simple-ipc/ipc-unix-socket.c +++ b/compat/simple-ipc/ipc-unix-socket.c @@ -189,10 +189,10 @@ void ipc_client_close_connection(struct ipc_client_connection *connection) free(connection); } -int ipc_client_send_command_to_connection( +static int ipc_client_send_command_to_connection_1( struct ipc_client_connection *connection, const char *message, size_t message_len, - struct strbuf *answer) + struct strbuf *answer, int gentle) { int ret = 0; @@ -203,14 +203,14 @@ int ipc_client_send_command_to_connection( if (write_packetized_from_buf_no_flush(message, message_len, connection->fd) < 0 || packet_flush_gently(connection->fd) < 0) { - ret = error(_("could not send IPC command")); + ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } if (read_packetized_to_strbuf( connection->fd, answer, PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { - ret = error(_("could not read IPC response")); + ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } @@ -219,6 +219,24 @@ int ipc_client_send_command_to_connection( return ret; } +int ipc_client_send_command_to_connection( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 0); +} + +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 1); +} + int ipc_client_send_command(const char *path, const struct ipc_client_connect_options *options, const char *message, size_t message_len, diff --git a/compat/simple-ipc/ipc-win32.c b/compat/simple-ipc/ipc-win32.c index 4a3e7df9c739e1..f1b4124d3ae8df 100644 --- a/compat/simple-ipc/ipc-win32.c +++ b/compat/simple-ipc/ipc-win32.c @@ -235,10 +235,10 @@ void ipc_client_close_connection(struct ipc_client_connection *connection) free(connection); } -int ipc_client_send_command_to_connection( +static int ipc_client_send_command_to_connection_1( struct ipc_client_connection *connection, const char *message, size_t message_len, - struct strbuf *answer) + struct strbuf *answer, int gentle) { int ret = 0; @@ -249,7 +249,7 @@ int ipc_client_send_command_to_connection( if (write_packetized_from_buf_no_flush(message, message_len, connection->fd) < 0 || packet_flush_gently(connection->fd) < 0) { - ret = error(_("could not send IPC command")); + ret = gentle ? -1 : error(_("could not send IPC command")); goto done; } @@ -258,7 +258,7 @@ int ipc_client_send_command_to_connection( if (read_packetized_to_strbuf( connection->fd, answer, PACKET_READ_GENTLE_ON_EOF | PACKET_READ_GENTLE_ON_READ_ERROR) < 0) { - ret = error(_("could not read IPC response")); + ret = gentle ? -1 : error(_("could not read IPC response")); goto done; } @@ -267,6 +267,24 @@ int ipc_client_send_command_to_connection( return ret; } +int ipc_client_send_command_to_connection( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 0); +} + +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer) +{ + return ipc_client_send_command_to_connection_1( + connection, message, message_len, answer, 1); +} + int ipc_client_send_command(const char *path, const struct ipc_client_connect_options *options, const char *message, size_t message_len, diff --git a/config.mak.openai b/config.mak.openai new file mode 100644 index 00000000000000..0ae133c8bb2ecf --- /dev/null +++ b/config.mak.openai @@ -0,0 +1,74 @@ +# OpenAI release build settings. +# +# The Codex release workflow copies this file to config.mak before +# building. Keep release-only optimizations here instead of in the +# upstream Makefile. + +OPENAI_PROFILE_DIR := $(CURDIR) +OPENAI_PROFILE_RAW := $(OPENAI_PROFILE_DIR)/default_%m_%p.profraw +OPENAI_PROFILE_DATA := $(OPENAI_PROFILE_DIR)/default.profdata +OPENAI_PROFILE_TRAINING ?= .github/workflows/codex-pgo-training.sh +OPENAI_LLVM_PROFDATA ?= llvm-profdata + +ifdef OPENAI_LTO +CFLAGS_APPEND += -flto=$(OPENAI_LTO) +LDFLAGS_APPEND += -flto=$(OPENAI_LTO) +endif + +ifeq ("$(OPENAI_PROFILE_FORMAT)","LLVM") +ifeq ("$(OPENAI_PROFILE)","GEN") + BASIC_CFLAGS += -fprofile-instr-generate=$(OPENAI_PROFILE_RAW) + BASIC_CFLAGS += -DNO_NORETURN=1 + export CCACHE_DISABLE = t + V = 1 +else +ifneq ("$(OPENAI_PROFILE)","") + BASIC_CFLAGS += -fprofile-instr-use=$(OPENAI_PROFILE_DATA) + BASIC_CFLAGS += -Wno-profile-instr-unprofiled -DNO_NORETURN=1 + export CCACHE_DISABLE = t + V = 1 +endif +endif +else +ifeq ("$(OPENAI_PROFILE)","GEN") + BASIC_CFLAGS += -fprofile-generate=$(OPENAI_PROFILE_DIR) + BASIC_CFLAGS += -DNO_NORETURN=1 + EXTLIBS += -lgcov + export CCACHE_DISABLE = t + V = 1 +else +ifneq ("$(OPENAI_PROFILE)","") + BASIC_CFLAGS += -fprofile-use=$(OPENAI_PROFILE_DIR) + BASIC_CFLAGS += -fprofile-correction -DNO_NORETURN=1 + export CCACHE_DISABLE = t + V = 1 +endif +endif +endif + +# Every C object already waits for Git's forced GIT-CFLAGS target. Gate that +# target so "make strip install" finishes PGO before its normal prerequisites +# can start compiling with profile-use flags. +ifeq "$(OPENAI_PROFILE)" "BUILD" +GIT-CFLAGS: openai-profile +endif + +openai-profile: profile-clean openai-profile-clean + $(MAKE) OPENAI_PROFILE=GEN all + $(SHELL_PATH) $(OPENAI_PROFILE_TRAINING) + $(MAKE) OPENAI_PROFILE= openai-profile-merge + $(MAKE) OPENAI_PROFILE=USE all + +openai-profile-merge: +ifeq ("$(OPENAI_PROFILE_FORMAT)","LLVM") + $(OPENAI_LLVM_PROFDATA) merge \ + -output=$(OPENAI_PROFILE_DATA) \ + $(OPENAI_PROFILE_DIR)/*.profraw +endif + +clean: openai-profile-clean + +openai-profile-clean: + $(RM) $(OPENAI_PROFILE_DIR)/*.profraw $(OPENAI_PROFILE_DATA) + +.PHONY: openai-profile openai-profile-merge openai-profile-clean diff --git a/config.mak.uname b/config.mak.uname index 9ebd240378ca59..4d205c4c493939 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -63,6 +63,12 @@ ifeq ($(uname_S),Linux) PROCFS_EXECUTABLE_PATH = /proc/self/exe HAVE_PLATFORM_PROCINFO = YesPlease COMPAT_OBJS += compat/linux/procinfo.o + PRELOAD_INDEX_BULK_BACKEND = linux + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-linux-entry.o + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-linux-open.o + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-linux-scan.o + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-linux-stat.o + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-linux-topology.o EXTLIBS += -ldl # centos7/rhel7 provides gcc 4.8.5 and zlib 1.2.7. ifneq ($(findstring .el7.,$(uname_R)),) @@ -162,6 +168,8 @@ ifeq ($(uname_S),Darwin) USE_ENHANCED_BASIC_REGULAR_EXPRESSIONS = YesPlease HAVE_PLATFORM_PROCINFO = YesPlease COMPAT_OBJS += compat/darwin/procinfo.o + PRELOAD_INDEX_BULK_BACKEND = darwin + PRELOAD_INDEX_BULK_PLATFORM_OBJS += compat/preload-index/bulk-darwin-root.o ifeq ($(uname_M),arm64) HOMEBREW_PREFIX = /opt/homebrew diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index a57c4b464fa456..6440b31a4483a1 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -103,6 +103,7 @@ macro(parse_makefile_for_sources list_var makefile regex) file(STRINGS ${makefile} ${list_var} REGEX "^${regex} \\+=(.*)") string(REPLACE "${regex} +=" "" ${list_var} ${${list_var}}) string(REPLACE "$(COMPAT_OBJS)" "" ${list_var} ${${list_var}}) #remove "$(COMPAT_OBJS)" This is only for libgit. + string(REPLACE "$(PRELOAD_INDEX_BULK_OBJS)" "" ${list_var} ${${list_var}}) string(STRIP ${${list_var}} ${list_var}) #remove trailing/leading whitespaces string(REPLACE ".o" ".c;" ${list_var} ${${list_var}}) #change .o to .c, ; is for converting the string into a list list(TRANSFORM ${list_var} STRIP) #remove trailing/leading whitespaces for each element in list @@ -271,10 +272,26 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") set(NO_UNIX_SOCKETS 1) elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") - add_compile_definitions(PROCFS_EXECUTABLE_PATH="/proc/self/exe" HAVE_DEV_TTY ) - list(APPEND compat_SOURCES unix-socket.c unix-stream-server.c compat/linux/procinfo.c) + add_compile_definitions(HAVE_PRELOAD_INDEX_BULK + PROCFS_EXECUTABLE_PATH="/proc/self/exe" HAVE_DEV_TTY) + list(APPEND compat_SOURCES + unix-socket.c + unix-stream-server.c + compat/linux/procinfo.c + compat/preload-index/bulk-linux.c + compat/preload-index/bulk-linux-entry.c + compat/preload-index/bulk-linux-open.c + compat/preload-index/bulk-linux-scan.c + compat/preload-index/bulk-linux-stat.c + compat/preload-index/bulk-linux-topology.c) elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - list(APPEND compat_SOURCES compat/darwin/procinfo.c) + add_compile_definitions(HAVE_PRELOAD_INDEX_BULK PRECOMPOSE_UNICODE + USE_ST_TIMESPEC) + list(APPEND compat_SOURCES + compat/darwin/procinfo.c + compat/precompose_utf8.c + compat/preload-index/bulk-darwin.c + compat/preload-index/bulk-darwin-root.c) endif() if(CMAKE_SYSTEM_NAME STREQUAL "Windows") @@ -664,6 +681,14 @@ include_directories(${CMAKE_BINARY_DIR}) #libgit parse_makefile_for_sources(libgit_SOURCES ${CMAKE_SOURCE_DIR}/Makefile "LIB_OBJS") +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR + CMAKE_SYSTEM_NAME STREQUAL "Linux") + list(APPEND libgit_SOURCES + preload-index-bulk-index.c + preload-index-bulk-thread.c + preload-index-bulk.c) +endif() + list(TRANSFORM libgit_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") list(TRANSFORM compat_SOURCES PREPEND "${CMAKE_SOURCE_DIR}/") diff --git a/convert.c b/convert.c index 036506842c3d41..f7ca4d780466e7 100644 --- a/convert.c +++ b/convert.c @@ -1318,11 +1318,8 @@ static int git_path_check_ident(struct attr_check_item *check) static struct attr_check *check; -void convert_attrs(struct index_state *istate, - struct conv_attrs *ca, const char *path) +static void convert_attrs_init(void) { - struct attr_check_item *ccheck = NULL; - if (!check) { check = attr_check_initl("crlf", "ident", "filter", "eol", "text", "working-tree-encoding", @@ -1330,9 +1327,33 @@ void convert_attrs(struct index_state *istate, user_convert_tail = &user_convert; repo_config(the_repository, read_convert_config, NULL); } +} - git_check_attr(istate, path, check); - ccheck = check->items; +struct attr_check *convert_attrs_check_alloc(void) +{ + return attr_check_initl("crlf", "ident", "filter", + "eol", "text", "working-tree-encoding", + NULL); +} + +void convert_attrs_prepare(struct index_state *istate) +{ + convert_attrs_init(); + /* Prime default_attr_source() and the root attribute stack on main. */ + git_check_attr(istate, "", check); +} + +void convert_attrs_with_check(struct index_state *istate, + struct conv_attrs *ca, const char *path, + struct attr_check *attr_check) +{ + struct attr_check_item *ccheck; + + if (!attr_check || attr_check->nr != 6) + BUG("invalid per-thread conversion attribute check"); + + git_check_attr(istate, path, attr_check); + ccheck = attr_check->items; ca->crlf_action = git_path_check_crlf(ccheck + 4); if (ca->crlf_action == CRLF_UNDEFINED) ca->crlf_action = git_path_check_crlf(ccheck + 0); @@ -1363,6 +1384,34 @@ void convert_attrs(struct index_state *istate, ca->crlf_action = CRLF_AUTO_INPUT; } +void convert_attrs(struct index_state *istate, + struct conv_attrs *ca, const char *path) +{ + convert_attrs_init(); + convert_attrs_with_check(istate, ca, path, check); +} + +int convert_attrs_has_clean_filter(const struct conv_attrs *ca) +{ + return ca->drv && + (ca->drv->clean || ca->drv->process || ca->drv->required); +} + +int convert_attrs_are_raw_safe(const struct conv_attrs *ca) +{ + return !ca->drv && !ca->working_tree_encoding && !ca->ident && + ca->crlf_action == CRLF_BINARY; +} + +int convert_attrs_is_raw_safe(struct index_state *istate, const char *path, + struct attr_check *attr_check) +{ + struct conv_attrs ca; + + convert_attrs_with_check(istate, &ca, path, attr_check); + return convert_attrs_are_raw_safe(&ca); +} + void reset_parsed_attributes(void) { struct convert_driver *drv, *next; diff --git a/convert.h b/convert.h index 0a6e4086b8f932..241cd65c6e02a9 100644 --- a/convert.h +++ b/convert.h @@ -8,6 +8,7 @@ #include "string-list.h" struct index_state; +struct attr_check; struct strbuf; #define CONV_EOL_RNDTRP_DIE (1<<0) /* Die if CRLF to LF to CRLF is different */ @@ -91,6 +92,34 @@ struct conv_attrs { void convert_attrs(struct index_state *istate, struct conv_attrs *ca, const char *path); +/* + * Prepare conversion configuration and the default attribute source on the + * main thread before using per-thread attribute checks below. + */ +void convert_attrs_prepare(struct index_state *istate); + +/* Allocate the exact six-attribute check used by convert_attrs(). */ +struct attr_check *convert_attrs_check_alloc(void); + +/* + * Thread-friendly variant. Each concurrent caller must supply a distinct + * check allocated by convert_attrs_check_alloc(), and conversion/attribute + * global state must remain immutable until all callers have finished. + */ +void convert_attrs_with_check(struct index_state *istate, + struct conv_attrs *ca, const char *path, + struct attr_check *check); + +/* True when the selected driver can affect conversion into the index. */ +int convert_attrs_has_clean_filter(const struct conv_attrs *ca); + +/* True only when hashing the worktree bytes verbatim is exact. */ +int convert_attrs_are_raw_safe(const struct conv_attrs *ca); + +/* True only when hashing the worktree bytes verbatim is exact. */ +int convert_attrs_is_raw_safe(struct index_state *istate, const char *path, + struct attr_check *check); + extern enum eol core_eol; extern char *check_roundtrip_encoding; const char *get_cached_convert_stats_ascii(struct index_state *istate, diff --git a/diff-lib.c b/diff-lib.c index 46cae637ecda83..c6175021c81f9f 100644 --- a/diff-lib.c +++ b/diff-lib.c @@ -30,6 +30,30 @@ * diff-files */ +int diff_has_bounded_regular_pathspec(const struct pathspec *pathspec) +{ + int i; + + if (pathspec->nr <= 0 || pathspec->nr > 64 || + pathspec->has_wildcard || + (pathspec->magic & + (PATHSPEC_GLOB | PATHSPEC_ICASE | + PATHSPEC_EXCLUDE | PATHSPEC_ATTR))) + return 0; + for (i = 0; i < pathspec->nr; i++) { + const struct pathspec_item *item = &pathspec->items[i]; + struct stat st; + + if (!item->match || item->len <= 0 || + item->match[item->len - 1] == '/' || + !strcmp(item->match, ".") || + has_symlink_leading_path(item->match, item->len) || + lstat(item->match, &st) || !S_ISREG(st.st_mode)) + return 0; + } + return 1; +} + /* * Has the work tree entity been removed? * @@ -90,7 +114,10 @@ static int match_stat_with_submodule(struct diff_options *diffopt, struct stat *st, unsigned ce_option, unsigned *dirty_submodule) { - int changed = ie_match_stat(diffopt->repo->index, ce, st, ce_option); + int changed; + + changed = ie_match_stat_with_content_check( + diffopt->repo->index, ce, st, ce_option); if (S_ISGITLINK(ce->ce_mode)) { struct diff_flags orig_flags = diffopt->flags; if (!diffopt->flags.override_submodule_config) @@ -127,7 +154,10 @@ void run_diff_files(struct rev_info *revs, unsigned int option) entries = istate->cache_nr; for (i = 0; i < entries; i++) { unsigned int oldmode, newmode; + int fsmonitor_valid = 0; struct cache_entry *ce = istate->cache[i]; + struct stat st; + int has_stat = 0; int changed; unsigned dirty_submodule = 0; const struct object_id *old_oid, *new_oid; @@ -246,9 +276,9 @@ void run_diff_files(struct rev_info *revs, unsigned int option) if (ce->ce_flags & (CE_VALID | CE_FSMONITOR_VALID)) { changed = 0; newmode = ce->ce_mode; + fsmonitor_valid = + !!(ce->ce_flags & CE_FSMONITOR_VALID); } else { - struct stat st; - changed = check_removed(ce, &st); if (changed) { if (changed < 0) { @@ -270,12 +300,20 @@ void run_diff_files(struct rev_info *revs, unsigned int option) changed = match_stat_with_submodule(&revs->diffopt, ce, &st, ce_option, &dirty_submodule); + has_stat = 1; newmode = ce_mode_from_stat(revs->repo, ce, st.st_mode); + fsmonitor_valid = fsmonitor_stat_can_be_valid(&st); } if (!changed && !dirty_submodule) { + if ((option & DIFF_UPDATE_INDEX_STAT) && has_stat && + (ce->ce_flags & CE_CONTENT_CHECK_REQUIRED)) { + refresh_index_entry_stat(istate, i, &st); + ce = istate->cache[i]; + } ce_mark_uptodate(ce); - mark_fsmonitor_valid(istate, ce); + if (fsmonitor_valid) + mark_fsmonitor_valid(istate, ce); if (revs->diffopt.flags.find_copies_harder) diff_same(&revs->diffopt, newmode, &ce->oid, ce->name); diff --git a/diff.h b/diff.h index bb5cddaf3499e9..cc2b4e00ecc8d8 100644 --- a/diff.h +++ b/diff.h @@ -698,7 +698,10 @@ void diff_get_merge_base(const struct rev_info *revs, struct object_id *mb); #define DIFF_SILENT_ON_REMOVED 01 /* report racily-clean paths as modified */ #define DIFF_RACY_IS_MODIFIED 02 +/* update index stat data for content-checked entries */ +#define DIFF_UPDATE_INDEX_STAT 04 void run_diff_files(struct rev_info *revs, unsigned int option); +int diff_has_bounded_regular_pathspec(const struct pathspec *pathspec); #define DIFF_INDEX_CACHED 01 #define DIFF_INDEX_MERGE_BASE 02 diff --git a/dir.c b/dir.c index 95d8a1cce90f77..b0e8aea8c85b22 100644 --- a/dir.c +++ b/dir.c @@ -15,10 +15,13 @@ #include "convert.h" #include "dir.h" #include "environment.h" +#include "exclude-source-proof.h" #include "gettext.h" #include "name-hash.h" #include "object-file.h" +#include "oidmap.h" #include "path.h" +#include "path-namespace.h" #include "refs.h" #include "repository.h" #include "wildmatch.h" @@ -27,12 +30,15 @@ #include "varint.h" #include "ewah/ewok.h" #include "fsmonitor-ll.h" +#include "fsmonitor.h" #include "read-cache-ll.h" #include "setup.h" #include "sparse-index.h" #include "strbuf.h" #include "submodule-config.h" #include "symlinks.h" +#include "thread-utils.h" +#include "trace.h" #include "trace2.h" #include "tree.h" #include "hex.h" @@ -71,10 +77,1094 @@ struct cached_dir { struct untracked_cache_dir *ucd; }; +/* + * Older UNTR writers hash the extra newline used by the ignore parser. + * A checked file read can establish that representation for every path + * with the same indexed blob. Workers share only this content relation; + * each path still needs its own strong stat and conversion checks. + */ +struct normalized_exclude_oid { + struct oidmap_entry ent; + struct object_id normalized; + size_t candidates; + unsigned int valid : 1; +}; + +struct untracked_cache_preload_task { + struct untracked_cache_dir *ucd; + struct normalized_exclude_oid *normalized_oid; + char *path; + struct stat_data stat_data; + struct object_id exclude_oid; + unsigned int exclude_mode; + unsigned int was_valid : 1; + unsigned int stat_checked : 1; + unsigned int stat_matches : 1; + unsigned int exclude_matches : 1; + unsigned int exclude_index_present : 1; + unsigned int exclude_index_candidate : 1; + unsigned int exclude_index_normalized_equivalent : 1; + unsigned int exclude_index_matches : 1; + unsigned int exclude_index_content_matches : 1; + unsigned int normalize_exclude_oid : 1; + unsigned int update_stat_data : 1; +}; + +struct untracked_cache_preload; + +struct untracked_cache_preload_data { + pthread_t pthread; + struct untracked_cache_preload *preload; + size_t offset, nr; + int started; +}; + +struct untracked_cache_preload { + struct repository *repo; + struct index_state *istate; + struct untracked_cache *uc; + struct untracked_cache_dir *root; + const struct pathspec *pathspec; + struct untracked_cache_preload_task *tasks; + struct object_id *exclude_index_oids; + struct oidmap normalized_excludes; + pthread_mutex_t normalized_mutex; + struct untracked_cache_preload_data *data; + struct cache_time index_timestamp; + char *exclude_per_dir; + size_t nr; + size_t normalized_objects; + int threads; + unsigned int dir_flags; + uint64_t started_at; + unsigned int fsmonitor_excludes_only : 1; + unsigned int normalized_mutex_initialized : 1; +}; + +#define UNTRACKED_CACHE_MAX_OVERLAP_THREADS 6 +#define UNTRACKED_CACHE_MAX_RECOVERY_THREADS 16 +#define UNTRACKED_CACHE_PRELOAD_COST 1000 +#define UNTRACKED_CACHE_EXCLUDE_PRELOAD_COST 256 +#define UNTRACKED_CACHE_MAX_EXCLUDE_SIZE (1024 * 1024) + +static void invalidate_gitignore(struct untracked_cache *uc, + struct untracked_cache_dir *dir); +static void invalidate_directory(struct untracked_cache *uc, + struct untracked_cache_dir *dir); + +static int match_untracked_dir_stat_racy(const struct cache_time *timestamp, + const struct stat_data *sd, + const struct stat *st); +static void *preload_untracked_cache_thread(void *data); + +static const struct pathspec *untracked_cache_preload_pathspec( + const struct pathspec *pathspec) +{ + int i, positive = 0; + + if (!pathspec || !pathspec->nr || + (pathspec->magic & (PATHSPEC_ATTR | PATHSPEC_ICASE))) + return NULL; + + for (i = 0; i < pathspec->nr; i++) { + const struct pathspec_item *item = &pathspec->items[i]; + + if (item->magic & PATHSPEC_EXCLUDE) + continue; + if (!item->nowildcard_len || strstr(item->match, "//") || + starts_with(item->match, "./") || + strstr(item->match, "/./") || + strstr(item->match, "/../")) + return NULL; + positive = 1; + } + return positive ? pathspec : NULL; +} + +static int untracked_cache_preload_pathspec_matches( + const struct pathspec *pathspec, + const char *path, + size_t pathlen) +{ + int i; + + if (!pathspec || !pathlen) + return 1; + + for (i = 0; i < pathspec->nr; i++) { + const struct pathspec_item *item = &pathspec->items[i]; + size_t len = item->nowildcard_len; + int wildcard = len != item->len; + + if (item->magic & PATHSPEC_EXCLUDE) + continue; + if (!wildcard) + while (len && item->match[len - 1] == '/') + len--; + if (!len) + return 1; + if (strncmp(path, item->match, pathlen < len ? pathlen : len)) + continue; + if (pathlen == len || + (pathlen < len && item->match[pathlen] == '/') || + (pathlen > len && (wildcard || path[len] == '/'))) + return 1; + } + return 0; +} + +static void collect_untracked_cache_preload_tasks( + struct untracked_cache_dir *ucd, + struct strbuf *path, + struct untracked_cache_preload_task **tasks, + size_t *nr, + size_t *alloc, + int fsmonitor_excludes_only, + const struct pathspec *pathspec) +{ + size_t i; + + if (!untracked_cache_preload_pathspec_matches( + pathspec, path->buf, path->len)) + return; + + if (!fsmonitor_excludes_only || + !is_null_oid(&ucd->exclude_oid)) { + ALLOC_GROW(*tasks, *nr + 1, *alloc); + memset(&(*tasks)[*nr], 0, sizeof(**tasks)); + (*tasks)[*nr].ucd = ucd; + (*tasks)[*nr].path = xstrdup(path->len ? path->buf : "."); + (*tasks)[*nr].stat_data = ucd->stat_data; + oidcpy(&(*tasks)[*nr].exclude_oid, &ucd->exclude_oid); + (*tasks)[*nr].was_valid = ucd->valid; + (*nr)++; + } + + for (i = 0; i < ucd->dirs_nr; i++) { + struct untracked_cache_dir *child = ucd->dirs[i]; + size_t old_len = path->len; + + if (path->len) + strbuf_addch(path, '/'); + strbuf_addstr(path, child->name); + collect_untracked_cache_preload_tasks(child, path, tasks, nr, + alloc, fsmonitor_excludes_only, + pathspec); + strbuf_setlen(path, old_len); + } +} + +static size_t count_untracked_cache_dirs_bounded( + const struct untracked_cache_dir *ucd, + size_t limit) +{ + size_t i, nr = 1; + + for (i = 0; i < ucd->dirs_nr && nr < limit; i++) + nr += count_untracked_cache_dirs_bounded(ucd->dirs[i], limit - nr); + return nr; +} + +#define UNTRACKED_CACHE_AUTO_PRELOAD_MIN_DIRS 2000 + +static int untracked_cache_auto_preload_worthwhile( + const struct untracked_cache *uc) +{ + if (!uc || !uc->root || uc->use_fsmonitor || !uc->root->valid) + return 0; + if (git_env_bool("GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD", 0)) + return 1; + return count_untracked_cache_dirs_bounded( + uc->root, UNTRACKED_CACHE_AUTO_PRELOAD_MIN_DIRS) >= + UNTRACKED_CACHE_AUTO_PRELOAD_MIN_DIRS; +} + +static int exclude_path_matches_fd(const char *path, + const struct stat *expected) +{ + struct stat st; + int fd = open_nofollow(path, O_RDONLY); + int ret = fd >= 0 && !fstat(fd, &st) && S_ISREG(st.st_mode) && + path_namespace_stat_equal(expected, &st); + + if (fd >= 0) + close(fd); + return ret; +} + +static int cached_exclude_file_matches( + const struct git_hash_algo *algo, + const char *path, const struct object_id *cached_oid, + struct object_id *raw_oid_out, struct object_id *normalized_oid_out, + unsigned int *mode_out) +{ + struct object_id raw_oid, normalized_oid; + struct stat st, st_after; + char *buf; + size_t size; + int fd, ret = 0; + + fd = open_nofollow(path, O_RDONLY); + if (fd < 0) + return 0; + if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_size < 0 || + st.st_size > UNTRACKED_CACHE_MAX_EXCLUDE_SIZE) + goto out_close; + + size = xsize_t(st.st_size); + buf = xmallocz(size + 1); + if (read_in_full(fd, buf, size) != size) + goto out; + /* Prove both the opened file and its pathname stayed unchanged. */ + if (fstat(fd, &st_after) || + !path_namespace_stat_equal(&st, &st_after) || + !exclude_path_matches_fd(path, &st_after)) + goto out; + if (mode_out) + *mode_out = st_after.st_mode; + + /* add_patterns() may record either the blob or its LF-normalized form. */ + hash_object_file(algo, buf, size, OBJ_BLOB, &raw_oid); + if (raw_oid_out) + oidcpy(raw_oid_out, &raw_oid); + if (oideq(&raw_oid, cached_oid) && !normalized_oid_out) { + ret = 1; + goto out; + } + buf[size] = '\n'; + hash_object_file(algo, buf, size + 1, OBJ_BLOB, + &normalized_oid); + if (normalized_oid_out) + oidcpy(normalized_oid_out, &normalized_oid); + ret = oideq(&raw_oid, cached_oid) || + oideq(&normalized_oid, cached_oid); +out: + free(buf); +out_close: + close(fd); + return ret; +} + +static int cached_exclude_file_matches_index_stat( + const struct stat_data *sd, + const struct stat *st) +{ + struct stat_data current; + struct stat st_copy = *st; + + /* + * Compare every field saved in the index, independent of the user's + * ordinary stat-match settings. Unreliable object identities and + * multiply-linked files can conceal changes through paths outside + * the monitor's watch cone, so both retain the content-hash check. + */ + if (!fstat_is_reliable() || !S_ISREG(st->st_mode) || + st->st_nlink != 1) + return 0; + fill_stat_data(¤t, &st_copy); + return sd->sd_ctime.sec == current.sd_ctime.sec && + sd->sd_ctime.nsec == current.sd_ctime.nsec && + sd->sd_mtime.sec == current.sd_mtime.sec && + sd->sd_mtime.nsec == current.sd_mtime.nsec && + sd->sd_dev == current.sd_dev && + sd->sd_ino == current.sd_ino && + sd->sd_uid == current.sd_uid && + sd->sd_gid == current.sd_gid && + sd->sd_size == current.sd_size; +} + +static void preload_fsmonitor_excludes_from_index( + struct untracked_cache_preload *preload) +{ + struct repo_config_values *cfg = + repo_config_values(preload->istate->repo); + size_t i; + int stat_candidates = cfg->trust_ctime && cfg->check_stat; + + for (i = 0; i < preload->nr; i++) { + struct untracked_cache_preload_task *task = &preload->tasks[i]; + struct strbuf exclude_path = STRBUF_INIT; + struct cache_entry *ce; + int pos; + + if (!preload->exclude_per_dir) + continue; + if (strcmp(task->path, ".")) + strbuf_addstr(&exclude_path, task->path); + if (exclude_path.len) + strbuf_addch(&exclude_path, '/'); + strbuf_addstr(&exclude_path, preload->exclude_per_dir); + pos = index_name_pos_sparse( + preload->istate, exclude_path.buf, + exclude_path.len); + if (pos < 0) + goto next; + ce = preload->istate->cache[pos]; + if (!S_ISREG(ce->ce_mode) || + (!(ce->ce_flags & CE_FSMONITOR_VALID) && + fstat_is_reliable()) || + ce_skip_worktree(ce) || + (ce->ce_flags & CE_REMOVE) || + ce_intent_to_add(ce)) + goto next; + oidcpy(&preload->exclude_index_oids[i], &ce->oid); + task->exclude_index_present = 1; + if (!stat_candidates || + is_racy_timestamp(preload->istate, ce) || + (ce->ce_flags & CE_VALID)) + goto next; + /* + * Snapshot before launching workers. The main thread may + * refresh cache entries while exclude checks run. + */ + task->stat_data = ce->ce_stat_data; + task->exclude_index_candidate = 1; + if (preload->normalized_mutex_initialized && + fstat_is_reliable() && !ce_stage(ce) && + !oideq(&ce->oid, &task->exclude_oid)) { + struct normalized_exclude_oid *entry = + oidmap_get(&preload->normalized_excludes, &ce->oid); + + if (!entry) { + CALLOC_ARRAY(entry, 1); + oidcpy(&entry->ent.oid, &ce->oid); + oidmap_put(&preload->normalized_excludes, entry); + } + entry->candidates++; + task->normalized_oid = entry; + } +next: + strbuf_release(&exclude_path); + } + /* A unique blob has no other observation to share. */ + for (i = 0; i < preload->nr; i++) { + struct untracked_cache_preload_task *task = &preload->tasks[i]; + + if (task->normalized_oid && task->normalized_oid->candidates < 2) + task->normalized_oid = NULL; + } +} + +static int preload_normalized_exclude_matches( + struct untracked_cache_preload *preload, + const struct untracked_cache_preload_task *task) +{ + const struct normalized_exclude_oid *entry = task->normalized_oid; + int matches; + + if (!entry) + return 0; + pthread_mutex_lock(&preload->normalized_mutex); + matches = entry->valid && oideq(&entry->normalized, &task->exclude_oid); + pthread_mutex_unlock(&preload->normalized_mutex); + return matches; +} + +static void preload_remember_normalized_exclude( + struct untracked_cache_preload *preload, + struct untracked_cache_preload_task *task, + const struct object_id *raw, const struct object_id *normalized) +{ + struct normalized_exclude_oid *entry = task->normalized_oid; + + if (!entry || !oideq(raw, &entry->ent.oid) || + !oideq(normalized, &task->exclude_oid)) + return; + pthread_mutex_lock(&preload->normalized_mutex); + if (!entry->valid) { + oidcpy(&entry->normalized, normalized); + entry->valid = 1; + preload->normalized_objects++; + } + pthread_mutex_unlock(&preload->normalized_mutex); +} + +static struct untracked_cache_preload *untracked_cache_preload_start_1( + struct index_state *istate, unsigned int dir_flags, int automatic, + int fsmonitor_excludes_only, const struct pathspec *pathspec) +{ + struct untracked_cache *uc = istate->untracked; + struct untracked_cache_preload *preload; + struct strbuf path = STRBUF_INIT; + size_t alloc = 0, offset = 0, work, i; + unsigned long test_threads; + int threads, online, create_threads = 1; + + if (!uc || !uc->root || uc->dir_flags != dir_flags || + (fsmonitor_excludes_only ? + !uc->use_fsmonitor : + uc->use_fsmonitor)) + return NULL; + + CALLOC_ARRAY(preload, 1); + preload->repo = istate->repo; + preload->istate = istate; + preload->uc = uc; + preload->root = uc->root; + preload->pathspec = fsmonitor_excludes_only ? + untracked_cache_preload_pathspec(pathspec) : NULL; + preload->index_timestamp = istate->timestamp; + preload->exclude_per_dir = xstrdup_or_null(uc->exclude_per_dir); + preload->dir_flags = dir_flags; + preload->fsmonitor_excludes_only = fsmonitor_excludes_only; + collect_untracked_cache_preload_tasks( + uc->root, &path, &preload->tasks, &preload->nr, &alloc, + fsmonitor_excludes_only, preload->pathspec); + strbuf_release(&path); + if (fsmonitor_excludes_only) { + if (!HAVE_THREADS || + !pthread_mutex_init(&preload->normalized_mutex, NULL)) { + preload->normalized_mutex_initialized = 1; + oidmap_init(&preload->normalized_excludes, 0); + } + CALLOC_ARRAY(preload->exclude_index_oids, preload->nr); + preload_fsmonitor_excludes_from_index(preload); + } + + threads = HAVE_THREADS ? + preload->nr / (fsmonitor_excludes_only ? + UNTRACKED_CACHE_EXCLUDE_PRELOAD_COST : + UNTRACKED_CACHE_PRELOAD_COST) : + 1; + online = HAVE_THREADS ? online_cpus() : 1; + if (threads > online * 3) + threads = online * 3; + if (threads > UNTRACKED_CACHE_MAX_OVERLAP_THREADS) + threads = UNTRACKED_CACHE_MAX_OVERLAP_THREADS; + test_threads = git_env_ulong("GIT_TEST_UNTRACKED_CACHE_THREADS", 0); + if (test_threads && HAVE_THREADS) { + unsigned long limit = fsmonitor_excludes_only ? + UNTRACKED_CACHE_MAX_OVERLAP_THREADS : + UNTRACKED_CACHE_MAX_RECOVERY_THREADS; + + threads = test_threads > limit ? limit : test_threads; + } + if (threads < 1) + threads = 1; + if (preload->nr && (size_t)threads > preload->nr) + threads = preload->nr; + preload->threads = threads; + + preload->started_at = getnanotime(); + trace2_data_intmax("dir", istate->repo, + "preload_untracked_cache/threads", threads); + trace2_data_intmax("dir", istate->repo, + "preload_untracked_cache/automatic", automatic); + trace2_data_intmax("dir", istate->repo, + "preload_untracked_cache/fsmonitor-excludes-only", + fsmonitor_excludes_only); + CALLOC_ARRAY(preload->data, threads); + work = DIV_ROUND_UP(preload->nr, threads); + for (i = 0; i < threads; i++) { + struct untracked_cache_preload_data *data = &preload->data[i]; + int err; + + data->preload = preload; + data->offset = offset; + data->nr = offset < preload->nr ? + (preload->nr - offset < work ? + preload->nr - offset : work) : 0; + offset += data->nr; + if (threads == 1 || !create_threads) + continue; + err = pthread_create(&data->pthread, NULL, + preload_untracked_cache_thread, data); + if (err) { + create_threads = 0; + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/thread_failure", err); + continue; + } + data->started = 1; + } + return preload; +} + +struct untracked_cache_preload * +untracked_cache_preload_start_fsmonitor_excludes( + struct index_state *istate, unsigned int dir_flags, + const struct pathspec *pathspec) +{ + return untracked_cache_preload_start_1( + istate, dir_flags, 0, 1, pathspec); +} + +struct untracked_cache_preload *untracked_cache_preload_start_ordinary( + struct index_state *istate, unsigned int dir_flags) +{ + struct untracked_cache *uc = istate->untracked; + + if (!uc || uc->dir_flags != dir_flags || + !untracked_cache_auto_preload_worthwhile(uc)) + return NULL; + return untracked_cache_preload_start_1(istate, dir_flags, 1, 0, NULL); +} + +static void *preload_untracked_cache_thread(void *_data) +{ + struct untracked_cache_preload_data *data = _data; + struct untracked_cache_preload *preload = data->preload; + size_t i; + + for (i = data->offset; i < data->offset + data->nr; i++) { + struct untracked_cache_preload_task *task = &preload->tasks[i]; + struct strbuf exclude_path = STRBUF_INIT; + struct stat st; + + if (preload->fsmonitor_excludes_only) { + struct object_id raw_oid, normalized_oid; + int normalized_equivalent; + + if (!preload->exclude_per_dir) + continue; + if (strcmp(task->path, ".")) + strbuf_addstr(&exclude_path, task->path); + if (exclude_path.len) + strbuf_addch(&exclude_path, '/'); + strbuf_addstr(&exclude_path, + preload->exclude_per_dir); + normalized_equivalent = + preload_normalized_exclude_matches(preload, task); + if (task->exclude_index_candidate && + (normalized_equivalent || + oideq(&preload->exclude_index_oids[i], + &task->exclude_oid)) && + !lstat(exclude_path.buf, &st) && + cached_exclude_file_matches_index_stat( + &task->stat_data, &st)) { + task->exclude_mode = st.st_mode; + task->exclude_index_matches = 1; + task->exclude_index_content_matches = 1; + task->exclude_matches = 1; + task->exclude_index_normalized_equivalent = + normalized_equivalent; + task->normalize_exclude_oid = normalized_equivalent; + strbuf_release(&exclude_path); + continue; + } + task->exclude_matches = cached_exclude_file_matches( + preload->repo->hash_algo, + exclude_path.buf, + &task->exclude_oid, &raw_oid, + task->normalized_oid ? &normalized_oid : NULL, + &task->exclude_mode); + if (task->exclude_matches && task->normalized_oid) + preload_remember_normalized_exclude( + preload, task, &raw_oid, &normalized_oid); + if (task->exclude_matches && + task->exclude_index_present && + oideq(&preload->exclude_index_oids[i], + &raw_oid)) { + task->exclude_index_content_matches = 1; + if (!oideq(&preload->exclude_index_oids[i], + &task->exclude_oid)) + task->normalize_exclude_oid = 1; + } + strbuf_release(&exclude_path); + continue; + } + if (!task->was_valid) + continue; + task->stat_checked = 1; + if (lstat(task->path, &st)) { + memset(&task->stat_data, 0, sizeof(task->stat_data)); + task->update_stat_data = 1; + continue; + } + if (!match_untracked_dir_stat_racy( + &preload->index_timestamp, &task->stat_data, &st)) { + task->stat_matches = 1; + } else { + fill_stat_data(&task->stat_data, &st); + task->update_stat_data = 1; + continue; + } + if (is_null_oid(&task->exclude_oid)) { + task->exclude_matches = 1; + continue; + } + if (!preload->exclude_per_dir) + continue; + if (strcmp(task->path, ".")) + strbuf_addstr(&exclude_path, task->path); + if (exclude_path.len) + strbuf_addch(&exclude_path, '/'); + strbuf_addstr(&exclude_path, preload->exclude_per_dir); + task->exclude_matches = cached_exclude_file_matches( + preload->repo->hash_algo, exclude_path.buf, + &task->exclude_oid, NULL, NULL, NULL); + strbuf_release(&exclude_path); + } + return NULL; +} + +static int untracked_cache_has_collapsed_child( + const struct untracked_cache_dir *parent, + const struct untracked_cache_dir *child) +{ + size_t i, len = strlen(child->name); + + for (i = 0; i < parent->untracked_nr; i++) { + const char *name = parent->untracked[i]; + + if (strlen(name) == len + 1 && name[len] == '/' && + !strncmp(name, child->name, len)) + return 1; + } + return 0; +} + +static int compute_untracked_cache_valid_recursive( + struct untracked_cache *uc, + struct untracked_cache_dir *ucd, + int invalidate_ancestors) +{ + size_t i; + int local_valid = ucd->valid && ucd->stat_checked && + ucd->stat_matches && ucd->exclude_matches; + int valid = local_valid; + int invalidate_self = !local_valid; + + for (i = 0; i < ucd->dirs_nr; i++) { + int child_valid = compute_untracked_cache_valid_recursive( + uc, ucd->dirs[i], invalidate_ancestors); + + if (!child_valid) { + valid = 0; + if (untracked_cache_has_collapsed_child(ucd, ucd->dirs[i])) + invalidate_self = 1; + } + } + if (invalidate_self && invalidate_ancestors) { + if (!local_valid && ucd->valid && ucd->stat_checked && + ucd->stat_matches && !ucd->exclude_matches) + invalidate_gitignore(uc, ucd); + else + invalidate_directory(uc, ucd); + } + ucd->valid_recursive = valid; + return valid; +} + +static int compute_untracked_cache_disk_valid_recursive( + struct untracked_cache_dir *ucd) +{ + size_t i; + int valid = ucd->valid && is_null_oid(&ucd->exclude_oid); + + for (i = 0; i < ucd->dirs_nr; i++) + if (!compute_untracked_cache_disk_valid_recursive(ucd->dirs[i])) + valid = 0; + ucd->valid_recursive = valid; + return valid; +} + +static int compute_untracked_cache_fsmonitor_valid_recursive( + struct untracked_cache_dir *ucd) +{ + size_t i; + int valid = ucd->valid && !ucd->fsmonitor_dirty; + int has_untracked = !!ucd->untracked_nr; + + for (i = 0; i < ucd->dirs_nr; i++) { + if (!compute_untracked_cache_fsmonitor_valid_recursive( + ucd->dirs[i])) + valid = 0; + if (ucd->dirs[i]->recurse && ucd->dirs[i]->has_untracked) + has_untracked = 1; + } + ucd->valid_recursive = valid; + ucd->has_untracked = has_untracked; + return valid; +} + +void untracked_cache_recompute_fsmonitor_valid_recursive( + struct untracked_cache *uc) +{ + if (!uc || !uc->root) + return; + compute_untracked_cache_fsmonitor_valid_recursive(uc->root); +} + +static void untracked_cache_preload_join( + struct untracked_cache_preload *preload) +{ + int i; + + if (preload->threads == 1) { + preload_untracked_cache_thread(&preload->data[0]); + return; + } + for (i = 0; i < preload->threads; i++) { + if (!preload->data[i].started) { + preload_untracked_cache_thread(&preload->data[i]); + continue; + } + if (pthread_join(preload->data[i].pthread, NULL)) + die(_("unable to join untracked-cache preload thread")); + } +} + +static void untracked_cache_preload_free( + struct untracked_cache_preload *preload) +{ + size_t i; + + trace2_data_intmax("dir", preload->repo, + "preload_untracked_cache/dirs", preload->nr); + trace2_data_intmax("dir", preload->repo, + "preload_untracked_cache/wall_us", + (getnanotime() - preload->started_at) / 1000); + if (preload->fsmonitor_excludes_only) + trace2_data_intmax("dir", preload->repo, + "preload_untracked_cache/index-normalized-objects", + preload->normalized_objects); + if (preload->normalized_mutex_initialized) { + pthread_mutex_destroy(&preload->normalized_mutex); + oidmap_clear(&preload->normalized_excludes, 1); + } + free(preload->data); + for (i = 0; i < preload->nr; i++) + free(preload->tasks[i].path); + free(preload->tasks); + free(preload->exclude_index_oids); + free(preload->exclude_per_dir); + free(preload); +} + +static int converted_exclude_matches_cache_and_index( + struct untracked_cache_preload *preload, + struct untracked_cache_preload_task *task, + struct cache_entry *ce, + const char *path) +{ + struct object_id converted_oid, normalized_oid, raw_oid; + struct stat before, after; + char *buf = NULL; + size_t size; + int converted_fd, fd = -1; + int cached_matches, ret = 0; + + fd = open_nofollow(path, O_RDONLY); + if (fd < 0 || fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_size < 0 || + before.st_size > UNTRACKED_CACHE_MAX_EXCLUDE_SIZE) + goto done; + size = xsize_t(before.st_size); + buf = xmallocz(size + 1); + if (read_in_full(fd, buf, size) != size) + goto done; + + hash_object_file(preload->repo->hash_algo, buf, size, OBJ_BLOB, + &raw_oid); + cached_matches = oideq(&raw_oid, &task->exclude_oid); + if (!cached_matches) { + buf[size] = '\n'; + hash_object_file(preload->repo->hash_algo, buf, size + 1, + OBJ_BLOB, &normalized_oid); + cached_matches = oideq(&normalized_oid, + &task->exclude_oid); + } + if (!cached_matches || lseek(fd, 0, SEEK_SET) < 0) + goto done; + + converted_fd = xdup(fd); + if (index_fd(preload->istate, &converted_oid, converted_fd, &before, + OBJ_BLOB, path, 0) || + !oideq(&converted_oid, &ce->oid)) + goto done; + + /* + * Keep the descriptor open while computing both identities, then + * prove that neither the opened file nor its pathname changed. + */ + if (fstat(fd, &after) || + !path_namespace_stat_equal(&before, &after) || + !exclude_path_matches_fd(path, &after)) + goto done; + fill_stat_data(&task->stat_data, &after); + task->exclude_mode = after.st_mode; + ret = 1; +done: + free(buf); + if (fd >= 0) + close(fd); + return ret; +} + +static int update_preloaded_exclude_index_uptodate( + struct untracked_cache_preload *preload, + struct untracked_cache_preload_task *task, + size_t task_nr, + size_t *normalized, + size_t *index_invalidated, + int *exclude_revalidated) +{ + struct strbuf path = STRBUF_INIT; + struct cache_entry *ce; + int content_matches, converts, pos, marked = 0; + + *exclude_revalidated = -1; + if (!task->exclude_index_present || !preload->exclude_per_dir) + return 0; + if (strcmp(task->path, ".")) + strbuf_addstr(&path, task->path); + if (path.len) + strbuf_addch(&path, '/'); + strbuf_addstr(&path, preload->exclude_per_dir); + pos = index_name_pos_sparse(preload->istate, path.buf, path.len); + if (pos < 0) + goto done; + ce = preload->istate->cache[pos]; + if (!ce_stage(ce) && S_ISREG(ce->ce_mode) && + oideq(&ce->oid, &preload->exclude_index_oids[task_nr])) { + if (task->exclude_index_matches && + !task->exclude_index_normalized_equivalent) { + converts = 0; + content_matches = 1; + } else { + /* + * The normalized relation only describes raw blob bytes. + * Keep conversion checks on the main thread and revalidate + * the actual file when the current path converts. + */ + converts = would_convert_to_git( + preload->istate, path.buf); + content_matches = converts ? + converted_exclude_matches_cache_and_index( + preload, task, ce, path.buf) : + task->exclude_index_content_matches; + if (converts) + *exclude_revalidated = content_matches; + } + if (!converts && task->normalize_exclude_oid) { + oidcpy(&task->ucd->exclude_oid, + &preload->exclude_index_oids[task_nr]); + (*normalized)++; + } + if (content_matches && + (ce->ce_flags & CE_FSMONITOR_VALID) && + !ce_skip_worktree(ce) && + !(ce->ce_flags & CE_REMOVE) && + !ce_intent_to_add(ce) && + (!repo_trust_executable_bit(preload->istate->repo) || + !((ce->ce_mode ^ task->exclude_mode) & 0100))) { + if (converts && + memcmp(&ce->ce_stat_data, &task->stat_data, + sizeof(ce->ce_stat_data))) { + ce->ce_stat_data = task->stat_data; + ce->ce_flags |= CE_UPDATE_IN_BASE; + preload->istate->cache_changed |= + CE_ENTRY_CHANGED; + } + ce_mark_uptodate(ce); + marked = 1; + } else { + fsmonitor_invalidate_cache_entry(ce); + preload->istate->cache_changed |= FSMONITOR_CHANGED; + (*index_invalidated)++; + } + } +done: + strbuf_release(&path); + return marked; +} + +static void invalidate_scoped_preloaded_exclude( + struct untracked_cache_preload *preload, + const struct untracked_cache_preload_task *task) +{ + struct strbuf path = STRBUF_INIT; + + if (!preload->exclude_per_dir) { + preload->root->valid = 0; + preload->root->valid_recursive = 0; + return; + } + if (strcmp(task->path, ".")) { + strbuf_addstr(&path, task->path); + strbuf_addch(&path, '/'); + } + strbuf_addstr(&path, preload->exclude_per_dir); + untracked_cache_invalidate_path(preload->istate, path.buf, 1); + strbuf_release(&path); +} + +int untracked_cache_preload_finish(struct untracked_cache_preload *preload, + struct index_state *istate, + unsigned int dir_flags, + size_t *index_invalidated) +{ + struct untracked_cache *uc; + size_t i; + int applied = 0; + + if (index_invalidated) + *index_invalidated = 0; + if (!preload) + return 0; + untracked_cache_preload_join(preload); + uc = istate->untracked; + if (uc != preload->uc || !uc || uc->root != preload->root || + dir_flags != preload->dir_flags || + (preload->fsmonitor_excludes_only && !uc->use_fsmonitor)) + goto done; + + if (preload->fsmonitor_excludes_only) { + size_t index_matches = 0; + size_t normalized_matches = 0; + size_t invalidated = 0; + size_t index_uptodate = 0; + size_t normalized = 0; + + for (i = 0; i < preload->nr; i++) { + struct untracked_cache_preload_task *task = + &preload->tasks[i]; + int exclude_matches = + oideq(&task->exclude_oid, + &task->ucd->exclude_oid) && + task->exclude_matches; + int exclude_invalidated = !exclude_matches; + int exclude_revalidated; + int index_marked; + + if (!exclude_matches) + invalidate_gitignore(uc, task->ucd); + else { + if (task->exclude_index_matches) + index_matches++; + } + index_marked = + update_preloaded_exclude_index_uptodate( + preload, task, i, &normalized, + &invalidated, &exclude_revalidated); + index_uptodate += index_marked; + if (index_marked && task->exclude_index_matches && + task->exclude_index_normalized_equivalent && + exclude_revalidated < 0) + normalized_matches++; + if (exclude_matches && exclude_revalidated == 0) { + invalidate_gitignore(uc, task->ucd); + exclude_invalidated = 1; + } + if (preload->pathspec && exclude_invalidated) + invalidate_scoped_preloaded_exclude(preload, task); + } + if (normalized) + istate->cache_changed |= UNTRACKED_CHANGED; + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/index-excludes", + index_matches); + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/index-normalized-excludes", + normalized_matches); + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/index-uptodate", + index_uptodate); + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/index-invalidated", + invalidated); + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/normalized-excludes", + normalized); + trace2_data_intmax( + "dir", istate->repo, + "preload_untracked_cache/valid", + preload->pathspec ? preload->root->valid_recursive : + compute_untracked_cache_fsmonitor_valid_recursive( + preload->root)); + if (index_invalidated) + *index_invalidated = invalidated; + applied = 1; + goto done; + } + + for (i = 0; i < preload->nr; i++) { + struct untracked_cache_preload_task *task = &preload->tasks[i]; + struct untracked_cache_dir *ucd = task->ucd; + + ucd->stat_checked = 0; + ucd->stat_matches = 0; + ucd->exclude_matches = 0; + ucd->valid_recursive = 0; + /* Invalidation performed after the snapshot always wins. */ + if (!task->was_valid || !ucd->valid) + continue; + ucd->stat_checked = task->stat_checked; + ucd->stat_matches = task->stat_matches; + ucd->exclude_matches = task->exclude_matches; + if (task->update_stat_data) + ucd->stat_data = task->stat_data; + } + trace2_data_intmax("dir", istate->repo, + "preload_untracked_cache/valid", + compute_untracked_cache_valid_recursive( + uc, preload->root, + dir_flags & DIR_SHOW_OTHER_DIRECTORIES)); + applied = 1; +done: + trace2_data_intmax("dir", istate->repo, + "preload_untracked_cache/applied", applied); + untracked_cache_preload_free(preload); + return applied; +} + +void untracked_cache_preload_release(struct untracked_cache_preload *preload) +{ + if (!preload) + return; + untracked_cache_preload_join(preload); + untracked_cache_preload_free(preload); +} + +/* + * Unlike cache entries, an untracked-cache directory has no later content + * check to correct a false stat match. Compare every field saved in UNTR, + * regardless of core.checkStat or core.trustCtime. + */ +static int match_untracked_dir_stat(const struct stat_data *sd, + const struct stat *st) +{ + return !S_ISDIR(st->st_mode) || + sd->sd_ctime.sec != (unsigned int)st->st_ctime || + sd->sd_ctime.nsec != ST_CTIME_NSEC(*st) || + sd->sd_mtime.sec != (unsigned int)st->st_mtime || + sd->sd_mtime.nsec != ST_MTIME_NSEC(*st) || + sd->sd_dev != (unsigned int)st->st_dev || + sd->sd_ino != (unsigned int)st->st_ino || + sd->sd_uid != (unsigned int)st->st_uid || + sd->sd_gid != (unsigned int)st->st_gid || + sd->sd_size != (unsigned int)st->st_size; +} + +static int match_untracked_dir_stat_racy(const struct cache_time *timestamp, + const struct stat_data *sd, + const struct stat *st) +{ + if (timestamp->sec && +#ifdef USE_NSEC + (timestamp->sec < sd->sd_mtime.sec || + (timestamp->sec == sd->sd_mtime.sec && + timestamp->nsec <= sd->sd_mtime.nsec))) +#else + timestamp->sec <= sd->sd_mtime.sec) +#endif + return MTIME_CHANGED; + return match_untracked_dir_stat(sd, st); +} + static enum path_treatment read_directory_recursive(struct dir_struct *dir, struct index_state *istate, const char *path, int len, struct untracked_cache_dir *untracked, int check_only, int stop_at_first_file, const struct pathspec *pathspec); +static int resolve_dtype_with_error(int dtype, struct index_state *istate, + const char *path, int len, int *failed); static int resolve_dtype(int dtype, struct index_state *istate, const char *path, int len); struct dirent *readdir_skip_dot_and_dotdot(DIR *dirp) @@ -1098,6 +2188,9 @@ static void do_invalidate_gitignore(struct untracked_cache_dir *dir) { int i; dir->valid = 0; + dir->valid_recursive = 0; + dir->fsmonitor_dirty = 0; + dir->has_untracked = 0; for (size_t i = 0; i < dir->untracked_nr; i++) free(dir->untracked[i]); dir->untracked_nr = 0; @@ -1112,6 +2205,82 @@ static void invalidate_gitignore(struct untracked_cache *uc, do_invalidate_gitignore(dir); } +static void clear_untracked_cache_validation(struct untracked_cache_dir *dir) +{ + size_t i; + + dir->valid_recursive = 0; + dir->stat_checked = 0; + dir->stat_matches = 0; + dir->exclude_matches = 0; + for (i = 0; i < dir->dirs_nr; i++) + clear_untracked_cache_validation(dir->dirs[i]); +} + +static int ident_in_untracked(const struct untracked_cache *uc); + +int untracked_cache_preserve_for_revalidation(struct index_state *istate) +{ + struct untracked_cache *uc = istate->untracked; + const char *builtin_suffix, *pending_suffix; + int paired, pending; + + if (!uc || !uc->root || !uc->root->valid || + uc->fsmonitor_dirty_paths.len || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + !istate->fsmonitor_last_update || + !istate->fsmonitor_untracked_token) + return 0; + + paired = istate->fsmonitor_untracked_valid && + uc->root->valid_recursive && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token); + pending = !istate->fsmonitor_untracked_valid && + istate == istate->repo->index && + !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + (fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC || + fsm_settings__is_watch_limit_backoff(istate->repo)) && + (!getenv(GIT_WORK_TREE_ENVIRONMENT) || + ident_in_untracked(uc)) && + skip_prefix(istate->fsmonitor_last_update, + "builtin:", &builtin_suffix) && + *builtin_suffix && strcmp(builtin_suffix, "fake") && + skip_prefix(istate->fsmonitor_untracked_token, + "pending:", &pending_suffix) && + !strcmp(builtin_suffix, pending_suffix); + if (!paired && !pending) + return 0; + + /* + * The paired provider proof authenticates these previously complete + * lists, but its boundary is no longer replayable. Keep their ordinary + * directory snapshots as candidates; every directory and exclude + * source must be revalidated before a fresh provider token is closed. + */ + clear_untracked_cache_validation(uc->root); + uc->use_fsmonitor = 0; + uc->fsmonitor_revalidation = 1; + if (paired || istate->fsmonitor_untracked_revalidation_authenticated) + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/provider-reset-preserved", 1); + return 1; +} + +void untracked_cache_invalidate_all(struct index_state *istate) +{ + istate->fsmonitor_untracked_revalidation_authenticated = 0; + if (!istate->untracked || !istate->untracked->root) + return; + invalidate_gitignore(istate->untracked, istate->untracked->root); + istate->untracked->use_fsmonitor = 0; + istate->untracked->fsmonitor_revalidation = 0; + istate->cache_changed |= UNTRACKED_CHANGED; +} + static void invalidate_directory(struct untracked_cache *uc, struct untracked_cache_dir *dir) { @@ -1127,6 +2296,8 @@ static void invalidate_directory(struct untracked_cache *uc, uc->dir_invalidated++; dir->valid = 0; + dir->valid_recursive = 0; + dir->fsmonitor_dirty = 0; for (size_t i = 0; i < dir->untracked_nr; i++) free(dir->untracked[i]); dir->untracked_nr = 0; @@ -1148,34 +2319,63 @@ static void invalidate_directory(struct untracked_cache *uc, */ static int add_patterns(const char *fname, const char *base, int baselen, struct pattern_list *pl, struct index_state *istate, - unsigned flags, struct oid_stat *oid_stat) + unsigned flags, struct oid_stat *oid_stat, + struct exclude_source_proof *source_proof) { + struct exclude_source_capture *capture = + exclude_source_capture_begin(source_proof, fname, + !!(flags & PATTERN_NOFOLLOW)); struct stat st; int r; int fd; size_t size = 0; char *buf; - if (flags & PATTERN_NOFOLLOW) + if (capture) + fd = exclude_source_capture_open(capture); + else if (flags & PATTERN_NOFOLLOW) fd = open_nofollow(fname, O_RDONLY); else fd = open(fname, O_RDONLY); if (fd < 0 || fstat(fd, &st) < 0) { - if (fd < 0) + if (fd < 0) { warn_on_fopen_errors(fname); - else + if (capture && exclude_source_capture_absent(capture)) + exclude_source_capture_record(capture, -1, NULL, + NULL, 0); + else + exclude_source_capture_error(capture); + } else { + exclude_source_capture_error(capture); close(fd); - if (!istate) + } + if (!istate) { + exclude_source_capture_release(capture); return -1; + } r = read_skip_worktree_file_from_index(istate, fname, &size, &buf, oid_stat); + if (r == 1) + exclude_source_capture_error(capture); + exclude_source_capture_release(capture); + capture = NULL; if (r != 1) return r; } else { size = xsize_t(st.st_size); + if (size > PATTERN_MAX_FILE_SIZE) { + exclude_source_capture_error(capture); + exclude_source_capture_release(capture); + warning("ignoring excessively large pattern file: %s", + fname); + close(fd); + return -1; + } if (size == 0) { + exclude_source_capture_record(capture, fd, &st, NULL, 0); + exclude_source_capture_release(capture); if (oid_stat) { fill_stat_data(&oid_stat->stat, &st); oidcpy(&oid_stat->oid, the_hash_algo->empty_blob); @@ -1186,10 +2386,15 @@ static int add_patterns(const char *fname, const char *base, int baselen, } buf = xmallocz(size); if (read_in_full(fd, buf, size) != size) { + exclude_source_capture_error(capture); + exclude_source_capture_release(capture); free(buf); close(fd); return -1; } + exclude_source_capture_record(capture, fd, &st, buf, size); + exclude_source_capture_release(capture); + capture = NULL; buf[size++] = '\n'; close(fd); if (oid_stat) { @@ -1201,6 +2406,10 @@ static int add_patterns(const char *fname, const char *base, int baselen, (pos = index_name_pos(istate, fname, strlen(fname))) >= 0 && !ce_stage(istate->cache[pos]) && ce_uptodate(istate->cache[pos]) && + !(istate->cache[pos]->ce_flags & + (CE_VALID | CE_REMOVE)) && + !ce_skip_worktree(istate->cache[pos]) && + !ce_intent_to_add(istate->cache[pos]) && !would_convert_to_git(istate, fname)) oidcpy(&oid_stat->oid, &istate->cache[pos]->oid); @@ -1258,7 +2467,8 @@ int add_patterns_from_file_to_list(const char *fname, const char *base, struct index_state *istate, unsigned flags) { - return add_patterns(fname, base, baselen, pl, istate, flags, NULL); + return add_patterns(fname, base, baselen, pl, istate, flags, NULL, + NULL); } int add_patterns_from_blob_to_list( @@ -1303,10 +2513,11 @@ struct pattern_list *add_pattern_list(struct dir_struct *dir, /* * Used to set up core.excludesfile and .git/info/exclude lists. */ -static void add_patterns_from_file_1(struct dir_struct *dir, const char *fname, - struct oid_stat *oid_stat) +static int add_patterns_from_file_1(struct dir_struct *dir, const char *fname, + struct oid_stat *oid_stat, int gentle) { struct pattern_list *pl; + int ret; /* * catch setup_standard_excludes() that's called before * dir->untracked is assigned. That function behaves @@ -1315,14 +2526,17 @@ static void add_patterns_from_file_1(struct dir_struct *dir, const char *fname, if (!dir->untracked) dir->internal.unmanaged_exclude_files++; pl = add_pattern_list(dir, EXC_FILE, fname); - if (add_patterns(fname, "", 0, pl, NULL, 0, oid_stat) < 0) + ret = add_patterns(fname, "", 0, pl, NULL, 0, oid_stat, + dir->internal.exclude_source_proof); + if (ret < 0 && !gentle) die(_("cannot use %s as an exclude file"), fname); + return ret; } void add_patterns_from_file(struct dir_struct *dir, const char *fname) { dir->internal.unmanaged_exclude_files++; /* see validate_untracked_cache() */ - add_patterns_from_file_1(dir, fname, NULL); + add_patterns_from_file_1(dir, fname, NULL, 0); } int match_basename(const char *basename, int basenamelen, @@ -1770,9 +2984,25 @@ static void prep_exclude(struct dir_struct *dir, strbuf_addbuf(&sb, &dir->internal.basebuf); strbuf_addstr(&sb, dir->exclude_per_dir); pl->src = strbuf_detach(&sb, NULL); - add_patterns(pl->src, pl->src, stk->baselen, pl, istate, - PATTERN_NOFOLLOW, - untracked ? &oid_stat : NULL); + if (add_patterns(pl->src, pl->src, stk->baselen, pl, + istate, PATTERN_NOFOLLOW, + untracked ? &oid_stat : NULL, + dir->internal.exclude_source_proof) < 0 && + untracked && is_null_oid(&oid_stat.oid)) { + struct stat st; + + /* + * Keep a non-blob sentinel for a source that is + * present but unreadable. Otherwise a valid + * untracked-cache directory cannot distinguish + * that state from an absent per-directory + * exclude file. + */ + if (!lstat(pl->src, &st) || + !is_missing_file_error(errno)) + oidcpy(&oid_stat.oid, + the_hash_algo->empty_tree); + } } /* * NEEDSWORK: when untracked cache is enabled, prep_exclude() @@ -1790,7 +3020,19 @@ static void prep_exclude(struct dir_struct *dir, */ if (untracked && !oideq(&oid_stat.oid, &untracked->exclude_oid)) { - invalidate_gitignore(dir->untracked, untracked); + struct object_id raw_oid, normalized_oid; + int compatible; + + /* Older caches include the parser's synthetic final LF. */ + compatible = oid_stat.valid && + cached_exclude_file_matches(the_hash_algo, pl->src, + &untracked->exclude_oid, + &raw_oid, &normalized_oid, + NULL) && + (oideq(&raw_oid, &oid_stat.oid) || + oideq(&normalized_oid, &oid_stat.oid)); + if (!compatible) + invalidate_gitignore(dir->untracked, untracked); oidcpy(&untracked->exclude_oid, &oid_stat.oid); } dir->internal.exclude_stack = stk; @@ -2367,6 +3609,12 @@ unsigned char get_dtype(struct dirent *e, struct strbuf *path, static int resolve_dtype(int dtype, struct index_state *istate, const char *path, int len) +{ + return resolve_dtype_with_error(dtype, istate, path, len, NULL); +} + +static int resolve_dtype_with_error(int dtype, struct index_state *istate, + const char *path, int len, int *failed) { struct stat st; @@ -2375,8 +3623,11 @@ static int resolve_dtype(int dtype, struct index_state *istate, dtype = get_index_dtype(istate, path, len); if (dtype != DT_UNKNOWN) return dtype; - if (lstat(path, &st)) + if (lstat(path, &st)) { + if (failed && !is_missing_file_error(errno)) + *failed = 1; return dtype; + } if (S_ISREG(st.st_mode)) return DT_REG; if (S_ISDIR(st.st_mode)) @@ -2432,6 +3683,7 @@ static enum path_treatment treat_path(struct dir_struct *dir, const struct pathspec *pathspec) { int has_path_in_index, dtype, excluded; + int dtype_failed = 0; if (!cdir->d_name) return treat_path_fast(dir, cdir, istate, path, @@ -2443,7 +3695,11 @@ static enum path_treatment treat_path(struct dir_struct *dir, if (simplify_away(path->buf, path->len, pathspec)) return path_none; - dtype = resolve_dtype(cdir->d_type, istate, path->buf, path->len); + dtype = resolve_dtype_with_error( + cdir->d_type, istate, path->buf, path->len, + &dtype_failed); + if (dtype_failed) + dir->internal.traversal_failed = 1; /* Always exclude indexed files */ has_path_in_index = !!index_file_exists(istate, path->buf, path->len, @@ -2518,6 +3774,97 @@ static void add_untracked(struct untracked_cache_dir *dir, const char *name) ALLOC_GROW(dir->untracked, dir->untracked_nr + 1, dir->untracked_alloc); dir->untracked[dir->untracked_nr++] = xstrdup(name); + dir->has_untracked = 1; +} + +static int refresh_cached_fsmonitor_files( + struct dir_struct *dir, + struct index_state *istate, + struct untracked_cache_dir *untracked, + struct strbuf *directory) +{ + struct untracked_cache *uc = dir->untracked; + struct strbuf path = STRBUF_INIT; + const char *event, *end; + size_t base_len, refreshed = 0; + int valid, untracked_changed = 0; + + if (!uc || !untracked->valid || !untracked->fsmonitor_dirty || + !uc->fsmonitor_dirty_paths.len) + return 0; + + strbuf_addbuf(&path, directory); + strbuf_complete(&path, '/'); + base_len = path.len; + event = uc->fsmonitor_dirty_paths.buf; + end = event + uc->fsmonitor_dirty_paths.len; + while (event < end) { + struct cached_dir cdir = { 0 }; + enum path_treatment state; + const char *name; + size_t i; + int was_untracked = 0; + + if (strncmp(event, path.buf, base_len)) + goto next; + name = event + base_len; + if (!*name || strchr(name, '/')) + goto next; + + for (i = 0; i < untracked->untracked_nr; i++) { + if (strcmp(untracked->untracked[i], name)) + continue; + free(untracked->untracked[i]); + MOVE_ARRAY(untracked->untracked + i, + untracked->untracked + i + 1, + untracked->untracked_nr - i - 1); + untracked->untracked_nr--; + was_untracked = 1; + break; + } + + cdir.d_name = name; + cdir.d_type = DT_UNKNOWN; + state = treat_path(dir, untracked, &cdir, istate, &path, + base_len, NULL); + dir->internal.visited_paths++; + if (state == path_recurse) { + strbuf_release(&path); + return 0; + } + if (state == path_untracked) + add_untracked(untracked, name); + if (was_untracked != (state == path_untracked)) + untracked_changed = 1; + refreshed++; + +next: + event += strlen(event) + 1; + strbuf_setlen(&path, base_len); + } + strbuf_release(&path); + if (!refreshed || !untracked->valid) + return 0; + + if (untracked_changed) { + istate->cache_changed |= UNTRACKED_CHANGED; + istate->fsmonitor_untracked_must_persist = 1; + } + untracked->fsmonitor_dirty = 0; + untracked->has_untracked = !!untracked->untracked_nr; + valid = untracked->valid; + for (size_t i = 0; i < untracked->dirs_nr; i++) { + struct untracked_cache_dir *child = untracked->dirs[i]; + + if (!child->valid_recursive) + valid = 0; + if (child->recurse && child->has_untracked) + untracked->has_untracked = 1; + } + untracked->valid_recursive = valid; + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/targeted-refresh", refreshed); + return 1; } static int valid_cached_dir(struct dir_struct *dir, @@ -2536,14 +3883,24 @@ static int valid_cached_dir(struct dir_struct *dir, */ refresh_fsmonitor(istate); if (!(dir->untracked->use_fsmonitor && untracked->valid)) { - if (lstat(path->len ? path->buf : ".", &st)) { - memset(&untracked->stat_data, 0, sizeof(untracked->stat_data)); - return 0; - } - if (!untracked->valid || - match_stat_data_racy(istate, &untracked->stat_data, &st)) { - fill_stat_data(&untracked->stat_data, &st); - return 0; + if (dir->internal.untracked_cache_preloaded && + untracked->stat_checked) { + if (!untracked->valid || !untracked->stat_matches || + !untracked->exclude_matches) + return 0; + } else { + if (lstat(path->len ? path->buf : ".", &st)) { + memset(&untracked->stat_data, 0, + sizeof(untracked->stat_data)); + return 0; + } + if (!untracked->valid || + match_untracked_dir_stat_racy( + &istate->timestamp, + &untracked->stat_data, &st)) { + fill_stat_data(&untracked->stat_data, &st); + return 0; + } } } @@ -2565,7 +3922,12 @@ static int valid_cached_dir(struct dir_struct *dir, prep_exclude(dir, istate, path->buf, path->len); /* hopefully prep_exclude() haven't invalidated this entry... */ - return untracked->valid; + if (!untracked->valid) + return 0; + if (untracked->fsmonitor_dirty && + !refresh_cached_fsmonitor_files(dir, istate, untracked, path)) + return 0; + return 1; } static int open_cached_dir(struct cached_dir *cdir, @@ -2583,8 +3945,10 @@ static int open_cached_dir(struct cached_dir *cdir, return 0; c_path = path->len ? path->buf : "."; cdir->fdir = opendir(c_path); - if (!cdir->fdir) + if (!cdir->fdir) { + dir->internal.traversal_failed = 1; warning_errno(_("could not open directory '%s'"), c_path); + } if (dir->untracked) { invalidate_directory(dir->untracked, untracked); dir->untracked->dir_opened++; @@ -2594,13 +3958,16 @@ static int open_cached_dir(struct cached_dir *cdir, return 0; } -static int read_cached_dir(struct cached_dir *cdir) +static int read_cached_dir(struct cached_dir *cdir, struct dir_struct *dir) { struct dirent *de; if (cdir->fdir) { + errno = 0; de = readdir_skip_dot_and_dotdot(cdir->fdir); if (!de) { + if (errno) + dir->internal.traversal_failed = 1; cdir->d_name = NULL; cdir->d_type = DT_UNKNOWN; return -1; @@ -2628,8 +3995,32 @@ static int read_cached_dir(struct cached_dir *cdir) return -1; } +static void remove_collapsed_untracked_child( + struct untracked_cache *uc, + struct untracked_cache_dir *parent, + const struct untracked_cache_dir *child) +{ + size_t i, len = strlen(child->name); + + for (i = 0; i < parent->untracked_nr; i++) { + char *name = parent->untracked[i]; + + if (strlen(name) != len + 1 || name[len] != '/' || + strncmp(name, child->name, len)) + continue; + free(name); + MOVE_ARRAY(parent->untracked + i, parent->untracked + i + 1, + parent->untracked_nr - i - 1); + parent->untracked_nr--; + uc->dir_invalidated++; + return; + } +} + static void close_cached_dir(struct cached_dir *cdir) { + int i; + if (cdir->fdir) closedir(cdir->fdir); /* @@ -2639,6 +4030,12 @@ static void close_cached_dir(struct cached_dir *cdir) if (cdir->untracked) { cdir->untracked->valid = 1; cdir->untracked->recurse = 1; + cdir->untracked->has_untracked = !!cdir->untracked->untracked_nr; + for (i = 0; !cdir->untracked->has_untracked && + i < cdir->untracked->dirs_nr; i++) + cdir->untracked->has_untracked = + cdir->untracked->dirs[i]->recurse && + cdir->untracked->dirs[i]->has_untracked; } } @@ -2710,6 +4107,16 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir, struct strbuf path = STRBUF_INIT; strbuf_add(&path, base, baselen); + if (untracked && + (dir->internal.untracked_cache_preloaded || + dir->untracked->use_fsmonitor) && + untracked->valid && untracked->valid_recursive && + untracked->check_only == !!check_only && + !untracked->has_untracked && + (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)) { + untracked->recurse = 1; + goto out; + } if (open_cached_dir(&cdir, dir, untracked, istate, &path, check_only)) goto out; @@ -2718,10 +4125,27 @@ static enum path_treatment read_directory_recursive(struct dir_struct *dir, if (untracked) untracked->check_only = !!check_only; - while (!read_cached_dir(&cdir)) { + while (!read_cached_dir(&cdir, dir)) { /* check how the file or directory should be treated */ state = treat_path(dir, untracked, &cdir, istate, &path, baselen, pathspec); + if (!cdir.d_name && cdir.ucd && cdir.ucd->check_only && + state < path_untracked && untracked && + untracked_cache_has_collapsed_child(untracked, cdir.ucd) && + (dir->flags & DIR_SHOW_OTHER_DIRECTORIES)) { + /* + * A collapsed directory retains one descendant as its + * untracked witness. Rescan before dropping a stale witness; + * an unvisited sibling may still make the directory untracked. + */ + invalidate_directory(dir->untracked, cdir.ucd); + state = read_directory_recursive( + dir, istate, path.buf, path.len, cdir.ucd, + 1, 0, pathspec); + if (state < path_untracked) + remove_collapsed_untracked_child( + dir->untracked, untracked, cdir.ucd); + } dir->internal.visited_paths++; if (state > dir_state) @@ -2879,6 +4303,8 @@ static int treat_leading_path(struct dir_struct *dir, return state == path_recurse; } +#define UNTRACKED_CACHE_IDENT_VERSION 2 + static const char *get_ident_string(void) { static struct strbuf sb = STRBUF_INIT; @@ -2888,8 +4314,9 @@ static const char *get_ident_string(void) return sb.buf; if (uname(&uts) < 0) die_errno(_("failed to get kernel name and information")); - strbuf_addf(&sb, "Location %s, system %s", repo_get_work_tree(the_repository), - uts.sysname); + strbuf_addf(&sb, "Location %s, system %s, cache version %d", + repo_get_work_tree(the_repository), uts.sysname, + UNTRACKED_CACHE_IDENT_VERSION); return sb.buf; } @@ -2904,6 +4331,19 @@ static int ident_in_untracked(const struct untracked_cache *uc) return !strcmp(uc->ident.buf, get_ident_string()); } +static int legacy_ident_in_untracked(const struct untracked_cache *uc) +{ + static const char suffix[] = ", cache version 2"; + const char *current = get_ident_string(); + size_t current_len = strlen(current); + size_t suffix_len = sizeof(suffix) - 1; + + return current_len > suffix_len && + !strcmp(current + current_len - suffix_len, suffix) && + strlen(uc->ident.buf) == current_len - suffix_len && + !memcmp(uc->ident.buf, current, current_len - suffix_len); +} + static void set_untracked_ident(struct untracked_cache *uc) { strbuf_reset(&uc->ident); @@ -2954,12 +4394,50 @@ void add_untracked_cache(struct index_state *istate) new_untracked_cache(istate, -1); } else { if (!ident_in_untracked(istate->untracked)) { + if (istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + starts_with(istate->fsmonitor_last_update, + "builtin:") && + !istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + !istate->split_index && + fsm_settings__get_mode(istate->repo) == + FSMONITOR_MODE_IPC && + legacy_ident_in_untracked(istate->untracked)) { + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/legacy-preserved", 1); + return; + } free_untracked_cache(istate->untracked); new_untracked_cache(istate, -1); } } } +int untracked_cache_adopt_legacy(struct index_state *istate) +{ + if (!istate->untracked || + !legacy_ident_in_untracked(istate->untracked)) + return 0; + set_untracked_ident(istate->untracked); + untracked_cache_recompute_fsmonitor_valid_recursive( + istate->untracked); + istate->cache_changed |= UNTRACKED_CHANGED; + return 1; +} + +void untracked_cache_discard_legacy(struct index_state *istate) +{ + if (!istate->untracked || + !legacy_ident_in_untracked(istate->untracked)) + return; + free_untracked_cache(istate->untracked); + new_untracked_cache(istate, -1); + istate->untracked->fsmonitor_legacy_discarded = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/legacy-discarded", 1); +} + void remove_untracked_cache(struct index_state *istate) { if (istate->untracked) { @@ -3023,7 +4501,9 @@ static struct untracked_cache_dir *validate_untracked_cache(struct dir_struct *d if (dir->internal.exclude_list_group[EXC_CMDL].nr) return NULL; - if (!ident_in_untracked(dir->untracked)) { + if (!ident_in_untracked(dir->untracked) && + !(istate->fsmonitor_legacy_untracked_fallback && + legacy_ident_in_untracked(dir->untracked))) { warning(_("untracked cache is disabled on this system or location")); return NULL; } @@ -3142,6 +4622,7 @@ int read_directory(struct dir_struct *dir, struct index_state *istate, dir->internal.visited_directories = 0; if (has_symlink_leading_path(path, len)) { + dir->internal.untracked_cache_preloaded = 0; trace2_region_leave("dir", "read_directory", istate->repo); return dir->nr; } @@ -3180,9 +4661,100 @@ int read_directory(struct dir_struct *dir, struct index_state *istate, } } + dir->internal.untracked_cache_preloaded = 0; return dir->nr; } +static void recompute_cached_fsmonitor_ancestors( + struct untracked_cache *uc, + const char *path, + int len) +{ + struct untracked_cache_dir **parents = NULL; + struct untracked_cache_dir *current = uc->root; + size_t nr = 0, alloc = 0; + int offset = 0; + + ALLOC_GROW(parents, nr + 1, alloc); + parents[nr++] = current; + while (offset < len) { + const char *slash; + int component_len; + + while (offset < len && path[offset] == '/') + offset++; + if (offset == len) + break; + slash = memchr(path + offset, '/', len - offset); + component_len = slash ? slash - (path + offset) : len - offset; + current = lookup_untracked(uc, current, + path + offset, component_len); + ALLOC_GROW(parents, nr + 1, alloc); + parents[nr++] = current; + offset += component_len; + } + + while (nr) { + struct untracked_cache_dir *parent = parents[--nr]; + int valid = parent->valid && !parent->fsmonitor_dirty; + int has_untracked = !!parent->untracked_nr; + + for (size_t i = 0; i < parent->dirs_nr; i++) { + struct untracked_cache_dir *child = parent->dirs[i]; + + if (!child->valid_recursive) + valid = 0; + if (child->recurse && child->has_untracked) + has_untracked = 1; + } + parent->valid_recursive = valid; + parent->has_untracked = has_untracked; + } + free(parents); +} + +int read_directory_cached_subtree(struct dir_struct *dir, + struct index_state *istate, + struct untracked_cache_dir *untracked, + const char *path, int len, + const struct pathspec *pathspec) +{ + int repaired = 0; + + if (!untracked || !dir->untracked || + dir->untracked != istate->untracked || + !dir->untracked->use_fsmonitor || + !istate->fsmonitor_untracked_valid || + has_symlink_leading_path(path, len)) + return -1; + + trace2_region_enter("dir", "read_cached_subtree", istate->repo); + dir->internal.visited_paths = 0; + dir->internal.visited_directories = 0; + if (treat_leading_path(dir, istate, path, len, pathspec)) { + if (untracked->valid && untracked->fsmonitor_dirty) { + struct strbuf directory = STRBUF_INIT; + + strbuf_add(&directory, path, len); + repaired = valid_cached_dir( + dir, untracked, istate, &directory, 0) && + untracked->valid_recursive; + strbuf_release(&directory); + } + if (!repaired) { + read_directory_recursive(dir, istate, path, len, + untracked, 0, 0, pathspec); + compute_untracked_cache_fsmonitor_valid_recursive(untracked); + } + } + QSORT(dir->entries, dir->nr, cmp_dir_entry); + QSORT(dir->ignored, dir->ignored_nr, cmp_dir_entry); + recompute_cached_fsmonitor_ancestors(dir->untracked, path, len); + emit_traversal_statistics(dir, istate->repo, path, len); + trace2_region_leave("dir", "read_cached_subtree", istate->repo); + return dir->internal.traversal_failed ? -1 : dir->nr; +} + int file_exists(const char *f) { struct stat sb; @@ -3486,16 +5058,23 @@ void setup_standard_excludes(struct dir_struct *dir) dir->exclude_per_dir = ".gitignore"; /* core.excludesfile defaulting to $XDG_CONFIG_HOME/git/ignore */ - if (excludes_file && !access_or_warn(excludes_file, R_OK, 0)) + if (excludes_file && + (dir->internal.exclude_source_proof || + !access_or_warn(excludes_file, R_OK, 0))) add_patterns_from_file_1(dir, excludes_file, - dir->untracked ? &dir->internal.ss_excludes_file : NULL); + dir->untracked ? + &dir->internal.ss_excludes_file : NULL, + !!dir->internal.exclude_source_proof); /* per repository user preference */ if (startup_info->have_repository) { const char *path = git_path_info_exclude(); - if (!access_or_warn(path, R_OK, 0)) + if (dir->internal.exclude_source_proof || + !access_or_warn(path, R_OK, 0)) add_patterns_from_file_1(dir, path, - dir->untracked ? &dir->internal.ss_info_exclude : NULL); + dir->untracked ? + &dir->internal.ss_info_exclude : NULL, + !!dir->internal.exclude_source_proof); } } @@ -3621,6 +5200,13 @@ static void write_one_dir(struct untracked_cache_dir *untracked, uint8_t intlen; int i = wd->index++; + /* Pending provider paths are process-local and cannot survive index I/O. */ + if (untracked->fsmonitor_dirty) { + untracked->valid = 0; + untracked->valid_recursive = 0; + untracked->fsmonitor_dirty = 0; + } + /* * untracked_nr should be reset whenever valid is clear, but * for safety.. @@ -3743,6 +5329,7 @@ void free_untracked_cache(struct untracked_cache *uc) free(uc->exclude_per_dir_to_free); strbuf_release(&uc->ident); + strbuf_release(&uc->fsmonitor_dirty_paths); free_untracked(uc->root); free(uc); } @@ -3824,7 +5411,11 @@ static int read_one_dir(struct untracked_cache_dir **untracked_, for (i = 0; i < untracked->dirs_nr; i++) { if (read_one_dir(untracked->dirs + i, rd) < 0) return -1; + if (untracked->dirs[i]->has_untracked) + untracked->has_untracked = 1; } + if (untracked->untracked_nr) + untracked->has_untracked = 1; return 0; } @@ -3947,6 +5538,8 @@ struct untracked_cache *read_untracked_extension(const void *data, unsigned long ewah_each_bit(rd.valid, read_stat, &rd); ewah_each_bit(rd.sha1_valid, read_oid, &rd); next = rd.data; + if (next == end) + compute_untracked_cache_disk_valid_recursive(uc->root); done: free(rd.ucd); @@ -3966,11 +5559,91 @@ static void invalidate_one_directory(struct untracked_cache *uc, { uc->dir_invalidated++; ucd->valid = 0; + ucd->valid_recursive = 0; + ucd->fsmonitor_dirty = 0; for (size_t i = 0; i < ucd->untracked_nr; i++) free(ucd->untracked[i]); ucd->untracked_nr = 0; } +static int directory_has_indexed_children( + struct index_state *istate, + const char *path, + size_t len) +{ + int pos = index_name_pos(istate, path, len); + + if (pos >= 0) + return 0; + pos = -pos - 1; + return pos < istate->cache_nr && + ce_namelen(istate->cache[pos]) > len && + istate->cache[pos]->name[len] == '/' && + !strncmp(istate->cache[pos]->name, path, len); +} + +static int record_cached_fsmonitor_file( + struct untracked_cache *uc, + struct untracked_cache_dir *dir, + struct index_state *istate, + const char *full_path, + const char *name) +{ + struct stat st; + const char *event, *end; + size_t parent_len, path_len = strlen(full_path); + int first, last; + + if (!istate->fsmonitor_untracked_valid || + istate->fsmonitor_legacy_untracked_fallback || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC || + !dir->valid || !dir->recurse || + dir->check_only || name == full_path || + (uc->exclude_per_dir && !strcmp(name, uc->exclude_per_dir))) + return 0; + parent_len = name - full_path - 1; + if (!directory_has_indexed_children(istate, full_path, parent_len) || + directory_has_indexed_children(istate, full_path, path_len)) + return 0; + if (lstat(full_path, &st)) { + if (!is_missing_file_error(errno)) + return 0; + } else if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)) { + return 0; + } + + first = 0; + last = dir->dirs_nr; + while (last > first) { + int next = first + ((last - first) >> 1); + int compare = strcmp(name, dir->dirs[next]->name); + + if (!compare) + return 0; + if (compare < 0) + last = next; + else + first = next + 1; + } + + if (uc->fsmonitor_dirty_paths.len) { + event = uc->fsmonitor_dirty_paths.buf; + end = event + uc->fsmonitor_dirty_paths.len; + while (event < end) { + if (!strcmp(event, full_path)) + goto recorded; + event += strlen(event) + 1; + } + } + strbuf_addstr(&uc->fsmonitor_dirty_paths, full_path); + strbuf_addch(&uc->fsmonitor_dirty_paths, '\0'); + +recorded: + dir->fsmonitor_dirty = 1; + dir->valid_recursive = 0; + return 1; +} + /* * Normally when an entry is added or removed from a directory, * invalidating that directory is enough. No need to touch its @@ -3997,7 +5670,10 @@ static void invalidate_one_directory(struct untracked_cache *uc, */ static int invalidate_one_component(struct untracked_cache *uc, struct untracked_cache_dir *dir, - const char *path, int len) + struct index_state *istate, + const char *path, int len, + const char *full_path, + int allow_tracked_stop) { const char *rest = strchr(path, '/'); @@ -4006,14 +5682,30 @@ static int invalidate_one_component(struct untracked_cache *uc, struct untracked_cache_dir *d = lookup_untracked(uc, dir, path, component_len); int ret = - invalidate_one_component(uc, d, rest + 1, - len - (component_len + 1)); - if (ret) - invalidate_one_directory(uc, dir); + invalidate_one_component(uc, d, istate, rest + 1, + len - (component_len + 1), + full_path, allow_tracked_stop); + if (ret) { + size_t directory_len = rest - full_path; + if (allow_tracked_stop && uc->use_fsmonitor && + directory_has_indexed_children( + istate, full_path, directory_len)) { + dir->valid_recursive = 0; + ret = 0; + } else { + invalidate_one_directory(uc, dir); + } + } + if (!d->valid_recursive) + dir->valid_recursive = 0; return ret; } - invalidate_one_directory(uc, dir); + if (!allow_tracked_stop || + !record_cached_fsmonitor_file(uc, dir, istate, full_path, path)) + invalidate_one_directory(uc, dir); + else + return 0; return uc->dir_flags & DIR_SHOW_OTHER_DIRECTORIES; } @@ -4025,7 +5717,7 @@ void untracked_cache_invalidate_path(struct index_state *istate, if (!safe_path && !verify_path(path, 0)) return; invalidate_one_component(istate->untracked, istate->untracked->root, - path, strlen(path)); + istate, path, strlen(path), path, !safe_path); } void untracked_cache_invalidate_trimmed_path(struct index_state *istate, diff --git a/dir.h b/dir.h index 83e0f648a81f36..527ba58cc653e2 100644 --- a/dir.h +++ b/dir.h @@ -7,6 +7,7 @@ #include "statinfo.h" #include "strbuf.h" +struct exclude_source_proof; struct repository; /** @@ -182,7 +183,18 @@ struct untracked_cache_dir { /* all data except 'dirs' in this struct are good */ unsigned int valid : 1; unsigned int recurse : 1; - /* null object ID means this directory does not have .gitignore */ + /* this subtree contains at least one cached untracked entry */ + unsigned int has_untracked : 1; + /* transient results from directory-stat preloading */ + unsigned int stat_checked : 1; + unsigned int stat_matches : 1; + unsigned int exclude_matches : 1; + unsigned int valid_recursive : 1; + unsigned int fsmonitor_dirty : 1; + /* + * A null object ID means this directory does not have .gitignore. + * The empty-tree ID records a present source that could not be read. + */ struct object_id exclude_oid; char name[FLEX_ARRAY]; }; @@ -204,8 +216,13 @@ struct untracked_cache { int gitignore_invalidated; int dir_invalidated; int dir_opened; + struct strbuf fsmonitor_dirty_paths; /* fsmonitor invalidation data */ unsigned int use_fsmonitor : 1; + /* A lost provider boundary requires ordinary directory validation. */ + unsigned int fsmonitor_revalidation : 1; + /* Process-local: a legacy cache was discarded on read. */ + unsigned int fsmonitor_legacy_discarded : 1; }; /** @@ -353,6 +370,14 @@ struct dir_struct { /* Stats about the traversal */ unsigned visited_paths; unsigned visited_directories; + unsigned untracked_cache_preloaded : 1; + unsigned traversal_failed : 1; + + /* + * Optional borrowed proof that covers every exclusion source + * consulted by this traversal. + */ + struct exclude_source_proof *exclude_source_proof; } internal; }; @@ -401,6 +426,11 @@ int fill_directory(struct dir_struct *dir, int read_directory(struct dir_struct *, struct index_state *istate, const char *path, int len, const struct pathspec *pathspec); +int read_directory_cached_subtree(struct dir_struct *, + struct index_state *istate, + struct untracked_cache_dir *untracked, + const char *path, int len, + const struct pathspec *pathspec); enum pattern_match_result { UNDECIDED = -1, @@ -597,6 +627,8 @@ int cmp_dir_entry(const void *p1, const void *p2); int check_dir_entry_contains(const struct dir_entry *out, const struct dir_entry *in); void untracked_cache_invalidate_path(struct index_state *, const char *, int safe_path); +void untracked_cache_invalidate_all(struct index_state *); +int untracked_cache_preserve_for_revalidation(struct index_state *); /* * Invalidate the untracked-cache for this path, but first strip * off a trailing slash, if present. @@ -604,14 +636,30 @@ void untracked_cache_invalidate_path(struct index_state *, const char *, int saf void untracked_cache_invalidate_trimmed_path(struct index_state *, const char *path, int safe_path); +void untracked_cache_recompute_fsmonitor_valid_recursive( + struct untracked_cache *); void untracked_cache_remove_from_index(struct index_state *, const char *); void untracked_cache_add_to_index(struct index_state *, const char *); +struct untracked_cache_preload; +struct untracked_cache_preload * +untracked_cache_preload_start_fsmonitor_excludes( + struct index_state *, unsigned int dir_flags, + const struct pathspec *pathspec); +struct untracked_cache_preload *untracked_cache_preload_start_ordinary( + struct index_state *, unsigned int dir_flags); +int untracked_cache_preload_finish(struct untracked_cache_preload *, + struct index_state *, unsigned int dir_flags, + size_t *index_invalidated); +void untracked_cache_preload_release(struct untracked_cache_preload *); + void free_untracked_cache(struct untracked_cache *); struct untracked_cache *read_untracked_extension(const void *data, unsigned long sz); void write_untracked_extension(struct strbuf *out, struct untracked_cache *untracked); void add_untracked_cache(struct index_state *istate); void remove_untracked_cache(struct index_state *istate); +int untracked_cache_adopt_legacy(struct index_state *istate); +void untracked_cache_discard_legacy(struct index_state *istate); /* * Connect a worktree to a git directory by creating (or overwriting) a diff --git a/entry.c b/entry.c index 1c4f0f44070ea3..9284e0d0d49123 100644 --- a/entry.c +++ b/entry.c @@ -512,8 +512,9 @@ int checkout_entry_ca(struct cache_entry *ce, struct conv_attrs *ca, if (!check_path(path.buf, path.len, &st, state->base_dir_len)) { const struct submodule *sub; - unsigned changed = ie_match_stat(state->istate, ce, &st, - CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); + unsigned changed = ie_match_stat_with_content_check( + state->istate, ce, &st, + CE_MATCH_IGNORE_VALID | CE_MATCH_IGNORE_SKIP_WORKTREE); /* * Needs to be checked before !changed returns early, * as the possibly empty directory was not changed diff --git a/exclude-source-proof.c b/exclude-source-proof.c new file mode 100644 index 00000000000000..266e5f57b95267 --- /dev/null +++ b/exclude-source-proof.c @@ -0,0 +1,504 @@ +#include "git-compat-util.h" +#include "exclude-source-proof.h" +#include "hash-framing.h" +#include "object-file.h" +#include "path-namespace.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "strmap.h" +#include "trace2.h" + +/* + * Each entry describes one path/policy observation for replay or a stable + * digest. Filesystem identities make capture and validation coherent; the + * durable observation is the source's existence and bytes. + */ +struct exclude_source_proof_entry { + char *path; + size_t size; + struct object_id oid; + unsigned exists : 1; + unsigned nofollow : 1; +}; + +struct exclude_source_proof { + struct index_state *istate; + void *open_data; + exclude_source_open_parent_fn open_parent; + struct exclude_source_proof_entry *entries; + struct strintmap entries_by_path[2]; + size_t nr; + size_t alloc; + unsigned nonblocking : 1; + unsigned invalid : 1; +}; + +struct exclude_source_capture { + struct exclude_source_proof *proof; + char *path; + char *parent; + char *relative; + int parent_fd; + struct stat parent_stat; + unsigned nofollow : 1; +}; + +static char *source_parent(const char *path) +{ + const char *slash = strrchr(path, '/'); + + if (!slash) + return xstrdup("."); + if (slash == path) + return xstrdup("/"); + return xmemdupz(path, slash - path); +} + +static char *source_relative(const char *path, const char *parent) +{ + const char *relative; + size_t len; + + if (!strcmp(parent, ".")) + return xstrdup(path); + if (!strcmp(parent, "/")) { + relative = path + 1; + } else { + len = strlen(parent); + if (strncmp(path, parent, len) || path[len] != '/') + BUG("exclude source is not below its parent"); + relative = path + len + 1; + } + return xstrdup(*relative ? relative : "."); +} + +static int parent_up(char *parent) +{ + char *slash; + + if (!strcmp(parent, ".") || !strcmp(parent, "/")) + return 0; + slash = strrchr(parent, '/'); + if (!slash) { + parent[0] = '.'; + parent[1] = '\0'; + } else if (slash == parent) { + parent[1] = '\0'; + } else { + *slash = '\0'; + } + return 1; +} + +static int open_source_at(int parent_fd, const char *relative, int nofollow, + int nonblocking) +{ +#if EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN + int flags = O_RDONLY | O_CLOEXEC; + + if (nofollow) + flags |= O_NOFOLLOW; + if (nonblocking) + flags |= O_NONBLOCK; + return openat(parent_fd, relative, flags); +#else + (void)parent_fd; + (void)relative; + (void)nofollow; + (void)nonblocking; + errno = ENOSYS; + return -1; +#endif +} + +static int parent_identity_stable( + struct exclude_source_proof *proof, const char *parent, + int held_fd, const struct stat *expected, int regular_source) +{ + struct stat held, reopened; + int fd = proof->open_parent(proof->open_data, parent); + /* + * A regular source has its own held descriptor and repeated target + * identity checks. Absence and nonregular sources cannot distinguish a + * transient target change from harmless parent-directory churn. + */ + int stable = !fstat(held_fd, &held) && + fd >= 0 && !fstat(fd, &reopened) && + (regular_source ? + (path_namespace_directory_stat_equal(expected, &held) && + path_namespace_directory_stat_equal(expected, &reopened)) : + (path_namespace_stat_equal(expected, &held) && + path_namespace_stat_equal(expected, &reopened))); + + if (fd >= 0) + close(fd); + return stable; +} + +static int parent_stable(struct exclude_source_capture *capture, + int regular_source) +{ + return parent_identity_stable( + capture->proof, capture->parent, capture->parent_fd, + &capture->parent_stat, regular_source); +} + +static void capture_free(struct exclude_source_capture *capture) +{ + if (!capture) + return; + if (capture->parent_fd >= 0) + close(capture->parent_fd); + free(capture->path); + free(capture->parent); + free(capture->relative); + free(capture); +} + +static struct exclude_source_capture *capture_begin( + struct exclude_source_proof *proof, const char *path, + int nofollow, int invalidate) +{ + struct exclude_source_capture *capture; + + if (!proof || proof->invalid) + return NULL; + if (!path) { + if (invalidate) + proof->invalid = 1; + return NULL; + } + CALLOC_ARRAY(capture, 1); + capture->proof = proof; + capture->nofollow = nofollow; + capture->parent_fd = -1; + capture->path = xstrdup(path); + capture->parent = source_parent(path); + for (;;) { + capture->parent_fd = proof->open_parent(proof->open_data, + capture->parent); + if (capture->parent_fd >= 0) + break; + if (!is_missing_file_error(errno) || + !parent_up(capture->parent)) + break; + } + if (capture->parent_fd < 0 || + fstat(capture->parent_fd, &capture->parent_stat) || + !S_ISDIR(capture->parent_stat.st_mode)) { + if (invalidate) + proof->invalid = 1; + capture_free(capture); + return NULL; + } + capture->relative = source_relative(path, capture->parent); + return capture; +} + +struct exclude_source_proof *exclude_source_proof_create( + struct index_state *istate, void *open_data, + exclude_source_open_parent_fn open_parent, unsigned flags) +{ + struct exclude_source_proof *proof; + + CALLOC_ARRAY(proof, 1); + proof->istate = istate; + proof->open_data = open_data; + proof->open_parent = open_parent; + proof->nonblocking = + !!(flags & EXCLUDE_SOURCE_PROOF_NONBLOCKING); + strintmap_init_with_options(&proof->entries_by_path[0], -1, + NULL, 0); + strintmap_init_with_options(&proof->entries_by_path[1], -1, + NULL, 0); + if (!EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN || + !istate || !istate->repo || !istate->repo->hash_algo || + !open_parent || + (flags & ~EXCLUDE_SOURCE_PROOF_NONBLOCKING)) + proof->invalid = 1; + return proof; +} + +struct exclude_source_capture *exclude_source_capture_begin( + struct exclude_source_proof *proof, const char *path, + int nofollow) +{ + return capture_begin(proof, path, nofollow, 1); +} + +int exclude_source_capture_open(struct exclude_source_capture *capture) +{ + if (!capture) { + errno = EINVAL; + return -1; + } + return open_source_at(capture->parent_fd, capture->relative, + capture->nofollow, + capture->proof->nonblocking); +} + +int exclude_source_capture_absent(struct exclude_source_capture *capture) +{ +#if EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN + struct stat st; + + if (!capture) + return 0; + if (!fstatat(capture->parent_fd, capture->relative, &st, + AT_SYMLINK_NOFOLLOW)) + return 0; + return is_missing_file_error(errno); +#else + (void)capture; + return 0; +#endif +} + +static int source_matches(struct exclude_source_capture *capture, + const struct stat *expected) +{ + struct stat st; + int fd = open_source_at(capture->parent_fd, capture->relative, + capture->nofollow, 1); + int ret = fd >= 0 && !fstat(fd, &st) && + path_namespace_stat_equal(expected, &st); + + if (fd >= 0) + close(fd); + return ret; +} + +static int source_matches_after_read(struct exclude_source_capture *capture, + const struct stat *expected) +{ +#if EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN + if (S_ISREG(expected->st_mode)) { + struct stat named; + int flags = capture->nofollow ? AT_SYMLINK_NOFOLLOW : 0; + + /* The final reopened descriptor still proves readability. */ + return !fstatat(capture->parent_fd, capture->relative, + &named, flags) && + path_namespace_stat_equal(expected, &named); + } +#endif + return source_matches(capture, expected); +} + +static int same_observation( + const struct exclude_source_proof_entry *entry, + int exists, size_t size, const struct object_id *oid) +{ + return entry->exists == exists && + (!exists || + (entry->size == size && oideq(&entry->oid, oid))); +} + +static void record_observation( + struct exclude_source_capture *capture, int exists, + size_t size, const struct object_id *oid) +{ + struct exclude_source_proof *proof = capture->proof; + struct strintmap *map = + &proof->entries_by_path[!!capture->nofollow]; + struct exclude_source_proof_entry *entry; + int index = strintmap_get(map, capture->path); + + if (index >= 0) { + if (!same_observation(&proof->entries[index], + exists, size, oid)) + proof->invalid = 1; + return; + } + + ALLOC_GROW(proof->entries, proof->nr + 1, proof->alloc); + entry = &proof->entries[proof->nr]; + memset(entry, 0, sizeof(*entry)); + entry->path = xstrdup(capture->path); + entry->nofollow = capture->nofollow; + entry->exists = exists; + if (exists) { + entry->size = size; + oidcpy(&entry->oid, oid); + } + strintmap_set(map, entry->path, proof->nr); + proof->nr++; +} + +void exclude_source_capture_record( + struct exclude_source_capture *capture, + int source_fd, + const struct stat *source_stat, + const void *buf, size_t size) +{ + struct exclude_source_proof *proof; + struct object_id oid; + struct stat final; + + if (!capture) + return; + proof = capture->proof; + if (proof->invalid) + return; + + if (!source_stat) { + if (!exclude_source_capture_absent(capture) || + !parent_stable(capture, 0) || + !exclude_source_capture_absent(capture)) { + proof->invalid = 1; + return; + } + record_observation(capture, 0, 0, NULL); + return; + } + + if (source_fd < 0 || source_stat->st_size < 0 || + (!buf && size) || + xsize_t(source_stat->st_size) != size || + fstat(source_fd, &final) || + !path_namespace_stat_equal(source_stat, &final) || + !parent_stable(capture, S_ISREG(final.st_mode)) || + fstat(source_fd, &final) || + !path_namespace_stat_equal(source_stat, &final) || + !source_matches(capture, &final)) { + proof->invalid = 1; + return; + } + if (!size) + oidcpy(&oid, proof->istate->repo->hash_algo->empty_blob); + else + hash_object_file(proof->istate->repo->hash_algo, buf, size, + OBJ_BLOB, &oid); + record_observation(capture, 1, size, &oid); +} + +void exclude_source_capture_error(struct exclude_source_capture *capture) +{ + if (capture) + capture->proof->invalid = 1; +} + +void exclude_source_capture_release(struct exclude_source_capture *capture) +{ + capture_free(capture); +} + +static int proof_entry_matches( + struct exclude_source_proof *proof, + const struct exclude_source_proof_entry *entry) +{ + struct exclude_source_capture *capture = + capture_begin(proof, entry->path, entry->nofollow, 0); + struct object_id oid; + struct stat before, after, final; + char *buf = NULL; + size_t size; + int fd = -1; + int ret = 0; + + if (!capture) + goto done; + if (!entry->exists) { + ret = exclude_source_capture_absent(capture) && + parent_stable(capture, 0) && + exclude_source_capture_absent(capture); + goto done; + } + + fd = open_source_at(capture->parent_fd, capture->relative, + entry->nofollow, 1); + if (fd < 0 || fstat(fd, &before) || before.st_size < 0 || + xsize_t(before.st_size) != entry->size) + goto done; + size = entry->size; + buf = xmalloc(size ? size : 1); + if ((size_t)read_in_full(fd, buf, size) != size || + fstat(fd, &after) || + !path_namespace_stat_equal(&before, &after) || + !source_matches_after_read(capture, &after)) + goto done; + hash_object_file(proof->istate->repo->hash_algo, buf, size, + OBJ_BLOB, &oid); + if (!oideq(&oid, &entry->oid) || + !parent_stable(capture, S_ISREG(after.st_mode)) || + fstat(fd, &final) || + !path_namespace_stat_equal(&after, &final) || + !source_matches(capture, &final)) + goto done; + ret = 1; +done: + free(buf); + if (fd >= 0) + close(fd); + capture_free(capture); + return ret; +} + +int exclude_source_proof_validate(struct exclude_source_proof *proof) +{ + int valid; + + if (!proof) + return 0; + valid = !proof->invalid; + for (size_t i = 0; valid && i < proof->nr; i++) + valid = proof_entry_matches(proof, &proof->entries[i]); + if (proof->istate && proof->istate->repo) { + trace2_data_intmax("exclude", proof->istate->repo, + "proof_entries", proof->nr); + trace2_data_intmax("exclude", proof->istate->repo, + "proof_valid", valid); + } + return valid; +} + +int exclude_source_proof_digest( + struct exclude_source_proof *proof, + const struct git_hash_algo *algo, + struct object_id *oid) +{ + static const char domain[] = "git-exclude-source-proof-digest-v1"; + const struct git_hash_algo *source_algo; + struct git_hash_ctx ctx; + unsigned char count[sizeof(uint64_t)]; + unsigned char format[sizeof(uint32_t)]; + + if (!proof || !algo || !oid || + !exclude_source_proof_validate(proof)) + return -1; + source_algo = proof->istate->repo->hash_algo; + git_hash_init(&ctx, algo); + hash_length_delimited(&ctx, domain, sizeof(domain) - 1); + put_be32(format, source_algo->format_id); + hash_length_delimited(&ctx, format, sizeof(format)); + put_be64(count, proof->nr); + hash_length_delimited(&ctx, count, sizeof(count)); + for (size_t i = 0; i < proof->nr; i++) { + const struct exclude_source_proof_entry *entry = + &proof->entries[i]; + unsigned char policy[] = { + entry->nofollow, + entry->exists, + }; + + hash_length_delimited(&ctx, entry->path, + strlen(entry->path)); + hash_length_delimited(&ctx, policy, sizeof(policy)); + hash_length_delimited(&ctx, entry->oid.hash, + entry->exists ? source_algo->rawsz : 0); + } + git_hash_final_oid(oid, &ctx); + return 0; +} + +void exclude_source_proof_release(struct exclude_source_proof *proof) +{ + if (!proof) + return; + strintmap_clear(&proof->entries_by_path[0]); + strintmap_clear(&proof->entries_by_path[1]); + for (size_t i = 0; i < proof->nr; i++) + free(proof->entries[i].path); + free(proof->entries); + free(proof); +} diff --git a/exclude-source-proof.h b/exclude-source-proof.h new file mode 100644 index 00000000000000..ab9b626c664380 --- /dev/null +++ b/exclude-source-proof.h @@ -0,0 +1,52 @@ +#ifndef EXCLUDE_SOURCE_PROOF_H +#define EXCLUDE_SOURCE_PROOF_H + +#if (defined(__APPLE__) || defined(__linux__)) && \ + defined(O_CLOEXEC) && defined(O_NONBLOCK) && \ + defined(O_NOFOLLOW) && defined(O_DIRECTORY) && \ + defined(AT_SYMLINK_NOFOLLOW) +#define EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN 1 +#else +#define EXCLUDE_SOURCE_PROOF_HAS_ANCHORED_OPEN 0 +#endif + +struct exclude_source_capture; +struct exclude_source_proof; +struct git_hash_algo; +struct index_state; +struct object_id; +struct stat; + +typedef int (*exclude_source_open_parent_fn)(void *data, const char *path); + +enum exclude_source_proof_flags { + EXCLUDE_SOURCE_PROOF_NONBLOCKING = (1 << 0), +}; + +struct exclude_source_proof *exclude_source_proof_create( + struct index_state *istate, void *open_data, + exclude_source_open_parent_fn open_parent, unsigned flags); +struct exclude_source_capture *exclude_source_capture_begin( + struct exclude_source_proof *proof, const char *path, + int nofollow); +int exclude_source_capture_open(struct exclude_source_capture *capture); +int exclude_source_capture_absent(struct exclude_source_capture *capture); +void exclude_source_capture_record( + struct exclude_source_capture *capture, + int source_fd, + const struct stat *source_stat, + const void *buf, size_t size); +void exclude_source_capture_error(struct exclude_source_capture *capture); +void exclude_source_capture_release(struct exclude_source_capture *capture); +int exclude_source_proof_validate(struct exclude_source_proof *proof); +/* + * Hash unique observations in first-observation order. The caller must + * capture them in a deterministic order when comparing across processes. + */ +int exclude_source_proof_digest( + struct exclude_source_proof *proof, + const struct git_hash_algo *algo, + struct object_id *oid); +void exclude_source_proof_release(struct exclude_source_proof *proof); + +#endif /* EXCLUDE_SOURCE_PROOF_H */ diff --git a/exec-cmd.c b/exec-cmd.c index 507e67d528b0dd..dc801d1a6d35d4 100644 --- a/exec-cmd.c +++ b/exec-cmd.c @@ -27,6 +27,14 @@ static const char *system_prefix(void); +/* + * Absolute path to the current executable, when it can be determined. Keep + * this separately from executable_dirname because some callers need to + * re-execute this exact Git rather than resolve "git" through PATH. + */ +static const char *executable_path; +static int executable_is_dispatcher; + #ifdef RUNTIME_PREFIX /** @@ -257,7 +265,8 @@ void git_resolve_executable_dir(const char *argv0) return; } - resolved = strbuf_detach(&buf, NULL); + executable_path = strbuf_detach(&buf, NULL); + resolved = xstrdup(executable_path); slash = find_last_dir_sep(resolved); if (slash) resolved[slash - resolved] = '\0'; @@ -278,15 +287,42 @@ static const char *system_prefix(void) } /* - * This is called during initialization, but No work needs to be done here when - * runtime prefix is not being used. + * A non-runtime-prefix build does not need the executable directory for path + * discovery, but an explicit argv[0] is still useful for exact re-execution. */ -void git_resolve_executable_dir(const char *argv0 UNUSED) +void git_resolve_executable_dir(const char *argv0) { + struct strbuf buf = STRBUF_INIT; + + /* A bare argv[0] would require a PATH lookup and is not authoritative. */ + if (!argv0 || !*argv0 || !find_last_dir_sep(argv0)) + return; + strbuf_add_absolute_path(&buf, argv0); + if (strbuf_normalize_path(&buf)) { + trace_printf("trace: could not normalize executable path: %s\n", + buf.buf); + strbuf_release(&buf); + return; + } + executable_path = strbuf_detach(&buf, NULL); + trace2_cmd_path(executable_path); } #endif /* RUNTIME_PREFIX */ +const char *git_executable_path(void) +{ + /* Helpers have an exact path too, but cannot dispatch Git builtins. */ + if (!executable_path || !executable_is_dispatcher) + return NULL; + return executable_path; +} + +void git_mark_executable_as_dispatcher(void) +{ + executable_is_dispatcher = 1; +} + char *system_path(const char *path) { struct strbuf d = STRBUF_INIT; diff --git a/exec-cmd.h b/exec-cmd.h index 330b41d54dec52..0613765ef7e4ee 100644 --- a/exec-cmd.h +++ b/exec-cmd.h @@ -5,6 +5,8 @@ struct strvec; void git_set_exec_path(const char *exec_path); void git_resolve_executable_dir(const char *path); +const char *git_executable_path(void); +void git_mark_executable_as_dispatcher(void); const char *git_exec_path(void); void setup_path(void); const char **prepare_git_cmd(struct strvec *out, const char **argv); diff --git a/fsmonitor--daemon.h b/fsmonitor--daemon.h index 5cbbec8d940ba7..850188f872b783 100644 --- a/fsmonitor--daemon.h +++ b/fsmonitor--daemon.h @@ -40,6 +40,7 @@ struct fsmonitor_daemon_state { pthread_mutex_t main_lock; struct strbuf path_worktree_watch; + struct strbuf worktree_identity; struct strbuf path_gitdir_watch; struct alias_info alias; int nr_paths_watching; diff --git a/fsmonitor-clean-proof.c b/fsmonitor-clean-proof.c new file mode 100644 index 00000000000000..c150f86e208d71 --- /dev/null +++ b/fsmonitor-clean-proof.c @@ -0,0 +1,128 @@ +#include "git-compat-util.h" +#include "attr-manifest.h" +#include "fsmonitor-clean-proof.h" +#include "hash-framing.h" +#include "strbuf.h" + +#define FSMONITOR_CLEAN_PROOF_MAGIC 0x46534331 /* "FSC1" */ +#define FSMONITOR_CLEAN_PROOF_HEADER_WORDS 5 + +int fsmonitor_clean_proof_parse(struct fsmonitor_clean_proof *proof, + const void *data, size_t len, + const struct git_hash_algo *algo) +{ + struct fsmonitor_clean_proof parsed = { 0 }; + const unsigned char *p = data; + const unsigned char *end = p + len; + unsigned char checksum[GIT_MAX_RAWSZ]; + size_t hashes_len = 4 * algo->rawsz; + uint32_t token_len, manifest_len; + + memset(proof, 0, sizeof(*proof)); + if (len < FSMONITOR_CLEAN_PROOF_HEADER_WORDS * sizeof(uint32_t) + + hashes_len + 1) + return -1; + parsed.version = get_be32(p); + if (parsed.version != FSMONITOR_CLEAN_PROOF_VERSION_LEGACY && + parsed.version != FSMONITOR_CLEAN_PROOF_VERSION) + return -1; + if (parsed.version == FSMONITOR_CLEAN_PROOF_VERSION) + hashes_len += algo->rawsz; + p += sizeof(uint32_t); + if (get_be32(p) != FSMONITOR_CLEAN_PROOF_MAGIC) + return -1; + p += sizeof(uint32_t); + parsed.flags = get_be32(p); + p += sizeof(uint32_t); + token_len = get_be32(p); + p += sizeof(uint32_t); + manifest_len = get_be32(p); + p += sizeof(uint32_t); + if (parsed.flags & ~FSMONITOR_CLEAN_PROOF_ALL || !token_len || + token_len > FSMONITOR_CLEAN_PROOF_TOKEN_MAX || + manifest_len < sizeof(uint32_t) || + (size_t)(end - p) < token_len || memchr(p, '\0', token_len)) + return -1; + parsed.token = p; + parsed.token_len = token_len; + p += token_len; + if ((size_t)(end - p) < hashes_len || + (size_t)(end - p) - hashes_len != manifest_len) + return -1; + parsed.config_hash = p; + p += algo->rawsz; + parsed.semantic_hash = p; + p += algo->rawsz; + parsed.attr_hash = p; + p += algo->rawsz; + if (parsed.version == FSMONITOR_CLEAN_PROOF_VERSION) { + parsed.tracked_policy_hash = p; + p += algo->rawsz; + } + parsed.attr_manifest = p; + parsed.attr_manifest_len = manifest_len; + p += manifest_len; + if (!attr_manifest_valid(parsed.attr_manifest, + parsed.attr_manifest_len, algo)) + return -1; + hash_buffer_digest(algo, data, len - algo->rawsz, checksum); + if (memcmp(checksum, p, algo->rawsz)) + return -1; + *proof = parsed; + return 0; +} + +int fsmonitor_clean_proof_write(struct strbuf *out, + const struct fsmonitor_clean_proof *proof, + const struct git_hash_algo *algo) +{ + uint32_t value; + + strbuf_reset(out); + if (!proof->token || !proof->token_len || + proof->token_len > FSMONITOR_CLEAN_PROOF_TOKEN_MAX || + proof->token_len > UINT32_MAX || + memchr(proof->token, '\0', proof->token_len) || + proof->flags & ~FSMONITOR_CLEAN_PROOF_ALL || + !proof->config_hash || !proof->semantic_hash || !proof->attr_hash || + !proof->attr_manifest || proof->attr_manifest_len > UINT32_MAX || + !attr_manifest_valid(proof->attr_manifest, + proof->attr_manifest_len, algo)) + return -1; + + put_be32(&value, proof->tracked_policy_hash ? + FSMONITOR_CLEAN_PROOF_VERSION : + FSMONITOR_CLEAN_PROOF_VERSION_LEGACY); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, FSMONITOR_CLEAN_PROOF_MAGIC); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, proof->flags); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, proof->token_len); + strbuf_add(out, &value, sizeof(value)); + put_be32(&value, proof->attr_manifest_len); + strbuf_add(out, &value, sizeof(value)); + strbuf_add(out, proof->token, proof->token_len); + strbuf_add(out, proof->config_hash, algo->rawsz); + strbuf_add(out, proof->semantic_hash, algo->rawsz); + strbuf_add(out, proof->attr_hash, algo->rawsz); + if (proof->tracked_policy_hash) + strbuf_add(out, proof->tracked_policy_hash, algo->rawsz); + strbuf_add(out, proof->attr_manifest, proof->attr_manifest_len); + hash_append_checksum(out, algo); + return 0; +} + +int fsmonitor_clean_proof_copy_without_bindings( + struct strbuf *out, const void *data, size_t len, + const struct git_hash_algo *algo) +{ + struct fsmonitor_clean_proof proof; + + strbuf_reset(out); + if (fsmonitor_clean_proof_parse(&proof, data, len, algo)) + return -1; + proof.flags &= ~(FSMONITOR_CLEAN_PROOF_TOKEN_BOUND | + FSMONITOR_CLEAN_PROOF_STAT_BOUND); + return fsmonitor_clean_proof_write(out, &proof, algo); +} diff --git a/fsmonitor-clean-proof.h b/fsmonitor-clean-proof.h new file mode 100644 index 00000000000000..2e2a3c2b252c39 --- /dev/null +++ b/fsmonitor-clean-proof.h @@ -0,0 +1,45 @@ +#ifndef FSMONITOR_CLEAN_PROOF_H +#define FSMONITOR_CLEAN_PROOF_H + +#include "hash.h" + +struct strbuf; + +#define FSMONITOR_CLEAN_PROOF_VERSION_LEGACY 1 +#define FSMONITOR_CLEAN_PROOF_VERSION 2 +#define FSMONITOR_CLEAN_PROOF_TOKEN_MAX 4096 + +#define FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE (1u << 0) +#define FSMONITOR_CLEAN_PROOF_TOKEN_BOUND (1u << 1) +#define FSMONITOR_CLEAN_PROOF_STAT_BOUND (1u << 2) +#define FSMONITOR_CLEAN_PROOF_FULL_INDEX (1u << 3) +#define FSMONITOR_CLEAN_PROOF_ALL \ + (FSMONITOR_CLEAN_PROOF_MANIFEST_COMPLETE | \ + FSMONITOR_CLEAN_PROOF_TOKEN_BOUND | \ + FSMONITOR_CLEAN_PROOF_STAT_BOUND | \ + FSMONITOR_CLEAN_PROOF_FULL_INDEX) + +struct fsmonitor_clean_proof { + uint32_t version; + uint32_t flags; + const unsigned char *token; + size_t token_len; + const unsigned char *config_hash; + const unsigned char *semantic_hash; + const unsigned char *attr_hash; + const unsigned char *tracked_policy_hash; + const unsigned char *attr_manifest; + size_t attr_manifest_len; +}; + +int fsmonitor_clean_proof_parse(struct fsmonitor_clean_proof *proof, + const void *data, size_t len, + const struct git_hash_algo *algo); +int fsmonitor_clean_proof_write(struct strbuf *out, + const struct fsmonitor_clean_proof *proof, + const struct git_hash_algo *algo); +int fsmonitor_clean_proof_copy_without_bindings( + struct strbuf *out, const void *data, size_t len, + const struct git_hash_algo *algo); + +#endif /* FSMONITOR_CLEAN_PROOF_H */ diff --git a/fsmonitor-ipc.c b/fsmonitor-ipc.c index 6112d130644f04..5ddd7503ca60f4 100644 --- a/fsmonitor-ipc.c +++ b/fsmonitor-ipc.c @@ -1,7 +1,15 @@ #define USE_THE_REPOSITORY_VARIABLE #include "git-compat-util.h" +#include "abspath.h" +#include "config.h" +#include "environment.h" +#include "exec-cmd.h" #include "gettext.h" +#include "hash.h" +#include "lockfile.h" +#include "parse.h" +#include "path.h" #include "simple-ipc.h" #include "fsmonitor-ipc.h" #include "repository.h" @@ -9,6 +17,55 @@ #include "strbuf.h" #include "trace2.h" +#ifdef __APPLE__ +#include +#include +#endif + +#ifdef __linux__ +#include +#endif + +int fsmonitor_ipc__get_worktree_identity(struct repository *r, + struct strbuf *identity) +{ + static const char hex[] = "0123456789abcdef"; + struct strbuf canonical = STRBUF_INIT; + struct strbuf stable = STRBUF_INIT; + git_SHA256_CTX ctx; + unsigned char hash[GIT_SHA256_RAWSZ]; + struct stat st; + const char *worktree = repo_get_work_tree(r); + int ret = -1; + + if (!worktree || + !strbuf_realpath(&canonical, worktree, 0) || + stat(canonical.buf, &st)) + goto done; + strbuf_addf(&stable, "v1\n%"PRIuMAX":", (uintmax_t)canonical.len); + strbuf_addbuf(&stable, &canonical); + strbuf_addf(&stable, "\n%"PRIuMAX"\n%"PRIuMAX, + (uintmax_t)st.st_dev, (uintmax_t)st.st_ino); +#ifdef __APPLE__ + strbuf_addf(&stable, "\n%"PRIdMAX"\n%ld\n%"PRIu32, + (intmax_t)st.st_birthtimespec.tv_sec, + st.st_birthtimespec.tv_nsec, st.st_gen); +#endif + git_SHA256_Init(&ctx); + git_SHA256_Update(&ctx, stable.buf, stable.len); + git_SHA256_Final(hash, &ctx); + strbuf_reset(identity); + for (size_t i = 0; i < ARRAY_SIZE(hash); i++) { + strbuf_addch(identity, hex[hash[i] >> 4]); + strbuf_addch(identity, hex[hash[i] & 0xf]); + } + ret = 0; +done: + strbuf_release(&stable); + strbuf_release(&canonical); + return ret; +} + #ifndef HAVE_FSMONITOR_DAEMON_BACKEND /* @@ -21,6 +78,20 @@ int fsmonitor_ipc__is_supported(void) return 0; } +void fsmonitor_ipc__record_watch_limit_failure( + const char *worktree_identity UNUSED) +{ +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r UNUSED) +{ + return 0; +} + const char *fsmonitor_ipc__get_path(struct repository *r UNUSED) { return NULL; @@ -32,7 +103,8 @@ enum ipc_active_state fsmonitor_ipc__get_state(void) } int fsmonitor_ipc__send_query(const char *since_token UNUSED, - struct strbuf *answer UNUSED) + struct strbuf *answer UNUSED, + int *legacy_worktree_authenticated UNUSED) { return -1; } @@ -45,6 +117,17 @@ int fsmonitor_ipc__send_command(const char *command UNUSED, #else +static void prepare_spawn_env(struct strvec *env) +{ + /* Let the child rediscover this repository from the worktree. */ + strvec_push(env, GIT_DIR_ENVIRONMENT); + strvec_push(env, GIT_WORK_TREE_ENVIRONMENT); + strvec_push(env, GIT_COMMON_DIR_ENVIRONMENT); + strvec_push(env, GIT_PREFIX_ENVIRONMENT); + strvec_push(env, GIT_IMPLICIT_WORK_TREE_ENVIRONMENT); + strvec_push(env, INDEX_ENVIRONMENT); +} + int fsmonitor_ipc__is_supported(void) { return 1; @@ -55,60 +138,864 @@ enum ipc_active_state fsmonitor_ipc__get_state(void) return ipc_get_active_state(fsmonitor_ipc__get_path(the_repository)); } +#define FSMONITOR_START_TIMEOUT_KEY "fsmonitor.starttimeout" +#define FSMONITOR_START_TIMEOUT_DEFAULT 60 +#define FSMONITOR_RESTART_ATTEMPTS 3 + +#if defined(__linux__) || defined(__APPLE__) +#define FSMONITOR_WATCH_LIMIT_MARKER "fsmonitor--daemon.inotify-limit" +#define FSMONITOR_WATCH_LIMIT_MAGIC "inotify-limit-v1\n" +#define FSMONITOR_WATCH_LIMIT_BACKOFF_SECONDS 60 + +static int watch_limit_backoff_enabled(void) +{ +#ifdef __linux__ + return 1; +#else + return git_env_bool("GIT_TEST_FSMONITOR_INOTIFY_BACKOFF", 0); +#endif +} + +static int read_inotify_watch_limit(unsigned long *limit) +{ +#ifdef __linux__ + struct strbuf value = STRBUF_INIT; + int ret = -1; + + if (strbuf_read_file(&value, + "/proc/sys/fs/inotify/max_user_watches", 64) < 0) + goto done; + strbuf_trim(&value); + if (git_parse_ulong(value.buf, limit)) + ret = 0; +done: + strbuf_release(&value); + return ret; +#else + *limit = 0; + return 0; +#endif +} + +void fsmonitor_ipc__record_watch_limit_failure(const char *worktree_identity) +{ + struct lock_file lock = LOCK_INIT; + struct strbuf contents = STRBUF_INIT; + unsigned long limit; + char *path; + int fd; + + if (!watch_limit_backoff_enabled() || !worktree_identity || + strlen(worktree_identity) != FSMONITOR_IPC_WORKTREE_ID_HEX || + read_inotify_watch_limit(&limit)) + return; + path = repo_git_path(the_repository, FSMONITOR_WATCH_LIMIT_MARKER); + fd = hold_lock_file_for_update(&lock, path, LOCK_NO_DEREF); + if (fd < 0) + goto done; + strbuf_addf(&contents, "%s%s\n%lu\n", + FSMONITOR_WATCH_LIMIT_MAGIC, worktree_identity, limit); + if (fchmod(fd, 0600) || + write_in_full(fd, contents.buf, contents.len) != + (ssize_t)contents.len || + commit_lock_file(&lock)) + rollback_lock_file(&lock); +done: + strbuf_release(&contents); + free(path); +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ + char *path; + + if (!watch_limit_backoff_enabled()) + return; + path = repo_git_path(the_repository, FSMONITOR_WATCH_LIMIT_MARKER); + unlink(path); + free(path); +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r) +{ + struct strbuf contents = STRBUF_INIT; + struct strbuf identity = STRBUF_INIT; + struct stat st; + const char *recorded_identity, *recorded_limit; + unsigned long limit, current_limit; + time_t now; + char *path, *identity_end, *limit_end; + int fd, ret = 0; + + if (!watch_limit_backoff_enabled()) + return 0; + path = repo_git_path(r, FSMONITOR_WATCH_LIMIT_MARKER); + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); + if (fd < 0) + goto done; + if (fstat(fd, &st) || !S_ISREG(st.st_mode) || + st.st_uid != geteuid() || st.st_nlink != 1 || + (st.st_mode & 077) || st.st_size < 0 || st.st_size > 256) + goto close_fd; + now = time(NULL); + if (now < st.st_mtime || + now - st.st_mtime > FSMONITOR_WATCH_LIMIT_BACKOFF_SECONDS) + goto clear_marker; + if (strbuf_read(&contents, fd, st.st_size) != st.st_size || + !skip_prefix(contents.buf, FSMONITOR_WATCH_LIMIT_MAGIC, + &recorded_identity) || + !(identity_end = strchr(contents.buf + + strlen(FSMONITOR_WATCH_LIMIT_MAGIC), '\n'))) + goto close_fd; + *identity_end = '\0'; + recorded_limit = identity_end + 1; + if (!(limit_end = strchr(identity_end + 1, '\n')) || limit_end[1]) + goto close_fd; + *limit_end = '\0'; + if (!git_parse_ulong(recorded_limit, &limit) || + read_inotify_watch_limit(¤t_limit)) + goto close_fd; + if (limit != current_limit) + goto clear_marker; + if (fsmonitor_ipc__get_worktree_identity(r, &identity)) + goto close_fd; + if (strcmp(recorded_identity, identity.buf)) { +#ifndef __linux__ + if (!git_env_bool("GIT_TEST_FSMONITOR_INOTIFY_BACKOFF", 0) || + strcmp(recorded_identity, "test-worktree")) +#endif + goto close_fd; + } + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + goto clear_marker; + ret = 1; + goto close_fd; + +clear_marker: + unlink(path); +close_fd: + close(fd); +done: + strbuf_release(&identity); + strbuf_release(&contents); + free(path); + return ret; +} +#else +void fsmonitor_ipc__record_watch_limit_failure( + const char *worktree_identity UNUSED) +{ +} + +void fsmonitor_ipc__clear_watch_limit_failure(void) +{ +} + +int fsmonitor_ipc__watch_limit_backoff(struct repository *r UNUSED) +{ + return 0; +} +#endif + +static unsigned int get_start_timeout(void) +{ + const char *value; + int timeout; + + if (!repo_config_get_value(the_repository, + FSMONITOR_START_TIMEOUT_KEY, &value) && + value && git_parse_int(value, &timeout) && timeout >= 0) + return timeout; + return FSMONITOR_START_TIMEOUT_DEFAULT; +} + +static int spawn_wait_cb(const struct child_process *cmd UNUSED, + void *cb_data UNUSED) +{ + switch (fsmonitor_ipc__get_state()) { + case IPC_STATE__LISTENING: + return 0; + case IPC_STATE__NOT_LISTENING: + case IPC_STATE__PATH_NOT_FOUND: + return 1; + default: + case IPC_STATE__INVALID_PATH: + case IPC_STATE__OTHER_ERROR: + return -1; + } +} + static int spawn_daemon(void) { struct child_process cmd = CHILD_PROCESS_INIT; + struct strbuf canonical_worktree = STRBUF_INIT; + enum start_bg_result result; + unsigned int timeout = get_start_timeout(); + const char *git = git_executable_path(); + const char *worktree = repo_get_work_tree(the_repository); + int ret = -1; - cmd.git_cmd = 1; + if (!worktree || + !strbuf_realpath(&canonical_worktree, worktree, 0)) { + error(_("cannot start fsmonitor daemon without a work tree")); + goto done; + } + + prepare_spawn_env(&cmd.env); + cmd.dir = canonical_worktree.buf; + if (git) + strvec_push(&cmd.args, git); + else + cmd.git_cmd = 1; cmd.no_stdin = 1; cmd.no_stdout = 1; cmd.no_stderr = 1; cmd.close_fd_above_stderr = 1; cmd.trace2_child_class = "fsmonitor"; - strvec_pushl(&cmd.args, "fsmonitor--daemon", "start", NULL); + strvec_pushl(&cmd.args, "fsmonitor--daemon", "run", "--detach", NULL); + + result = start_bg_command(&cmd, spawn_wait_cb, NULL, timeout); + if (result == SBGR_READY || + fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + ret = 0; +done: + strbuf_release(&canonical_worktree); + return ret; +} + +static int try_send_command(const char *command, struct strbuf *answer, + enum ipc_active_state *state_out, int quietly) +{ + struct ipc_client_connection *connection = NULL; + struct ipc_client_connect_options options + = IPC_CLIENT_CONNECT_OPTIONS_INIT; + enum ipc_active_state state; + int ret = -1; + + strbuf_reset(answer); + options.wait_if_busy = 1; + options.wait_if_not_found = 0; + + state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository), + &options, &connection); + if (state == IPC_STATE__LISTENING) { + if (quietly) + ret = ipc_client_send_command_to_connection_gently( + connection, command, strlen(command), answer); + else + ret = ipc_client_send_command_to_connection( + connection, command, strlen(command), answer); + ipc_client_close_connection(connection); + } + + if (state_out) + *state_out = state; + return ret; +} + +static int is_trivial_response(const struct strbuf *answer) +{ + const char *nul = memchr(answer->buf, '\0', answer->len); + + return nul && nul != answer->buf && + answer->len == (size_t)(nul - answer->buf) + 3 && + nul[1] == '/' && nul[2] == '\0'; +} + +static int has_capability(const struct strbuf *answer, + const char *capability) +{ + const char *p = answer->buf; + const char *end = answer->buf + answer->len; + size_t capability_len = strlen(capability); + + while (p < end) { + const char *eol = memchr(p, '\n', end - p); + const char *line_end = eol ? eol : end; + + if ((size_t)(line_end - p) == capability_len && + !memcmp(p, capability, capability_len)) + return 1; + if (!eol) + break; + p = eol + 1; + } + return 0; +} + +static int server_supports_bound_queries(void) +{ + struct strbuf answer = STRBUF_INIT; + int ret; + + ret = !try_send_command(FSMONITOR_IPC_CAPABILITY_COMMAND, + &answer, NULL, 1) && + has_capability(&answer, FSMONITOR_IPC_QUERY_VERSION); + strbuf_release(&answer); + return ret; +} - return run_command(&cmd); +static int server_supports_required_capabilities(void) +{ + struct strbuf answer = STRBUF_INIT; + int ret; + + ret = !try_send_command(FSMONITOR_IPC_CAPABILITY_COMMAND, + &answer, NULL, 1) && + has_capability(&answer, FSMONITOR_IPC_QUERY_VERSION) && + has_capability(&answer, + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY); +#ifdef __APPLE__ + ret = ret && + has_capability(&answer, + FSMONITOR_IPC_HARDLINK_QUERY_VERSION) && + has_capability(&answer, + FSMONITOR_IPC_DIR_METADATA_CAPABILITY) && + has_capability(&answer, + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY); +#endif + strbuf_release(&answer); + return ret; +} + +static int response_identifies_cookie_retiring_daemon( + const struct strbuf *answer) +{ + static const char prefix[] = + "builtin:" +#ifdef __APPLE__ + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX +#endif + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX; + const char *end = memchr(answer->buf, '\0', answer->len); + + return end && + (size_t)(end - answer->buf) >= sizeof(prefix) - 1 && + !memcmp(answer->buf, prefix, sizeof(prefix) - 1); +} + +#if defined(__APPLE__) || defined(__linux__) +static int legacy_peer_credentials( + struct ipc_client_connection *connection, pid_t *pid) +{ +#ifdef __APPLE__ + uid_t uid; + gid_t gid; + socklen_t size = sizeof(*pid); + + if (getpeereid(connection->fd, &uid, &gid) || + uid != geteuid() || + getsockopt(connection->fd, SOL_LOCAL, LOCAL_PEERPID, + pid, &size) || size != sizeof(*pid)) + return 0; +#else + struct ucred peer; + socklen_t size = sizeof(peer); + + if (getsockopt(connection->fd, SOL_SOCKET, SO_PEERCRED, + &peer, &size) || size != sizeof(peer) || + peer.uid != geteuid()) + return 0; + *pid = peer.pid; +#endif + return *pid > 0; +} + +static int legacy_peer_start_identity(pid_t pid, struct strbuf *identity) +{ +#ifdef __APPLE__ + struct proc_bsdinfo info; + + if (proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, + &info, sizeof(info)) != sizeof(info) || + info.pbi_pid != (uint32_t)pid || + info.pbi_uid != geteuid()) + return 0; + strbuf_addf(identity, "%"PRIu64".%"PRIu64, + info.pbi_start_tvsec, info.pbi_start_tvusec); +#else + struct strbuf path = STRBUF_INIT; + struct strbuf stat = STRBUF_INIT; + const char *value, *end; + int valid = 0; + + strbuf_addf(&path, "/proc/%"PRIuMAX"/stat", (uintmax_t)pid); + if (strbuf_read_file(&stat, path.buf, 4096) < 0 || + !(value = strrchr(stat.buf, ')')) || + value[1] != ' ') + goto done; + value += 2; + for (int field = 3; field < 22; field++) { + value = strchr(value, ' '); + if (!value) + goto done; + while (*value == ' ') + value++; + } + end = strchr(value, ' '); + if (!end || end == value) + goto done; + for (const char *p = value; p < end; p++) + if (!isdigit(*p)) + goto done; + strbuf_add(identity, value, end - value); + valid = 1; +done: + strbuf_release(&path); + strbuf_release(&stat); + return valid; +#endif + return 1; +} + +#ifdef __APPLE__ +static int legacy_peer_watches_worktree( + pid_t pid, const char *worktree, const struct stat *root) +{ + struct proc_fdinfo *fds = NULL; + int size, bytes, matches = 0; + + size = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, NULL, 0); + if (size <= 0 || size > 1024 * 1024 - + 16 * (int)sizeof(*fds)) + return 0; + size += 16 * sizeof(*fds); + fds = xmalloc(size); + bytes = proc_pidinfo(pid, PROC_PIDLISTFDS, 0, fds, size); + if (bytes < 0 || bytes % sizeof(*fds)) + goto done; + for (int i = 0; i < bytes / (int)sizeof(*fds); i++) { + struct vnode_fdinfowithpath vnode; + const struct vinfo_stat *stat; + + if (fds[i].proc_fdtype != PROX_FDTYPE_VNODE || + proc_pidfdinfo(pid, fds[i].proc_fd, + PROC_PIDFDVNODEPATHINFO, + &vnode, sizeof(vnode)) != sizeof(vnode)) + continue; + stat = &vnode.pvip.vip_vi.vi_stat; + if ((uintmax_t)stat->vst_dev == (uintmax_t)root->st_dev && + (uintmax_t)stat->vst_ino == (uintmax_t)root->st_ino && + !strcmp(vnode.pvip.vip_path, worktree)) { + matches = 1; + break; + } + } +done: + free(fds); + return matches; +} +#else +static int legacy_peer_watches_worktree( + pid_t pid, const char *worktree UNUSED, const struct stat *root) +{ + struct strbuf directory = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + struct strbuf target = STRBUF_INIT; + struct strbuf line = STRBUF_INIT; + uintmax_t device = ((uintmax_t)major(root->st_dev) << 20) | + (uintmax_t)minor(root->st_dev); + DIR *fds = NULL; + struct dirent *entry; + int matches = 0; + + strbuf_addf(&directory, "/proc/%"PRIuMAX"/fd", (uintmax_t)pid); + fds = opendir(directory.buf); + if (!fds) + goto done; + while ((entry = readdir(fds)) != NULL) { + FILE *info; + + if (!strcmp(entry->d_name, ".") || + !strcmp(entry->d_name, "..")) + continue; + strbuf_reset(&path); + strbuf_addf(&path, "%s/%s", directory.buf, entry->d_name); + strbuf_reset(&target); + if (strbuf_readlink(&target, path.buf, 32) < 0 || + strcmp(target.buf, "anon_inode:inotify")) + continue; + strbuf_reset(&path); + strbuf_addf(&path, "/proc/%"PRIuMAX"/fdinfo/%s", + (uintmax_t)pid, entry->d_name); + info = fopen(path.buf, "r"); + if (!info) + continue; + while (!strbuf_getline_lf(&line, info)) { + uintmax_t inode, source_device; + unsigned int watch; + + if (!starts_with(line.buf, "inotify wd:1 ")) + continue; + if (sscanf(line.buf, + "inotify wd:%x ino:%"SCNxMAX" sdev:%"SCNxMAX, + &watch, &inode, &source_device) == 3 && + watch == 1 && inode == (uintmax_t)root->st_ino && + source_device == device) + matches = 1; + break; + } + fclose(info); + if (matches) + break; + } +done: + if (fds) + closedir(fds); + strbuf_release(&directory); + strbuf_release(&path); + strbuf_release(&target); + strbuf_release(&line); + return matches; +} +#endif + +static int legacy_identity_cache_matches( + const char *path, const struct strbuf *expected) +{ + struct strbuf actual = STRBUF_INIT; + struct stat st; + int fd, matches = 0; + + fd = open(path, O_RDONLY | O_CLOEXEC | O_NOFOLLOW); + if (fd < 0) + return 0; + if (!fstat(fd, &st) && S_ISREG(st.st_mode) && + st.st_uid == geteuid() && !(st.st_mode & 022) && + st.st_size >= 0 && (uintmax_t)st.st_size == expected->len && + strbuf_read(&actual, fd, expected->len) == (ssize_t)expected->len) + matches = !strbuf_cmp(&actual, expected); + close(fd); + strbuf_release(&actual); + return matches; } +static void cache_legacy_peer_identity( + const char *path, const struct strbuf *identity) +{ + struct lock_file lock = LOCK_INIT; + int fd = hold_lock_file_for_update(&lock, path, LOCK_NO_DEREF); + + if (fd < 0) + return; + if (fchmod(fd, 0600) || + write_in_full(fd, identity->buf, identity->len) != + (ssize_t)identity->len || + commit_lock_file(&lock)) + rollback_lock_file(&lock); +} + +static int try_send_attested_legacy_query( + const char *token, const struct strbuf *identity, + struct strbuf *answer) +{ + struct ipc_client_connect_options options = + IPC_CLIENT_CONNECT_OPTIONS_INIT; + struct ipc_client_connection *connection = NULL; + struct strbuf worktree = STRBUF_INIT; + struct strbuf path = STRBUF_INIT; + struct strbuf expected = STRBUF_INIT; + struct strbuf peer_start = STRBUF_INIT; + struct stat root, socket; + pid_t pid; + int cached, ret = -1; + + if (!token || !starts_with(token, "builtin:") || + !repo_get_work_tree(the_repository) || + !strbuf_realpath(&worktree, + repo_get_work_tree(the_repository), 0) || + stat(worktree.buf, &root) || !S_ISDIR(root.st_mode)) + goto done; + options.wait_if_busy = 1; + if (ipc_client_try_connect( + fsmonitor_ipc__get_path(the_repository), + &options, &connection) != IPC_STATE__LISTENING || + !legacy_peer_credentials(connection, &pid) || + !legacy_peer_start_identity(pid, &peer_start) || + lstat(fsmonitor_ipc__get_path(the_repository), &socket) || + !S_ISSOCK(socket.st_mode)) + goto done; + strbuf_addf(&path, "%s.legacy-identity", + fsmonitor_ipc__get_path(the_repository)); + strbuf_addf(&expected, + "v1\n%s\n%"PRIuMAX"\n%"PRIuMAX"\n%s\n%"PRIuMAX"\n%"PRIuMAX"\n", + identity->buf, (uintmax_t)geteuid(), (uintmax_t)pid, + peer_start.buf, + (uintmax_t)socket.st_dev, (uintmax_t)socket.st_ino); + cached = legacy_identity_cache_matches(path.buf, &expected); + if (!cached && + !legacy_peer_watches_worktree(pid, worktree.buf, &root)) + goto done; + if (!cached) + cache_legacy_peer_identity(path.buf, &expected); + trace2_data_intmax("fsm_client", NULL, + cached ? "query/legacy-peer-cached" : + "query/legacy-peer-authenticated", 1); + ret = ipc_client_send_command_to_connection_gently( + connection, token, strlen(token), answer); +done: + ipc_client_close_connection(connection); + strbuf_release(&worktree); + strbuf_release(&path); + strbuf_release(&expected); + strbuf_release(&peer_start); + return ret; +} +#else +static int try_send_attested_legacy_query( + const char *token UNUSED, const struct strbuf *identity UNUSED, + struct strbuf *answer UNUSED) +{ + return -1; +} +#endif + +static int wait_for_daemon_exit(const struct stat *original_socket) +{ + uintmax_t elapsed_ms = 0; + uintmax_t timeout_ms = (uintmax_t)get_start_timeout() * 1000; + + while (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) { + if (original_socket) { + struct stat current_socket; + + if (!lstat(fsmonitor_ipc__get_path(the_repository), + ¤t_socket) && + (current_socket.st_dev != original_socket->st_dev || + current_socket.st_ino != original_socket->st_ino)) + return 1; + } + if (elapsed_ms >= timeout_ms) + return -1; + sleep_millisec(50); + elapsed_ms += 50; + } + return 0; +} + +static int restart_incompatible_daemon(void) +{ + struct strbuf answer = STRBUF_INIT; + struct strbuf lock_path = STRBUF_INIT; + struct lock_file restart_lock = LOCK_INIT; + uintmax_t timeout_ms = (uintmax_t)get_start_timeout() * 1000; + long lock_timeout_ms = timeout_ms > LONG_MAX ? + LONG_MAX : (long)timeout_ms; + unsigned int restart_attempts = 0; + int have_lock = 0; + int ret = -1; + + /* + * Serialize the re-probe, quit, wait, and spawn sequence. This uses a + * different lock from the one used briefly while binding the socket. + */ + strbuf_addf(&lock_path, "%s.restart", + fsmonitor_ipc__get_path(the_repository)); + if (hold_lock_file_for_update_timeout(&restart_lock, lock_path.buf, + LOCK_NO_DEREF, + lock_timeout_ms) < 0) { + if (server_supports_required_capabilities()) + ret = 0; + goto done; + } + have_lock = 1; + + trace2_data_intmax("fsm_client", NULL, + "query/incompatible-daemon", 1); + while (restart_attempts++ < 32) { + struct stat socket_stat; + const struct stat *original_socket = NULL; + int wait_result; + + /* Another client may have replaced the daemon while we waited. */ + if (server_supports_required_capabilities()) + goto success; + if (!lstat(fsmonitor_ipc__get_path(the_repository), + &socket_stat)) + original_socket = &socket_stat; + if (try_send_command("quit", &answer, NULL, 1)) { + /* + * The failed connection may already have been replaced. + * Re-read its state before abandoning the upgrade. + */ + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) { + if (server_supports_required_capabilities()) + ret = 0; + goto done; + } + } + + wait_result = wait_for_daemon_exit(original_socket); + if (wait_result < 0) + goto done; + if (wait_result > 0) { + trace2_data_intmax("fsm_client", NULL, + "query/restart-raced", 1); + continue; + } + + /* The retried bound query still verifies any raced replacement. */ + if (fsmonitor_ipc__get_state() != IPC_STATE__LISTENING && + spawn_daemon()) + goto done; + goto success; + } + goto done; + +success: + ret = 0; + +done: + if (have_lock) + rollback_lock_file(&restart_lock); + strbuf_release(&lock_path); + strbuf_release(&answer); + return ret; +} + +#ifdef __APPLE__ +static int spawn_daemon_serialized(void) +{ + struct strbuf lock_path = STRBUF_INIT; + struct lock_file restart_lock = LOCK_INIT; + uintmax_t timeout_ms = (uintmax_t)get_start_timeout() * 1000; + long lock_timeout_ms = timeout_ms > LONG_MAX ? + LONG_MAX : (long)timeout_ms; + int have_lock = 0; + int ret = -1; + + strbuf_addf(&lock_path, "%s.restart", + fsmonitor_ipc__get_path(the_repository)); + if (hold_lock_file_for_update_timeout(&restart_lock, lock_path.buf, + LOCK_NO_DEREF, + lock_timeout_ms) < 0) { + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING) + ret = 0; + goto done; + } + have_lock = 1; + if (fsmonitor_ipc__get_state() == IPC_STATE__LISTENING || + !spawn_daemon()) + ret = 0; + +done: + if (have_lock) + rollback_lock_file(&restart_lock); + strbuf_release(&lock_path); + return ret; +} +#endif + int fsmonitor_ipc__send_query(const char *since_token, - struct strbuf *answer) + struct strbuf *answer, + int *legacy_worktree_authenticated) { + struct strbuf command = STRBUF_INIT; + struct strbuf identity = STRBUF_INIT; int ret = -1; - int tried_to_spawn = 0; + int lifecycle_attempts = 0; enum ipc_active_state state = IPC_STATE__OTHER_ERROR; struct ipc_client_connection *connection = NULL; struct ipc_client_connect_options options = IPC_CLIENT_CONNECT_OPTIONS_INIT; const char *tok = since_token ? since_token : ""; - size_t tok_len = since_token ? strlen(since_token) : 0; + + if (legacy_worktree_authenticated) + *legacy_worktree_authenticated = 0; + trace2_region_enter("fsm_client", "query", NULL); + if (fsmonitor_ipc__get_worktree_identity(the_repository, &identity)) { + trace2_data_intmax("fsm_client", NULL, + "query/worktree-identity-error", 1); + goto done; + } +#ifdef __APPLE__ + strbuf_addstr(&command, FSMONITOR_IPC_HARDLINK_QUERY_PREFIX); +#else + strbuf_addstr(&command, FSMONITOR_IPC_QUERY_PREFIX); +#endif + strbuf_addbuf(&command, &identity); + strbuf_addch(&command, '\n'); + strbuf_addstr(&command, tok); options.wait_if_busy = 1; options.wait_if_not_found = 0; - trace2_region_enter("fsm_client", "query", NULL); trace2_data_string("fsm_client", NULL, "query/command", tok); try_again: + strbuf_reset(answer); state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository), &options, &connection); switch (state) { case IPC_STATE__LISTENING: - ret = ipc_client_send_command_to_connection( - connection, tok, tok_len, answer); + ret = ipc_client_send_command_to_connection_gently( + connection, command.buf, command.len, answer); ipc_client_close_connection(connection); + connection = NULL; + if (ret && lifecycle_attempts++ < FSMONITOR_RESTART_ATTEMPTS) { + trace2_data_intmax("fsm_client", NULL, + "query/reconnect-after-failed-send", 1); + /* Let a missing daemon enter normal startup without polling. */ + options.wait_if_not_found = 0; + goto try_again; + } trace2_data_intmax("fsm_client", NULL, "query/response-length", answer->len); + if (!ret && + !response_identifies_cookie_retiring_daemon(answer)) { + int compatible = server_supports_required_capabilities(); + + trace2_data_intmax("fsm_client", NULL, + "query/unmarked-response", 1); + strbuf_reset(answer); + ret = -1; + if (lifecycle_attempts++ >= FSMONITOR_RESTART_ATTEMPTS || + (!compatible && restart_incompatible_daemon())) + goto done; + options.wait_if_not_found = !compatible; + goto try_again; + } + if (!ret && is_trivial_response(answer) && + !server_supports_bound_queries()) { + if (!try_send_attested_legacy_query( + tok, &identity, answer)) { + if (response_identifies_cookie_retiring_daemon(answer)) { + if (legacy_worktree_authenticated) + *legacy_worktree_authenticated = 1; + ret = 0; + goto done; + } + trace2_data_intmax("fsm_client", NULL, + "query/unmarked-response", 1); + } + /* + * A daemon predating bound queries treats query-v1 as + * garbage and returns a valid trivial response. Never + * accept that unbound result. Replace the daemon with + * the invoking Git executable and retry instead. + */ + strbuf_reset(answer); + ret = -1; + if (lifecycle_attempts++ >= FSMONITOR_RESTART_ATTEMPTS || + restart_incompatible_daemon()) + goto done; + options.wait_if_not_found = 1; + goto try_again; + } goto done; case IPC_STATE__NOT_LISTENING: case IPC_STATE__PATH_NOT_FOUND: - if (tried_to_spawn) + if (lifecycle_attempts++ >= FSMONITOR_RESTART_ATTEMPTS) goto done; - tried_to_spawn++; +#ifdef __APPLE__ + if (spawn_daemon_serialized()) +#else if (spawn_daemon()) +#endif goto done; /* @@ -137,6 +1024,8 @@ int fsmonitor_ipc__send_query(const char *since_token, done: trace2_region_leave("fsm_client", "query", NULL); + strbuf_release(&identity); + strbuf_release(&command); return ret; } @@ -144,30 +1033,15 @@ int fsmonitor_ipc__send_query(const char *since_token, int fsmonitor_ipc__send_command(const char *command, struct strbuf *answer) { - struct ipc_client_connection *connection = NULL; - struct ipc_client_connect_options options - = IPC_CLIENT_CONNECT_OPTIONS_INIT; - int ret; enum ipc_active_state state; const char *c = command ? command : ""; - size_t c_len = command ? strlen(command) : 0; - - strbuf_reset(answer); - - options.wait_if_busy = 1; - options.wait_if_not_found = 0; + int ret = try_send_command(c, answer, &state, 0); - state = ipc_client_try_connect(fsmonitor_ipc__get_path(the_repository), - &options, &connection); if (state != IPC_STATE__LISTENING) { die(_("fsmonitor--daemon is not running")); return -1; } - ret = ipc_client_send_command_to_connection(connection, c, c_len, - answer); - ipc_client_close_connection(connection); - if (ret == -1) { die(_("could not send '%s' command to fsmonitor--daemon"), c); return -1; diff --git a/fsmonitor-ipc.h b/fsmonitor-ipc.h index 8b489da762b047..ba1c05eea5c065 100644 --- a/fsmonitor-ipc.h +++ b/fsmonitor-ipc.h @@ -5,6 +5,31 @@ struct repository; +#define FSMONITOR_IPC_QUERY_VERSION "query-v1" +#define FSMONITOR_IPC_QUERY_PREFIX FSMONITOR_IPC_QUERY_VERSION " " +#define FSMONITOR_IPC_HARDLINK_QUERY_VERSION "query-v2" +#define FSMONITOR_IPC_HARDLINK_QUERY_PREFIX \ + FSMONITOR_IPC_HARDLINK_QUERY_VERSION " " +#define FSMONITOR_IPC_CAPABILITY_COMMAND "get-capabilities" +#define FSMONITOR_IPC_DIR_METADATA_CAPABILITY "dir-metadata-filter-v1" +#define FSMONITOR_IPC_DIR_METADATA_TOKEN_PREFIX "dirmeta-v1." +#define FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "hardlink-inode-v1" +#define FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX \ + FSMONITOR_IPC_DIR_METADATA_TOKEN_PREFIX "inode-v1." +#define FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY \ + "cookie-token-retirement-v1" +#define FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX "cookie-v1." +#define FSMONITOR_IPC_WORKTREE_ID_HEX 64 + +/* Hash the canonical worktree root and its stable filesystem identity. */ +int fsmonitor_ipc__get_worktree_identity(struct repository *r, + struct strbuf *identity); + +/* Remember a bounded, worktree-specific inotify watch-limit failure. */ +void fsmonitor_ipc__record_watch_limit_failure(const char *worktree_identity); +void fsmonitor_ipc__clear_watch_limit_failure(void); +int fsmonitor_ipc__watch_limit_backoff(struct repository *r); + /* * Returns true if built-in file system monitor daemon is defined * for this platform. @@ -35,7 +60,8 @@ enum ipc_active_state fsmonitor_ipc__get_state(void); * Returns -1 on error; 0 on success. */ int fsmonitor_ipc__send_query(const char *since_token, - struct strbuf *answer); + struct strbuf *answer, + int *legacy_worktree_authenticated); /* * Connect to a `git-fsmonitor--daemon` process via simple-ipc and diff --git a/fsmonitor-ll.h b/fsmonitor-ll.h index 0504ca07d62fa1..a458fc4fa3788a 100644 --- a/fsmonitor-ll.h +++ b/fsmonitor-ll.h @@ -4,6 +4,19 @@ struct index_state; struct strbuf; +/* A provider-only marker; worktree-relative paths cannot begin with '/'. */ +#define FSMONITOR_PATH_GLOBAL_INVALIDATE "//" +#define FSMONITOR_PATH_HARDLINK_INODE_PREFIX "//inode:" +#define FSMONITOR_PATH_HARDLINK_INODE_HEX 16 + +enum fsmonitor_token_result { + FSMONITOR_TOKEN_NOT_PENDING = 0, + FSMONITOR_TOKEN_CLEAN, + FSMONITOR_TOKEN_CHANGED, + FSMONITOR_TOKEN_TRIVIAL, + FSMONITOR_TOKEN_ERROR, +}; + extern struct trace_key trace_fsmonitor; /* @@ -12,11 +25,19 @@ extern struct trace_key trace_fsmonitor; */ int read_fsmonitor_extension(struct index_state *istate, const void *data, unsigned long sz); +int read_fsmonitor_untracked_extension(struct index_state *istate, + const void *data, unsigned long sz); +void write_fsmonitor_untracked_extension(struct strbuf *sb, + struct index_state *istate); +void prepare_fsmonitor_untracked(struct index_state *istate); + /* * Fill the fsmonitor_dirty ewah bits with their state from the index, * before it is split during writing. */ void fill_fsmonitor_bitmap(struct index_state *istate); +void snapshot_fsmonitor_extension(struct strbuf *sb, + struct index_state *istate); /* * Write the CE_FSMONITOR_VALID state into the fsmonitor index @@ -44,6 +65,22 @@ void tweak_fsmonitor(struct index_state *istate); */ void refresh_fsmonitor(struct index_state *istate); +int fsmonitor_invalidate_attributes_path(struct index_state *istate, + const char *name); + +/* Close a provider token which was obtained before a required scan. */ +int fsmonitor_has_pending_token(const struct index_state *istate); +int fsmonitor_pending_token_from_provider(const struct index_state *istate); +/* Reopen the last accepted IPC token after an in-process operation. */ +int fsmonitor_reopen_token(struct index_state *istate); +enum fsmonitor_token_result fsmonitor_query_pending_token( + struct index_state *istate, int untracked_ready); +void fsmonitor_accept_pending_token(struct index_state *istate, + int untracked_proof_complete, + int untracked_cache_valid); +void fsmonitor_reject_pending_token(struct index_state *istate); +void fsmonitor_mark_untracked_cache_valid(struct index_state *istate); + /* * Does the received result contain the "trivial" response? */ diff --git a/fsmonitor-settings.c b/fsmonitor-settings.c index a6587a8972b184..0ae8a8c8da956a 100644 --- a/fsmonitor-settings.c +++ b/fsmonitor-settings.c @@ -5,6 +5,7 @@ #include "fsmonitor-ipc.h" #include "fsmonitor-settings.h" #include "fsmonitor-path-utils.h" +#include "trace2.h" /* * We keep this structure definition private and have getters @@ -14,6 +15,7 @@ struct fsmonitor_settings { enum fsmonitor_mode mode; enum fsmonitor_reason reason; char *hook_path; + unsigned watch_limit_backoff : 1; }; /* @@ -119,7 +121,12 @@ static void lookup_fsmonitor_settings(struct repository *r) switch (repo_config_get_maybe_bool(r, "core.fsmonitor", &bool_value)) { case 0: /* config value was set to */ - if (bool_value) + if (bool_value && fsmonitor_ipc__watch_limit_backoff(r)) { + trace2_data_intmax("fsm_client", r, + "settings/inotify-watch-limit-backoff", 1); + fsm_settings__set_disabled(r); + r->settings.fsmonitor->watch_limit_backoff = 1; + } else if (bool_value) fsm_settings__set_ipc(r); else fsm_settings__set_disabled(r); @@ -154,6 +161,15 @@ enum fsmonitor_mode fsm_settings__get_mode(struct repository *r) return r->settings.fsmonitor->mode; } +int fsm_settings__is_watch_limit_backoff(struct repository *r) +{ + if (!r->settings.fsmonitor) + lookup_fsmonitor_settings(r); + + return r->settings.fsmonitor->mode == FSMONITOR_MODE_DISABLED && + r->settings.fsmonitor->watch_limit_backoff; +} + const char *fsm_settings__get_hook_path(struct repository *r) { if (!r->settings.fsmonitor) @@ -180,6 +196,7 @@ void fsm_settings__set_ipc(struct repository *r) r->settings.fsmonitor->mode = FSMONITOR_MODE_IPC; r->settings.fsmonitor->reason = reason; + r->settings.fsmonitor->watch_limit_backoff = 0; FREE_AND_NULL(r->settings.fsmonitor->hook_path); } @@ -201,6 +218,7 @@ void fsm_settings__set_hook(struct repository *r, const char *path) r->settings.fsmonitor->mode = FSMONITOR_MODE_HOOK; r->settings.fsmonitor->reason = reason; + r->settings.fsmonitor->watch_limit_backoff = 0; FREE_AND_NULL(r->settings.fsmonitor->hook_path); r->settings.fsmonitor->hook_path = strdup(path); } @@ -212,6 +230,7 @@ void fsm_settings__set_disabled(struct repository *r) r->settings.fsmonitor->mode = FSMONITOR_MODE_DISABLED; r->settings.fsmonitor->reason = FSMONITOR_REASON_OK; + r->settings.fsmonitor->watch_limit_backoff = 0; FREE_AND_NULL(r->settings.fsmonitor->hook_path); } @@ -223,6 +242,7 @@ void fsm_settings__set_incompatible(struct repository *r, r->settings.fsmonitor->mode = FSMONITOR_MODE_INCOMPATIBLE; r->settings.fsmonitor->reason = reason; + r->settings.fsmonitor->watch_limit_backoff = 0; FREE_AND_NULL(r->settings.fsmonitor->hook_path); } diff --git a/fsmonitor-settings.h b/fsmonitor-settings.h index ab02e3995ee8f4..07e6081a5d878a 100644 --- a/fsmonitor-settings.h +++ b/fsmonitor-settings.h @@ -30,6 +30,7 @@ void fsm_settings__set_incompatible(struct repository *r, enum fsmonitor_reason reason); enum fsmonitor_mode fsm_settings__get_mode(struct repository *r); +int fsm_settings__is_watch_limit_backoff(struct repository *r); const char *fsm_settings__get_hook_path(struct repository *r); enum fsmonitor_reason fsm_settings__get_reason(struct repository *r); diff --git a/fsmonitor.c b/fsmonitor.c index 107767527ebec7..4b8a52939ee227 100644 --- a/fsmonitor.c +++ b/fsmonitor.c @@ -2,25 +2,104 @@ #define DISABLE_SIGN_COMPARE_WARNINGS #include "git-compat-util.h" +#include "abspath.h" +#include "attr.h" +#include "clean-status.h" +#include "clean-status-manifest.h" #include "config.h" #include "dir.h" #include "environment.h" #include "ewah/ewok.h" +#include "ewah/ewok_rlw.h" #include "fsmonitor.h" #include "fsmonitor-ipc.h" +#include "hashmap.h" +#include "hex-ll.h" #include "name-hash.h" +#include "replace-object.h" #include "repository.h" #include "run-command.h" #include "strbuf.h" #include "trace2.h" +#include "wrapper.h" #define INDEX_EXTENSION_VERSION1 (1) #define INDEX_EXTENSION_VERSION2 (2) +#define FSMONITOR_TOKEN_MAX (4096) #define HOOK_INTERFACE_VERSION1 (1) #define HOOK_INTERFACE_VERSION2 (2) struct trace_key trace_fsmonitor = TRACE_KEY_INIT(FSMONITOR); +static struct index_state *scoped_bootstrap_index; +static int scoped_bootstrap_eligible; +static int scoped_bootstrap_used; + +static int fsmonitor_scoped_bootstrap_is_eligible(struct index_state *istate) +{ + struct repository *repo = istate->repo; + struct stat st; + char *physical, *selected, *canonical; + int eligible; + + if (istate != repo->index || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || + getenv(INDEX_ENVIRONMENT) || + getenv(GIT_WORK_TREE_ENVIRONMENT) || + getenv(GIT_COMMON_DIR_ENVIRONMENT) || + getenv(DB_ENVIRONMENT) || + getenv(ALTERNATE_DB_ENVIRONMENT) || + !fstat_is_reliable() || + fsm_settings__get_mode(repo) != FSMONITOR_MODE_IPC || + repo_config_get_split_index(repo) > 0 || + repo_config_values(repo)->apply_sparse_checkout || + repo_has_replace_refs_uncached(repo)) + return 0; + + physical = xstrfmt("%s/index", repo_get_git_dir(repo)); + selected = real_pathdup(repo_get_index_file(repo), 0); + canonical = real_pathdup(physical, 0); + eligible = selected && canonical && + !fspathcmp(selected, canonical) && + !lstat(physical, &st) && S_ISREG(st.st_mode) && + st.st_nlink == 1; + free(canonical); + free(selected); + free(physical); + return eligible; +} + +void fsmonitor_begin_scoped_bootstrap(struct index_state *istate) +{ + if (scoped_bootstrap_index) + BUG("nested scoped fsmonitor bootstrap"); + scoped_bootstrap_index = istate; + scoped_bootstrap_eligible = + fsmonitor_scoped_bootstrap_is_eligible(istate); + scoped_bootstrap_used = 0; +} + +int fsmonitor_scoped_bootstrap_is_active(const struct index_state *istate) +{ + return scoped_bootstrap_index == istate && + scoped_bootstrap_eligible && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + !repo_config_values(istate->repo)->apply_sparse_checkout; +} + +int fsmonitor_end_scoped_bootstrap(struct index_state *istate) +{ + int used; + + if (scoped_bootstrap_index != istate) + BUG("scoped fsmonitor bootstrap index changed"); + used = scoped_bootstrap_used; + scoped_bootstrap_index = NULL; + scoped_bootstrap_eligible = 0; + scoped_bootstrap_used = 0; + return used; +} + static void assert_index_minimum(struct index_state *istate, size_t pos) { if (pos > istate->cache_nr) @@ -39,6 +118,46 @@ static void fsmonitor_ewah_callback(size_t pos, void *is) ce->ce_flags &= ~CE_FSMONITOR_VALID; } +static int fsmonitor_ewah_is_valid(struct ewah_bitmap *bitmap) +{ + size_t pointer = 0, expanded_words = 0; + size_t logical_words = bitmap->bit_size / BITS_IN_EWORD + + !!(bitmap->bit_size % BITS_IN_EWORD); + size_t padding = bitmap->bit_size % BITS_IN_EWORD; + eword_t *last_rlw = NULL; + + while (pointer < bitmap->buffer_size) { + eword_t *rlw = &bitmap->buffer[pointer]; + size_t running_words = rlw_get_running_len(rlw); + size_t literal_words = rlw_get_literal_words(rlw); + size_t i; + + last_rlw = rlw; + if (literal_words > bitmap->buffer_size - pointer - 1) + return 0; + if (running_words > logical_words - expanded_words) + return 0; + expanded_words += running_words; + if (rlw_get_run_bit(rlw) && running_words && padding && + expanded_words == logical_words) + return 0; + if (literal_words > logical_words - expanded_words) + return 0; + for (i = 0; i < literal_words; i++) { + eword_t literal = bitmap->buffer[pointer + 1 + i]; + + if (padding && + expanded_words + i + 1 == logical_words && + literal >> padding) + return 0; + } + expanded_words += literal_words; + pointer += 1 + literal_words; + } + + return expanded_words == logical_words && bitmap->rlw == last_rlw; +} + static int fsmonitor_hook_version(void) { int hook_version; @@ -59,44 +178,82 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data, unsigned long sz) { const char *index = data; + const char *end = index + sz; + const char *nul; uint32_t hdr_version; uint32_t ewah_size; + uint32_t ewah_words; + uint32_t ewah_rlw; struct ewah_bitmap *fsmonitor_dirty; int ret; uint64_t timestamp; struct strbuf last_update = STRBUF_INIT; - if (sz < sizeof(uint32_t) + 1 + sizeof(uint32_t)) - return error("corrupt fsmonitor extension (too short)"); + if (istate->fsmonitor_extension_seen) + goto invalid; + istate->fsmonitor_extension_seen = 1; + if (end - index < sizeof(uint32_t)) + goto invalid; hdr_version = get_be32(index); index += sizeof(uint32_t); if (hdr_version == INDEX_EXTENSION_VERSION1) { + if (end - index < sizeof(uint64_t)) + goto invalid; timestamp = get_be64(index); strbuf_addf(&last_update, "%"PRIu64"", timestamp); index += sizeof(uint64_t); } else if (hdr_version == INDEX_EXTENSION_VERSION2) { - strbuf_addstr(&last_update, index); - index += last_update.len + 1; + nul = memchr(index, '\0', end - index); + if (!nul || nul == index || nul - index > FSMONITOR_TOKEN_MAX) + goto invalid; + strbuf_add(&last_update, index, nul - index); + index = nul + 1; } else { - return error("bad fsmonitor version %d", hdr_version); + goto invalid; } - istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL); - + if (end - index < sizeof(uint32_t)) + goto invalid; ewah_size = get_be32(index); index += sizeof(uint32_t); + if (ewah_size != end - index || ewah_size < 3 * sizeof(uint32_t)) + goto invalid; + + /* Reject impossible EWAH lengths before its parser allocates memory. */ + ewah_words = get_be32(index + sizeof(uint32_t)); + if (ewah_words > (ewah_size - 3 * sizeof(uint32_t)) / + sizeof(eword_t) || + 3 * sizeof(uint32_t) + (size_t)ewah_words * sizeof(eword_t) != + ewah_size) + goto invalid; + ewah_rlw = get_be32(index + ewah_size - sizeof(uint32_t)); + if (ewah_rlw >= ewah_words) + goto invalid; fsmonitor_dirty = ewah_new(); ret = ewah_read_mmap(fsmonitor_dirty, index, ewah_size); if (ret != ewah_size) { ewah_free(fsmonitor_dirty); - return error("failed to parse ewah bitmap reading fsmonitor index extension"); + goto invalid; + } + if (!fsmonitor_ewah_is_valid(fsmonitor_dirty)) { + ewah_free(fsmonitor_dirty); + goto invalid; + } + if (!istate->split_index && + fsmonitor_dirty->bit_size > istate->cache_nr) { + ewah_free(fsmonitor_dirty); + goto invalid; } - istate->fsmonitor_dirty = fsmonitor_dirty; - if (!istate->split_index) - assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size); + /* Publish only after the complete optional extension is validated. */ + FREE_AND_NULL(istate->fsmonitor_last_update); + if (istate->fsmonitor_dirty) + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL); + istate->fsmonitor_dirty = fsmonitor_dirty; + istate->fsmonitor_token_valid = 1; trace2_data_string("index", NULL, "extension/fsmn/read/token", istate->fsmonitor_last_update); @@ -104,21 +261,128 @@ int read_fsmonitor_extension(struct index_state *istate, const void *data, "read fsmonitor extension successful '%s'", istate->fsmonitor_last_update); return 0; + +invalid: + istate->fsmonitor_extension_seen = 1; + istate->fsmonitor_token_valid = 0; + istate->fsmonitor_untracked_valid = 0; + FREE_AND_NULL(istate->fsmonitor_last_update); + if (istate->fsmonitor_dirty) { + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; + } + strbuf_release(&last_update); + trace2_data_intmax("fsmonitor", istate->repo, + "extension/invalid", 1); + return 0; } -void fill_fsmonitor_bitmap(struct index_state *istate) +#define FSMONITOR_UNTRACKED_EXTENSION_VERSION 1 + +int read_fsmonitor_untracked_extension(struct index_state *istate, + const void *data, unsigned long sz) +{ + const char *p = data; + const char *nul; + uint32_t version; + + if (istate->fsmonitor_untracked_extension_seen) + goto invalid; + istate->fsmonitor_untracked_extension_seen = 1; + if (sz < sizeof(version) + 2) + goto invalid; + version = get_be32(p); + p += sizeof(version); + sz -= sizeof(version); + if (version != FSMONITOR_UNTRACKED_EXTENSION_VERSION) + goto invalid; + nul = memchr(p, '\0', sz); + if (!nul || nul == p || (size_t)(nul - p + 1) != sz || + nul - p > FSMONITOR_TOKEN_MAX) + goto invalid; + + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = xstrdup(p); + return 0; + +invalid: + istate->fsmonitor_untracked_extension_seen = 1; + istate->fsmonitor_untracked_extension_invalid = 1; + istate->fsmonitor_untracked_valid = 0; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/invalid-extension", 1); + return 0; +} + +void write_fsmonitor_untracked_extension(struct strbuf *sb, + struct index_state *istate) +{ + const char *suffix; + uint32_t version; + + put_be32(&version, FSMONITOR_UNTRACKED_EXTENSION_VERSION); + strbuf_add(sb, &version, sizeof(version)); + if (!istate->fsmonitor_untracked_valid && istate->untracked && + istate->untracked->fsmonitor_revalidation) { + if (!skip_prefix(istate->fsmonitor_last_update, + "builtin:", &suffix) || + !*suffix || !strcmp(suffix, "fake")) + BUG("cannot serialize unauthenticated fsmonitor cache"); + strbuf_addstr(sb, "pending:"); + strbuf_addstr(sb, suffix); + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/provider-reset-pending", 1); + } else { + strbuf_addstr(sb, istate->fsmonitor_last_update); + } + strbuf_addch(sb, '\0'); +} + +void prepare_fsmonitor_untracked(struct index_state *istate) +{ + istate->fsmonitor_untracked_valid = + !istate->fsmonitor_untracked_extension_invalid && + (!istate->untracked || !istate->untracked->root || + (istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token))); + if (istate->fsmonitor_untracked_valid) + untracked_cache_recompute_fsmonitor_valid_recursive( + istate->untracked); + else if (istate->fsmonitor_untracked_token && + starts_with(istate->fsmonitor_untracked_token, "pending:")) + untracked_cache_preserve_for_revalidation(istate); +} + +static struct ewah_bitmap *fsmonitor_bitmap_from_index( + struct index_state *istate) { + struct ewah_bitmap *bitmap = ewah_new(); unsigned int i, skipped = 0; - istate->fsmonitor_dirty = ewah_new(); + for (i = 0; i < istate->cache_nr; i++) { if (istate->cache[i]->ce_flags & CE_REMOVE) skipped++; else if (!(istate->cache[i]->ce_flags & CE_FSMONITOR_VALID)) - ewah_set(istate->fsmonitor_dirty, i - skipped); + ewah_set(bitmap, i - skipped); } + return bitmap; } -void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate) +void fill_fsmonitor_bitmap(struct index_state *istate) +{ + struct ewah_bitmap *bitmap = fsmonitor_bitmap_from_index(istate); + + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = bitmap; +} + +static void serialize_fsmonitor_extension(struct strbuf *sb, + struct index_state *istate, + struct ewah_bitmap *bitmap) { uint32_t hdr_version; uint32_t ewah_start; @@ -126,7 +390,7 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate) int fixup = 0; if (!istate->split_index) - assert_index_minimum(istate, istate->fsmonitor_dirty->bit_size); + assert_index_minimum(istate, bitmap->bit_size); put_be32(&hdr_version, INDEX_EXTENSION_VERSION2); strbuf_add(sb, &hdr_version, sizeof(uint32_t)); @@ -138,13 +402,27 @@ void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate) strbuf_add(sb, &ewah_size, sizeof(uint32_t)); /* we'll fix this up later */ ewah_start = sb->len; - ewah_serialize_strbuf(istate->fsmonitor_dirty, sb); - ewah_free(istate->fsmonitor_dirty); - istate->fsmonitor_dirty = NULL; + ewah_serialize_strbuf(bitmap, sb); /* fix up size field */ put_be32(&ewah_size, sb->len - ewah_start); memcpy(sb->buf + fixup, &ewah_size, sizeof(uint32_t)); +} + +void snapshot_fsmonitor_extension(struct strbuf *sb, + struct index_state *istate) +{ + struct ewah_bitmap *bitmap = fsmonitor_bitmap_from_index(istate); + + serialize_fsmonitor_extension(sb, istate, bitmap); + ewah_free(bitmap); +} + +void write_fsmonitor_extension(struct strbuf *sb, struct index_state *istate) +{ + serialize_fsmonitor_extension(sb, istate, istate->fsmonitor_dirty); + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; trace2_data_string("index", NULL, "extension/fsmn/write/token", istate->fsmonitor_last_update); @@ -189,13 +467,14 @@ static int query_fsmonitor_hook(struct repository *r, } /* - * Invalidate the FSM bit on this CE. This is like mark_fsmonitor_invalid() - * but we've already handled the untracked-cache, so let's not repeat that - * work. This also lets us have a different trace message so that we can - * see everything that was done as part of the refresh-callback. + * Strongly invalidate one cache entry without touching attributes or the + * untracked cache. Callers choose those wider invalidation scopes explicitly. */ -static void invalidate_ce_fsm(struct cache_entry *ce) +void fsmonitor_invalidate_cache_entry(struct cache_entry *ce) { + ce->ce_flags &= ~CE_UPTODATE; + memset(&ce->ce_stat_data, 0, sizeof(ce->ce_stat_data)); + ce->ce_flags |= CE_CONTENT_CHECK_REQUIRED; if (ce->ce_flags & CE_FSMONITOR_VALID) { trace_printf_key(&trace_fsmonitor, "fsmonitor_refresh_callback INV: '%s'", @@ -205,7 +484,57 @@ static void invalidate_ce_fsm(struct cache_entry *ce) } static size_t handle_path_with_trailing_slash( - struct index_state *istate, const char *name, int pos); + struct index_state *istate, const char *name, int pos, + int directory_is_semantically_safe); + +int fsmonitor_invalidate_attributes_path(struct index_state *istate, + const char *name) +{ + size_t len = strlen(name), base, attr_len = strlen(GITATTRIBUTES_FILE); + size_t invalidated = 0; + unsigned int first = 0, i; + int bounded = 0; + + while (len && is_dir_sep(name[len - 1])) + len--; + base = len; + while (base && !is_dir_sep(name[base - 1])) + base--; + if (len - base != attr_len || + fspathncmp(name + base, GITATTRIBUTES_FILE, attr_len)) + return 0; + + git_attr_invalidate_all(); + if (base && !repo_ignore_case(the_repository)) { +#if defined(GIT_WINDOWS_NATIVE) || defined(__CYGWIN__) + bounded = !memchr(name, '\\', base); +#else + bounded = 1; +#endif + } + if (bounded) { + int pos = index_name_pos_sparse(istate, name, base); + + first = pos < 0 ? -pos - 1 : pos; + } + for (i = first; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + + if (base && (ce->ce_namelen < base || + fspathncmp(ce->name, name, base))) { + if (bounded) + break; + continue; + } + fsmonitor_invalidate_cache_entry(ce); + invalidated++; + } + if (invalidated) + istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/attributes-scope", base); + return invalidated > 0; +} /* * Use the name-hash to do a case-insensitive cache-entry lookup with @@ -254,7 +583,7 @@ static size_t handle_using_name_hash_icase( */ untracked_cache_invalidate_trimmed_path(istate, ce->name, 0); - invalidate_ce_fsm(ce); + fsmonitor_invalidate_cache_entry(ce); return 1; } @@ -309,7 +638,9 @@ static size_t handle_using_dir_name_hash_icase( pos = index_name_pos(istate, canonical_path.buf, canonical_path.len); nr_in_cone = handle_path_with_trailing_slash( - istate, canonical_path.buf, pos); + istate, canonical_path.buf, pos, + clean_status_directory_event_is_semantically_safe( + istate, canonical_path.buf)); strbuf_release(&canonical_path); return nr_in_cone; } @@ -347,7 +678,7 @@ static size_t handle_path_without_trailing_slash( * cache-entry with the same pathname, nor for a cone * at that directory. (That is, assume no D/F conflicts.) */ - invalidate_ce_fsm(istate->cache[pos]); + fsmonitor_invalidate_cache_entry(istate->cache[pos]); return 1; } else { size_t nr_in_cone; @@ -365,7 +696,9 @@ static size_t handle_path_without_trailing_slash( strbuf_addch(&work_path, '/'); pos = index_name_pos(istate, work_path.buf, work_path.len); nr_in_cone = handle_path_with_trailing_slash( - istate, work_path.buf, pos); + istate, work_path.buf, pos, + clean_status_directory_event_is_semantically_safe( + istate, work_path.buf)); strbuf_release(&work_path); return nr_in_cone; } @@ -404,7 +737,8 @@ static size_t handle_path_without_trailing_slash( * untracked or case-incorrect. */ static size_t handle_path_with_trailing_slash( - struct index_state *istate, const char *name, int pos) + struct index_state *istate, const char *name, int pos, + int directory_is_semantically_safe) { int i; size_t nr_in_cone = 0; @@ -425,27 +759,78 @@ static size_t handle_path_with_trailing_slash( for (i = pos; i < istate->cache_nr; i++) { if (!starts_with(istate->cache[i]->name, name)) break; - invalidate_ce_fsm(istate->cache[i]); + fsmonitor_invalidate_cache_entry(istate->cache[i]); nr_in_cone++; } + if (nr_in_cone && !directory_is_semantically_safe) { + /* + * A matched directory event may stand in for a nested + * attribute-file change. + */ + git_attr_invalidate_all(); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/attributes-cone", nr_in_cone); + } + return nr_in_cone; } -static void fsmonitor_refresh_callback(struct index_state *istate, char *name) +static void fsmonitor_refresh_callback(struct index_state *istate, char *name, + int closing_delta) { int len = strlen(name); - int pos = index_name_pos(istate, name, len); + int pos; + int attributes_may_have_changed; + int directory_is_semantically_safe; size_t nr_in_cone; trace_printf_key(&trace_fsmonitor, "fsmonitor_refresh_callback '%s' (pos %d)", - name, pos); + name, !strcmp(name, FSMONITOR_PATH_GLOBAL_INVALIDATE) ? + -1 : index_name_pos(istate, name, len)); + if (!strcmp(name, FSMONITOR_PATH_GLOBAL_INVALIDATE)) { + unsigned int i; + + clean_status_invalidate_current_manifest(istate); + git_attr_invalidate_all(); + untracked_cache_invalidate_all(istate); + for (i = 0; i < istate->cache_nr; i++) + fsmonitor_invalidate_cache_entry(istate->cache[i]); + istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", istate->repo, + "apply/global-invalidation", 1); + return; + } + pos = index_name_pos(istate, name, len); + if (pos >= 0 && + clean_status_manifest_reconcile_deleted_attribute(istate, name)) { + attributes_may_have_changed = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/attribute-source-reused", 1); + } else if (pos >= 0 && + clean_status_manifest_accept_current_display_only_attribute( + istate, name)) { + git_attr_invalidate_all(); + attributes_may_have_changed = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/nonconversion-attribute-replayed", 1); + } else { + attributes_may_have_changed = + fsmonitor_invalidate_attributes_path(istate, name); + } + directory_is_semantically_safe = name[len - 1] == '/' && + (clean_status_directory_event_is_semantically_safe(istate, name) || + (closing_delta && + clean_status_manifest_directory_unchanged(istate, name))); if (name[len - 1] == '/') - nr_in_cone = handle_path_with_trailing_slash(istate, name, pos); + nr_in_cone = handle_path_with_trailing_slash( + istate, name, pos, directory_is_semantically_safe); else nr_in_cone = handle_path_without_trailing_slash(istate, name, pos); + if (pos < 0 && nr_in_cone && !directory_is_semantically_safe) + attributes_may_have_changed = 1; /* * If we did not find an exact match for this pathname or any @@ -455,10 +840,15 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name) */ if (!nr_in_cone && repo_ignore_case(the_repository)) { nr_in_cone = handle_using_name_hash_icase(istate, name); - if (!nr_in_cone) + if (!nr_in_cone) { nr_in_cone = handle_using_dir_name_hash_icase( istate, name); + if (nr_in_cone) + attributes_may_have_changed = 1; + } } + if (attributes_may_have_changed) + clean_status_invalidate_current_manifest(istate); if (nr_in_cone) trace_printf_key(&trace_fsmonitor, @@ -503,6 +893,577 @@ static void fsmonitor_refresh_callback(struct index_state *istate, char *name) */ static int fsmonitor_force_update_threshold = 100; +static int is_trivial_response_at(const struct strbuf *result, size_t offset) +{ + size_t i; + + if (offset >= result->len || result->buf[offset] != '/') + return 0; + for (i = offset + 1; i < result->len; i++) + if (result->buf[i] != '\0' && result->buf[i] != '\n' && + result->buf[i] != '\r') + return 0; + return 1; +} + +void fsmonitor_query_result_release(struct fsmonitor_query_result *result) +{ + strbuf_release(&result->token); + strbuf_release(&result->paths); +} + +void fsmonitor_format_worktree_paths( + struct strbuf *paths, const char *path, size_t worktree_len, + int is_file, int is_directory) +{ + const char *relative = path + worktree_len; + + strbuf_reset(paths); + if (!is_file && !is_directory) + return; + + /* The root has no relative pathname; never read past its NUL. */ + if (!*relative || (*relative == '/' && !relative[1])) { + strbuf_addstr(paths, FSMONITOR_PATH_GLOBAL_INVALIDATE); + strbuf_addch(paths, '\0'); + return; + } + + relative++; + if (is_file) { + strbuf_addstr(paths, relative); + strbuf_addch(paths, '\0'); + } + if (is_directory) { + strbuf_addstr(paths, relative); + strbuf_addch(paths, '/'); + strbuf_addch(paths, '\0'); + } +} + +static int fsmonitor_valid_worktree_path(const char *path, size_t len) +{ + struct strbuf copy = STRBUF_INIT; + int valid = 0; + + if (!len || is_dir_sep(path[0]) || has_dos_drive_prefix(path)) + return 0; + strbuf_add(©, path, len); + if (is_dir_sep(copy.buf[copy.len - 1])) + strbuf_setlen(©, copy.len - 1); + if (!copy.len || is_dir_sep(copy.buf[copy.len - 1])) + goto done; + valid = verify_path(copy.buf, 0); + +done: + strbuf_release(©); + return valid; +} + +static int fsmonitor_parse_hardlink_inode(const char *path, size_t len, + uint32_t *inode) +{ + const char *hex; + uint64_t value = 0; + size_t i; + + if (!skip_prefix(path, FSMONITOR_PATH_HARDLINK_INODE_PREFIX, &hex)) + return 0; + if (len != strlen(FSMONITOR_PATH_HARDLINK_INODE_PREFIX) + + FSMONITOR_PATH_HARDLINK_INODE_HEX) + return -1; + for (i = 0; i < FSMONITOR_PATH_HARDLINK_INODE_HEX; i++) { + unsigned int digit = hexval(hex[i]); + + if (digit > 0xf) + return -1; + value = (value << 4) | digit; + } + if (!value) + return -1; + if (inode) + *inode = (uint32_t)value; + return 1; +} + +enum fsmonitor_query_outcome fsmonitor_parse_builtin_response( + const struct strbuf *raw, struct fsmonitor_query_result *result) +{ + const char *nul, *p, *end; + + if (!raw->len) + goto malformed; + nul = memchr(raw->buf, '\0', raw->len); + if (!nul || nul == raw->buf || nul - raw->buf > FSMONITOR_TOKEN_MAX) + goto malformed; + strbuf_add(&result->token, raw->buf, nul - raw->buf); + if (!starts_with(result->token.buf, "builtin:")) + goto malformed; + + p = nul + 1; + end = raw->buf + raw->len; + if (p == end) { + result->outcome = FSMONITOR_QUERY_DELTA; + return result->outcome; + } + if (end[-1] != '\0') + goto malformed; + if (end - p == 2 && p[0] == '/' && p[1] == '\0') { + result->outcome = FSMONITOR_QUERY_TRIVIAL; + return result->outcome; + } + + while (p < end) { + nul = memchr(p, '\0', end - p); + if (!nul || nul == p) + goto malformed; + if (strcmp(p, FSMONITOR_PATH_GLOBAL_INVALIDATE) && + fsmonitor_parse_hardlink_inode(p, nul - p, NULL) <= 0 && + !fsmonitor_valid_worktree_path(p, nul - p)) + goto malformed; + p = nul + 1; + } + strbuf_add(&result->paths, raw->buf + result->token.len + 1, + end - (raw->buf + result->token.len + 1)); + result->outcome = FSMONITOR_QUERY_DELTA; + return result->outcome; + +malformed: + strbuf_reset(&result->token); + strbuf_reset(&result->paths); + trace2_data_intmax("fsm_client", NULL, "query/invalid-response", 1); + return FSMONITOR_QUERY_ERROR; +} + +static int fsmonitor_test_query_barrier(size_t query_nr) +{ + const char *at = getenv("GIT_TEST_FSMONITOR_QUERY_BARRIER_AT"); + const char *ready = getenv("GIT_TEST_FSMONITOR_QUERY_BARRIER_READY"); + const char *resume = getenv("GIT_TEST_FSMONITOR_QUERY_BARRIER_RESUME"); + struct stat st; + uintmax_t selected; + char *end; + char resumed; + int fd, ret; + + if (!at && !ready && !resume) + return 0; + if (!at || !ready || !resume || !*at || !*ready || !*resume || + !isdigit((unsigned char)*at)) + return -1; + errno = 0; + selected = strtoumax(at, &end, 10); + if (errno || *end || !selected) + return -1; + if (selected != (uintmax_t)query_nr) + return 0; + if (lstat(resume, &st) || !S_ISFIFO(st.st_mode)) + return -1; + fd = open(ready, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); + if (fd < 0) + return -1; + ret = write_in_full(fd, "ready\n", 6) == 6 ? 0 : -1; + if (close(fd) || ret) + return -1; + fd = open(resume, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -1; + ret = read_in_full(fd, &resumed, 1) == 1 ? 0 : -1; + if (close(fd)) + ret = -1; + return ret; +} + +enum fsmonitor_query_outcome query_builtin_fsmonitor( + const char *since_token, struct fsmonitor_query_result *result) +{ + const char *test_sequence = + getenv("GIT_TEST_FSMONITOR_QUERY_SEQUENCE"); + struct strbuf raw = STRBUF_INIT; + int legacy_authenticated = 0; + + /* + * Tests may script clean, delta, trivial, and error responses with + * C, D, T, and E. A delta uses GIT_TEST_FSMONITOR_QUERY_PATH. + */ + if (test_sequence && *test_sequence) { + static size_t query_nr; + const char *path; + char outcome; + + if (query_nr >= strlen(test_sequence)) + return FSMONITOR_QUERY_ERROR; + outcome = test_sequence[query_nr++]; + if (fsmonitor_test_query_barrier(query_nr)) + return FSMONITOR_QUERY_ERROR; + if (outcome == 'E') + return FSMONITOR_QUERY_ERROR; + + strbuf_addf(&result->token, "builtin:test:%"PRIuMAX, + (uintmax_t)query_nr); + if (outcome == 'T') { + result->outcome = FSMONITOR_QUERY_TRIVIAL; + return result->outcome; + } + if (outcome == 'D') { + path = getenv("GIT_TEST_FSMONITOR_QUERY_PATH"); + if (!path || !*path) + return FSMONITOR_QUERY_ERROR; + strbuf_addstr(&result->paths, path); + strbuf_addch(&result->paths, '\0'); + } else if (outcome != 'C') { + return FSMONITOR_QUERY_ERROR; + } + result->outcome = FSMONITOR_QUERY_DELTA; + return result->outcome; + } + + if (!fsmonitor_ipc__send_query( + since_token, &raw, &legacy_authenticated)) { + fsmonitor_parse_builtin_response(&raw, result); + result->legacy_worktree_authenticated = + legacy_authenticated; + } + strbuf_release(&raw); + return result->outcome; +} + +struct fsmonitor_hardlink_inode { + struct hashmap_entry ent; + uint32_t inode; +}; + +static int fsmonitor_hardlink_inode_cmp(const void *unused UNUSED, + const struct hashmap_entry *eptr, + const struct hashmap_entry *entry_or_key, + const void *keydata) +{ + const struct fsmonitor_hardlink_inode *entry = + container_of(eptr, const struct fsmonitor_hardlink_inode, ent); + const uint32_t *inode = keydata; + + if (inode) + return entry->inode != *inode; + return entry->inode != + container_of(entry_or_key, + const struct fsmonitor_hardlink_inode, ent)->inode; +} + +static int apply_fsmonitor_paths(struct index_state *istate, + const struct strbuf *paths, int closing_delta) +{ + const char *p = paths->buf; + const char *end = paths->buf + paths->len; + struct hashmap inodes = HASHMAP_INIT(fsmonitor_hardlink_inode_cmp, NULL); + struct fsmonitor_hardlink_inode *entry; + unsigned int matches = 0; + int count = 0; + + if (closing_delta) { + for (const char *changed = p; changed < end; + changed += strlen(changed) + 1) { + size_t changed_len = strlen(changed); + + if (!strcmp(changed, FSMONITOR_PATH_GLOBAL_INVALIDATE) || + fsmonitor_parse_hardlink_inode( + changed, changed_len, NULL)) { + closing_delta = 0; + break; + } + } + } + + while (p < end) { + size_t len = strlen(p); + uint32_t inode; + int parsed = fsmonitor_parse_hardlink_inode(p, len, &inode); + + if (parsed < 0) { + fsmonitor_refresh_callback( + istate, (char *)FSMONITOR_PATH_GLOBAL_INVALIDATE, 0); + count++; + goto done; + } + if (!parsed) { + fsmonitor_refresh_callback( + istate, (char *)p, closing_delta); + count++; + } else if (!hashmap_get_entry_from_hash( + &inodes, memhash(&inode, sizeof(inode)), &inode, + struct fsmonitor_hardlink_inode, ent)) { + CALLOC_ARRAY(entry, 1); + entry->inode = inode; + hashmap_entry_init(&entry->ent, + memhash(&inode, sizeof(inode))); + hashmap_add(&inodes, &entry->ent); + } + p += len + 1; + } + + if (hashmap_get_size(&inodes)) { + unsigned int i; + + trace2_data_intmax("fsmonitor", istate->repo, + "apply/hardlink-inode-events", + hashmap_get_size(&inodes)); + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + uint32_t inode = ce->ce_stat_data.sd_ino; + + if (inode && + !hashmap_get_entry_from_hash( + &inodes, memhash(&inode, sizeof(inode)), &inode, + struct fsmonitor_hardlink_inode, ent)) + continue; + fsmonitor_refresh_callback(istate, ce->name, 0); + matches++; + count++; + } + trace2_data_intmax("fsmonitor", istate->repo, + "apply/hardlink-index-scan", 1); + trace2_data_intmax("fsmonitor", istate->repo, + "apply/hardlink-matches", matches); + } + +done: + hashmap_clear_and_free(&inodes, struct fsmonitor_hardlink_inode, ent); + return count; +} + +static void adopt_legacy_untracked_cache( + struct index_state *istate, + const struct fsmonitor_query_result *result, + int semantic_baseline_needed) +{ + if (fstat_is_reliable() && !istate->split_index && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat && + result->outcome == FSMONITOR_QUERY_TRIVIAL && + istate->fsmonitor_token_valid && + istate->fsmonitor_last_update && + !strcmp(istate->fsmonitor_last_update, "builtin:fake") && + !istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + istate->untracked && istate->untracked->root) { + /* + * A client without daemon support records builtin:fake. Its + * UNTR tree is still useful with ordinary directory timestamp + * validation, but it cannot certify fsmonitor acceleration. + */ + istate->fsmonitor_legacy_untracked_fallback = 1; + istate->untracked->use_fsmonitor = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/legacy-stat-fallback", 1); + return; + } + if (!semantic_baseline_needed || + !result->legacy_worktree_authenticated || + result->outcome != FSMONITOR_QUERY_DELTA || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || + istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_extension_invalid || + !istate->untracked || !istate->untracked->root) { + untracked_cache_discard_legacy(istate); + return; + } + if (!untracked_cache_adopt_legacy(istate)) + return; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + istate->fsmonitor_untracked_valid = 1; + istate->fsmonitor_legacy_untracked_adopted = 1; + istate->untracked->use_fsmonitor = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/legacy-adopted", 1); +} + +static void invalidate_all_fsmonitor(struct index_state *istate) +{ + unsigned int i; + int changed = 0; + + istate->fsmonitor_untracked_revalidation_authenticated = 0; + for (i = 0; i < istate->cache_nr; i++) { + if (istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) + changed = 1; + istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID; + } + istate->fsmonitor_untracked_valid = 0; + if (istate->untracked) + istate->untracked->use_fsmonitor = 0; + if (changed) + istate->cache_changed |= FSMONITOR_CHANGED; +} + +/* + * A forward baseline still needs one ordinary stat refresh before its + * provider token can certify the index. Clear only process-local + * uptodate state so that refresh_index() performs those stats without + * escalating to content checks. + */ +static void invalidate_all_fsmonitor_for_baseline( + struct index_state *istate) +{ + unsigned int i; + int preserve_untracked = istate->fsmonitor_legacy_untracked_adopted && + istate->fsmonitor_untracked_valid; + + invalidate_all_fsmonitor(istate); + if (preserve_untracked) { + istate->fsmonitor_untracked_valid = 1; + istate->untracked->use_fsmonitor = 1; + } + for (i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags &= ~CE_UPTODATE; +} + +static void invalidate_all_fsmonitor_strong(struct index_state *istate) +{ + unsigned int i; + int provider_disabled = + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_DISABLED; + + invalidate_all_fsmonitor(istate); + for (i = 0; i < istate->cache_nr; i++) { + struct cache_entry *ce = istate->cache[i]; + + if (provider_disabled && ce_skip_worktree(ce)) + continue; + fsmonitor_invalidate_cache_entry(ce); + } +} + +void fsmonitor_invalidate_semantics(struct index_state *istate) +{ + istate->fsmonitor_legacy_untracked_adopted = 0; + clean_status_invalidate_current_proof(istate); + git_attr_invalidate_all(); + invalidate_all_fsmonitor_strong(istate); + istate->cache_changed |= FSMONITOR_CHANGED; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/strong-invalidation", 1); +} + +static void invalidate_fsmonitor_for_bootstrap( + struct index_state *istate, enum fsmonitor_mode mode, + int semantic_adoption_needed, int semantic_baseline_needed, + int physical_history_unavailable, int provider_query_success) +{ + int manifest_refresh_failed; + + if (!fstat_is_reliable() || mode != FSMONITOR_MODE_IPC || + istate->split_index) { + invalidate_all_fsmonitor(istate); + return; + } + if (getenv(INDEX_ENVIRONMENT) && + !clean_status_has_persistent_fsmonitor_semantic_history(istate)) { + char *physical = xstrfmt("%s/index", repo_get_git_dir(istate->repo)); + char *selected = real_pathdup(repo_get_index_file(istate->repo), 0); + char *canonical = real_pathdup(physical, 0); + char *physical_lock = canonical ? xstrfmt("%s.lock", canonical) : NULL; + struct stat selected_stat, physical_stat; + int temporary = selected && canonical && + fspathcmp(selected, canonical) && + fspathcmp(selected, physical_lock) && + !stat(selected, &selected_stat) && + !stat(canonical, &physical_stat) && + (selected_stat.st_dev != physical_stat.st_dev || + selected_stat.st_ino != physical_stat.st_ino); + + free(physical_lock); + free(canonical); + free(selected); + free(physical); + if (temporary) { + fsmonitor_invalidate_semantics(istate); + untracked_cache_invalidate_all(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/temporary-index-stat-fallback", 1); + return; + } + } + + if (fsmonitor_scoped_bootstrap_is_active(istate)) { + fsmonitor_invalidate_semantics(istate); + untracked_cache_invalidate_all(istate); + scoped_bootstrap_used = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/scoped-reader-stat-fallback", 1); + return; + } + + if (physical_history_unavailable) { + int authenticated_manifest = + clean_status_has_authenticated_worktree_manifest(istate); + int pending_revalidation = + istate->fsmonitor_untracked_revalidation_authenticated; + int preserve_untracked = 0; + + if (istate->fsmonitor_legacy_untracked_fallback) { + invalidate_all_fsmonitor_for_baseline(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/legacy-stat-fallback", 1); + return; + } + manifest_refresh_failed = + (pending_revalidation || + !clean_status_has_authenticated_bootstrap_manifest(istate)) && + clean_status_refresh_worktree_manifest(istate) < 0; + if (provider_query_success && !manifest_refresh_failed && + !clean_status_manifest_global_fallback(istate) && + !clean_status_fsmonitor_strong_mismatch(istate) && + !clean_status_filter_scope_needs_validation(istate) && + (!pending_revalidation || + (istate->fsmonitor_untracked_revalidation_authenticated && + istate->untracked && istate->untracked->root && + istate->untracked->root->valid && + clean_status_pending_revalidation_manifest_unchanged( + istate))) && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat) { + /* Strong stat identity survives a lost provider boundary. */ + if ((authenticated_manifest && + !clean_status_fsmonitor_config_mismatch(istate)) || + pending_revalidation) + preserve_untracked = + untracked_cache_preserve_for_revalidation(istate); + if (pending_revalidation && preserve_untracked) + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/provider-reset-resumed", 1); + clean_status_begin_fsmonitor_semantic_baseline(istate); + invalidate_all_fsmonitor_for_baseline(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/token-reset-stat-baseline", 1); + } else { + fsmonitor_invalidate_semantics(istate); + } + if (!preserve_untracked) + untracked_cache_invalidate_all(istate); + return; + } + + manifest_refresh_failed = + !clean_status_has_authenticated_worktree_manifest(istate) && + clean_status_refresh_worktree_manifest(istate) < 0; + if (manifest_refresh_failed || + clean_status_manifest_global_fallback(istate) || + (semantic_adoption_needed && !semantic_baseline_needed)) { + fsmonitor_invalidate_semantics(istate); + } else { + if (semantic_baseline_needed) { + clean_status_begin_fsmonitor_semantic_baseline(istate); + invalidate_all_fsmonitor_for_baseline(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "semantic/adoption-baseline", 1); + } else { + invalidate_all_fsmonitor(istate); + } + } +} + void refresh_fsmonitor(struct index_state *istate) { static int warn_once = 0; @@ -514,6 +1475,10 @@ void refresh_fsmonitor(struct index_state *istate) char *buf; unsigned int i; int is_trivial = 0; + int tracked_requires_bootstrap; + int untracked_requires_bootstrap; + int semantic_adoption_needed; + int semantic_baseline_needed; struct repository *r = istate->repo; enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(r); enum fsmonitor_reason reason = fsm_settings__get_reason(r); @@ -530,28 +1495,33 @@ void refresh_fsmonitor(struct index_state *istate) return; istate->fsmonitor_has_run_once = 1; + semantic_adoption_needed = fstat_is_reliable() && + !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_semantic_adoption_needed(istate); + semantic_baseline_needed = fstat_is_reliable() && + !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_semantic_baseline_needed(istate); trace_printf_key(&trace_fsmonitor, "refresh fsmonitor"); if (fsm_mode == FSMONITOR_MODE_IPC) { - query_success = !fsmonitor_ipc__send_query( + struct fsmonitor_query_result result = + FSMONITOR_QUERY_RESULT_INIT; + + query_builtin_fsmonitor( istate->fsmonitor_last_update ? istate->fsmonitor_last_update : "builtin:fake", - &query_result); - if (query_success) { - /* - * The response contains a series of nul terminated - * strings. The first is the new token. - * - * Use `char *buf` as an interlude to trick the CI - * static analysis to let us use `strbuf_addstr()` - * here (and only copy the token) rather than - * `strbuf_addbuf()`. - */ - buf = query_result.buf; - strbuf_addstr(&last_update_token, buf); - bol = last_update_token.len + 1; - is_trivial = query_result.buf[bol] == '/'; + &result); + adopt_legacy_untracked_cache( + istate, &result, semantic_baseline_needed); + if (result.outcome != FSMONITOR_QUERY_ERROR) { + query_success = 1; + strbuf_addbuf(&last_update_token, &result.token); + is_trivial = result.outcome == FSMONITOR_QUERY_TRIVIAL; + if (!is_trivial) + strbuf_addbuf(&query_result, &result.paths); if (is_trivial) trace2_data_intmax("fsm_client", NULL, "query/trivial-response", 1); @@ -567,6 +1537,7 @@ void refresh_fsmonitor(struct index_state *istate) */ strbuf_addstr(&last_update_token, "builtin:fake"); } + fsmonitor_query_result_release(&result); goto apply_results; } @@ -612,7 +1583,8 @@ void refresh_fsmonitor(struct index_state *istate) query_success = 0; } else { bol = last_update_token.len + 1; - is_trivial = query_result.buf[bol] == '/'; + is_trivial = is_trivial_response_at( + &query_result, bol); } } else if (hook_version < 0) { hook_version = HOOK_INTERFACE_VERSION1; @@ -626,7 +1598,7 @@ void refresh_fsmonitor(struct index_state *istate) r, HOOK_INTERFACE_VERSION1, istate->fsmonitor_last_update, &query_result); if (query_success) - is_trivial = query_result.buf[0] == '/'; + is_trivial = is_trivial_response_at(&query_result, 0); } if (is_trivial) @@ -657,6 +1629,13 @@ void refresh_fsmonitor(struct index_state *istate) */ trace2_region_enter("fsmonitor", "apply_results", istate->repo); + tracked_requires_bootstrap = !query_success || is_trivial || + !istate->fsmonitor_token_valid || + (fstat_is_reliable() && !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_config_mismatch(istate)); + untracked_requires_bootstrap = !istate->fsmonitor_untracked_valid; + if (query_success && !is_trivial) { /* * Mark all pathnames returned by the monitor as dirty. @@ -665,22 +1644,70 @@ void refresh_fsmonitor(struct index_state *istate) */ int count = 0; - buf = query_result.buf; - for (i = bol; i < query_result.len; i++) { - if (buf[i] != '\0') - continue; - fsmonitor_refresh_callback(istate, buf + bol); - bol = i + 1; - count++; + if (fsm_mode == FSMONITOR_MODE_IPC) { + count = apply_fsmonitor_paths(istate, &query_result, 0); + } else { + buf = query_result.buf; + for (i = bol; i < query_result.len; i++) { + if (buf[i] != '\0') + continue; + if (i > bol) { + fsmonitor_refresh_callback( + istate, buf + bol, 0); + count++; + } + bol = i + 1; + } + if (bol < query_result.len) { + fsmonitor_refresh_callback(istate, buf + bol, 0); + count++; + } } - if (bol < query_result.len) { - fsmonitor_refresh_callback(istate, buf + bol); - count++; + + /* + * Applying a provider event may expire semantic history after + * the initial bootstrap decision. Keep the new token pending + * until status has rescanned against rebuilt inputs. + */ + if (fstat_is_reliable() && !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_config_mismatch(istate)) { + if (clean_status_try_preserve_tracked_config_epoch(istate)) { + tracked_requires_bootstrap = 0; + trace2_data_intmax("fsmonitor", istate->repo, + "config/tracked-epoch-preserved", 1); + } else { + tracked_requires_bootstrap = 1; + } + } + + if (tracked_requires_bootstrap) { + /* + * Provider paths can invalidate the manifest or + * semantic inputs after our pre-query snapshot. + * Recheck before choosing the narrow baseline lane. + */ + semantic_adoption_needed = fstat_is_reliable() && + !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_semantic_adoption_needed( + istate); + semantic_baseline_needed = fstat_is_reliable() && + !istate->split_index && + fsm_mode == FSMONITOR_MODE_IPC && + clean_status_fsmonitor_semantic_baseline_needed( + istate); + invalidate_fsmonitor_for_bootstrap( + istate, fsm_mode, semantic_adoption_needed, + semantic_baseline_needed, + !istate->fsmonitor_token_valid, query_success); } /* Now mark the untracked cache for fsmonitor usage */ if (istate->untracked) - istate->untracked->use_fsmonitor = 1; + istate->untracked->use_fsmonitor = + !tracked_requires_bootstrap && + !untracked_requires_bootstrap; if (count > fsmonitor_force_update_threshold) istate->cache_changed |= FSMONITOR_CHANGED; @@ -697,32 +1724,223 @@ void refresh_fsmonitor(struct index_state *istate) * we've actually changed entries, so keep track if we * actually changed entries or not. */ - int is_cache_changed = 0; + invalidate_fsmonitor_for_bootstrap( + istate, fsm_mode, semantic_adoption_needed, + semantic_baseline_needed, 1, query_success); + } + trace2_region_leave("fsmonitor", "apply_results", istate->repo); - for (i = 0; i < istate->cache_nr; i++) { - if (istate->cache[i]->ce_flags & CE_FSMONITOR_VALID) { - is_cache_changed = 1; - istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID; - } - } + strbuf_release(&query_result); + /* + * A token obtained before a full scan cannot describe changes which + * race with that scan. Keep it in memory until the caller closes the + * race with a second query. The last valid token remains safe because + * a query relative to it will return a superset of changes. + */ + if (tracked_requires_bootstrap) { + if (!last_update_token.len) { + if (istate->fsmonitor_last_update) + strbuf_addstr(&last_update_token, + istate->fsmonitor_last_update); + else + strbuf_addstr(&last_update_token, "builtin:fake"); + } + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_last_update_pending = + strbuf_detach(&last_update_token, NULL); /* - * If we're going to check every file, ensure we save - * the results. + * A trivial response cannot validate prior state, but its + * returned token is still a provider-owned boundary. Use it + * to anchor the complete scan which the caller will close with + * another query. Hook providers cannot perform that closing + * query, so do not publish their trivial-response tokens. */ - if (is_cache_changed) - istate->cache_changed |= FSMONITOR_CHANGED; + istate->fsmonitor_pending_token_from_provider = + query_success && + (fsm_mode == FSMONITOR_MODE_IPC || !is_trivial); + if (istate->fsmonitor_legacy_untracked_adopted) { + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update_pending); + } else { + istate->fsmonitor_untracked_valid = 0; + } + } else { + /* + * The applied delta carries an existing proof forward: + * tracked paths are now invalid in FSMN, while semantic + * events have already expired the proof itself. + */ + if (fsm_mode == FSMONITOR_MODE_IPC) + clean_status_advance_fsmonitor_config_token( + istate, last_update_token.buf); + FREE_AND_NULL(istate->fsmonitor_last_update); + istate->fsmonitor_last_update = + strbuf_detach(&last_update_token, NULL); + if (untracked_requires_bootstrap) { + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_last_update_pending = + xstrdup(istate->fsmonitor_last_update); + istate->fsmonitor_pending_token_from_provider = 1; + } else { + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_pending_token_from_provider = 0; + } + if (istate->fsmonitor_untracked_valid && istate->untracked) { + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + } + } +} - if (istate->untracked) - istate->untracked->use_fsmonitor = 0; +int fsmonitor_has_pending_token(const struct index_state *istate) +{ + return !!istate->fsmonitor_last_update_pending; +} + +int fsmonitor_pending_token_from_provider(const struct index_state *istate) +{ + return istate->fsmonitor_last_update_pending && + istate->fsmonitor_pending_token_from_provider; +} + +int fsmonitor_reopen_token(struct index_state *istate) +{ + if (!fstat_is_reliable() || istate->split_index || + fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC) + return 0; + if (istate->fsmonitor_last_update_pending) + return istate->fsmonitor_pending_token_from_provider; + if (!istate->fsmonitor_token_valid || !istate->fsmonitor_last_update) + return 0; + istate->fsmonitor_last_update_pending = + xstrdup(istate->fsmonitor_last_update); + istate->fsmonitor_pending_token_from_provider = 1; + return 1; +} + +enum fsmonitor_token_result fsmonitor_query_pending_token( + struct index_state *istate, int untracked_ready) +{ + struct fsmonitor_query_result result = FSMONITOR_QUERY_RESULT_INIT; + enum fsmonitor_token_result ret; + int count; + + if (!istate->fsmonitor_last_update_pending) + return FSMONITOR_TOKEN_NOT_PENDING; + if (fsm_settings__get_mode(istate->repo) != FSMONITOR_MODE_IPC) + return FSMONITOR_TOKEN_ERROR; + + query_builtin_fsmonitor(istate->fsmonitor_last_update_pending, &result); + if (result.outcome == FSMONITOR_QUERY_ERROR) { + istate->fsmonitor_pending_token_from_provider = 0; + ret = FSMONITOR_TOKEN_ERROR; + goto done; } - trace2_region_leave("fsmonitor", "apply_results", istate->repo); - strbuf_release(&query_result); + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_last_update_pending = + strbuf_detach(&result.token, NULL); + istate->fsmonitor_pending_token_from_provider = 1; + if (result.outcome == FSMONITOR_QUERY_TRIVIAL) { + invalidate_all_fsmonitor_strong(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "token_closure/trivial", 1); + ret = FSMONITOR_TOKEN_TRIVIAL; + goto done; + } + + count = apply_fsmonitor_paths(istate, &result.paths, 1); + if (istate->untracked) + istate->untracked->use_fsmonitor = !!untracked_ready; + trace2_data_intmax("fsmonitor", istate->repo, + "token_closure/apply_count", count); + ret = count ? FSMONITOR_TOKEN_CHANGED : FSMONITOR_TOKEN_CLEAN; - /* Now that we've updated istate, save the last_update_token */ +done: + fsmonitor_query_result_release(&result); + return ret; +} + +void fsmonitor_accept_pending_token(struct index_state *istate, + int untracked_proof_complete, + int untracked_cache_valid) +{ + if (untracked_cache_valid && !untracked_proof_complete) + BUG("valid untracked cache without a complete proof"); + if (!fsmonitor_pending_token_from_provider(istate)) + return; + istate->fsmonitor_untracked_revalidation_authenticated = 0; FREE_AND_NULL(istate->fsmonitor_last_update); - istate->fsmonitor_last_update = strbuf_detach(&last_update_token, NULL); + istate->fsmonitor_last_update = istate->fsmonitor_last_update_pending; + istate->fsmonitor_last_update_pending = NULL; + istate->fsmonitor_pending_token_from_provider = 0; + istate->fsmonitor_token_valid = 1; + istate->fsmonitor_untracked_valid = !!untracked_cache_valid; + if (istate->untracked) { + if (istate->untracked->fsmonitor_revalidation && + untracked_cache_valid) { + istate->fsmonitor_untracked_must_persist = 1; + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/provider-reset-revalidated", 1); + } + if (untracked_cache_valid && + !istate->fsmonitor_untracked_extension_seen && + istate == istate->repo->index && + !getenv(INDEX_ENVIRONMENT) && !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC) + istate->fsmonitor_untracked_must_persist = 1; + istate->untracked->fsmonitor_revalidation = 0; + istate->untracked->use_fsmonitor = !!untracked_cache_valid; + } + istate->cache_changed |= FSMONITOR_CHANGED; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + if (untracked_cache_valid) + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + else if (!untracked_proof_complete) { + /* + * Keep a query anchored at the accepted tracked token. A + * later in-process status may need to close work done after + * this point before validating its untracked cache. + */ + istate->fsmonitor_last_update_pending = + xstrdup(istate->fsmonitor_last_update); + istate->fsmonitor_pending_token_from_provider = 1; + } + trace2_data_intmax("fsmonitor", istate->repo, + "token_closure/accepted", 1); +} + +void fsmonitor_reject_pending_token(struct index_state *istate) +{ + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_pending_token_from_provider = 0; + if (istate->untracked) + istate->untracked->fsmonitor_revalidation = 0; + if (!istate->fsmonitor_token_valid) + FREE_AND_NULL(istate->fsmonitor_last_update); + invalidate_all_fsmonitor_strong(istate); + trace2_data_intmax("fsmonitor", istate->repo, + "token_closure/rejected", 1); +} + +void fsmonitor_mark_untracked_cache_valid(struct index_state *istate) +{ + if (fsm_settings__is_watch_limit_backoff(istate->repo) || + istate->fsmonitor_last_update_pending || + !istate->fsmonitor_token_valid || + !istate->fsmonitor_last_update || !istate->untracked || + istate->fsmonitor_untracked_valid) + return; + istate->fsmonitor_untracked_valid = 1; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + istate->fsmonitor_untracked_token = + xstrdup(istate->fsmonitor_last_update); + istate->cache_changed |= FSMONITOR_CHANGED; } /* @@ -754,6 +1972,7 @@ static void initialize_fsmonitor_last_update(struct index_state *istate) strbuf_addf(&last_update, "%"PRIu64"", getnanotime()); istate->fsmonitor_last_update = strbuf_detach(&last_update, NULL); + istate->fsmonitor_token_valid = 0; } void add_fsmonitor(struct index_state *istate) @@ -772,7 +1991,7 @@ void add_fsmonitor(struct index_state *istate) /* reset the untracked cache */ if (istate->untracked) { add_untracked_cache(istate); - istate->untracked->use_fsmonitor = 1; + istate->untracked->use_fsmonitor = 0; } /* Update the fsmonitor state */ @@ -782,6 +2001,14 @@ void add_fsmonitor(struct index_state *istate) void remove_fsmonitor(struct index_state *istate) { + istate->fsmonitor_token_valid = 0; + istate->fsmonitor_untracked_valid = 0; + istate->fsmonitor_untracked_revalidation_authenticated = 0; + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_pending_token_from_provider = 0; + FREE_AND_NULL(istate->fsmonitor_untracked_token); + if (istate->untracked) + istate->untracked->use_fsmonitor = 0; if (istate->fsmonitor_last_update) { trace_printf_key(&trace_fsmonitor, "remove fsmonitor"); istate->cache_changed |= FSMONITOR_CHANGED; @@ -795,6 +2022,27 @@ void tweak_fsmonitor(struct index_state *istate) int fsmonitor_enabled = (fsm_settings__get_mode(istate->repo) > FSMONITOR_MODE_DISABLED); + if (fsm_settings__is_watch_limit_backoff(istate->repo)) { + int suspended = clean_status_suspend_fsmonitor_for_backoff(istate); + + /* Historical tokens never make a live entry clean while disabled. */ + for (i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags &= + ~(CE_FSMONITOR_VALID | CE_UPTODATE); + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; + if (!suspended) { + remove_fsmonitor(istate); + return; + } + istate->fsmonitor_untracked_valid = 0; + istate->fsmonitor_untracked_revalidation_authenticated = 0; + FREE_AND_NULL(istate->fsmonitor_last_update_pending); + istate->fsmonitor_pending_token_from_provider = 0; + istate->untracked->use_fsmonitor = 0; + return; + } + if (istate->fsmonitor_dirty) { if (fsmonitor_enabled) { /* Mark all entries valid */ diff --git a/fsmonitor.h b/fsmonitor.h index 5195a8624db82b..6d5f3d3bc34c29 100644 --- a/fsmonitor.h +++ b/fsmonitor.h @@ -2,12 +2,75 @@ #define FSMONITOR_H #include "fsmonitor-ll.h" +#include "clean-status.h" #include "dir.h" #include "fsmonitor-settings.h" #include "object.h" #include "read-cache-ll.h" +#include "strbuf.h" #include "trace.h" +/* + * Force the next stat-aware caller to verify this entry's content. Wider + * invalidation, such as attributes or untracked-cache state, is the caller's + * responsibility. + */ +void fsmonitor_invalidate_cache_entry(struct cache_entry *ce); + +enum fsmonitor_query_outcome { + FSMONITOR_QUERY_ERROR = 0, + FSMONITOR_QUERY_DELTA, + FSMONITOR_QUERY_TRIVIAL, +}; + +struct fsmonitor_query_result { + enum fsmonitor_query_outcome outcome; + struct strbuf token; + struct strbuf paths; + unsigned int legacy_worktree_authenticated : 1; +}; + +#define FSMONITOR_QUERY_RESULT_INIT { \ + .outcome = FSMONITOR_QUERY_ERROR, \ + .token = STRBUF_INIT, \ + .paths = STRBUF_INIT, \ +} + +void fsmonitor_query_result_release(struct fsmonitor_query_result *result); + +/* + * Encode an already classified and alias-resolved worktree event. The caller + * must have verified that worktree_len names the path's worktree prefix. + */ +void fsmonitor_format_worktree_paths( + struct strbuf *paths, const char *path, size_t worktree_len, + int is_file, int is_directory); + +enum fsmonitor_query_outcome fsmonitor_parse_builtin_response( + const struct strbuf *raw, struct fsmonitor_query_result *result); +enum fsmonitor_query_outcome query_builtin_fsmonitor( + const char *since_token, struct fsmonitor_query_result *result); + +/* + * A pathname monitor cannot prove that every name for a multiply-linked + * inode is inside its watch cone. When the platform reports real link + * counts, keep such regular files out of the persistent valid bitmap so + * that every new process checks their stat data. Platforms that synthesize + * link counts retain their existing fsmonitor behavior. The in-process + * CE_UPTODATE bit is still safe after the caller's lstat(). + */ +static inline int fsmonitor_stat_can_be_valid(const struct stat *st) +{ + return !S_ISREG(st->st_mode) || st->st_nlink <= 1; +} + +void fsmonitor_invalidate_semantics(struct index_state *istate); + +/* Bound conservative bootstrap to one index read; never issue a proof. */ +void fsmonitor_begin_scoped_bootstrap(struct index_state *istate); +int fsmonitor_scoped_bootstrap_is_active(const struct index_state *istate); +int fsmonitor_end_scoped_bootstrap(struct index_state *istate); + /* * Check if refresh_fsmonitor has been called at least once. * refresh_fsmonitor is idempotent. Returns true if fsmonitor is @@ -60,8 +123,11 @@ static inline void mark_fsmonitor_valid(struct index_state *istate, struct cache static inline void mark_fsmonitor_invalid(struct index_state *istate, struct cache_entry *ce) { enum fsmonitor_mode fsm_mode = fsm_settings__get_mode(istate->repo); + int backoff = fsm_settings__is_watch_limit_backoff(istate->repo); - if (fsm_mode > FSMONITOR_MODE_DISABLED) { + if (fsm_mode > FSMONITOR_MODE_DISABLED || backoff) { + if (backoff) + clean_status_invalidate_current_proof(istate); ce->ce_flags &= ~CE_FSMONITOR_VALID; untracked_cache_invalidate_path(istate, ce->name, 1); trace_printf_key(&trace_fsmonitor, "mark_fsmonitor_invalid '%s'", ce->name); diff --git a/git.c b/git.c index e5f1811b6bb762..956a0c2e4d8f54 100644 --- a/git.c +++ b/git.c @@ -929,6 +929,9 @@ int cmd_main(int argc, const char **argv) if (slash) cmd = slash + 1; } + /* A renamed dispatcher is still safer to re-exec than a Git from PATH. */ + if (!starts_with(cmd, "git-")) + git_mark_executable_as_dispatcher(); trace_command_performance(argv); diff --git a/hash-framing.h b/hash-framing.h new file mode 100644 index 00000000000000..6808cc288faa04 --- /dev/null +++ b/hash-framing.h @@ -0,0 +1,51 @@ +#ifndef HASH_FRAMING_H +#define HASH_FRAMING_H + +#include "hash.h" +#include "strbuf.h" + +static inline void hash_length_delimited(struct git_hash_ctx *ctx, + const void *data, size_t len) +{ + uint32_t size; + + if (len > UINT32_MAX) + BUG("length-delimited hash input too long"); + put_be32(&size, len); + git_hash_update(ctx, &size, sizeof(size)); + if (len) + git_hash_update(ctx, data, len); +} + +static inline void hash_optional_cstring(struct git_hash_ctx *ctx, + const char *value) +{ + static const unsigned char missing = 0; + + if (value) + hash_length_delimited(ctx, value, strlen(value)); + else + hash_length_delimited(ctx, &missing, sizeof(missing)); +} + +static inline void hash_buffer_digest(const struct git_hash_algo *algo, + const void *data, size_t len, + unsigned char *hash) +{ + struct git_hash_ctx ctx; + + git_hash_init(&ctx, algo); + git_hash_update(&ctx, data, len); + git_hash_final(hash, &ctx); +} + +static inline void hash_append_checksum(struct strbuf *out, + const struct git_hash_algo *algo) +{ + unsigned char hash[GIT_MAX_RAWSZ]; + + hash_buffer_digest(algo, out->buf, out->len, hash); + strbuf_add(out, hash, algo->rawsz); +} + +#endif /* HASH_FRAMING_H */ diff --git a/merge-ort.c b/merge-ort.c index c410a5d353234c..88157406d1750e 100644 --- a/merge-ort.c +++ b/merge-ort.c @@ -24,6 +24,7 @@ #include "advice.h" #include "attr.h" #include "cache-tree.h" +#include "clean-status.h" #include "commit.h" #include "commit-reach.h" #include "config.h" @@ -4603,7 +4604,8 @@ static int process_entries(struct merge_options *opt, static int checkout(struct merge_options *opt, struct tree *prev, - struct tree *next) + struct tree *next, + int preserve_semantic_history) { /* Switch the index/working copy from old to new */ int ret; @@ -4629,6 +4631,12 @@ static int checkout(struct merge_options *opt, /* 2-way merge to the new branch */ unpack_opts.update = 1; unpack_opts.merge = 1; + unpack_opts.preserve_semantic_history = + preserve_semantic_history && + clean_status_revalidated_token_matches(opt->repo->index); + unpack_opts.preserve_backoff_history = + preserve_semantic_history && + clean_status_fsmonitor_backoff_suspended(opt->repo->index); unpack_opts.quiet = 0; /* FIXME: sequencer might want quiet? */ unpack_opts.verbose_update = (opt->verbosity > 2); unpack_opts.fn = twoway_merge; @@ -4933,7 +4941,7 @@ void merge_switch_to_result(struct merge_options *opt, assert(opt->priv == NULL); if (result->clean >= 0 && update_worktree_and_index) { trace2_region_enter("merge", "checkout", opt->repo); - if (checkout(opt, head, result->tree)) { + if (checkout(opt, head, result->tree, result->clean > 0)) { /* failure to function */ result->clean = -1; merge_finalize(opt, result); diff --git a/merge.c b/merge.c index 0f5e823e63ed5f..ac37e84ad87465 100644 --- a/merge.c +++ b/merge.c @@ -2,6 +2,7 @@ #include "git-compat-util.h" #include "gettext.h" +#include "clean-status.h" #include "hash.h" #include "hex.h" #include "lockfile.h" @@ -96,6 +97,8 @@ int checkout_fast_forward(struct repository *r, opts.update = 1; opts.verbose_update = 1; opts.merge = 1; + opts.preserve_semantic_history = + clean_status_revalidated_token_matches(r->index); opts.fn = twoway_merge; init_checkout_metadata(&opts.meta, NULL, remote, NULL); setup_unpack_trees_porcelain(&opts, "merge"); diff --git a/meson.build b/meson.build index 7073d5844d2531..e584fe0f7b36dd 100644 --- a/meson.build +++ b/meson.build @@ -317,6 +317,8 @@ libgit_sources = [ 'archive-tar.c', 'archive-zip.c', 'archive.c', + 'attr-fingerprint.c', + 'attr-manifest.c', 'attr.c', 'base85.c', 'bisect.c', @@ -331,6 +333,17 @@ libgit_sources = [ 'chdir-notify.c', 'checkout.c', 'chunk-format.c', + 'clean-status.c', + 'clean-status-config.c', + 'clean-status-epoch.c', + 'clean-status-history-store.c', + 'clean-status-history.c', + 'clean-status-identity.c', + 'clean-status-index.c', + 'clean-status-manifest.c', + 'clean-status-sidecar.c', + 'clean-status-fast.c', + 'clean-status-sidecar-issue.c', 'color.c', 'column.c', 'combine-diff.c', @@ -368,6 +381,7 @@ libgit_sources = [ 'editor.c', 'entry.c', 'environment.c', + 'exclude-source-proof.c', 'ewah/bitmap.c', 'ewah/ewah_bitmap.c', 'ewah/ewah_io.c', @@ -378,6 +392,7 @@ libgit_sources = [ 'fetch-pack.c', 'fmt-merge-msg.c', 'fsck.c', + 'fsmonitor-clean-proof.c', 'fsmonitor.c', 'fsmonitor-ipc.c', 'fsmonitor-settings.c', @@ -460,6 +475,7 @@ libgit_sources = [ 'patch-delta.c', 'patch-ids.c', 'path.c', + 'path-namespace.c', 'path-walk.c', 'pathspec.c', 'pkt-line.c', @@ -522,6 +538,11 @@ libgit_sources = [ 'run-command.c', 'send-pack.c', 'sequencer.c', + 'semantic-verify-file.c', + 'semantic-verify-path.c', + 'semantic-verify-root.c', + 'semantic-verify-worker.c', + 'semantic-verify.c', 'serve.c', 'server-info.c', 'setup.c', @@ -576,6 +597,8 @@ libgit_sources = [ 'walker.c', 'wildmatch.c', 'worktree.c', + 'worktree-attr-manifest.c', + 'worktree-attr-source.c', 'wrapper.c', 'write-or-die.c', 'ws.c', @@ -1296,8 +1319,11 @@ endif if host_machine.system() == 'darwin' compat_sources += 'compat/precompose_utf8.c' + libgit_c_args += '-DHAVE_PRELOAD_INDEX_BULK' libgit_c_args += '-DPRECOMPOSE_UNICODE' libgit_c_args += '-DPROTECT_HFS_DEFAULT' +elif host_machine.system() == 'linux' + libgit_c_args += '-DHAVE_PRELOAD_INDEX_BULK' endif # Configure general compatibility wrappers. @@ -1339,11 +1365,33 @@ elif host_machine.system() == 'windows' endif if host_machine.system() == 'linux' - compat_sources += 'compat/linux/procinfo.c' + compat_sources += [ + 'compat/linux/procinfo.c', + 'compat/preload-index/bulk-linux.c', + 'compat/preload-index/bulk-linux-entry.c', + 'compat/preload-index/bulk-linux-open.c', + 'compat/preload-index/bulk-linux-scan.c', + 'compat/preload-index/bulk-linux-stat.c', + 'compat/preload-index/bulk-linux-topology.c', + ] + libgit_sources += [ + 'preload-index-bulk-index.c', + 'preload-index-bulk-thread.c', + 'preload-index-bulk.c', + ] elif host_machine.system() == 'windows' compat_sources += 'compat/win32/trace2_win32_process_info.c' elif host_machine.system() == 'darwin' - compat_sources += 'compat/darwin/procinfo.c' + compat_sources += [ + 'compat/darwin/procinfo.c', + 'compat/preload-index/bulk-darwin.c', + 'compat/preload-index/bulk-darwin-root.c', + ] + libgit_sources += [ + 'preload-index-bulk-index.c', + 'preload-index-bulk-thread.c', + 'preload-index-bulk.c', + ] else compat_sources += 'compat/stub/procinfo.c' endif diff --git a/name-hash.c b/name-hash.c index 83757db8746230..47c659d6c75374 100644 --- a/name-hash.c +++ b/name-hash.c @@ -619,6 +619,14 @@ static void lazy_init_name_hash(struct index_state *istate) trace_performance_leave("initialize name hash"); } +int prepare_index_casefolding(struct index_state *istate) +{ + if (!repo_ignore_case(istate->repo)) + return 0; + lazy_init_name_hash(istate); + return 1; +} + /* * A test routine for t/helper/ sources. * diff --git a/name-hash.h b/name-hash.h index 0cbfc4286316b2..cc7e752ab1e27d 100644 --- a/name-hash.h +++ b/name-hash.h @@ -10,6 +10,8 @@ int index_dir_find(struct index_state *istate, const char *name, int namelen, #define index_dir_exists(i, n, l) index_dir_find((i), (n), (l), NULL) +/* Prepare the name and directory hashes for concurrent case-folded lookups. */ +int prepare_index_casefolding(struct index_state *istate); void adjust_dirname_case(struct index_state *istate, char *name); struct cache_entry *index_file_exists(struct index_state *istate, const char *name, int namelen, int igncase); diff --git a/path-namespace.c b/path-namespace.c new file mode 100644 index 00000000000000..3ff199ada72917 --- /dev/null +++ b/path-namespace.c @@ -0,0 +1,268 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "hash.h" +#include "hash-framing.h" +#include "path-namespace.h" +#include "strbuf.h" + +enum namespace_entry_state { + NAMESPACE_ENTRY_MISSING = 0, + NAMESPACE_ENTRY_PRESENT = 1, +}; + +struct stat_fingerprint { + struct path_stat_identity identity; + unsigned int state; +}; + +struct path_namespace_snapshot { + struct stat_fingerprint *entries; + size_t nr; + size_t alloc; +}; + +void path_stat_identity_init(struct path_stat_identity *identity, + const struct stat *st) +{ + memset(identity, 0, sizeof(*identity)); + identity->fields[0] = st->st_dev; + identity->fields[1] = st->st_ino; + identity->fields[2] = st->st_mode; + identity->fields[3] = st->st_nlink; + identity->fields[4] = st->st_uid; + identity->fields[5] = st->st_gid; + identity->fields[6] = st->st_size; + identity->fields[7] = st->st_mtime; +#ifdef __APPLE__ + identity->fields[8] = st->st_mtimespec.tv_nsec; +#else + identity->fields[8] = ST_MTIME_NSEC(*st); +#endif + identity->fields[9] = st->st_ctime; +#ifdef __APPLE__ + identity->fields[10] = st->st_ctimespec.tv_nsec; +#else + identity->fields[10] = ST_CTIME_NSEC(*st); +#endif +#ifdef __APPLE__ + identity->fields[11] = st->st_birthtimespec.tv_sec; + identity->fields[12] = st->st_birthtimespec.tv_nsec; + identity->fields[13] = st->st_gen; +#endif +} + +int path_stat_identity_equal(const struct path_stat_identity *a, + const struct path_stat_identity *b) +{ + return !memcmp(a, b, sizeof(*a)); +} + +static void stat_fingerprint_init(struct stat_fingerprint *fingerprint, + const struct stat *st) +{ + memset(fingerprint, 0, sizeof(*fingerprint)); + fingerprint->state = NAMESPACE_ENTRY_PRESENT; + path_stat_identity_init(&fingerprint->identity, st); + if (S_ISDIR(st->st_mode)) { + /* Unrelated children do not change which object a path names. */ + fingerprint->identity.fields[3] = 0; + fingerprint->identity.fields[6] = 0; + for (size_t i = 7; i <= 10; i++) + fingerprint->identity.fields[i] = 0; + } +} + +static int stat_fingerprint_equal(const struct stat_fingerprint *a, + const struct stat_fingerprint *b) +{ + return a->state == b->state && + path_stat_identity_equal(&a->identity, &b->identity); +} + +static int capture_entry(const char *path, + struct path_namespace_snapshot *snapshot) +{ + struct stat st; + struct stat_fingerprint *entry; + + ALLOC_GROW(snapshot->entries, snapshot->nr + 1, snapshot->alloc); + entry = &snapshot->entries[snapshot->nr++]; + memset(entry, 0, sizeof(*entry)); + if (!lstat(path, &st)) { + stat_fingerprint_init(entry, &st); + return 0; + } + if (errno == ENOENT || errno == ENOTDIR) { + entry->state = NAMESPACE_ENTRY_MISSING; + return 0; + } + return -1; +} + +int path_namespace_capture(const char *path, + struct path_namespace_snapshot **snapshot_out) +{ + struct path_namespace_snapshot *snapshot; + struct strbuf prefix = STRBUF_INIT; + size_t root_len, pos; + int ret = -1; + + if (!fstat_is_reliable()) { + errno = EAGAIN; + return -1; + } + + CALLOC_ARRAY(snapshot, 1); + root_len = offset_1st_component(path); + if (!root_len) + goto done; + strbuf_add(&prefix, path, root_len); + if (capture_entry(prefix.buf, snapshot)) + goto done; + pos = root_len; + while (path[pos]) { + size_t start, end; + + while (path[pos] && is_dir_sep(path[pos])) + pos++; + if (!path[pos]) + break; + start = pos; + while (path[pos] && !is_dir_sep(path[pos])) + pos++; + end = pos; + strbuf_complete(&prefix, '/'); + strbuf_add(&prefix, path + start, end - start); + if (capture_entry(prefix.buf, snapshot)) + goto done; + } + *snapshot_out = snapshot; + snapshot = NULL; + ret = 0; +done: + path_namespace_clear(snapshot); + strbuf_release(&prefix); + return ret; +} + +int path_namespace_equal(const struct path_namespace_snapshot *a, + const struct path_namespace_snapshot *b) +{ + if (a->nr != b->nr) + return 0; + for (size_t i = 0; i < a->nr; i++) + if (!stat_fingerprint_equal(&a->entries[i], &b->entries[i])) + return 0; + return 1; +} + +int path_namespace_target_present( + const struct path_namespace_snapshot *snapshot) +{ + return snapshot->nr && + snapshot->entries[snapshot->nr - 1].state == + NAMESPACE_ENTRY_PRESENT; +} + +void path_namespace_hash(struct git_hash_ctx *ctx, + const struct path_namespace_snapshot *snapshot) +{ + uint32_t value; + uint64_t field; + + put_be32(&value, snapshot->nr); + hash_length_delimited(ctx, &value, sizeof(value)); + for (size_t i = 0; i < snapshot->nr; i++) { + put_be32(&value, snapshot->entries[i].state); + hash_length_delimited(ctx, &value, sizeof(value)); + for (size_t j = 0; + j < ARRAY_SIZE(snapshot->entries[i].identity.fields); j++) { + put_be64(&field, + snapshot->entries[i].identity.fields[j]); + hash_length_delimited(ctx, &field, sizeof(field)); + } + } +} + +void path_namespace_hash_stat(struct git_hash_ctx *ctx, const struct stat *st) +{ + struct path_stat_identity identity; + uint64_t field; + + path_stat_identity_init(&identity, st); + for (size_t i = 0; i < ARRAY_SIZE(identity.fields); i++) { + put_be64(&field, identity.fields[i]); + hash_length_delimited(ctx, &field, sizeof(field)); + } +} + +int path_namespace_stat_equal(const struct stat *a, const struct stat *b) +{ + struct path_stat_identity first, second; + + path_stat_identity_init(&first, a); + path_stat_identity_init(&second, b); + return path_stat_identity_equal(&first, &second); +} + +int path_namespace_directory_stat_equal(const struct stat *a, + const struct stat *b) +{ + struct stat_fingerprint first, second; + + if (!S_ISDIR(a->st_mode) || !S_ISDIR(b->st_mode)) + return 0; + stat_fingerprint_init(&first, a); + stat_fingerprint_init(&second, b); + return stat_fingerprint_equal(&first, &second); +} + +int path_namespace_reopen_component( + int parent_fd, const char *component, int flags, + path_namespace_open_fn open_fn, const struct stat *expected) +{ + struct stat reopened; + int fd, saved_errno; + + if (!open_fn || !component || !*component || + !strcmp(component, ".") || !strcmp(component, "..")) { + errno = EINVAL; + return -1; + } + for (const char *p = component; *p; p++) { + if (is_dir_sep(*p)) { + errno = EINVAL; + return -1; + } + } + if (!fstat_is_reliable()) { + errno = EAGAIN; + return -1; + } + + fd = open_fn(parent_fd, component, flags); + if (fd < 0) + return -1; + if (fstat(fd, &reopened)) { + saved_errno = errno; + goto error; + } + if (!path_namespace_stat_equal(expected, &reopened)) { + saved_errno = EAGAIN; + goto error; + } + return close(fd); + +error: + close(fd); + errno = saved_errno; + return -1; +} + +void path_namespace_clear(struct path_namespace_snapshot *snapshot) +{ + if (!snapshot) + return; + free(snapshot->entries); + free(snapshot); +} diff --git a/path-namespace.h b/path-namespace.h new file mode 100644 index 00000000000000..23a155e3ad999c --- /dev/null +++ b/path-namespace.h @@ -0,0 +1,38 @@ +#ifndef PATH_NAMESPACE_H +#define PATH_NAMESPACE_H + +struct git_hash_ctx; +struct path_namespace_snapshot; +struct stat; + +typedef int (*path_namespace_open_fn)(int dirfd, const char *path, int flags); + +#define PATH_STAT_IDENTITY_FIELDS 14 + +struct path_stat_identity { + uint64_t fields[PATH_STAT_IDENTITY_FIELDS]; +}; + +void path_stat_identity_init(struct path_stat_identity *identity, + const struct stat *st); +int path_stat_identity_equal(const struct path_stat_identity *a, + const struct path_stat_identity *b); +int path_namespace_capture(const char *path, + struct path_namespace_snapshot **snapshot_out); +int path_namespace_equal(const struct path_namespace_snapshot *a, + const struct path_namespace_snapshot *b); +int path_namespace_target_present( + const struct path_namespace_snapshot *snapshot); +void path_namespace_hash(struct git_hash_ctx *ctx, + const struct path_namespace_snapshot *snapshot); +void path_namespace_hash_stat(struct git_hash_ctx *ctx, + const struct stat *st); +int path_namespace_stat_equal(const struct stat *a, const struct stat *b); +int path_namespace_directory_stat_equal(const struct stat *a, + const struct stat *b); +int path_namespace_reopen_component( + int parent_fd, const char *component, int flags, + path_namespace_open_fn open_fn, const struct stat *expected); +void path_namespace_clear(struct path_namespace_snapshot *snapshot); + +#endif /* PATH_NAMESPACE_H */ diff --git a/preload-index-bulk-index.c b/preload-index-bulk-index.c new file mode 100644 index 00000000000000..3ff1b4879204a4 --- /dev/null +++ b/preload-index-bulk-index.c @@ -0,0 +1,260 @@ +#include "git-compat-util.h" +#include "clean-status.h" +#include "fsmonitor.h" +#include "name-hash.h" +#include "object.h" +#include "preload-index-bulk.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify.h" +#include "semantic-verify-internal.h" + +#ifndef __has_builtin +#define __has_builtin(x) 0 +#endif + +int preload_bulk_index_position(struct preload_bulk_scan *scan, + const char *path, size_t path_len) +{ + if (path_len > INT_MAX) + return -1; + return index_name_pos_sparse(scan->istate, path, path_len); +} + +static int first_tracked_descendant(struct index_state *istate, + const char *path, size_t path_len, + int pos) +{ + while ((unsigned int)pos < istate->cache_nr) { + const struct cache_entry *ce = istate->cache[pos]; + + if (ce_namelen(ce) <= path_len || + memcmp(ce->name, path, path_len)) + return -1; + if (ce->name[path_len] == '/') + return pos; + if ((unsigned char)ce->name[path_len] > '/') + return -1; + pos++; + } + return -1; +} + +int preload_bulk_index_pos_has_tracked_descendants( + struct preload_bulk_scan *scan, const char *path, size_t path_len, + int pos) +{ + struct index_state *istate = scan->istate; + + if (pos >= 0) + return 0; + pos = -pos - 1; + return first_tracked_descendant(istate, path, path_len, pos) >= 0; +} + +static int record_tracked_state(struct preload_bulk_worker *worker, int pos, + unsigned char state) +{ + struct preload_bulk_scan *scan = worker->scan; + int recorded = 1; + +#if GIT_GNUC_PREREQ(4, 7) || \ + (__has_builtin(__atomic_compare_exchange_n) && \ + __has_builtin(__atomic_store_n)) + unsigned char expected = PRELOAD_BULK_TRACKED_UNSEEN; + + if (!__atomic_compare_exchange_n(&scan->tracked_state[pos], &expected, + state, 0, __ATOMIC_RELAXED, + __ATOMIC_RELAXED)) { + __atomic_store_n(&scan->tracked_state[pos], + PRELOAD_BULK_TRACKED_FALLBACK, + __ATOMIC_RELAXED); + recorded = 0; + } +#else + pthread_mutex_lock(&scan->queue.mutex); + if (scan->tracked_state[pos] != PRELOAD_BULK_TRACKED_UNSEEN) { + state = PRELOAD_BULK_TRACKED_FALLBACK; + recorded = 0; + } + scan->tracked_state[pos] = state; + pthread_mutex_unlock(&scan->queue.mutex); +#endif + return recorded; +} + +static void record_stat_update(struct preload_bulk_worker *worker, int pos, + const struct stat_data *stat_data) +{ + struct preload_bulk_stat_update *update; + + ALLOC_GROW(worker->stat_updates, worker->stat_updates_nr + 1, + worker->stat_updates_alloc); + update = &worker->stat_updates[worker->stat_updates_nr++]; + update->cache_pos = pos; + memcpy(&update->stat_data, stat_data, sizeof(update->stat_data)); +} + +static int tracked_entry_is_eligible(const struct cache_entry *ce) +{ + return !ce_stage(ce) && + !ce_intent_to_add(ce) && + !ce_skip_worktree(ce) && + !(ce->ce_flags & (CE_VALID | CE_REMOVE)) && + (S_ISREG(ce->ce_mode) || S_ISLNK(ce->ce_mode)); +} + +/* + * Match ie_modified(): a nonzero cached size mismatch is a conclusive + * content change. Zero sizes and the historical Windows symlink sentinel + * still require an ordinary content check. Recompute the stat-data match + * because CE_MATCH_RACY_IS_DIRTY may make ie_match_stat() report a data + * change without a size mismatch. + */ +static int size_change_is_definitive(const struct cache_entry *ce, + const struct stat *st, + unsigned int changed) +{ + if (changed & (MODE_CHANGED | TYPE_CHANGED)) + return 0; +#ifdef GIT_WINDOWS_NATIVE + if (S_ISLNK(st->st_mode) && ce->ce_stat_data.sd_size == MAX_PATH) + return 0; +#endif + return ce->ce_stat_data.sd_size && + (match_stat_data(&ce->ce_stat_data, (struct stat *)st) & + DATA_CHANGED); +} + +static unsigned char verify_content_at( + struct preload_bulk_worker *worker, int pos, int parent_fd, + const char *basename, const struct stat *st, + int observed_has_platform_identity, struct stat_data *stat_data, + int *has_stat_update) +{ + struct preload_bulk_scan *scan = worker->scan; + struct cache_entry *ce = scan->istate->cache[pos]; + struct semantic_verify_file_result file; + + *has_stat_update = 0; + if (!scan->verify_content || + !semantic_verify_classify_entry( + scan->istate, ce, worker->attr_check, 0, &file)) + return PRELOAD_BULK_TRACKED_CONTENT_CHECK; + semantic_verify_file_at( + parent_fd, basename, st, observed_has_platform_identity, + scan->root_dev, ce, scan->istate->repo, + worker->hash_buffer, &file); + worker->bytes_hashed += file.bytes_hashed; + if (file.kind == SEMANTIC_VERIFY_RAW_MODIFIED) + return PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED; + if (file.kind != SEMANTIC_VERIFY_RAW_CLEAN || !file.persistable) + return PRELOAD_BULK_TRACKED_CONTENT_CHECK; + if (memcmp(&file.stat_data, &ce->ce_stat_data, + sizeof(file.stat_data))) { + memcpy(stat_data, &file.stat_data, sizeof(*stat_data)); + *has_stat_update = 1; + } + return PRELOAD_BULK_TRACKED_CLEAN; +} + +int preload_bulk_index_entry_is_gitlink(struct preload_bulk_scan *scan, + int pos) +{ + return pos >= 0 && S_ISGITLINK(scan->istate->cache[pos]->ce_mode); +} + +void preload_bulk_record_tracked( + struct preload_bulk_worker *worker, int pos, int parent_fd, + const char *basename, const struct stat *st, + int observed_has_platform_identity) +{ + struct preload_bulk_scan *scan = worker->scan; + struct cache_entry *ce = scan->istate->cache[pos]; + struct stat_data stat_data; + unsigned int changed; + unsigned char state; + int has_stat_update = 0; + + if (!tracked_entry_is_eligible(ce)) + return; + if (clean_status_fsmonitor_semantic_baseline_pending(scan->istate) && + !fsmonitor_stat_can_be_valid(st)) { + if (record_tracked_state(worker, pos, + PRELOAD_BULK_TRACKED_CONTENT_CHECK)) + fsmonitor_invalidate_cache_entry(ce); + return; + } + changed = ie_match_stat( + scan->istate, ce, (struct stat *)st, + CE_MATCH_RACY_IS_DIRTY | CE_MATCH_IGNORE_FSMONITOR); + if (!changed) + state = PRELOAD_BULK_TRACKED_CLEAN; + else if (size_change_is_definitive(ce, st, changed)) + state = PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED; + else + state = verify_content_at( + worker, pos, parent_fd, basename, st, + observed_has_platform_identity, &stat_data, + &has_stat_update); + if (record_tracked_state(worker, pos, state) && has_stat_update) + record_stat_update(worker, pos, &stat_data); +} + +void preload_bulk_record_tracked_fallback( + struct preload_bulk_worker *worker, int pos) +{ + if (!tracked_entry_is_eligible(worker->scan->istate->cache[pos])) + return; + record_tracked_state(worker, pos, + PRELOAD_BULK_TRACKED_FALLBACK); +} + +void preload_bulk_record_tracked_descendants_fallback( + struct preload_bulk_worker *worker, const char *path, + size_t path_len) +{ + struct index_state *istate = worker->scan->istate; + int pos = preload_bulk_index_position(worker->scan, path, path_len); + + if (pos < 0) + pos = -pos - 1; + else + pos++; + while ((pos = first_tracked_descendant(istate, path, path_len, pos)) >= 0) { + preload_bulk_record_tracked_fallback(worker, pos); + pos++; + } +} + +int preload_bulk_record_tracked_alias_fallback( + struct preload_bulk_worker *worker, const char *path, + size_t path_len) +{ + struct preload_bulk_scan *scan = worker->scan; + struct index_state *istate = scan->istate; + struct cache_entry *ce; + struct strbuf canonical = STRBUF_INIT; + int found = 0; + int pos; + + if (!scan->case_insensitive || path_len > INT_MAX) + return 0; + if (index_dir_find(istate, path, path_len, &canonical)) { + found = 1; + preload_bulk_record_tracked_descendants_fallback( + worker, canonical.buf, canonical.len); + goto out; + } + ce = index_file_exists(istate, path, path_len, 1); + if (!ce) + goto out; + found = 1; + pos = index_name_pos_sparse(istate, ce->name, ce_namelen(ce)); + if (pos >= 0) + preload_bulk_record_tracked_fallback(worker, pos); + +out: + strbuf_release(&canonical); + return found; +} diff --git a/preload-index-bulk-thread.c b/preload-index-bulk-thread.c new file mode 100644 index 00000000000000..6cfc4f8409c45a --- /dev/null +++ b/preload-index-bulk-thread.c @@ -0,0 +1,333 @@ +#include "git-compat-util.h" + +#include + +#include "attr.h" +#include "clean-status.h" +#include "convert.h" +#include "preload-index-bulk.h" +#include "read-cache-ll.h" +#include "semantic-verify-internal.h" +#include "trace2.h" + +#define PRELOAD_INDEX_BULK_OPEN_FD_CAP 128 +#define PRELOAD_INDEX_BULK_OPEN_FD_RESERVE 16 + +static void queue_set_failed(struct preload_bulk_queue *queue) +{ + pthread_mutex_lock(&queue->mutex); + queue->failed = 1; + pthread_mutex_unlock(&queue->mutex); +} + +static void enqueue_task(struct preload_bulk_scan *scan, + struct preload_bulk_task *task) +{ + struct preload_bulk_queue *queue = &scan->queue; + + pthread_mutex_lock(&queue->mutex); + task->next = queue->head; + queue->head = task; + queue->pending++; + pthread_cond_signal(&queue->cond); + pthread_mutex_unlock(&queue->mutex); +} + +static int reserve_open_fd(struct preload_bulk_queue *queue) +{ + int reserved = 0; + + pthread_mutex_lock(&queue->mutex); + if (queue->open_fds < queue->open_fd_limit) { + queue->open_fds++; + reserved = 1; + } + pthread_mutex_unlock(&queue->mutex); + return reserved; +} + +static void release_open_fd(struct preload_bulk_queue *queue) +{ + pthread_mutex_lock(&queue->mutex); + if (!queue->open_fds) + BUG("bulk preload open-fd count underflow"); + queue->open_fds--; + pthread_mutex_unlock(&queue->mutex); +} + +void preload_bulk_schedule_directory( + struct preload_bulk_worker *worker, int parent_fd, + const struct preload_bulk_dir_identity *parent_identity, + const struct preload_bulk_dir_identity *child_identity, + struct preload_bulk_untracked_root *untracked_root, + const char *name, const char *path, size_t path_len) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_task *task; + + FLEX_ALLOC_MEM(task, path, path, path_len); + if (parent_identity) { + task->parent_identity = *parent_identity; + task->has_parent_identity = 1; + } + if (child_identity) { + task->child_identity = *child_identity; + task->has_child_identity = 1; + } + task->untracked_root = untracked_root; + task->fd = -1; + if (reserve_open_fd(&scan->queue)) { + task->reserved_fd = 1; + task->fd = scan->backend->open_dir_at(worker, parent_fd, name); + if (task->fd < 0) { + int saved_errno = errno; + + task->reserved_fd = 0; + release_open_fd(&scan->queue); + if (saved_errno == EXDEV) { + preload_bulk_record_tracked_descendants_fallback( + worker, path, path_len); + if (scan->collect_untracked) + preload_bulk_invalidate_untracked(worker); + free(task); + return; + } + if (saved_errno != EMFILE && saved_errno != ENFILE) { + free(task); + queue_set_failed(&scan->queue); + return; + } + } + } + enqueue_task(scan, task); +} + +static size_t preload_bulk_open_fd_limit(void) +{ + struct rlimit limit; + rlim_t value; + + if (getrlimit(RLIMIT_NOFILE, &limit)) + return 1; + if (limit.rlim_cur == RLIM_INFINITY) + return PRELOAD_INDEX_BULK_OPEN_FD_CAP; + if (limit.rlim_cur <= PRELOAD_INDEX_BULK_OPEN_FD_RESERVE) + return 1; + value = limit.rlim_cur - PRELOAD_INDEX_BULK_OPEN_FD_RESERVE; + if (value > PRELOAD_INDEX_BULK_OPEN_FD_CAP) + value = PRELOAD_INDEX_BULK_OPEN_FD_CAP; + return value; +} + +static int queue_init(struct preload_bulk_queue *queue) +{ + memset(queue, 0, sizeof(*queue)); +#if HAVE_THREADS + if (pthread_mutex_init(&queue->mutex, NULL)) + return -1; + if (pthread_cond_init(&queue->cond, NULL)) { + pthread_mutex_destroy(&queue->mutex); + return -1; + } +#endif + queue->open_fd_limit = preload_bulk_open_fd_limit(); + return 0; +} + +static void queue_release(struct preload_bulk_queue *queue) +{ + if (queue->head || queue->pending || queue->open_fds) + BUG("releasing non-empty bulk preload queue"); +#if HAVE_THREADS + pthread_cond_destroy(&queue->cond); + pthread_mutex_destroy(&queue->mutex); +#endif + memset(queue, 0, sizeof(*queue)); +} + +static void *preload_bulk_worker_main(void *data) +{ + struct preload_bulk_worker *worker = data; + struct preload_bulk_queue *queue = &worker->scan->queue; + + for (;;) { + struct preload_bulk_task *task; + int failed, reserved_fd; + + pthread_mutex_lock(&queue->mutex); + while (!queue->head && queue->pending) + pthread_cond_wait(&queue->cond, &queue->mutex); + if (!queue->pending) { + pthread_mutex_unlock(&queue->mutex); + break; + } + task = queue->head; + queue->head = task->next; + pthread_mutex_unlock(&queue->mutex); + + failed = + worker->scan->backend->scan_directory(worker, task); + reserved_fd = task->reserved_fd; + free(task); + + pthread_mutex_lock(&queue->mutex); + if (failed) + queue->failed = 1; + if (reserved_fd) { + if (!queue->open_fds) + BUG("bulk preload open-fd count underflow"); + queue->open_fds--; + } + if (!queue->pending) + BUG("bulk preload task count underflow"); + queue->pending--; + if (!queue->pending) + pthread_cond_broadcast(&queue->cond); + pthread_mutex_unlock(&queue->mutex); + } + return NULL; +} + +static void release_workers(struct preload_bulk_scan *scan) +{ + for (int i = 0; i < scan->threads; i++) { + attr_check_free(scan->workers[i].attr_check); + free(scan->workers[i].buffer); + free(scan->workers[i].hash_buffer); + free(scan->workers[i].stat_updates); + strbuf_release(&scan->workers[i].path); + } + FREE_AND_NULL(scan->workers); +} + +static void prepare_content_verification(struct preload_bulk_scan *scan) +{ + if (!scan->proof_epoch) + return; + + convert_attrs_prepare(scan->istate); + for (int i = 0; i < scan->threads; i++) { + scan->workers[i].attr_check = convert_attrs_check_alloc(); + git_check_attr( + scan->istate, "", scan->workers[i].attr_check); + } + if (!clean_status_proof_epoch_prime_matches( + scan->istate, scan->proof_epoch)) { + for (int i = 0; i < scan->threads; i++) { + attr_check_free(scan->workers[i].attr_check); + scan->workers[i].attr_check = NULL; + } + git_attr_invalidate_all(); + return; + } + for (int i = 0; i < scan->threads; i++) + scan->workers[i].hash_buffer = + xmalloc(SEMANTIC_VERIFY_HASH_BUFFER_SIZE); + scan->verify_content = 1; + trace2_data_intmax("index", scan->repo, + "preload/bulk_content_verify", 1); +} + +static void collect_stat_updates(struct preload_bulk_scan *scan) +{ + size_t nr = 0; + + for (int i = 0; i < scan->threads; i++) { + struct preload_bulk_worker *worker = &scan->workers[i]; + + for (size_t j = 0; j < worker->stat_updates_nr; j++) { + struct preload_bulk_stat_update *update = + &worker->stat_updates[j]; + + if (update->cache_pos >= scan->istate->cache_nr) + BUG("bulk stat update position out of range"); + if (scan->tracked_state[update->cache_pos] == + PRELOAD_BULK_TRACKED_CLEAN) + nr++; + } + } + ALLOC_ARRAY(scan->stat_updates, nr); + for (int i = 0; i < scan->threads; i++) { + struct preload_bulk_worker *worker = &scan->workers[i]; + + for (size_t j = 0; j < worker->stat_updates_nr; j++) { + struct preload_bulk_stat_update *update = + &worker->stat_updates[j]; + + if (scan->tracked_state[update->cache_pos] != + PRELOAD_BULK_TRACKED_CLEAN) + continue; + scan->stat_updates[scan->stat_updates_nr++] = *update; + } + } +} + +int preload_bulk_run_scan(struct preload_bulk_scan *scan, + struct preload_bulk_run_result *result) +{ + struct preload_bulk_task *root_task; + int failed, started_threads = 1; + + if (scan->threads < 1) + BUG("bulk preload scan requires at least one worker"); + memset(result, 0, sizeof(*result)); + if (queue_init(&scan->queue)) + return -1; + CALLOC_ARRAY(scan->workers, scan->threads); + for (int i = 0; i < scan->threads; i++) { + scan->workers[i].scan = scan; + strbuf_init(&scan->workers[i].path, 0); + } + prepare_content_verification(scan); + + FLEX_ALLOC_STR(root_task, path, "."); + if (!reserve_open_fd(&scan->queue)) + BUG("bulk preload queue cannot reserve its root descriptor"); + root_task->reserved_fd = 1; + root_task->fd = fcntl(scan->root_fd, F_DUPFD_CLOEXEC, 0); + if (root_task->fd < 0) { + release_open_fd(&scan->queue); + free(root_task); + release_workers(scan); + queue_release(&scan->queue); + return -1; + } + enqueue_task(scan, root_task); + + for (int i = 1; i < scan->threads; i++) { + int err = pthread_create(&scan->workers[i].thread, NULL, + preload_bulk_worker_main, + &scan->workers[i]); + + if (err) + break; + scan->workers[i].started = 1; + started_threads++; + } + preload_bulk_worker_main(&scan->workers[0]); + for (int i = 1; i < scan->threads; i++) + if (scan->workers[i].started && + pthread_join(scan->workers[i].thread, NULL)) + BUG("unable to join bulk preload worker"); + + for (int i = 0; i < scan->threads; i++) { + struct preload_bulk_worker *worker = &scan->workers[i]; + + result->dirs += worker->dirs; + result->entries += worker->entries; + result->bulk_calls += worker->bulk_calls; + result->bytes_hashed += worker->bytes_hashed; + result->changed_dirs += worker->changed_dirs; + result->malformed += worker->malformed; + } + collect_stat_updates(scan); + result->threads = started_threads; + result->untracked_complete = + scan->collect_untracked && !scan->queue.untracked_invalid; + failed = scan->queue.failed || result->malformed || + result->changed_dirs; + + release_workers(scan); + queue_release(&scan->queue); + return failed ? -1 : 0; +} diff --git a/preload-index-bulk.c b/preload-index-bulk.c new file mode 100644 index 00000000000000..6dd2d1e552ec56 --- /dev/null +++ b/preload-index-bulk.c @@ -0,0 +1,363 @@ +#include "git-compat-util.h" +#include "abspath.h" +#include "dir.h" +#include "exclude-source-proof.h" +#include "name-hash.h" +#include "parse.h" +#include "preload-index-bulk.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "trace2.h" + +struct preload_bulk_untracked_root { + struct preload_bulk_untracked_root *next; + /* + * Normal-mode status reports an untracked directory after finding + * one visible descendant. Share that decision among workers below + * the directory. + */ + unsigned visible : 1; + char path[FLEX_ARRAY]; +}; + +static int backend_available(const struct preload_bulk_backend *backend) +{ + return backend && backend->start && backend->finish && + backend->release && backend->open_dir_at && + backend->scan_directory; +} + +static int open_exclude_parent(void *data, const char *path) +{ + struct preload_bulk_scan *scan = data; + + if (is_absolute_path(path)) + return open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC); + return scan->backend->open_proof_parent(scan, path); +} + +int preload_bulk_available(void) +{ + return backend_available(preload_bulk_platform_backend()); +} + +int preload_bulk_test_barrier(struct preload_bulk_scan *scan, + const char *path) +{ + struct strbuf buf = STRBUF_INIT; + int fd; + int result; + + if (!scan->test_barrier_path || + strcmp(scan->test_barrier_path, path)) + return 0; + if (!scan->test_barrier_ready || !scan->test_barrier_resume) + return -1; + + fd = open(scan->test_barrier_resume, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -1; + write_file(scan->test_barrier_ready, "ready"); + result = strbuf_read(&buf, fd, 1) > 0 ? 0 : -1; + close(fd); + strbuf_release(&buf); + return result; +} + +int preload_bulk_path_is_excluded(struct preload_bulk_worker *worker, + const char *path, int dtype) +{ + struct preload_bulk_scan *scan = worker->scan; + int result; + + if (!scan->exclude_dir) + BUG("bulk preload has no exclude state"); + pthread_mutex_lock(&scan->exclude_mutex); + result = is_excluded(scan->exclude_dir, scan->istate, path, &dtype); + pthread_mutex_unlock(&scan->exclude_mutex); + return result; +} + +void preload_bulk_invalidate_untracked( + struct preload_bulk_worker *worker) +{ + struct preload_bulk_queue *queue = &worker->scan->queue; + + pthread_mutex_lock(&queue->mutex); + queue->untracked_invalid = 1; + pthread_mutex_unlock(&queue->mutex); +} + +int preload_bulk_untracked_is_invalid( + struct preload_bulk_worker *worker) +{ + struct preload_bulk_queue *queue = &worker->scan->queue; + int invalid; + + pthread_mutex_lock(&queue->mutex); + invalid = queue->untracked_invalid; + pthread_mutex_unlock(&queue->mutex); + return invalid; +} + +struct preload_bulk_untracked_root *preload_bulk_untracked_root_new( + struct preload_bulk_worker *worker, const char *path, + size_t path_len) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_untracked_root *root; + + FLEX_ALLOC_MEM(root, path, path, path_len + 1); + root->path[path_len] = '/'; + root->path[path_len + 1] = '\0'; + + pthread_mutex_lock(&scan->queue.mutex); + root->next = scan->untracked_roots; + scan->untracked_roots = root; + pthread_mutex_unlock(&scan->queue.mutex); + return root; +} + +int preload_bulk_untracked_root_is_visible( + struct preload_bulk_worker *worker MAYBE_UNUSED, + const struct preload_bulk_untracked_root *root) +{ + int visible; + + if (!root) + return 0; + pthread_mutex_lock(&worker->scan->queue.mutex); + visible = root->visible; + pthread_mutex_unlock(&worker->scan->queue.mutex); + return visible; +} + +void preload_bulk_record_untracked( + struct preload_bulk_worker *worker, + struct preload_bulk_untracked_root *root, + const char *path) +{ + struct preload_bulk_scan *scan = worker->scan; + struct preload_bulk_queue *queue = &scan->queue; + int record = 1; + + pthread_mutex_lock(&queue->mutex); + if (queue->untracked_invalid) + record = 0; + else if (root) { + if (root->visible) + record = 0; + else + root->visible = 1; + } + if (record) + string_list_append(&scan->untracked, + root ? root->path : path); + pthread_mutex_unlock(&queue->mutex); +} + +static int collect_untracked_paths(struct preload_bulk_scan *scan, + struct preload_bulk_result *result) +{ + /* + * Do not publish provisional output until all closing validations + * have succeeded. + */ + string_list_sort(&scan->untracked); + for (size_t i = 1; i < scan->untracked.nr; i++) + if (!strcmp(scan->untracked.items[i - 1].string, + scan->untracked.items[i].string)) + return -1; + result->untracked = scan->untracked; + scan->untracked = (struct string_list)STRING_LIST_INIT_DUP; + return 0; +} + +int preload_bulk_collect(struct index_state *istate, int threads, + struct preload_bulk_result *result) +{ + struct dir_struct exclude_dir = DIR_INIT; + struct exclude_source_proof *exclude_proof = NULL; + const struct preload_bulk_backend *backend = + preload_bulk_platform_backend(); + struct preload_bulk_scan scan = { + .repo = istate->repo, + .istate = istate, + .backend = backend, + .proof_epoch = istate->preload_bulk_proof_epoch, + .root_fd = -1, + .threads = threads, + .untracked = STRING_LIST_INIT_DUP, + }; + struct preload_bulk_run_result run_result = { 0 }; + struct object_id standard_excludes_digest; + struct stat root_stat; + const char *start_error, *finish_error = NULL; + const char *untracked_reason = NULL; + int standard_excludes_digest_valid = 0; + int scan_error = -1; + int clean; + + memset(result, 0, sizeof(*result)); + result->untracked.strdup_strings = 1; + result->outcome = "start-fallback"; + result->reason = "backend-unavailable"; + if (!backend_available(backend)) + return -1; + if (istate->sparse_index == INDEX_EXPANDED) { + /* + * Workers may need case-folding lookups for names returned by + * the filesystem. Build the lazy hash before they start. + * + * A collapsed sparse index cannot expand itself concurrently + * from the worker threads. Leave its unseen entries to the + * existing preload path, which expands them on the main thread. + */ + scan.case_insensitive = prepare_index_casefolding(istate); + scan.can_skip_unseen_preload = 1; + } + scan.collect_untracked = + !!istate->preload_untracked && + backend->collects_untracked && + backend->open_proof_parent; + if (istate->preload_untracked && !scan.collect_untracked) + untracked_reason = "backend-unsupported"; + if (backend->max_threads > 0 && + scan.threads > backend->max_threads) + scan.threads = backend->max_threads; + if (scan.collect_untracked) { + scan.exclude_dir = &exclude_dir; +#if HAVE_THREADS + if (pthread_mutex_init(&scan.exclude_mutex, NULL)) { + return -1; + } +#endif + } + + if (git_env_bool("GIT_TEST_PRELOAD_INDEX_BULK", 0)) { + scan.test_barrier_path = getenv( + "GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_PATH"); + scan.test_barrier_ready = getenv( + "GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_READY"); + scan.test_barrier_resume = getenv( + "GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_RESUME"); + } + + CALLOC_ARRAY(scan.tracked_state, istate->cache_nr); + start_error = backend->start(&scan); + if (!start_error && (scan.proof_epoch || scan.collect_untracked) && + (scan.root_fd < 0 || fstat(scan.root_fd, &root_stat))) + start_error = "root-stat"; + if (!start_error && scan.proof_epoch) + scan.root_dev = root_stat.st_dev; + if (!start_error) { + if (scan.collect_untracked) { + exclude_proof = exclude_source_proof_create( + istate, &scan, open_exclude_parent, 0); + exclude_dir.internal.exclude_source_proof = + exclude_proof; + setup_standard_excludes(&exclude_dir); + standard_excludes_digest_valid = + !exclude_source_proof_digest( + exclude_proof, + istate->repo->hash_algo, + &standard_excludes_digest); + } + scan_error = preload_bulk_run_scan(&scan, &run_result); + if (!scan_error) + scan_error = preload_bulk_test_barrier(&scan, ""); + finish_error = backend->finish(&scan); + if (!scan_error && !finish_error && + run_result.untracked_complete) { + int exclude_proof_valid; + + trace2_region_enter( + "index", "preload/bulk_excludes", istate->repo); + exclude_proof_valid = + exclude_source_proof_validate(exclude_proof); + if (!standard_excludes_digest_valid || + !exclude_proof_valid) { + run_result.untracked_complete = 0; + untracked_reason = "exclude-race"; + } + trace2_region_leave( + "index", "preload/bulk_excludes", istate->repo); + } + } + + clean = !start_error && !scan_error && !finish_error && + !run_result.changed_dirs && + !run_result.malformed; + if (clean && run_result.untracked_complete && + collect_untracked_paths(&scan, result)) { + run_result.untracked_complete = 0; + untracked_reason = "duplicate-path"; + } + result->run = run_result; + result->untracked_reason = untracked_reason; + if (start_error) { + result->outcome = "start-fallback"; + result->reason = start_error; + } else if (run_result.changed_dirs) { + result->outcome = "scan-fallback"; + result->reason = "filesystem-race"; + } else if (run_result.malformed) { + result->outcome = "scan-fallback"; + result->reason = "malformed-record"; + } else if (scan_error) { + result->outcome = "scan-fallback"; + result->reason = "scan-error"; + } else if (finish_error) { + result->outcome = "finish-fallback"; + result->reason = finish_error; + } else { + result->outcome = "complete"; + result->reason = NULL; + } + if (clean) { + result->tracked_state = scan.tracked_state; + result->stat_updates = scan.stat_updates; + result->stat_updates_nr = scan.stat_updates_nr; + result->nr = istate->cache_nr; + result->can_skip_unseen_preload = + scan.can_skip_unseen_preload; + result->untracked_complete = run_result.untracked_complete; + if (result->untracked_complete) { + result->standard_excludes_digest_valid = 1; + oidcpy(&result->standard_excludes_digest, + &standard_excludes_digest); + result->scanned_worktree = root_stat; + } + scan.tracked_state = NULL; + scan.stat_updates = NULL; + scan.stat_updates_nr = 0; + } + + backend->release(&scan); + while (scan.untracked_roots) { + struct preload_bulk_untracked_root *next = + scan.untracked_roots->next; + + free(scan.untracked_roots); + scan.untracked_roots = next; + } + string_list_clear(&scan.untracked, 0); + if (scan.exclude_dir) { +#if HAVE_THREADS + pthread_mutex_destroy(&scan.exclude_mutex); +#endif + dir_clear(&exclude_dir); + exclude_source_proof_release(exclude_proof); + } + free(scan.tracked_state); + free(scan.stat_updates); + return clean ? 0 : -1; +} + +void preload_bulk_result_release(struct preload_bulk_result *result) +{ + FREE_AND_NULL(result->tracked_state); + FREE_AND_NULL(result->stat_updates); + string_list_clear(&result->untracked, 0); + memset(result, 0, sizeof(*result)); +} diff --git a/preload-index-bulk.h b/preload-index-bulk.h new file mode 100644 index 00000000000000..ff5583438fd07a --- /dev/null +++ b/preload-index-bulk.h @@ -0,0 +1,202 @@ +#ifndef PRELOAD_INDEX_BULK_H +#define PRELOAD_INDEX_BULK_H + +#include "git-compat-util.h" +#include "hash.h" +#include "preload-index.h" +#include "statinfo.h" +#include "strbuf.h" +#include "string-list.h" +#include "thread-utils.h" + +struct dir_struct; +struct attr_check; +struct clean_status_proof_epoch; +struct index_state; +struct repository; +struct preload_bulk_untracked_root; + +struct preload_bulk_dir_identity { + struct stat stat; + uint64_t platform_id; + unsigned complete : 1; +}; + +struct preload_bulk_task { + struct preload_bulk_task *next; + struct preload_bulk_dir_identity parent_identity; + struct preload_bulk_dir_identity child_identity; + struct preload_bulk_untracked_root *untracked_root; + int fd; + unsigned reserved_fd : 1; + unsigned has_parent_identity : 1; + unsigned has_child_identity : 1; + char path[FLEX_ARRAY]; +}; + +struct preload_bulk_queue { + pthread_mutex_t mutex; + pthread_cond_t cond; + struct preload_bulk_task *head; + /* + * pending includes queued and in-flight tasks. open_fds counts only + * descriptor reservations held by tasks. + */ + size_t pending; + size_t open_fds; + size_t open_fd_limit; + int failed; + unsigned untracked_invalid : 1; +}; + +struct preload_bulk_scan; + +struct preload_bulk_worker { + struct preload_bulk_scan *scan; + pthread_t thread; + void *buffer; + void *hash_buffer; + struct attr_check *attr_check; + struct preload_bulk_stat_update *stat_updates; + size_t stat_updates_nr; + size_t stat_updates_alloc; + struct strbuf path; + uint64_t dirs; + uint64_t entries; + uint64_t bulk_calls; + uint64_t bytes_hashed; + uint64_t changed_dirs; + uint64_t malformed; + unsigned started : 1; +}; + +struct preload_bulk_backend { + unsigned collects_untracked : 1; + int max_threads; + const char *(*start)(struct preload_bulk_scan *scan); + const char *(*finish)(struct preload_bulk_scan *scan); + void (*release)(struct preload_bulk_scan *scan); + int (*open_proof_parent)(struct preload_bulk_scan *scan, + const char *path); + int (*open_dir_at)(struct preload_bulk_worker *worker, int parent_fd, + const char *name); + /* + * Consume task->fd when it is non-negative, and close it before + * returning. + */ + int (*scan_directory)(struct preload_bulk_worker *worker, + struct preload_bulk_task *task); +}; + +struct preload_bulk_scan { + struct repository *repo; + struct index_state *istate; + const struct preload_bulk_backend *backend; + void *platform_data; + const char *test_barrier_path; + const char *test_barrier_ready; + const char *test_barrier_resume; + struct preload_bulk_queue queue; + struct preload_bulk_worker *workers; + unsigned char *tracked_state; + struct preload_bulk_stat_update *stat_updates; + size_t stat_updates_nr; + struct clean_status_proof_epoch *proof_epoch; + struct dir_struct *exclude_dir; + pthread_mutex_t exclude_mutex; + struct preload_bulk_untracked_root *untracked_roots; + struct string_list untracked; + int root_fd; + int threads; + dev_t root_dev; + unsigned collect_untracked : 1; + unsigned verify_content : 1; + unsigned case_insensitive : 1; + unsigned can_skip_unseen_preload : 1; +}; + +struct preload_bulk_run_result { + uint64_t dirs; + uint64_t entries; + uint64_t bulk_calls; + uint64_t changed_dirs; + uint64_t malformed; + uint64_t bytes_hashed; + int threads; + unsigned untracked_complete : 1; +}; + +struct preload_bulk_result { + unsigned char *tracked_state; + struct preload_bulk_stat_update *stat_updates; + size_t stat_updates_nr; + size_t nr; + const char *outcome; + const char *reason; + const char *untracked_reason; + struct preload_bulk_run_result run; + unsigned can_skip_unseen_preload : 1; + struct string_list untracked; + unsigned untracked_complete : 1; + unsigned standard_excludes_digest_valid : 1; + struct object_id standard_excludes_digest; + struct stat scanned_worktree; +}; + +struct preload_bulk_stat_update { + uint32_t cache_pos; + struct stat_data stat_data; +}; + +void preload_bulk_schedule_directory( + struct preload_bulk_worker *worker, int parent_fd, + const struct preload_bulk_dir_identity *parent_identity, + const struct preload_bulk_dir_identity *child_identity, + struct preload_bulk_untracked_root *untracked_root, + const char *name, const char *path, size_t path_len); +int preload_bulk_index_position(struct preload_bulk_scan *scan, + const char *path, size_t path_len); +int preload_bulk_index_pos_has_tracked_descendants( + struct preload_bulk_scan *scan, const char *path, size_t path_len, + int pos); +int preload_bulk_index_entry_is_gitlink(struct preload_bulk_scan *scan, + int pos); +void preload_bulk_record_tracked( + struct preload_bulk_worker *worker, int pos, int parent_fd, + const char *basename, const struct stat *st, + int observed_has_platform_identity); +void preload_bulk_record_tracked_fallback( + struct preload_bulk_worker *worker, int pos); +void preload_bulk_record_tracked_descendants_fallback( + struct preload_bulk_worker *worker, const char *path, + size_t path_len); +int preload_bulk_record_tracked_alias_fallback( + struct preload_bulk_worker *worker, const char *path, + size_t path_len); +int preload_bulk_path_is_excluded(struct preload_bulk_worker *worker, + const char *path, int dtype); +void preload_bulk_invalidate_untracked( + struct preload_bulk_worker *worker); +int preload_bulk_untracked_is_invalid( + struct preload_bulk_worker *worker); +struct preload_bulk_untracked_root *preload_bulk_untracked_root_new( + struct preload_bulk_worker *worker, const char *path, + size_t path_len); +int preload_bulk_untracked_root_is_visible( + struct preload_bulk_worker *worker, + const struct preload_bulk_untracked_root *root); +void preload_bulk_record_untracked( + struct preload_bulk_worker *worker, + struct preload_bulk_untracked_root *root, + const char *path); +int preload_bulk_run_scan(struct preload_bulk_scan *scan, + struct preload_bulk_run_result *result); +const struct preload_bulk_backend *preload_bulk_platform_backend(void); +int preload_bulk_collect(struct index_state *istate, int threads, + struct preload_bulk_result *result); +int preload_bulk_available(void); +int preload_bulk_test_barrier(struct preload_bulk_scan *scan, + const char *path); +void preload_bulk_result_release(struct preload_bulk_result *result); + +#endif /* PRELOAD_INDEX_BULK_H */ diff --git a/preload-index.c b/preload-index.c index b222821b448526..5197ffa5e61913 100644 --- a/preload-index.c +++ b/preload-index.c @@ -7,11 +7,15 @@ #include "git-compat-util.h" #include "pathspec.h" #include "dir.h" +#include "clean-status.h" #include "environment.h" #include "fsmonitor.h" #include "gettext.h" #include "parse.h" #include "preload-index.h" +#ifdef HAVE_PRELOAD_INDEX_BULK +#include "preload-index-bulk.h" +#endif #include "progress.h" #include "read-cache.h" #include "thread-utils.h" @@ -28,6 +32,9 @@ */ #define MAX_PARALLEL (20) #define THREAD_COST (500) +#define BULK_MAX_PARALLEL (32) +#define BULK_ENTRIES_PER_THREAD (5000) +#define BULK_MIN_CANDIDATE_DIVISOR (8) struct progress_data { unsigned long n; @@ -40,10 +47,23 @@ struct thread_data { struct index_state *index; struct pathspec pathspec; struct progress_data *progress; +#ifdef HAVE_PRELOAD_INDEX_BULK + const unsigned char *bulk_state; + unsigned bulk_provider_pending : 1; +#endif int offset, nr; int t2_nr_lstat; }; +static int preload_entry_needs_stat(const struct cache_entry *ce) +{ + return !ce_stage(ce) && + !S_ISGITLINK(ce->ce_mode) && + !ce_uptodate(ce) && + !ce_skip_worktree(ce) && + !(ce->ce_flags & CE_FSMONITOR_VALID); +} + static void *preload_thread(void *_data) { int nr, last_nr; @@ -51,6 +71,9 @@ static void *preload_thread(void *_data) struct index_state *index = p->index; struct cache_entry **cep = index->cache + p->offset; struct cache_def cache = CACHE_DEF_INIT; +#ifdef HAVE_PRELOAD_INDEX_BULK + const unsigned char *bulk_state = p->bulk_state; +#endif nr = p->nr; if (nr + p->offset > index->cache_nr) @@ -60,17 +83,21 @@ static void *preload_thread(void *_data) do { struct cache_entry *ce = *cep++; struct stat st; +#ifdef HAVE_PRELOAD_INDEX_BULK + unsigned char state = bulk_state ? + *bulk_state++ : PRELOAD_BULK_TRACKED_UNSEEN; +#endif - if (ce_stage(ce)) - continue; - if (S_ISGITLINK(ce->ce_mode)) + if (!preload_entry_needs_stat(ce)) continue; - if (ce_uptodate(ce)) - continue; - if (ce_skip_worktree(ce)) - continue; - if (ce->ce_flags & CE_FSMONITOR_VALID) +#ifdef HAVE_PRELOAD_INDEX_BULK + if (state == PRELOAD_BULK_TRACKED_CONTENT_CHECK || + state == PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED || + state == PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED || + (p->bulk_provider_pending && + state == PRELOAD_BULK_TRACKED_CLEAN)) continue; +#endif if (p->progress && !(nr & 31)) { struct progress_data *pd = p->progress; @@ -87,10 +114,17 @@ static void *preload_thread(void *_data) p->t2_nr_lstat++; if (lstat(ce->name, &st)) continue; + if (clean_status_fsmonitor_semantic_baseline_pending(index) && + !fsmonitor_stat_can_be_valid(&st)) { + /* An unwatched hard-link alias can evade coarse stat identity. */ + fsmonitor_invalidate_cache_entry(ce); + continue; + } if (ie_match_stat(index, ce, &st, CE_MATCH_RACY_IS_DIRTY|CE_MATCH_IGNORE_FSMONITOR)) continue; ce_mark_uptodate(ce); - mark_fsmonitor_valid(index, ce); + if (fsmonitor_stat_can_be_valid(&st)) + mark_fsmonitor_valid(index, ce); } while (--nr > 0); if (p->progress) { struct progress_data *pd = p->progress; @@ -103,6 +137,437 @@ static void *preload_thread(void *_data) return NULL; } +#ifdef HAVE_PRELOAD_INDEX_BULK +struct preload_bulk_pending { + unsigned char *tracked_state; + struct preload_bulk_stat_update *stat_updates; + size_t stat_updates_nr; + struct object_id standard_excludes_digest; + struct stat scanned_worktree; + unsigned provider : 1; + unsigned standard_excludes_digest_valid : 1; +}; + +static int stat_data_is_zero(const struct stat_data *sd) +{ + return !sd->sd_ctime.sec && + !sd->sd_ctime.nsec && + !sd->sd_mtime.sec && + !sd->sd_mtime.nsec && + !sd->sd_dev && + !sd->sd_ino && + !sd->sd_uid && + !sd->sd_gid && + !sd->sd_size; +} + +static int preload_bulk_entry_is_useful(const struct cache_entry *ce, + int allow_zero_stat) +{ + return preload_entry_needs_stat(ce) && + !ce_intent_to_add(ce) && + !(ce->ce_flags & (CE_VALID | CE_REMOVE)) && + (S_ISREG(ce->ce_mode) || S_ISLNK(ce->ce_mode)) && + (allow_zero_stat || !stat_data_is_zero(&ce->ce_stat_data)); +} + +static size_t preload_bulk_useful_candidates(struct index_state *index, + int allow_zero_stat) +{ + size_t useful = 0; + + for (size_t i = 0; i < index->cache_nr; i++) + if (preload_bulk_entry_is_useful( + index->cache[i], allow_zero_stat)) + useful++; + return useful; +} + +static size_t preload_bulk_apply_result( + struct index_state *index, + struct preload_bulk_result *result, + int defer_all, + int *has_deferred) +{ + size_t applied = 0; + + if (result->nr != index->cache_nr) + BUG("bulk preload result does not match the index"); + + for (size_t i = 0; i < result->nr; i++) { + struct cache_entry *ce = index->cache[i]; + unsigned char state = result->tracked_state[i]; + + /* + * A complete scan which did not observe a useful entry proves + * that the entry is absent. Avoid repeating the same lookup in + * speculative preload. A status consumer may use this result + * directly; other callers retain the authoritative refresh. + */ + if (result->can_skip_unseen_preload && + state == PRELOAD_BULK_TRACKED_UNSEEN && + preload_bulk_entry_is_useful(ce, defer_all)) { + state = PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED; + result->tracked_state[i] = state; + } + if ((state == PRELOAD_BULK_TRACKED_CONTENT_CHECK || + state == PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED || + state == PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED) && + preload_entry_needs_stat(ce)) + *has_deferred = 1; + if (defer_all && state == PRELOAD_BULK_TRACKED_CLEAN && + preload_entry_needs_stat(ce)) + *has_deferred = 1; + if (state != PRELOAD_BULK_TRACKED_CLEAN) + continue; + if (!preload_bulk_entry_is_useful(ce, defer_all)) + continue; + if (defer_all) + continue; + ce_mark_uptodate(ce); + mark_fsmonitor_valid(index, ce); + applied++; + } + return applied; +} + +static int preload_bulk_threads(size_t useful) +{ + int cpus = online_cpus(); + int threads = DIV_ROUND_UP(useful, BULK_ENTRIES_PER_THREAD); + + if (threads < 1) + threads = 1; + if (cpus > 0) { + int cpu_limit = cpus > BULK_MAX_PARALLEL / 2 ? + BULK_MAX_PARALLEL : cpus * 2; + + if (threads > cpu_limit) + threads = cpu_limit; + } + if (threads > BULK_MAX_PARALLEL) + threads = BULK_MAX_PARALLEL; + return threads; +} + +static void preload_bulk_trace_result( + struct index_state *index, + const struct preload_bulk_result *result, + size_t applied) +{ + uint64_t content_check = 0, definitive_modified = 0; + uint64_t definitive_deleted = 0, fallback = 0; + + for (size_t i = 0; i < result->nr; i++) { + switch (result->tracked_state[i]) { + case PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED: + definitive_modified++; + break; + case PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED: + definitive_deleted++; + break; + case PRELOAD_BULK_TRACKED_CONTENT_CHECK: + content_check++; + break; + case PRELOAD_BULK_TRACKED_FALLBACK: + fallback++; + break; + default: + break; + } + } + trace2_data_string("index", index->repo, "preload/bulk_result", + result->outcome); + if (result->reason) + trace2_data_string("index", index->repo, + "preload/bulk_reason", result->reason); + if (result->untracked_reason) + trace2_data_string("index", index->repo, + "preload/bulk_untracked_reason", + result->untracked_reason); + trace2_data_intmax("index", index->repo, "preload/bulk_applied", + applied); + trace2_data_intmax("index", index->repo, "preload/bulk_dirs", + result->run.dirs); + trace2_data_intmax("index", index->repo, "preload/bulk_entries", + result->run.entries); + trace2_data_intmax("index", index->repo, "preload/bulk_calls", + result->run.bulk_calls); + trace2_data_intmax("index", index->repo, + "preload/bulk_bytes_hashed", + result->run.bytes_hashed); + trace2_data_intmax("index", index->repo, "preload/bulk_workers", + result->run.threads); + trace2_data_intmax("index", index->repo, + "preload/bulk_definitive_modified", + definitive_modified); + trace2_data_intmax("index", index->repo, + "preload/bulk_definitive_deleted", + definitive_deleted); + trace2_data_intmax("index", index->repo, + "preload/bulk_content_check", content_check); + trace2_data_intmax("index", index->repo, + "preload/bulk_fallback", fallback); + trace2_data_intmax("index", index->repo, + "preload/bulk_untracked_complete", + result->untracked_complete); + trace2_data_intmax("index", index->repo, + "preload/bulk_untracked_count", + result->untracked.nr); +} + +static int preload_bulk_config_enabled(struct index_state *index) +{ + int enabled = 0; + int control; + + control = git_env_bool("GIT_TEST_PRELOAD_INDEX_BULK", -1); + if (control >= 0) + return control; + if (repo_config_get_bool(index->repo, "core.preloadindexbulk", + &enabled)) + enabled = index->preload_bulk_recovery_requested; + return enabled; +} + +static void preload_bulk_try(struct index_state *index, + unsigned int refresh_flags, + struct preload_bulk_pending *pending) +{ + struct preload_bulk_result result = { 0 }; + size_t useful; + size_t applied = 0; + int provider = !!index->preload_bulk_proof_epoch; + int has_deferred = 0; + int threads; + + /* + * Let the test variable override configuration without bypassing + * any of the proof checks. + */ + if (!preload_bulk_config_enabled(index) || + !preload_bulk_available()) + return; + if (provider) { + if (!(refresh_flags & REFRESH_DEFER_BULK_DIRTY) || + fsm_settings__get_mode(index->repo) != FSMONITOR_MODE_IPC || + !fsmonitor_pending_token_from_provider(index)) + return; + } else if (fsm_settings__get_mode(index->repo) != + FSMONITOR_MODE_DISABLED) { + return; + } + useful = preload_bulk_useful_candidates(index, provider); + trace2_data_intmax("index", index->repo, "preload/bulk_useful", + useful); + trace2_data_intmax("index", index->repo, "preload/bulk_cache_nr", + index->cache_nr); + if (!useful && !index->preload_untracked) + return; + threads = preload_bulk_threads(useful); + trace2_region_enter("index", "preload/bulk", index->repo); + if (!preload_bulk_collect(index, threads, &result)) { + applied = preload_bulk_apply_result(index, &result, + provider, + &has_deferred); + } + preload_bulk_trace_result(index, &result, applied); + if (has_deferred) { + pending->tracked_state = result.tracked_state; + result.tracked_state = NULL; + pending->provider = provider; + if (provider) { + pending->stat_updates = result.stat_updates; + pending->stat_updates_nr = result.stat_updates_nr; + result.stat_updates = NULL; + result.stat_updates_nr = 0; + } + } + if (result.standard_excludes_digest_valid) { + pending->provider = provider; + pending->standard_excludes_digest_valid = 1; + oidcpy(&pending->standard_excludes_digest, + &result.standard_excludes_digest); + pending->scanned_worktree = result.scanned_worktree; + } + if (result.untracked_complete && index->preload_untracked) { + *index->preload_untracked = result.untracked; + index->preload_untracked_complete = 1; + result.untracked = + (struct string_list)STRING_LIST_INIT_DUP; + } + trace2_region_leave("index", "preload/bulk", index->repo); + preload_bulk_result_release(&result); +} + +static void preload_bulk_finish_state(struct index_state *index, + struct preload_bulk_pending *pending, + unsigned int refresh_flags) +{ + if ((refresh_flags & REFRESH_DEFER_BULK_DIRTY) && + pending->tracked_state) { + index->preload_bulk_tracked_state = pending->tracked_state; + index->preload_bulk_tracked_nr = index->cache_nr; + index->preload_bulk_stat_updates = pending->stat_updates; + index->preload_bulk_stat_updates_nr = + pending->stat_updates_nr; + index->preload_bulk_provider_pending = pending->provider; + pending->tracked_state = NULL; + pending->stat_updates = NULL; + pending->stat_updates_nr = 0; + } + if (pending->standard_excludes_digest_valid) { + oidcpy(&index->preload_bulk_standard_excludes_digest, + &pending->standard_excludes_digest); + index->preload_bulk_scanned_worktree = + pending->scanned_worktree; + if (pending->provider) + index->preload_bulk_excludes_digest_pending = 1; + else + index->preload_bulk_excludes_digest_valid = 1; + } + free(pending->tracked_state); + free(pending->stat_updates); +} + +static int compare_stat_update(const void *va, const void *vb) +{ + const struct preload_bulk_stat_update *a = va; + const struct preload_bulk_stat_update *b = vb; + + return a->cache_pos < b->cache_pos ? -1 : + a->cache_pos > b->cache_pos ? 1 : 0; +} +#endif + +void preload_index_bulk_result_consume(struct index_state *index) +{ + FREE_AND_NULL(index->preload_bulk_tracked_state); + FREE_AND_NULL(index->preload_bulk_stat_updates); + index->preload_bulk_tracked_nr = 0; + index->preload_bulk_stat_updates_nr = 0; + index->preload_bulk_provider_pending = 0; + index->preload_bulk_excludes_digest_pending = 0; +} + +void preload_index_bulk_result_clear(struct index_state *index) +{ + preload_index_bulk_result_consume(index); + index->preload_bulk_excludes_digest_valid = 0; + oidclr(&index->preload_bulk_standard_excludes_digest, + index->repo->hash_algo); +} + +int preload_index_bulk_can_close_provider(struct index_state *index) +{ +#ifdef HAVE_PRELOAD_INDEX_BULK + int core_preload_index = 1; + size_t useful; + + repo_config_get_bool(index->repo, "core.preloadindex", + &core_preload_index); + if (!core_preload_index || !preload_bulk_config_enabled(index) || + !preload_bulk_available() || + index->sparse_index != INDEX_EXPANDED || + fsm_settings__get_mode(index->repo) != FSMONITOR_MODE_IPC || + !fsmonitor_pending_token_from_provider(index)) + return 0; + useful = preload_bulk_useful_candidates(index, 1); + if (!index->preload_untracked && + useful < DIV_ROUND_UP(index->cache_nr, + BULK_MIN_CANDIDATE_DIVISOR)) { + trace2_data_intmax("index", index->repo, + "preload/bulk_sparse_skip", useful); + return 0; + } + return useful || index->preload_untracked; +#else + (void)index; + return 0; +#endif +} + +int preload_index_bulk_result_accept(struct index_state *index) +{ +#ifdef HAVE_PRELOAD_INDEX_BULK + size_t update_nr = 0; + int applied = 0; + + if (!index->preload_bulk_provider_pending && + !index->preload_bulk_excludes_digest_pending) + return 0; + if (!index->preload_bulk_provider_pending) + goto accept_digest; + if (!index->preload_bulk_tracked_state || + index->preload_bulk_tracked_nr != index->cache_nr) + return -1; + + QSORT(index->preload_bulk_stat_updates, + index->preload_bulk_stat_updates_nr, compare_stat_update); + for (size_t i = 0; i < index->preload_bulk_stat_updates_nr; i++) { + struct preload_bulk_stat_update *update = + &index->preload_bulk_stat_updates[i]; + + if (update->cache_pos >= index->cache_nr || + index->preload_bulk_tracked_state[update->cache_pos] != + PRELOAD_BULK_TRACKED_CLEAN || + (i && update[-1].cache_pos == update->cache_pos)) + return -1; + } + + for (size_t i = 0; i < index->cache_nr; i++) { + struct cache_entry *ce = index->cache[i]; + struct preload_bulk_stat_update *update = NULL; + + if (index->preload_bulk_tracked_state[i] != + PRELOAD_BULK_TRACKED_CLEAN) + continue; + if (update_nr < index->preload_bulk_stat_updates_nr && + index->preload_bulk_stat_updates[update_nr].cache_pos == i) + update = + &index->preload_bulk_stat_updates[update_nr++]; + if (update && + memcmp(&ce->ce_stat_data, &update->stat_data, + sizeof(ce->ce_stat_data))) { + memcpy(&ce->ce_stat_data, &update->stat_data, + sizeof(ce->ce_stat_data)); + ce->ce_flags |= CE_UPDATE_IN_BASE; + index->cache_changed |= CE_ENTRY_CHANGED; + } + ce_mark_uptodate(ce); + mark_fsmonitor_valid(index, ce); + applied++; + } + if (update_nr != index->preload_bulk_stat_updates_nr) + BUG("validated bulk stat update was not applied"); + + FREE_AND_NULL(index->preload_bulk_stat_updates); + index->preload_bulk_stat_updates_nr = 0; + index->preload_bulk_provider_pending = 0; +accept_digest: + if (index->preload_bulk_excludes_digest_pending) { + index->preload_bulk_excludes_digest_pending = 0; + index->preload_bulk_excludes_digest_valid = 1; + } + trace2_data_intmax("index", index->repo, + "preload/bulk_provider_applied", applied); +#else + (void)index; +#endif + return 0; +} + +int preload_index_bulk_standard_excludes_digest( + const struct index_state *index, struct object_id *digest, + struct stat *scanned_worktree) +{ + if (!index->preload_bulk_excludes_digest_valid) + return -1; + oidcpy(digest, &index->preload_bulk_standard_excludes_digest); + *scanned_worktree = index->preload_bulk_scanned_worktree; + return 0; +} + void preload_index(struct index_state *index, const struct pathspec *pathspec, unsigned int refresh_flags) @@ -110,19 +575,41 @@ void preload_index(struct index_state *index, int threads, i, work, offset; struct thread_data data[MAX_PARALLEL]; struct progress_data pd; +#ifdef HAVE_PRELOAD_INDEX_BULK + struct preload_bulk_pending bulk = { 0 }; +#endif int t2_sum_lstat = 0; int core_preload_index = 1; + preload_index_bulk_result_clear(index); + index->preload_untracked_complete = 0; + if (index->preload_untracked) + string_list_clear(index->preload_untracked, 0); repo_config_get_bool(index->repo, "core.preloadindex", &core_preload_index); - if (!HAVE_THREADS || !core_preload_index) + if (!core_preload_index) + return; + +#ifdef HAVE_PRELOAD_INDEX_BULK + if (!pathspec || !pathspec->nr) + preload_bulk_try(index, refresh_flags, &bulk); +#endif + if (!HAVE_THREADS) { +#ifdef HAVE_PRELOAD_INDEX_BULK + preload_bulk_finish_state(index, &bulk, refresh_flags); +#endif return; + } threads = index->cache_nr / THREAD_COST; if ((index->cache_nr > 1) && (threads < 2) && git_env_bool("GIT_TEST_PRELOAD_INDEX", 0)) threads = 2; - if (threads < 2) + if (threads < 2) { +#ifdef HAVE_PRELOAD_INDEX_BULK + preload_bulk_finish_state(index, &bulk, refresh_flags); +#endif return; + } trace2_region_enter("index", "preload", NULL); @@ -146,6 +633,11 @@ void preload_index(struct index_state *index, int err; p->index = index; +#ifdef HAVE_PRELOAD_INDEX_BULK + p->bulk_state = bulk.tracked_state ? + bulk.tracked_state + offset : NULL; + p->bulk_provider_pending = bulk.provider; +#endif if (pathspec) copy_pathspec(&p->pathspec, pathspec); p->offset = offset; @@ -165,6 +657,9 @@ void preload_index(struct index_state *index, t2_sum_lstat += p->t2_nr_lstat; } stop_progress(&pd.progress); +#ifdef HAVE_PRELOAD_INDEX_BULK + preload_bulk_finish_state(index, &bulk, refresh_flags); +#endif if (pathspec) { /* earlier we made deep copies for each thread to work with */ diff --git a/preload-index.h b/preload-index.h index 251b1ed88e9820..87e6d0b89276a7 100644 --- a/preload-index.h +++ b/preload-index.h @@ -2,8 +2,19 @@ #define PRELOAD_INDEX_H struct index_state; +struct object_id; struct pathspec; struct repository; +struct stat; + +enum preload_bulk_tracked_state { + PRELOAD_BULK_TRACKED_UNSEEN = 0, + PRELOAD_BULK_TRACKED_CLEAN, + PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED, + PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED, + PRELOAD_BULK_TRACKED_CONTENT_CHECK, + PRELOAD_BULK_TRACKED_FALLBACK, +}; void preload_index(struct index_state *index, const struct pathspec *pathspec, @@ -11,5 +22,12 @@ void preload_index(struct index_state *index, int repo_read_index_preload(struct repository *, const struct pathspec *pathspec, unsigned refresh_flags); +void preload_index_bulk_result_clear(struct index_state *index); +void preload_index_bulk_result_consume(struct index_state *index); +int preload_index_bulk_can_close_provider(struct index_state *index); +int preload_index_bulk_result_accept(struct index_state *index); +int preload_index_bulk_standard_excludes_digest( + const struct index_state *index, struct object_id *digest, + struct stat *scanned_worktree); #endif /* PRELOAD_INDEX_H */ diff --git a/read-cache-ll.h b/read-cache-ll.h index 71b87615ebc6d3..16df8c24801d87 100644 --- a/read-cache-ll.h +++ b/read-cache-ll.h @@ -31,6 +31,11 @@ struct cache_entry { char name[FLEX_ARRAY]; /* more */ }; +struct clean_status_index_write_receipt; +struct clean_status_commit_checkpoint; +struct clean_status_proof_epoch; +struct preload_bulk_stat_update; + #define CE_STAGEMASK (0x3000) #define CE_EXTENDED (0x4000) #define CE_VALID (0x8000) @@ -69,14 +74,18 @@ struct cache_entry { */ #define CE_INTENT_TO_ADD (1 << 29) #define CE_SKIP_WORKTREE (1 << 30) -/* CE_EXTENDED2 is for future extension */ -#define CE_EXTENDED2 (1U << 31) +/* + * In-memory only. The cached stat data cannot be trusted, and callers which + * normally trust stat differences must verify content. This occupies the + * former never-persisted extension slot. + */ +#define CE_CONTENT_CHECK_REQUIRED (1U << 31) #define CE_EXTENDED_FLAGS (CE_INTENT_TO_ADD | CE_SKIP_WORKTREE) /* * Safeguard to avoid saving wrong flags: - * - CE_EXTENDED2 won't get saved until its semantic is known + * - CE_CONTENT_CHECK_REQUIRED is transient and must not be saved * - Bits in 0x0000FFFF have been saved in ce_flags already * - Bits in 0x003F0000 are currently in-memory flags */ @@ -120,7 +129,9 @@ static inline unsigned create_ce_flags(unsigned stage) #define ce_stage(ce) ((CE_STAGEMASK & (ce)->ce_flags) >> CE_STAGESHIFT) #define ce_uptodate(ce) ((ce)->ce_flags & CE_UPTODATE) #define ce_skip_worktree(ce) ((ce)->ce_flags & CE_SKIP_WORKTREE) -#define ce_mark_uptodate(ce) ((ce)->ce_flags |= CE_UPTODATE) +#define ce_mark_uptodate(ce) \ + ((ce)->ce_flags = ((ce)->ce_flags | CE_UPTODATE) & \ + ~CE_CONTENT_CHECK_REQUIRED) #define ce_intent_to_add(ce) ((ce)->ce_flags & CE_INTENT_TO_ADD) #define cache_entry_size(len) (offsetof(struct cache_entry,name) + (len) + 1) @@ -136,7 +147,9 @@ static inline unsigned create_ce_flags(unsigned stage) #define FSMONITOR_CHANGED (1 << 8) struct split_index; +struct clean_status_state; struct untracked_cache; +struct string_list; struct progress; struct pattern_list; @@ -176,18 +189,47 @@ struct index_state { drop_cache_tree : 1, updated_workdir : 1, updated_skipworktree : 1, - fsmonitor_has_run_once : 1; + fsmonitor_has_run_once : 1, + fsmonitor_token_valid : 1, + fsmonitor_extension_seen : 1, + fsmonitor_untracked_valid : 1, + fsmonitor_untracked_must_persist : 1, + fsmonitor_untracked_extension_seen : 1, + fsmonitor_untracked_extension_invalid : 1, + fsmonitor_untracked_revalidation_authenticated : 1, + fsmonitor_legacy_untracked_adopted : 1, + fsmonitor_legacy_untracked_fallback : 1, + fsmonitor_pending_token_from_provider : 1, + preload_untracked_complete : 1, + /* Read-only status request; never serialized. */ + preload_bulk_recovery_requested : 1, + preload_bulk_provider_pending : 1, + preload_bulk_excludes_digest_pending : 1, + preload_bulk_excludes_digest_valid : 1; enum sparse_index_mode sparse_index; struct hashmap name_hash; struct hashmap dir_hash; struct object_id oid; struct untracked_cache *untracked; + unsigned char *preload_bulk_tracked_state; + size_t preload_bulk_tracked_nr; + struct preload_bulk_stat_update *preload_bulk_stat_updates; + size_t preload_bulk_stat_updates_nr; + struct object_id preload_bulk_standard_excludes_digest; + struct stat preload_bulk_scanned_worktree; + /* Borrowed only while refresh_index() performs a provider scan. */ + struct clean_status_proof_epoch *preload_bulk_proof_epoch; + /* Borrowed for the duration of preload_index(). */ + struct string_list *preload_untracked; char *fsmonitor_last_update; + char *fsmonitor_last_update_pending; + char *fsmonitor_untracked_token; struct ewah_bitmap *fsmonitor_dirty; struct mem_pool *ce_mem_pool; struct progress *progress; struct repository *repo; struct pattern_list *sparse_checkout_patterns; + struct clean_status_state *clean_status; }; /** @@ -277,6 +319,20 @@ void prefetch_cache_entries(const struct index_state *istate, struct lock_file; int do_read_index(struct index_state *istate, const char *path, int must_exist); /* for testting only! */ +/* Takes ownership of fd, including when the state is already initialized. */ +int do_read_index_from_fd(struct index_state *istate, int fd, + const char *path); +/* + * Read only the entries of a full index into a fresh index_state. Optional + * extensions are ignored; split/sparse indexes, resolve-undo, malformed data, + * and unknown mandatory extensions are rejected. Nonzero checksums are always + * verified. A zero skipHash trailer requires separate authentication by the + * caller before using these entries as a proof. + * + * The caller owns fd. Its offset is unchanged, and failure leaves istate + * unchanged. Return 0 on success or -1 for a missing/unsupported witness. + */ +int read_index_entries_from_fd(struct index_state *istate, int fd); int read_index_from(struct index_state *, const char *path, const char *gitdir); int is_index_unborn(struct index_state *); @@ -306,6 +362,23 @@ int is_index_unborn(struct index_state *); */ int write_locked_index(struct index_state *, struct lock_file *lock, unsigned flags); +/* Commit's close-only main-index write and optional historical-only repair. */ +int write_locked_index_for_commit( + struct index_state *, struct lock_file *, + struct clean_status_commit_checkpoint **); +void restore_locked_index_for_commit( + struct index_state *, struct lock_file *, + const struct clean_status_commit_checkpoint *); + +/* + * Like repo_update_index_if_able(), with an optional receipt for the canonical + * file actually written. The receipt must be initialized by the caller and + * remains empty if the write is skipped, fails, or is not eligible. + */ +void repo_update_index_if_able_with_receipt( + struct repository *repo, struct lock_file *lock, + struct clean_status_index_write_receipt *receipt); + void discard_index(struct index_state *); void move_index_extensions(struct index_state *dst, struct index_state *src); int unmerged_index(const struct index_state *); @@ -383,6 +456,7 @@ static inline int index_pos_to_insert_pos(uintmax_t pos) #define ADD_CACHE_NEW_ONLY 16 /* Do not replace existing ones */ #define ADD_CACHE_KEEP_CACHE_TREE 32 /* Do not invalidate cache-tree */ #define ADD_CACHE_RENORMALIZE 64 /* Pass along HASH_RENORMALIZE */ +#define ADD_CACHE_PRESERVE_CLEAN_HISTORY 128 /* Preserve safe replacements */ int add_index_entry(struct index_state *, struct cache_entry *ce, int option); void rename_index_entry_at(struct index_state *, int pos, const char *new_name); @@ -396,6 +470,7 @@ int remove_file_from_index(struct index_state *, const char *path); #define ADD_CACHE_IGNORE_ERRORS 4 #define ADD_CACHE_IGNORE_REMOVAL 8 #define ADD_CACHE_INTENT 16 +#define ADD_CACHE_TRACK_CLEAN_HISTORY 32 /* * These two are used to add the contents of the file at path * to the index, marking the working tree up-to-date by storing @@ -431,6 +506,14 @@ int is_racy_timestamp(const struct index_state *istate, int has_racy_timestamp(struct index_state *istate); int ie_match_stat(struct index_state *, const struct cache_entry *, struct stat *, unsigned int); int ie_modified(struct index_state *, const struct cache_entry *, struct stat *, unsigned int); +/* + * Unlike ie_match_stat(), verify content for marked non-gitlinks and poisoned + * entries in an authenticated suspended fsmonitor epoch. Ordinary entries, + * including unmarked zero-stat entries, retain stat-only matching. + */ +int ie_match_stat_with_content_check(struct index_state *, + const struct cache_entry *, + struct stat *, unsigned int); int match_stat_data_racy(const struct index_state *istate, const struct stat_data *sd, struct stat *st); @@ -453,6 +536,8 @@ int fake_lstat(const struct cache_entry *ce, struct stat *st); #define REFRESH_IN_PORCELAIN (1 << 5) /* user friendly output, not "needs update" */ #define REFRESH_PROGRESS (1 << 6) /* show progress bar if stderr is tty */ #define REFRESH_IGNORE_SKIP_WORKTREE (1 << 7) /* ignore skip_worktree entries */ +#define REFRESH_DEFER_BULK_DIRTY (1 << 8) /* leave bulk results to diff */ +#define REFRESH_IN_PROOF_EPOCH (1 << 9) /* refresh is bounded by a proof epoch */ int refresh_index(struct index_state *, unsigned int flags, const struct pathspec *pathspec, char *seen, const char *header_msg); /* * Refresh the index and write it to disk. @@ -473,7 +558,11 @@ int repo_refresh_and_write_index(struct repository*, unsigned int refresh_flags, struct cache_entry *refresh_cache_entry(struct index_state *, struct cache_entry *, unsigned int); +/* The caller must first verify the entry's content and mode against st. */ +void refresh_index_entry_stat(struct index_state *, int, struct stat *); + void set_alternate_index_output(const char *); +const char *get_alternate_index_output(void); extern int verify_index_checksum; extern int verify_ce_order; diff --git a/read-cache.c b/read-cache.c index 6c449f393d8d4d..4e06c1fe0a3deb 100644 --- a/read-cache.c +++ b/read-cache.c @@ -16,6 +16,8 @@ #include "tempfile.h" #include "lockfile.h" #include "cache-tree.h" +#include "clean-status.h" +#include "clean-status-index.h" #include "refs.h" #include "dir.h" #include "object-file.h" @@ -25,13 +27,16 @@ #include "tree.h" #include "commit.h" #include "environment.h" +#include "ewah/ewok.h" #include "gettext.h" #include "mem-pool.h" #include "name-hash.h" #include "object-name.h" #include "path.h" +#include "path-namespace.h" #include "preload-index.h" #include "read-cache.h" +#include "replace-object.h" #include "repository.h" #include "resolve-undo.h" #include "revision.h" @@ -65,12 +70,14 @@ * is outside the range, to cause the reader to abort. */ -#define CACHE_EXT(s) ( (s[0]<<24)|(s[1]<<16)|(s[2]<<8)|(s[3]) ) +#define CACHE_EXT(s) get_be32(s) #define CACHE_EXT_TREE 0x54524545 /* "TREE" */ #define CACHE_EXT_RESOLVE_UNDO 0x52455543 /* "REUC" */ #define CACHE_EXT_LINK 0x6c696e6b /* "link" */ #define CACHE_EXT_UNTRACKED 0x554E5452 /* "UNTR" */ #define CACHE_EXT_FSMONITOR 0x46534D4E /* "FSMN" */ +#define CACHE_EXT_FSMONITOR_CONFIG 0x46534346 /* "FSCF" */ +#define CACHE_EXT_FSMONITOR_UNTRACKED 0x46535543 /* "FSUC" */ #define CACHE_EXT_ENDOFINDEXENTRIES 0x454F4945 /* "EOIE" */ #define CACHE_EXT_INDEXENTRYOFFSETTABLE 0x49454F54 /* "IEOT" */ #define CACHE_EXT_SPARSE_DIRECTORIES 0x73646972 /* "sdir" */ @@ -140,9 +147,47 @@ static void set_index_entry(struct index_state *istate, int nr, struct cache_ent add_name_hash(istate, ce); } -static void replace_index_entry(struct index_state *istate, int nr, struct cache_entry *ce) +static void replace_index_entry(struct index_state *istate, int nr, + struct cache_entry *ce, int options) { struct cache_entry *old = istate->cache[nr]; + int preserve_paired_history = + (options & ADD_CACHE_PRESERVE_CLEAN_HISTORY) && + fstat_is_reliable() && istate == istate->repo->index && + !alternate_index_output && !getenv(INDEX_ENVIRONMENT) && + !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(DB_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + !repo_config_values(istate->repo)->apply_sparse_checkout && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC && + !repo_has_replace_refs_uncached(istate->repo) && + istate->fsmonitor_token_valid && + istate->fsmonitor_untracked_valid && + istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + istate->fsmonitor_last_update && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token) && + clean_status_has_persistent_fsmonitor_semantic_history(istate) && + clean_status_revalidated_token_matches(istate); + /* Keep invalid nodes invalid; write_one_dir() expires pending events. */ + int preserve_untracked = istate->untracked && + istate->untracked->root && + ((istate->untracked->fsmonitor_revalidation && + istate->untracked->root->valid) || + (preserve_paired_history && + istate->untracked->use_fsmonitor)) && + S_ISREG(old->ce_mode) && S_ISREG(ce->ce_mode) && + clean_status_index_entry_is_semantically_safe(istate, old, ce); + int suspended_replacement = preserve_untracked && + clean_status_fsmonitor_backoff_suspended(istate) && + !oideq(&old->oid, &ce->oid); replace_index_entry_in_base(istate, old, ce); remove_name_hash(istate, old); @@ -150,7 +195,15 @@ static void replace_index_entry(struct index_state *istate, int nr, struct cache ce->ce_flags &= ~CE_HASHED; set_index_entry(istate, nr, ce); ce->ce_flags |= CE_UPDATE_IN_BASE; - mark_fsmonitor_invalid(istate, ce); + if (preserve_untracked) { + ce->ce_flags &= ~CE_FSMONITOR_VALID; + if (suspended_replacement) + fsmonitor_invalidate_cache_entry(ce); + } else + mark_fsmonitor_invalid(istate, ce); + if (preserve_paired_history && preserve_untracked) + trace2_data_intmax("fsmonitor", istate->repo, + "apply/untracked-replacement-preserved", 1); istate->cache_changed |= CE_ENTRY_CHANGED; } @@ -199,10 +252,35 @@ void fill_stat_cache_info(struct index_state *istate, struct cache_entry *ce, st if (S_ISREG(st->st_mode)) { ce_mark_uptodate(ce); - mark_fsmonitor_valid(istate, ce); + if (fsmonitor_stat_can_be_valid(st)) + mark_fsmonitor_valid(istate, ce); } } +static struct cache_entry *make_refreshed_cache_entry( + struct index_state *istate, const struct cache_entry *ce, + struct stat *st, int preserve_valid) +{ + struct cache_entry *updated = + make_empty_cache_entry(istate, ce_namelen(ce)); + + copy_cache_entry(updated, ce); + memcpy(updated->name, ce->name, ce->ce_namelen + 1); + fill_stat_cache_info(istate, updated, st); + /* Do not let assume-unchanged reacquire a caller-cleared CE_VALID. */ + if (preserve_valid && assume_unchanged && + !(ce->ce_flags & CE_VALID)) + updated->ce_flags &= ~CE_VALID; + return updated; +} + +void refresh_index_entry_stat(struct index_state *istate, int nr, + struct stat *st) +{ + replace_index_entry(istate, nr, make_refreshed_cache_entry( + istate, istate->cache[nr], st, 1), 0); +} + static unsigned int st_mode_from_ce(const struct cache_entry *ce) { switch (ce->ce_mode & S_IFMT) { @@ -492,6 +570,41 @@ int ie_modified(struct index_state *istate, return 0; } +int ie_match_stat_with_content_check(struct index_state *istate, + const struct cache_entry *ce, + struct stat *st, unsigned int options) +{ + const struct stat_data empty = { 0 }; + struct cache_entry *current; + int changed, pos; + + /* + * A suspended replacement persists its poisoned stat data, but not the + * transient content-check flag. Recover that obligation only from an + * authenticated suspended epoch; ordinary zero-stat entries keep their + * stat-only matching behavior. + */ + if (S_ISGITLINK(ce->ce_mode) || + (!(ce->ce_flags & CE_CONTENT_CHECK_REQUIRED) && + (!clean_status_fsmonitor_backoff_suspended(istate) || + memcmp(&ce->ce_stat_data, &empty, sizeof(empty))))) + return ie_match_stat(istate, ce, st, options); + + changed = ie_modified(istate, ce, st, options); + if (changed) + return changed; + + pos = index_name_pos(istate, ce->name, ce_namelen(ce)); + if (pos < 0 || istate->cache[pos] != ce) + return 0; + + current = istate->cache[pos]; + fill_stat_data(¤t->ce_stat_data, st); + current->ce_flags |= CE_UPDATE_IN_BASE; + istate->cache_changed |= CE_ENTRY_CHANGED; + return 0; +} + static int cache_name_stage_compare(const char *name1, int len1, int stage1, const char *name2, int len2, int stage2) { @@ -699,6 +812,20 @@ static struct cache_entry *create_alias_ce(struct index_state *istate, return new_entry; } +static int same_persistent_add_entry(const struct cache_entry *a, + const struct cache_entry *b) +{ + const unsigned int flags = + CE_STAGEMASK | CE_VALID | CE_EXTENDED_FLAGS; + + return a && b && + ce_namelen(a) == ce_namelen(b) && + !memcmp(a->name, b->name, ce_namelen(a)) && + a->ce_mode == b->ce_mode && + oideq(&a->oid, &b->oid) && + ((a->ce_flags ^ b->ce_flags) & flags) == 0; +} + void set_object_name_for_intent_to_add_entry(struct cache_entry *ce) { struct object_id oid; @@ -709,7 +836,8 @@ void set_object_name_for_intent_to_add_entry(struct cache_entry *ce) int add_to_index(struct index_state *istate, const char *path, struct stat *st, int flags) { - int namelen, was_same; + int namelen, was_same, logical_same, semantic_same; + int cache_nr = istate->cache_nr; mode_t st_mode = st->st_mode; struct cache_entry *ce, *alias = NULL; unsigned ce_option = CE_MATCH_IGNORE_VALID|CE_MATCH_IGNORE_SKIP_WORKTREE|CE_MATCH_RACY_IS_DIRTY; @@ -794,12 +922,24 @@ int add_to_index(struct index_state *istate, const char *path, struct stat *st, !ce_stage(alias) && oideq(&alias->oid, &ce->oid) && ce->ce_mode == alias->ce_mode); + logical_same = same_persistent_add_entry(alias, ce); + semantic_same = clean_status_index_entry_is_semantically_safe( + istate, alias, ce); + + if (!pretend && (flags & ADD_CACHE_TRACK_CLEAN_HISTORY) && + !logical_same && !semantic_same) + clean_status_invalidate_current_proof(istate); if (pretend) discard_cache_entry(ce); - else if (add_index_entry(istate, ce, add_option)) { - discard_cache_entry(ce); - return error(_("unable to add '%s' to index"), path); + else { + if (add_index_entry(istate, ce, add_option)) { + discard_cache_entry(ce); + return error(_("unable to add '%s' to index"), path); + } + if ((flags & ADD_CACHE_TRACK_CLEAN_HISTORY) && + cache_nr != istate->cache_nr && !semantic_same) + clean_status_invalidate_current_proof(istate); } if (verbose && !was_same) printf("add '%s'\n", path); @@ -1262,7 +1402,7 @@ static int add_index_entry_with_check(struct index_state *istate, struct cache_e /* existing match? Just replace it. */ if (pos >= 0) { if (!new_only) - replace_index_entry(istate, pos, ce); + replace_index_entry(istate, pos, ce, option); return 0; } pos = -pos-1; @@ -1392,6 +1532,10 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate, *err = errno; return NULL; } + if (clean_status_fsmonitor_semantic_baseline_pending(istate) && + !fsmonitor_stat_can_be_valid(&st) && + !(ce->ce_flags & CE_CONTENT_CHECK_REQUIRED)) + fsmonitor_invalidate_cache_entry(ce); changed = ie_match_stat(istate, ce, &st, options); if (changed_ret) @@ -1415,7 +1559,8 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate, */ if (!S_ISGITLINK(ce->ce_mode)) { ce_mark_uptodate(ce); - mark_fsmonitor_valid(istate, ce); + if (fsmonitor_stat_can_be_valid(&st)) + mark_fsmonitor_valid(istate, ce); } return ce; } @@ -1429,19 +1574,7 @@ static struct cache_entry *refresh_cache_ent(struct index_state *istate, return NULL; } - updated = make_empty_cache_entry(istate, ce_namelen(ce)); - copy_cache_entry(updated, ce); - memcpy(updated->name, ce->name, ce->ce_namelen + 1); - fill_stat_cache_info(istate, updated, &st); - /* - * If ignore_valid is not set, we should leave CE_VALID bit - * alone. Otherwise, paths marked with --no-assume-unchanged - * (i.e. things to be edited) will reacquire CE_VALID bit - * automatically, which is not really what we want. - */ - if (!ignore_valid && assume_unchanged && - !(ce->ce_flags & CE_VALID)) - updated->ce_flags &= ~CE_VALID; + updated = make_refreshed_cache_entry(istate, ce, &st, !ignore_valid); /* istate->cache_changed is updated in the caller */ return updated; @@ -1473,6 +1606,8 @@ int repo_refresh_and_write_index(struct repository *repo, return -1; if (refresh_index(repo->index, refresh_flags, pathspec, seen, header_msg)) ret = 1; + if (fsm_settings__is_watch_limit_backoff(repo)) + write_flags |= SKIP_IF_UNCHANGED; if (0 <= fd && write_locked_index(repo->index, &lock_file, COMMIT_LOCK | write_flags)) ret = -1; return ret; @@ -1521,7 +1656,7 @@ int refresh_index(struct index_state *istate, unsigned int flags, * cache entries quickly then in the single threaded loop below, * we only have to do the special cases that are left. */ - preload_index(istate, pathspec, 0); + preload_index(istate, pathspec, flags & REFRESH_DEFER_BULK_DIRTY); trace2_region_enter("index", "refresh", NULL); for (i = 0; i < istate->cache_nr; i++) { @@ -1565,6 +1700,23 @@ int refresh_index(struct index_state *istate, unsigned int flags, if (filtered) continue; + if ((flags & REFRESH_DEFER_BULK_DIRTY) && + istate->preload_bulk_tracked_nr == istate->cache_nr) { + unsigned char state = + istate->preload_bulk_tracked_state[i]; + + if (istate->preload_bulk_provider_pending && + state == PRELOAD_BULK_TRACKED_CLEAN) + continue; + if (state == PRELOAD_BULK_TRACKED_CONTENT_CHECK) { + ce->ce_flags |= CE_CONTENT_CHECK_REQUIRED; + continue; + } + if (state == PRELOAD_BULK_TRACKED_DEFINITIVE_MODIFIED || + state == PRELOAD_BULK_TRACKED_DEFINITIVE_DELETED) + continue; + } + new_entry = refresh_cache_ent(istate, ce, options, &cache_errno, &changed, &t2_did_lstat, &t2_did_scan); @@ -1602,7 +1754,18 @@ int refresh_index(struct index_state *istate, unsigned int flags, continue; } - replace_index_entry(istate, i, new_entry); + { + int fsmonitor_valid = + (new_entry->ce_flags & CE_FSMONITOR_VALID) && + ((flags & REFRESH_IN_PROOF_EPOCH) || + clean_status_fsmonitor_semantic_baseline_pending( + istate)); + + replace_index_entry(istate, i, new_entry, 0); + if (fsmonitor_valid) + mark_fsmonitor_valid(istate, + istate->cache[i]); + } } trace2_data_intmax("index", NULL, "refresh/sum_lstat", t2_sum_lstat); trace2_data_intmax("index", NULL, "refresh/sum_scan", t2_sum_scan); @@ -1683,14 +1846,10 @@ struct ondisk_cache_entry { char name[FLEX_ARRAY]; }; -/* These are only used for v3 or lower */ +/* Index v2/v3 entries are padded to a multiple of eight bytes. */ #define align_padding_size(size, len) ((size + (len) + 8) & ~7) - (size + len) -#define align_flex_name(STRUCT,len) ((offsetof(struct STRUCT,data) + (len) + 8) & ~7) -#define ondisk_cache_entry_size(len) align_flex_name(ondisk_cache_entry,len) #define ondisk_data_size(flags, len) (the_hash_algo->rawsz + \ ((flags & CE_EXTENDED) ? 2 : 1) * sizeof(uint16_t) + len) -#define ondisk_data_size_max(len) (ondisk_data_size(CE_EXTENDED, len)) -#define ondisk_ce_size(ce) (ondisk_cache_entry_size(ondisk_data_size((ce)->ce_flags, ce_namelen(ce)))) /* Allow fsck to force verification of the index checksum. */ int verify_index_checksum; @@ -1749,6 +1908,12 @@ static int read_index_extension(struct index_state *istate, case CACHE_EXT_FSMONITOR: read_fsmonitor_extension(istate, data, sz); break; + case CACHE_EXT_FSMONITOR_CONFIG: + clean_status_read_fsmonitor_config(istate, data, sz); + break; + case CACHE_EXT_FSMONITOR_UNTRACKED: + read_fsmonitor_untracked_extension(istate, data, sz); + break; case CACHE_EXT_ENDOFINDEXENTRIES: case CACHE_EXT_INDEXENTRYOFFSETTABLE: /* already handled in do_read_index() */ @@ -1767,30 +1932,76 @@ static int read_index_extension(struct index_state *istate, return 0; } +enum index_entry_decode_error { + INDEX_ENTRY_DECODE_OK, + INDEX_ENTRY_DECODE_CORRUPT, + INDEX_ENTRY_DECODE_FLAGS, + INDEX_ENTRY_DECODE_NAME, +}; + +enum index_entry_decode_flags { + INDEX_ENTRY_ALLOW_NAME_RESTART = 1 << 0, + INDEX_ENTRY_VERIFY_FORMAT = 1 << 1, +}; + +struct decoded_index_entry { + struct cache_entry *ce; + size_t size; + unsigned int bad_flags; +}; + +static int decode_index_entry_varint(const unsigned char **cursor, + const unsigned char *end, + uint64_t *result) +{ + const unsigned char *p = *cursor; + unsigned char c; + uint64_t value; + + if (p == end) + return -1; + c = *p++; + value = c & 127; + while (c & 128) { + value++; + if (!value || MSB(value, 7) || p == end) + return -1; + c = *p++; + value = (value << 7) + (c & 127); + } + *cursor = p; + *result = value; + return 0; +} + /* - * Parses the contents of the cache entry contained within the 'ondisk' buffer - * into a new incore 'cache_entry'. + * Decode one entry without reading beyond available bytes or reporting a + * fatal error. The main-index reader supplies its usual fatal wrapper below; + * optional index witnesses use the same decoder and treat errors as misses. * - * Note that 'char *ondisk' may not be aligned to a 4-byte address interval in - * index v4, so we cannot cast it to 'struct ondisk_cache_entry *' and access - * its members. Instead, we use the byte offsets of members within the struct to - * identify where 'get_be16()', 'get_be32()', and 'oidread()' (which can all - * read from an unaligned memory buffer) should read from the 'ondisk' buffer - * into the corresponding incore 'cache_entry' members. + * A v4 IEOT block starts with a complete name, but its strip count still + * describes the preceding block's last name. Preserve the main reader's + * treatment of a missing previous_ce as a name restart. The optional reader + * starts at the first entry and also requests the stricter format checks. + * + * V4 entries need not be aligned. Load fixed fields by their byte offsets, + * using get_be16(), get_be32(), and oidread() rather than a struct cast. */ -static struct cache_entry *create_from_disk(struct mem_pool *ce_mem_pool, - unsigned int version, - const char *ondisk, - unsigned long *ent_size, - const struct cache_entry *previous_ce) +static enum index_entry_decode_error decode_index_entry( + struct mem_pool *ce_mem_pool, const struct git_hash_algo *algo, + unsigned int version, const char *ondisk, size_t available, + const struct cache_entry *previous_ce, unsigned int options, + struct decoded_index_entry *decoded) { struct cache_entry *ce; - size_t len; - const char *name; - const unsigned hashsz = the_hash_algo->rawsz; - const char *flagsp = ondisk + offsetof(struct ondisk_cache_entry, data) + hashsz; + size_t len, suffix_len, consumed; + size_t fixed_size = offsetof(struct ondisk_cache_entry, data) + + algo->rawsz + sizeof(uint16_t); + const char *name, *end = ondisk + available; + const char *flagsp; unsigned int flags; size_t copy_len = 0; + int verify_format = options & INDEX_ENTRY_VERIFY_FORMAT; /* * Adjacent cache entries tend to share the leading paths, so it makes * sense to only store the differences in later entries. In the v4 @@ -1800,42 +2011,85 @@ static struct cache_entry *create_from_disk(struct mem_pool *ce_mem_pool, */ int expand_name_field = version == 4; + memset(decoded, 0, sizeof(*decoded)); + if (available < fixed_size) + return INDEX_ENTRY_DECODE_CORRUPT; + flagsp = ondisk + fixed_size - sizeof(uint16_t); + /* On-disk flags are just 16 bits */ flags = get_be16(flagsp); len = flags & CE_NAMEMASK; if (flags & CE_EXTENDED) { - int extended_flags; - extended_flags = get_be16(flagsp + sizeof(uint16_t)) << 16; + unsigned int extended_flags; + + if (available - fixed_size < sizeof(uint16_t)) + return INDEX_ENTRY_DECODE_CORRUPT; + extended_flags = + (unsigned int)get_be16(flagsp + sizeof(uint16_t)) << 16; /* We do not yet understand any bit out of CE_EXTENDED_FLAGS */ - if (extended_flags & ~CE_EXTENDED_FLAGS) - die(_("unknown index entry format 0x%08x"), extended_flags); + if (extended_flags & ~CE_EXTENDED_FLAGS) { + decoded->bad_flags = extended_flags; + return INDEX_ENTRY_DECODE_FLAGS; + } flags |= extended_flags; - name = (const char *)(flagsp + 2 * sizeof(uint16_t)); + fixed_size += sizeof(uint16_t); } - else - name = (const char *)(flagsp + sizeof(uint16_t)); + name = ondisk + fixed_size; if (expand_name_field) { const unsigned char *cp = (const unsigned char *)name; - uint64_t strip_len, previous_len; + uint64_t strip_len; - /* If we're at the beginning of a block, ignore the previous name */ - strip_len = decode_varint(&cp); + if (decode_index_entry_varint( + &cp, (const unsigned char *)end, &strip_len)) + return INDEX_ENTRY_DECODE_CORRUPT; if (previous_ce) { - previous_len = previous_ce->ce_namelen; - if (previous_len < strip_len) - die(_("malformed name field in the index, near path '%s'"), - previous_ce->name); - copy_len = previous_len - strip_len; - } + if (previous_ce->ce_namelen < strip_len) + return INDEX_ENTRY_DECODE_NAME; + copy_len = previous_ce->ce_namelen - strip_len; + } else if (strip_len && + !(options & INDEX_ENTRY_ALLOW_NAME_RESTART)) + return INDEX_ENTRY_DECODE_NAME; name = (const char *)cp; } if (len == CE_NAMEMASK) { - len = strlen(name); - if (expand_name_field) - len += copy_len; + const char *nul = memchr(name, '\0', end - name); + + if (!nul || copy_len > INT_MAX || + (size_t)(nul - name) > INT_MAX - copy_len) + return INDEX_ENTRY_DECODE_CORRUPT; + suffix_len = nul - name; + len = copy_len + suffix_len; + if (verify_format && len < CE_NAMEMASK) + return INDEX_ENTRY_DECODE_CORRUPT; + } else { + if (len < copy_len) + return INDEX_ENTRY_DECODE_CORRUPT; + suffix_len = len - copy_len; + if (suffix_len >= (size_t)(end - name) || name[suffix_len] || + (verify_format && memchr(name, '\0', suffix_len))) + return INDEX_ENTRY_DECODE_CORRUPT; + } + if (len > INT_MAX || + len > SIZE_MAX - offsetof(struct cache_entry, name) - 1) + return INDEX_ENTRY_DECODE_CORRUPT; + + consumed = (name - ondisk) + suffix_len + 1; + if (!expand_name_field) { + size_t padded; + + if (consumed > SIZE_MAX - 7) + return INDEX_ENTRY_DECODE_CORRUPT; + padded = (consumed + 7) & ~(size_t)7; + if (padded > available) + return INDEX_ENTRY_DECODE_CORRUPT; + if (verify_format) + for (size_t i = consumed; i < padded; i++) + if (ondisk[i]) + return INDEX_ENTRY_DECODE_CORRUPT; + consumed = padded; } ce = mem_pool__ce_alloc(ce_mem_pool, len); @@ -1866,18 +2120,189 @@ static struct cache_entry *create_from_disk(struct mem_pool *ce_mem_pool, ce->ce_namelen = len; ce->index = 0; oidread(&ce->oid, (const unsigned char *)ondisk + offsetof(struct ondisk_cache_entry, data), - the_repository->hash_algo); + algo); + + if (copy_len) + memcpy(ce->name, previous_ce->name, copy_len); + memcpy(ce->name + copy_len, name, suffix_len + 1); + decoded->ce = ce; + decoded->size = consumed; + return INDEX_ENTRY_DECODE_OK; +} - if (expand_name_field) { - if (copy_len) - memcpy(ce->name, previous_ce->name, copy_len); - memcpy(ce->name + copy_len, name, len + 1 - copy_len); - *ent_size = (name - ((char *)ondisk)) + len + 1 - copy_len; - } else { - memcpy(ce->name, name, len + 1); - *ent_size = ondisk_ce_size(ce); +static struct cache_entry *create_from_disk( + struct mem_pool *ce_mem_pool, unsigned int version, + const char *ondisk, size_t available, unsigned long *ent_size, + const struct cache_entry *previous_ce) +{ + struct decoded_index_entry decoded; + enum index_entry_decode_error err = decode_index_entry( + ce_mem_pool, the_hash_algo, version, ondisk, available, + previous_ce, INDEX_ENTRY_ALLOW_NAME_RESTART, &decoded); + + if (err == INDEX_ENTRY_DECODE_FLAGS) + die(_("unknown index entry format 0x%08x"), decoded.bad_flags); + if (err == INDEX_ENTRY_DECODE_NAME && previous_ce) + die(_("malformed name field in the index, near path '%s'"), + previous_ce->name); + if (err || decoded.size > ULONG_MAX) + die(_("index file corrupt")); + *ent_size = decoded.size; + return decoded.ce; +} + +/* Format-level checks only: a witness must not consult worktree config. */ +static int index_witness_entry_is_valid( + const struct cache_entry *ce, unsigned int version, + const struct cache_entry *previous) +{ + const char *component = ce->name; + + switch (ce->ce_mode) { + case 0100644: + case 0100755: + case 0120000: + case 0160000: + break; + default: + return 0; } - return ce; + if (!ce_namelen(ce) || + (version == 2 && (ce->ce_flags & CE_EXTENDED))) + return 0; + for (;;) { + const char *slash = strchr(component, '/'); + size_t len = slash ? (size_t)(slash - component) : + strlen(component); + + if (!len || (len == 1 && component[0] == '.') || + (len == 2 && !memcmp(component, "..", 2)) || + (len == 4 && component[0] == '.' && + (component[1] == 'g' || component[1] == 'G') && + (component[2] == 'i' || component[2] == 'I') && + (component[3] == 't' || component[3] == 'T'))) + return 0; + if (!slash) + break; + component = slash + 1; + } + if (previous) { + int cmp = strcmp(previous->name, ce->name); + + if (cmp > 0 || + (!cmp && (!ce_stage(previous) || + ce_stage(previous) >= ce_stage(ce)))) + return 0; + } + return 1; +} + +int read_index_entries_from_fd(struct index_state *istate, int fd) +{ + struct index_state parsed = INDEX_STATE_INIT(istate->repo); + const struct git_hash_algo *algo; + struct stat before, after; + unsigned char header[sizeof(struct cache_header)]; + char *data = NULL; + size_t size, end, offset, minimum_entry_size; + uint32_t nr; + int ret = -1; + + if (!istate->repo || !istate->repo->hash_algo || fd < 0 || + istate->initialized || istate->cache || istate->cache_nr || + istate->ce_mem_pool) + return -1; + algo = istate->repo->hash_algo; + trace2_region_enter("index", "read_index_entries", istate->repo); + if (fstat(fd, &before) || !S_ISREG(before.st_mode) || + before.st_size < 0 || + (uintmax_t)before.st_size > SIZE_MAX || + (uintmax_t)before.st_size > + (uintmax_t)maximum_signed_value_of_type(ssize_t)) + goto done; + size = (size_t)before.st_size; + if (size < sizeof(header) + algo->rawsz || + (size_t)pread_in_full(fd, header, sizeof(header), 0) != + sizeof(header) || memcmp(header, "DIRC", 4)) + goto done; + parsed.version = get_be32(header + 4); + if (parsed.version < INDEX_FORMAT_LB || + parsed.version > INDEX_FORMAT_UB) + goto done; + end = size - algo->rawsz; + nr = get_be32(header + 8); + offset = sizeof(header); + minimum_entry_size = offsetof(struct ondisk_cache_entry, data) + + algo->rawsz + sizeof(uint16_t) + 1 + (parsed.version == 4); + if (nr > INT_MAX || nr > (end - offset) / minimum_entry_size || + unsigned_mult_overflows((size_t)nr, sizeof(*parsed.cache))) + goto done; + /* A concurrent truncate must be a short read, not an mmap SIGBUS. */ + data = malloc(size); + if (!data || (size_t)pread_in_full(fd, data, size, 0) != size || + memcmp(data, header, sizeof(header))) + goto done; + oidread(&parsed.oid, (const unsigned char *)data + end, algo); + if (!is_null_oid(&parsed.oid) && + !hashfile_checksum_valid(algo, (const unsigned char *)data, size)) + goto done; + if (nr) { + parsed.cache = calloc(nr, sizeof(*parsed.cache)); + if (!parsed.cache) + goto done; + parsed.ce_mem_pool = malloc(sizeof(*parsed.ce_mem_pool)); + if (!parsed.ce_mem_pool) + goto done; + mem_pool_init(parsed.ce_mem_pool, 0); + } + parsed.cache_alloc = nr; + parsed.initialized = 1; + parsed.timestamp.sec = before.st_mtime; + parsed.timestamp.nsec = ST_MTIME_NSEC(before); + while (parsed.cache_nr < nr) { + struct decoded_index_entry decoded; + const struct cache_entry *previous = parsed.cache_nr ? + parsed.cache[parsed.cache_nr - 1] : NULL; + + if (decode_index_entry(parsed.ce_mem_pool, algo, parsed.version, + data + offset, end - offset, previous, + INDEX_ENTRY_VERIFY_FORMAT, &decoded) || + !index_witness_entry_is_valid(decoded.ce, parsed.version, + previous)) + goto done; + parsed.cache[parsed.cache_nr++] = decoded.ce; + offset += decoded.size; + } + while (offset < end) { + const char *ext = data + offset; + uint32_t ext_size; + + if (end - offset < 8) + goto done; + ext_size = get_be32(ext + 4); + if (ext_size > end - offset - 8 || + ext[0] < 'A' || ext[0] > 'Z' || + !memcmp(ext, "REUC", 4)) + goto done; + /* Optional acceleration extensions are deliberately not installed. */ + offset += 8; + offset += ext_size; + } + if (fstat(fd, &after) || !path_namespace_stat_equal(&before, &after)) + goto done; + + trace2_data_intmax("index", istate->repo, "read/entries-only", + parsed.cache_nr); + release_index(istate); + *istate = parsed; + index_state_init(&parsed, istate->repo); + ret = 0; + +done: + free(data); + release_index(&parsed); + trace2_region_leave("index", "read_index_entries", istate->repo); + return ret; } static void check_ce_order(struct index_state *istate) @@ -1950,6 +2375,9 @@ static void post_read_index_from(struct index_state *istate) check_ce_order(istate); tweak_untracked_cache(istate); tweak_split_index(istate); + clean_status_restore_external_history(istate); + prepare_fsmonitor_untracked(istate); + clean_status_prepare_fsmonitor_config(istate); tweak_fsmonitor(istate); } @@ -1994,30 +2422,149 @@ struct load_index_extensions const char *mmap; size_t mmap_size; unsigned long src_offset; + int allow_parallel; + int force_parallel; }; +struct load_index_extension { + pthread_t pthread; + struct index_state *istate; + const char *ext; + const char *data; + unsigned long size; + int result; +}; + +#define PARALLEL_INDEX_EXTENSION_THRESHOLD (1024 * 1024) + +static void *load_one_index_extension(void *_data) +{ + struct load_index_extension *p = _data; + + trace2_thread_start("index-extension"); + trace2_data_intmax("index", p->istate->repo, + "extension/parallel/tree-untracked", 1); + p->result = read_index_extension(p->istate, p->ext, p->data, p->size); + trace2_thread_exit(); + return NULL; +} + +/* + * TREE and UNTR are usually the two largest index extensions. They read the + * same immutable mmap but publish to separate index_state fields, so they can + * be decoded concurrently. Keep split indexes on the established serial + * path because LINK changes how the completed index is assembled. + */ +static int find_parallel_index_extensions(struct load_index_extensions *p, + struct load_index_extension *tree) +{ + size_t offset = p->src_offset; + size_t end = p->mmap_size - the_hash_algo->rawsz; + int tree_nr = 0, untracked_nr = 0, link_nr = 0; + uint32_t untracked_size = 0; + + if (!p->allow_parallel) + return 0; + + while (offset <= end - 8) { + const char *ext = p->mmap + offset; + uint32_t size = get_be32(ext + 4); + + if (size > end - offset - 8) + return 0; + + switch (CACHE_EXT(ext)) { + case CACHE_EXT_TREE: + tree_nr++; + tree->istate = p->istate; + tree->ext = ext; + tree->data = ext + 8; + tree->size = size; + break; + case CACHE_EXT_UNTRACKED: + untracked_nr++; + untracked_size = size; + break; + case CACHE_EXT_LINK: + link_nr++; + break; + } + + offset += 8 + size; + } + + return offset == end && tree_nr == 1 && untracked_nr == 1 && !link_nr && + (p->force_parallel || + (tree->size >= PARALLEL_INDEX_EXTENSION_THRESHOLD && + untracked_size >= PARALLEL_INDEX_EXTENSION_THRESHOLD)); +} + static void *load_index_extensions(void *_data) { struct load_index_extensions *p = _data; - unsigned long src_offset = p->src_offset; + size_t src_offset = p->src_offset; + size_t end; + struct load_index_extension tree = { 0 }; + int tree_thread = 0; + int extension_error = 0; + + if (p->mmap_size < the_hash_algo->rawsz) { + extension_error = 1; + goto join_tree; + } + end = p->mmap_size - the_hash_algo->rawsz; + if (src_offset > end) { + extension_error = 1; + goto join_tree; + } + if (find_parallel_index_extensions(p, &tree) && + !pthread_create(&tree.pthread, NULL, load_one_index_extension, &tree)) { + tree_thread = 1; + } - while (src_offset <= p->mmap_size - the_hash_algo->rawsz - 8) { + while (src_offset < end) { /* After an array of active_nr index entries, * there can be arbitrary number of extended * sections, each of which is prefixed with * extension name (4-byte) and section length * in 4-byte network byte order. */ - uint32_t extsize = get_be32(p->mmap + src_offset + 4); - if (read_index_extension(p->istate, - p->mmap + src_offset, - p->mmap + src_offset + 8, - extsize) < 0) { - munmap((void *)p->mmap, p->mmap_size); - die(_("index file corrupt")); + uint32_t extsize; + const char *ext = p->mmap + src_offset; + + if (end - src_offset < 8) { + extension_error = 1; + break; } - src_offset += 8; - src_offset += extsize; + extsize = get_be32(ext + 4); + if (extsize > end - src_offset - 8) { + extension_error = 1; + break; + } + if ((!tree_thread || CACHE_EXT(ext) != CACHE_EXT_TREE) && + read_index_extension(p->istate, ext, ext + 8, extsize) < 0) { + extension_error = 1; + break; + } + src_offset += 8 + extsize; + } + if (src_offset != end) + extension_error = 1; + +join_tree: + if (tree_thread) { + int err = pthread_join(tree.pthread, NULL); + + if (err) + die(_("unable to join load_index_extension thread: %s"), + strerror(err)); + if (tree.result < 0) + extension_error = 1; + } + + if (extension_error) { + munmap((void *)p->mmap, p->mmap_size); + die(_("index file corrupt")); } return NULL; @@ -2029,20 +2576,32 @@ static void *load_index_extensions(void *_data) */ static unsigned long load_cache_entry_block(struct index_state *istate, struct mem_pool *ce_mem_pool, int offset, int nr, const char *mmap, - unsigned long start_offset, const struct cache_entry *previous_ce) + size_t mmap_size, unsigned long start_offset, + const struct cache_entry *previous_ce) { int i; unsigned long src_offset = start_offset; + size_t end; + + if (mmap_size < the_hash_algo->rawsz || offset < 0 || nr < 0 || + nr > INT_MAX - offset || (unsigned int)offset > istate->cache_nr || + (unsigned int)nr > istate->cache_nr - offset) + die(_("index file corrupt")); + end = mmap_size - the_hash_algo->rawsz; for (i = offset; i < offset + nr; i++) { struct cache_entry *ce; unsigned long consumed; + if (src_offset > end) + die(_("index file corrupt")); ce = create_from_disk(ce_mem_pool, istate->version, - mmap + src_offset, + mmap + src_offset, end - src_offset, &consumed, previous_ce); set_index_entry(istate, i, ce); + if (consumed > ULONG_MAX - src_offset) + die(_("index file corrupt")); src_offset += consumed; previous_ce = ce; } @@ -2064,7 +2623,8 @@ static unsigned long load_all_cache_entries(struct index_state *istate, } consumed = load_cache_entry_block(istate, istate->ce_mem_pool, - 0, istate->cache_nr, mmap, src_offset, NULL); + 0, istate->cache_nr, mmap, mmap_size, + src_offset, NULL); return consumed; } @@ -2084,6 +2644,7 @@ struct load_cache_entries_thread_data struct mem_pool *ce_mem_pool; int offset; const char *mmap; + size_t mmap_size; struct index_entry_offset_table *ieot; int ieot_start; /* starting index into the ieot array */ int ieot_blocks; /* count of ieot entries to process */ @@ -2102,7 +2663,8 @@ static void *load_cache_entries_thread(void *_data) /* iterate across all ieot blocks assigned to this thread */ for (i = p->ieot_start; i < p->ieot_start + p->ieot_blocks; i++) { p->consumed += load_cache_entry_block(p->istate, p->ce_mem_pool, - p->offset, p->ieot->entries[i].nr, p->mmap, p->ieot->entries[i].offset, NULL); + p->offset, p->ieot->entries[i].nr, p->mmap, + p->mmap_size, p->ieot->entries[i].offset, NULL); p->offset += p->ieot->entries[i].nr; } return NULL; @@ -2139,6 +2701,7 @@ static unsigned long load_cache_entries_threaded(struct index_state *istate, con p->istate = istate; p->offset = offset; p->mmap = mmap; + p->mmap_size = mmap_size; p->ieot = ieot; p->ieot_start = ieot_start; p->ieot_blocks = ieot_blocks; @@ -2194,8 +2757,9 @@ static void set_new_index_sparsity(struct index_state *istate) istate->sparse_index = 1; } -/* remember to discard_cache() before reading a different cache! */ -int do_read_index(struct index_state *istate, const char *path, int must_exist) +/* A nonnegative source_fd is owned by this reader. */ +static int do_read_index_1(struct index_state *istate, const char *path, + int must_exist, int source_fd) { int fd; struct stat st; @@ -2208,12 +2772,16 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) int nr_threads, cpus; struct index_entry_offset_table *ieot = NULL; - if (istate->initialized) + clean_status_attach_config(istate); + if (istate->initialized) { + if (source_fd >= 0) + close(source_fd); return istate->cache_nr; + } istate->timestamp.sec = 0; istate->timestamp.nsec = 0; - fd = open(path, O_RDONLY); + fd = source_fd >= 0 ? source_fd : git_open_cloexec(path, O_RDONLY); if (fd < 0) { if (!must_exist && errno == ENOENT) { set_new_index_sparsity(istate); @@ -2225,16 +2793,21 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) if (fstat(fd, &st)) die_errno(_("%s: cannot stat the open index"), path); + clean_status_record_source_identity(istate, &st); mmap_size = xsize_t(st.st_size); if (mmap_size < sizeof(struct cache_header) + the_hash_algo->rawsz) die(_("%s: index file smaller than expected"), path); mmap = xmmap_gently(NULL, mmap_size, PROT_READ, MAP_PRIVATE, fd, 0); - if (mmap == MAP_FAILED) + if (mmap == MAP_FAILED) { + int mmap_errno = errno; + + close(fd); + errno = mmap_errno; die_errno(_("%s: unable to map index file%s"), path, mmap_os_err()); - close(fd); + } hdr = (const struct cache_header *)mmap; if (verify_hdr(hdr, mmap_size) < 0) @@ -2251,6 +2824,8 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) p.istate = istate; p.mmap = mmap; p.mmap_size = mmap_size; + p.allow_parallel = 0; + p.force_parallel = git_env_bool("GIT_TEST_PARALLEL_INDEX_EXTENSIONS", 0); src_offset = sizeof(*hdr); @@ -2272,13 +2847,21 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) extension_offset = read_eoie_extension(mmap, mmap_size); if (extension_offset) { int err; + struct load_index_extension tree = { 0 }; p.src_offset = extension_offset; + /* Keep at least two workers available for cache entries. */ + p.allow_parallel = nr_threads > 3; + if (p.allow_parallel) + p.allow_parallel = + find_parallel_index_extensions(&p, &tree); err = pthread_create(&p.pthread, NULL, load_index_extensions, &p); if (err) die(_("unable to create load_index_extensions thread: %s"), strerror(err)); nr_threads--; + if (p.allow_parallel) + nr_threads--; } } @@ -2326,13 +2909,35 @@ int do_read_index(struct index_state *istate, const char *path, int must_exist) else ensure_correct_sparsity(istate); + if (!clean_status_retain_source_index_fd(istate, fd, &st)) + close(fd); + return istate->cache_nr; unmap: + close(fd); munmap((void *)mmap, mmap_size); die(_("index file corrupt")); } +/* remember to discard_cache() before reading a different cache! */ +int do_read_index(struct index_state *istate, const char *path, int must_exist) +{ + return do_read_index_1(istate, path, must_exist, -1); +} + +int do_read_index_from_fd(struct index_state *istate, int fd, + const char *path) +{ + if (fd < 0) + return -1; + if (istate->initialized) { + close(fd); + return -1; + } + return do_read_index_1(istate, path, 1, fd); +} + /* * Signal that the shared index is used by updating its mtime. * @@ -2438,6 +3043,13 @@ void release_index(struct index_state *istate) free_name_hash(istate); cache_tree_free(&(istate->cache_tree)); free(istate->fsmonitor_last_update); + free(istate->fsmonitor_last_update_pending); + free(istate->fsmonitor_untracked_token); + ewah_free(istate->fsmonitor_dirty); + istate->fsmonitor_dirty = NULL; + clean_status_release(istate); + free(istate->preload_bulk_tracked_state); + free(istate->preload_bulk_stat_updates); free(istate->cache); discard_split_index(istate); free_untracked_cache(istate->untracked); @@ -2711,6 +3323,9 @@ static int verify_index_from(const struct index_state *istate, const char *path) if (st.st_size < sizeof(struct cache_header) + the_hash_algo->rawsz) goto out; + if (is_null_oid(&istate->oid) && + !clean_status_verify_null_index(istate, &st)) + goto out; n = pread_in_full(fd, hash, the_hash_algo->rawsz, st.st_size - the_hash_algo->rawsz); if (n != the_hash_algo->rawsz) @@ -2745,17 +3360,32 @@ int has_racy_timestamp(struct index_state *istate) return 0; } -void repo_update_index_if_able(struct repository *repo, - struct lock_file *lockfile) +static int write_locked_index_with_receipt( + struct index_state *istate, struct lock_file *lock, + unsigned flags, struct clean_status_index_write_receipt *receipt, + struct clean_status_commit_checkpoint *checkpoint); + +void repo_update_index_if_able_with_receipt( + struct repository *repo, struct lock_file *lockfile, + struct clean_status_index_write_receipt *receipt) { + if (receipt) + clean_status_index_write_receipt_release(receipt); if ((repo->index->cache_changed || has_racy_timestamp(repo->index)) && repo_verify_index(repo)) - write_locked_index(repo->index, lockfile, COMMIT_LOCK); + write_locked_index_with_receipt(repo->index, lockfile, + COMMIT_LOCK, receipt, NULL); else rollback_lock_file(lockfile); } +void repo_update_index_if_able(struct repository *repo, + struct lock_file *lockfile) +{ + repo_update_index_if_able_with_receipt(repo, lockfile, NULL); +} + static int record_eoie(void) { int val; @@ -2793,18 +3423,71 @@ enum write_extensions { WRITE_RESOLVE_UNDO_EXTENSION = 1<<2, WRITE_UNTRACKED_CACHE_EXTENSION = 1<<3, WRITE_FSMONITOR_EXTENSION = 1<<4, + WRITE_FSCF_EXTENSION = 1<<5, }; #define WRITE_ALL_EXTENSIONS ((enum write_extensions)-1) +static int fsmonitor_can_persist_untracked_revalidation( + const struct index_state *istate, const char *index_path) +{ + const char *suffix, *pending; + int suspended = clean_status_fsmonitor_backoff_suspended(istate); + int same_token = istate->fsmonitor_last_update && + istate->fsmonitor_untracked_token && + !strcmp(istate->fsmonitor_last_update, + istate->fsmonitor_untracked_token); + + if (suspended) { + if (alternate_index_output || + !clean_status_index_path_is_main(istate->repo, index_path) || + getenv(DB_ENVIRONMENT) || + !fstat_is_reliable() || + repo_config_values(istate->repo)->apply_sparse_checkout || + repo_has_replace_refs_uncached(istate->repo)) + return 0; + if (!same_token && istate->fsmonitor_untracked_token && + skip_prefix(istate->fsmonitor_last_update, "builtin:", &suffix) && + skip_prefix(istate->fsmonitor_untracked_token, "pending:", &pending)) + same_token = !strcmp(suffix, pending); + } + return istate->untracked && istate->untracked->root && + istate->untracked->root->valid && + istate->untracked->fsmonitor_revalidation && + istate->fsmonitor_token_valid && + istate->fsmonitor_untracked_extension_seen && + !istate->fsmonitor_untracked_extension_invalid && + istate->fsmonitor_last_update && + starts_with(istate->fsmonitor_last_update, "builtin:") && + istate->fsmonitor_last_update[strlen("builtin:")] && + strcmp(istate->fsmonitor_last_update, "builtin:fake") && + same_token && + (!getenv(INDEX_ENVIRONMENT) || suspended) && + !getenv(GIT_WORK_TREE_ENVIRONMENT) && + !getenv(GIT_COMMON_DIR_ENVIRONMENT) && + !getenv(ALTERNATE_DB_ENVIRONMENT) && + istate == istate->repo->index && + !istate->split_index && + istate->sparse_index == INDEX_EXPANDED && + !(istate->cache_changed & + (CE_ENTRY_ADDED | CE_ENTRY_REMOVED)) && + istate->repo->config_values_private_.trust_ctime && + istate->repo->config_values_private_.check_stat && + (fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC || + suspended) && + clean_status_fsmonitor_semantic_baseline_pending(istate); +} + /* * On success, `tempfile` is closed. If it is the temporary file * of a `struct lock_file`, we will therefore effectively perform * a 'close_lock_file_gently()`. Since that is an implementation * detail of lockfiles, callers of `do_write_index()` should not - * rely on it. + * rely on it. The optional index_path names a lockfile's intended + * destination; suspended history may only be carried to the main index. */ static int do_write_index(struct index_state *istate, struct tempfile *tempfile, - enum write_extensions write_extensions, unsigned flags) + enum write_extensions write_extensions, unsigned flags, + const char *index_path) { uint64_t start = getnanotime(); struct hashfile *f; @@ -3044,6 +3727,53 @@ static int do_write_index(struct index_state *istate, struct tempfile *tempfile, goto out; } } + if (write_extensions & WRITE_FSMONITOR_EXTENSION && + istate->untracked && + istate->fsmonitor_last_update && + (istate->fsmonitor_untracked_valid || + fsmonitor_can_persist_untracked_revalidation(istate, index_path)) && + !istate->fsmonitor_legacy_untracked_fallback) { + strbuf_reset(&sb); + + write_fsmonitor_untracked_extension(&sb, istate); + err = write_index_ext_header(f, eoie_c, + CACHE_EXT_FSMONITOR_UNTRACKED, + sb.len) < 0; + hashwrite(f, sb.buf, sb.len); + if (err) { + ret = -1; + goto out; + } + } else if ((write_extensions & WRITE_FSMONITOR_EXTENSION) && + (write_extensions & WRITE_UNTRACKED_CACHE_EXTENSION) && + istate == istate->repo->index && + !alternate_index_output && + !istate->split_index && + !getenv(INDEX_ENVIRONMENT) && + istate->untracked && + istate->fsmonitor_last_update && + starts_with(istate->fsmonitor_last_update, "builtin:") && + istate->fsmonitor_last_update[strlen("builtin:")] && + strcmp(istate->fsmonitor_last_update, "builtin:fake") && + !istate->fsmonitor_legacy_untracked_fallback && + fsm_settings__get_mode(istate->repo) == FSMONITOR_MODE_IPC) { + trace2_data_intmax("fsmonitor", istate->repo, + "untracked/proof-missing", 1); + } + if (write_extensions & WRITE_FSCF_EXTENSION && + !istate->fsmonitor_legacy_untracked_fallback && + clean_status_should_write_fsmonitor_config(istate)) { + strbuf_reset(&sb); + clean_status_write_fsmonitor_config(&sb, istate); + err = write_index_ext_header(f, eoie_c, + CACHE_EXT_FSMONITOR_CONFIG, + sb.len) < 0; + hashwrite(f, sb.buf, sb.len); + if (err) { + ret = -1; + goto out; + } + } if (istate->sparse_index) { if (write_index_ext_header(f, eoie_c, CACHE_EXT_SPARSE_DIRECTORIES, 0) < 0) { ret = -1; @@ -3110,6 +3840,11 @@ void set_alternate_index_output(const char *name) alternate_index_output = name; } +const char *get_alternate_index_output(void) +{ + return alternate_index_output; +} + static int commit_locked_index(struct lock_file *lk) { if (alternate_index_output) @@ -3118,36 +3853,62 @@ static int commit_locked_index(struct lock_file *lk) return commit_lock_file(lk); } -static int do_write_locked_index(struct index_state *istate, - struct lock_file *lock, - unsigned flags, - enum write_extensions write_extensions) +static int do_write_locked_index( + struct index_state *istate, struct lock_file *lock, unsigned flags, + enum write_extensions write_extensions, + struct clean_status_index_write_receipt *receipt, + struct clean_status_commit_checkpoint *checkpoint) { int ret; int was_full = istate->sparse_index == INDEX_EXPANDED; + int receipt_prepared = 0; + char *index_path = NULL; + + if (receipt && (flags & COMMIT_LOCK) && !alternate_index_output && + !(write_extensions & WRITE_SPLIT_INDEX_EXTENSION)) + receipt_prepared = !clean_status_index_prepare_write_receipt( + istate, get_lock_file_fd(lock), receipt); ret = convert_to_sparse(istate, 0); if (ret) { + if (receipt_prepared) + clean_status_index_write_receipt_release(receipt); warning(_("failed to convert to a sparse-index")); return ret; } + if (clean_status_fsmonitor_backoff_suspended(istate) && + !alternate_index_output) + index_path = get_locked_file_path(lock); trace2_region_enter_printf("index", "do_write_index", istate->repo, "%s", get_lock_file_path(lock)); - ret = do_write_index(istate, lock->tempfile, write_extensions, flags); + ret = do_write_index(istate, lock->tempfile, write_extensions, flags, + index_path); + free(index_path); trace2_region_leave_printf("index", "do_write_index", istate->repo, "%s", get_lock_file_path(lock)); if (was_full) ensure_full_index(istate); - if (ret) + if (ret) { + if (receipt_prepared) + clean_status_index_write_receipt_release(receipt); return ret; + } if (flags & COMMIT_LOCK) ret = commit_locked_index(lock); else ret = close_lock_file_gently(lock); + if (receipt_prepared) { + if (!ret) + clean_status_index_record_write_receipt(istate, receipt); + else + clean_status_index_write_receipt_release(receipt); + } + if (!ret && checkpoint && !(flags & COMMIT_LOCK)) + clean_status_record_commit_checkpoint(checkpoint, istate, lock); run_hooks_l(the_repository, "post-index-change", istate->updated_workdir ? "1" : "0", @@ -3164,7 +3925,8 @@ static int write_split_index(struct index_state *istate, { int ret; prepare_to_write_split_index(istate); - ret = do_write_locked_index(istate, lock, flags, WRITE_ALL_EXTENSIONS); + ret = do_write_locked_index(istate, lock, flags, WRITE_ALL_EXTENSIONS, + NULL, NULL); finish_writing_split_index(istate); return ret; } @@ -3250,7 +4012,7 @@ static int write_shared_index(struct index_state *istate, trace2_region_enter_printf("index", "shared/do_write_index", the_repository, "%s", get_tempfile_path(*temp)); - ret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags); + ret = do_write_index(si->base, *temp, WRITE_NO_EXTENSION, flags, NULL); trace2_region_leave_printf("index", "shared/do_write_index", the_repository, "%s", get_tempfile_path(*temp)); @@ -3306,8 +4068,10 @@ static int too_many_not_shared_entries(struct index_state *istate) return (int64_t)istate->cache_nr * max_split < (int64_t)not_shared * 100; } -int write_locked_index(struct index_state *istate, struct lock_file *lock, - unsigned flags) +static int write_locked_index_with_receipt( + struct index_state *istate, struct lock_file *lock, + unsigned flags, struct clean_status_index_write_receipt *receipt, + struct clean_status_commit_checkpoint *checkpoint) { int new_shared_index, ret, test_split_index_env; struct split_index *si = istate->split_index; @@ -3321,6 +4085,14 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock, rollback_lock_file(lock); return 0; } + if (fsm_settings__is_watch_limit_backoff(istate->repo)) { + /* An actual write may retain history, never a live clean bitmap. */ + for (size_t i = 0; i < istate->cache_nr; i++) + istate->cache[i]->ce_flags &= ~CE_FSMONITOR_VALID; + istate->fsmonitor_untracked_valid = 0; + if (istate->untracked) + istate->untracked->use_fsmonitor = 0; + } if (istate->fsmonitor_last_update) fill_fsmonitor_bitmap(istate); @@ -3331,7 +4103,8 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock, alternate_index_output || (istate->cache_changed & ~EXTMASK)) { ret = do_write_locked_index(istate, lock, flags, - ~WRITE_SPLIT_INDEX_EXTENSION); + ~WRITE_SPLIT_INDEX_EXTENSION, + receipt, checkpoint); goto out; } @@ -3361,7 +4134,8 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock, free(path); if (!temp) { ret = do_write_locked_index(istate, lock, flags, - ~WRITE_SPLIT_INDEX_EXTENSION); + ~WRITE_SPLIT_INDEX_EXTENSION, + receipt, checkpoint); goto out; } ret = write_shared_index(istate, &temp, flags); @@ -3391,6 +4165,102 @@ int write_locked_index(struct index_state *istate, struct lock_file *lock, return ret; } +int write_locked_index(struct index_state *istate, struct lock_file *lock, + unsigned flags) +{ + return write_locked_index_with_receipt(istate, lock, flags, NULL, NULL); +} + +int write_locked_index_for_commit( + struct index_state *istate, struct lock_file *lock, + struct clean_status_commit_checkpoint **checkpoint) +{ + struct clean_status_commit_checkpoint *candidate; + int ret; + + clean_status_release_commit_checkpoint(*checkpoint); + *checkpoint = NULL; + candidate = clean_status_capture_commit_checkpoint(istate, lock); + ret = write_locked_index_with_receipt(istate, lock, 0, NULL, candidate); + if (ret) + clean_status_release_commit_checkpoint(candidate); + else + *checkpoint = candidate; + return ret; +} + +void restore_locked_index_for_commit( + struct index_state *istate, struct lock_file *lock, + const struct clean_status_commit_checkpoint *checkpoint) +{ + struct repository *repo = istate->repo; + struct index_state replacement = INDEX_STATE_INIT(repo); + struct clean_status_index_snapshot current = { .fd = -1 }; + struct lock_file rewrite = LOCK_INIT; + struct strbuf cache_tree_data = STRBUF_INIT; + char *destination = NULL; + const char *path; + int ret; + + if (!clean_status_commit_checkpoint_changed(checkpoint, lock) || + !clean_status_commit_checkpoint_still_valid(checkpoint, lock)) + return; + path = get_lock_file_path(lock); + destination = get_locked_file_path(lock); + if (!clean_status_index_path_is_main(repo, destination) || + clean_status_index_snapshot_open_allow_null_checksum( + ¤t, path, repo->hash_algo) || + hold_lock_file_for_update(&rewrite, path, LOCK_NO_DEREF) < 0 || + !clean_status_index_snapshot_still_matches_path( + ¤t, path, repo->hash_algo) || + !clean_status_prepare_commit_checkpoint_restore( + checkpoint, lock, istate, &replacement, current.fd)) + goto done; + + /* The logical entries are equal, so the parent's cache tree is reusable. */ + if (istate->cache_tree) { + cache_tree_write(&cache_tree_data, istate->cache_tree); + replacement.cache_tree = cache_tree_read( + cache_tree_data.buf, cache_tree_data.len); + } + + /* + * Only this parent owns the final canonical destination. A nested lock + * preserves the hook's file on rejection or I/O failure, and avoids + * reopening an untrusted pathname with O_TRUNC. The regular serializer + * retains the post-hook stat data and performs its usual racy smudging. + */ + repo->index = &replacement; + if (!untracked_cache_preserve_for_revalidation(&replacement)) { + repo->index = istate; + goto done; + } + fill_fsmonitor_bitmap(&replacement); + trace2_region_enter_printf("index", "do_write_index", repo, + "%s", get_lock_file_path(&rewrite)); + ret = do_write_index(&replacement, rewrite.tempfile, + ~WRITE_SPLIT_INDEX_EXTENSION, 0, destination); + trace2_region_leave_printf("index", "do_write_index", repo, + "%s", get_lock_file_path(&rewrite)); + repo->index = istate; + if (ret || + !clean_status_index_snapshot_still_matches_path( + ¤t, path, repo->hash_algo) || + !clean_status_commit_checkpoint_still_valid(checkpoint, lock) || + commit_lock_file(&rewrite)) + goto done; + + /* The original logical write already ran post-index-change. */ + trace2_data_intmax("fsmonitor", repo, + "history/commit-backoff-restored", 1); +done: + rollback_lock_file(&rewrite); + clean_status_index_snapshot_release(¤t); + strbuf_release(&cache_tree_data); + release_index(&replacement); + free(destination); +} + /* * Read the index file that is potentially unmerged into given * index_state, dropping any unmerged entries to stage #0 (potentially @@ -3498,6 +4368,7 @@ void *read_blob_data_from_index(struct index_state *istate, void move_index_extensions(struct index_state *dst, struct index_state *src) { + clean_status_copy_fsmonitor_history(dst, src); dst->untracked = src->untracked; src->untracked = NULL; dst->cache_tree = src->cache_tree; @@ -4001,8 +4872,19 @@ static void update_callback(struct diff_queue_struct *q, case DIFF_STATUS_DELETED: if (data->flags & ADD_CACHE_IGNORE_REMOVAL) break; - if (!(data->flags & ADD_CACHE_PRETEND)) + if (!(data->flags & ADD_CACHE_PRETEND)) { + int pos = index_name_pos(data->index, path, + strlen(path)); + + if ((data->flags & ADD_CACHE_TRACK_CLEAN_HISTORY) && + !clean_status_index_entry_is_semantically_safe( + data->index, + pos >= 0 ? data->index->cache[pos] : NULL, + NULL)) + clean_status_invalidate_current_proof( + data->index); remove_file_from_index(data->index, path); + } if (data->flags & (ADD_CACHE_PRETEND|ADD_CACHE_VERBOSE)) printf(_("remove '%s'\n"), path); break; diff --git a/refs.c b/refs.c index 92d5df5b71fa4b..845a5fd3f7587a 100644 --- a/refs.c +++ b/refs.c @@ -2350,6 +2350,23 @@ static struct ref_store *ref_store_init(struct repository *repo, return refs; } +int refs_for_each_replace_ref_uncached(struct repository *repo, + refs_for_each_cb cb, void *cb_data) +{ + struct ref_store *refs; + int ret; + + if (!repo->gitdir) + BUG("attempting to get uncached refs outside of repository"); + + refs = ref_store_init(repo, repo->ref_storage_format, repo->gitdir, + REF_STORE_READ); + ret = refs_for_each_replace_ref(refs, cb, cb_data); + ref_store_release(refs); + free(refs); + return ret; +} + void ref_store_release(struct ref_store *ref_store) { ref_store->be->release(ref_store); diff --git a/refs.h b/refs.h index 9979446d15fd3b..1f80233c5fc472 100644 --- a/refs.h +++ b/refs.h @@ -520,6 +520,12 @@ int refs_for_each_remote_ref(struct ref_store *refs, refs_for_each_cb fn, void *cb_data); int refs_for_each_replace_ref(struct ref_store *refs, refs_for_each_cb fn, void *cb_data); +/* + * Iterate replacement refs through a fresh read-only ref store, without + * consulting caches held by the repository's main ref store. + */ +int refs_for_each_replace_ref_uncached(struct repository *repo, + refs_for_each_cb fn, void *cb_data); /** * Iterate all refs in "prefixes" by partitioning prefixes into disjoint sets diff --git a/replace-object.c b/replace-object.c index 03d0f1f083bed9..29f7c4903e20de 100644 --- a/replace-object.c +++ b/replace-object.c @@ -107,3 +107,16 @@ int replace_refs_enabled(struct repository *r) /* repository has no objects or refs. */ return 0; } + +static int has_replace_ref(const struct reference *ref UNUSED, + void *data UNUSED) +{ + return 1; +} + +int repo_has_replace_refs_uncached(struct repository *r) +{ + if (!replace_refs_enabled(r)) + return 0; + return refs_for_each_replace_ref_uncached(r, has_replace_ref, NULL) != 0; +} diff --git a/replace-object.h b/replace-object.h index 4c9f2a2383d577..595b4598e7e629 100644 --- a/replace-object.h +++ b/replace-object.h @@ -31,6 +31,13 @@ const struct object_id *do_lookup_replace_object(struct repository *r, */ int replace_refs_enabled(struct repository *r); +/* + * Return whether the repository currently has any replacement objects that + * would be honored by lookup_replace_object(). Do not consult the cached + * replacement map. + */ +int repo_has_replace_refs_uncached(struct repository *r); + /* * If object sha1 should be replaced, return the replacement object's * name (replaced recursively, if necessary). The return value is diff --git a/reset.c b/reset.c index 71254bde93fc51..6d284f80c622ef 100644 --- a/reset.c +++ b/reset.c @@ -1,5 +1,6 @@ #include "git-compat-util.h" #include "cache-tree.h" +#include "clean-status.h" #include "gettext.h" #include "hex.h" #include "lockfile.h" @@ -166,6 +167,11 @@ int reset_working_tree(struct repository *r, unpack_tree_opts.update = !dry_run; unpack_tree_opts.dry_run = dry_run; unpack_tree_opts.merge = 1; + unpack_tree_opts.preserve_semantic_history = + !dry_run && + (!reset_hard || + (opts->flags & RESET_WORKING_TREE_PRESERVE_SEMANTIC_HISTORY)) && + clean_status_revalidated_token_matches(istate); unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */ init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL); if (reset_hard) { diff --git a/reset.h b/reset.h index 4c992ba671c7f1..5d8a39b705ba25 100644 --- a/reset.h +++ b/reset.h @@ -30,6 +30,9 @@ enum reset_working_tree_flags { * any user-visible state. */ RESET_WORKING_TREE_DRY_RUN = (1 << 6), + + /* Preserve authenticated semantic history during an autostash reset. */ + RESET_WORKING_TREE_PRESERVE_SEMANTIC_HISTORY = (1 << 7), }; struct reset_working_tree_options { diff --git a/semantic-verify-file.c b/semantic-verify-file.c new file mode 100644 index 00000000000000..40d95c2abb67cb --- /dev/null +++ b/semantic-verify-file.c @@ -0,0 +1,272 @@ +#include "git-compat-util.h" +#include "convert.h" +#include "environment.h" +#include "object-file.h" +#include "path-namespace.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify.h" +#include "semantic-verify-internal.h" + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +static int mode_matches_ce(struct repository *repo, + const struct cache_entry *ce, + const struct stat *st) +{ + if (!S_ISREG(st->st_mode)) + return 0; + if (repo_trust_executable_bit(repo) && + ((ce->ce_mode ^ st->st_mode) & 0100)) + return 0; + return 1; +} + +static int hash_raw_blob(int fd, size_t size, + const struct git_hash_algo *algo, + struct object_id *oid, void *buffer, + size_t *bytes_hashed) +{ + struct git_hash_ctx ctx; + char header[MAX_HEADER_LEN]; + int header_len; + size_t remaining = size; + + header_len = format_object_header(header, sizeof(header), OBJ_BLOB, size); + git_hash_init(&ctx, algo); + git_hash_update(&ctx, header, header_len); + + while (remaining) { + size_t want = remaining < SEMANTIC_VERIFY_HASH_BUFFER_SIZE ? + remaining : SEMANTIC_VERIFY_HASH_BUFFER_SIZE; + ssize_t nr = xread(fd, buffer, want); + + if (nr < 0) + return -1; + if (!nr) { + errno = EIO; + return -1; + } + git_hash_update(&ctx, buffer, nr); + remaining -= nr; + *bytes_hashed += nr; + } + + /* Do not silently omit an append which raced with the declared size. */ + { + char extra; + ssize_t nr = xread(fd, &extra, 1); + + if (nr < 0) + return -1; + if (nr) { + errno = EAGAIN; + return -1; + } + } + + git_hash_final_oid(oid, &ctx); + return 0; +} + +static unsigned int classify_resolve_error(int error) +{ + if (error == ENOENT) + return SEMANTIC_VERIFY_RAW_MODIFIED; + if (error == ELOOP || error == ENOTDIR || error == EXDEV || + error == EINVAL) + return SEMANTIC_VERIFY_STRUCTURAL; + return SEMANTIC_VERIFY_ERROR; +} +#endif + +int semantic_verify_classify_entry(struct index_state *istate, + const struct cache_entry *ce, + struct attr_check *check, + int validate_filter_scope, + struct semantic_verify_file_result *result) +{ + struct conv_attrs ca; + int attrs_resolved = 0; + + memset(result, 0, sizeof(*result)); + if (validate_filter_scope) { + convert_attrs_with_check(istate, &ca, ce->name, check); + attrs_resolved = 1; + result->active_filter = convert_attrs_has_clean_filter(&ca); + } + if (ce_skip_worktree(ce) || (ce->ce_flags & CE_VALID)) { + result->kind = SEMANTIC_VERIFY_SKIPPED; + return 0; + } + if (ce_stage(ce) || ce_intent_to_add(ce) || + S_ISSPARSEDIR(ce->ce_mode)) { + result->kind = SEMANTIC_VERIFY_STRUCTURAL; + return 0; + } + if (!S_ISREG(ce->ce_mode)) { + result->kind = SEMANTIC_VERIFY_SENSITIVE; + return 0; + } + if (!attrs_resolved) + convert_attrs_with_check(istate, &ca, ce->name, check); + if (!convert_attrs_are_raw_safe(&ca)) { + result->kind = SEMANTIC_VERIFY_SENSITIVE; + return 0; + } + return 1; +} + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +static int observed_stat_equal(const struct stat *a, const struct stat *b, + int has_platform_identity) +{ + if (a->st_dev != b->st_dev || a->st_ino != b->st_ino || + a->st_mode != b->st_mode || a->st_nlink != b->st_nlink || + a->st_uid != b->st_uid || a->st_gid != b->st_gid || + a->st_size != b->st_size || a->st_mtime != b->st_mtime || + ST_MTIME_NSEC(*a) != ST_MTIME_NSEC(*b) || + a->st_ctime != b->st_ctime || + ST_CTIME_NSEC(*a) != ST_CTIME_NSEC(*b)) + return 0; +#ifdef __APPLE__ + if (has_platform_identity && + (a->st_birthtimespec.tv_sec != b->st_birthtimespec.tv_sec || + a->st_birthtimespec.tv_nsec != b->st_birthtimespec.tv_nsec || + a->st_gen != b->st_gen)) + return 0; +#else + (void)has_platform_identity; +#endif + return 1; +} + +void semantic_verify_file_at(int parent_fd, const char *basename, + const struct stat *observed, + int observed_has_platform_identity, dev_t root_dev, + const struct cache_entry *ce, + struct repository *repo, void *buffer, + struct semantic_verify_file_result *result) +{ + struct stat path_before = *observed, fd_before, fd_after, path_after; + struct object_id oid; + int fd = -1; + int saved_errno; + + memset(result, 0, sizeof(*result)); + if (!mode_matches_ce(repo, ce, &path_before)) { + result->kind = SEMANTIC_VERIFY_RAW_MODIFIED; + return; + } + if (path_before.st_size < 0 || + (uintmax_t)path_before.st_size > (uintmax_t)SIZE_MAX) { + result->kind = SEMANTIC_VERIFY_SENSITIVE; + return; + } + + fd = semantic_verify_openat(parent_fd, basename, + O_RDONLY | O_NONBLOCK | O_NOFOLLOW); + if (fd < 0) { + result->error = errno; + result->kind = errno == ENOENT || errno == ENOTDIR || + errno == ELOOP ? + SEMANTIC_VERIFY_UNSTABLE : SEMANTIC_VERIFY_ERROR; + return; + } + if (fstat(fd, &fd_before)) + goto unstable; + if (fd_before.st_dev != root_dev || + !observed_stat_equal(&path_before, &fd_before, + observed_has_platform_identity)) { + errno = EAGAIN; + goto unstable; + } + if (hash_raw_blob(fd, (size_t)fd_before.st_size, repo->hash_algo, &oid, + buffer, &result->bytes_hashed)) + goto unstable; + if (fstat(fd, &fd_after)) + goto unstable; + if (fstatat(parent_fd, basename, &path_after, AT_SYMLINK_NOFOLLOW)) + goto unstable; + if (!path_namespace_stat_equal(&fd_before, &fd_after) || + !path_namespace_stat_equal(&fd_after, &path_after)) { + errno = EAGAIN; + goto unstable; + } + if (path_namespace_reopen_component( + parent_fd, basename, O_RDONLY | O_NONBLOCK | O_NOFOLLOW, + semantic_verify_openat, &fd_after)) + goto unstable; + close(fd); + + if (!oideq(&oid, &ce->oid)) { + result->kind = SEMANTIC_VERIFY_RAW_MODIFIED; + return; + } + result->kind = SEMANTIC_VERIFY_RAW_CLEAN; + result->persistable = fd_after.st_nlink == 1; + fill_stat_data(&result->stat_data, &fd_after); + return; + +unstable: + saved_errno = errno; + close(fd); + result->kind = SEMANTIC_VERIFY_UNSTABLE; + result->error = saved_errno; +} + +void semantic_verify_file(struct semantic_verify_root *root, + struct semantic_verify_path *path, + const struct cache_entry *ce, size_t cache_pos, + struct repository *repo, void *buffer, + struct semantic_verify_file_result *result) +{ + struct stat path_before; + const char *basename; + int parent_fd; + + memset(result, 0, sizeof(*result)); + if (semantic_verify_resolve_parent(path, ce->name, cache_pos, + &parent_fd, &basename)) { + result->error = errno; + result->kind = classify_resolve_error(errno); + return; + } + if (fstatat(parent_fd, basename, &path_before, AT_SYMLINK_NOFOLLOW)) { + result->error = errno; + result->kind = errno == ENOENT || errno == ENOTDIR ? + SEMANTIC_VERIFY_RAW_MODIFIED : SEMANTIC_VERIFY_ERROR; + return; + } + semantic_verify_file_at(parent_fd, basename, &path_before, 1, + root->stat.st_dev, ce, repo, buffer, result); +} +#else +static void semantic_verify_file_unavailable( + struct semantic_verify_file_result *result) +{ + memset(result, 0, sizeof(*result)); + result->kind = SEMANTIC_VERIFY_ERROR; + result->error = ENOSYS; +} + +void semantic_verify_file_at( + int parent_fd UNUSED, const char *basename UNUSED, + const struct stat *observed UNUSED, + int observed_has_platform_identity UNUSED, dev_t root_dev UNUSED, + const struct cache_entry *ce UNUSED, + struct repository *repo UNUSED, void *buffer UNUSED, + struct semantic_verify_file_result *result) +{ + semantic_verify_file_unavailable(result); +} + +void semantic_verify_file( + struct semantic_verify_root *root UNUSED, + struct semantic_verify_path *path UNUSED, + const struct cache_entry *ce UNUSED, size_t cache_pos UNUSED, + struct repository *repo UNUSED, void *buffer UNUSED, + struct semantic_verify_file_result *result) +{ + semantic_verify_file_unavailable(result); +} +#endif diff --git a/semantic-verify-internal.h b/semantic-verify-internal.h new file mode 100644 index 00000000000000..b4a896fc8195f5 --- /dev/null +++ b/semantic-verify-internal.h @@ -0,0 +1,158 @@ +#ifndef SEMANTIC_VERIFY_INTERNAL_H +#define SEMANTIC_VERIFY_INTERNAL_H + +#include "hash.h" +#include "statinfo.h" +#include "thread-utils.h" + +#ifdef __linux__ +#include +#if !defined(SYS_openat2) && defined(__NR_openat2) +#define SYS_openat2 __NR_openat2 +#elif !defined(SYS_openat2) && \ + (defined(__x86_64__) || defined(__i386__)) +#define SYS_openat2 437 +#endif +#endif + +#if defined(__APPLE__) && defined(O_NONBLOCK) && \ + defined(O_NOFOLLOW) && defined(O_DIRECTORY) && \ + defined(AT_SYMLINK_NOFOLLOW) +#define SEMANTIC_VERIFY_HAS_ANCHORED_OPEN 1 +#elif defined(__linux__) && defined(SYS_openat2) && \ + defined(O_CLOEXEC) && defined(O_NONBLOCK) && \ + defined(O_NOFOLLOW) && defined(O_DIRECTORY) && \ + defined(AT_SYMLINK_NOFOLLOW) +#define SEMANTIC_VERIFY_HAS_ANCHORED_OPEN 1 +#else +#define SEMANTIC_VERIFY_HAS_ANCHORED_OPEN 0 +#endif + +struct attr_check; +struct repository; +struct clean_status_proof_epoch; +struct clean_status_progress; +struct cache_entry; +struct git_hash_algo; +struct index_state; +struct semantic_verify_result; +struct semantic_verify_path; + +#define SEMANTIC_VERIFY_HASH_BUFFER_SIZE (256 * 1024) + +struct semantic_verify_root { + int fd; + char *path; + struct stat stat; +}; + +int semantic_verify_root_init(struct repository *repo, + struct semantic_verify_root **root_out); +int semantic_verify_root_stable(const struct semantic_verify_root *root); +void semantic_verify_root_clear(struct semantic_verify_root *root); + +int semantic_verify_openat(int dirfd, const char *path, int flags); + +struct semantic_verify_path *semantic_verify_path_new( + struct semantic_verify_root *root); +int semantic_verify_resolve_parent(struct semantic_verify_path *path, + const char *name, size_t cache_pos, + int *parent_fd, const char **basename); +void semantic_verify_path_free(struct semantic_verify_path *path, + unsigned int *namespace_unstable, + size_t *namespace_unstable_from); + +struct semantic_verify_file_result { + struct stat_data stat_data; + size_t bytes_hashed; + int error; + unsigned int kind; + unsigned int persistable; + unsigned int active_filter; +}; + +int semantic_verify_classify_entry(struct index_state *istate, + const struct cache_entry *ce, + struct attr_check *check, + int validate_filter_scope, + struct semantic_verify_file_result *result); +void semantic_verify_file(struct semantic_verify_root *root, + struct semantic_verify_path *path, + const struct cache_entry *ce, size_t cache_pos, + struct repository *repo, void *buffer, + struct semantic_verify_file_result *result); +void semantic_verify_file_at(int parent_fd, const char *basename, + const struct stat *observed, + int observed_has_platform_identity, dev_t root_dev, + const struct cache_entry *ce, + struct repository *repo, void *buffer, + struct semantic_verify_file_result *result); + +struct semantic_verify_stat_update { + uint32_t cache_pos; + struct stat_data stat_data; +}; + +struct semantic_verify_entry_identity { + const struct cache_entry *entry; + struct object_id oid; + struct stat_data stat_data; + char *name; + unsigned int mode; + unsigned int flags; +}; + +struct semantic_verify_worker { + pthread_t pthread; + unsigned int started; + struct index_state *istate; + struct semantic_verify_root *root; + struct semantic_verify_result *results; + struct clean_status_progress *progress; + struct attr_check *check; + size_t start; + size_t end; + struct semantic_verify_stat_update *updates; + size_t updates_nr; + size_t updates_alloc; + size_t bytes_hashed; + size_t raw_clean; + size_t raw_modified; + size_t sensitive; + size_t structural; + size_t skipped; + size_t unstable; + size_t errors; + size_t hardlinks; + size_t active_filters; + unsigned int namespace_unstable; + unsigned int validate_filter_scope; +}; + +void semantic_verify_worker_run(struct semantic_verify_worker *worker); + +struct semantic_verify_proof { + struct index_state *istate; + struct semantic_verify_root *root; + struct clean_status_proof_epoch *epoch; + struct semantic_verify_result *results; + struct semantic_verify_entry_identity *entry_identities; + struct semantic_verify_stat_update *stat_updates; + size_t cache_nr; + size_t stat_updates_nr; + size_t bytes_hashed; + size_t raw_clean; + size_t raw_modified; + size_t sensitive; + size_t structural; + size_t skipped; + size_t unstable; + size_t errors; + size_t hardlinks; + size_t active_filters; + unsigned int namespace_unstable; + unsigned int epoch_required; + unsigned int filter_scope_checked; +}; + +#endif /* SEMANTIC_VERIFY_INTERNAL_H */ diff --git a/semantic-verify-path.c b/semantic-verify-path.c new file mode 100644 index 00000000000000..2ae011add3eff4 --- /dev/null +++ b/semantic-verify-path.c @@ -0,0 +1,194 @@ +#include "git-compat-util.h" +#include "path-namespace.h" +#include "semantic-verify-internal.h" +#include "strbuf.h" + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +struct anchored_dir { + char *component; + int fd; + struct stat stat; + size_t first_cache_pos; +}; + +struct semantic_verify_path { + struct semantic_verify_root *root; + struct anchored_dir *dirs; + size_t dirs_nr; + size_t dirs_alloc; + size_t namespace_unstable_from; + unsigned int namespace_unstable; + struct strbuf component; +}; + +static void note_namespace_unstable(struct semantic_verify_path *path, + size_t from) +{ + path->namespace_unstable = 1; + if (from < path->namespace_unstable_from) + path->namespace_unstable_from = from; +} + +static void pop_anchored_dir(struct semantic_verify_path *path) +{ + struct anchored_dir *dir = &path->dirs[path->dirs_nr - 1]; + int parent_fd = path->dirs_nr == 1 ? path->root->fd : + path->dirs[path->dirs_nr - 2].fd; + struct stat named_stat; + +#ifdef __APPLE__ + if (fstatat(parent_fd, dir->component, &named_stat, + AT_SYMLINK_NOFOLLOW) || + !path_namespace_stat_equal(&dir->stat, &named_stat)) + note_namespace_unstable(path, dir->first_cache_pos); +#else + int named_fd; + + named_fd = semantic_verify_openat(parent_fd, dir->component, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (named_fd < 0 || fstat(named_fd, &named_stat) || + !path_namespace_stat_equal(&dir->stat, &named_stat)) + note_namespace_unstable(path, dir->first_cache_pos); + if (named_fd >= 0) + close(named_fd); +#endif + close(dir->fd); + free(dir->component); + path->dirs_nr--; +} + +struct semantic_verify_path *semantic_verify_path_new( + struct semantic_verify_root *root) +{ + struct semantic_verify_path *path; + + CALLOC_ARRAY(path, 1); + path->root = root; + path->namespace_unstable_from = SIZE_MAX; + path->component = (struct strbuf)STRBUF_INIT; + return path; +} + +int semantic_verify_resolve_parent(struct semantic_verify_path *path, + const char *name, size_t cache_pos, + int *parent_fd, const char **basename) +{ + const char *slash = strrchr(name, '/'); + size_t parent_len = slash ? (size_t)(slash - name) : 0; + size_t begin = 0, depth = 0; + + *basename = slash ? slash + 1 : name; + if (!**basename) { + errno = EINVAL; + return -1; + } + + /* Find the component-aligned prefix already pinned by this worker. */ + while (begin < parent_len && depth < path->dirs_nr) { + size_t end = begin; + struct anchored_dir *dir = &path->dirs[depth]; + + while (end < parent_len && name[end] != '/') + end++; + if (strlen(dir->component) != end - begin || + memcmp(dir->component, name + begin, end - begin)) + break; + depth++; + begin = end + 1; + } + while (path->dirs_nr > depth) + pop_anchored_dir(path); + + while (begin < parent_len) { + size_t end = begin; + struct anchored_dir *dir; + int dirfd, fd; + struct stat st; + + while (end < parent_len && name[end] != '/') + end++; + if (end == begin || + (end - begin == 1 && name[begin] == '.') || + (end - begin == 2 && name[begin] == '.' && + name[begin + 1] == '.')) { + errno = EINVAL; + return -1; + } + strbuf_reset(&path->component); + strbuf_add(&path->component, name + begin, end - begin); + dirfd = path->dirs_nr ? path->dirs[path->dirs_nr - 1].fd : + path->root->fd; + fd = semantic_verify_openat(dirfd, path->component.buf, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (fd < 0) + return -1; + if (fstat(fd, &st)) { + int saved_errno = errno; + + close(fd); + errno = saved_errno; + return -1; + } + if (!S_ISDIR(st.st_mode) || st.st_dev != path->root->stat.st_dev) { + close(fd); + errno = EXDEV; + return -1; + } + ALLOC_GROW(path->dirs, path->dirs_nr + 1, path->dirs_alloc); + dir = &path->dirs[path->dirs_nr++]; + dir->component = xstrdup(path->component.buf); + dir->fd = fd; + memcpy(&dir->stat, &st, sizeof(st)); + dir->first_cache_pos = cache_pos; + begin = end + 1; + } + + *parent_fd = path->dirs_nr ? path->dirs[path->dirs_nr - 1].fd : + path->root->fd; + return 0; +} + +void semantic_verify_path_free(struct semantic_verify_path *path, + unsigned int *namespace_unstable, + size_t *namespace_unstable_from) +{ + if (!path) + return; + while (path->dirs_nr) + pop_anchored_dir(path); + if (namespace_unstable) + *namespace_unstable = path->namespace_unstable; + if (namespace_unstable_from) + *namespace_unstable_from = path->namespace_unstable_from; + free(path->dirs); + strbuf_release(&path->component); + free(path); +} +#else +struct semantic_verify_path *semantic_verify_path_new( + struct semantic_verify_root *root UNUSED) +{ + errno = ENOSYS; + return NULL; +} + +int semantic_verify_resolve_parent( + struct semantic_verify_path *path UNUSED, + const char *name UNUSED, size_t cache_pos UNUSED, + int *parent_fd UNUSED, const char **basename UNUSED) +{ + errno = ENOSYS; + return -1; +} + +void semantic_verify_path_free( + struct semantic_verify_path *path UNUSED, + unsigned int *namespace_unstable, + size_t *namespace_unstable_from) +{ + if (namespace_unstable) + *namespace_unstable = 0; + if (namespace_unstable_from) + *namespace_unstable_from = SIZE_MAX; +} +#endif diff --git a/semantic-verify-root.c b/semantic-verify-root.c new file mode 100644 index 00000000000000..fbec93ac147607 --- /dev/null +++ b/semantic-verify-root.c @@ -0,0 +1,168 @@ +#include "git-compat-util.h" +#include "path-namespace.h" +#include "repository.h" +#include "semantic-verify-internal.h" +#include "wrapper.h" + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && defined(__linux__) +struct semantic_open_how { + uint64_t flags; + uint64_t mode; + uint64_t resolve; +}; + +#define SEMANTIC_RESOLVE_NO_XDEV 0x01 +#define SEMANTIC_RESOLVE_NO_MAGICLINKS 0x02 +#define SEMANTIC_RESOLVE_NO_SYMLINKS 0x04 +#define SEMANTIC_RESOLVE_BENEATH 0x08 +#endif + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN && !defined(__linux__) +static int set_fd_cloexec(int fd) +{ +#if defined(F_GETFD) && defined(F_SETFD) && defined(FD_CLOEXEC) + int flags = fcntl(fd, F_GETFD); + + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + return -1; +#endif + return 0; +} +#endif + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +int semantic_verify_openat(int dirfd, const char *path, int flags) +{ +#ifdef __linux__ + struct semantic_open_how how = { + .flags = flags | O_CLOEXEC, + .resolve = SEMANTIC_RESOLVE_BENEATH | + SEMANTIC_RESOLVE_NO_SYMLINKS | + SEMANTIC_RESOLVE_NO_MAGICLINKS | + SEMANTIC_RESOLVE_NO_XDEV, + }; + + return syscall(SYS_openat2, dirfd, path, &how, sizeof(how)); +#else + int fd; + int saved_errno; + +#ifdef O_CLOEXEC + fd = openat(dirfd, path, flags | O_CLOEXEC); + if (fd >= 0) + return fd; + if (errno != EINVAL) + return -1; +#endif + fd = openat(dirfd, path, flags); + if (fd < 0) + return -1; + if (!set_fd_cloexec(fd)) + return fd; + saved_errno = errno; + close(fd); + errno = saved_errno; + return -1; +#endif +} +#else +int semantic_verify_openat(int dirfd UNUSED, const char *path UNUSED, + int flags UNUSED) +{ + errno = ENOSYS; + return -1; +} +#endif + +#if SEMANTIC_VERIFY_HAS_ANCHORED_OPEN +int semantic_verify_root_init(struct repository *repo, + struct semantic_verify_root **root_out) +{ + struct semantic_verify_root *root; + const char *path = repo_get_work_tree(repo); + + if (!path) { + errno = ENOENT; + return -1; + } + CALLOC_ARRAY(root, 1); + root->fd = -1; + root->path = xstrdup(path); + root->fd = git_open_cloexec(root->path, + O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (root->fd < 0 || fstat(root->fd, &root->stat) || + !S_ISDIR(root->stat.st_mode)) { + int saved_errno = errno ? errno : ENOTDIR; + + semantic_verify_root_clear(root); + errno = saved_errno; + return -1; + } +#ifdef __linux__ + { + struct stat probe_stat; + int probe_fd = semantic_verify_openat( + root->fd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + int saved_errno; + + if (probe_fd < 0) { + saved_errno = errno; + semantic_verify_root_clear(root); + errno = saved_errno; + return -1; + } + if (fstat(probe_fd, &probe_stat)) { + saved_errno = errno; + close(probe_fd); + semantic_verify_root_clear(root); + errno = saved_errno; + return -1; + } + if (!path_namespace_stat_equal(&root->stat, &probe_stat)) { + close(probe_fd); + semantic_verify_root_clear(root); + errno = EAGAIN; + return -1; + } + if (close(probe_fd)) { + saved_errno = errno; + semantic_verify_root_clear(root); + errno = saved_errno; + return -1; + } + } +#endif + *root_out = root; + return 0; +} +#else +int semantic_verify_root_init(struct repository *repo UNUSED, + struct semantic_verify_root **root_out UNUSED) +{ + errno = ENOSYS; + return -1; +} +#endif + +int semantic_verify_root_stable(const struct semantic_verify_root *root) +{ + struct stat fd_stat, path_stat; + + if (!root || root->fd < 0) + return 0; + if (fstat(root->fd, &fd_stat) || lstat(root->path, &path_stat) || + !S_ISDIR(path_stat.st_mode)) + return 0; + return path_namespace_stat_equal(&root->stat, &fd_stat) && + path_namespace_stat_equal(&fd_stat, &path_stat); +} + +void semantic_verify_root_clear(struct semantic_verify_root *root) +{ + if (!root) + return; + if (root->fd >= 0) + close(root->fd); + free(root->path); + free(root); +} diff --git a/semantic-verify-worker.c b/semantic-verify-worker.c new file mode 100644 index 00000000000000..d63461335472db --- /dev/null +++ b/semantic-verify-worker.c @@ -0,0 +1,135 @@ +#define USE_THE_REPOSITORY_VARIABLE + +#include "git-compat-util.h" +#include "attr.h" +#include "clean-status.h" +#include "convert.h" +#include "object.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify.h" +#include "semantic-verify-internal.h" + +#define SEMANTIC_VERIFY_PROGRESS_BATCH 128 + +static void record_stat_update(struct semantic_verify_worker *worker, + uint32_t cache_pos, + const struct stat_data *stat_data) +{ + struct semantic_verify_stat_update *update; + + ALLOC_GROW(worker->updates, worker->updates_nr + 1, + worker->updates_alloc); + update = &worker->updates[worker->updates_nr++]; + update->cache_pos = cache_pos; + memcpy(&update->stat_data, stat_data, sizeof(*stat_data)); +} + +static void count_result(struct semantic_verify_worker *worker, + enum semantic_verify_kind kind) +{ + switch (kind) { + case SEMANTIC_VERIFY_SKIPPED: + worker->skipped++; + break; + case SEMANTIC_VERIFY_RAW_CLEAN: + worker->raw_clean++; + break; + case SEMANTIC_VERIFY_RAW_MODIFIED: + worker->raw_modified++; + break; + case SEMANTIC_VERIFY_SENSITIVE: + worker->sensitive++; + break; + case SEMANTIC_VERIFY_STRUCTURAL: + worker->structural++; + break; + case SEMANTIC_VERIFY_UNSTABLE: + worker->unstable++; + break; + case SEMANTIC_VERIFY_ERROR: + worker->errors++; + break; + case SEMANTIC_VERIFY_UNCHECKED: + BUG("cannot count an unchecked semantic result"); + } +} + +void semantic_verify_worker_run(struct semantic_verify_worker *worker) +{ + struct semantic_verify_path *path = + semantic_verify_path_new(worker->root); + struct attr_check *check = worker->check; + void *buffer = xmalloc(SEMANTIC_VERIFY_HASH_BUFFER_SIZE); + size_t unstable_from = SIZE_MAX, completed = 0; + + worker->check = NULL; + if (!check) + check = convert_attrs_check_alloc(); + + for (size_t i = worker->start; i < worker->end; i++) { + struct cache_entry *ce = worker->istate->cache[i]; + struct semantic_verify_result *result = &worker->results[i]; + struct semantic_verify_file_result file; + int active_filter; + + if (!semantic_verify_classify_entry(worker->istate, ce, check, + worker->validate_filter_scope, + &file)) { + result->kind = file.kind; + if (file.active_filter) { + result->flags |= SEMANTIC_VERIFY_ACTIVE_FILTER; + worker->active_filters++; + } + count_result(worker, result->kind); + goto counted; + } + active_filter = file.active_filter; + + semantic_verify_file(worker->root, path, ce, i, + worker->istate->repo, + buffer, &file); + result->kind = file.kind; + result->error = file.error > UINT16_MAX ? EIO : file.error; + if (active_filter) { + result->flags |= SEMANTIC_VERIFY_ACTIVE_FILTER; + worker->active_filters++; + } + worker->bytes_hashed += file.bytes_hashed; + if (result->kind == SEMANTIC_VERIFY_RAW_CLEAN) { + if (file.persistable) + result->flags |= SEMANTIC_VERIFY_PERSISTABLE; + else + worker->hardlinks++; + if (memcmp(&file.stat_data, &ce->ce_stat_data, + sizeof(file.stat_data))) + record_stat_update(worker, i, &file.stat_data); + } + count_result(worker, result->kind); + counted: + if (++completed == SEMANTIC_VERIFY_PROGRESS_BATCH) { + clean_status_update_progress(worker->progress, completed); + completed = 0; + } + } + clean_status_update_progress(worker->progress, completed); + + semantic_verify_path_free(path, &worker->namespace_unstable, + &unstable_from); + if (worker->namespace_unstable) { + for (size_t i = unstable_from; i < worker->end; i++) { + struct semantic_verify_result *result = &worker->results[i]; + + if (result->kind != SEMANTIC_VERIFY_RAW_CLEAN) + continue; + result->kind = SEMANTIC_VERIFY_UNSTABLE; + result->flags = 0; + result->error = EAGAIN; + worker->raw_clean--; + worker->unstable++; + } + } + + free(buffer); + attr_check_free(check); +} diff --git a/semantic-verify.c b/semantic-verify.c new file mode 100644 index 00000000000000..dc6af79c2ce796 --- /dev/null +++ b/semantic-verify.c @@ -0,0 +1,440 @@ +#define USE_THE_REPOSITORY_VARIABLE + +#include "git-compat-util.h" +#include "attr.h" +#include "clean-status.h" +#include "convert.h" +#include "fsmonitor.h" +#include "gettext.h" +#include "object.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify.h" +#include "semantic-verify-internal.h" +#include "trace2.h" + +#define SEMANTIC_VERIFY_ENTRY_FLAGS \ + (CE_VALID | CE_STAGEMASK | CE_INTENT_TO_ADD | CE_SKIP_WORKTREE | \ + CE_UPTODATE | CE_FSMONITOR_VALID | CE_CONTENT_CHECK_REQUIRED) +#define SEMANTIC_VERIFY_MAX_THREADS 32 + +static void *run_worker(void *data) +{ + semantic_verify_worker_run(data); + return NULL; +} + +static unsigned int select_thread_count( + size_t cache_nr, + const struct semantic_verify_options *options) +{ + unsigned int nr; + + if (!HAVE_THREADS) + return 1; + if (options && options->nr_threads) { + nr = options->nr_threads; + } else { + unsigned int cpus = online_cpus(); + + nr = cpus > SEMANTIC_VERIFY_MAX_THREADS / 2 ? + SEMANTIC_VERIFY_MAX_THREADS : cpus * 2; + } + if (nr > SEMANTIC_VERIFY_MAX_THREADS) + nr = SEMANTIC_VERIFY_MAX_THREADS; + if (nr > cache_nr && cache_nr) + nr = cache_nr; + return nr; +} + +static void combine_worker(struct semantic_verify_proof *proof, + struct semantic_verify_worker *worker) +{ + size_t base = proof->stat_updates_nr; + + if (worker->updates_nr) + COPY_ARRAY(proof->stat_updates + base, worker->updates, + worker->updates_nr); + for (size_t i = 0; i < worker->updates_nr; i++) { + uint32_t cache_pos = worker->updates[i].cache_pos; + + proof->results[cache_pos].stat_update_index = base + i; + } + proof->stat_updates_nr += worker->updates_nr; + proof->bytes_hashed += worker->bytes_hashed; + proof->raw_clean += worker->raw_clean; + proof->raw_modified += worker->raw_modified; + proof->sensitive += worker->sensitive; + proof->structural += worker->structural; + proof->skipped += worker->skipped; + proof->unstable += worker->unstable; + proof->errors += worker->errors; + proof->hardlinks += worker->hardlinks; + proof->active_filters += worker->active_filters; + proof->namespace_unstable |= worker->namespace_unstable; + free(worker->updates); +} + +int semantic_verify_prepare(struct index_state *istate, + const struct semantic_verify_options *options, + struct semantic_verify_proof **proof_out) +{ + struct semantic_verify_proof *proof; + struct semantic_verify_worker *workers; + struct clean_status_progress *progress; + unsigned int nr_threads; + size_t updates_nr = 0; + int create_threads = 1; + + if (!istate || !proof_out) + BUG("semantic_verify_prepare requires an index and output"); + if (sizeof(struct semantic_verify_result) != 8) + BUG("semantic verify result unexpectedly grew to %"PRIuMAX" bytes", + (uintmax_t)sizeof(struct semantic_verify_result)); + CALLOC_ARRAY(proof, 1); + proof->istate = istate; + proof->epoch_required = options && options->require_proof_epoch; + proof->filter_scope_checked = options && + options->validate_filter_scope; + proof->cache_nr = istate->cache_nr; + CALLOC_ARRAY(proof->results, proof->cache_nr); + CALLOC_ARRAY(proof->entry_identities, proof->cache_nr); + for (size_t i = 0; i < proof->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + struct semantic_verify_entry_identity *identity = + &proof->entry_identities[i]; + + proof->results[i].stat_update_index = UINT32_MAX; + identity->entry = ce; + oidcpy(&identity->oid, &ce->oid); + identity->stat_data = ce->ce_stat_data; + identity->name = xstrdup(ce->name); + identity->mode = ce->ce_mode; + identity->flags = ce->ce_flags & SEMANTIC_VERIFY_ENTRY_FLAGS; + } + *proof_out = proof; + if (!proof->cache_nr && !proof->epoch_required) + return 0; + if (istate->sparse_index != INDEX_EXPANDED) { + for (size_t i = 0; i < proof->cache_nr; i++) { + proof->results[i].kind = SEMANTIC_VERIFY_STRUCTURAL; + proof->structural++; + } + return 0; + } + if (semantic_verify_root_init(istate->repo, &proof->root)) { + int saved_errno = errno; + + for (size_t i = 0; i < proof->cache_nr; i++) { + proof->results[i].kind = SEMANTIC_VERIFY_ERROR; + proof->results[i].error = saved_errno > UINT16_MAX ? + EIO : saved_errno; + } + proof->errors = proof->cache_nr; + return -1; + } + if (proof->epoch_required) { + proof->epoch = clean_status_capture_proof_epoch( + istate, options->attr_snapshot, + proof->filter_scope_checked); + if (!proof->epoch) { + for (size_t i = 0; i < proof->cache_nr; i++) { + proof->results[i].kind = SEMANTIC_VERIFY_ERROR; + proof->results[i].error = EAGAIN; + } + proof->errors = proof->cache_nr; + return -1; + } + } + if (!proof->cache_nr) + return 0; + + /* Initialize conversion config and default attribute state serially. */ + convert_attrs_prepare(istate); + nr_threads = select_thread_count(proof->cache_nr, options); + CALLOC_ARRAY(workers, nr_threads); + if (proof->epoch_required) { + /* + * Load each worker's system, global, root, and info + * attribute frames before closing the proof epoch. + */ + for (unsigned int i = 0; i < nr_threads; i++) { + workers[i].check = convert_attrs_check_alloc(); + git_check_attr(istate, "", workers[i].check); + } + if (!clean_status_proof_epoch_prime_matches( + istate, proof->epoch)) { + for (unsigned int i = 0; i < nr_threads; i++) + attr_check_free(workers[i].check); + free(workers); + for (size_t i = 0; i < proof->cache_nr; i++) { + proof->results[i].kind = SEMANTIC_VERIFY_ERROR; + proof->results[i].error = EAGAIN; + } + proof->errors = proof->cache_nr; + git_attr_invalidate_all(); + return -1; + } + } + trace2_region_enter("semantic_verify", "prepare", istate->repo); + trace2_data_intmax("semantic_verify", istate->repo, + "threads", nr_threads); + trace2_data_intmax("semantic_verify", istate->repo, + "result-bytes", sizeof(struct semantic_verify_result)); + progress = clean_status_start_progress( + istate->repo, _("Verifying tracked files"), proof->cache_nr); + + for (unsigned int i = 0; i < nr_threads; i++) { + struct semantic_verify_worker *worker = &workers[i]; + int err; + + worker->istate = istate; + worker->root = proof->root; + worker->results = proof->results; + worker->progress = progress; + worker->start = st_mult(proof->cache_nr, i) / nr_threads; + worker->end = st_mult(proof->cache_nr, i + 1) / nr_threads; + worker->validate_filter_scope = proof->filter_scope_checked; + if (nr_threads == 1 || !create_threads) { + semantic_verify_worker_run(worker); + continue; + } + err = pthread_create(&worker->pthread, NULL, run_worker, worker); + if (!err) { + worker->started = 1; + continue; + } + create_threads = 0; + trace2_data_intmax("semantic_verify", istate->repo, + "thread-failure", err); + semantic_verify_worker_run(worker); + } + for (unsigned int i = 0; i < nr_threads; i++) { + int err; + + if (!workers[i].started) + continue; + err = pthread_join(workers[i].pthread, NULL); + if (err) + die("could not join semantic verifier thread: %s", + strerror(err)); + } + clean_status_stop_progress(&progress); + + for (unsigned int i = 0; i < nr_threads; i++) + updates_nr += workers[i].updates_nr; + ALLOC_ARRAY(proof->stat_updates, updates_nr); + for (unsigned int i = 0; i < nr_threads; i++) + combine_worker(proof, &workers[i]); + free(workers); + + trace2_data_intmax("semantic_verify", istate->repo, + "raw-clean", proof->raw_clean); + trace2_data_intmax("semantic_verify", istate->repo, + "raw-modified", proof->raw_modified); + trace2_data_intmax("semantic_verify", istate->repo, + "sensitive", proof->sensitive); + trace2_data_intmax("semantic_verify", istate->repo, + "structural", proof->structural); + trace2_data_intmax("semantic_verify", istate->repo, + "unstable", proof->unstable); + trace2_data_intmax("semantic_verify", istate->repo, + "errors", proof->errors); + trace2_data_intmax("semantic_verify", istate->repo, + "bytes-hashed", proof->bytes_hashed); + trace2_data_intmax("semantic_verify", istate->repo, + "active-filters", proof->active_filters); + trace2_data_intmax("semantic_verify", istate->repo, + "filter-scope-checked", proof->filter_scope_checked); + trace2_region_leave("semantic_verify", "prepare", istate->repo); + return 0; +} + +int semantic_verify_root_is_stable(const struct semantic_verify_proof *proof) +{ + return proof && semantic_verify_root_stable(proof->root); +} + +int semantic_verify_start_token_is_current( + struct index_state *istate, + const struct semantic_verify_proof *proof) +{ + return proof && proof->istate == istate && + (!proof->epoch_required || + clean_status_proof_epoch_start_token_matches( + istate, proof->epoch)); +} + +int semantic_verify_proof_is_current( + struct index_state *istate, + const struct semantic_verify_proof *proof) +{ + return istate && proof && proof->istate == istate && + proof->cache_nr == istate->cache_nr && + !proof->namespace_unstable && + semantic_verify_root_is_stable(proof) && + (!proof->epoch_required || + clean_status_proof_epoch_content_matches( + istate, proof->epoch)); +} + +void semantic_verify_get_stats(const struct semantic_verify_proof *proof, + struct semantic_verify_stats *stats) +{ + if (!proof || !stats) + BUG("semantic_verify_get_stats requires proof and output"); + stats->cache_nr = proof->cache_nr; + stats->stat_updates_nr = proof->stat_updates_nr; + stats->bytes_hashed = proof->bytes_hashed; + stats->raw_clean = proof->raw_clean; + stats->raw_modified = proof->raw_modified; + stats->sensitive = proof->sensitive; + stats->structural = proof->structural; + stats->skipped = proof->skipped; + stats->unstable = proof->unstable; + stats->errors = proof->errors; + stats->hardlinks = proof->hardlinks; + stats->active_filters = proof->active_filters; + stats->namespace_unstable = proof->namespace_unstable; + stats->filter_scope_checked = proof->filter_scope_checked; +} + +const struct semantic_verify_result *semantic_verify_result_at( + const struct semantic_verify_proof *proof, size_t cache_pos) +{ + if (!proof || cache_pos >= proof->cache_nr) + BUG("semantic verifier result position out of range"); + return &proof->results[cache_pos]; +} + +int semantic_verify_apply_after_closure( + struct index_state *istate, + const struct semantic_verify_proof *proof) +{ + int applied = 0; + int poisoned = 0; + size_t validated_updates = 0; + + if (!semantic_verify_proof_is_current(istate, proof)) + return -1; + if (proof->active_filters) { + trace2_data_intmax("semantic_verify", istate->repo, + "filter-scope-rejected", 1); + return -1; + } + + for (size_t i = 0; i < proof->cache_nr; i++) { + const struct semantic_verify_entry_identity *identity = + &proof->entry_identities[i]; + const struct cache_entry *ce = istate->cache[i]; + + if (ce != identity->entry || + !oideq(&ce->oid, &identity->oid) || + memcmp(&ce->ce_stat_data, &identity->stat_data, + sizeof(ce->ce_stat_data)) || + strcmp(ce->name, identity->name) || + ce->ce_mode != identity->mode || + (ce->ce_flags & SEMANTIC_VERIFY_ENTRY_FLAGS) != + identity->flags) + return -1; + } + + /* Validate the complete proof before changing any cache entry. */ + for (size_t i = 0; i < proof->cache_nr; i++) { + const struct semantic_verify_result *result = &proof->results[i]; + + if (result->kind > SEMANTIC_VERIFY_ERROR || + (result->flags & ~(SEMANTIC_VERIFY_PERSISTABLE | + SEMANTIC_VERIFY_ACTIVE_FILTER))) + return -1; + if (result->kind == SEMANTIC_VERIFY_UNCHECKED || + result->kind == SEMANTIC_VERIFY_STRUCTURAL || + result->kind == SEMANTIC_VERIFY_UNSTABLE || + result->kind == SEMANTIC_VERIFY_ERROR) + return -1; + if (result->kind != SEMANTIC_VERIFY_RAW_CLEAN) { + if (result->flags || + result->stat_update_index != UINT32_MAX) + return -1; + continue; + } + if (result->stat_update_index != UINT32_MAX) { + const struct semantic_verify_stat_update *update; + + if (result->stat_update_index >= proof->stat_updates_nr) + return -1; + update = &proof->stat_updates[result->stat_update_index]; + if (update->cache_pos != i) + return -1; + validated_updates++; + } + } + if (validated_updates != proof->stat_updates_nr) + return -1; + + for (size_t i = 0; i < proof->cache_nr; i++) { + const struct semantic_verify_result *result = &proof->results[i]; + struct cache_entry *ce = istate->cache[i]; + + /* Force the ordinary refresh tail to preserve mismatches. */ + if (result->kind == SEMANTIC_VERIFY_RAW_MODIFIED || + (result->kind == SEMANTIC_VERIFY_RAW_CLEAN && + !(result->flags & SEMANTIC_VERIFY_PERSISTABLE))) { + fsmonitor_invalidate_cache_entry(ce); + mark_fsmonitor_invalid(istate, ce); + ce->ce_flags |= CE_UPDATE_IN_BASE; + istate->cache_changed |= CE_ENTRY_CHANGED; + poisoned++; + if (result->kind == SEMANTIC_VERIFY_RAW_CLEAN) + applied++; + continue; + } + if (result->kind != SEMANTIC_VERIFY_RAW_CLEAN) + continue; + if (result->stat_update_index != UINT32_MAX) { + const struct semantic_verify_stat_update *update = + &proof->stat_updates[result->stat_update_index]; + + memcpy(&ce->ce_stat_data, &update->stat_data, + sizeof(ce->ce_stat_data)); + ce->ce_flags |= CE_UPDATE_IN_BASE; + istate->cache_changed |= CE_ENTRY_CHANGED; + } + ce_mark_uptodate(ce); + if (result->flags & SEMANTIC_VERIFY_PERSISTABLE) + mark_fsmonitor_valid(istate, ce); + applied++; + } + trace2_data_intmax("semantic_verify", istate->repo, + "applied", applied); + trace2_data_intmax("semantic_verify", istate->repo, + "poisoned-for-tail", poisoned); + return applied; +} + +int semantic_verify_accept_filter_scope( + struct index_state *istate, + const struct semantic_verify_proof *proof) +{ + if (!proof || !proof->filter_scope_checked) + return 0; + if (!proof->epoch_required || proof->active_filters || + !semantic_verify_proof_is_current(istate, proof)) + return -1; + clean_status_mark_filter_scope_valid(istate); + return 1; +} + +void semantic_verify_proof_clear(struct semantic_verify_proof *proof) +{ + if (!proof) + return; + clean_status_release_proof_epoch(proof->epoch); + semantic_verify_root_clear(proof->root); + for (size_t i = 0; i < proof->cache_nr; i++) + free(proof->entry_identities[i].name); + free(proof->entry_identities); + free(proof->stat_updates); + free(proof->results); + free(proof); +} diff --git a/semantic-verify.h b/semantic-verify.h new file mode 100644 index 00000000000000..1895a384e28f2d --- /dev/null +++ b/semantic-verify.h @@ -0,0 +1,86 @@ +#ifndef SEMANTIC_VERIFY_H +#define SEMANTIC_VERIFY_H + +struct index_state; +struct attr_source_snapshot; +struct semantic_verify_proof; + +struct semantic_verify_options { + unsigned int nr_threads; + const struct attr_source_snapshot *attr_snapshot; + unsigned int require_proof_epoch : 1; + unsigned int validate_filter_scope : 1; +}; + +#define SEMANTIC_VERIFY_OPTIONS_INIT { 0 } + +enum semantic_verify_kind { + SEMANTIC_VERIFY_UNCHECKED = 0, + SEMANTIC_VERIFY_SKIPPED, + SEMANTIC_VERIFY_RAW_CLEAN, + SEMANTIC_VERIFY_RAW_MODIFIED, + SEMANTIC_VERIFY_SENSITIVE, + SEMANTIC_VERIFY_STRUCTURAL, + SEMANTIC_VERIFY_UNSTABLE, + SEMANTIC_VERIFY_ERROR, +}; + +enum semantic_verify_result_flags { + /* The clean result may receive persistent fsmonitor validity. */ + SEMANTIC_VERIFY_PERSISTABLE = (1u << 0), + /* The selected driver can affect conversion into the index. */ + SEMANTIC_VERIFY_ACTIVE_FILTER = (1u << 1), +}; + +/* Exactly eight bytes per cache entry. */ +struct semantic_verify_result { + uint32_t stat_update_index; + uint16_t error; + uint8_t kind; + uint8_t flags; +}; + +struct semantic_verify_stats { + size_t cache_nr; + size_t stat_updates_nr; + size_t bytes_hashed; + size_t raw_clean; + size_t raw_modified; + size_t sensitive; + size_t structural; + size_t skipped; + size_t unstable; + size_t errors; + size_t hardlinks; + size_t active_filters; + unsigned int namespace_unstable; + unsigned int filter_scope_checked; +}; + +/* Build a proof candidate without changing the index. */ +int semantic_verify_prepare(struct index_state *istate, + const struct semantic_verify_options *options, + struct semantic_verify_proof **proof_out); +int semantic_verify_apply_after_closure( + struct index_state *istate, + const struct semantic_verify_proof *proof); +int semantic_verify_accept_filter_scope( + struct index_state *istate, + const struct semantic_verify_proof *proof); +int semantic_verify_root_is_stable( + const struct semantic_verify_proof *proof); +int semantic_verify_start_token_is_current( + struct index_state *istate, + const struct semantic_verify_proof *proof); +int semantic_verify_proof_is_current( + struct index_state *istate, + const struct semantic_verify_proof *proof); +void semantic_verify_proof_clear(struct semantic_verify_proof *proof); + +/* Introspection used by the semantic verifier test helper. */ +void semantic_verify_get_stats(const struct semantic_verify_proof *proof, + struct semantic_verify_stats *stats); +const struct semantic_verify_result *semantic_verify_result_at( + const struct semantic_verify_proof *proof, size_t cache_pos); + +#endif /* SEMANTIC_VERIFY_H */ diff --git a/sequencer.c b/sequencer.c index 82ab3c536f94a2..6a8fcd1170bd98 100644 --- a/sequencer.c +++ b/sequencer.c @@ -4724,7 +4724,8 @@ static void create_autostash_internal(struct repository *r, struct child_process stash = CHILD_PROCESS_INIT; struct reset_working_tree_options ropts = { .flags = RESET_WORKING_TREE_HARD | - RESET_WORKING_TREE_UPDATE_HEAD, + RESET_WORKING_TREE_UPDATE_HEAD | + RESET_WORKING_TREE_PRESERVE_SEMANTIC_HISTORY, }; struct object_id oid; diff --git a/simple-ipc.h b/simple-ipc.h index 701e005cb8e5f3..cc7471c97624eb 100644 --- a/simple-ipc.h +++ b/simple-ipc.h @@ -106,6 +106,15 @@ int ipc_client_send_command_to_connection( const char *message, size_t message_len, struct strbuf *answer); +/* + * Like ipc_client_send_command_to_connection(), but suppress the generic IPC + * transport diagnostic so callers can recover from a disappearing server. + */ +int ipc_client_send_command_to_connection_gently( + struct ipc_client_connection *connection, + const char *message, size_t message_len, + struct strbuf *answer); + /* * Used by the client to synchronously connect and send and receive a * message to the server listening at the given path. diff --git a/t/README b/t/README index 4252774f86c8ce..31ead13c82aeb5 100644 --- a/t/README +++ b/t/README @@ -422,6 +422,22 @@ overridden by the --no-path-walk command-line argument. GIT_TEST_PRELOAD_INDEX= exercises the preload-index code path by overriding the minimum number of cache entries required per thread. +GIT_TEST_PRELOAD_INDEX_BULK= overrides the +`core.preloadIndexBulk` setting. + +GIT_TEST_PRELOAD_INDEX_BULK_DIRENT_TYPE=:, when +GIT_TEST_PRELOAD_INDEX_BULK is enabled, overrides the Linux directory +entry type for one worktree-relative path. is `dir` or `reg`. + +GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_PATH=, +GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_READY=, and +GIT_TEST_PRELOAD_INDEX_BULK_BARRIER_RESUME=, when +GIT_TEST_PRELOAD_INDEX_BULK is enabled, pause a bulk preload before +scanning the named directory. An empty directory path pauses after the +complete walk. Git writes `ready` to the ready path, then waits until it +can read from the resume path. Tests which set one barrier variable must +set all three. + GIT_TEST_INDEX_THREADS= enables exercising the multi-threaded loading of the index for the whole test suite by bypassing the default number of cache entries and thread minimums. Setting this to 1 will make the diff --git a/t/helper/meson.build b/t/helper/meson.build index 3235f10ab8aae1..7c97bfb1e6ec51 100644 --- a/t/helper/meson.build +++ b/t/helper/meson.build @@ -59,6 +59,7 @@ test_tool_sources = [ 'test-rot13-filter.c', 'test-run-command.c', 'test-scrap-cache-tree.c', + 'test-semantic-verify.c', 'test-serve-v2.c', 'test-sha1.c', 'test-sha256.c', diff --git a/t/helper/test-fsmonitor-client.c b/t/helper/test-fsmonitor-client.c index dc1dff23fb8ed5..653d09455382bb 100644 --- a/t/helper/test-fsmonitor-client.c +++ b/t/helper/test-fsmonitor-client.c @@ -8,6 +8,7 @@ #include "test-tool.h" #include "parse-options.h" #include "fsmonitor-ipc.h" +#include "path.h" #include "read-cache-ll.h" #include "repository.h" #include "setup.h" @@ -53,7 +54,7 @@ static int do_send_query(const char *token) if (!token || !*token) token = get_token_from_index(); - ret = fsmonitor_ipc__send_query(token, &answer); + ret = fsmonitor_ipc__send_query(token, &answer, NULL); if (ret < 0) die("could not query fsmonitor--daemon"); @@ -85,6 +86,38 @@ static int do_send_flush(void) return 0; } +static int do_record_watch_limit(void) +{ +#if defined(__linux__) || defined(__APPLE__) + struct strbuf identity = STRBUF_INIT; + struct stat st; + char *path = NULL; + int ret = 1; + + if (fsmonitor_ipc__get_worktree_identity(the_repository, &identity)) { + error("could not identify the fsmonitor worktree"); + goto done; + } + fsmonitor_ipc__record_watch_limit_failure(identity.buf); + path = repo_git_path(the_repository, + "fsmonitor--daemon.inotify-limit"); + if (lstat(path, &st) || !S_ISREG(st.st_mode) || + st.st_uid != geteuid() || st.st_nlink != 1 || + (st.st_mode & 077)) { + error("could not record an owned fsmonitor watch-limit marker"); + goto done; + } + ret = 0; + +done: + free(path); + strbuf_release(&identity); + return ret; +#else + return error("watch-limit markers are not supported on this platform"); +#endif +} + struct hammer_thread_data { pthread_t pthread_id; @@ -109,7 +142,7 @@ static void *hammer_thread_proc(void *_hammer_thread_data) for (k = 0; k < data->nr_requests; k++) { strbuf_reset(&answer); - ret = fsmonitor_ipc__send_query(data->token, &answer); + ret = fsmonitor_ipc__send_query(data->token, &answer, NULL); if (ret < 0) data->sum_errors++; else @@ -189,6 +222,7 @@ int cmd__fsmonitor_client(int argc, const char **argv) const char * const fsmonitor_client_usage[] = { "test-tool fsmonitor-client query []", "test-tool fsmonitor-client flush", + "test-tool fsmonitor-client record-watch-limit", "test-tool fsmonitor-client hammer [] [] []", NULL, }; @@ -218,6 +252,9 @@ int cmd__fsmonitor_client(int argc, const char **argv) if (!strcmp(subcmd, "flush")) return !!do_send_flush(); + if (!strcmp(subcmd, "record-watch-limit")) + return !!do_record_watch_limit(); + if (!strcmp(subcmd, "hammer")) return !!do_hammer(token, nr_threads, nr_requests); diff --git a/t/helper/test-read-cache.c b/t/helper/test-read-cache.c index 6b08ba8f078d00..1a759163d12dfc 100644 --- a/t/helper/test-read-cache.c +++ b/t/helper/test-read-cache.c @@ -1,17 +1,599 @@ #define USE_THE_REPOSITORY_VARIABLE #include "test-tool.h" +#include "attr.h" +#include "attr-fingerprint.h" +#include "attr-manifest.h" +#include "clean-status.h" +#include "clean-status-index.h" +#include "clean-status-internal.h" #include "config.h" +#include "dir.h" #include "environment.h" +#include "ewah/ewok.h" +#include "ewah/ewok_rlw.h" +#include "fsmonitor.h" +#include "fsmonitor-clean-proof.h" +#include "fsmonitor-ll.h" +#include "lockfile.h" #include "read-cache-ll.h" #include "repository.h" #include "setup.h" +#include "strbuf.h" + +static int witness_has_only_entries(const struct index_state *istate) +{ + const unsigned int disk_flags = + CE_STAGEMASK | CE_EXTENDED | CE_VALID | CE_EXTENDED_FLAGS; + + if (!istate->initialized || istate->cache_changed || + istate->name_hash_initialized || istate->cache_tree || + istate->resolve_undo || istate->split_index || + istate->sparse_index != INDEX_EXPANDED || istate->untracked || + istate->clean_status || istate->fsmonitor_dirty || + istate->fsmonitor_last_update || + istate->fsmonitor_last_update_pending || + istate->fsmonitor_untracked_token || + istate->fsmonitor_token_valid || istate->fsmonitor_extension_seen || + istate->fsmonitor_untracked_extension_seen || + istate->fsmonitor_untracked_valid) + return 0; + for (size_t i = 0; i < istate->cache_nr; i++) + if (istate->cache[i]->ce_flags & ~disk_flags) + return 0; + return 1; +} + +static int compare_witness_entries(const struct index_state *witness, + const struct index_state *full) +{ + const unsigned int disk_flags = + CE_STAGEMASK | CE_EXTENDED | CE_VALID | CE_EXTENDED_FLAGS; + + if (witness->version != full->version || + witness->cache_nr != full->cache_nr || + !oideq(&witness->oid, &full->oid) || + witness->timestamp.sec != full->timestamp.sec || + witness->timestamp.nsec != full->timestamp.nsec) + return error("witness index header differs from the full reader"); + for (size_t i = 0; i < witness->cache_nr; i++) { + const struct cache_entry *a = witness->cache[i]; + const struct cache_entry *b = full->cache[i]; + + if (memcmp(&a->ce_stat_data, &b->ce_stat_data, + sizeof(a->ce_stat_data)) || + a->ce_mode != b->ce_mode || + ((a->ce_flags ^ b->ce_flags) & disk_flags) || + !oideq(&a->oid, &b->oid) || + ce_namelen(a) != ce_namelen(b) || + memcmp(a->name, b->name, ce_namelen(a) + 1)) + return error("witness entry %"PRIuMAX" differs from the full reader", + (uintmax_t)i); + } + return 0; +} + +static int test_read_index_witness(const char *path, int compare, + int unlink_after_open, int expect_miss) +{ + struct index_state witness = INDEX_STATE_INIT(the_repository); + struct index_state full = INDEX_STATE_INIT(the_repository); + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + struct stat st; + int flags = O_RDONLY | O_CLOEXEC; + int fd = -1, ret = 1, read_result; + + setup_git_directory(the_repository); + repo_config(the_repository, git_default_config, NULL); +#ifdef O_NONBLOCK + flags |= O_NONBLOCK; +#else + /* The parser is still useful for regular-file fixtures on this platform. */ + if (lstat(path, &st) || !S_ISREG(st.st_mode)) { + ret = !expect_miss; + goto done; + } +#endif + fd = open_nofollow(path, flags); + if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode)) { + ret = !expect_miss; + goto done; + } + if (lseek(fd, 1, SEEK_SET) != 1) + goto done; + if (unlink_after_open && + (clean_status_index_snapshot_open_allow_null_checksum( + &snapshot, path, the_repository->hash_algo) || + unlink(path))) + goto done; + read_result = read_index_entries_from_fd(&witness, fd); + if (fstat(fd, &st) || lseek(fd, 0, SEEK_CUR) != 1) { + error("witness reader consumed its borrowed descriptor"); + goto done; + } + if (read_result) { + if (witness.initialized || witness.cache || witness.cache_nr || + witness.ce_mem_pool) { + error("failed witness read published partial state"); + goto done; + } + ret = !expect_miss; + goto done; + } + if (expect_miss) { + error("invalid witness was accepted"); + goto done; + } + if (!witness_has_only_entries(&witness)) { + error("witness reader installed non-entry state"); + goto done; + } + if (unlink_after_open && + clean_status_index_snapshot_still_matches_path( + &snapshot, path, the_repository->hash_algo)) { + error("unlinked witness retained its named snapshot"); + goto done; + } + if (compare) { + do_read_index(&full, path, 1); + if (compare_witness_entries(&witness, &full)) + goto done; + } + ret = 0; + +done: + if (fd >= 0) + close(fd); + clean_status_index_snapshot_release(&snapshot); + release_index(&full); + release_index(&witness); + return ret; +} + +static int test_index_witness_snapshot(const char *path) +{ + struct clean_status_index_snapshot snapshot = { .fd = -1 }; + int ret; + + setup_git_directory(the_repository); + ret = clean_status_index_snapshot_open_allow_null_checksum( + &snapshot, path, the_repository->hash_algo); + clean_status_index_snapshot_release(&snapshot); + return !!ret; +} + +static int test_fsmonitor_content_recovery(const char *path) +{ + struct index_state *istate; + struct cache_entry *ce; + struct stat_data empty = { 0 }; + struct stat st; + int pos; + + setup_git_directory(the_repository); + repo_config(the_repository, git_default_config, NULL); + if (repo_read_index(the_repository) < 0) + return error("unable to read test index"); + istate = the_repository->index; + pos = index_name_pos(istate, path, strlen(path)); + if (pos < 0) + return error("path is not indexed: %s", path); + ce = istate->cache[pos]; + if (lstat(path, &st)) + return error_errno("unable to stat indexed path"); + + fsmonitor_invalidate_cache_entry(ce); + if (memcmp(&ce->ce_stat_data, &empty, sizeof(empty))) + return error("invalidation did not poison cached stat data"); + if (ie_match_stat_with_content_check(istate, ce, &st, 0)) + return error("clean content did not match"); + if (!memcmp(&ce->ce_stat_data, &empty, sizeof(empty))) + return error("verified clean entry retained poisoned stat data"); + if (!(ce->ce_flags & CE_UPDATE_IN_BASE) || + !(istate->cache_changed & CE_ENTRY_CHANGED)) + return error("verified stat refresh was not marked for persistence"); + return 0; +} + +static int fsuc_failed_closed(const struct index_state *istate) +{ + return istate->fsmonitor_untracked_extension_seen && + istate->fsmonitor_untracked_extension_invalid && + !istate->fsmonitor_untracked_token; +} + +static int test_fsuc_parser(void) +{ + struct index_state duplicate = INDEX_STATE_INIT(the_repository); + struct index_state truncated = INDEX_STATE_INIT(the_repository); + struct untracked_cache untracked = { 0 }; + struct untracked_cache_dir root = { 0 }; + struct untracked_cache_dir child = { 0 }; + struct untracked_cache_dir *dirs[] = { &child }; + struct strbuf encoded = STRBUF_INIT; + struct strbuf written = STRBUF_INIT; + uint32_t version; + + put_be32(&version, 1); + strbuf_add(&encoded, &version, sizeof(version)); + strbuf_addstr(&encoded, "token"); + strbuf_addch(&encoded, '\0'); + read_fsmonitor_untracked_extension( + &duplicate, encoded.buf, encoded.len); + if (duplicate.fsmonitor_untracked_extension_invalid || + !duplicate.fsmonitor_untracked_token || + strcmp(duplicate.fsmonitor_untracked_token, "token")) + return error("valid FSUC was not published"); + + duplicate.fsmonitor_last_update = xstrdup("token"); + write_fsmonitor_untracked_extension(&written, &duplicate); + if (written.len != encoded.len || + memcmp(written.buf, encoded.buf, encoded.len)) + return error("FSUC did not round-trip"); + duplicate.fsmonitor_token_valid = 1; + duplicate.untracked = &untracked; + untracked.root = &root; + root.valid = child.valid = 1; + root.dirs = dirs; + root.dirs_nr = ARRAY_SIZE(dirs); + prepare_fsmonitor_untracked(&duplicate); + if (!duplicate.fsmonitor_untracked_valid) + return error("matching FSMN and FSUC tokens were not paired"); + if (!root.valid_recursive || !child.valid_recursive) + return error("matching FSUC did not restore recursive validity"); + free(duplicate.fsmonitor_last_update); + duplicate.fsmonitor_last_update = xstrdup("other"); + prepare_fsmonitor_untracked(&duplicate); + if (duplicate.fsmonitor_untracked_valid) + return error("mismatched FSMN and FSUC tokens were paired"); + read_fsmonitor_untracked_extension( + &duplicate, encoded.buf, encoded.len); + if (!fsuc_failed_closed(&duplicate)) + return error("duplicate FSUC did not fail closed"); + + truncated.fsmonitor_untracked_token = xstrdup("old"); + read_fsmonitor_untracked_extension( + &truncated, encoded.buf, sizeof(version)); + if (!fsuc_failed_closed(&truncated)) + return error("truncated FSUC was partially published"); + + free(duplicate.fsmonitor_last_update); + strbuf_release(&written); + strbuf_release(&encoded); + return 0; +} + +static void wrap_fsmn_ewah(struct strbuf *out, const struct strbuf *ewah) +{ + uint32_t value; + + put_be32(&value, 2); + strbuf_add(out, &value, sizeof(value)); + strbuf_addstr(out, "token"); + strbuf_addch(out, '\0'); + put_be32(&value, ewah->len); + strbuf_add(out, &value, sizeof(value)); + strbuf_addbuf(out, ewah); +} + +static void make_valid_fsmn(struct strbuf *out) +{ + struct ewah_bitmap *dirty = ewah_new(); + struct strbuf ewah = STRBUF_INIT; + + ewah_set(dirty, 0); + ewah_serialize_strbuf(dirty, &ewah); + wrap_fsmn_ewah(out, &ewah); + ewah_free(dirty); + strbuf_release(&ewah); +} + +static void make_raw_fsmn(struct strbuf *out, uint32_t bit_size, + const eword_t *words, uint32_t word_count, + uint32_t rlw) +{ + struct strbuf ewah = STRBUF_INIT; + uint32_t value; + uint32_t i; + + put_be32(&value, bit_size); + strbuf_add(&ewah, &value, sizeof(value)); + put_be32(&value, word_count); + strbuf_add(&ewah, &value, sizeof(value)); + for (i = 0; i < word_count; i++) { + eword_t word = htonll(words[i]); + + strbuf_add(&ewah, &word, sizeof(word)); + } + put_be32(&value, rlw); + strbuf_add(&ewah, &value, sizeof(value)); + wrap_fsmn_ewah(out, &ewah); + strbuf_release(&ewah); +} + +static int fsmn_failed_closed(const struct index_state *istate) +{ + return istate->fsmonitor_extension_seen && + !istate->fsmonitor_last_update && !istate->fsmonitor_dirty && + !istate->fsmonitor_token_valid; +} + +static int check_invalid_fsmn(const struct strbuf *encoded, + const char *description) +{ + struct index_state invalid = INDEX_STATE_INIT(the_repository); + + invalid.cache_nr = 1; + invalid.fsmonitor_last_update = xstrdup("old"); + invalid.fsmonitor_dirty = ewah_new(); + invalid.fsmonitor_token_valid = 1; + read_fsmonitor_extension(&invalid, encoded->buf, encoded->len); + if (!fsmn_failed_closed(&invalid)) + return error("%s FSMN was published", description); + return 0; +} + +static int test_fsmn_bitmap_ownership(const struct strbuf *encoded) +{ + struct index_state parsed = INDEX_STATE_INIT(the_repository); + struct index_state regenerated = INDEX_STATE_INIT(the_repository); + + parsed.cache_nr = 1; + read_fsmonitor_extension(&parsed, encoded->buf, encoded->len); + if (!parsed.fsmonitor_token_valid || !parsed.fsmonitor_dirty) + return error("raw FSMN bitmap was not published"); + parsed.cache_nr = 0; + release_index(&parsed); + + fill_fsmonitor_bitmap(®enerated); + if (!regenerated.fsmonitor_dirty) + return error("initial FSMN bitmap was not published"); + fill_fsmonitor_bitmap(®enerated); + if (!regenerated.fsmonitor_dirty) + return error("regenerated FSMN bitmap did not replace its owner"); + release_index(®enerated); + + if (parsed.fsmonitor_dirty || regenerated.fsmonitor_dirty) + return error("released index retained its FSMN bitmap"); + return 0; +} + +static int test_fsmn_parser(void) +{ + struct index_state duplicate = INDEX_STATE_INIT(the_repository); + struct index_state truncated = INDEX_STATE_INIT(the_repository); + struct strbuf encoded = STRBUF_INIT; + struct strbuf malformed = STRBUF_INIT; + eword_t words[2] = { 0 }; + + duplicate.cache_nr = truncated.cache_nr = 1; + make_valid_fsmn(&encoded); + read_fsmonitor_extension(&duplicate, encoded.buf, encoded.len); + if (!duplicate.fsmonitor_token_valid || + !duplicate.fsmonitor_last_update || + strcmp(duplicate.fsmonitor_last_update, "token") || + !duplicate.fsmonitor_dirty) + return error("valid FSMN was not published"); + read_fsmonitor_extension(&duplicate, encoded.buf, encoded.len); + if (!fsmn_failed_closed(&duplicate)) + return error("duplicate FSMN did not fail closed"); + + truncated.fsmonitor_last_update = xstrdup("old"); + truncated.fsmonitor_dirty = ewah_new(); + truncated.fsmonitor_token_valid = 1; + read_fsmonitor_extension(&truncated, encoded.buf, encoded.len - 1); + if (!fsmn_failed_closed(&truncated)) + return error("truncated FSMN was partially published"); + + rlw_set_literal_words(&words[0], 1); + make_raw_fsmn(&malformed, 1, words, 1, 0); + if (check_invalid_fsmn(&malformed, "out-of-bounds literal")) + return 1; + strbuf_reset(&malformed); + + words[0] = 0; + rlw_set_run_bit(&words[0], 1); + rlw_set_running_len(&words[0], 1); + make_raw_fsmn(&malformed, 1, words, 1, 0); + if (check_invalid_fsmn(&malformed, "oversized set-bit run")) + return 1; + strbuf_reset(&malformed); + + words[0] = words[1] = 0; + rlw_set_literal_words(&words[0], 1); + words[1] = 2; + make_raw_fsmn(&malformed, 1, words, 2, 0); + if (check_invalid_fsmn(&malformed, "set padding bit")) + return 1; + strbuf_reset(&malformed); + + words[1] = 1; + make_raw_fsmn(&malformed, 1, words, 2, 1); + if (check_invalid_fsmn(&malformed, "non-final RLW")) + return 1; + if (test_fsmn_bitmap_ownership(&encoded)) + return 1; + + strbuf_release(&malformed); + strbuf_release(&encoded); + return 0; +} + +static int write_test_index(void) +{ + struct lock_file index_lock = LOCK_INIT; + + repo_hold_locked_index(the_repository, &index_lock, LOCK_DIE_ON_ERROR); + if (write_locked_index(the_repository->index, &index_lock, COMMIT_LOCK)) + return error("unable to write test index"); + return 0; +} + +static int test_fscf_history_is_coherent(const struct index_state *istate) +{ + const struct clean_status_state *state = istate->clean_status; + + return state && state->disk_config_valid && + !state->disk_config_invalid && state->disk_semantic_valid && + state->disk_attr_valid && state->manifest.disk_valid && + (state->manifest.disk_flags & FSMONITOR_CLEAN_PROOF_ALL) == + FSMONITOR_CLEAN_PROOF_ALL && + state->disk_config_raw.len && state->initial_coherent; +} + +static int test_fscf_config(const char *key, const char *value, + const struct config_context *ctx, void *cb) +{ + struct clean_status_config_digest *config = cb; + + clean_status_config_add(config, key, value, ctx); + return git_default_config(key, value, ctx, NULL); +} + +static int test_fscf_history(void) +{ + struct clean_status_config_digest config; + struct attr_fingerprint attrs; + struct attr_manifest_writer writer; + struct strbuf manifest = STRBUF_INIT; + struct strbuf encoded = STRBUF_INIT; + unsigned char index_hash[GIT_MAX_RAWSZ] = { 0 }; + const char *token; + struct fsmonitor_clean_proof proof = { + .flags = FSMONITOR_CLEAN_PROOF_ALL, + }; + const struct git_hash_algo *algo; + int ret = 1; + + setup_git_directory(the_repository); + algo = the_repository->hash_algo; + clean_status_config_init(&config, algo); + repo_config(the_repository, test_fscf_config, &config); + clean_status_config_final(&config); + clean_status_set_config_digest(the_repository, &config); + if (repo_read_index(the_repository) < 0) + return error("unable to read test index"); + token = "fscf-test-token"; + if (attr_fingerprint_repository(the_repository, &attrs)) + return error("unable to fingerprint attribute sources"); + + attr_manifest_writer_init(&writer, &manifest, algo); + if (attr_manifest_writer_add(&writer, ".gitattributes", + ATTR_MANIFEST_INDEX, index_hash)) + return error("unable to write test attribute manifest"); + proof.config_hash = config.hash; + proof.semantic_hash = config.semantic_hash; + proof.attr_hash = attrs.content_hash; + proof.token = (const unsigned char *)token; + proof.token_len = strlen(token); + proof.attr_manifest = (const unsigned char *)manifest.buf; + proof.attr_manifest_len = manifest.len; + if (fsmonitor_clean_proof_write(&encoded, &proof, algo)) + return error("unable to write test clean proof"); + + FREE_AND_NULL(the_repository->index->fsmonitor_last_update); + the_repository->index->fsmonitor_last_update = xstrdup(token); + the_repository->index->fsmonitor_token_valid = 1; + clean_status_read_fsmonitor_config(the_repository->index, + encoded.buf, encoded.len); + clean_status_prepare_fsmonitor_config(the_repository->index); + if (!test_fscf_history_is_coherent(the_repository->index)) + return error("test clean proof was not coherent"); + if (write_test_index()) + goto done; + + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) + return error("unable to reread test index"); + if (!test_fscf_history_is_coherent(the_repository->index)) + return error("FSCF did not survive an index round trip"); + + clean_status_invalidate_current_manifest(the_repository->index); + if (write_test_index()) + goto done; + discard_index(the_repository->index); + if (repo_read_index(the_repository) < 0) + return error("unable to reread preserved test index"); + if (clean_status_has_persistent_fsmonitor_semantic_history( + the_repository->index)) + return error("generic rewrite retained FSCF epoch bindings"); + if (!clean_status_has_worktree_manifest_history(the_repository->index)) + return error("generic rewrite discarded FSCF manifest history"); + ret = 0; + +done: + strbuf_release(&encoded); + strbuf_release(&manifest); + return ret; +} + +static int test_fsmonitor_directory_attributes(void) +{ + struct attr_check *check; + int ret = 1; + + setup_git_directory(the_repository); + repo_config(the_repository, git_default_config, NULL); + if (repo_read_index(the_repository) < 0) + return error("unable to read test index"); + + check = attr_check_initl("marker", NULL); + git_check_attr(the_repository->index, "tracked-dir/tracked", check); + if (!check->items[0].value || + strcmp(check->items[0].value, "old")) { + error("initial attribute value was not cached"); + goto done; + } + + write_file("tracked-dir/.gitattributes", "tracked marker=new\n"); + /* + * repo_read_index() consumed the normal refresh. Re-arm it after + * caching the pre-event attribute value. + */ + the_repository->index->fsmonitor_has_run_once = 0; + refresh_fsmonitor(the_repository->index); + git_check_attr(the_repository->index, "tracked-dir/tracked", check); + if (!check->items[0].value || + strcmp(check->items[0].value, "new")) { + error("directory event did not invalidate cached attributes"); + goto done; + } + ret = 0; + +done: + attr_check_free(check); + discard_index(the_repository->index); + return ret; +} int cmd__read_cache(int argc, const char **argv) { int i, cnt = 1; const char *name = NULL; + if (argc == 3 && !strcmp(argv[1], "--read-index-witness")) + return test_read_index_witness(argv[2], 0, 0, 0); + if (argc == 3 && !strcmp(argv[1], "--expect-index-witness-miss")) + return test_read_index_witness(argv[2], 0, 0, 1); + if (argc == 3 && !strcmp(argv[1], "--compare-index-witness")) + return test_read_index_witness(argv[2], 1, 0, 0); + if (argc == 3 && !strcmp(argv[1], "--read-index-witness-unlink")) + return test_read_index_witness(argv[2], 0, 1, 0); + if (argc == 3 && !strcmp(argv[1], "--index-witness-snapshot")) + return test_index_witness_snapshot(argv[2]); + if (argc == 3 && + !strcmp(argv[1], "--test-fsmonitor-content-recovery")) + return test_fsmonitor_content_recovery(argv[2]); + if (argc == 2 && !strcmp(argv[1], "--test-fsuc-parser")) + return test_fsuc_parser(); + if (argc == 2 && !strcmp(argv[1], "--test-fsmn-parser")) + return test_fsmn_parser(); + if (argc == 2 && !strcmp(argv[1], "--test-fscf-round-trip")) + return test_fscf_history(); + if (argc == 2 && + !strcmp(argv[1], "--test-fsmonitor-directory-attributes")) + return test_fsmonitor_directory_attributes(); + if (argc > 1 && skip_prefix(argv[1], "--print-and-refresh=", &name)) { argc--; argv++; diff --git a/t/helper/test-semantic-verify.c b/t/helper/test-semantic-verify.c new file mode 100644 index 00000000000000..a868eb80fc4be2 --- /dev/null +++ b/t/helper/test-semantic-verify.c @@ -0,0 +1,146 @@ +#define USE_THE_REPOSITORY_VARIABLE + +#include "test-tool.h" +#include "config.h" +#include "parse-options.h" +#include "read-cache-ll.h" +#include "repository.h" +#include "semantic-verify.h" +#include "setup.h" + +static const char *kind_name(enum semantic_verify_kind kind) +{ + switch (kind) { + case SEMANTIC_VERIFY_UNCHECKED: + return "unchecked"; + case SEMANTIC_VERIFY_SKIPPED: + return "skipped"; + case SEMANTIC_VERIFY_RAW_CLEAN: + return "raw-clean"; + case SEMANTIC_VERIFY_RAW_MODIFIED: + return "raw-modified"; + case SEMANTIC_VERIFY_SENSITIVE: + return "sensitive"; + case SEMANTIC_VERIFY_STRUCTURAL: + return "structural"; + case SEMANTIC_VERIFY_UNSTABLE: + return "unstable"; + case SEMANTIC_VERIFY_ERROR: + return "error"; + } + BUG("unknown semantic verification kind"); +} + +int cmd__semantic_verify(int argc, const char **argv) +{ + struct semantic_verify_options options = SEMANTIC_VERIFY_OPTIONS_INIT; + struct semantic_verify_proof *proof = NULL; + struct semantic_verify_stats stats; + int thread_count = 0; + int show_results = 0; + int apply = 0; + int validate_filter_scope = 0; + int applied = -2; + int before_uptodate = 0, after_uptodate = 0; + int before_valid = 0, after_valid = 0; + int ret; + const char *replace_after_prepare = NULL; + const char * const usage[] = { + "test-tool semantic-verify []", + NULL + }; + struct option opts[] = { + OPT_INTEGER(0, "threads", &thread_count, + "number of verifier threads"), + OPT_BOOL(0, "show-results", &show_results, + "show one result per cache entry"), + OPT_BOOL(0, "apply", &apply, "apply the completed proof"), + OPT_BOOL(0, "validate-filter-scope", &validate_filter_scope, + "classify filter use for every index entry"), + OPT_STRING(0, "replace-after-prepare", &replace_after_prepare, + "path", "replace an entry after preparing the proof"), + OPT_END() + }; + + argc = parse_options(argc, argv, NULL, opts, usage, 0); + if (argc) + usage_with_options(usage, opts); + if (thread_count < 0) + die("negative semantic verifier thread count"); + options.nr_threads = thread_count; + options.validate_filter_scope = validate_filter_scope; + + setup_git_directory(the_repository); + repo_config(the_repository, git_default_config, NULL); + prepare_repo_settings(the_repository); + the_repository->settings.command_requires_full_index = 0; + if (repo_read_index(the_repository) < 0) + die("unable to read index"); + ret = semantic_verify_prepare(the_repository->index, &options, &proof); + semantic_verify_get_stats(proof, &stats); + if (replace_after_prepare) { + struct index_state *istate = the_repository->index; + struct cache_entry *replacement; + int pos = index_name_pos(istate, replace_after_prepare, + strlen(replace_after_prepare)); + + if (pos < 0) + die("%s not in index", replace_after_prepare); + replacement = dup_cache_entry(istate->cache[pos], istate); + replacement->oid.hash[0] ^= 1; + replacement->ce_flags &= + ~(CE_UPTODATE | CE_FSMONITOR_VALID); + if (add_index_entry(istate, replacement, + ADD_CACHE_OK_TO_REPLACE | + ADD_CACHE_KEEP_CACHE_TREE)) + die("unable to replace %s", replace_after_prepare); + } + for (size_t i = 0; i < stats.cache_nr; i++) { + struct cache_entry *ce = the_repository->index->cache[i]; + + before_uptodate += !!ce_uptodate(ce); + before_valid += !!(ce->ce_flags & CE_FSMONITOR_VALID); + } + if (show_results) { + for (size_t i = 0; i < stats.cache_nr; i++) { + const struct semantic_verify_result *result = + semantic_verify_result_at(proof, i); + + printf("%s %s persist=%d error=%u\n", + the_repository->index->cache[i]->name, + kind_name(result->kind), + !!(result->flags & SEMANTIC_VERIFY_PERSISTABLE), + result->error); + } + } + if (apply) + applied = semantic_verify_apply_after_closure( + the_repository->index, proof); + for (size_t i = 0; i < stats.cache_nr; i++) { + struct cache_entry *ce = the_repository->index->cache[i]; + + after_uptodate += !!ce_uptodate(ce); + after_valid += !!(ce->ce_flags & CE_FSMONITOR_VALID); + } + printf("entries=%"PRIuMAX" clean=%"PRIuMAX + " modified=%"PRIuMAX" sensitive=%"PRIuMAX + " structural=%"PRIuMAX" unstable=%"PRIuMAX + " errors=%"PRIuMAX" hardlinks=%"PRIuMAX + " bytes=%"PRIuMAX" stat_updates=%"PRIuMAX + " root_stable=%d namespace_stable=%d applied=%d" + " before_uptodate=%d before_valid=%d" + " after_uptodate=%d after_valid=%d" + " active_filters=%"PRIuMAX" filter_scope_checked=%d\n", + (uintmax_t)stats.cache_nr, (uintmax_t)stats.raw_clean, + (uintmax_t)stats.raw_modified, (uintmax_t)stats.sensitive, + (uintmax_t)stats.structural, (uintmax_t)stats.unstable, + (uintmax_t)stats.errors, (uintmax_t)stats.hardlinks, + (uintmax_t)stats.bytes_hashed, + (uintmax_t)stats.stat_updates_nr, + semantic_verify_root_is_stable(proof), + !stats.namespace_unstable, applied, + before_uptodate, before_valid, after_uptodate, after_valid, + (uintmax_t)stats.active_filters, stats.filter_scope_checked); + semantic_verify_proof_clear(proof); + return !!ret; +} diff --git a/t/helper/test-simple-ipc.c b/t/helper/test-simple-ipc.c index 442ad6b16f18d8..a7e4750fc9be2a 100644 --- a/t/helper/test-simple-ipc.c +++ b/t/helper/test-simple-ipc.c @@ -3,6 +3,7 @@ */ #include "test-tool.h" +#include "fsmonitor-ipc.h" #include "gettext.h" #include "simple-ipc.h" #include "parse-options.h" @@ -159,9 +160,89 @@ static int app__sendbytes_command(const char *received, size_t received_len, * data is handled properly. */ static int my_app_data = 42; +static int fsmonitor_legacy; +static int fsmonitor_capability_superset; +static int fsmonitor_pre_dir_metadata; +static int fsmonitor_pre_cookie_retirement; +static int fsmonitor_unmarked_response; +static int fsmonitor_disconnect_first; static ipc_server_application_cb test_app_cb; +static int app__fsmonitor_capability_superset( + const char *command, size_t command_len, + ipc_server_reply_cb *reply_cb, + struct ipc_server_reply_data *reply_data) +{ + static const char capability_command[] = "get-capabilities"; + static const char capabilities[] = + FSMONITOR_IPC_QUERY_VERSION "\n" + FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_CAPABILITY "\n" +#ifdef __APPLE__ + FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" +#endif + ; + static const char pre_cookie_capabilities[] = + FSMONITOR_IPC_QUERY_VERSION "\n" +#ifdef __APPLE__ + FSMONITOR_IPC_HARDLINK_QUERY_VERSION "\n" + FSMONITOR_IPC_DIR_METADATA_CAPABILITY "\n" + FSMONITOR_IPC_HARDLINK_INODE_CAPABILITY "\n" +#endif + ; + static const char pre_dir_metadata_capabilities[] = + FSMONITOR_IPC_QUERY_VERSION "\n"; + static const char current_token[] = + "builtin:" +#ifdef __APPLE__ + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX +#endif + FSMONITOR_IPC_COOKIE_TOKEN_RETIREMENT_PREFIX "test-capable:0"; + static const char old_token[] = + "builtin:" +#ifdef __APPLE__ + FSMONITOR_IPC_HARDLINK_INODE_TOKEN_PREFIX +#endif + "test-pre-cookie:0"; + const char *token; + const char *query; + size_t token_len, query_len; + int ret; + + trace2_data_string("fsmonitor", NULL, "request", command); + + if (command_len == sizeof(capability_command) - 1 && + !memcmp(command, capability_command, command_len)) { + if (fsmonitor_pre_dir_metadata) + return reply_cb(reply_data, + pre_dir_metadata_capabilities, + sizeof(pre_dir_metadata_capabilities) - 1); + if (fsmonitor_pre_cookie_retirement) + return reply_cb(reply_data, + pre_cookie_capabilities, + sizeof(pre_cookie_capabilities) - 1); + return reply_cb(reply_data, capabilities, + sizeof(capabilities) - 1); + } + + token = fsmonitor_pre_dir_metadata || + fsmonitor_pre_cookie_retirement || fsmonitor_unmarked_response ? + old_token : current_token; + token_len = strlen(token); + query = memchr(command, '\n', command_len); + query_len = query ? command_len - (query + 1 - command) : 0; + ret = reply_cb(reply_data, token, token_len + 1); + if (!ret && + ((!starts_with(command, "query-v1 ") && + !starts_with(command, "query-v2 ")) || + query_len != token_len || + memcmp(query + 1, token, query_len))) + ret = reply_cb(reply_data, "/", 2); + return ret; +} + /* * This is the "application callback" that sits on top of the * "ipc-server". It completely defines the set of commands supported @@ -181,6 +262,12 @@ static int test_app_cb(void *application_data, if (application_data != (void*)&my_app_data) BUG("application_cb: application_data pointer wrong"); + /* Exit before the server can flush a response to this bound query. */ + if (fsmonitor_disconnect_first && + (starts_with(command, "query-v1 ") || + starts_with(command, "query-v2 "))) + _exit(0); + if (command_len == 4 && !strncmp(command, "quit", 4)) { /* * The client sent a "quit" command. This is an async @@ -201,6 +288,21 @@ static int test_app_cb(void *application_data, return SIMPLE_IPC_QUIT; } + if (fsmonitor_capability_superset || fsmonitor_pre_dir_metadata || + fsmonitor_pre_cookie_retirement || fsmonitor_unmarked_response) + return app__fsmonitor_capability_superset( + command, command_len, reply_cb, reply_data); + + if (fsmonitor_legacy) { + static const char token[] = "builtin:test-legacy:0"; + int ret; + + ret = reply_cb(reply_data, token, sizeof(token)); + if (!ret && !starts_with(command, "builtin:")) + ret = reply_cb(reply_data, "/", 2); + return ret; + } + if (command_len == 4 && !strncmp(command, "ping", 4)) { const char *answer = "pong"; return reply_cb(reply_data, answer, strlen(answer)); @@ -310,6 +412,18 @@ static int daemon__start_server(void) strvec_push(&cp.args, "run-daemon"); strvec_pushf(&cp.args, "--name=%s", cl_args.path); strvec_pushf(&cp.args, "--threads=%d", cl_args.nr_threads); + if (fsmonitor_legacy) + strvec_push(&cp.args, "--fsmonitor-legacy"); + if (fsmonitor_capability_superset) + strvec_push(&cp.args, "--fsmonitor-capability-superset"); + if (fsmonitor_pre_dir_metadata) + strvec_push(&cp.args, "--fsmonitor-pre-dir-metadata"); + if (fsmonitor_pre_cookie_retirement) + strvec_push(&cp.args, "--fsmonitor-pre-cookie-retirement"); + if (fsmonitor_unmarked_response) + strvec_push(&cp.args, "--fsmonitor-unmarked-response"); + if (fsmonitor_disconnect_first) + strvec_push(&cp.args, "--fsmonitor-disconnect-first"); cp.no_stdin = 1; cp.no_stdout = 1; @@ -602,6 +716,23 @@ int cmd__simple_ipc(int argc, const char **argv) OPT_INTEGER(0, "bytecount", &cl_args.bytecount, N_("number of bytes")), OPT_INTEGER(0, "batchsize", &cl_args.batchsize, N_("number of requests per thread")), + OPT_BOOL(0, "fsmonitor-legacy", &fsmonitor_legacy, + N_("emulate the legacy fsmonitor query protocol")), + OPT_BOOL(0, "fsmonitor-capability-superset", + &fsmonitor_capability_superset, + N_("advertise multiple fsmonitor query versions")), + OPT_BOOL(0, "fsmonitor-pre-dir-metadata", + &fsmonitor_pre_dir_metadata, + N_("emulate a daemon without directory metadata filtering")), + OPT_BOOL(0, "fsmonitor-pre-cookie-retirement", + &fsmonitor_pre_cookie_retirement, + N_("emulate a daemon without failed-cookie token retirement")), + OPT_BOOL(0, "fsmonitor-unmarked-response", + &fsmonitor_unmarked_response, + N_("advertise token retirement but return an unmarked token")), + OPT_BOOL(0, "fsmonitor-disconnect-first", + &fsmonitor_disconnect_first, + N_("disconnect while handling the first fsmonitor query")), /* * The "byte" string here is not marked for translation and diff --git a/t/helper/test-tool.c b/t/helper/test-tool.c index b71a22b43bbc9e..5ccf3864beb235 100644 --- a/t/helper/test-tool.c +++ b/t/helper/test-tool.c @@ -70,6 +70,7 @@ static struct test_cmd cmds[] = { { "revision-walking", cmd__revision_walking }, { "run-command", cmd__run_command }, { "scrap-cache-tree", cmd__scrap_cache_tree }, + { "semantic-verify", cmd__semantic_verify }, { "serve-v2", cmd__serve_v2 }, { "sha1", cmd__sha1 }, { "sha1-is-sha1dc", cmd__sha1_is_sha1dc }, diff --git a/t/helper/test-tool.h b/t/helper/test-tool.h index f2885b33d58aa8..d1044198247ae4 100644 --- a/t/helper/test-tool.h +++ b/t/helper/test-tool.h @@ -63,6 +63,7 @@ int cmd__repository(int argc, const char **argv); int cmd__revision_walking(int argc, const char **argv); int cmd__run_command(int argc, const char **argv); int cmd__scrap_cache_tree(int argc, const char **argv); +int cmd__semantic_verify(int argc, const char **argv); int cmd__serve_v2(int argc, const char **argv); int cmd__sha1(int argc, const char **argv); int cmd__sha1_is_sha1dc(int argc, const char **argv); diff --git a/t/lib-semantic-verify.sh b/t/lib-semantic-verify.sh new file mode 100644 index 00000000000000..b46fd064711daf --- /dev/null +++ b/t/lib-semantic-verify.sh @@ -0,0 +1,9 @@ +test_lazy_prereq SEMANTIC_VERIFY_ANCHORED_OPEN ' + test_create_repo semantic-anchored-open-probe && + test_commit -C semantic-anchored-open-probe base tracked && + ( + cd semantic-anchored-open-probe && + test-tool semantic-verify --show-results >actual && + test_grep "^tracked raw-clean " actual + ) +' diff --git a/t/meson.build b/t/meson.build index a25f37d2f5ae7d..b5aa03844cbff8 100644 --- a/t/meson.build +++ b/t/meson.build @@ -1,7 +1,22 @@ clar_test_suites = [ + 'unit-tests/u-attr-fingerprint.c', + 'unit-tests/u-attr-manifest.c', + 'unit-tests/u-clean-status-config.c', + 'unit-tests/u-clean-status-history-store.c', + 'unit-tests/u-clean-status-history.c', + 'unit-tests/u-clean-status-identity.c', + 'unit-tests/u-clean-status-index.c', + 'unit-tests/u-clean-status-manifest.c', + 'unit-tests/u-clean-status-progress.c', + 'unit-tests/u-clean-status-sidecar.c', + 'unit-tests/u-clean-status-store.c', 'unit-tests/u-ctype.c', 'unit-tests/u-dir.c', 'unit-tests/u-example-decorate.c', + 'unit-tests/u-exclude-source-proof.c', + 'unit-tests/u-fsmonitor-attributes.c', + 'unit-tests/u-fsmonitor-clean-proof.c', + 'unit-tests/u-fsmonitor-response.c', 'unit-tests/u-hash.c', 'unit-tests/u-hashmap.c', 'unit-tests/u-list-objects-filter-options.c', @@ -10,6 +25,8 @@ clar_test_suites = [ 'unit-tests/u-oid-array.c', 'unit-tests/u-oidmap.c', 'unit-tests/u-oidtree.c', + 'unit-tests/u-path-namespace.c', + 'unit-tests/u-preload-index-bulk-darwin.c', 'unit-tests/u-prio-queue.c', 'unit-tests/u-reftable-basics.c', 'unit-tests/u-reftable-block.c', @@ -27,6 +44,7 @@ clar_test_suites = [ 'unit-tests/u-trailer.c', 'unit-tests/u-urlmatch-normalization.c', 'unit-tests/u-utf8-width.c', + 'unit-tests/u-worktree-attr-source.c', ] clar_sources = [ @@ -248,6 +266,7 @@ integration_tests = [ 't1517-outside-repo.sh', 't1600-index.sh', 't1601-index-bogus.sh', + 't1602-index-witness.sh', 't1700-split-index.sh', 't1701-racy-split-index.sh', 't1800-hook.sh', @@ -943,6 +962,15 @@ integration_tests = [ 't7526-commit-pathspec-file.sh', 't7527-builtin-fsmonitor.sh', 't7528-signed-commit-ssh.sh', + 't7529-preload-index-apfs.sh', + 't7530-status-clean-sidecar.sh', + 't7531-semantic-verify.sh', + 't7532-preload-index-linux.sh', + 't7533-status-scoped-stash.sh', + 't7534-status-scoped-readers.sh', + 't7535-fsmonitor-cookie-reset.sh', + 't7536-fsmonitor-watch-limit-backoff.sh', + 't7537-fsmonitor-cookie-compat.sh', 't7600-merge.sh', 't7601-merge-pull-config.sh', 't7602-merge-octopus-many.sh', diff --git a/t/t0003-attributes.sh b/t/t0003-attributes.sh index 582e207aa12eb1..62c55ebd2b040e 100755 --- a/t/t0003-attributes.sh +++ b/t/t0003-attributes.sh @@ -241,6 +241,23 @@ test_expect_success 'core.attributesfile' ' attr_check global precedence ' +test_expect_success 'empty core.attributesfile disables global attributes' ' + test_when_finished "rm -rf empty-global-attributes" && + test_create_repo empty-global-attributes && + ( + cd empty-global-attributes && + echo "global test=global" >.git/global-attributes && + echo "local test=local" >.gitattributes && + git config core.attributesfile "$PWD/.git/global-attributes" && + attr_check global global && + attr_check global unspecified "-c core.attributesFile=" && + attr_check local local "-c core.attributesFile=" && + git -c core.attributesFile= status --porcelain=v1 \ + --untracked-files=no >actual && + test_must_be_empty actual + ) +' + test_expect_success 'attribute test: read paths from stdin' ' grep -v notest expect && sed -e "s/:.*//" actual && diff --git a/t/t0008-ignores.sh b/t/t0008-ignores.sh index ed95faf3272e60..949897c36aa9b2 100755 --- a/t/t0008-ignores.sh +++ b/t/t0008-ignores.sh @@ -959,7 +959,7 @@ test_expect_success EXPENSIVE 'large exclude file ignored in tree' ' test_when_finished "rm .gitignore" && find . -name .gitignore -exec rm "{}" ";" && dd if=/dev/zero of=.gitignore bs=101M count=1 && - git ls-files -o --exclude-standard 2>err && + GIT_ALLOC_LIMIT=1m git ls-files -o --exclude-standard 2>err && echo "warning: ignoring excessively large pattern file: .gitignore" >expect && test_cmp expect err ' diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh index 74b1761e0c8507..48d4d30cae67e7 100755 --- a/t/t1091-sparse-checkout-builtin.sh +++ b/t/t1091-sparse-checkout-builtin.sh @@ -1274,4 +1274,63 @@ test_expect_success 'sparse-checkout operations with merge conflicts' ' ) ' +test_expect_success 'unchanged sparse-checkout commands preserve the index' ' + test_when_finished "rm -rf sparse-unchanged" && + test_create_repo sparse-unchanged && + mkdir sparse-unchanged/included sparse-unchanged/omitted && + test_write_lines included >sparse-unchanged/included/tracked && + test_write_lines omitted >sparse-unchanged/omitted/tracked && + git -C sparse-unchanged add . && + git -C sparse-unchanged commit -qm base && + git -C sparse-unchanged sparse-checkout set included && + for sparse_command in reapply set add + do + case "$sparse_command" in + reapply) set -- reapply ;; + set) set -- set included ;; + add) set -- add included ;; + esac && + cp sparse-unchanged/.git/index \ + "sparse-$sparse_command.index" && + GIT_TRACE2_EVENT="$PWD/sparse-$sparse_command.trace" \ + git -C sparse-unchanged sparse-checkout "$@" && + test_cmp_bin "sparse-$sparse_command.index" \ + sparse-unchanged/.git/index && + test_grep ! "\"label\":\"do_write_index\"" \ + "sparse-$sparse_command.trace" || return 1 + done +' + +test_expect_success 'sparse-checkout preserves hooks and explicit index modes' ' + test_when_finished "rm -rf sparse-hook" && + test_create_repo sparse-hook && + mkdir sparse-hook/included sparse-hook/omitted && + test_write_lines included >sparse-hook/included/tracked && + test_write_lines omitted >sparse-hook/omitted/tracked && + git -C sparse-hook add . && + git -C sparse-hook commit -qm base && + git -C sparse-hook sparse-checkout set included && + mkdir sparse-hook/hooks && + git -C sparse-hook config core.hooksPath hooks && + write_script sparse-hook/hooks/post-index-change <<-\EOF && + printf "%s %s\n" "$1" "$2" >>hook-actual + EOF + GIT_TRACE2_EVENT="$PWD/sparse-hook.trace" \ + git -C sparse-hook sparse-checkout reapply && + test_write_lines "0 0" >sparse-hook.expect && + test_cmp sparse-hook.expect sparse-hook/hook-actual && + test_grep "\"label\":\"do_write_index\"" sparse-hook.trace && + rm sparse-hook/hooks/post-index-change && + GIT_TRACE2_EVENT="$PWD/sparse-collapse.trace" \ + git -C sparse-hook sparse-checkout reapply --sparse-index && + git -C sparse-hook ls-files --sparse >sparse-collapsed && + test_grep "^omitted/$" sparse-collapsed && + test_grep "\"label\":\"do_write_index\"" sparse-collapse.trace && + GIT_TRACE2_EVENT="$PWD/sparse-expand.trace" \ + git -C sparse-hook sparse-checkout reapply --no-sparse-index && + git -C sparse-hook ls-files --sparse >sparse-expanded && + test_grep "^omitted/tracked$" sparse-expanded && + test_grep "\"label\":\"do_write_index\"" sparse-expand.trace +' + test_done diff --git a/t/t1602-index-witness.sh b/t/t1602-index-witness.sh new file mode 100755 index 00000000000000..88c8793bcf035f --- /dev/null +++ b/t/t1602-index-witness.sh @@ -0,0 +1,1007 @@ +#!/bin/sh + +test_description='gentle entries-only reads of optional index witnesses' + +. ./test-lib.sh +. "$TEST_DIRECTORY"/lib-semantic-verify.sh + +test_lazy_prereq UNTRACKED_CACHE ' + { git update-index --test-untracked-cache; ret=$?; } && + test $ret -ne 1 +' + +sane_unset GIT_TEST_SPLIT_INDEX GIT_TEST_INDEX_VERSION \ + GIT_TEST_INDEX_THREADS GIT_TEST_FSMONITOR + +test_expect_success PERL_TEST_HELPERS 'write index fixture generator' ' + cat >make-index.pl <<-\EOF + use strict; + use warnings; + use Digest::SHA qw(sha1 sha256); + binmode STDIN; + binmode STDOUT; + my ($algo, $case) = @ARGV; + my $rawsz = $algo eq "sha256" ? 32 : 20; + my $fixed = 40 + $rawsz + 2; + sub digest { return $rawsz == 32 ? sha256($_[0]) : sha1($_[0]); } + sub varint { + my ($n) = @_; + my @bytes = ($n & 127); + while ($n >>= 7) { unshift @bytes, 128 | (--$n & 127); } + return pack("C*", @bytes); + } + sub entry { + my %o = @_; + my $name = $o{name} // "alpha"; + my $version = $o{version} // 2; + my $len = length($name); + my $flags = ($o{flags} // 0) | + ($o{namelen} // ($len < 0xfff ? $len : 0xfff)); + my $data = pack("N10", 11, 12, 13, 14, 15, 16, + $o{mode} // 0100644, 17, 18, 19) . + ("\x11" x $rawsz) . pack("n", $flags); + $data .= pack("n", $o{extended} // 0) if $flags & 0x4000; + if ($version == 4) { + $data .= varint($o{strip} // 0) . + ($o{suffix} // $name) . "\0"; + } else { + $data .= $name . "\0"; + $data .= "\0" x ((8 - length($data) % 8) % 8); + } + return $data; + } + if ($case eq "strip-proofs" || $case eq "unbind-proof") { + local $/; + my $data = ; + my ($version, $nr) = unpack("NN", substr($data, 4, 8)); + die "expected an uncompressed index\n" if $version < 2 || $version > 3; + my $offset = 12; + for (1 .. $nr) { + my $flags = unpack("n", substr($data, $offset + 40 + $rawsz, 2)); + my $header = $fixed + (($flags & 0x4000) ? 2 : 0); + my $len = $flags & 0xfff; + $len = index($data, "\0", $offset + $header) - $offset - $header + if $len == 0xfff; + die "invalid name\n" if $len < 0; + $offset += ($header + $len + 8) & ~7; + } + my $out = substr($data, 0, $offset); + my $end = length($data) - $rawsz; + my $proof_seen = 0; + while ($offset < $end) { + die "short extension\n" if $end - $offset < 8; + my ($name, $size) = unpack("a4N", substr($data, $offset, 8)); + die "long extension\n" if $size > $end - $offset - 8; + my $body = substr($data, $offset + 8, $size); + $offset += 8 + $size; + next if $name eq "FSUC"; + next if $case eq "strip-proofs" && $name eq "FSCF"; + next if $case eq "unbind-proof" && $name eq "FSMN"; + if ($case eq "unbind-proof" && $name eq "FSCF") { + $proof_seen++; + my $flags = unpack("N", substr($body, 8, 4)); + substr($body, 8, 4, pack("N", $flags & ~6)); + $body = substr($body, 0, -$rawsz); + $body .= digest($body); + } + $out .= pack("a4N", $name, length($body)) . $body; + } + die "missing FSCF extension\n" if $case eq "unbind-proof" && !$proof_seen; + print $out, digest($out); + exit; + } + my $version = 2; + my @entries = (entry()); + my $extra = ""; + my $signature = "DIRC"; + my $count; + if ($case eq "empty") { @entries = (); } + elsif ($case eq "stages") { + @entries = map { entry(flags => $_ << 12) } 1 .. 3; + } + elsif ($case eq "extended") { + $version = 3; + @entries = (entry(version => 3, flags => 0xc000, extended => 0x6000)); + } + elsif ($case eq "compressed") { + $version = 4; + @entries = (entry(version => 4), + entry(version => 4, name => "alphabet", suffix => "bet"), + entry(version => 4, name => "beta", strip => 8)); + } + elsif ($case eq "long-compressed") { + $version = 4; + my $prefix = "long/" . ("a/" x 2100); + @entries = (entry(version => 4, name => $prefix . "one"), + entry(version => 4, name => $prefix . "two", strip => 3, + suffix => "two")); + } + elsif ($case eq "optional-extensions") { + $extra .= pack("a4N", $_, 4) . "junk" + for qw(TREE UNTR FSMN FSCF FSUC IEOT EOIE ZZZZ); + } + elsif ($case eq "high-bit-extension") { + my $size = 12 + $rawsz; + $extra = pack("a4N", "ZZZZ", $size) . ("\x95" x $size); + } + elsif ($case eq "high-bit-signature") { + $extra = pack("a4N", "Z\x95ZZ", 0); + } + elsif ($case eq "bad-signature") { $signature = "NOPE"; } + elsif ($case eq "bad-version") { $version = 5; } + elsif ($case eq "bad-count") { $count = 0xffffffff; } + elsif ($case eq "truncated-header") { + my $data = "DIRC"; + print $data, digest($data); + exit; + } + elsif ($case eq "truncated-fixed") { $entries[0] = substr($entries[0], 0, $fixed - 1); } + elsif ($case eq "truncated-flags") { + $version = 3; + @entries = (substr(entry(version => 3, flags => 0x4000, + extended => 0x4000), 0, $fixed + 1)); + } + elsif ($case eq "unknown-flags") { + $version = 3; + @entries = (entry(version => 3, flags => 0x4000, extended => 1)); + } + elsif ($case eq "v2-extended") { + @entries = (entry(flags => 0x4000, extended => 0x4000)); + } + elsif ($case eq "missing-nul") { + $version = 4; + @entries = (substr(entry(version => 4), 0, -1)); + } + elsif ($case eq "embedded-nul") { @entries = (entry(name => "al\0ha")); } + elsif ($case eq "bad-padding") { + @entries = (entry(name => "ab")); + substr($entries[0], -1, 1, "\1"); + } + elsif ($case eq "truncated-varint" || $case eq "overflow-varint") { + $version = 4; + @entries = (substr(entry(version => 4), 0, $fixed) . + ($case eq "truncated-varint" ? "\x80\x80" : ("\x80" x 10) . "\0")); + } + elsif ($case eq "first-strip") { + $version = 4; + @entries = (entry(version => 4, strip => 1)); + } + elsif ($case eq "excessive-strip") { + $version = 4; + @entries = (entry(version => 4), + entry(version => 4, name => "beta", strip => 6)); + } + elsif ($case eq "short-name") { + $version = 4; + @entries = (entry(version => 4), + entry(version => 4, name => "b", suffix => "b")); + } + elsif ($case eq "short-long-name") { + $version = 4; + @entries = (entry(version => 4, namelen => 0xfff)); + } + elsif ($case eq "unordered") { @entries = (entry(name => "beta"), entry()); } + elsif ($case eq "duplicate-stage") { @entries = (entry(flags => 0x1000)) x 2; } + elsif ($case eq "mixed-stages") { @entries = (entry(), entry(flags => 0x1000)); } + elsif ($case eq "bad-mode") { @entries = (entry(mode => 0100664)); } + elsif ($case eq "empty-name") { @entries = (entry(name => "")); } + elsif ($case eq "absolute-name") { @entries = (entry(name => "/alpha")); } + elsif ($case eq "dotdot-name") { @entries = (entry(name => "a/../b")); } + elsif ($case eq "dotgit-name") { @entries = (entry(name => "a/.GiT/b")); } + elsif ($case eq "sparse-entry") { @entries = (entry(name => "dir/", mode => 0040000)); } + elsif ($case eq "resolve-undo" || $case eq "split-index" || + $case eq "sparse-index" || $case eq "mandatory-extension") { + my %names = ("resolve-undo" => "REUC", "split-index" => "link", + "sparse-index" => "sdir", "mandatory-extension" => "zzzz"); + $extra = pack("a4N", $names{$case}, 0); + } + elsif ($case eq "truncated-extension") { $extra = "FSMN"; } + elsif ($case eq "oversized-extension") { $extra = pack("a4N", "FSMN", 10) . "x"; } + elsif ($case ne "valid" && $case ne "skiphash" && + $case ne "bad-checksum" && $case ne "truncated-trailer") { + die "unknown fixture $case\n"; + } + my $data = $signature . pack("NN", $version, $count // scalar(@entries)) . + join("", @entries) . $extra; + my $checksum = $case eq "skiphash" ? "\0" x $rawsz : digest($data); + substr($checksum, 0, 1, chr(ord(substr($checksum, 0, 1)) ^ 1)) + if $case eq "bad-checksum"; + $checksum = substr($checksum, 0, -1) if $case eq "truncated-trailer"; + print $data, $checksum; + EOF +' + +for algo in sha1 sha256 +do + test_expect_success "$algo writer-produced v2/v3/v4 and skipHash witnesses" ' + git init --object-format="$algo" "$algo" && + git -C "$algo" config core.fsmonitor false && + git -C "$algo" config core.untrackedCache false && + git -C "$algo" config index.threads 1 && + mkdir "$algo/dir" && + test_write_lines alpha >"$algo/dir/alpha" && + test_write_lines alphabet >"$algo/dir/alphabet" && + test_write_lines beta >"$algo/dir/beta" && + git -C "$algo" add dir && + for version in 2 3 4 + do + if test "$version" = 2 + then + git -C "$algo" update-index --no-skip-worktree dir/alpha + else + git -C "$algo" update-index --skip-worktree dir/alpha + fi && + for skip in false true + do + git -C "$algo" -c index.skipHash="$skip" update-index \ + --index-version="$version" --force-write-index && + test "$version" = "$(git -C "$algo" update-index --show-index-version)" && + cp "$algo/.git/index" "$algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --compare-index-witness .git/witness || return 1 + done || return 1 + done + ' + + test_expect_success PTHREADS "$algo v4 IEOT block restarts use the shared decoder" ' + git -C "$algo" config index.threads 3 && + git -C "$algo" -c index.skipHash=false update-index \ + --index-version=4 --force-write-index && + cp "$algo/.git/index" "$algo/.git/witness" && + test_grep IEOT "$algo/.git/witness" && + test_grep EOIE "$algo/.git/witness" && + test-tool -C "$algo" read-cache --compare-index-witness .git/witness && + git -C "$algo" config index.threads 1 + ' + + test_expect_success PERL_TEST_HELPERS "$algo exact entry fields and long compressed names" ' + for kind in valid empty stages extended compressed long-compressed skiphash + do + perl make-index.pl "$algo" "$kind" >"$algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --compare-index-witness .git/witness || return 1 + done + ' + + test_expect_success PERL_TEST_HELPERS "$algo optional extensions are not decoded or installed" ' + perl make-index.pl "$algo" optional-extensions >"$algo/.git/witness" && + test-tool -C "$algo" read-cache --read-index-witness .git/witness + ' + + test_expect_success PERL_TEST_HELPERS "$algo malformed and unsupported witnesses are clean misses" ' + for kind in bad-signature bad-version bad-count bad-checksum \ + truncated-header truncated-fixed truncated-flags unknown-flags \ + v2-extended missing-nul embedded-nul bad-padding \ + truncated-varint overflow-varint first-strip excessive-strip \ + short-name short-long-name unordered duplicate-stage mixed-stages \ + bad-mode empty-name absolute-name dotdot-name dotgit-name \ + sparse-entry resolve-undo split-index sparse-index \ + mandatory-extension truncated-extension oversized-extension \ + truncated-trailer + do + perl make-index.pl "$algo" "$kind" >"$algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --expect-index-witness-miss .git/witness || return 1 + done + ' + + test_expect_success PERL_TEST_HELPERS "$algo real-index corruption remains fatal" ' + for kind in truncated-header unknown-flags excessive-strip + do + perl make-index.pl "$algo" "$kind" >"$algo/.git/witness" && + test_must_fail env GIT_INDEX_FILE="$PWD/$algo/.git/witness" \ + git -C "$algo" ls-files >out 2>err && + test_grep "^fatal:" err || return 1 + done + ' + + test_expect_success PERL_TEST_HELPERS "$algo pinned reader never reopens a pruned pathname" ' + perl make-index.pl "$algo" valid >"$algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --read-index-witness-unlink .git/witness && + test_path_is_missing "$algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --expect-index-witness-miss .git/witness + ' + + test_expect_success PIPE "$algo witness and installer snapshot reject a FIFO" ' + rm -f "$algo/.git/witness" && + mkfifo "$algo/.git/witness" && + test_when_finished "rm -f $algo/.git/witness" && + test-tool -C "$algo" read-cache \ + --expect-index-witness-miss .git/witness && + test_must_fail test-tool -C "$algo" read-cache \ + --index-witness-snapshot .git/witness + ' +done + +test_lazy_prereq INDEX_WITNESS_APFS ' + test_have_prereq MACOS && + /bin/df -t apfs "$TRASH_DIRECTORY" >/dev/null +' + +# Inspect the framed extensions, not an incidental "FSCF" string in the index. +# These fixtures deliberately write v2 indexes with real checksums. Without +# an explicit expected token, require the real Darwin provider used below. +test_index_witness_full_proof () { + perl - "$1" "$(git rev-parse --show-object-format)" "${2-}" <<-\EOF + use strict; + use warnings; + use Digest::SHA qw(sha1 sha256); + my ($path, $algo, $expected_token) = @ARGV; + my $rawsz = $algo eq "sha256" ? 32 : 20; + sub digest { return $rawsz == 32 ? sha256($_[0]) : sha1($_[0]); } + open my $input, "<", $path or die "cannot read $path: $!\n"; + binmode $input; + local $/; + my $index = <$input>; + my $end = length($index) - $rawsz; + die "bad index checksum in $path\n" if $end < 12 || + digest(substr($index, 0, $end)) ne substr($index, $end); + my ($signature, $version, $nr) = unpack("a4NN", substr($index, 0, 12)); + die "expected an uncompressed index in $path\n" + if $signature ne "DIRC" || $version < 2 || $version > 3; + my $offset = 12; + for (1 .. $nr) { + my $fixed = 40 + $rawsz + 2; + die "short index entry in $path\n" if $end - $offset < $fixed; + my $flags = unpack("n", substr($index, $offset + $fixed - 2, 2)); + my $header = $fixed + (($flags & 0x4000) ? 2 : 0); + die "short entry flags in $path\n" if $end - $offset < $header; + my $nul = index($index, "\0", $offset + $header); + my $len = $flags & 0xfff; + die "bad index name in $path\n" if $nul < 0 || $nul >= $end || + ($len != 0xfff && $nul != $offset + $header + $len); + $len = $nul - $offset - $header; + $offset += ($header + $len + 8) & ~7; + die "short index padding in $path\n" if $offset > $end; + } + my %ext; + while ($offset < $end) { + die "short extension in $path\n" if $end - $offset < 8; + my ($name, $size) = unpack("a4N", substr($index, $offset, 8)); + die "bad extension $name in $path\n" + if $size > $end - $offset - 8 || exists $ext{$name}; + $ext{$name} = substr($index, $offset + 8, $size); + $offset += 8 + $size; + } + my $proof = $ext{FSCF} // die "missing FSCF in $path\n"; + die "short FSCF in $path\n" if length($proof) < 20; + my ($pv, $magic, $flags, $token_len, $manifest_len) = + unpack("N5", substr($proof, 0, 20)); + die "incomplete FSCF in $path (version $pv, flags $flags)\n" + if ($pv != 1 && $pv != 2) || $magic != 0x46534331 || + $flags != 15 || !$token_len || + length($proof) != 20 + $token_len + $manifest_len + + ($pv == 2 ? 5 : 4) * $rawsz || + digest(substr($proof, 0, -$rawsz)) ne substr($proof, -$rawsz); + my $token = substr($proof, 20, $token_len); + if (length($expected_token)) { + die "unexpected provider token in $path\n" + if $token ne $expected_token; + } else { + die "not a real builtin token in $path\n" + if $token !~ /^builtin:dirmeta-v1\.inode-v1\./; + } + for my $name (qw(FSMN FSUC)) { + my $body = $ext{$name} // die "missing $name in $path\n"; + my $want_version = $name eq "FSMN" ? 2 : 1; + my $nul = index($body, "\0", 4); + die "unbound $name in $path\n" + if length($body) < 5 || unpack("N", substr($body, 0, 4)) != + $want_version || $nul < 4 || + substr($body, 4, $nul - 4) ne $token; + } + die "missing UNTR in $path\n" if !exists $ext{UNTR}; + print "FSCF version $pv flags $flags token $token\n"; + EOF +} + +test_index_witness_cookie_health () ( + witness_cookie_label=$1 && + GIT_TRACE2_EVENT="$PWD/.git/$witness_cookie_label.cookie-initial.trace" \ + test-tool fsmonitor-client query --token 0 \ + >".git/$witness_cookie_label.cookie-initial" && + nul_to_q <".git/$witness_cookie_label.cookie-initial" \ + >".git/$witness_cookie_label.cookie-initial.q" && + test_grep "^builtin:.*Q/Q$" \ + ".git/$witness_cookie_label.cookie-initial.q" && + witness_cookie_token=$(sed "s/Q.*//" \ + ".git/$witness_cookie_label.cookie-initial.q") && + # A failed startup cookie may already have retired an older epoch. + wc -c <.git/witness-daemon.trace \ + >".git/$witness_cookie_label.cookie-daemon.offset" && + witness_cookie_log_offset=$(cat \ + ".git/$witness_cookie_label.cookie-daemon.offset") && + GIT_TRACE2_EVENT="$PWD/.git/$witness_cookie_label.cookie-healthy.trace" \ + test-tool fsmonitor-client query --token "$witness_cookie_token" \ + >".git/$witness_cookie_label.cookie-healthy" && + tail -c "+$((witness_cookie_log_offset + 1))" .git/witness-daemon.trace \ + >".git/$witness_cookie_label.cookie-daemon.trace" && + nul_to_q <".git/$witness_cookie_label.cookie-healthy" \ + >".git/$witness_cookie_label.cookie-healthy.q" && + test_grep "^builtin:.*Q" \ + ".git/$witness_cookie_label.cookie-healthy.q" && + test_grep ! "Q/Q$" ".git/$witness_cookie_label.cookie-healthy.q" && + test_grep "cookie-seen:" ".git/$witness_cookie_label.cookie-daemon.trace" && + test_grep ! "cookie_wait timed out" \ + ".git/$witness_cookie_label.cookie-daemon.trace" +) + +test_index_witness_physical_prime () ( + witness_prime_label=$1 && + test_index_witness_cookie_health "$witness_prime_label" && + # The physical index must carry the full source proof before CSH issuance. + GIT_OPTIONAL_LOCKS=1 GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TRACE2_EVENT="$PWD/.git/$witness_prime_label.prime.trace" \ + git status --porcelain=v2 >".git/$witness_prime_label.prime" && + test_index_witness_full_proof .git/index \ + >".git/$witness_prime_label.proof" +) + +test_index_witness_native_baseline () { + sane_unset GIT_INDEX_FILE GIT_INDEX_VERSION \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE GIT_TEST_FSMONITOR_QUERY_PATH \ + GIT_TEST_FSMONITOR_QUERY_BARRIER_AT \ + GIT_TEST_FSMONITOR_QUERY_BARRIER_READY \ + GIT_TEST_FSMONITOR_QUERY_BARRIER_RESUME GIT_TEST_FSMONITOR_TOKEN && + git config index.version 2 && + git config index.skipHash false && + git config core.autocrlf false && + git config core.trustctime true && + git config core.checkStat default && + git config core.untrackedCache true && + git config core.fsmonitor false && + test-tool chmtime -120 "$@" && + git update-index --refresh && + git update-index --index-version=2 --force-write-index && + git config core.fsmonitor true && + GIT_TRACE_FSMONITOR="$PWD/.git/witness-daemon.trace" \ + GIT_TRACE2_EVENT="$PWD/.git/witness-daemon.trace2" \ + git fsmonitor--daemon start --start-timeout=10 && + GIT_TRACE2_EVENT="$PWD/.git/baseline.enable.trace" \ + git update-index --fsmonitor && + test_index_witness_physical_prime baseline && + test_must_be_empty .git/baseline.prime +} + +test_index_witness_issue_history () { + witness_issue_label=$1 && + witness_issue_expected_token=${2-} && + GIT_OPTIONAL_LOCKS=1 \ + GIT_TRACE2_EVENT="$PWD/.git/$witness_issue_label.issue.trace" \ + git status --short >".git/$witness_issue_label.issue" && + test_trace2_data fsmonitor history/external-stored 1 \ + <".git/$witness_issue_label.issue.trace" && + find .git -maxdepth 1 -type f -name "index.csh1.*" >.git/checkpoints && + find .git -maxdepth 1 -type f -name "index.cswi.*" >.git/witnesses && + test_line_count = 1 .git/checkpoints && + test_line_count = 1 .git/witnesses && + checkpoint=$(cat .git/checkpoints) && + witness=$(cat .git/witnesses) && + test_index_witness_full_proof "$witness" "$witness_issue_expected_token" \ + >".git/$witness_issue_label.witness-proof" && + cp "$checkpoint" .git/checkpoint.good && + cp "$witness" .git/witness.good +} + +test_lazy_prereq INDEX_WITNESS_HEALTHY_NATIVE_COOKIE ' + test_have_prereq INDEX_WITNESS_APFS,FSMONITOR_DAEMON && + test_create_repo index-witness-native-cookie-prerequisite && + ( + cd index-witness-native-cookie-prerequisite && + trap "git fsmonitor--daemon stop >/dev/null 2>&1 || :" 0 && + git config core.fsmonitor true && + GIT_TRACE_FSMONITOR="$PWD/.git/witness-daemon.trace" \ + git fsmonitor--daemon start --start-timeout=10 && + test_index_witness_cookie_health native-prerequisite && + test_grep ! "cookie_wait timed out" .git/witness-daemon.trace + ) +' + +test_expect_success INDEX_WITNESS_APFS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS,INDEX_WITNESS_HEALTHY_NATIVE_COOKIE \ + 'corrupt external semantic witnesses fall back with a valid main index' ' + test_when_finished "git -C recovery fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo recovery && + ( + cd recovery && + test_commit base tracked && + test_index_witness_native_baseline tracked && + test_index_witness_issue_history recovery && + test_must_be_empty .git/recovery.issue && + test_write_lines changed >tracked && + git update-index --add tracked && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + strip-proofs <.git/index >.git/index.foreign && + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + cp .git/witness.good "$witness" && + git -c core.fsmonitor=false --no-optional-locks \ + status --porcelain=v2 >.git/expect && + GIT_TRACE2_EVENT="$PWD/.git/valid.trace" \ + git --no-optional-locks status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_trace2_data fsmonitor history/external-semantic-restored 1 \ + <.git/valid.trace && + for kind in truncated-header unknown-flags excessive-strip + do + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + "$kind" >"$witness" && + GIT_TRACE2_EVENT="$PWD/.git/$kind.trace" \ + git --no-optional-locks status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_cmp_bin .git/index.foreign .git/index && + ! test_trace2_data fsmonitor history/external-semantic-restored 1 \ + <".git/$kind.trace" || return 1 + done && + if test_have_prereq PIPE + then + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + rm -f "$witness" && + mkfifo "$witness" && + GIT_TRACE2_EVENT="$PWD/.git/fifo.trace" \ + git --no-optional-locks status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_cmp_bin .git/index.foreign .git/index && + ! test_trace2_data fsmonitor history/external-semantic-restored 1 \ + <.git/fifo.trace && + rm -f "$witness" + fi + ) +' + +test_expect_success INDEX_WITNESS_APFS,FSMONITOR_DAEMON,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS,INDEX_WITNESS_HEALTHY_NATIVE_COOKIE \ + 'bootstrap manifest recovery treats a damaged witness as a miss' ' + test_when_finished "git -C bootstrap fsmonitor--daemon stop 2>/dev/null || :" && + test_create_repo bootstrap && + ( + cd bootstrap && + test_commit base tracked && + test_write_lines "tracked diff=old" >.gitattributes && + git add .gitattributes && + git commit -qm attributes && + test_index_witness_native_baseline tracked .gitattributes && + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.old-oid && + test_write_lines "tracked diff=new" >.gitattributes && + test-tool chmtime -120 .gitattributes && + test_index_witness_physical_prime bootstrap && + test_grep "^1 \\.M .* .gitattributes$" .git/bootstrap.prime && + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.still-staged && + test_cmp .git/attributes.old-oid .git/attributes.still-staged && + test_index_witness_issue_history bootstrap && + test_write_lines " M .gitattributes" >.git/bootstrap.expect && + test_cmp .git/bootstrap.expect .git/bootstrap.issue && + GIT_INDEX_FILE="$PWD/.git/witness.good" \ + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.witness-oid && + test_cmp .git/attributes.old-oid .git/attributes.witness-oid && + git -c core.fsmonitor=false update-index --add .gitattributes && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + unbind-proof <.git/index >.git/index.foreign && + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + cp .git/witness.good "$witness" && + git -c core.fsmonitor=false --no-optional-locks \ + status --porcelain=v2 >.git/expect && + GIT_TRACE2_EVENT="$PWD/.git/valid.trace" \ + git --no-optional-locks status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_trace2_data fsmonitor history/external-bootstrap-manifest 1 \ + <.git/valid.trace && + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + truncated-header >"$witness" && + GIT_TRACE2_EVENT="$PWD/.git/corrupt.trace" \ + git --no-optional-locks status --porcelain=v2 >.git/actual && + test_cmp .git/expect .git/actual && + test_cmp_bin .git/index.foreign .git/index && + ! test_trace2_data fsmonitor history/external-bootstrap-manifest 1 \ + <.git/corrupt.trace + ) +' + +test_lazy_prereq INDEX_WITNESS_SCRIPTED_IPC ' + test-tool simple-ipc SUPPORTS_SIMPLE_IPC +' + +# This provider exists in the pre-fix runtime too. Its one stable token is +# truthful only while the worktree is unchanged: make every worktree edit +# before starting it, and keep all later fixture writes inside .git. +if test_have_prereq MACOS +then + index_witness_scripted_token=builtin:dirmeta-v1.inode-v1.cookie-v1.test-capable:0 +else + index_witness_scripted_token=builtin:cookie-v1.test-capable:0 +fi + +test_index_witness_scripted_prepare () { + sane_unset GIT_INDEX_FILE GIT_INDEX_VERSION \ + GIT_TEST_FSMONITOR GIT_TEST_FSMONITOR_QUERY_SEQUENCE \ + GIT_TEST_FSMONITOR_QUERY_PATH GIT_TEST_FSMONITOR_QUERY_BARRIER_AT \ + GIT_TEST_FSMONITOR_QUERY_BARRIER_READY \ + GIT_TEST_FSMONITOR_QUERY_BARRIER_RESUME GIT_TEST_FSMONITOR_TOKEN && + git config index.version 2 && + git config index.skipHash false && + git config core.autocrlf false && + git config core.trustctime true && + git config core.checkStat default && + git config core.preloadIndex false && + git config core.untrackedCache true && + git config core.fsmonitor false && + test-tool chmtime -120 "$@" && + git update-index --refresh && + git update-index --index-version=2 --force-write-index && + git config core.fsmonitor true +} + +test_index_witness_scripted_start () { + witness_ipc_path=$(git rev-parse --path-format=absolute \ + --git-path fsmonitor--daemon.ipc) && + GIT_TRACE2_EVENT="$PWD/.git/scripted-provider.trace" \ + test-tool simple-ipc start-daemon --name="$witness_ipc_path" \ + --threads=1 --fsmonitor-capability-superset && + printf "%s\000/\000" "$index_witness_scripted_token" \ + >.git/scripted-initial.expect && + printf "%s\000" "$index_witness_scripted_token" \ + >.git/scripted-clean.expect && + GIT_TRACE2_EVENT="$PWD/.git/scripted-initial.trace" \ + test-tool fsmonitor-client query --token 0 \ + >.git/scripted-initial.actual && + test_cmp_bin .git/scripted-initial.expect .git/scripted-initial.actual && + for witness_query in first repeated + do + GIT_TRACE2_EVENT="$PWD/.git/scripted-$witness_query.trace" \ + test-tool fsmonitor-client query \ + --token "$index_witness_scripted_token" \ + >".git/scripted-$witness_query.actual" && + test_cmp_bin .git/scripted-clean.expect \ + ".git/scripted-$witness_query.actual" || return 1 + done && + GIT_TRACE2_EVENT="$PWD/.git/scripted-enable.trace" \ + git update-index --fsmonitor +} + +test_index_witness_scripted_prime () { + witness_prime_label=$1 && + GIT_OPTIONAL_LOCKS=1 GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TRACE2_EVENT="$PWD/.git/$witness_prime_label.prime.trace" \ + git status --porcelain=v2 >".git/$witness_prime_label.prime" && + test_index_witness_full_proof .git/index \ + "$index_witness_scripted_token" \ + >".git/$witness_prime_label.proof" +} + +# Check the real, issued CSHS v2 source alias. Perl exposes the ordinary stat +# fields; Darwin stat adds the durable birth time and inode generation. The +# nanosecond fields remain in the authenticated record and are range-checked. +test_index_witness_scripted_source () { + perl - "$checkpoint" "$witness" \ + "$(git rev-parse --show-object-format)" \ + "$index_witness_scripted_token" <<-\EOF + use strict; + use warnings; + use Digest::SHA qw(sha1 sha256); + my ($checkpoint, $witness, $algo, $token) = @ARGV; + my $rawsz = $algo eq "sha256" ? 32 : 20; + sub digest { return $rawsz == 32 ? sha256($_[0]) : sha1($_[0]); } + sub read_file { + open my $fh, "<", $_[0] or die "cannot read $_[0]: $!\n"; + binmode $fh; + local $/; + return <$fh>; + } + my $source = read_file(".git/scripted-source.index"); + my $record = read_file($checkpoint); + my $end = length($record) - $rawsz; + my $offset = 12 + 2 * $rawsz; + die "bad source checksum\n" if length($source) < 12 + $rawsz || + digest(substr($source, 0, -$rawsz)) ne substr($source, -$rawsz); + die "bad CSHS checksum\n" if $end < $offset + 112 + 8 + $rawsz + 16 || + digest(substr($record, 0, $end)) ne substr($record, $end); + my ($magic, $version, $flags) = unpack("a4NN", substr($record, 0, 12)); + die "missing complete CSHS v2 source alias\n" + if $magic ne "CSHS" || $version != 2 || $flags != 15; + my $namespace = unpack("H*", substr($record, 12, $rawsz)); + die "checkpoint and witness namespaces differ\n" + if $checkpoint !~ /\.csh1\.\Q$namespace\E\z/ || + $witness !~ /\.cswi\.\Q$namespace\E\z/; + my @identity = unpack("Q>*", substr($record, $offset, 112)); + $offset += 112; + my @stat = split(/\s+/, read_file(".git/scripted-source.stat")); + my @fields = (0, 1, 2, 3, 4, 5, 6, 7, 9, 11, 13); + die "incomplete source stat\n" if @stat != @fields; + for my $i (0 .. $#fields) { + die "source identity field $fields[$i] differs\n" + if $identity[$fields[$i]] != $stat[$i]; + } + die "source is not an owned regular single-link index\n" + if ($identity[2] & 0170000) != 0100000 || + $identity[3] != 1 || $identity[4] != $>; + for my $i (8, 10, 12) { + die "invalid source nanoseconds\n" if $identity[$i] >= 1000000000; + } + my ($source_version, $source_nr) = unpack("NN", substr($record, $offset, 8)); + $offset += 8; + die "source header differs\n" + if substr($source, 0, 4) ne "DIRC" || + substr($source, 4, 8) ne pack("NN", $source_version, $source_nr); + die "source trailer differs\n" + if substr($record, $offset, $rawsz) ne substr($source, -$rawsz); + $offset += $rawsz; + my @lengths = unpack("N4", substr($record, $offset, 16)); + $offset += 16; + my %ext; + for my $name (qw(FSMN UNTR FSCF FSUC)) { + my $len = shift @lengths; + die "short checkpoint $name\n" if !$len || $len > $end - $offset; + $ext{$name} = substr($record, $offset, $len); + $offset += $len; + } + die "trailing checkpoint bytes\n" if $offset != $end; + my $proof = $ext{FSCF}; + die "short checkpoint FSCF\n" if length($proof) < 20; + my ($pv, $pmagic, $pf, $token_len, $manifest_len) = + unpack("N5", substr($proof, 0, 20)); + die "checkpoint does not carry FULL15\n" + if ($pv != 1 && $pv != 2) || $pmagic != 0x46534331 || $pf != 15 || + $token_len != length($token) || substr($proof, 20, $token_len) ne $token || + length($proof) != 20 + $token_len + $manifest_len + + ($pv == 2 ? 5 : 4) * $rawsz || + digest(substr($proof, 0, -$rawsz)) ne substr($proof, -$rawsz); + for my $name (qw(FSMN FSUC)) { + my $body = $ext{$name}; + my $want_version = $name eq "FSMN" ? 2 : 1; + die "checkpoint has an unbound $name\n" + if substr($body, 0, 4) ne pack("N", $want_version) || + substr($body, 4, length($token) + 1) ne "$token\0"; + } + print "CSHS v2 source $identity[0]:$identity[1] ", + "birth $identity[11] generation $identity[13] ", + "index $source_version entries $source_nr checksum ", + unpack("H*", substr($source, -$rawsz)), "\n"; + EOF +} + +test_index_witness_scripted_issue_history () { + perl -e ' + use strict; + use warnings; + my @st = lstat($ARGV[0]); + die "cannot stat source index: $!\n" if !@st; + print join(" ", @st[0, 1, 2, 3, 4, 5, 7, 9, 10]), "\n"; + ' .git/index >.git/scripted-source.stat && + /usr/bin/stat -f "%DB %Uv" .git/index >>.git/scripted-source.stat && + cp .git/index .git/scripted-source.index && + test_index_witness_issue_history "$1" "$index_witness_scripted_token" && + test_cmp_bin .git/scripted-source.index .git/witness.good && + test_index_witness_scripted_source >.git/scripted-source.proof +} + +# A FIFO regression must fail instead of hanging the whole test suite. Keep +# the child's actual exit status, and reserve 124 for a killed timeout. +test_index_witness_watchdog () { + perl -e ' + use strict; + use warnings; + use Errno qw(EINTR); + my $seconds = shift @ARGV; + my $pid = fork(); + die "cannot fork watchdog: $!\n" if !defined($pid); + if (!$pid) { + exec @ARGV or die "cannot exec $ARGV[0]: $!\n"; + } + my $timed_out = 0; + $SIG{ALRM} = sub { $timed_out = 1; kill "KILL", $pid; }; + alarm $seconds; + my $waited; + do { $waited = waitpid($pid, 0); } while $waited < 0 && $! == EINTR; + my $status = $?; + alarm 0; + die "cannot reap watchdog child: $!\n" if $waited != $pid; + if ($timed_out) { + warn "index witness command timed out after $seconds seconds\n"; + exit 124; + } + exit(($status & 127) ? 128 + ($status & 127) : $status >> 8); + ' "$@" +} + +test_index_witness_scripted_restore () { + cp .git/index.foreign .git/index && + cp .git/checkpoint.good "$checkpoint" && + rm -f "$witness" && + cp .git/witness.good "$witness" +} + +test_index_witness_scripted_status () ( + witness_status_label=$1 && + witness_status_key=$2 && + witness_status_expected=$3 && + if test_env GIT_TRACE2_EVENT="$PWD/.git/$witness_status_label.trace" \ + test_index_witness_watchdog 20 git --no-optional-locks \ + status --porcelain=v2 >".git/$witness_status_label.actual" \ + 2>".git/$witness_status_label.err" + then + echo 0 >".git/$witness_status_label.exit" + else + witness_status_ret=$? && + echo "$witness_status_ret" >".git/$witness_status_label.exit" && + cat ".git/$witness_status_label.err" >&2 + return 1 + fi && + test_cmp .git/expect ".git/$witness_status_label.actual" && + test_cmp_bin .git/index.foreign .git/index && + test_cmp_bin .git/checkpoint.good "$checkpoint" && + test_grep ! '"key":"query/incompatible-daemon"' \ + ".git/$witness_status_label.trace" && + test_grep ! '"argv":.*"fsmonitor--daemon","run","--detach"' \ + ".git/$witness_status_label.trace" && + if test "$witness_status_expected" = restored + then + test_trace2_data fsmonitor "$witness_status_key" 1 \ + <".git/$witness_status_label.trace" + else + ! test_trace2_data fsmonitor "$witness_status_key" 1 \ + <".git/$witness_status_label.trace" + fi +) + +test_index_witness_scripted_recovery () ( + witness_recovery_key=$1 && + test_index_witness_scripted_restore && + test_index_witness_scripted_status valid-before \ + "$witness_recovery_key" restored || return 1 + witness_recovery_failed=0 + for witness_kind in truncated-header unknown-flags excessive-strip + do + if test_index_witness_scripted_restore && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + "$witness_kind" >"$witness" && + test_index_witness_scripted_status "$witness_kind" \ + "$witness_recovery_key" miss + then + : + else + witness_recovery_failed=1 + fi + done + if test_index_witness_scripted_restore && + rm -f "$witness" && + mkfifo "$witness" && + test_index_witness_scripted_status fifo \ + "$witness_recovery_key" miss && + test -p "$witness" + then + : + else + witness_recovery_failed=1 + fi + # Even a pre-fix failure must reach the FIFO and closing positive control. + if test_index_witness_scripted_restore && + test_index_witness_scripted_status valid-after \ + "$witness_recovery_key" restored + then + : + else + witness_recovery_failed=1 + fi + test "$witness_recovery_failed" = 0 +) + +test_expect_success INDEX_WITNESS_APFS,FSMONITOR_DAEMON,INDEX_WITNESS_SCRIPTED_IPC,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS,PIPE \ + 'scripted-provider semantic recovery ignores damaged optional witnesses' ' + test_when_finished "test-tool -C scripted-recovery simple-ipc stop-daemon --name=.git/fsmonitor--daemon.ipc --max-wait=5 2>/dev/null || :" && + test_create_repo scripted-recovery && + ( + cd scripted-recovery && + test_commit base tracked && + test_write_lines stable >stable && + git add stable && + git commit -qm stable && + test_write_lines changed >.git/replacement && + git hash-object -w --stdin <.git/replacement >.git/replacement.oid && + test_index_witness_scripted_prepare tracked stable && + test_index_witness_scripted_start && + test_index_witness_scripted_prime semantic && + test_must_be_empty .git/semantic.prime && + test_index_witness_scripted_issue_history semantic && + test_must_be_empty .git/semantic.issue && + # Only the index changes; the provider can truthfully stay at its token. + git update-index --cacheinfo \ + "100644,$(cat .git/replacement.oid),tracked" && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + strip-proofs <.git/index >.git/index.foreign && + GIT_INDEX_FILE="$PWD/.git/index.foreign" \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + --no-optional-locks status --porcelain=v2 >.git/expect && + test_line_count = 1 .git/expect && + test_grep "^1 MM .* tracked$" .git/expect && + test_index_witness_scripted_recovery \ + history/external-semantic-restored + ) +' + +test_expect_success INDEX_WITNESS_APFS,FSMONITOR_DAEMON,INDEX_WITNESS_SCRIPTED_IPC,UNTRACKED_CACHE,SEMANTIC_VERIFY_ANCHORED_OPEN,PERL_TEST_HELPERS,PIPE \ + 'scripted-provider bootstrap recovery ignores damaged optional witnesses' ' + test_when_finished "test-tool -C scripted-bootstrap simple-ipc stop-daemon --name=.git/fsmonitor--daemon.ipc --max-wait=5 2>/dev/null || :" && + test_create_repo scripted-bootstrap && + ( + cd scripted-bootstrap && + test_commit base tracked && + test_write_lines "tracked diff=old" >.gitattributes && + git add .gitattributes && + git commit -qm attributes && + test_index_witness_scripted_prepare tracked .gitattributes && + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.old-oid && + # This edit predates the synthetic provider; no later query may omit it. + test_write_lines "tracked diff=new" >.gitattributes && + test-tool chmtime -120 .gitattributes && + test_index_witness_scripted_start && + test_index_witness_scripted_prime bootstrap && + test_grep "^1 \\.M .* .gitattributes$" .git/bootstrap.prime && + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.still-staged && + test_cmp .git/attributes.old-oid .git/attributes.still-staged && + test_index_witness_scripted_issue_history bootstrap && + test_write_lines " M .gitattributes" >.git/bootstrap.expect && + test_cmp .git/bootstrap.expect .git/bootstrap.issue && + GIT_INDEX_FILE="$PWD/.git/witness.good" \ + git -c core.fsmonitor=false --no-optional-locks \ + rev-parse :.gitattributes >.git/attributes.witness-oid && + test_cmp .git/attributes.old-oid .git/attributes.witness-oid && + git -c core.fsmonitor=false update-index --add .gitattributes && + perl ../make-index.pl "$(git rev-parse --show-object-format)" \ + unbind-proof <.git/index >.git/index.foreign && + GIT_INDEX_FILE="$PWD/.git/index.foreign" \ + git -c core.fsmonitor=false -c core.untrackedCache=false \ + --no-optional-locks status --porcelain=v2 >.git/expect && + test_line_count = 1 .git/expect && + test_grep "^1 M\\. .* .gitattributes$" .git/expect && + test_index_witness_scripted_recovery \ + history/external-bootstrap-manifest + ) +' + +# An EOIE lookup probes backwards from the checksum before it knows whether +# the bytes are an extension signature. Keep both the current SHA-1-sized +# probe and a hash-sized probe inside a fixed high-bit optional payload. +# Also exercise the ordinary reader with a high-bit byte after the uppercase +# first byte of an optional extension signature. +for algo in sha1 sha256 +do + test_expect_success PTHREADS,PERL_TEST_HELPERS \ + "$algo index extension signatures use unsigned bytes" ' + repo=high-bit-extension-$algo && + git init --object-format="$algo" "$repo" && + perl make-index.pl "$algo" high-bit-extension >"$repo/.git/witness" && + GIT_INDEX_FILE="$PWD/$repo/.git/witness" \ + git -C "$repo" -c core.fsmonitor=false \ + -c core.untrackedCache=false -c index.threads=1 \ + --no-optional-locks ls-files --stage >expect 2>serial.err && + test_line_count = 1 expect && + test_grep "^100644 .*alpha$" expect && + test_grep "ignoring ZZZZ extension" serial.err && + GIT_INDEX_FILE="$PWD/$repo/.git/witness" \ + git -C "$repo" -c core.fsmonitor=false \ + -c core.untrackedCache=false -c index.threads=2 \ + --no-optional-locks ls-files --stage >actual 2>threaded.err && + test_cmp expect actual && + test_grep "ignoring ZZZZ extension" threaded.err && + perl make-index.pl "$algo" high-bit-signature >"$repo/.git/witness" && + GIT_INDEX_FILE="$PWD/$repo/.git/witness" \ + git -C "$repo" -c core.fsmonitor=false \ + -c core.untrackedCache=false -c index.threads=1 \ + --no-optional-locks ls-files --stage >actual 2>signature.err && + test_cmp expect actual && + test_grep "ignoring .* extension" signature.err + ' +done + +test_done diff --git a/t/t2006-checkout-index-basic.sh b/t/t2006-checkout-index-basic.sh index 6538a24c951f9b..f1ade19c9c9f4d 100755 --- a/t/t2006-checkout-index-basic.sh +++ b/t/t2006-checkout-index-basic.sh @@ -107,4 +107,35 @@ test_expect_success 'checkout-index --temp correctly reports error for submodule test_grep "cannot create temporary submodule sub" stderr ' +test_expect_success 'checkout-index -u preserves an unchanged index' ' + test_when_finished "rm -rf checkout-index-unchanged" && + test_create_repo checkout-index-unchanged && + test_commit -C checkout-index-unchanged base tracked && + test-tool -C checkout-index-unchanged chmtime -120 tracked && + git -C checkout-index-unchanged update-index --refresh && + cp checkout-index-unchanged/.git/index checkout-index.before && + GIT_TRACE2_EVENT="$PWD/checkout-index.trace" \ + git -C checkout-index-unchanged checkout-index -u tracked && + test_cmp_bin checkout-index.before checkout-index-unchanged/.git/index && + test_grep ! "\"label\":\"do_write_index\"" checkout-index.trace +' + +test_expect_success 'checkout-index -u retains post-index-change hooks' ' + test_when_finished "rm -rf checkout-index-hook" && + test_create_repo checkout-index-hook && + test_commit -C checkout-index-hook base tracked && + test-tool -C checkout-index-hook chmtime -120 tracked && + git -C checkout-index-hook update-index --refresh && + mkdir checkout-index-hook/hooks && + git -C checkout-index-hook config core.hooksPath hooks && + write_script checkout-index-hook/hooks/post-index-change <<-\EOF && + printf "%s %s\n" "$1" "$2" >hook-actual + EOF + GIT_TRACE2_EVENT="$PWD/checkout-index-hook.trace" \ + git -C checkout-index-hook checkout-index -u tracked && + test_write_lines "0 0" >checkout-index-hook.expect && + test_cmp checkout-index-hook.expect checkout-index-hook/hook-actual && + test_grep "\"label\":\"do_write_index\"" checkout-index-hook.trace +' + test_done diff --git a/t/t2022-checkout-paths.sh b/t/t2022-checkout-paths.sh index c49ba7f9bd4fe0..ac1ba6e3558672 100755 --- a/t/t2022-checkout-paths.sh +++ b/t/t2022-checkout-paths.sh @@ -19,6 +19,16 @@ test_expect_success setup ' test_tick && git commit -m "next has dir/next but not dir/main" ' +test_expect_success 'checkout does not rewrite an unchanged index' ' + test_config core.fsmonitor false && + git update-index --no-fsmonitor && + test-tool chmtime =1000000000 .git/index && + git checkout -- dir/common && + test "$(test-tool chmtime --get .git/index)" = 1000000000 && + git checkout HEAD -- dir/common && + test "$(test-tool chmtime --get .git/index)" = 1000000000 +' + test_expect_success 'checking out paths out of a tree does not clobber unrelated paths' ' git checkout next && git reset --hard && diff --git a/t/t2070-restore.sh b/t/t2070-restore.sh index 2c222fb9342777..81b870c77074ec 100755 --- a/t/t2070-restore.sh +++ b/t/t2070-restore.sh @@ -21,6 +21,16 @@ test_expect_success 'setup' ' git update-ref refs/heads/one main ' +test_expect_success 'restore does not rewrite an unchanged index' ' + test_config core.fsmonitor false && + git update-index --no-fsmonitor && + test-tool chmtime =1000000000 .git/index && + git restore --worktree first.t && + test "$(test-tool chmtime --get .git/index)" = 1000000000 && + git restore --staged first.t && + test "$(test-tool chmtime --get .git/index)" = 1000000000 +' + test_expect_success 'restore without pathspec is not ok' ' test_must_fail git restore && test_must_fail git restore --source=first diff --git a/t/t3903-stash.sh b/t/t3903-stash.sh index da27a6599a6a79..1401b82e4b1360 100755 --- a/t/t3903-stash.sh +++ b/t/t3903-stash.sh @@ -953,6 +953,31 @@ test_expect_success 'store called with non-stash commit' ' test_must_fail git stash store HEAD ' +test_expect_success 'stash store and push support explicit SHA-256 repositories' ' + test_when_finished "rm -rf stash-explicit-sha256" && + git init --object-format=sha256 stash-explicit-sha256 && + ( + cd stash-explicit-sha256 && + git config core.fsmonitor false && + test "$(git rev-parse --show-object-format)" = sha256 && + echo original >tracked && + git add tracked && + git commit -m base && + echo stored >tracked && + oid=$(git stash create) && + test "${#oid}" -eq 64 && + git stash store -m stored "$oid" && + test "$oid" = "$(git rev-parse refs/stash)" && + test "$oid" = "$(git reflog --format=%H -1 refs/stash)" && + git stash clear && + echo pushed >tracked && + git stash push -m pushed -- tracked && + test "$(cat tracked)" = original && + git stash pop && + test "$(cat tracked)" = pushed + ) +' + test_expect_success 'store updates stash ref and reflog' ' git stash clear && git reset --hard && @@ -1290,6 +1315,33 @@ test_expect_success 'push : show no changes when there are none' ' test_cmp expect actual ' +test_expect_success 'clean stash push does not rewrite an unchanged index' ' + test_when_finished "rm -rf clean-stash-index" && + test_create_repo clean-stash-index && + ( + cd clean-stash-index && + test_commit base tracked && + test_set_magic_mtime .git/index +1 && + git stash push >actual && + test_grep "No local changes to save" actual && + test_is_magic_mtime .git/index +1 && + git --no-optional-locks stash push >actual && + test_grep "No local changes to save" actual && + test_is_magic_mtime .git/index +1 && + test_set_magic_mtime tracked && + git --no-optional-locks stash push >actual && + test_grep "No local changes to save" actual && + test_is_magic_mtime .git/index +1 && + git stash push >actual && + test_grep "No local changes to save" actual && + test_is_magic_mtime .git/index +1 && + test_write_lines changed >tracked && + git stash push >actual && + test_grep "Saved working directory" actual && + ! test_is_magic_mtime .git/index +1 + ) +' + test_expect_success 'push: not in the repository errors out' ' >untracked && test_must_fail git stash push untracked && @@ -1697,6 +1749,28 @@ test_expect_success 'stash push reports a locked index' ' ) ' +test_expect_success 'stash push rolls back its lock for an unmerged index' ' + test_when_finished "rm -rf stash-unmerged-lock" && + test_create_repo stash-unmerged-lock && + ( + cd stash-unmerged-lock && + test_commit base tracked && + git checkout -b side && + test_write_lines side >tracked && + git commit -am side && + git checkout - && + test_write_lines main >tracked && + git commit -am main && + test_must_fail git merge side && + test_must_fail git stash push >actual 2>err && + test_grep "needs merge" actual && + test_grep "could not write index" err && + test_path_is_missing .git/index.lock && + git ls-files --unmerged >stages && + test_line_count = 3 stages + ) +' + test_expect_success 'stash apply reports a locked index' ' test_when_finished "rm -rf repo" && git init repo && diff --git a/t/t5331-pack-objects-stdin.sh b/t/t5331-pack-objects-stdin.sh index c74b5861af322f..2a983e28ac43e7 100755 --- a/t/t5331-pack-objects-stdin.sh +++ b/t/t5331-pack-objects-stdin.sh @@ -368,7 +368,8 @@ test_expect_success '--stdin-packs does not perform backfill fetch' ' git -C remote config set --local uploadpack.allowfilter 1 && git -C remote config set --local uploadpack.allowanysha1inwant 1 && - git clone --filter=tree:0 "file://$(pwd)/remote" client && + git -c maintenance.auto=false clone --filter=tree:0 \ + "file://$(pwd)/remote" client && ( cd client && ls .git/objects/pack/*.promisor | sed "s|.*/||; s/\.promisor$/.pack/" >packs && diff --git a/t/t6120-describe.sh b/t/t6120-describe.sh index 7a7c46658a3a81..77a14b73a638f8 100755 --- a/t/t6120-describe.sh +++ b/t/t6120-describe.sh @@ -392,6 +392,19 @@ test_expect_success 'setup and absorb a submodule' ' test_cmp expect out ' +test_expect_success 'describe --broken ignores diff submodule presentation settings' ' + test_when_finished "git -C sub1 checkout -- initial.t && rm -f sub1/untracked" && + test_config diff.ignoreSubmodules all && + test_write_lines untracked >sub1/untracked && + git --no-optional-locks describe --dirty --broken >out && + test_grep ! ".*-dirty$" out && + test_write_lines changed >sub1/initial.t && + test_set_magic_mtime .git/index && + git --no-optional-locks describe --dirty --broken >out && + test_grep ".*-dirty$" out && + test_is_magic_mtime .git/index +' + test_expect_success 'describe chokes on severely broken submodules' ' mv .git/modules/sub1/ .git/modules/sub_moved && test_must_fail git describe --dirty @@ -402,6 +415,13 @@ test_expect_success 'describe ignoring a broken submodule' ' test_grep broken out ' +test_expect_success 'describe --broken honors --no-optional-locks' ' + test_set_magic_mtime .git/index && + git --no-optional-locks describe --broken >out && + test_grep broken out && + test_is_magic_mtime .git/index +' + test_expect_success 'describe with --work-tree ignoring a broken submodule' ' ( cd "$TEST_DIRECTORY" && @@ -791,6 +811,34 @@ test_expect_success 'describe --broken --dirty with a file with changed stat' ' ) ' +for broken in '' '--broken' +do + test_expect_success "describe --dirty $broken honors --no-optional-locks" ' + test_when_finished "rm -fr describe-optional-locks" && + git init describe-optional-locks && + ( + cd describe-optional-locks && + test_commit --annotate base tracked && + git config diff.autoRefreshIndex false && + test_set_magic_mtime tracked && + test_set_magic_mtime .git/index +1 && + git --no-optional-locks describe --dirty $broken >actual && + test_grep "^base$" actual && + test_is_magic_mtime .git/index +1 && + test_write_lines changed >tracked && + git --no-optional-locks describe --dirty $broken >actual && + test_grep "^base-dirty$" actual && + test_is_magic_mtime .git/index +1 && + git checkout -- tracked && + test_set_magic_mtime tracked && + test_set_magic_mtime .git/index +1 && + git describe --dirty $broken >actual && + test_grep "^base$" actual && + ! test_is_magic_mtime .git/index +1 + ) + ' +done + test_expect_success '--always with no refs falls back to commit hash' ' git rev-parse HEAD >expect && git describe --no-abbrev --always --match=no-such-tag >actual && diff --git a/t/t7063-status-untracked-cache.sh b/t/t7063-status-untracked-cache.sh index 8929ef481f926c..1cf25b5088284e 100755 --- a/t/t7063-status-untracked-cache.sh +++ b/t/t7063-status-untracked-cache.sh @@ -991,4 +991,220 @@ test_expect_success 'empty repo (no index) and core.untrackedCache' ' git -C emptyrepo -c core.untrackedCache=true write-tree ' +test_expect_success 'directory snapshots ignore weak file-stat configuration' ' + test_create_repo weak-dir && + ( + cd weak-dir && + mkdir nested && + echo tracked >tracked && + echo one >nested/one && + git add tracked && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor false && + git config core.trustCtime false && + git config core.checkStat minimal && + avoid_racy && + git status --porcelain -uall >/dev/null && + git status --porcelain -uall >/dev/null && + dir_mtime=$(test-tool chmtime --get nested) && + mv nested/one nested/two && + test-tool chmtime =$dir_mtime nested && + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain -uall >.git/expect && + git status --porcelain -uall >.git/actual && + test_cmp .git/expect .git/actual + ) +' + +test_expect_success 'automatic preload observes its directory threshold' ' + test_create_repo auto-preload-threshold && + ( + cd auto-preload-threshold && + test_commit base tracked && + git config core.untrackedCache true && + git config core.fsmonitor false && + sane_unset GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD && + for i in $(test_seq 1 1998) + do + mkdir "d$i" && + >"d$i/file" || return 1 + done && + git status --porcelain >/dev/null && + GIT_TRACE2_EVENT="$PWD/.git/below-threshold.trace" \ + git status --porcelain >/dev/null && + test_grep ! "preload_untracked_cache/automatic" \ + .git/below-threshold.trace && + + mkdir d1999 && + >d1999/file && + git status --porcelain >/dev/null && + GIT_TRACE2_EVENT="$PWD/.git/at-threshold.trace" \ + git status --porcelain >/dev/null && + test_grep \ + "preload_untracked_cache/automatic.*value.*1" \ + .git/at-threshold.trace + ) +' + +test_expect_success 'preload verifies cached per-directory excludes' ' + test_create_repo auto-exclude && + ( + cd auto-exclude && + test_write_lines hide-a >.gitignore && + test_write_lines tracked >tracked && + git add .gitignore tracked && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor false && + test_write_lines a >hide-a && + test_write_lines b >hide-b && + git status --porcelain >/dev/null && + git status --porcelain >/dev/null && + avoid_racy && + mtime=$(test-tool chmtime --get .gitignore) && + test_write_lines hide-b >.gitignore && + test-tool chmtime =$mtime .gitignore && + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain >.git/expect && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=4 \ + GIT_TRACE2_EVENT="$PWD/.git/actual.trace" \ + git status --porcelain >.git/actual && + test_cmp .git/expect .git/actual && + test_grep "preload_untracked_cache/valid.*value.*0" \ + .git/actual.trace + ) +' + +test_expect_success 'recursive preload checks descendant directory mtimes' ' + test_create_repo recursive-preload && + ( + cd recursive-preload && + mkdir -p a/b && + echo tracked >a/b/tracked && + git add a/b/tracked && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor false && + git status --porcelain >/dev/null && + git status --porcelain >/dev/null && + avoid_racy && + git status --porcelain >/dev/null && + git status --porcelain >/dev/null && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=4 \ + GIT_TRACE2_EVENT="$PWD/.git/pruned.trace" \ + git status --porcelain >.git/pruned && + test_must_be_empty .git/pruned && + test_grep \ + "directories-visited.*value.*0" \ + .git/pruned.trace && + avoid_racy && + echo untracked >a/b/new-untracked && + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain >.git/expect && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=4 \ + git status --porcelain >.git/actual && + test_cmp .git/expect .git/actual + ) +' + +test_expect_success 'recursive preload rescans a vanished collapsed witness' ' + test_create_repo collapsed-witness && + ( + cd collapsed-witness && + test_write_lines "*.ignored" >.gitignore && + git add .gitignore && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor false && + for i in 00 01 + do + mkdir -p "scratch/d$i" && + test_write_lines "$i" >"scratch/d$i/file" || return 1 + done && + echo "?? scratch/" >.git/expect && + git status --porcelain >/dev/null && + git status --porcelain >/dev/null && + test-tool dump-untracked-cache >.git/cache && + witness=$(sed -n \ + "s#^/scratch/\\(d[0-9][0-9]*\\)/ .*#\\1#p" \ + .git/cache | sed -n 1p) && + test -n "$witness" && + avoid_racy && + rm "scratch/$witness/file" && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=4 \ + git status --porcelain >.git/actual && + test_cmp .git/expect .git/actual + ) +' + +test_expect_success 'status preloads cached-directory validation' ' + test_create_repo auto-preload && + ( + cd auto-preload && + mkdir -p nested/deep && + echo tracked >nested/tracked && + git add nested/tracked && + git commit -m base && + git config core.untrackedCache true && + git config core.fsmonitor false && + echo visible >nested/deep/visible && + git -c core.untrackedCache=false status --porcelain \ + >.git/expect && + avoid_racy && + git status --porcelain >/dev/null && + git status --porcelain >/dev/null && + + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=3 \ + GIT_TRACE2_EVENT="$PWD/.git/normal.trace" \ + git status --porcelain >.git/actual && + test_cmp .git/expect .git/actual && + if test_have_prereq PTHREADS + then + expect_threads=3 + else + expect_threads=1 + fi && + test_grep \ + "preload_untracked_cache/threads.*value.*$expect_threads" \ + .git/normal.trace && + test_grep "preload_untracked_cache/valid.*value.*1" \ + .git/normal.trace && + test_grep "opendir.*value.*0" .git/normal.trace && + echo changed >nested/deep/changed && + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain >.git/expect-changed && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=3 \ + GIT_TRACE2_EVENT="$PWD/.git/changed.trace" \ + git status --porcelain >.git/actual-changed && + test_cmp .git/expect-changed .git/actual-changed && + test_grep "preload_untracked_cache/valid.*value.*0" \ + .git/changed.trace && + test_grep "opendir.*value.*[1-9][0-9]*" \ + .git/changed.trace && + + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain -- nested >.git/expect-pathspec && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TRACE2_EVENT="$PWD/.git/pathspec.trace" \ + git status --porcelain -- nested >.git/actual-pathspec && + test_cmp .git/expect-pathspec .git/actual-pathspec && + test_grep ! 'preload_untracked_cache/threads' \ + .git/pathspec.trace && + GIT_OPTIONAL_LOCKS=0 git -c core.untrackedCache=false \ + status --porcelain -uall >.git/expect-uall && + GIT_TEST_UNTRACKED_CACHE_AUTO_PRELOAD=1 \ + GIT_TRACE2_EVENT="$PWD/.git/uall.trace" \ + git status --porcelain -uall >.git/actual-uall && + test_cmp .git/expect-uall .git/actual-uall && + test_grep ! 'preload_untracked_cache/threads' .git/uall.trace + ) +' + test_done diff --git a/t/t7102-reset.sh b/t/t7102-reset.sh index 298272cb13c033..993b7c260d0f84 100755 --- a/t/t7102-reset.sh +++ b/t/t7102-reset.sh @@ -482,6 +482,48 @@ test_expect_success 'resetting an unmodified path is a no-op' ' git diff-index --cached --exit-code HEAD ' +test_expect_success 'mixed resets do not rewrite an unchanged index' ' + test_when_finished "rm -rf reset-unchanged-index" && + git init reset-unchanged-index && + ( + cd reset-unchanged-index && + sane_unset GIT_TEST_SPLIT_INDEX && + git config core.fsmonitor false && + test_commit base tracked && + git commit --allow-empty -m same-tree && + git update-index --no-fsmonitor && + test_set_magic_mtime .git/index && + + GIT_TRACE2_EVENT="$PWD/.git/head.trace" \ + git reset --mixed HEAD && + test_is_magic_mtime .git/index && + test_grep ! "\"label\":\"do_write_index\"" .git/head.trace && + + GIT_TRACE2_EVENT="$PWD/.git/path.trace" \ + git reset HEAD -- tracked && + test_is_magic_mtime .git/index && + test_grep ! "\"label\":\"do_write_index\"" .git/path.trace && + + old_head=$(git rev-parse HEAD) && + GIT_TRACE2_EVENT="$PWD/.git/same-tree.trace" \ + git reset --mixed HEAD^ && + test_is_magic_mtime .git/index && + test_grep ! "\"label\":\"do_write_index\"" \ + .git/same-tree.trace && + test "$(git rev-parse ORIG_HEAD)" = "$old_head" && + test "$(git rev-parse HEAD)" = "$(git rev-parse base)" && + + test_hook --setup post-index-change <<-\EOF && + echo "$1 $2" >.git/hook-args + EOF + GIT_TRACE2_EVENT="$PWD/.git/hook.trace" \ + git reset --mixed HEAD && + test_grep "\"label\":\"do_write_index\"" .git/hook.trace && + test_grep "^0 1$" .git/hook-args && + ! test_is_magic_mtime .git/index + ) +' + test_reset_refreshes_index () { # To test whether the index is refreshed in `git reset --mixed` with diff --git a/t/t7501-commit-basic-functionality.sh b/t/t7501-commit-basic-functionality.sh index 7794babe462bbe..a15de3a3a32097 100755 --- a/t/t7501-commit-basic-functionality.sh +++ b/t/t7501-commit-basic-functionality.sh @@ -79,6 +79,37 @@ test_expect_success '--dry-run fails with nothing to commit' ' test_must_fail git commit -m initial --dry-run ' +test_expect_success '--no-optional-locks prevents dry-run index updates' ' + test_when_finished "rm -rf optional-locks-dry-run" && + test_create_repo optional-locks-dry-run && + ( + cd optional-locks-dry-run && + git config core.fsmonitor false && + test_commit base tracked && + test_set_magic_mtime tracked && + test_set_magic_mtime .git/index +1 && + test_must_fail git --no-optional-locks commit --dry-run >../actual && + test_grep "working tree clean" ../actual && + test_is_magic_mtime .git/index +1 && + echo modified >>tracked && + test_must_fail git --no-optional-locks commit --dry-run >../actual && + test_grep "modified:.*tracked" ../actual && + test_is_magic_mtime .git/index +1 && + git add tracked && + test_set_magic_mtime tracked && + test_set_magic_mtime .git/index +1 && + >.git/index.lock && + git --no-optional-locks commit --dry-run >../actual && + test_grep "modified:.*tracked" ../actual && + test_is_magic_mtime .git/index +1 && + test_must_fail git commit --dry-run >../actual 2>../err && + test_grep "index.lock" ../err && + rm .git/index.lock && + git commit --dry-run >../actual && + ! test_is_magic_mtime .git/index +1 + ) +' + test_expect_success '--short fails with nothing to commit' ' test_must_fail git commit -m initial --short ' diff --git a/t/t7508-status.sh b/t/t7508-status.sh index 0fd7c79911572e..beb84cbf3d657c 100755 --- a/t/t7508-status.sh +++ b/t/t7508-status.sh @@ -1681,6 +1681,23 @@ test_expect_success '--no-optional-locks prevents index update' ' ! test_is_magic_mtime .git/index ' +test_expect_success '--no-optional-locks prevents diff index update' ' + test_when_finished "rm -rf optional-locks-diff" && + test_create_repo optional-locks-diff && + ( + cd optional-locks-diff && + test_commit base tracked && + test_set_magic_mtime tracked && + test_set_magic_mtime .git/index +1 && + git --no-optional-locks diff -- tracked >actual && + test_must_be_empty actual && + test_is_magic_mtime .git/index +1 && + git diff -- tracked >actual && + test_must_be_empty actual && + ! test_is_magic_mtime .git/index +1 + ) +' + test_expect_success 'racy timestamps will be fixed for clean worktree' ' echo content >racy-dirty && echo content >racy-racy && @@ -1789,4 +1806,44 @@ test_expect_success EXPENSIVE,SIZE_T_IS_64BIT 'status does not re-read unchanged ) ' +test_expect_success 'status uses only a matching effective cache-tree' ' + test_when_finished "rm -rf cache-tree-status" && + test_create_repo cache-tree-status && + ( + cd cache-tree-status && + sane_unset GIT_TEST_SPLIT_INDEX && + test_write_lines base >tracked && + git add tracked && + git commit -m base && + + GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ + git status >.git/clean && + test_grep "nothing to commit, working tree clean" \ + .git/clean && + test_trace2_data status index/cache-tree-match 1 \ + <.git/clean.trace && + + test_write_lines staged >tracked && + git add tracked && + git write-tree >.git/staged-tree && + GIT_TRACE2_EVENT="$PWD/.git/staged.trace" \ + git status >.git/staged && + test_grep "Changes to be committed:" .git/staged && + test_grep "modified:.*tracked" .git/staged && + ! test_trace2_data status index/cache-tree-match 1 \ + <.git/staged.trace && + + replacement_tree=$(cat .git/staged-tree) && + git reset --hard HEAD && + head_tree=$(git rev-parse HEAD^{tree}) && + git replace "$head_tree" "$replacement_tree" && + GIT_TRACE2_EVENT="$PWD/.git/replaced.trace" \ + git status >.git/replaced && + test_grep "Changes to be committed:" .git/replaced && + test_grep "modified:.*tracked" .git/replaced && + ! test_trace2_data status index/cache-tree-match 1 \ + <.git/replaced.trace + ) +' + test_done diff --git a/t/t7519-status-fsmonitor.sh b/t/t7519-status-fsmonitor.sh index 93973ed25a448b..9454c11695077f 100755 --- a/t/t7519-status-fsmonitor.sh +++ b/t/t7519-status-fsmonitor.sh @@ -3,6 +3,7 @@ test_description='git status with file system watcher' . ./test-lib.sh +. "$TEST_DIRECTORY"/lib-semantic-verify.sh # Note, after "git reset --hard HEAD" no extensions exist other than 'TREE' # "git update-index --fsmonitor" can be used to get the extension written @@ -55,425 +56,6656 @@ test_lazy_prereq UNTRACKED_CACHE ' test $ret -ne 1 ' -# Test that we detect and disallow repos that are incompatible with FSMonitor. -test_expect_success 'incompatible bare repo' ' - test_when_finished "rm -rf ./bare-clone actual expect" && - git init --bare bare-clone && +test_lazy_prereq HARDLINKS ' + : >hardlink-a && + ln hardlink-a hardlink-b +' - test_must_fail \ - git -C ./bare-clone -c core.fsmonitor=foo \ - update-index --fsmonitor 2>actual && - test_grep "bare repository .* is incompatible with fsmonitor" actual && +test_lazy_prereq STATUS_BULK_PRELOAD ' + test_create_repo status-bulk-preload-prereq && + ( + cd status-bulk-preload-prereq && + test_write_lines tracked >tracked && + test_write_lines sibling >sibling && + git add tracked sibling && + git commit -qm base && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_PRELOAD_INDEX=1 \ + GIT_TRACE2_EVENT="$PWD/.git/bulk.trace" \ + git -c core.fsmonitor=false \ + -c core.preloadIndexBulk=true \ + status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_trace2_data index preload/bulk_result complete \ + <.git/bulk.trace + ) +' - test_must_fail \ - git -C ./bare-clone -c core.fsmonitor=true \ - update-index --fsmonitor 2>actual && - test_grep "bare repository .* is incompatible with fsmonitor" actual +test_expect_success 'FSMN parser fails closed' ' + test-tool read-cache --test-fsmn-parser ' -test_expect_success FSMONITOR_DAEMON 'run fsmonitor-daemon in bare repo' ' - test_when_finished "rm -rf ./bare-clone actual" && - git init --bare bare-clone && - test_must_fail git -C ./bare-clone fsmonitor--daemon run 2>actual && - test_grep "bare repository .* is incompatible with fsmonitor" actual +test_expect_success 'FSUC parser fails closed' ' + test-tool read-cache --test-fsuc-parser ' -test_expect_success MINGW,FSMONITOR_DAEMON 'run fsmonitor-daemon in virtual repo' ' - test_when_finished "rm -rf ./fake-virtual-clone actual" && - git init fake-virtual-clone && - test_must_fail git -C ./fake-virtual-clone \ - -c core.virtualfilesystem=true \ - fsmonitor--daemon run 2>actual && - test_grep "virtual repository .* is incompatible with fsmonitor" actual +test_expect_success !SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'unsupported identity preserves an ordinary provider token' ' + test_when_finished "rm -rf unsupported-provider-token" && + test_create_repo unsupported-provider-token && + ( + cd unsupported-provider-token && + test_commit base tracked && + git config core.untrackedCache true && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/status.trace" \ + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test-tool dump-fsmonitor >.git/fsmonitor && + test_grep "^fsmonitor last update builtin:test:" \ + .git/fsmonitor && + test_trace2_data fsmonitor token_closure/accepted 1 \ + <.git/status.trace && + ! test_trace2_data fsmonitor semantic/strong-invalidation 1 \ + <.git/status.trace + ) ' -test_expect_success 'setup' ' - : >tracked && - : >modified && - mkdir dir1 && - : >dir1/tracked && - : >dir1/modified && - mkdir dir2 && - : >dir2/tracked && - : >dir2/modified && - git -c core.fsmonitor= add . && - git -c core.fsmonitor= commit -m initial && - git config core.fsmonitor .git/hooks/fsmonitor-test && - cat >.gitignore <<-\EOF - .gitignore - expect* - actual* - marker* - trace2* - EOF +test_expect_success SEMANTIC_VERIFY_ANCHORED_OPEN \ + 'FSCF survives index I/O and generic rewrites' ' + test_when_finished "rm -rf fscf-round-trip" && + test_create_repo fscf-round-trip && + ( + cd fscf-round-trip && + test_commit base tracked && + test-tool read-cache --test-fscf-round-trip && + test_grep FSCF .git/index + ) ' -# test that the fsmonitor extension is off by default -test_expect_success 'fsmonitor extension is off by default' ' - test-tool dump-fsmonitor >actual && - test_grep "^no fsmonitor" actual +test_expect_success 'hook parser ignores empty path records' ' + test_when_finished "rm -rf empty-hook-record" && + test_create_repo empty-hook-record && + ( + cd empty-hook-record && + test_commit base tracked && + test_hook --setup fsmonitor-test <<-\EOF && + printf "token\0" + printf "\0" + printf "tracked\0" + EOF + git config core.fsmonitor .git/hooks/fsmonitor-test && + git config core.fsmonitorHookVersion 2 && + echo changed >tracked && + git status --porcelain --untracked-files=no >actual && + echo " M tracked" >expect && + test_cmp expect actual + ) ' -# test that "update-index --fsmonitor" adds the fsmonitor extension -test_expect_success 'update-index --fsmonitor" adds the fsmonitor extension' ' - git update-index --fsmonitor && - test-tool dump-fsmonitor >actual && - test_grep "^fsmonitor last update" actual +test_expect_success UNTRACKED_CACHE 'trivial hook clears a paired UNTR token' ' + test_when_finished "rm -rf hook-token-pair" && + test_create_repo hook-token-pair && + ( + cd hook-token-pair && + test_commit base tracked && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_hook --setup fsmonitor-test <<-\EOF && + printf "token1\0" + EOF + git config core.fsmonitor .git/hooks/fsmonitor-test && + git config core.fsmonitorHookVersion 2 && + git update-index --fsmonitor && + test_grep ! FSUC .git/index && + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_grep FSUC .git/index && + test_hook --clobber fsmonitor-test <<-\EOF && + printf "token2\0/\0" + EOF + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_grep ! FSUC .git/index + ) ' -# test that "update-index --no-fsmonitor" removes the fsmonitor extension -test_expect_success 'update-index --no-fsmonitor" removes the fsmonitor extension' ' - git update-index --no-fsmonitor && - test-tool dump-fsmonitor >actual && - test_grep "^no fsmonitor" actual +test_expect_success UNTRACKED_CACHE 'failed hook clears a paired UNTR token' ' + test_when_finished "rm -rf hook-token-error" && + test_create_repo hook-token-error && + ( + cd hook-token-error && + test_commit base tracked && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain=v2 >/dev/null && + test_hook --setup fsmonitor-test <<-\EOF && + printf "token1\0" + EOF + git config core.fsmonitor .git/hooks/fsmonitor-test && + git config core.fsmonitorHookVersion 2 && + git update-index --fsmonitor && + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_grep FSUC .git/index && + test_hook --clobber fsmonitor-test <<-\EOF && + exit 1 + EOF + git status --porcelain=v2 >.git/actual && + test_must_be_empty .git/actual && + test_grep ! FSUC .git/index + ) ' -cat >expect <cached/empty/deep/tracked && + git add cached/empty/deep/tracked && + git commit -m base && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain=v2 \ + >.git/prime && + test_must_be_empty .git/prime && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime-fsmonitor && + test_must_be_empty .git/prime-fsmonitor && + test_grep FSUC .git/index && -# test that "update-index --fsmonitor-valid" sets the fsmonitor valid bit -test_expect_success 'update-index --fsmonitor-valid" sets the fsmonitor valid bit' ' - test_hook fsmonitor-test<<-\EOF && - printf "last_update_token\0" - EOF - git update-index --fsmonitor && - git update-index --fsmonitor-valid dir1/modified && - git update-index --fsmonitor-valid dir2/modified && - git update-index --fsmonitor-valid modified && - git ls-files -f >actual && - test_cmp expect actual + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ + git status --porcelain=v2 >.git/clean && + test_must_be_empty .git/clean && + test_trace2_data read_directory directories-visited 0 \ + <.git/clean.trace && + + test_write_lines untracked >cached/empty/deep/new && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=DCCC \ + GIT_TEST_FSMONITOR_QUERY_PATH=cached/empty/deep/new \ + git status --porcelain=v2 >.git/changed && + test_grep "^? cached/empty/deep/new$" .git/changed + ) ' -cat >expect <cached/.gitignore && + test_write_lines hidden >cached/ignored && + test_write_lines ignored >cached2/.gitignore && + test_write_lines hidden >cached2/ignored && + test_write_lines ignored >cached3/.gitignore && + test_write_lines hidden >cached3/ignored && + git add cached/.gitignore cached2/.gitignore \ + cached3/.gitignore && + git commit -m base && + test-tool chmtime +60 cached2/.gitignore && + test-tool chmtime =-60 cached3/.gitignore && + git update-index --refresh && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain=v2 \ + >.git/prime && + test_must_be_empty .git/prime && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain=v2 >.git/prime-fsmonitor && + test_must_be_empty .git/prime-fsmonitor && + test_grep FSUC .git/index && + ln cached/.gitignore ../fsmonitor-untracked-exclude-alias && -# test that "update-index --no-fsmonitor-valid" clears the fsmonitor valid bit -test_expect_success 'update-index --no-fsmonitor-valid" clears the fsmonitor valid bit' ' - git update-index --no-fsmonitor-valid dir1/modified && - git update-index --no-fsmonitor-valid dir2/modified && - git update-index --no-fsmonitor-valid modified && - git ls-files -f >actual && - test_cmp expect actual + if test_have_prereq PTHREADS + then + threads=2 + else + threads=1 + fi && + if test_have_prereq MINGW || test_have_prereq CYGWIN + then + index_excludes=0 + else + index_excludes=1 + fi && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT_NESTING=10 \ + GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ + git status --porcelain >.git/clean && + test_must_be_empty .git/clean && + test_trace2_data dir \ + preload_untracked_cache/fsmonitor-excludes-only 1 \ + <.git/clean.trace && + test_trace2_data dir preload_untracked_cache/threads \ + $threads \ + <.git/clean.trace && + test_trace2_data dir preload_untracked_cache/dirs 3 \ + <.git/clean.trace && + test_trace2_data dir \ + preload_untracked_cache/index-excludes "$index_excludes" \ + <.git/clean.trace && + test_trace2_data read_directory directories-visited 0 \ + <.git/clean.trace && + + if test_have_prereq FILEMODE + then + chmod +x ../fsmonitor-untracked-exclude-alias && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/mode.trace" \ + git status --porcelain >.git/mode && + test_trace2_data dir \ + preload_untracked_cache/index-uptodate 2 \ + <.git/mode.trace && + chmod -x ../fsmonitor-untracked-exclude-alias + else + : + fi && + + mtime=$(test-tool chmtime --get cached/.gitignore) && + test_write_lines visible \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/changed.trace" \ + git status >.git/changed && + test_grep "modified:.*cached/.gitignore" .git/changed && + test_grep "cached/ignored" .git/changed && + test_trace2_data status \ + fsmonitor/exclude-index-invalidated 1 \ + <.git/changed.trace && + + test_write_lines ignored \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/restored.trace" \ + git status >.git/restored && + test_grep "nothing to commit, working tree clean" \ + .git/restored && + + test_write_lines "?? cached/ignored" >.git/flagged.expect && + + git update-index --assume-unchanged cached/.gitignore && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain >.git/assume-prime && + test_must_be_empty .git/assume-prime && + mtime=$(test-tool chmtime --get cached/.gitignore) && + test_write_lines visible \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/assume-changed.trace" \ + git status --porcelain >.git/assume-changed && + test_cmp .git/flagged.expect .git/assume-changed && + test_write_lines ignored \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/assume-restored.trace" \ + git status --porcelain >.git/assume-restored && + test_must_be_empty .git/assume-restored && + git update-index --no-assume-unchanged cached/.gitignore && + + git update-index --skip-worktree cached/.gitignore && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain >.git/skip-prime && + test_must_be_empty .git/skip-prime && + mtime=$(test-tool chmtime --get cached/.gitignore) && + test_write_lines visible \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/skip-changed.trace" \ + git status --porcelain >.git/skip-changed && + test_cmp .git/flagged.expect .git/skip-changed && + test_write_lines ignored \ + >../fsmonitor-untracked-exclude-alias && + test-tool chmtime =$mtime cached/.gitignore && + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=2 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/skip-restored.trace" \ + git status --porcelain >.git/skip-restored && + test_must_be_empty .git/skip-restored && + git update-index --no-skip-worktree cached/.gitignore + ) ' -cat >expect <cached/.gitignore && + test_write_lines hidden >cached/ignored && + test_write_lines "cached/.gitignore text eol=lf" \ + >.gitattributes && + git add .gitattributes cached/.gitignore && + git commit -m base && + git config core.untrackedCache true && + git -c core.fsmonitor=false status --porcelain >.git/prime && + test_must_be_empty .git/prime && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status --porcelain >.git/prime-fsmonitor && + test_must_be_empty .git/prime-fsmonitor && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + git status >.git/settle && + test_grep "nothing to commit, working tree clean" \ + .git/settle && -# test that all files returned by the script get flagged as invalid -test_expect_success 'all files returned by integration script get flagged as invalid' ' - write_integration_script && - dirty_repo && - git update-index --fsmonitor && - git ls-files -f >actual && - test_cmp expect actual + GIT_OPTIONAL_LOCKS=0 \ + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCC \ + GIT_TRACE2_EVENT="$PWD/.git/clean.trace" \ + git status >.git/clean && + test_grep "nothing to commit, working tree clean" \ + .git/clean && + test_trace2_data dir \ + preload_untracked_cache/index-uptodate 1 \ + <.git/clean.trace && + test_trace2_data dir \ + preload_untracked_cache/index-invalidated 0 \ + <.git/clean.trace && + test_trace2_data dir \ + preload_untracked_cache/normalized-excludes 0 \ + <.git/clean.trace && + test_trace2_data read_directory directories-visited 0 \ + <.git/clean.trace + ) ' -cat >expect <one/.gitignore + else + printf ignored >one/.gitignore + fi && + cp one/.gitignore two/.gitignore && + test_write_lines hidden >one/ignored && + test_write_lines hidden >two/ignored && + git add one/.gitignore two/.gitignore && + git commit -qm base && + test-tool chmtime -120 one/.gitignore two/.gitignore && + git update-index --refresh && + git config core.untrackedCache true && + raw=$(git rev-parse :one/.gitignore) && + normalized=$( + { cat one/.gitignore && printf "\n"; } | + git hash-object --stdin + ) && + test "$raw" != "$normalized" && -# test that newly added files are marked valid -test_expect_success 'newly added files are marked valid' ' - test_hook --setup --clobber fsmonitor-test<<-\EOF && - printf "last_update_token\0" - EOF - git add new && - git add dir1/new && - git add dir2/new && - git ls-files -f >actual && - test_cmp expect actual + # Exercise the genuine historical add_patterns() encoding. + git update-index --assume-unchanged \ + one/.gitignore two/.gitignore && + git -c core.fsmonitor=false status --porcelain=v2 \ + >.git/historical && + test_must_be_empty .git/historical && + test-tool dump-untracked-cache >.git/historical.dump && + test_grep "^/one/ $normalized .*valid" \ + .git/historical.dump && + test_grep "^/two/ $normalized .*valid" \ + .git/historical.dump && + git update-index --no-assume-unchanged \ + one/.gitignore two/.gitignore && + git config core.fsmonitor true && + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=C \ + git update-index --fsmonitor && + GIT_INDEX_FILE="$PWD/.git/index" \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + git status --porcelain=v2 >.git/prime && + test_must_be_empty .git/prime && + test_grep FSMN .git/index && + test_grep FSUC .git/index && + test_grep FSCF .git/index && + cat >.git/restore-historical-excludes.pl <<-\EOF && + use Digest::SHA qw(sha1 sha256); + binmode STDIN; + binmode STDOUT; + local $/; + my $index = ; + my ($algorithm, $raw_hex, $normalized_hex) = @ARGV; + my $size = $algorithm eq "sha256" ? 32 : 20; + my $body = substr($index, 0, -$size); + my $offset = index($body, "UNTR"); + die "missing UNTR extension\n" if $offset < 0; + my $length = unpack("N", substr($body, $offset + 4, 4)); + die "invalid UNTR size\n" + if $offset + 8 + $length > length($body); + my $payload = substr($body, $offset + 8, $length); + my $raw = pack("H*", $raw_hex); + my $normalized = pack("H*", $normalized_hex); + my $cursor = 0; + my $replaced = 0; + while (($cursor = index($payload, $raw, $cursor)) >= 0) { + substr($payload, $cursor, $size, $normalized); + $cursor += $size; + $replaced++; + } + die "expected exactly two historical excludes\n" + unless $replaced == 2; + substr($body, $offset + 8, $length, $payload); + print $body, + $size == 32 ? sha256($body) : sha1($body); + EOF + perl .git/restore-historical-excludes.pl \ + "$(test_oid algo)" "$raw" "$normalized" \ + <.git/index >.git/index.historical && + mv .git/index.historical .git/index && + test-tool dump-untracked-cache >.git/restored.dump && + test_grep "^/one/ $normalized " .git/restored.dump && + test_grep "^/two/ $normalized " .git/restored.dump && + GIT_OPTIONAL_LOCKS=0 \ + git -c core.fsmonitor=false \ + -c core.untrackedCache=false \ + status --porcelain=v2 >.git/expect && + cp .git/index .git/readonly.index && + for run in first second + do + GIT_OPTIONAL_LOCKS=0 \ + GIT_TEST_UNTRACKED_CACHE_THREADS=1 \ + GIT_TEST_FSMONITOR_QUERY_SEQUENCE=CCCCCCCC \ + GIT_TRACE2_EVENT="$PWD/.git/$run.trace" \ + git status --porcelain=v2 \ + >".git/$run.actual" && + test_cmp .git/expect ".git/$run.actual" && + test_cmp_bin .git/readonly.index .git/index && + test_trace2_data fsmonitor config/coherent 1 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/index-excludes 1 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/index-normalized-excludes 1 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/index-normalized-objects 1 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/index-uptodate 2 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/index-invalidated 0 \ + <".git/$run.trace" && + test_trace2_data dir \ + preload_untracked_cache/normalized-excludes 2 \ + <".git/$run.trace" && + ! test_trace2_data fsmonitor \ + semantic/manifest-scan-count 1 \ + <".git/$run.trace" && + ! test_trace2_data index refresh/sum_lstat \ + "[1-9][0-9]*" <".git/$run.trace" && + ! test_region index do_write_index \ + ".git/$run.trace" || return 1 + done + ) || return 1 + done ' -cat >expect <