Replace finite clone/worktree boolean allowlists with the closed separate-value grammar, including Git's accepted long abbreviations and bundled short options. Keep placement operands distinct from HOME-valued sources, metadata, commit-ish values, and rule-generated --no-* flags while preserving separate-git-dir and later-command traps. Canonicalize shell-known HOME spellings, dot aliases, and existing symlink parents before placement comparison. Expand the hermetic suite from 242 to 292 fixtures and document the requirements and review evidence. Deliberate residuals: a future unclassified value-taking clone placement option can fail open, and a future worktree value option can shift the inferred path; defaulting it to flag grammar avoids present-day over-blocking of Git's non-enumerable boolean family. PreToolUse symlink canonicalization is non-atomic against replacement after inspection; architectural closure is tracked by #1199.
This commit is contained in:
@@ -40,6 +40,12 @@ FIXTURES="$TMP/fixtures.tsv"
|
||||
printf '2\t{"tool_input":{"command":"git clone x \\"$HOME\\"/wt"}}\ta closing quote between HOME and slash does not hide the path\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone x ${HOME}/wt"}}\tthe braced HOME spelling is the same home path\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone x \\"${HOME}\\"/wt"}}\tbraced HOME may also end a quoted span before the slash\n'
|
||||
# Lexically equivalent absolute paths must be compared after shell-known HOME
|
||||
# expansion and dot-segment normalization, without resolving filesystem links.
|
||||
printf '2\t{"tool_input":{"command":"git clone x /var/../$HOME/wt"}}\tHOME expansion after parent traversal is normalized before comparison\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add /var/../${HOME}/wt"}}\tworktree placement also normalizes embedded HOME expansion\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone --separate-git-dir=/var/../$HOME/gd x /src/wt"}}\tseparate Git state cannot hide behind parent traversal\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone x $HOME/../outside-home/wt"}}\ta parent segment that leaves HOME is not over-blocked\n'
|
||||
# The target may be HOME itself. End-of-command and whitespace terminate the
|
||||
# token just as a slash does; punctuation that can extend a path does not.
|
||||
printf '2\t{"tool_input":{"command":"git clone x $HOME"}}\tthe unbraced variable may name HOME exactly\n'
|
||||
@@ -100,6 +106,57 @@ FIXTURES="$TMP/fixtures.tsv"
|
||||
printf '0\t{"tool_input":{"command":"git clone --template $HOME/t https://example.invalid/x /src/wt"}}\ta space-separated HOME template remains a source\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone 2>/dev/null https://example.invalid/x $HOME/wt"}}\ta redirection before clone arguments does not become the destination\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone --reference $HOME https://example.invalid/x $HOME/wt"}}\ta source option does not hide a later HOME destination\n'
|
||||
# Round eleven: Git accepts boolean options as a rule-generated family,
|
||||
# including --no-* negations. Each command below was checked with Git itself:
|
||||
# `git clone <option> /nonexistent-src /nonexistent-dst` reaches the missing
|
||||
# source instead of reporting an unknown option. The HOME word is the source,
|
||||
# not the explicit /src destination, so Bash expansion is allowed here.
|
||||
printf '0\t{"tool_input":{"command":"git clone --bare $HOME/source /src/wt"}}\tbare clone keeps its HOME source distinct from the safe destination\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --mirror $HOME/source /src/wt"}}\tmirror is an accepted flag and does not consume the HOME source\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --ipv4 $HOME/source /src/wt"}}\tipv4 is an accepted flag and does not consume the HOME source\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --ipv6 $HOME/source /src/wt"}}\tipv6 is an accepted flag and does not consume the HOME source\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-local $HOME/source /src/wt"}}\tgenerated no-local remains a flag rather than a placement option\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-reject-shallow $HOME/source /src/wt"}}\tgenerated no-reject-shallow remains a flag rather than placement\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone -4 $HOME/source /src/wt"}}\tthe short IPv4 flag leaves the HOME word in source position\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone -6 $HOME/source /src/wt"}}\tthe short IPv6 flag leaves the HOME word in source position\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-bare $HOME/source /src/wt"}}\tan unusual generated negation is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-sparse $HOME/source /src/wt"}}\tgenerated no-sparse is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-dissociate $HOME/source /src/wt"}}\tgenerated no-dissociate is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-shallow-submodules $HOME/source /src/wt"}}\ta long generated negation is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-quiet $HOME/source /src/wt"}}\tgenerated no-quiet is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-progress $HOME/source /src/wt"}}\tgenerated no-progress is accepted without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone --no-recurse-submodules $HOME/source /src/wt"}}\tgenerated no-recurse-submodules is accepted without enumeration\n'
|
||||
# Git also generates accepted long abbreviations and short-option bundles.
|
||||
# The closed value-taking option grammar must consume their values correctly.
|
||||
printf '0\t{"tool_input":{"command":"git clone --templ $HOME/t $HOME/source /src/wt"}}\tan accepted template abbreviation consumes metadata rather than the source\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone -qj 1 $HOME/source /src/wt"}}\ta short flag bundle ending in jobs consumes its separate value\n'
|
||||
printf '0\t{"tool_input":{"command":"git clone -qb topic $HOME/source /src/wt"}}\ta short flag bundle ending in branch consumes its separate value\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone --separate-git-d=$HOME/gd https://example.invalid/x /src/wt"}}\tan accepted placement-option abbreviation remains blocked in attached form\n'
|
||||
printf '2\t{"tool_input":{"command":"git clone --separate-git-d $HOME/gd https://example.invalid/x /src/wt"}}\tan accepted placement-option abbreviation remains blocked in separate form\n'
|
||||
# Worktree boolean options have the same generated-negation grammar. The next
|
||||
# positional is its real path, so safe paths allow and HOME paths still block.
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --no-force /src/wt"}}\tgenerated worktree no-force accepts a safe path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --no-detach /src/wt"}}\tgenerated worktree no-detach accepts a safe path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --no-lock /src/wt"}}\tgenerated worktree no-lock accepts a safe path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --no-guess-remote /src/wt"}}\ta long worktree negation accepts a safe path without enumeration\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add -d /src/wt"}}\tthe documented short detach flag accepts a safe path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add -q /src/wt"}}\tthe documented short quiet flag accepts a safe path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --lock --rea $HOME/note /src/wt"}}\tan accepted reason abbreviation consumes metadata rather than the path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add -fb $HOME/topic /src/wt"}}\ta short branch bundle consumes its HOME-valued branch before the safe path\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add -fb topic $HOME/wt"}}\ta short branch bundle does not hide the later HOME path\n'
|
||||
# Upstream Git defines --orphan as a boolean flag; -b still carries the branch.
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --orphan /src/wt"}}\torphan mode accepts a safe path without consuming it as a value\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add --orphan $HOME/wt"}}\torphan mode does not hide its HOME path\n'
|
||||
printf '0\t{"tool_input":{"command":"git worktree add --orphan -b $HOME/topic /src/wt"}}\torphan mode leaves HOME branch metadata to the branch option\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add --orphan -b topic $HOME/wt"}}\torphan mode plus a branch option preserves HOME path blocking\n'
|
||||
# The optional second positional is commit-ish metadata, never placement.
|
||||
# HOME expands here, but the explicit worktree path remains safely under /src.
|
||||
printf '0\t{"tool_input":{"command":"git worktree add /src/wt $HOME/topic"}}\ta HOME-shaped commit-ish is not the worktree path\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add --no-force $HOME/wt"}}\ta generated worktree negation does not hide the HOME path\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add --no-guess-remote $HOME/wt"}}\ta long worktree negation preserves HOME placement blocking\n'
|
||||
# Explicit placement options and later simple commands remain traps.
|
||||
printf '2\t{"tool_input":{"command":"git clone --bare $HOME/source /src/wt && git clone x $HOME/wt"}}\ta boolean flag in one command does not hide a later HOME destination\n'
|
||||
printf '2\t{"tool_input":{"command":"git worktree add --no-force /src/wt; git clone x $HOME/wt"}}\ta worktree flag before a boundary does not hide later HOME placement\n'
|
||||
# Routing this arm through the shared name site also repaired an over-block it
|
||||
# had carried from the start: the old whole-command regex found `git` INSIDE a
|
||||
# longer word, so these two were refused at every head before this commit.
|
||||
@@ -539,8 +596,34 @@ home_case 'quotes around the exact literal HOME path do not change the target' \
|
||||
2 '/home/tester' 'git clone https://example.invalid/x "/home/tester"' 'checks a repository out under'
|
||||
home_case 'a quoted literal HOME segment remains contiguous with the suffix' \
|
||||
2 '/home/tester' 'git clone https://example.invalid/x "/home/tester"/wt' 'checks a repository out under'
|
||||
home_case 'a repeated leading slash is the same absolute HOME path' \
|
||||
2 '/home/tester' 'git clone https://example.invalid/x //home/tester/wt' 'checks a repository out under'
|
||||
home_case 'dot segments cannot disguise the literal HOME path' \
|
||||
2 '/home/tester' 'git worktree add /var/../home/tester/./wt' 'checks a repository out under'
|
||||
home_case 'parent traversal into HOME is normalized for separate Git state' \
|
||||
2 '/home/tester' 'git clone --separate-git-dir=/home/other/../tester/gd x /src/wt' 'checks a repository out under'
|
||||
home_case 'normalization still permits a literal HOME sibling' \
|
||||
0 '/home/tester' 'git clone x /home/tester/../tester-sibling/wt'
|
||||
home_case 'and the unexpanded $HOME spelling, which needs no resolution at all' \
|
||||
2 '/home/tester' 'git worktree add $HOME/wt topic' 'checks a repository out under'
|
||||
|
||||
# Resolve the longest existing parent physically before appending a nonexistent
|
||||
# destination. Lexical normalization alone cannot see a symlink into HOME, and
|
||||
# it applies `..` in the wrong order when the preceding component is a symlink.
|
||||
SYMLINK_HOME="$TMP/symlink-home"
|
||||
SYMLINK_SAFE="$TMP/symlink-safe"
|
||||
mkdir -p "$SYMLINK_HOME/nested" "$SYMLINK_SAFE"
|
||||
ln -s "$SYMLINK_HOME" "$TMP/home-link"
|
||||
ln -s "$SYMLINK_HOME/nested" "$TMP/home-nested-link"
|
||||
ln -s "$SYMLINK_SAFE" "$TMP/safe-link"
|
||||
home_case 'a clone path through a symlink into HOME is refused' \
|
||||
2 "$SYMLINK_HOME" "git clone x $TMP/home-link/wt" 'checks a repository out under'
|
||||
home_case 'a worktree path through a symlink into HOME is refused' \
|
||||
2 "$SYMLINK_HOME" "git worktree add $TMP/home-link/wt" 'checks a repository out under'
|
||||
home_case 'symlink resolution occurs before a following parent segment' \
|
||||
2 "$SYMLINK_HOME" "git clone x $TMP/home-nested-link/../wt" 'checks a repository out under'
|
||||
home_case 'a symlink to a physical path outside HOME remains allowed' \
|
||||
0 "$SYMLINK_HOME" "git clone x $TMP/safe-link/wt"
|
||||
# The fail-closed arm is scoped to checkouts. If it were not, a seat with no
|
||||
# HOME would have every command it runs refused, which is how a guard gets
|
||||
# disabled rather than fixed.
|
||||
|
||||
@@ -288,6 +288,104 @@ case "${HOME-}" in
|
||||
/?*) HOME_DIR="$HOME"; home_known=1 ;;
|
||||
esac
|
||||
|
||||
# Resolve shell-known HOME spellings without evaluating arbitrary expansions.
|
||||
# Path mode already encoded quoted/escaped dollar and tilde markers, so only
|
||||
# expansion-capable spellings reach these token replacements.
|
||||
expand_known_home() {
|
||||
local path="$1"
|
||||
awk -v path="$path" -v home="$HOME_DIR" '
|
||||
function replace_home_token(value, token, bounded, out, pos, rest, nextc) {
|
||||
out = ""
|
||||
while ((pos = index(value, token)) > 0) {
|
||||
rest = substr(value, pos + length(token))
|
||||
nextc = substr(rest, 1, 1)
|
||||
if (!bounded || nextc == "" || nextc !~ /[[:alnum:]_]/) {
|
||||
out = out substr(value, 1, pos - 1) home
|
||||
value = rest
|
||||
} else {
|
||||
out = out substr(value, 1, pos + length(token) - 1)
|
||||
value = rest
|
||||
}
|
||||
}
|
||||
return out value
|
||||
}
|
||||
BEGIN {
|
||||
path = replace_home_token(path, "${HOME}", 0)
|
||||
path = replace_home_token(path, "$HOME", 1)
|
||||
if (path == "~" || substr(path, 1, 2) == "~/") {
|
||||
path = home substr(path, 2)
|
||||
}
|
||||
printf "%s", path
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# Collapse repeated separators and dot segments after filesystem resolution.
|
||||
lexically_normalize_absolute_path() {
|
||||
local path="$1"
|
||||
awk -v path="$path" '
|
||||
BEGIN {
|
||||
if (substr(path, 1, 1) != "/") {
|
||||
printf "%s", path
|
||||
exit
|
||||
}
|
||||
count = split(path, component, "/")
|
||||
depth = 0
|
||||
for (i = 1; i <= count; i++) {
|
||||
if (component[i] == "" || component[i] == ".") continue
|
||||
if (component[i] == "..") {
|
||||
if (depth > 0) depth--
|
||||
continue
|
||||
}
|
||||
normalized[++depth] = component[i]
|
||||
}
|
||||
printf "/"
|
||||
for (i = 1; i <= depth; i++) {
|
||||
if (i > 1) printf "/"
|
||||
printf "%s", normalized[i]
|
||||
}
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# Resolve the longest existing directory prefix physically, then append and
|
||||
# normalize the nonexistent suffix. Resolving before collapsing `..` matters:
|
||||
# the kernel follows a symlink first, then applies the parent segment. This is a
|
||||
# pre-execution check, so a concurrent symlink replacement remains an inherent
|
||||
# TOCTOU residual; existing aliases are nevertheless adjudicated correctly.
|
||||
canonicalize_placement_path() {
|
||||
local expanded candidate suffix leaf physical
|
||||
expanded="$(expand_known_home "$1")"
|
||||
case "$expanded" in
|
||||
/*) ;;
|
||||
*) printf '%s' "$expanded"; return 0 ;;
|
||||
esac
|
||||
|
||||
candidate="$expanded"
|
||||
suffix=""
|
||||
while [ "$candidate" != "/" ] && [ "${candidate%/}" != "$candidate" ]; do
|
||||
candidate="${candidate%/}"
|
||||
done
|
||||
while [ ! -d "$candidate" ]; do
|
||||
[ "$candidate" = "/" ] && break
|
||||
leaf="${candidate##*/}"
|
||||
suffix="/$leaf$suffix"
|
||||
candidate="${candidate%/*}"
|
||||
[ -n "$candidate" ] || candidate="/"
|
||||
done
|
||||
|
||||
physical="$(cd -P -- "$candidate" 2>/dev/null && pwd -P)" || return 1
|
||||
lexically_normalize_absolute_path "$physical$suffix"
|
||||
}
|
||||
|
||||
HOME_CANON=""
|
||||
if [ "$home_known" -eq 1 ]; then
|
||||
if ! HOME_CANON="$(canonicalize_placement_path "$HOME_DIR")"; then
|
||||
HOME_CANON=""
|
||||
home_known=0
|
||||
fi
|
||||
fi
|
||||
|
||||
W="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
[ -x "$W/pr-review.sh" ] || W="$HOME_DIR/.config/mosaic/tools/git"
|
||||
|
||||
@@ -317,7 +415,7 @@ W="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# as `${HOME}` would create a home path the shell never resolves.
|
||||
home_re='~|\$HOME|\$\{HOME\}'
|
||||
if [ "$home_known" -eq 1 ]; then
|
||||
home_re="$home_re|$(printf '%s' "$HOME_DIR" | sed 's/[][\.*^$+?(){}|]/\\&/g')"
|
||||
home_re="$home_re|$(printf '%s' "$HOME_CANON" | sed 's/[][\.*^$+?(){}|]/\\&/g')"
|
||||
fi
|
||||
|
||||
# Emit only paths the checkout syntax can PLACE. The previous whole-command
|
||||
@@ -330,58 +428,89 @@ fi
|
||||
# splitting quoted whitespace; redirection markers let this scanner discard
|
||||
# redirection operands rather than mistake them for clone destinations.
|
||||
#
|
||||
# Known Git options are classified by whether they consume a value. An unknown
|
||||
# option with a separate following word is deliberately emitted as a possible
|
||||
# placement as well as left available to positional parsing: Git can add a new
|
||||
# path-taking option, and an unreadable option value must fail closed when it
|
||||
# names HOME rather than silently becoming another bypass.
|
||||
# Git options are classified by the small, closed grammar that consumes a
|
||||
# SEPARATE value. Boolean flags are deliberately not listed: Git generates a
|
||||
# `--no-` spelling for every boolean option, so that family is defined by a rule
|
||||
# and cannot be completed by enumeration. Any option not in the separate-value
|
||||
# grammar is one option token and leaves the next word in positional context.
|
||||
#
|
||||
# Git also accepts unique long-option abbreviations and bundled short options.
|
||||
# Prefix matching models the former. For a short bundle, the first value-taking
|
||||
# letter consumes the rest of that token; it consumes the next word only when it
|
||||
# is the bundle's final letter.
|
||||
#
|
||||
# Deliberate residual: a future value-taking option absent from these closed
|
||||
# lists defaults to flag grammar, so its following word remains positional. For
|
||||
# clone, that can fail open if the future option itself places repository state.
|
||||
# For worktree, it can shift which word is read as the path. Every value-taking
|
||||
# and placement option Git supports today is classified (including accepted
|
||||
# abbreviations of `--separate-git-dir`). Accepting this hypothetical future
|
||||
# ambiguity avoids failing closed on Git's unbounded present-day boolean and
|
||||
# generated-negation family.
|
||||
checkout_placements() {
|
||||
printf '%s' "$CMD_PATHS" | awk \
|
||||
-v wb="$PATH_WORD_BOUNDARY" \
|
||||
-v cb="$PATH_COMMAND_BOUNDARY" \
|
||||
-v rb="$PATH_REDIRECTION" '
|
||||
BEGIN { RS = cb; FS = wb }
|
||||
BEGIN {
|
||||
RS = cb; FS = wb
|
||||
clone_value_count = split("--origin --branch --upload-pack --template --reference --reference-if-able --depth --shallow-since --shallow-exclude --filter --server-option --jobs --config --bundle-uri --revision --ref-format", clone_value_name, " ")
|
||||
worktree_value_count = split("--reason", worktree_value_name, " ")
|
||||
clone_placement_name = "--separate-git-dir"
|
||||
}
|
||||
|
||||
function is_git(word) {
|
||||
return word ~ /(^|\/)git$/
|
||||
}
|
||||
function clone_value_option(word) {
|
||||
return word ~ /^(-[obujc]|--(origin|branch|upload-pack|template|reference|reference-if-able|depth|shallow-since|shallow-exclude|filter|server-option|jobs|config|bundle-uri|revision|ref-format))$/
|
||||
function clone_separate_value_option(word, i, c) {
|
||||
if (word ~ /^-[^-]/) {
|
||||
for (i = 2; i <= length(word); i++) {
|
||||
c = substr(word, i, 1)
|
||||
if (c ~ /[obujc]/) return i == length(word)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if (word !~ /^--/ || word ~ /^--no-/ || index(word, "=") > 0) return 0
|
||||
for (i = 1; i <= clone_value_count; i++) {
|
||||
if (index(clone_value_name[i], word) == 1) return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
function clone_flag_option(word) {
|
||||
return word ~ /^--(local|no-hardlinks|shared|dissociate|quiet|verbose|progress|no-checkout|reject-shallow|no-tags|single-branch|no-single-branch|recurse-submodules|shallow-submodules|remote-submodules|sparse|also-filter-submodules)$/ || word ~ /^-[lqvns]$/
|
||||
function clone_placement_option(word) {
|
||||
return word ~ /^--/ && word !~ /^--no-/ && index(clone_placement_name, word) == 1
|
||||
}
|
||||
function worktree_value_option(word) {
|
||||
return word ~ /^(-b|-B|--reason|--orphan)$/
|
||||
function worktree_separate_value_option(word, i, c) {
|
||||
if (word ~ /^-[^-]/) {
|
||||
for (i = 2; i <= length(word); i++) {
|
||||
c = substr(word, i, 1)
|
||||
if (c ~ /[bB]/) return i == length(word)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if (word !~ /^--/ || word ~ /^--no-/ || index(word, "=") > 0) return 0
|
||||
for (i = 1; i <= worktree_value_count; i++) {
|
||||
if (index(worktree_value_name[i], word) == 1) return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
function worktree_flag_option(word) {
|
||||
return word ~ /^--(force|detach|checkout|no-checkout|lock|guess-remote|track|no-track)$/ || word == "-f"
|
||||
}
|
||||
function attached_known_value(word) {
|
||||
return word ~ /^-[obujc].+/ || word ~ /^--(origin|branch|upload-pack|template|reference|reference-if-able|depth|shallow-since|shallow-exclude|filter|server-option|jobs|config|bundle-uri|revision|ref-format)=/ || word ~ /^(-b|-B).+/ || word ~ /^--(reason|orphan|track)=/
|
||||
}
|
||||
function emit_clone(start, count, i, j, word, options, positions, value) {
|
||||
function emit_clone(start, count, i, j, word, options, positions, equals, value) {
|
||||
delete positional
|
||||
options = 1; positions = 0
|
||||
for (i = start; i <= count; i++) {
|
||||
word = token[i]
|
||||
if (options && word == "--") { options = 0; continue }
|
||||
if (options && word ~ /^--separate-git-dir=/) {
|
||||
value = substr(word, index(word, "=") + 1)
|
||||
equals = index(word, "=")
|
||||
if (options && equals > 0 && clone_placement_option(substr(word, 1, equals - 1))) {
|
||||
value = substr(word, equals + 1)
|
||||
if (value != "") print value
|
||||
continue
|
||||
}
|
||||
if (options && word == "--separate-git-dir") {
|
||||
if (options && clone_placement_option(word)) {
|
||||
if (i < count) print token[++i]
|
||||
continue
|
||||
}
|
||||
if (options && clone_value_option(word)) { i++; continue }
|
||||
if (options && (clone_flag_option(word) || attached_known_value(word))) continue
|
||||
if (options && word ~ /^-/) {
|
||||
# Unknown separate option value: fail closed if it is HOME-shaped.
|
||||
if (word !~ /=/ && i < count) print token[i + 1]
|
||||
continue
|
||||
}
|
||||
if (options && clone_separate_value_option(word)) { i++; continue }
|
||||
if (options && word ~ /^-/) continue
|
||||
positional[++positions] = word
|
||||
}
|
||||
# clone positional 1 is the source; every later positional can only be an
|
||||
@@ -393,13 +522,10 @@ checkout_placements() {
|
||||
for (i = start; i <= count; i++) {
|
||||
word = token[i]
|
||||
if (options && word == "--") { options = 0; continue }
|
||||
if (options && worktree_value_option(word)) { i++; continue }
|
||||
if (options && (worktree_flag_option(word) || attached_known_value(word))) continue
|
||||
if (options && word ~ /^-/) {
|
||||
# Same forward-compatible fail-closed rule as clone.
|
||||
if (word !~ /=/ && i < count) print token[i + 1]
|
||||
continue
|
||||
}
|
||||
if (options && worktree_separate_value_option(word)) { i++; continue }
|
||||
if (options && word ~ /^-/) continue
|
||||
# Only the first positional is placement; the optional second one is
|
||||
# commit-ish metadata and must not impersonate the worktree path.
|
||||
print word
|
||||
return
|
||||
}
|
||||
@@ -456,7 +582,11 @@ EOF
|
||||
placement_blocked=0
|
||||
while IFS= read -r placement; do
|
||||
[ -n "$placement" ] || continue
|
||||
if printf '%s' "$placement" | grep -Eq "^($home_re)(/|$)"; then
|
||||
if ! normalized_placement="$(canonicalize_placement_path "$placement")"; then
|
||||
placement_blocked=1
|
||||
break
|
||||
fi
|
||||
if printf '%s' "$normalized_placement" | grep -Eq "^($home_re)(/|$)"; then
|
||||
placement_blocked=1
|
||||
break
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user