trim_excess_metadata.sh #16

  • //
  • guest/
  • tom_tyler/
  • sw/
  • main/
  • tem/
  • trim_excess_metadata.sh
  • View
  • Commits
  • Open Download .zip Download (30 KB)
#!/bin/bash

declare ThisScript=${0##*/}
declare Version=2.1.3
declare CmdLine="$0 $*"
declare ThisUser=
declare ThisHost=${HOSTNAME%%.*}
declare -i ErrorCount=0
declare -i WarningCount=0
declare -i Debug=0
declare -i NoOp=1
declare ServerTag=
declare KeepBranchesFile=
declare KeepClientsFile=
declare KeepGroupsFile=
declare KeepLabelsFile=
declare KeepUsersFile=
declare AccessLevel=
declare CurrentP4User=
declare UserP4CONFIG=
declare ConfigDir=$PWD
declare Log=
declare TmpFile=
declare H1="=============================================================================="
declare H2="------------------------------------------------------------------------------"
declare UtilsList="awk cat grep ls p4 seq wc"

# Per-phase counters used in the summary. In Dry Run mode "Deleted" counts
# items that *would* have been deleted, since cmd() returns 0 without acting.
declare -i LdapSpecsDeleted=0
declare -i LdapSpecsFailed=0

declare -i ClientsTotal=0
declare -i ClientsWithView=0
declare -i ClientsKeptByFile=0
declare -i ClientsKeptByOwner=0
declare -i ClientsDeleted=0
declare -i ClientsFailed=0

declare -i BranchesTotal=0
declare -i BranchesWithView=0
declare -i BranchesKeptByFile=0
declare -i BranchesKeptByOwner=0
declare -i BranchesDeleted=0
declare -i BranchesFailed=0

declare -i LabelsTotal=0
declare -i LabelsWithView=0
declare -i LabelsKeptByFile=0
declare -i LabelsKeptByOwner=0
declare -i LabelsDeleted=0
declare -i LabelsFailed=0

declare -i UsersTotal=0
declare -i UsersKept=0
declare -i UsersDeleted=0
declare -i UsersFailed=0

declare -i ShelvesDeleted=0
declare -i ShelvesFailed=0

declare -i GroupsTotal=0
declare -i GroupsKept=0
declare -i GroupsDeleted=0
declare -i GroupsFailed=0

declare -i SubmittedCLsAttempted=0

declare -i DepotsTotal=0
declare -i DepotsEmpty=0
declare -i DepotsNonEmpty=0
declare -i DepotsDeleted=0
declare -i DepotsFailed=0

function msg () { echo -e "$*"; }
function dbg () { [[ "$Debug" -eq 0 ]] || msg "DEBUG: $*"; }
function warnmsg () { msg "Warning: ${1:-Unknown Warning}"; WarningCount+=1; }
function errmsg () { msg "Error: ${1:-Unknown Error}"; ErrorCount+=1; }
function bail () { errmsg "${1:-Unknown Error}"; exit "$ErrorCount"; }
# Summary line: fixed-width label column so values line up regardless of
# whether DeletedLabel is "Deleted" or "Would delete".
function sline () { printf "  %-42s %d\n" "${1}:" "${2}"; }

function cmd () {
   # Usage: cmd <description> <command> [args...]
   # Pass an empty string "" as description to suppress the description line.
   # Using an array (via "$@") ensures that command arguments with special
   # shell characters (e.g. '$', '(', ')') are passed verbatim without word
   # splitting or command substitution.
   local desc=${1:-}
   shift
   local -a cmdArray=("$@")
   local -i cmdExitCode=0

   [[ -n "$desc" ]] && msg "$desc"
   msg "Executing: ${cmdArray[*]}"

   if [[ "$NoOp" -eq 0 ]]; then
      "${cmdArray[@]}"
      cmdExitCode=$?
   else
      msg "NO_OP: Would have run: ${cmdArray[*]}"
   fi
   return $cmdExitCode
}

#------------------------------------------------------------------------------
# Function: terminate
# shellcheck disable=SC2317
function terminate
{
   # Disable signal trapping.
   trap - EXIT SIGINT SIGTERM

   dbg "$ThisScript: EXITCODE: $ErrorCount"

   # Stop logging.
   [[ "$Log" == off ]] || msg "\nLog is: $Log\n${H1}"

   # With the trap removed, exit with the error count.
   exit "$ErrorCount"
}

#------------------------------------------------------------------------------
# Function: usage (required function)
#
# Input:
# $1 - style, either -h (for short form) or -man (for man-page like format).
# The default is -h.
#
# $2 - error message (optional).  Specify this if usage() is called due to
# user error, in which case the given message displayed first, followed by the
# standard usage message (short or long depending on $1).  If displaying an
# error, usually $1 should be -h so that the longer usage message doesn't
# obscure the error message.
#
# Sample Usage:
# usage
# usage -h
# usage -man
# usage -h "Incorrect command line usage."
#------------------------------------------------------------------------------
function usage
{
   declare style=${1:--h}
   declare usageErrorMessage=${2:-Unset}

   if [[ "$usageErrorMessage" != Unset ]]; then
      msg "\\n\\nUsage Error:\\n\\n$usageErrorMessage\\n\\n"
   fi

   msg "USAGE for $ThisScript version $Version:

$ThisScript <ServerTag> [-L <log>] [-y] [-d|-D]

or

$ThisScript [-h|-man|-V]
"
   if [[ $style == -man ]]; then
      msg "
DESCRIPTION:

	*** WARNING WARNING WARNING WARNING ***
	This script could be dangerous if misused. Point it only at non-production servers.

	This script trims P4 metadata after a filtered replica is used in a divestiture situation.

	It should be pointed to a a commit server that was recently promoted from a filtered forwarding
	replica.  The replication filtering cleaned up path-based data.  This cleans up additional 
	metadata.

	IMPORTANT: See FILES section below for required P4CONFIG files.

	Phase 0: Preflight Checks
	Phase 1: LDAP Disconnect and Cleanup
	Phase 2: Client Cleanup (viewless clients only)
	Phase 3: Branch Cleanup (viewless branches only)
	Phase 4: Label Cleanup (viewless labels only)
	Phase 5: Group Cleanup
	Phase 6: Shelved CL Cleanup
	Phase 7: User Cleanup
	Phase 8: Stream Cleanup (**NOT IMPLEMENTED**)
	Phase 9: Job and Fix Cleanup (**NOT IMPLEMENTED**)
	Phase 10: Submitted CL Cleanup (empty CLs only)
	Phase 11: Depot Cleanup (empty depots only)

OPERATOR TIPS:
	1. Run this script in Dry Run mode first. Verify the expected results before running it with -y for a Live Run.
	2. Have a HUGE amount of space available for P4JOURNAL bloat that occurs during metadata removal.

EXTRA MANUAL STEPS
	This script DOES NOT clean up the following, which must be handled manually:

	* Protections table cleanup.
	* Triggers table cleanup.
	* Extensions removal and associated extension depot obliteration and cert cleanup.

	Other spots may be missed as well. Review the results carefully.

OPTIONS:
 -y	Live operation mode. By default, this script runs in Dry Run/Preview mode,
	where commands affecting data are displayed but not executed.  Use '-y' to
	execute after previewing.

 -d     Display debugging messages.

 -D     Set extreme debugging verbosity.

LOGGING:
	This script is self-logging.  That is, output displayed on the screen
	is simultaneously captured in the log file.  Using redirection operators
	like '> log' or '2>&1' and using 'tee' are unnecessary (but harmless).

HELP OPTIONS:
 -h	Display short help message
 -man	Display man-style help message
 -V	Display version info for this script and its libraries.

FILES:
	== P4CONFIG Files

	This script requires P4CONFIG files to be in the current directory when
	this script starts, named .p4config.<ServerTag>

	This P4CONFIG file should contain values for P4PORT, P4USER, P4TICKETS,
	and if needed, P4TRUST. These should reference the targeted server,
	which is generally a commit server recently promoted from a filtered
	forwarding replica.

	== Keep Users File

	When removing users, any users that should NOT be remove must have the P4USER
	account name listed in a \"keep users\" file in the current directory when
	this script starts.  Users are listed one per line, with no leading or
	trailing white space. If a file named keep_users.<ServerTag>.txt exists, it will
	be used. Otherwise a file named keep_users.txt will be used.  So there can be
	a single set of users for each server tag, or each server tag can have its own
	set of users.

	For example, the keep_users.txt file may look like
	this:

	admin
	bruno
	perforce

	At the very least, the \"keep users\" file must contain one user with super
	access. This file is required and this script will abort during preflight checks if
	it does not exist.

	== Keep Clients, Branches, Groups and Labels Files.

	When removing clients, branches, groups, and labels, any of these that should
	NOT be remove must have their names listed in the appropriate file, one entry
	per line:

	* keep_branches.<ServerTag>.txt OR keep_branches.txt
	* keep_clients.<ServerTag>.txt OR keep_clients.txt
	* keep_groups.<ServerTag>.txt OR keep_groups.txt
	* keep_labels.<ServerTag>.txt OR keep_labels.txt

	These optional files must appear in the directory where this script starts to
	have effect. The name including <ServerTag> takes precedence if it exists,
	otherwise the script falls back to other file name.

	In addition to the explicit lists above, any client, branch, or label whose
	'Owner:' field names a user listed in the Keep Users file is preserved
	automatically.  This avoids deleting metadata belonging to users that are
	being kept.  Note: groups are NOT subject to this owner-based rule; groups
	are kept only if listed in the Keep Groups file.

EXAMPLES:
	Example 1: Dry Run for instance 17777
	echo p4admin > keep_users.17777.txt
	$ThisScript 17777

	Example 2: Live Run for instance 17777
	$ThisScript 17777 -y
"
   fi

   exit 2
}

#==============================================================================
# Command Line Processing

declare -i shiftArgs=0

set +u
while [[ $# -gt 0 ]]; do
   case $1 in
      (-h) usage -h;;
      (-man|--help) usage -man;;
      (-V|--version) msg "$ThisScript version $Version"; exit 0;;
      (-y) NoOp=0;;
      (-d) Debug=1;;
      (-D) Debug=1; set -x;; # Use bash 'set -x' extreme debug mode.
      (-*) usage -h "Unknown option ($1).";;
      (*)
         if [[ -z "$ServerTag" ]]; then
            ServerTag="$1"
         else
            usage -h "Unknown parameter '$1'. ServerTag already set as '$ServerTag'."
         fi
      ;;
   esac

   # Shift (modify $#) the appropriate number of times.
   shift; while [[ $shiftArgs -gt 0 ]]; do
      [[ $# -eq 0 ]] && usage -h "Incorrect number of arguments."
      shiftArgs=$shiftArgs-1
      shift
   done
done
set -u

#==============================================================================
# Command Line Verification

[[ -n "$Log" ]] || \
   Log="${LOGS:-${HOME:-/tmp}}/${ThisScript%.sh}.$(date +'%Y%m%d-%H%M%S').log"

[[ -z "$ServerTag" ]] && usage -h "The <ServerTag> parameter is required unless."

#==============================================================================
# Main Program

if [[ "$Log" != off ]]; then
   exec > >(tee "$Log")
   exec 2>&1

   touch "$Log" || bail "Couldn't touch log file [$Log]."

   msg "${H1}\\nLog is: $Log"
fi

trap terminate EXIT SIGINT SIGTERM

ThisUser=$(id -n -u)
msg "Starting $ThisScript version $Version as $ThisUser@$ThisHost on $(date) as:\\n$CmdLine\\n"

if [[ "$NoOp" -eq 0 ]]; then
   msg "Operating in Live Operation mode."
else
   msg "Operating in Dry Run/Preview mode."
fi

TmpFile=$(mktemp)

msg "${H2}\nPhase 0: Preflight Checks"

for Util in $UtilsList; do
   command -v "$Util" > /dev/null || errmsg "Missing required utility '$Util'. Adjust path?"
done

UserP4CONFIG="$ConfigDir/.p4config.$ServerTag"
if [[ -r "$UserP4CONFIG" ]]; then
   msg  "\nContents of P4CONFIG [$UserP4CONFIG]:"
   cat "$UserP4CONFIG"
else
   bail "Missing user P4CONFIG file '$UserP4CONFIG'."
fi

# Look first for keep_users.<ServerTag>.txt files, then for keep_users.txt.
if [[ -r "$PWD/keep_users.$ServerTag.txt" ]]; then
   KeepUsersFile="$PWD/keep_users.$ServerTag.txt"
elif [[ -r "$PWD/keep_users.txt" ]]; then
   KeepUsersFile="$PWD/keep_users.txt"
else
   KeepUsersFile=
fi

# Look first for keep_branches.<ServerTag>.txt files, then for keep_branches.txt.
if [[ -r "$PWD/keep_branches.$ServerTag.txt" ]]; then
   KeepBranchesFile="$PWD/keep_branches.$ServerTag.txt"
elif [[ -r "$PWD/keep_branches.txt" ]]; then
   KeepBranchesFile="$PWD/keep_branches.txt"
else
   KeepBranchesFile=
fi

# Look first for keep_clients.<ServerTag>.txt files, then for keep_clients.txt.
if [[ -r "$PWD/keep_clients.$ServerTag.txt" ]]; then
   KeepClientsFile="$PWD/keep_clients.$ServerTag.txt"
elif [[ -r "$PWD/keep_clients.txt" ]]; then
   KeepClientsFile="$PWD/keep_clients.txt"
else
   KeepClientsFile=
fi

# Look first for keep_groups.<ServerTag>.txt files, then for keep_groups.txt.
if [[ -r "$PWD/keep_groups.$ServerTag.txt" ]]; then
   KeepGroupsFile="$PWD/keep_groups.$ServerTag.txt"
elif [[ -r "$PWD/keep_groups.txt" ]]; then
   KeepGroupsFile="$PWD/keep_groups.txt"
else
   KeepGroupsFile=
fi

# Look first for keep_labels.<ServerTag>.txt files, then for keep_labels.txt.
if [[ -r "$PWD/keep_labels.$ServerTag.txt" ]]; then
   KeepLabelsFile="$PWD/keep_labels.$ServerTag.txt"
elif [[ -r "$PWD/keep_labels.txt" ]]; then
   KeepLabelsFile="$PWD/keep_labels.txt"
else
   KeepLabelsFile=
fi

if [[ -n "$KeepUsersFile" ]]; then
   msg  "\nContents of Keep Users File [$KeepUsersFile]:"
   cat "$KeepUsersFile"
else
   bail "Missing Keep Users File '$KeepUsersFile'."
fi

if [[ -n "$KeepBranchesFile" ]]; then
   msg  "\nContents of Keep Branches File [$KeepBranchesFile]:"
   cat "$KeepBranchesFile"
else
   msg "\nNo Keep Branches file exists. All viewless branches will be removed."
fi

if [[ -n "$KeepLabelsFile" ]]; then
   msg  "\nContents of Keep Labels File [$KeepLabelsFile]:"
   cat "$KeepLabelsFile"
else
   msg "\nNo Keep Labels file exists. All viewless labels will be removed."
fi

if [[ -n "$KeepClientsFile" ]]; then
   msg  "\nContents of Keep Clients File [$KeepClientsFile]:"
   cat "$KeepClientsFile"
else
   msg "\nNo Keep Clients file exists. All viewless clients will be removed."
fi

if [[ -n "$KeepGroupsFile" ]]; then
   msg  "\nContents of Keep Groups File [$KeepGroupsFile]:"
   cat "$KeepGroupsFile"
else
   msg "\nNo Keep Groups file exists. All groups will be removed."
fi

unset P4ENVIRO
export P4CONFIG="$UserP4CONFIG"

AccessLevel=$(p4 protects -m)

if [[ -n "$AccessLevel" ]]; then
   if [[ "$AccessLevel" == super ]]; then
      msg "\nVerified: Max Access Level is 'super', as required."
   else
      errmsg "Max Access Level is '$AccessLevel', but 'super' is required."
   fi
else
   errmsg "Could not determine max access level using P4CONFIG=$UserP4CONFIG"
fi

# Refuse to run if the operating P4 user is not in the Keep Users file, since
# Phase 7 would otherwise delete the running user mid-script.
CurrentP4User=$(p4 set -q P4USER | sed 's/^P4USER=//')
if [[ -z "$CurrentP4User" ]]; then
   # Fall back to p4 info if p4 set didn't return a value.
   CurrentP4User=$(p4 -ztag -F %userName% info)
fi
if [[ -n "$CurrentP4User" ]]; then
   if grep -Fxq -- "$CurrentP4User" "$KeepUsersFile"; then
      msg "\nVerified: Operating P4 user '$CurrentP4User' is listed in Keep Users file."
   else
      errmsg "Operating P4 user '$CurrentP4User' is NOT listed in Keep Users file '$KeepUsersFile'. Add it before running."
   fi
else
   errmsg "Could not determine operating P4 user via 'p4 set' or 'p4 info' using P4CONFIG=$UserP4CONFIG"
fi

# Verify the current user has an explicit 'super user' entry in the Protections
# table. Phase 5 deletes all groups; if super access comes solely through a
# group membership, deleting that group would revoke super access mid-script.
ProtectSpec=$(p4 protect -o 2>/dev/null)
if [[ -n "$ProtectSpec" ]]; then
   if echo "$ProtectSpec" | grep -qE "^\s+super\s+user\s+${CurrentP4User}(\s|$)"; then
      msg "\nVerified: Explicit 'super user $CurrentP4User' entry exists in Protections table."
   else
      errmsg "No explicit 'super user $CurrentP4User' entry found in Protections table. Deleting all groups (Phase 5) may revoke super access mid-script. Add an explicit 'super user $CurrentP4User * //...' protection entry before running."
   fi
else
   errmsg "Could not read Protections table via 'p4 protect -o'."
fi

if [[ "$ErrorCount" -eq 0 ]]; then
   msg "\nAll preflight checks are OK. Proceeding."
else
   bail "Aborting early before any actions were taken due to $ErrorCount failed preflight checks."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 1: LDAP Disconnect and Cleanup"

for i in $(seq 10 -1 1); do
   if [[ $(p4 -ztag -F %Type% configure show "auth.ldap.order.$i") == "configure" ]]; then
      cmd "" p4 configure unset "auth.ldap.order.$i"
   fi
done

if [[ $(p4 -ztag -F %Type% configure show auth.default.method) == "configure" ]]; then
   cmd "" p4 configure unset auth.default.method
fi

if p4 -ztag -F %Name% ldaps > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r LDAP; do
         if cmd "Removing ldap spec '$LDAP'." p4 -s ldap -d "$LDAP"; then
            LdapSpecsDeleted+=1
         else
            LdapSpecsFailed+=1
            errmsg "Failed to remove ldap spec '$LDAP'."
         fi
      done < "$TmpFile"
   else
      msg "No ldap specs detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of ldap specs."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 2: Client Cleanup (viewless clients only)"

if [[ -n "$KeepClientsFile" ]]; then
   msg "Deleting all viewless clients except those listed in '$KeepClientsFile'."
else
   msg "Deleting all viewless clients."
fi

if p4 -ztag -F %client% clients > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r Client; do
         ClientsTotal+=1

         # Use a temp var so we can check the p4 exit code separately from the value.
         # If the p4 call itself fails (e.g. a p4 wrapper script chokes on special chars
         # in the client name), skip rather than misclassifying it as viewless.
         if ! ClientView=$(p4 -ztag -F %View0% client -o "$Client" 2>/dev/null); then
            warnmsg "Could not inspect view for client '$Client' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$ClientView" ]]; then
            dbg "Ignoring client with non-empty view: '$Client'."
            ClientsWithView+=1
            continue
         fi

         if [[ -n "$KeepClientsFile" ]] && grep -Fxq -- "$Client" "$KeepClientsFile"; then
            dbg "Ignoring \"keep\" client '$Client'."
            ClientsKeptByFile+=1
            continue
         fi

         if ! ClientOwner=$(p4 -ztag -F %Owner% client -o "$Client" 2>/dev/null); then
            warnmsg "Could not inspect owner for client '$Client' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$ClientOwner" ]] && grep -Fxq -- "$ClientOwner" "$KeepUsersFile"; then
            dbg "Ignoring client '$Client' owned by kept user '$ClientOwner'."
            ClientsKeptByOwner+=1
            continue
         fi

         if cmd "Removing client '$Client'." p4 -s client -df -Fs "$Client"; then
            ClientsDeleted+=1
         else
            ClientsFailed+=1
            errmsg "Failed to remove client '$Client'."
         fi
      done < "$TmpFile"
   else
      msg "No clients detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of clients."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 3: Branch Cleanup (viewless branches only)"

if [[ -n "$KeepBranchesFile" ]]; then
   msg "Deleting all viewless branches except those listed in '$KeepBranchesFile'."
else
   msg "Deleting all viewless branches."
fi

if p4 -ztag -F %branch% branches > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r Branch; do
         BranchesTotal+=1

         if ! BranchView=$(p4 -ztag -F %View0% branch -o "$Branch" 2>/dev/null); then
            warnmsg "Could not inspect view for branch '$Branch' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$BranchView" ]]; then
            dbg "Ignoring branch with non-empty view: '$Branch'."
            BranchesWithView+=1
            continue
         fi

         if [[ -n "$KeepBranchesFile" ]] && grep -Fxq -- "$Branch" "$KeepBranchesFile"; then
            dbg "Ignoring \"keep\" branch '$Branch'."
            BranchesKeptByFile+=1
            continue
         fi

         if ! BranchOwner=$(p4 -ztag -F %Owner% branch -o "$Branch" 2>/dev/null); then
            warnmsg "Could not inspect owner for branch '$Branch' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$BranchOwner" ]] && grep -Fxq -- "$BranchOwner" "$KeepUsersFile"; then
            dbg "Ignoring branch '$Branch' owned by kept user '$BranchOwner'."
            BranchesKeptByOwner+=1
            continue
         fi

         if cmd "Removing viewless branch '$Branch'." p4 -s branch -df "$Branch"; then
            BranchesDeleted+=1
         else
            BranchesFailed+=1
            errmsg "Failed to remove branch '$Branch'."
         fi
      done < "$TmpFile"
   else
      msg "No branches detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of branches."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 4: Label Cleanup (viewless labels only)"

