Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions lib/bash/git/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,16 @@ log_info "Current branch: $branch"
directories return success with an empty result variable.
- `git_update_repo` changes into the target repository while it runs because
its submodule update sequence depends on repository-relative execution.
- `git_update_repo` only treats an allowed dirty path as safe when every tracked
change stays within that path. Rename records must have both source and
destination inside the allowed path.
- `check_script_up_to_date` treats missing git state, untracked scripts, or missing upstreams as skip conditions rather than hard failures.
- `check_script_up_to_date <script>` compares `HEAD` with the local remote-tracking upstream ref. It does not fetch by default, so the result reflects the freshness of local refs.
- `check_script_up_to_date --fetch <script>` runs `git fetch --quiet` first, then compares against the refreshed upstream ref. If fetch fails, the helper logs a warning and falls back to local remote-tracking refs.
- `check_script_up_to_date` returns `2` when the repository is behind upstream,
and `3` when the script has local modifications. If both are true, local
modifications take precedence and the helper returns `3` after logging both
conditions.

## Tests

Expand Down
21 changes: 18 additions & 3 deletions lib/bash/git/lib_git.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@ readonly __lib_git_sourced__=1
#
# @param $1 allowed_path Path in repository root that may be dirty (for example "shared").
#
_git_path_matches_allowed_path() {
local path="$1" allowed_path="$2"

[[ "$path" == "$allowed_path" || "$path" == "$allowed_path/"* ]]
}

_git_only_path_dirty() {
local allowed_path="$1"
local status_output line path
local status_output line path source_path destination_path

status_output="$(git status --porcelain --untracked-files=no --ignore-submodules=none)"
[[ -z "$status_output" ]] && return 1
Expand All @@ -26,9 +32,15 @@ _git_only_path_dirty() {
[[ -z "$line" ]] && continue
path="${line:3}"
if [[ "$path" == *" -> "* ]]; then
path="${path#* -> }"
source_path="${path%% -> *}"
destination_path="${path#* -> }"
if ! _git_path_matches_allowed_path "$source_path" "$allowed_path" ||
! _git_path_matches_allowed_path "$destination_path" "$allowed_path"; then
return 1
fi
continue
fi
if [[ "$path" != "$allowed_path" && "$path" != "$allowed_path/"* ]]; then
if ! _git_path_matches_allowed_path "$path" "$allowed_path"; then
return 1
fi
done <<< "$status_output"
Expand Down Expand Up @@ -353,6 +365,9 @@ check_script_up_to_date() {
ahead=$(git -C "$repo_root" rev-list --count "$upstream"..HEAD 2>/dev/null)
if [[ -n "$behind" && "$behind" -gt 0 ]]; then
log_warn "Repository is $behind commit(s) behind $upstream. Script may be out of date."
if [[ "$dirty" == true ]]; then
return 3
fi
return 2
elif [[ -n "$ahead" && "$ahead" -gt 0 ]]; then
log_info "Repository is $ahead commit(s) ahead of $upstream."
Expand Down
61 changes: 61 additions & 0 deletions lib/bash/git/tests/lib_git.bats
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,44 @@ setup() {
[ "$rc" -eq 1 ]
}

@test "_git_only_path_dirty rejects renames from outside the allowed path" {
local repo="$TEST_TMPDIR/repo"
local rc

init_git_repo "$repo"
mkdir -p "$repo/shared" "$repo/src"
printf 'one\n' > "$repo/src/one.txt"
commit_all "$repo" "Initial commit"
git -C "$repo" mv src/one.txt shared/one.txt

pushd "$repo" >/dev/null
set +e
_git_only_path_dirty "shared"
rc=$?
set -e
popd >/dev/null

[ "$rc" -eq 1 ]
}

@test "_git_only_path_dirty accepts renames inside the allowed path" {
local repo="$TEST_TMPDIR/repo"
local rc

init_git_repo "$repo"
mkdir -p "$repo/shared"
printf 'one\n' > "$repo/shared/one.txt"
commit_all "$repo" "Initial commit"
git -C "$repo" mv shared/one.txt shared/two.txt

pushd "$repo" >/dev/null
_git_only_path_dirty "shared"
rc=$?
popd >/dev/null

[ "$rc" -eq 0 ]
}

@test "git_update_repo cleans up temp log without changing RETURN trap" {
local repo="$TEST_TMPDIR/repo"
local temp_dir="$TEST_TMPDIR/git-temp"
Expand Down Expand Up @@ -597,3 +635,26 @@ setup() {
[ "$status" -eq 3 ]
[[ "$output" == *"has local modifications"* ]]
}

@test "check_script_up_to_date returns 3 when a script is both behind and dirty" {
local other="$TEST_TMPDIR/other"
local repo="$TEST_TMPDIR/repo"
local remote="$TEST_TMPDIR/remote.git"
local script_path="$repo/scripts/tool.sh"

create_tracked_repo_with_upstream "$repo" "$remote" "scripts/tool.sh" "#!/usr/bin/env bash"
git clone "$remote" "$other" >/dev/null 2>&1
git -C "$other" config user.name "Bats Test"
git -C "$other" config user.email "bats@example.com"
printf 'echo remote\n' >> "$other/scripts/tool.sh"
git -C "$other" add scripts/tool.sh
git -C "$other" commit -m "Update remote script" >/dev/null 2>&1
git -C "$other" push origin main >/dev/null 2>&1
printf 'echo dirty\n' >> "$script_path"

bats_run check_script_up_to_date --fetch "$script_path"

[ "$status" -eq 3 ]
[[ "$output" == *"has local modifications"* ]]
[[ "$output" == *"Repository is 1 commit(s) behind origin/main"* ]]
}
Loading