#!/bin/sh # Latchpoint - one-time backfill of existing Perforce history (Linux/macOS Helix Core servers) # # The change-commit trigger only ever sees NEW changelists. Without this, a studio # installs Latchpoint and sees an empty development panel until someone happens to # submit. This walks the history you already have and publishes it, so the panel is # populated the moment you finish setup. # # Run once, by hand: # # export LATCHPOINT_URL='' # export LATCHPOINT_SECRET='' # ./latchpoint-backfill.sh --max 2000 --dry-run # see what it would do # ./latchpoint-backfill.sh --max 2000 # do it # # Options: # --max N consider the N most recent changelists (default 1000) # --since N only changelists newer than N (resume, or import a slice) # --path SPEC depot path to walk (default //...) # --batch N changelists per request (default 200, the endpoint maximum) # --delay N seconds between batches (default 10) - see the note below # --dry-run print what would be sent, send nothing # # Unlike the trigger, this is NOT in the submit path. It is allowed to fail loudly, # and it does - a silent partial import is worse than an obvious failure. # # Safe to re-run. Jira keys development information by commit id and we derive that # id from the changelist number, so a second run updates the same entries rather # than duplicating them. # # --delay is correctness, not politeness. Jira's development-information API is # asynchronous. Firing several updates at the SAME repository within a second or so # loses writes: every request returns 202 and reports success, and the commits from # the earlier batches are simply not there afterwards. Measured directly against the # stored data, not inferred from the UI: 12 changelists in one request landed # completely, the identical 12 split across three back-to-back requests landed # nothing at all. Do not set --delay 0 on a real import. set -u MAX=1000 SINCE=0 PATH_SPEC='//...' BATCH=200 DELAY=10 DRY=0 while [ $# -gt 0 ]; do case "$1" in --max) MAX="$2"; shift 2 ;; --since) SINCE="$2"; shift 2 ;; --path) PATH_SPEC="$2"; shift 2 ;; --batch) BATCH="$2"; shift 2 ;; --delay) DELAY="$2"; shift 2 ;; --dry-run) DRY=1; shift ;; -h|--help) sed -n '2,28p' "$0"; exit 0 ;; *) echo "unknown option: $1" >&2; exit 2 ;; esac done P4BIN="${P4_EXE:-p4}" DEPOT_NAME="${LATCHPOINT_DEPOT:-depot}" SWARM="${LATCHPOINT_SWARM_URL:-}" URL="${LATCHPOINT_URL:-}" SECRET="${LATCHPOINT_SECRET:-}" if [ "$DRY" -eq 0 ]; then [ -z "$URL" ] && { echo "LATCHPOINT_URL is not set." >&2; exit 1; } [ -z "$SECRET" ] && { echo "LATCHPOINT_SECRET is not set." >&2; exit 1; } fi TMP="${TMPDIR:-/tmp}/latchpoint-backfill.$$" mkdir -p "$TMP" || { echo "cannot create temp dir" >&2; exit 1; } trap 'rm -rf "$TMP"' EXIT INT TERM # JSON-escape: backslash, quote, control chars, then newlines to \n. esc() { printf '%s' "$1" \ | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/\t/\\t/g' \ | awk 'BEGIN{ORS=""} {print (NR>1 ? "\\n" : "") $0}' } # Identical parsing to the trigger, and it must stay that way. p4 -ztag emits a # multi-line value as "... desc " followed by unprefixed lines; reading # only the prefixed line truncates the description and, when the Jira key sits in # the body, drops the changelist entirely. field() { printf '%s\n' "$2" | awk -v key="$1" ' /^\.\.\. / { if (want) exit if ($2 == key) { want=1; sub(/^\.\.\. [^ ]+ ?/, ""); out=$0 } next } want { if ($0 == "") { blanks++; next } while (blanks-- >= 0) out = out "\n" blanks = 0 out = out $0 next } END { if (want) printf "%s", out } ' } echo "Reading up to $MAX changelists from $PATH_SPEC ..." LIST=$("$P4BIN" -ztag changes -m "$MAX" "$PATH_SPEC" 2>/dev/null) || { echo "p4 changes failed. Check P4PORT / P4USER and that you are logged in." >&2; exit 1; } NUMBERS=$(printf '%s\n' "$LIST" \ | sed -n 's/^\.\.\. change \([0-9][0-9]*\)[[:space:]]*$/\1/p' \ | awk -v since="$SINCE" '$1 > since' \ | sort -n) COUNT=$(printf '%s\n' "$NUMBERS" | grep -c '[0-9]' || true) echo " $COUNT changelist(s) in range." [ "$COUNT" -eq 0 ] && { echo "Nothing to do."; exit 0; } # --- describe each, keep the ones carrying a Jira key ------------------------ KEPT=0 SKIPPED=0 : > "$TMP/items" for n in $NUMBERS; do RAW=$("$P4BIN" -ztag describe -s "$n" 2>/dev/null) || { echo " changelist $n : p4 describe failed, skipping" >&2; SKIPPED=$((SKIPPED+1)); continue; } DESC=$(field desc "$RAW") [ -z "$DESC" ] && { SKIPPED=$((SKIPPED+1)); continue; } printf '%s' "$DESC" | grep -Eq '\b[A-Z][A-Z0-9]+-[0-9]+\b' || { SKIPPED=$((SKIPPED+1)); continue; } USR=$(field user "$RAW") EPOCH=$(field time "$RAW") FILES=$(printf '%s\n' "$RAW" | grep -c '^\.\.\. depotFile' || true) ISO=$(date -u -d "@$EPOCH" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u -r "$EPOCH" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ || date -u +%Y-%m-%dT%H:%M:%SZ) printf '{"change":"%s","description":"%s","user":"%s","time":"%s","fileCount":%s}\n' \ "$(esc "$n")" "$(esc "$DESC")" "$(esc "$USR")" "$ISO" "${FILES:-0}" >> "$TMP/items" KEPT=$((KEPT+1)) done echo "" echo " $KEPT changelist(s) carry a Jira issue key" echo " $SKIPPED skipped (no key, or unreadable)" [ "$KEPT" -eq 0 ] && { echo "Nothing to publish."; exit 0; } # --- publish in batches ------------------------------------------------------ SERVER_JSON='' [ -n "$SWARM" ] && SERVER_JSON=$(printf ',"serverUrl":"%s"' "$(esc "${SWARM%/}")") split -l "$BATCH" "$TMP/items" "$TMP/batch." 2>/dev/null \ || awk -v n="$BATCH" -v d="$TMP" 'NR%n==1{f=sprintf("%s/batch.%03d",d,++i)} {print > f}' "$TMP/items" PUBLISHED=0 FAILED=0 TOTAL=$(ls "$TMP"/batch.* 2>/dev/null | wc -l | tr -d ' ') IDX=0 for bf in "$TMP"/batch.*; do IDX=$((IDX+1)) BODY=$(printf '{"depot":"%s"%s,"changelists":[%s]}' \ "$(esc "$DEPOT_NAME")" "$SERVER_JSON" "$(paste -sd, "$bf")") if [ "$DRY" -eq 1 ]; then echo "[dry run] batch $IDX/$TOTAL : $(wc -l < "$bf" | tr -d ' ') changelist(s), ${#BODY} bytes" [ "$IDX" -eq 1 ] && { echo "[dry run] first changelist in batch 1:"; head -1 "$bf"; } continue fi CODE=$(curl -sS -m 60 -o "$TMP/resp" -w '%{http_code}' -X POST "$URL" \ -H 'Content-Type: application/json; charset=utf-8' \ -H "X-Latchpoint-Secret: $SECRET" \ --data "$BODY" 2>/dev/null) || CODE=000 if [ "$CODE" = "200" ]; then N=$(sed -n 's/.*"published":\([0-9]*\).*/\1/p' "$TMP/resp") PUBLISHED=$((PUBLISHED + ${N:-0})) echo " batch $IDX/$TOTAL : published ${N:-0}" else echo " batch $IDX/$TOTAL FAILED (HTTP $CODE: $(head -c 160 "$TMP/resp" 2>/dev/null))" >&2 FAILED=$((FAILED + $(wc -l < "$bf" | tr -d ' '))) fi # A 202 does not mean the data is stored. Do not let the next batch overtake it. [ "$IDX" -lt "$TOTAL" ] && [ "$DELAY" -gt 0 ] && sleep "$DELAY" done echo "" if [ "$DRY" -eq 1 ]; then echo "Dry run complete. Nothing was sent. Re-run without --dry-run to publish." elif [ "$FAILED" -gt 0 ]; then echo "Backfill finished with errors: $PUBLISHED published, $FAILED not sent." echo "Safe to re-run - republishing updates existing entries rather than duplicating them." exit 1 else echo "Backfill complete: $PUBLISHED changelist(s) published." echo "They may take a few minutes to appear in Jira's development panel." fi exit 0