if [[ -n "$KeepLabelsFile" ]]; then
   msg "Deleting all viewless labels except those listed in '$KeepLabelsFile'."
else
   msg "Deleting all viewless labels."
fi

if p4 -ztag -F %label% labels > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r Label; do
         LabelsTotal+=1

         if ! LabelView=$(p4 -ztag -F %View0% label -o "$Label" 2>/dev/null); then
            warnmsg "Could not inspect view for label '$Label' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$LabelView" ]]; then
            dbg "Ignoring label with non-empty view: '$Label'."
            LabelsWithView+=1
            continue
         fi

         if [[ -n "$KeepLabelsFile" ]] && grep -Fxq -- "$Label" "$KeepLabelsFile"; then
            dbg "Ignoring \"keep\" label '$Label'."
            LabelsKeptByFile+=1
            continue
         fi

         if ! LabelOwner=$(p4 -ztag -F %Owner% label -o "$Label" 2>/dev/null); then
            warnmsg "Could not inspect owner for label '$Label' - skipping to avoid unsafe deletion."
            continue
         fi
         if [[ -n "$LabelOwner" ]] && grep -Fxq -- "$LabelOwner" "$KeepUsersFile"; then
            dbg "Ignoring label '$Label' owned by kept user '$LabelOwner'."
            LabelsKeptByOwner+=1
            continue
         fi

         if cmd "Removing viewless label '$Label'." p4 -s label -df "$Label"; then
            LabelsDeleted+=1
         else
            LabelsFailed+=1
            errmsg "Failed to remove label '$Label'."
         fi
      done < "$TmpFile"
   else
      msg "No labels detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of labels."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 5: Group Cleanup"

