#!/opt/homebrew/bin/bash
#------------------------------------------------------------------------------
## Repro for: 'p4 opened' disagrees with itself after a stream Remapped edit.
set -u
## Usage:
##
## ./repro.sh [-f] 2>&1
##
## Scenario: p4 opened Disagreement after Remapped Path Injection
##
## This repro tests what happens to a workspace that already has opened
## (checked out) files when the stream spec's Paths: field gains a new
## Remapped: entry that injects an extra directory level at the top of the
## client view, e.g.:
##
## Remapped:
## ... extra_dir/...
##
## That kind of edit is a legitimate (if unusual) stream reconfiguration:
## someone decides the stream's content should appear one level deeper in
## every client workspace, so they add a Remapped rule rather than moving
## files in the depot.
##
## The problem: once that edit is live, and after a subsequent 'p4 sync'
## picks up the new mapping, the client's already-opened file(s) become
## orphaned relative to the new view. The workspace is left disagreeing
## with itself about whether anything is opened:
##
## p4 opened --> File(s) not opened on this client. (WRONG)
## p4 opened <depotpath> --> file(s) not opened on this client. (WRONG)
## p4 opened ... --> correctly lists the still-opened file.
## p4 opened -a --> correctly lists the still-opened file.
##
## In other words, the no-argument and explicit-depot-path forms of
## 'p4 opened' wrongly report nothing is opened, while the wildcard form
## 'p4 opened ...' (and 'p4 opened -a') correctly reports the file as
## opened. This is a disagreement within the same command, on the same
## workspace, at the same moment in time -- not a case of the file simply
## being outside the client view.
##
## The test uses a disposable P4-native DVCS micro-repo created with:
##
## p4 init -C0 -n
##
## This creates the default stream depot named 'stream' and configures the
## local funky-looking P4PORT value used by P4 personal servers. This
## script does not create a depot manually.
export P4CONFIG=.p4config.local
export P4ENVIRO=/dev/null/.p4enviro
declare ThisScript=${0##*/}
declare Version=1.0.0
declare -i ErrorCount=0
declare AppHome="$PWD"
declare H1="=============================================================================="
declare Log="${ThisScript%.sh}.log"
declare CmdLog=
declare ReproDir=/tmp/repro
## Micro functions.
function msg () { echo -e "${1:-Hi}"; }
function errmsg () { msg "\nError: ${1:-Unknown Error}"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }
function cmd () { msg "${2:-Executing command: $1}"; $1; return $?; }
function usage () { grep '^##' "$0" | sed 's/^##//'; exit 1; }
declare -i Force=0
declare -i Scenario=1
declare ScenarioTitle="p4 opened Disagreement after Remapped Path Injection"
#------------------------------------------------------------------------------
## Command Line Args
set +u
while [[ $# -gt 0 ]]; do
case $1 in
(-f) Force=1;;
(-h) usage;;
(-*) bail "Usage error: Unknown option ($1).";;
(*) bail "Usage error: Unknown parameter ($1).";;
esac
shift
done
set -u
if [[ "$Log" != off ]]; then
touch "$Log" || bail "Couldn't touch log file [$Log]."
## Redirect stdout and stderr to a log file.
exec > >(tee "$Log")
exec 2>&1
fi
msg "Started ${0##*/} v$Version at $(date) in $AppHome."
msg "ReproDir=$ReproDir"
[[ -d "$ReproDir" && "$Force" -eq 1 ]] && /bin/rm -rf "$ReproDir"
[[ -d "$ReproDir" ]] && bail "Old repro dir [$ReproDir] exists. Use -f to remove it first."
mkdir "$ReproDir" || bail "Could not do: mkdir $ReproDir"
cd "$ReproDir" || bail "Could not do: cd $ReproDir"
CmdLog="$ReproDir/cmd.log"
msg "$H1\nScenario $Scenario: $ScenarioTitle\n"
msg "\nPreliminary info: Show versions of p4/p4d on the PATH:"
cmd "p4 -V"
cmd "p4d -V"
msg "\nPreliminary setup: Spin up a local repo."
msg "Operating in: $PWD"
cmd "p4 init -C0 -n"
msg "\nAfter 'p4 init -C0 -n', the local repo should already have a stream depot named 'stream' and a mainline stream named //stream/main, along with a client workspace switched to that stream."
cmd "p4 info"
cmd "p4 client -o" | grep -A2 '^View:'
#------------------------------------------------------------------------------
# Scenario setup: Add and submit a file, then open it for edit.
#------------------------------------------------------------------------------
msg "\n$H1\nScenario $Scenario setup: Add, submit, then open a file for edit.\n"
msg "Adding foo.txt at the top level of the stream."
echo "Original content." > foo.txt
cmd "p4 add foo.txt"
cmd "p4 submit -d Add-foo.txt"
msg "\nOpening foo.txt for edit. This represents a normal in-progress checkout that a developer has not yet submitted."
cmd "p4 edit foo.txt"
msg "\nSanity check: 'p4 opened' correctly reports the file as opened before we touch the stream spec."
cmd "p4 opened"
#------------------------------------------------------------------------------
# Inject an extra directory level via a Remapped stream path entry.
#------------------------------------------------------------------------------
Scenario=$((Scenario+1))
msg "\n$H1\nScenario $Scenario: Add a Remapped entry that injects 'extra_dir' at the top level.\n"
msg "The mainline stream generated by p4 init starts with a single ordinary shared stream path:"
msg "\n Paths:\n share ...\n"
msg "This repro adds a Remapped entry that pushes everything down one level into extra_dir, without moving or renaming anything in the depot:"
msg "\n Remapped:\n ... extra_dir/...\n"
p4 stream -o //stream/main > main.stream.orig
## Paths: is the last field in a freshly auto-created mainline stream spec, so
## the new Remapped: field is simply appended after it.
cat main.stream.orig > main.stream
{
echo ""
echo "Remapped:"
echo -e "\t... extra_dir/..."
} >> main.stream
msg "Modified //stream/main stream spec:"
grep -v '^#' main.stream
msg "p4 -s stream -i < main.stream"
p4 -s stream -i < main.stream
msg "\n//stream/main after update:"
p4 stream -o //stream/main | grep -v '^#'
msg "\nThe client view now maps the stream's content one level deeper, into extra_dir:"
cmd "p4 client -o" | grep -A2 '^View:'
#------------------------------------------------------------------------------
# Sync to pick up the new mapping and observe the disagreement.
#------------------------------------------------------------------------------
Scenario=$((Scenario+1))
msg "\n$H1\nScenario $Scenario: Sync to pick up the new view, then compare forms of 'p4 opened'.\n"
msg "Syncing picks up the new Remapped view. Note foo.txt is added again under extra_dir/, while the previously opened foo.txt at the old top-level location is left behind on disk."
cmd "p4 sync"
msg "\nDirectory listing after the sync. Both the old top-level foo.txt (still opened for edit, orphaned relative to the new view) and the new extra_dir/foo.txt (freshly synced) exist on disk:"
cmd "ls -la ."
cmd "ls -la extra_dir"
msg "\n--- Comparing forms of 'p4 opened' ---"
msg "\n'p4 opened' with no arguments:"
p4 opened > "$CmdLog" 2>&1 || true
cat "$CmdLog"
if ! grep -q 'not opened on this client' "$CmdLog"; then
errmsg "'p4 opened' unexpectedly found the file. Expected the bare form to wrongly report nothing opened."
fi
msg "\n'p4 opened //stream/main/foo.txt' with an explicit depot path:"
p4 opened //stream/main/foo.txt > "$CmdLog" 2>&1 || true
cat "$CmdLog"
if ! grep -q 'not opened on this client' "$CmdLog"; then
errmsg "'p4 opened //stream/main/foo.txt' unexpectedly found the file. Expected the explicit depot path form to wrongly report nothing opened."
fi
msg "\n'p4 opened ...' with a wildcard:"
p4 opened ... > "$CmdLog" 2>&1 || true
cat "$CmdLog"
if ! grep -q 'edit default change' "$CmdLog"; then
errmsg "'p4 opened ...' did not find the opened file. Expected the wildcard form to correctly report it as opened."
fi
msg "\n'p4 opened -a' (all users/clients):"
p4 opened -a > "$CmdLog" 2>&1 || true
cat "$CmdLog"
if ! grep -q 'edit default change' "$CmdLog"; then
errmsg "'p4 opened -a' did not find the opened file. Expected it to correctly report it as opened."
fi
msg "\nFor comparison, 'p4 fstat foo.txt' still shows the file as opened for edit, still rooted at the pre-Remapped client path (not under extra_dir):"
cmd "p4 fstat foo.txt"
#------------------------------------------------------------------------------
# Result.
#------------------------------------------------------------------------------
msg "\n$H1\nThe Result:\n"
if [[ "$ErrorCount" -eq 0 ]]; then
rm -f "$CmdLog"
msg "Yay, we reproduced the disagreement!"
msg "\nAfter adding a Remapped stream path entry that injects 'extra_dir' at the top level, and syncing to pick up that new view, the workspace has a still-opened file (foo.txt, opened for edit before the Remapped change) that 'p4 opened' and 'p4 opened <depotpath>' wrongly report as NOT opened, while 'p4 opened ...' and 'p4 opened -a' correctly report it as opened."
msg "\nThis is not a case of the file simply falling outside the client view: it is a disagreement between different invocations of the same 'p4 opened' command on the same workspace at the same moment. The no-argument and explicit-depot-path forms appear to resolve against the file's stale, pre-Remapped client-relative path, while the wildcard/all-clients forms iterate opened records directly and find it regardless."
msg "\nThe practical impact: automation or scripts that call bare 'p4 opened' (or 'p4 opened <file>') to check for pending checkouts before doing something like 'p4 revert' or 'p4 sync -f' can be fooled into believing there is nothing opened, when in fact there is."
else
msg "One or more checks reported errors. Review the log above; the repro conditions may not have reproduced as expected on this version of p4d."
fi
exit "$ErrorCount"