#!/usr/bin/env bash# Reset one collaborator's clone to origin/master — the operator's repair of last resort.## sudo bash scripts/ops/reset-user.sh <slug> [--dry-run] [--yes] [--no-filter]## Runs, as the collaborator, the two commands that make their clone match the remote# exactly — branch, and tags:## git fetch --prune --tags --force --prune-tags origin# git reset --hard origin/master## The tag flags are not decoration. Origin is the source of truth for release tags, and a# repair that fixes the branch but leaves a stale or *moved* tag behind has not finished:# a plain `git fetch --tags` refuses to clobber an existing tag and exits non-zero, which# is enough to fail the collaborator's next `git-sync` outright — and `git-sync` is the# only git verb a reader's profile has. That is not hypothetical; it is what a history# rewrite on master did to six clones at once.## **This discards their local commits and every uncommitted change.** That is the point —# it is for a clone wedged in a state its owner cannot get out of with the verbs their# profile has (a reader has no `git-reset` and no `!`), which is precisely when someone# else has to reach in. It is not a routine sync: `git-sync` is, and it preserves work.# So the state about to be destroyed is printed first and confirmed, unless --yes.## Why run it as them rather than as root: git writes objects, the index and the worktree.# Doing that as root leaves root-owned files in a home the collaborator's own uid then# cannot write, which converts one wedged clone into a permanently wedged one.## The smudge filter and the vault: `reset --hard` checks out solutions/private, which the# clean/smudge filter must decrypt, which needs the user's master key — and their private# key may be vault-encrypted, unlocked only inside their own session. A `sudo -u` shell has# no such session, so the reset can fail on decryption alone. --no-filter is the way through# that: reset with the filter not required, leaving solutions/private as **ciphertext** in a# structurally correct clone, which their own `git-filter install` then decrypts in place.## Egress: the host is default-deny outbound and the Squid allowlist is the only way to# github.com. The per-user services get that wiring from /etc/euler/egress.env via# EnvironmentFile=, but a `sudo -u` shell inherits nothing, so a bare fetch here fails at# DNS ("Could not resolve host: github.com") rather than at the network. This script passes# the same env through explicitly — see docs/web-server-guide.md § Egress.## Author: Vikas Munshi <vikas.munshi@gmail.com># Copyright (c) 2026. All rights reserved.# Licensed under the MIT License.set-uopipefailSLUG=''DRY_RUN=0ASSUME_YES=0NO_FILTER=0usage(){cat<<USAGEUsage: sudo bash $0 <slug> [--dry-run] [--yes] [--no-filter] <slug> the collaborator's system slug (their uid / home / branch name) --dry-run print the commands instead of running them --yes skip the confirmation (for scripted repairs) --no-filter reset with the crypt filter not required — solutions/private stays ciphertext, and their own \`git-filter install\` decrypts it later. Use when the reset fails because the vault is locked outside their session.Discards local commits AND uncommitted changes in /home/<slug>/euler. See \`git-sync\`for the routine, work-preserving pull.USAGE}forargin"$@";docase"${arg}"in--dry-run)DRY_RUN=1;;--yes|-y)ASSUME_YES=1;;--no-filter)NO_FILTER=1;;-h|--help|help)usage;exit0;;-*)echo"Error: unknown option: ${arg}">&2;usage>&2;exit2;;*)if[[-n"${SLUG}"]];thenecho"Error: one slug at a time (got '${SLUG}' and '${arg}')">&2exit2fiSLUG="${arg}";;esacdoneif[[-z"${SLUG}"]];thenusage>&2;exit2;fiif[[${EUID}-ne0]];thenecho"Error: run under sudo — this acts on another user's home.">&2exit1fiif!id-u"${SLUG}"&>/dev/null;thenecho"Error: no such user: ${SLUG}">&2exit1fiHOME_DIR="$(getentpasswd"${SLUG}"|cut-d:-f6)"CLONE="${HOME_DIR}/euler"if[[!-d"${CLONE}/.git"]];thenecho"Error: no clone at ${CLONE}">&2exit1fi# The proxy wiring the services get from EnvironmentFile= and a sudo shell does not.# Passed as an explicit `env` prefix rather than exported: sudo resets the environment# (env_reset), so an export here would simply be dropped.EGRESS_ENV='/etc/euler/egress.env'PROXY_ENV=()if[[-r"${EGRESS_ENV}"]];thenwhileIFS=read-rline;doif[["${line}"=~^[A-Za-z_][A-Za-z0-9_]*=]];thenPROXY_ENV+=("${line}");fidone<"${EGRESS_ENV}"fi# …and the one variable that tells the crypt filter WHICH clone it is filtering. The# per-user services set it; a sudo shell has nothing, and the filter then has to guess from# its own cwd. It guesses correctly (git runs a filter at the top of the worktree), but# saying it outright costs nothing and is what every other caller does.PROXY_ENV+=("EULER_REPO_ROOT=${CLONE}")# as_user — run git in their clone, as them. Never as root (see the header).as_user(){sudo-u"${SLUG}"env${PROXY_ENV[@]+"${PROXY_ENV[@]}"}git-C"${CLONE}""$@"}# run_step — execute, or print under --dry-run.run_step(){if[[${DRY_RUN}-eq1]];thenecho" [dry-run] sudo -u ${SLUG} env ${PROXY_ENV[*]+${#PROXY_ENV[@]} proxy var(s)} "\"git -C ${CLONE}$*"return0fias_user"$@"}echo"── ${SLUG} — ${CLONE}"branch="$(as_userrev-parse--abbrev-refHEAD2>/dev/null||echo'(unknown)')"dirty_count="$(as_user--no-optional-locksstatus--porcelain2>/dev/null|wc-l)"stash_count="$(as_userstashlist2>/dev/null|wc-l)"unpushed="$(as_userlog--onelineorigin/master..HEAD2>/dev/null|wc-l)"echo" branch : ${branch}"echo" uncommitted : ${dirty_count} file(s) <- will be DISCARDED"echo" commits not on origin/master: ${unpushed} <- will be DISCARDED"echo" stash entries : ${stash_count} (untouched by this script)"if[[${DRY_RUN}-ne1&&${ASSUME_YES}-ne1]];thenread-r-p"Reset ${SLUG}'s clone to origin/master, discarding the above? [y/N] "replyif[[!"${reply}"=~^[Yy]$]];thenecho" nothing done"exit0fifiecho" fetching (branch + tags, mirroring origin)..."if!run_stepfetch--prune--tags--force--prune-tagsorigin;thenecho"Error: fetch failed.">&2if[[${#PROXY_ENV[@]}-eq0]];thenecho" No proxy env found at ${EGRESS_ENV}: on a host whose egress is default-deny,">&2echo" git cannot even resolve github.com from here. Check the egress kit.">&2elseecho" The egress proxy env was passed through; check euler-proxy.service and the">&2echo" allowlist (scripts/setup/egress.sh status), then this user's git remote.">&2fiexit1fiecho" resetting to origin/master..."if[[${NO_FILTER}-eq1]];then# `required=false` makes git keep a blob the filter refused, rather than failing the# checkout: solutions/private lands as ciphertext and the clone is otherwise correct.run_step-cfilter.solver-crypt.required=falsereset--hardorigin/masterelserun_stepreset--hardorigin/masterfirc=$?if[[${rc}-ne0]];thenecho>&2echo"Error: reset failed (${rc}).">&2echo" If it failed decrypting solutions/private, the vault is locked outside their">&2echo" session — re-run with --no-filter, then have them run \`git-filter install\`">&2echo" in their own shell to decrypt in place.">&2exit"${rc}"fiif[[${DRY_RUN}-eq1]];thenecho" [dry-run] nothing was changed"exit0fiecho" now at: $(as_userlog--oneline-12>/dev/null)"left="$(as_user--no-optional-locksstatus--porcelain2>/dev/null|wc-l)"echo" uncommitted after reset: ${left}"if[[${NO_FILTER}-eq1]];thenecho" NOTE: solutions/private is ciphertext — they must run \`git-filter install\`."fiecho" Have them reconnect their web shell before testing (read_master_key is cached)."