# Groups are deleted before users. P4 forbids deleting a user who is the last
# member of a group; removing all groups first avoids that constraint.
if [[ -n "$KeepGroupsFile" ]]; then
   msg "Deleting all groups except those listed in '$KeepGroupsFile'."
else
   msg "Deleting all groups."
fi

if p4 groups > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r Group; do
         GroupsTotal+=1
         if [[ -n "$KeepGroupsFile" ]] && grep -Fxq -- "$Group" "$KeepGroupsFile"; then
            dbg "Ignoring \"keep\" group '$Group'."
            GroupsKept+=1
            continue
         fi

         if cmd "Removing group '$Group'." p4 -s group -d "$Group"; then
            GroupsDeleted+=1
         else
            GroupsFailed+=1
            errmsg "Failed to remove group '$Group'."
         fi
      done < "$TmpFile"
   else
      msg "No groups detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of groups."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 6: Shelved CL Cleanup"

msg "Deleting all shelved changes."

if p4 -ztag -F %change% changes -r -s shelved > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r ShelvedCL; do
         if cmd "" p4 -s shelve -df -c "$ShelvedCL"; then
            ShelvesDeleted+=1
         else
            ShelvesFailed+=1
            msg "Failed to remove shelved CL $ShelvedCL"
         fi
      done < "$TmpFile"
   else
      msg "No shelves detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of shelved changes."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 7: User Cleanup"

msg "Deleting all users except those listed in '$KeepUsersFile'."

if p4 -ztag -F %User% users -a > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r User; do
         UsersTotal+=1
         if grep -Fxq -- "$User" "$KeepUsersFile"; then
            dbg "Ignoring \"keep\" user '$User'."
            UsersKept+=1
            continue
         fi

         if cmd "Removing User '$User'." p4 -s user -df -F -D -y "$User"; then
            UsersDeleted+=1
         else
            UsersFailed+=1
            errmsg "Failed to remove user '$User'."
         fi
      done < "$TmpFile"
   else
      msg "No users detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of users."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 8: Stream Cleanup (**NOT IMPLEMENTED**)"

warnmsg "Stream cleanup logic has not been implemented."

#------------------------------------------------------------------------------
msg "${H2}\nPhase 9: Job and Fix Cleanup (**NOT IMPLEMENTED**)"

warnmsg "Job and Fix cleanup logic has not been implemented."

#------------------------------------------------------------------------------
msg "${H2}\nPhase 10: Submitted CL Cleanup (empty CLs only)"

msg "Removing empty submitted changelists."

if p4 -ztag -F %change% changes -r -s submitted > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      msg "\nThis will attempt to delete every submitted changelist. Only empty submitted changelists will be deleted. It is safe to attempt to delete a non-empty submitted change; that attempt will simply fail."
      while read -r SubmittedCL; do
         SubmittedCLsAttempted+=1
         cmd "" p4 -s change -df "$SubmittedCL"
      done < "$TmpFile"
   else
      errmsg "No changes detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of submitted changes."
fi

#------------------------------------------------------------------------------
msg "${H2}\nPhase 11: Depot Cleanup (empty depots only)"

# Note: don't pipe 'p4 depots' through grep here. A grep with zero matches
# returns non-zero, which would mask a successful 'p4 depots' call and route
# control into the "Could not get list of depots" error branch. Instead,
# capture to TmpFile and filter inside the loop.
if p4 -ztag -F %type%:%name% depots > "$TmpFile"; then
   if [[ -s "$TmpFile" ]]; then
      while read -r DepotData; do
         case "$DepotData" in
            local:*|remote:*|stream:*) ;;
            *) dbg "Ignoring depot (unwanted type): '$DepotData'."; continue ;;
         esac
         Depot=${DepotData#*:}
         DepotsTotal+=1
         DepotFileCount=$(p4 -ztag -F %fileCount% sizes -sah "//$Depot/...")
         if [[ "$DepotFileCount" == 0 ]]; then
            DepotsEmpty+=1
            if cmd "" p4 -s depot -df "$Depot"; then
               DepotsDeleted+=1
            else
               DepotsFailed+=1
               msg "Failed to delete empty depot '$Depot'."
            fi
         else
            DepotsNonEmpty+=1
            dbg "Ignoring non-empty depot '$Depot'."
         fi
      done < "$TmpFile"
      if [[ "$DepotsTotal" -eq 0 ]]; then
         errmsg "No local/remote/stream depots detected."
      fi
   else
      errmsg "No depots detected."
   fi
   rm -f "$TmpFile"
else
   errmsg "Could not get list of depots."
fi

#------------------------------------------------------------------------------
msg "\nTime: $ThisScript ran for $((SECONDS/3600)) hours $((SECONDS%3600/60)) minutes $((SECONDS%60)) seconds.\\n"

if [[ "$NoOp" -eq 0 ]]; then
   declare RunMode="Live Operation mode"
   declare DeletedLabel="Deleted"
else
   declare RunMode="Dry Run/Preview mode"
   declare DeletedLabel="Would delete"
fi

msg "${H2}\nSummary (${RunMode}):"

msg "\nLDAP specs:"
sline "${DeletedLabel}"           "$LdapSpecsDeleted"
sline "Failed"                    "$LdapSpecsFailed"

msg "\nClients:"
sline "Total examined"            "$ClientsTotal"
sline "Ignored (non-empty view)"  "$ClientsWithView"
sline "Kept (listed in keep file)" "$ClientsKeptByFile"
sline "Kept (owned by kept user)" "$ClientsKeptByOwner"
sline "${DeletedLabel}"           "$ClientsDeleted"
sline "Failed"                    "$ClientsFailed"

msg "\nBranches:"
sline "Total examined"            "$BranchesTotal"
sline "Ignored (non-empty view)"  "$BranchesWithView"
sline "Kept (listed in keep file)" "$BranchesKeptByFile"
sline "Kept (owned by kept user)" "$BranchesKeptByOwner"
sline "${DeletedLabel}"           "$BranchesDeleted"
sline "Failed"                    "$BranchesFailed"

msg "\nLabels:"
sline "Total examined"            "$LabelsTotal"
sline "Ignored (non-empty view)"  "$LabelsWithView"
sline "Kept (listed in keep file)" "$LabelsKeptByFile"
sline "Kept (owned by kept user)" "$LabelsKeptByOwner"
sline "${DeletedLabel}"           "$LabelsDeleted"
sline "Failed"                    "$LabelsFailed"

msg "\nUsers:"
sline "Total examined"            "$UsersTotal"
sline "Kept (listed in keep file)" "$UsersKept"
sline "${DeletedLabel}"           "$UsersDeleted"
sline "Failed"                    "$UsersFailed"

msg "\nShelved changelists:"
sline "${DeletedLabel}"           "$ShelvesDeleted"
sline "Failed"                    "$ShelvesFailed"

msg "\nGroups:"
sline "Total examined"            "$GroupsTotal"
sline "Kept (listed in keep file)" "$GroupsKept"
sline "${DeletedLabel}"           "$GroupsDeleted"
sline "Failed"                    "$GroupsFailed"

msg "\nSubmitted changelists:"
sline "Delete attempts (only empties succeed)" "$SubmittedCLsAttempted"

msg "\nDepots:"
sline "Total examined (local/remote/stream)" "$DepotsTotal"
sline "Empty"                     "$DepotsEmpty"
sline "Non-empty (ignored)"       "$DepotsNonEmpty"
sline "${DeletedLabel}"           "$DepotsDeleted"
sline "Failed"                    "$DepotsFailed"

msg "\n${H2}"
if [[ "$ErrorCount" -eq 0 && "$WarningCount" -eq 0 ]]; then
   msg "Processing completed successfully (${RunMode}) with no errors or warnings."
elif [[ "$ErrorCount" -eq 0 ]]; then
   msg "Processing completed with no errors but $WarningCount warnings (${RunMode})."
else
   msg "Processing completed with $ErrorCount errors and $WarningCount warnings (${RunMode})."
fi

exit "$ErrorCount"
# Change User Description Committed
#16 32698 C. Thomas Tyler v2.1.3: Move Group Cleanup before User Cleanup (fix last-member-of-group
deletion error). Add preflight check for explicit super user protection
entry. Consistent passive-voice phase titles. Fix declare AccessLevel=.
#15 32697 C. Thomas Tyler v2.1.2: Fix summary column alignment using printf/sline helper.
#14 32696 C. Thomas Tyler v2.1.1: Bump version number (missed in prior submit).
#13 32695 C. Thomas Tyler v2.1.0: Fix shellcheck SC2181 warnings; inline assignment+exit-check into if-!
pattern. Update command summary doc.
#12 32693 C. Thomas Tyler v2.1.0: Fix cmd() to use array args, preventing shell expansion of special chars in spec names (e.g.
'$(basename_$PWD)'). Add p4 exit-code guards on view/owner checks so a wrapper script failure causes a safe skip rather than a misclassified deletion.
#11 32692 C. Thomas Tyler Overhauled.
#10 32691 C. Thomas Tyler Minor spelling/typo fixes.
#9 32690 Perforce maintenance Only viewless clients are now removed.

Better loggic for handling keep files.
#8 32689 Perforce maintenance Cosmetic improvements.
#7 32688 Perforce maintenance Added handling for labels.
Fixed bug removing shelves.
#6 32684 Perforce maintenance Extended 'keep' logic for branches, clients, and groups.
#5 32683 Perforce maintenance Added timing summary.
Refined bits.
#4 32682 Perforce maintenance Fixed user removal bug.
#3 32679 Perforce maintenance More logic added.
#2 32678 Perforce maintenance WIP.
#1 32672 Perforce maintenance Added script to trim excess metadata.