feat(dev-env): backport matured dev-env implementation from /etc/nixos
Replace the stub dev-env with the real, working implementation that grew
in the reference machine config — de-identified for the public scaffold.
Nix layer:
- options.nix: full project schema (url/upstream/fork/category/
worktreeRoot/worktrees{branch,path,remote}/isClone/deployFlakeInput),
deploy.targets, github.forkUser, writeDirenvHints. Drops the
forgejo-URL block + deploy-flake auto-derivation (incoherent in a
scaffold that uses explicit per-project urls).
- lib.nix: mkProject + worktreePath/bareRepoPath/projectRemotes,
generalized to the explicit-url model (origin falls back to upstream).
- config.nix: renders /etc/dev-env/{config.sh,projects.json,
tmux-sessions.json}, installs helpers via writeShellScriptBin, loads
shell functions into interactive shells, wires the git pre-commit hook.
Scripts (config-driven, read /etc/dev-env at runtime):
- bootstrap.sh, nav.sh, worktree.sh, pr-helpers.sh, rebase.sh,
status.sh, deploy.sh, regtest.sh, tmux-launch.sh.
- Stripped aiolabs/forgejo/bitspire/lamassu/webapp hardcoding; the
github-fork remote is renamed 'fork' to match git.remotes vocabulary.
- Removes the dev.sh stub (the matured impl uses discrete commands +
shell functions, not a unified 'dev' CLI).
presets/example.nix: a worked, generic project list replacing the
identity-specific aiolabs preset. tests/smoke.nix + flake checks
exercise the schema; 'nix flake check' is green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
773632562e
commit
e38d313db2
17 changed files with 2925 additions and 147 deletions
290
modules/dev-env/scripts/bootstrap.sh
Normal file
290
modules/dev-env/scripts/bootstrap.sh
Normal file
|
|
@ -0,0 +1,290 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env-bootstrap: idempotent materialization of bare repos and worktrees
|
||||
#
|
||||
# Reads /etc/dev-env/projects.json (rendered by config.nix) and ensures
|
||||
# every declared project has:
|
||||
#
|
||||
# 1. A bare repo at ${REPOS_DIR}/<name>.git with the declared remotes
|
||||
# (origin, optional upstream, optional fork).
|
||||
# 2. A worktree at the declared path for each entry in `worktrees`.
|
||||
# 3. (Optional) A `.envrc` containing `use flake` if the worktree has
|
||||
# a flake.nix and DEVENV_WRITE_DIRENV_HINTS=1.
|
||||
#
|
||||
# Safety:
|
||||
# - Never clobbers an existing worktree whose current branch differs
|
||||
# from the declared one — prints a warning and skips.
|
||||
# - Never silently rewrites a remote URL — prints the diff and asks.
|
||||
# - --dry-run shows everything that would happen without touching anything.
|
||||
# - Re-running is a no-op when the tree already matches the spec.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Config -----------------------------------------------------------
|
||||
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
|
||||
DEV_ROOT="${DEV_ROOT:-$HOME/dev}"
|
||||
REPOS_DIR="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}"
|
||||
WRITE_DIRENV="${DEVENV_WRITE_DIRENV_HINTS:-1}"
|
||||
|
||||
# --- Args -------------------------------------------------------------
|
||||
|
||||
DRY_RUN=false
|
||||
VERBOSE=false
|
||||
FORCE_REMOTES=false
|
||||
ONLY_PROJECT=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
dev-env-bootstrap — materialize bare repos + worktrees from /etc/dev-env/projects.json
|
||||
|
||||
USAGE:
|
||||
dev-env-bootstrap [OPTIONS] [project-name]
|
||||
|
||||
OPTIONS:
|
||||
-n, --dry-run show what would happen, do not change anything
|
||||
-v, --verbose print every git command
|
||||
-f, --force-remotes silently rewrite remote URLs that differ from spec
|
||||
-h, --help show this help
|
||||
|
||||
EXAMPLES:
|
||||
dev-env-bootstrap --dry-run # full preview
|
||||
dev-env-bootstrap # bring everything up to spec
|
||||
dev-env-bootstrap lnbits # only operate on the lnbits project
|
||||
|
||||
Reads from: $PROJECTS_JSON
|
||||
Writes to: $DEV_ROOT
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--dry-run) DRY_RUN=true ;;
|
||||
-v|--verbose) VERBOSE=true ;;
|
||||
-f|--force-remotes) FORCE_REMOTES=true ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) echo "unknown option: $1"; usage; exit 1 ;;
|
||||
*) ONLY_PROJECT="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
# --- Output -----------------------------------------------------------
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
DIM='\033[2m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[..]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[ok]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
|
||||
err() { echo -e "${RED}[XX]${NC} $*" >&2; }
|
||||
trace() { if $VERBOSE; then echo -e "${DIM}\$ $*${NC}"; fi; }
|
||||
|
||||
run() {
|
||||
trace "$*"
|
||||
if $DRY_RUN; then
|
||||
echo " (dry-run) $*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Preflight --------------------------------------------------------
|
||||
|
||||
if [[ ! -r "$PROJECTS_JSON" ]]; then
|
||||
err "projects.json not found at $PROJECTS_JSON"
|
||||
err "is the dev-env module enabled in your nixos config?"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null; then
|
||||
err "jq is required (should be in environment.systemPackages)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v git >/dev/null; then
|
||||
err "git is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run mkdir -p "$REPOS_DIR" "$DEV_ROOT"
|
||||
|
||||
# --- Per-project work -------------------------------------------------
|
||||
|
||||
# Returns "remote-name<TAB>url" for the given project, one line per remote.
|
||||
project_remotes_jq() {
|
||||
local proj="$1"
|
||||
jq -r --arg p "$proj" '
|
||||
.[$p].remotes
|
||||
| to_entries[]
|
||||
| "\(.key)\t\(.value)"
|
||||
' "$PROJECTS_JSON"
|
||||
}
|
||||
|
||||
# Returns "wtname<TAB>branch<TAB>path<TAB>remote" for each declared worktree.
|
||||
project_worktrees_jq() {
|
||||
local proj="$1"
|
||||
jq -r --arg p "$proj" '
|
||||
.[$p].worktrees
|
||||
| to_entries[]
|
||||
| "\(.key)\t\(.value.branch)\t\(.value.path)\t\(.value.remote)"
|
||||
' "$PROJECTS_JSON"
|
||||
}
|
||||
|
||||
ensure_bare_repo() {
|
||||
local proj="$1"
|
||||
local bare_path
|
||||
bare_path="$(jq -r --arg p "$proj" '.[$p].barePath' "$PROJECTS_JSON")"
|
||||
|
||||
if [[ ! -d "$bare_path" ]]; then
|
||||
info "creating bare repo $bare_path"
|
||||
run git init --bare "$bare_path"
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r remote_name url; do
|
||||
[[ -z "$remote_name" ]] && continue
|
||||
local current=""
|
||||
current="$(git -C "$bare_path" remote get-url "$remote_name" 2>/dev/null || echo "")"
|
||||
if [[ -z "$current" ]]; then
|
||||
info " + remote $remote_name → $url"
|
||||
run git -C "$bare_path" remote add "$remote_name" "$url"
|
||||
elif [[ "$current" != "$url" ]]; then
|
||||
warn " ! remote $remote_name URL differs:"
|
||||
warn " have: $current"
|
||||
warn " want: $url"
|
||||
if $FORCE_REMOTES; then
|
||||
info " (--force-remotes) updating"
|
||||
run git -C "$bare_path" remote set-url "$remote_name" "$url"
|
||||
else
|
||||
warn " (skipped — pass --force-remotes to overwrite)"
|
||||
fi
|
||||
fi
|
||||
done < <(project_remotes_jq "$proj")
|
||||
|
||||
# Initial fetch (one-shot, only if origin has never been fetched)
|
||||
if [[ ! -d "$bare_path/refs/remotes/origin" ]]; then
|
||||
info " fetching origin (first time)"
|
||||
run git -C "$bare_path" fetch origin 2>&1 | sed 's/^/ /' || \
|
||||
warn " fetch failed — check ssh access to origin"
|
||||
fi
|
||||
if git -C "$bare_path" remote get-url upstream &>/dev/null \
|
||||
&& [[ ! -d "$bare_path/refs/remotes/upstream" ]]; then
|
||||
info " fetching upstream (first time)"
|
||||
run git -C "$bare_path" fetch upstream 2>&1 | sed 's/^/ /' || \
|
||||
warn " upstream fetch failed"
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_worktrees() {
|
||||
local proj="$1"
|
||||
local bare_path is_clone
|
||||
bare_path="$(jq -r --arg p "$proj" '.[$p].barePath' "$PROJECTS_JSON")"
|
||||
is_clone="$(jq -r --arg p "$proj" '.[$p].isClone' "$PROJECTS_JSON")"
|
||||
|
||||
# Single-clone projects: clone to category/projectname instead of using worktrees
|
||||
if [[ "$is_clone" == "true" ]]; then
|
||||
local clone_path
|
||||
clone_path="$(jq -r --arg p "$proj" '.[$p].clonePath' "$PROJECTS_JSON")"
|
||||
if [[ -d "$clone_path/.git" ]]; then
|
||||
ok " clone exists: $clone_path"
|
||||
return 0
|
||||
fi
|
||||
local origin_url
|
||||
origin_url="$(jq -r --arg p "$proj" '.[$p].remotes.origin' "$PROJECTS_JSON")"
|
||||
info " cloning $origin_url → $clone_path"
|
||||
run mkdir -p "$(dirname "$clone_path")"
|
||||
run git clone "$origin_url" "$clone_path" || warn " clone failed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS=$'\t' read -r wt_name branch wt_path remote; do
|
||||
[[ -z "$wt_name" ]] && continue
|
||||
[[ "$wt_path" == "null" ]] && wt_path=""
|
||||
|
||||
if [[ -z "$wt_path" ]]; then
|
||||
warn " worktree $wt_name has no path; skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -d "$wt_path/.git" ]] || [[ -f "$wt_path/.git" ]]; then
|
||||
local current_branch
|
||||
current_branch="$(git -C "$wt_path" branch --show-current 2>/dev/null || echo '?')"
|
||||
if [[ "$current_branch" == "$branch" ]]; then
|
||||
ok " worktree $wt_name @ $branch (exists)"
|
||||
else
|
||||
warn " worktree $wt_name @ $current_branch (declared: $branch) — leaving alone"
|
||||
fi
|
||||
maybe_write_envrc "$wt_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ -e "$wt_path" ]]; then
|
||||
warn " $wt_path exists but is not a git worktree; skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Need to create the worktree. First make sure the local branch exists.
|
||||
if ! git -C "$bare_path" show-ref --verify --quiet "refs/heads/$branch"; then
|
||||
if git -C "$bare_path" show-ref --verify --quiet "refs/remotes/$remote/$branch"; then
|
||||
info " creating local branch $branch from $remote/$branch"
|
||||
run git -C "$bare_path" branch "$branch" "$remote/$branch"
|
||||
else
|
||||
warn " branch $branch not found on $remote — fetch and retry"
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
info " + worktree $wt_name → $wt_path ($branch)"
|
||||
run mkdir -p "$(dirname "$wt_path")"
|
||||
run git -C "$bare_path" worktree add "$wt_path" "$branch"
|
||||
maybe_write_envrc "$wt_path"
|
||||
done < <(project_worktrees_jq "$proj")
|
||||
}
|
||||
|
||||
maybe_write_envrc() {
|
||||
local wt="$1"
|
||||
[[ "$WRITE_DIRENV" != "1" ]] && return 0
|
||||
[[ -f "$wt/flake.nix" ]] || return 0
|
||||
[[ -e "$wt/.envrc" ]] && return 0
|
||||
info " + .envrc (use flake) → $wt/.envrc"
|
||||
if ! $DRY_RUN; then
|
||||
echo "use flake" > "$wt/.envrc"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- Main loop --------------------------------------------------------
|
||||
|
||||
mapfile -t PROJECTS < <(jq -r 'keys[]' "$PROJECTS_JSON")
|
||||
|
||||
if [[ -n "$ONLY_PROJECT" ]]; then
|
||||
if ! printf '%s\n' "${PROJECTS[@]}" | grep -qx "$ONLY_PROJECT"; then
|
||||
err "project '$ONLY_PROJECT' not in $PROJECTS_JSON"
|
||||
exit 1
|
||||
fi
|
||||
PROJECTS=("$ONLY_PROJECT")
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "dev-env bootstrap"
|
||||
echo " root: $DEV_ROOT"
|
||||
echo " projects: ${#PROJECTS[@]}"
|
||||
if $DRY_RUN; then echo " mode: dry-run"; fi
|
||||
echo ""
|
||||
|
||||
for proj in "${PROJECTS[@]}"; do
|
||||
echo "─── $proj ───"
|
||||
ensure_bare_repo "$proj"
|
||||
ensure_worktrees "$proj"
|
||||
echo ""
|
||||
done
|
||||
|
||||
ok "bootstrap complete"
|
||||
if $DRY_RUN; then echo "(no changes were made)"; fi
|
||||
217
modules/dev-env/scripts/deploy.sh
Normal file
217
modules/dev-env/scripts/deploy.sh
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-deploy: thin wrapper around your unified deploy flake
|
||||
#
|
||||
# Usage:
|
||||
# dev-deploy <host> switch on host (uses locked deploy flake input)
|
||||
# dev-deploy <host> test test build (no switch)
|
||||
# dev-deploy <host> build local build only
|
||||
# dev-deploy --local <host> same as switch but with --override-input
|
||||
# wired up from local worktrees so the deploy
|
||||
# builds against your in-progress changes
|
||||
# dev-deploy all [test|build] every host in parallel
|
||||
# dev-deploy update nix flake update
|
||||
#
|
||||
# Deploy host → SSH target mapping comes from /etc/dev-env/config.sh
|
||||
# (devEnv.deploy.targets). The flake working copy lives at
|
||||
# ${DEV_ROOT}/deploy[/<flakeInput>].
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
|
||||
DEPLOY_BASE="${DEPLOY_DIR:-$DEV_ROOT/deploy}"
|
||||
if [[ -n "${DEVENV_DEPLOY_FLAKE_INPUT:-}" ]]; then
|
||||
DEPLOY_DIR="$DEPLOY_BASE/$DEVENV_DEPLOY_FLAKE_INPUT"
|
||||
else
|
||||
DEPLOY_DIR="$DEPLOY_BASE"
|
||||
fi
|
||||
PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
dev-deploy — wraps nixos-rebuild against your unified deploy flake
|
||||
|
||||
USAGE:
|
||||
dev-deploy <host> deploy (switch)
|
||||
dev-deploy <host> test test build (no switch)
|
||||
dev-deploy <host> build local build only (no target)
|
||||
dev-deploy --local <host> deploy with --override-input from local worktrees
|
||||
dev-deploy --local <host> test test with overrides
|
||||
dev-deploy all [test|build] every host in parallel
|
||||
dev-deploy update [args...] nix flake update
|
||||
|
||||
Configured targets:
|
||||
EOF
|
||||
env | grep '^DEPLOY_TARGET_' | sed 's/^DEPLOY_TARGET_/ /' | sed 's/=/ → /' || true
|
||||
if [[ -r "$DEPLOY_DIR/flake.nix" ]]; then
|
||||
echo ""
|
||||
echo "Backing flake: $DEPLOY_DIR"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
# Pull the host → target map. We accept either:
|
||||
# 1. DEPLOY_TARGET_<HOST>=<ssh> (rendered by config.nix as env vars)
|
||||
# 2. ${DEPLOY_DIR}/deploy.sh's TARGETS associative array (fallback)
|
||||
target_for() {
|
||||
local host="$1"
|
||||
local var="DEPLOY_TARGET_${host//-/_}"
|
||||
if [[ -n "${!var:-}" ]]; then
|
||||
echo "${!var}"
|
||||
return
|
||||
fi
|
||||
# Fallback: parse a deploy.sh TARGETS array (simple regex; brittle
|
||||
# but enough as a fallback)
|
||||
if [[ -r "$DEPLOY_DIR/deploy.sh" ]]; then
|
||||
awk -v h="$host" '
|
||||
/^[[:space:]]*\[/ {
|
||||
if (match($0, /\[([^]]+)\]="([^"]*)"/, m)) {
|
||||
if (m[1] == h) { print m[2]; exit }
|
||||
}
|
||||
}
|
||||
' "$DEPLOY_DIR/deploy.sh"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- subcommands ---
|
||||
|
||||
cmd_update() {
|
||||
shift
|
||||
cd "$DEPLOY_DIR"
|
||||
nix flake update "$@"
|
||||
}
|
||||
|
||||
# Build the --override-input flags for a host using projects.json's
|
||||
# `deployFlakeInput` mapping. For each project that has deployFlakeInput
|
||||
# set AND a worktree resolved to a real on-disk path, we emit
|
||||
# --override-input <input> path:<worktree-path>
|
||||
# Picks the first worktree by default; pass DEVENV_OVERRIDE_WORKTREE=<name>
|
||||
# to choose a specific one.
|
||||
build_overrides() {
|
||||
local args=()
|
||||
local prefer="${DEVENV_OVERRIDE_WORKTREE:-}"
|
||||
[[ -r "$PROJECTS_JSON" ]] || { printf '%s\n' ""; return; }
|
||||
|
||||
while IFS=$'\t' read -r input wt_path; do
|
||||
[[ -z "$input" || "$input" == "null" ]] && continue
|
||||
[[ -d "$wt_path" ]] || continue
|
||||
args+=(--override-input "$input" "path:$wt_path")
|
||||
done < <(jq -r --arg prefer "$prefer" '
|
||||
to_entries[]
|
||||
| select(.value.deployFlakeInput != null)
|
||||
| .value as $v
|
||||
| (if ($prefer | length) > 0 and ($v.worktrees | has($prefer))
|
||||
then $v.worktrees[$prefer].path
|
||||
else
|
||||
($v.worktrees | to_entries | first.value.path // null)
|
||||
end) as $path
|
||||
| "\($v.deployFlakeInput)\t\($path // "")"
|
||||
' "$PROJECTS_JSON")
|
||||
|
||||
printf '%s\0' "${args[@]}"
|
||||
}
|
||||
|
||||
run_host() {
|
||||
local host="$1" action="$2" use_local="$3"
|
||||
local target
|
||||
target="$(target_for "$host")"
|
||||
|
||||
if [[ -z "$target" ]] && [[ "$action" != "build" ]]; then
|
||||
echo "no SSH target for host '$host' (set DEPLOY_TARGET_${host//-/_})" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
cd "$DEPLOY_DIR"
|
||||
|
||||
local overrides=()
|
||||
if $use_local; then
|
||||
# Read the null-delimited overrides into an array
|
||||
while IFS= read -r -d '' arg; do
|
||||
[[ -n "$arg" ]] && overrides+=("$arg")
|
||||
done < <(build_overrides)
|
||||
if (( ${#overrides[@]} > 0 )); then
|
||||
echo "[$host] using ${#overrides[@]} local override(s):"
|
||||
local i=0
|
||||
while (( i < ${#overrides[@]} )); do
|
||||
echo " ${overrides[$((i+1))]} -> ${overrides[$((i+2))]}"
|
||||
i=$((i + 3))
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$action" in
|
||||
deploy)
|
||||
echo "[$host] switch on $target..."
|
||||
nixos-rebuild switch \
|
||||
--flake ".#$host" \
|
||||
--target-host "$target" \
|
||||
"${overrides[@]}"
|
||||
;;
|
||||
test)
|
||||
echo "[$host] test on $target..."
|
||||
nixos-rebuild test \
|
||||
--flake ".#$host" \
|
||||
--target-host "$target" \
|
||||
"${overrides[@]}"
|
||||
;;
|
||||
build)
|
||||
echo "[$host] local build..."
|
||||
nix build \
|
||||
".#nixosConfigurations.$host.config.system.build.toplevel" \
|
||||
"${overrides[@]}"
|
||||
;;
|
||||
*)
|
||||
echo "unknown action: $action" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
USE_LOCAL=false
|
||||
if [[ "$1" == "--local" ]]; then
|
||||
USE_LOCAL=true
|
||||
shift
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
update)
|
||||
cmd_update "$@"
|
||||
;;
|
||||
all)
|
||||
action="${2:-deploy}"
|
||||
# iterate every DEPLOY_TARGET_* env var
|
||||
pids=()
|
||||
hosts=()
|
||||
for var in $(env | grep -o '^DEPLOY_TARGET_[A-Za-z0-9_]*' || true); do
|
||||
host="${var#DEPLOY_TARGET_}"
|
||||
host="${host//_/-}"
|
||||
run_host "$host" "$action" "$USE_LOCAL" &
|
||||
pids+=($!)
|
||||
hosts+=("$host")
|
||||
done
|
||||
failed=()
|
||||
for i in "${!pids[@]}"; do
|
||||
if ! wait "${pids[$i]}"; then
|
||||
failed+=("${hosts[$i]}")
|
||||
fi
|
||||
done
|
||||
if (( ${#failed[@]} > 0 )); then
|
||||
echo "FAILED: ${failed[*]}"
|
||||
exit 1
|
||||
fi
|
||||
echo "All hosts deployed successfully."
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
host="$1"
|
||||
action="${2:-deploy}"
|
||||
run_host "$host" "$action" "$USE_LOCAL"
|
||||
;;
|
||||
esac
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev — single entry point for spinning up an LNbits dev environment.
|
||||
#
|
||||
# dev up [--fakewallet|--regtest] start lnbits (default: --fakewallet)
|
||||
# dev down tear down whatever's running
|
||||
# dev logs follow lnbits (and docker, if regtest) logs
|
||||
# dev shell drop into the lnbits venv (or regtest container)
|
||||
#
|
||||
# Modes:
|
||||
# --fakewallet (default) LNBITS_BACKEND_WALLET_CLASS=FakeWallet — no docker,
|
||||
# no chains, instant. Good for extension / UI work.
|
||||
# --regtest docker-compose up the multi-node regtest stack
|
||||
# (LND + CLN + Eclair + bitcoind + electrs), then
|
||||
# start lnbits pointed at the LND-rest endpoint.
|
||||
# Wraps lnbits/legend-regtest-enviroment.
|
||||
#
|
||||
# Skeleton — no real wiring yet. The substantive pass will:
|
||||
# - locate the lnbits checkout via inputs.lnbits-src (or a configurable path)
|
||||
# - locate the regtest docker repo via config.lnbits-sensei.devEnv.regtest.repoUrl
|
||||
# - bring up containers, wait-for-it, populate LND credentials, exec lnbits
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cmd="${1:-up}"
|
||||
shift || true
|
||||
|
||||
mode="fakewallet"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--fakewallet) mode="fakewallet"; shift ;;
|
||||
--regtest) mode="regtest"; shift ;;
|
||||
*) echo "dev: unknown flag: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$cmd" in
|
||||
up)
|
||||
echo "dev up --$mode: TODO — wire substantive startup in next pass."
|
||||
;;
|
||||
down)
|
||||
echo "dev down: TODO — wire teardown in next pass."
|
||||
;;
|
||||
logs)
|
||||
echo "dev logs: TODO — tail lnbits + (docker logs if regtest) here."
|
||||
;;
|
||||
shell)
|
||||
echo "dev shell: TODO — drop into venv / regtest container here."
|
||||
;;
|
||||
*)
|
||||
echo "Usage: dev {up|down|logs|shell} [--fakewallet|--regtest]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
168
modules/dev-env/scripts/nav.sh
Normal file
168
modules/dev-env/scripts/nav.sh
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env: navigation helpers
|
||||
#
|
||||
# Sourced by user shells (not invoked as a script) via the loader in
|
||||
# /etc/profile.d/dev-env-functions.sh. Every function reads
|
||||
# /etc/dev-env/config.sh at call time, so adding a new worktree on disk
|
||||
# is immediately visible without a nixos-rebuild.
|
||||
|
||||
# Load runtime config (idempotent).
|
||||
_devenv_load_config() {
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
}
|
||||
|
||||
# Navigate to the dev root.
|
||||
dev() {
|
||||
_devenv_load_config
|
||||
cd "${DEV_ROOT:-$HOME/dev}" || return 1
|
||||
}
|
||||
|
||||
# Navigate to an lnbits worktree.
|
||||
# Usage: lb [worktree] (worktree ∈ whatever the filesystem shows, e.g. dev/main)
|
||||
lb() {
|
||||
_devenv_load_config
|
||||
local env="${1:-}"
|
||||
local lnbits_dir="${LNBITS_DIR:-$DEV_ROOT/lnbits}"
|
||||
|
||||
if [[ -z "$env" ]]; then
|
||||
echo "Usage: lb <worktree>"
|
||||
echo ""
|
||||
echo "Current lnbits worktrees:"
|
||||
if [[ -d "$lnbits_dir" ]]; then
|
||||
for d in "$lnbits_dir"/*/; do
|
||||
[[ -d "$d" ]] || continue
|
||||
local name branch
|
||||
name="$(basename "$d")"
|
||||
branch="$(git -C "$d" branch --show-current 2>/dev/null || echo '?')"
|
||||
printf " %-12s (%s)\n" "$name" "$branch"
|
||||
done
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -d "$lnbits_dir/$env" ]]; then
|
||||
cd "$lnbits_dir/$env" || return 1
|
||||
else
|
||||
echo "Unknown lnbits worktree: $env"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Navigate within a project-group folder (a `category` directory holding
|
||||
# one or more sub-repos). Sub-repos may be single clones or bare-repo
|
||||
# worktree sets; worktree sets accept an optional worktree name.
|
||||
# Usage: g <category> [repo] [worktree]
|
||||
_dev_group_nav() {
|
||||
local group_dir="$1" repo="${2:-}" worktree="${3:-dev}"
|
||||
|
||||
if [[ -z "$repo" ]]; then
|
||||
echo "repos under $(basename "$group_dir")/:"
|
||||
if [[ -d "$group_dir" ]]; then
|
||||
for d in "$group_dir"/*/; do
|
||||
[[ -d "$d" ]] && echo " $(basename "$d")"
|
||||
done
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Single-clone sub-repo (has its own .git at the top level)
|
||||
if [[ -d "$group_dir/$repo/.git" ]]; then
|
||||
cd "$group_dir/$repo" || return 1
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Worktree-based sub-repo
|
||||
if [[ -d "$group_dir/$repo/$worktree" ]]; then
|
||||
cd "$group_dir/$repo/$worktree" || return 1
|
||||
elif [[ -d "$group_dir/$repo" ]]; then
|
||||
cd "$group_dir/$repo" || return 1
|
||||
else
|
||||
echo "Unknown repo under $(basename "$group_dir")/: $repo"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Navigate into a project category directory.
|
||||
# Usage: g <category> [repo] [worktree]
|
||||
g() {
|
||||
_devenv_load_config
|
||||
local category="${1:-}"
|
||||
if [[ -z "$category" ]]; then
|
||||
echo "Usage: g <category> [repo] [worktree]"
|
||||
echo ""
|
||||
echo "Categories under $DEV_ROOT:"
|
||||
for d in "$DEV_ROOT"/*/; do
|
||||
[[ -d "$d" ]] && echo " $(basename "$d")"
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
_dev_group_nav "$DEV_ROOT/$category" "${2:-}" "${3:-dev}"
|
||||
}
|
||||
|
||||
# Navigate to an extension under shared/extensions.
|
||||
ext() {
|
||||
_devenv_load_config
|
||||
local extension="${1:-}"
|
||||
local ext_root="${SHARED_DIR:-$DEV_ROOT/shared}/extensions"
|
||||
|
||||
if [[ -z "$extension" ]]; then
|
||||
echo "Available extensions:"
|
||||
[[ -d "$ext_root" ]] && ls -1 "$ext_root"
|
||||
return 0
|
||||
fi
|
||||
if [[ -d "$ext_root/$extension" ]]; then
|
||||
cd "$ext_root/$extension" || return 1
|
||||
else
|
||||
echo "Unknown extension: $extension"
|
||||
[[ -d "$ext_root" ]] && ls -1 "$ext_root"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Navigate to a deploy host config inside the deploy flake working copy.
|
||||
deploy_nav() {
|
||||
_devenv_load_config
|
||||
local host="${1:-}"
|
||||
local deploy_root="${DEPLOY_DIR:-$DEV_ROOT/deploy}"
|
||||
|
||||
if [[ -z "$host" ]]; then
|
||||
echo "Usage: dep <host>"
|
||||
echo ""
|
||||
echo "Deploy targets (from /etc/dev-env/config.sh):"
|
||||
env | grep '^DEPLOY_TARGET_' | sed 's/^DEPLOY_TARGET_/ /' | sed 's/=/ → /' || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -d "$deploy_root/hosts/$host" ]]; then
|
||||
cd "$deploy_root/hosts/$host" || return 1
|
||||
elif [[ -d "$deploy_root/$host" ]]; then
|
||||
cd "$deploy_root/$host" || return 1
|
||||
elif [[ -d "$deploy_root" ]]; then
|
||||
cd "$deploy_root" || return 1
|
||||
else
|
||||
echo "Deploy path not found: $deploy_root"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
alias dep=deploy_nav
|
||||
|
||||
# Navigate to the shared repos directory.
|
||||
shared() {
|
||||
_devenv_load_config
|
||||
cd "${SHARED_DIR:-$DEV_ROOT/shared}" || return 1
|
||||
}
|
||||
|
||||
# Navigate to the bare-repos directory.
|
||||
repos() {
|
||||
_devenv_load_config
|
||||
cd "${REPOS_DIR:-$DEV_ROOT/repos}" || return 1
|
||||
}
|
||||
|
||||
# Navigate to the upstream-prs directory.
|
||||
prs() {
|
||||
_devenv_load_config
|
||||
cd "${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}" || return 1
|
||||
}
|
||||
144
modules/dev-env/scripts/pr-helpers.sh
Normal file
144
modules/dev-env/scripts/pr-helpers.sh
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env: upstream PR worktree helpers
|
||||
#
|
||||
# Sourced into interactive shells. Provides prb / prc / prl. Each PR gets
|
||||
# a throwaway worktree at ${UPSTREAM_PRS_DIR}/<repo>-<branch> based on
|
||||
# upstream/main (or master), ready to push to the `fork` remote and open
|
||||
# a PR. Reads paths from /etc/dev-env/config.sh.
|
||||
|
||||
_devenv_load_config() {
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
}
|
||||
|
||||
# Create a PR worktree at ${UPSTREAM_PRS_DIR}/<repo>-<branch>.
|
||||
#
|
||||
# Usage: git-pr-branch <repo-name> <branch-name>
|
||||
git-pr-branch() {
|
||||
_devenv_load_config
|
||||
local repo_name="${1:-}"
|
||||
local branch_name="${2:-}"
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
local prs_dir="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}"
|
||||
|
||||
if [[ -z "$repo_name" || -z "$branch_name" ]]; then
|
||||
echo "Usage: git-pr-branch <repo-name> <branch-name>"
|
||||
echo "Example: git-pr-branch lnbits fix-invoice-bug"
|
||||
echo ""
|
||||
echo "Creates a worktree at $prs_dir/<repo>-<branch>"
|
||||
echo "based on upstream/main, ready for an upstream PR."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local bare_repo="$repos_dir/${repo_name}.git"
|
||||
local pr_path="$prs_dir/${repo_name}-${branch_name}"
|
||||
|
||||
if [[ ! -d "$bare_repo" ]]; then
|
||||
echo "Repo not found: $bare_repo"
|
||||
echo "Available repos:"
|
||||
ls -1 "$repos_dir"/*.git 2>/dev/null | xargs -n1 basename | sed 's/\.git$//'
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -d "$pr_path" ]]; then
|
||||
echo "PR worktree already exists: $pr_path"
|
||||
echo "To remove it: git-pr-cleanup $repo_name $branch_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Fetch upstream
|
||||
echo "Fetching upstream..."
|
||||
git -C "$bare_repo" fetch upstream 2>/dev/null || {
|
||||
echo "No 'upstream' remote on $repo_name — cannot create PR branch"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Determine base branch (main or master)
|
||||
local base_branch="main"
|
||||
git -C "$bare_repo" show-ref --verify --quiet "refs/remotes/upstream/main" \
|
||||
|| base_branch="master"
|
||||
|
||||
echo "Creating branch '$branch_name' from upstream/$base_branch..."
|
||||
git -C "$bare_repo" branch "$branch_name" "upstream/$base_branch" 2>/dev/null \
|
||||
|| git -C "$bare_repo" branch -f "$branch_name" "upstream/$base_branch"
|
||||
|
||||
mkdir -p "$prs_dir"
|
||||
git -C "$bare_repo" worktree add "$pr_path" "$branch_name"
|
||||
|
||||
if ! git -C "$bare_repo" remote get-url fork &>/dev/null; then
|
||||
echo ""
|
||||
echo "Note: 'fork' remote not configured for $repo_name."
|
||||
echo "Add it with:"
|
||||
echo " git -C $bare_repo remote add fork git@github.com:${GITHUB_FORK_USER:-<user>}/${repo_name}.git"
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Ready! Your PR worktree is at:
|
||||
cd $pr_path
|
||||
|
||||
Workflow:
|
||||
1. cd $pr_path
|
||||
2. Edit, test, commit
|
||||
3. git push fork $branch_name
|
||||
4. Open the PR on GitHub (against upstream/$base_branch)
|
||||
5. After merge: git-pr-cleanup $repo_name $branch_name
|
||||
EOF
|
||||
}
|
||||
|
||||
# Remove a PR worktree after the PR is merged (or abandoned).
|
||||
git-pr-cleanup() {
|
||||
_devenv_load_config
|
||||
local repo_name="${1:-}"
|
||||
local branch_name="${2:-}"
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
local prs_dir="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}"
|
||||
|
||||
if [[ -z "$repo_name" || -z "$branch_name" ]]; then
|
||||
echo "Usage: git-pr-cleanup <repo-name> <branch-name>"
|
||||
echo ""
|
||||
echo "Active PR worktrees:"
|
||||
ls -1 "$prs_dir" 2>/dev/null || echo " (none)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local bare_repo="$repos_dir/${repo_name}.git"
|
||||
local pr_path="$prs_dir/${repo_name}-${branch_name}"
|
||||
|
||||
[[ -d "$pr_path" ]] || { echo "PR worktree not found: $pr_path"; return 1; }
|
||||
|
||||
echo "Removing worktree: $pr_path"
|
||||
git -C "$bare_repo" worktree remove "$pr_path"
|
||||
|
||||
echo "Deleting branch: $branch_name"
|
||||
git -C "$bare_repo" branch -d "$branch_name" 2>/dev/null \
|
||||
|| git -C "$bare_repo" branch -D "$branch_name"
|
||||
|
||||
echo "Done."
|
||||
}
|
||||
|
||||
# List all active PR worktrees.
|
||||
git-pr-list() {
|
||||
_devenv_load_config
|
||||
local prs_dir="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}"
|
||||
|
||||
echo "=== Active PR Worktrees ==="
|
||||
if [[ -d "$prs_dir" && -n "$(ls -A "$prs_dir" 2>/dev/null)" ]]; then
|
||||
for pr_dir in "$prs_dir"/*; do
|
||||
[[ -d "$pr_dir" ]] || continue
|
||||
local name branch status
|
||||
name="$(basename "$pr_dir")"
|
||||
branch="$(git -C "$pr_dir" branch --show-current 2>/dev/null || echo '?')"
|
||||
status="$(git -C "$pr_dir" status -sb 2>/dev/null | head -1 || echo '?')"
|
||||
printf " %-40s %s %s\n" "$name" "$branch" "$status"
|
||||
done
|
||||
else
|
||||
echo " (none)"
|
||||
fi
|
||||
}
|
||||
|
||||
alias prb='git-pr-branch'
|
||||
alias prc='git-pr-cleanup'
|
||||
alias prl='git-pr-list'
|
||||
409
modules/dev-env/scripts/rebase.sh
Normal file
409
modules/dev-env/scripts/rebase.sh
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env: safe fork-onto-upstream rebase helper
|
||||
#
|
||||
# Walks every worktree under $DEV_ROOT that has an `upstream` remote and
|
||||
# rebases your fork commits onto upstream, with backups and a
|
||||
# force-with-lease push gated on confirmation. Reads paths from
|
||||
# /etc/dev-env/config.sh; the rebase log lives under the user's XDG
|
||||
# state dir.
|
||||
#
|
||||
# Workflow for a single rebase:
|
||||
# 1. Safety checks (no uncommitted changes, upstream remote exists)
|
||||
# 2. Create backup branch `backup/pre-rebase-YYYYMMDD-HHMMSS`
|
||||
# 3. Show incoming + outgoing commits
|
||||
# 4. Rebase and force-with-lease push (with confirmation)
|
||||
# 5. On conflict, print resolution guide and preserve backup
|
||||
|
||||
set -e
|
||||
|
||||
# Load runtime config (sets DEV_ROOT, REPOS_DIR, SHARED_DIR, …)
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
else
|
||||
echo "Error: /etc/dev-env/config.sh not found (is the dev-env module enabled?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEV_ROOT="${DEV_ROOT:-$HOME/dev}"
|
||||
REPOS_DIR="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
SHARED_DIR="${SHARED_DIR:-$DEV_ROOT/shared}"
|
||||
|
||||
BACKUP_PREFIX="backup/pre-rebase"
|
||||
LOG_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/dev-env/rebase.log"
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Colors
|
||||
#-------------------------------------------------------------------------------
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
success() { echo -e "${GREEN}[OK]${NC} $1"; }
|
||||
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
header() { echo -e "\n${BOLD}${CYAN}═══ $1 ═══${NC}\n"; }
|
||||
|
||||
log_action() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local prompt="${1:-Continue?}"
|
||||
read -r -p "$prompt (y/N) " -n 1 REPLY
|
||||
echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]]
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Generic repo helpers
|
||||
#-------------------------------------------------------------------------------
|
||||
get_upstream_branch() {
|
||||
local repo_path="$1"
|
||||
(cd "$repo_path"
|
||||
for branch in main master; do
|
||||
if git rev-parse "upstream/$branch" &>/dev/null; then
|
||||
echo "$branch"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
echo "main")
|
||||
}
|
||||
|
||||
check_repo_clean() {
|
||||
local repo_path="$1"
|
||||
[[ -z "$(git -C "$repo_path" status --porcelain)" ]]
|
||||
}
|
||||
|
||||
has_upstream() {
|
||||
git -C "$1" remote get-url upstream &>/dev/null
|
||||
}
|
||||
|
||||
create_backup() {
|
||||
local repo_path="$1"
|
||||
local branch
|
||||
branch="$(git -C "$repo_path" branch --show-current)"
|
||||
local backup_name="${BACKUP_PREFIX}-$(date +%Y%m%d-%H%M%S)"
|
||||
git -C "$repo_path" branch -f "$backup_name" "$branch"
|
||||
echo "$backup_name"
|
||||
}
|
||||
|
||||
show_divergence() {
|
||||
local repo_path="$1" upstream_branch="$2"
|
||||
local behind ahead
|
||||
behind=$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo 0)
|
||||
ahead=$(git -C "$repo_path" rev-list --count "upstream/$upstream_branch..HEAD" 2>/dev/null || echo 0)
|
||||
echo -e " ${CYAN}Behind upstream:${NC} $behind commits"
|
||||
echo -e " ${GREEN}Ahead (your work):${NC} $ahead commits"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Core rebase
|
||||
#-------------------------------------------------------------------------------
|
||||
rebase_single_repo() {
|
||||
local repo_path="$1" repo_name="$2"
|
||||
local upstream_branch="${3:-}"
|
||||
local auto_mode="${4:-false}"
|
||||
|
||||
header "Rebasing: $repo_name"
|
||||
|
||||
if [[ ! -d "$repo_path/.git" ]] && [[ ! -f "$repo_path/.git" ]]; then
|
||||
error "Not a git repository: $repo_path"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! has_upstream "$repo_path"; then
|
||||
warn "No upstream remote configured. Skipping."
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! check_repo_clean "$repo_path"; then
|
||||
error "Uncommitted changes detected!"
|
||||
echo " Stash or commit first: git stash / git commit"
|
||||
return 1
|
||||
fi
|
||||
|
||||
[[ -z "$upstream_branch" ]] && upstream_branch="$(get_upstream_branch "$repo_path")"
|
||||
|
||||
local current_branch
|
||||
current_branch="$(git -C "$repo_path" branch --show-current)"
|
||||
info "Current branch: $current_branch"
|
||||
info "Upstream branch: upstream/$upstream_branch"
|
||||
|
||||
info "Fetching upstream..."
|
||||
git -C "$repo_path" fetch upstream
|
||||
|
||||
show_divergence "$repo_path" "$upstream_branch"
|
||||
|
||||
local behind
|
||||
behind=$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo 0)
|
||||
if [[ "$behind" -eq 0 ]]; then
|
||||
success "Already up to date with upstream!"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}New commits from upstream:${NC}"
|
||||
git -C "$repo_path" log --oneline --graph "HEAD..upstream/$upstream_branch" | head -15
|
||||
local total_upstream
|
||||
total_upstream="$(git -C "$repo_path" rev-list --count "HEAD..upstream/$upstream_branch")"
|
||||
(( total_upstream > 15 )) && echo " ... and $((total_upstream - 15)) more commits"
|
||||
|
||||
echo ""
|
||||
echo -e "${BOLD}Your commits to replay:${NC}"
|
||||
git -C "$repo_path" log --oneline "upstream/$upstream_branch..HEAD" | head -15
|
||||
local total_yours
|
||||
total_yours="$(git -C "$repo_path" rev-list --count "upstream/$upstream_branch..HEAD")"
|
||||
(( total_yours > 15 )) && echo " ... and $((total_yours - 15)) more commits"
|
||||
|
||||
echo ""
|
||||
if [[ "$auto_mode" != "true" ]] && ! confirm "Proceed with rebase?"; then
|
||||
warn "Skipped."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local backup_branch
|
||||
backup_branch="$(create_backup "$repo_path")"
|
||||
success "Backup created: $backup_branch"
|
||||
|
||||
info "Rebasing onto upstream/$upstream_branch..."
|
||||
if git -C "$repo_path" rebase "upstream/$upstream_branch"; then
|
||||
success "Rebase completed successfully!"
|
||||
echo ""
|
||||
if confirm "Push to origin with --force-with-lease?"; then
|
||||
git -C "$repo_path" push origin "$current_branch" --force-with-lease
|
||||
success "Pushed to origin!"
|
||||
log_action "REBASE SUCCESS: $repo_name onto upstream/$upstream_branch"
|
||||
else
|
||||
warn "Not pushed. When ready: git push origin $current_branch --force-with-lease"
|
||||
fi
|
||||
echo ""
|
||||
info "Backup branch '$backup_branch' preserved."
|
||||
echo " Delete later: git branch -D $backup_branch"
|
||||
return 0
|
||||
else
|
||||
error "Rebase encountered conflicts!"
|
||||
cat <<EOF
|
||||
|
||||
${BOLD}${YELLOW}Conflict Resolution Guide:${NC}
|
||||
1. Check conflicts: ${CYAN}git status${NC}
|
||||
2. Resolve markers <<<<<<< ======= >>>>>>>
|
||||
3. Stage: ${CYAN}git add <file>${NC}
|
||||
4. Continue: ${CYAN}git rebase --continue${NC}
|
||||
5. Skip empty: ${CYAN}git rebase --skip${NC}
|
||||
6. Abort: ${CYAN}git rebase --abort${NC}
|
||||
7. After success: ${CYAN}git push origin $current_branch --force-with-lease${NC}
|
||||
|
||||
Backup: ${GREEN}$backup_branch${NC}
|
||||
EOF
|
||||
log_action "REBASE CONFLICT: $repo_name - manual resolution required"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Discovery: walk actual worktrees under $DEV_ROOT instead of the old
|
||||
# $PROJECTS_DIR layout that no longer exists.
|
||||
#-------------------------------------------------------------------------------
|
||||
find_forked_repos() {
|
||||
# Print one "<path>:<display-name>" per rebase-eligible repo (has
|
||||
# upstream remote). We look at every git dir (.git dir or .git file
|
||||
# for worktrees) under $DEV_ROOT, excluding $REPOS_DIR bare repos.
|
||||
find "$DEV_ROOT" -type d -name '.git' -prune 2>/dev/null | while read -r gitdir; do
|
||||
local repo_path
|
||||
repo_path="$(dirname "$gitdir")"
|
||||
# Skip bare repos under REPOS_DIR
|
||||
[[ "$repo_path" == "$REPOS_DIR"* ]] && continue
|
||||
# Skip node_modules
|
||||
[[ "$repo_path" == *"/node_modules/"* ]] && continue
|
||||
if has_upstream "$repo_path"; then
|
||||
local display
|
||||
display="${repo_path#$DEV_ROOT/}"
|
||||
echo "$repo_path:$display"
|
||||
fi
|
||||
done
|
||||
# Also bare repos under REPOS_DIR (the .git file case)
|
||||
find "$DEV_ROOT" -type f -name '.git' 2>/dev/null | while read -r gitfile; do
|
||||
local repo_path
|
||||
repo_path="$(dirname "$gitfile")"
|
||||
[[ "$repo_path" == *"/node_modules/"* ]] && continue
|
||||
if has_upstream "$repo_path"; then
|
||||
local display
|
||||
display="${repo_path#$DEV_ROOT/}"
|
||||
echo "$repo_path:$display"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Batch modes
|
||||
#-------------------------------------------------------------------------------
|
||||
rebase_all() {
|
||||
header "Rebasing ALL Forks with Upstreams"
|
||||
warn "This walks $DEV_ROOT for every worktree with an 'upstream' remote."
|
||||
echo ""
|
||||
if ! confirm "Continue?"; then info "Aborted."; return 0; fi
|
||||
|
||||
local failed=()
|
||||
while IFS=: read -r path name; do
|
||||
if ! rebase_single_repo "$path" "$name"; then
|
||||
failed+=("$name")
|
||||
fi
|
||||
done < <(find_forked_repos)
|
||||
|
||||
echo ""
|
||||
header "Summary"
|
||||
if (( ${#failed[@]} == 0 )); then
|
||||
success "All repositories rebased successfully!"
|
||||
else
|
||||
error "Failed (${#failed[@]}):"
|
||||
printf ' - %s\n' "${failed[@]}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
show_all_status() {
|
||||
header "Repository Rebase Status"
|
||||
while IFS=: read -r path name; do
|
||||
local upstream_branch behind ahead
|
||||
upstream_branch="$(get_upstream_branch "$path")"
|
||||
git -C "$path" fetch upstream --quiet 2>/dev/null || true
|
||||
behind=$(git -C "$path" rev-list --count "HEAD..upstream/$upstream_branch" 2>/dev/null || echo ?)
|
||||
ahead=$(git -C "$path" rev-list --count "upstream/$upstream_branch..HEAD" 2>/dev/null || echo ?)
|
||||
printf " %-40s ↓%-3s ↑%-3s\n" "$name" "$behind" "$ahead"
|
||||
done < <(find_forked_repos)
|
||||
echo ""
|
||||
echo -e "${CYAN}Legend:${NC} ↓=behind upstream ↑=ahead (your commits)"
|
||||
}
|
||||
|
||||
view_log() {
|
||||
header "Rebase Log"
|
||||
if [[ -f "$LOG_FILE" ]]; then tail -50 "$LOG_FILE"
|
||||
else info "No rebase log yet at $LOG_FILE"; fi
|
||||
}
|
||||
|
||||
cleanup_backups() {
|
||||
header "Cleanup Backup Branches"
|
||||
echo "Options:"
|
||||
echo " 1) Delete backups older than 7 days"
|
||||
echo " 2) Delete backups older than 30 days"
|
||||
echo " 3) Delete ALL backup branches"
|
||||
echo " 4) Cancel"
|
||||
read -r -p "Select option: " -n 1 REPLY; echo ""
|
||||
|
||||
local days=0
|
||||
case $REPLY in
|
||||
1) days=7 ;;
|
||||
2) days=30 ;;
|
||||
3) days=0 ;;
|
||||
*) info "Cancelled."; return 0 ;;
|
||||
esac
|
||||
|
||||
local cutoff=""
|
||||
(( days > 0 )) && cutoff=$(date -d "$days days ago" +%Y%m%d 2>/dev/null || date -v-"${days}d" +%Y%m%d)
|
||||
|
||||
local deleted=0
|
||||
while IFS=: read -r path _; do
|
||||
cd "$path" || continue
|
||||
git branch --list "backup/*" 2>/dev/null | while read -r branch; do
|
||||
branch="$(echo "$branch" | tr -d ' *')"
|
||||
local bdate
|
||||
bdate="$(echo "$branch" | grep -oE '[0-9]{8}' | head -1)"
|
||||
if [[ $days -eq 0 ]] \
|
||||
|| { [[ -n "$bdate" ]] && [[ "$bdate" < "$cutoff" ]]; }; then
|
||||
git branch -D "$branch" 2>/dev/null && deleted=$((deleted + 1)) || true
|
||||
fi
|
||||
done
|
||||
done < <(find_forked_repos)
|
||||
success "Deleted $deleted backup branch(es)."
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Interactive / CLI
|
||||
#-------------------------------------------------------------------------------
|
||||
interactive_menu() {
|
||||
while true; do
|
||||
header "Rebase Helper"
|
||||
echo " 1) Rebase a single repository (by path)"
|
||||
echo " 2) Rebase ALL forks with upstream"
|
||||
echo " s) Status of all repos"
|
||||
echo " l) View rebase log"
|
||||
echo " c) Clean up old backup branches"
|
||||
echo " q) Quit"
|
||||
echo ""
|
||||
read -r -p "Select: " -n 1 REPLY; echo ""
|
||||
case $REPLY in
|
||||
1) read -r -p "Repo path: " p
|
||||
[[ -d "$p/.git" || -f "$p/.git" ]] && rebase_single_repo "$p" "$(basename "$p")" || warn "Not a repo: $p" ;;
|
||||
2) rebase_all ;;
|
||||
s) show_all_status ;;
|
||||
l) view_log ;;
|
||||
c) cleanup_backups ;;
|
||||
q) echo "Bye!"; exit 0 ;;
|
||||
*) warn "Invalid option" ;;
|
||||
esac
|
||||
echo ""
|
||||
read -r -p "Press Enter to continue..."
|
||||
done
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
${BOLD}rebase${NC} — Safely rebase forks onto upstream
|
||||
|
||||
USAGE:
|
||||
rebase Interactive mode
|
||||
rebase single <path> Rebase a single repository
|
||||
rebase all Rebase all forks with upstream remote
|
||||
rebase status Show status of all forks
|
||||
rebase log Show rebase log
|
||||
rebase cleanup Clean up old backup branches
|
||||
|
||||
OPTIONS:
|
||||
-b, --branch <name> Upstream branch to rebase onto
|
||||
-y, --yes Auto-confirm prompts
|
||||
-h, --help Show this help
|
||||
|
||||
Backups are branched automatically as backup/pre-rebase-YYYYMMDD-HHMMSS
|
||||
and never deleted without explicit cleanup.
|
||||
EOF
|
||||
}
|
||||
|
||||
main() {
|
||||
local command="" repo_path="" upstream_branch="" auto_yes=false
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-b|--branch) upstream_branch="$2"; shift 2 ;;
|
||||
-y|--yes) auto_yes=true; shift ;;
|
||||
single|all|status|log|cleanup) command="$1"; shift ;;
|
||||
*)
|
||||
if [[ -z "$command" ]] && [[ -d "$1/.git" || -f "$1/.git" ]]; then
|
||||
command="single"; repo_path="$1"
|
||||
else
|
||||
repo_path="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case $command in
|
||||
"") interactive_menu ;;
|
||||
single) [[ -z "$repo_path" ]] && { error "Need path"; exit 1; }
|
||||
rebase_single_repo "$repo_path" "$(basename "$repo_path")" "$upstream_branch" "$auto_yes" ;;
|
||||
all) rebase_all ;;
|
||||
status) show_all_status ;;
|
||||
log) view_log ;;
|
||||
cleanup) cleanup_backups ;;
|
||||
*) error "Unknown command: $command"; usage; exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
267
modules/dev-env/scripts/regtest.sh
Normal file
267
modules/dev-env/scripts/regtest.sh
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env: Bitcoin/Lightning regtest docker environment
|
||||
#
|
||||
# Provides:
|
||||
# regtest-start [worktree|pr:<branch>] [--path <dir>] [--seed|--keep]
|
||||
# regtest-stop
|
||||
# regtest-status
|
||||
# regtest-logs [service]
|
||||
# regtest-cli # source CLI helpers
|
||||
# regtest # cd to regtest dir
|
||||
#
|
||||
# Reads paths from /etc/dev-env/config.sh. Wraps the
|
||||
# `lnbits/legend-regtest-enviroment` compose stack (or your fork of it,
|
||||
# set via devEnv.regtest.repoUrl). Container names and ports below assume
|
||||
# that stack's layout — adjust if you've customised it.
|
||||
#
|
||||
# This file is sourced into interactive shells (function definitions) AND
|
||||
# dropped into the system path as standalone wrapper scripts so
|
||||
# `regtest-start` works from a fresh non-interactive shell.
|
||||
|
||||
_devenv_load_config() {
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
REGTEST_DIR="${LOCAL_DIR:-$DEV_ROOT/local}/docker/regtest"
|
||||
LNBITS_DIR="${LNBITS_DIR:-$DEV_ROOT/lnbits}"
|
||||
UPSTREAM_PRS_DIR="${UPSTREAM_PRS_DIR:-$DEV_ROOT/upstream-prs}"
|
||||
# Node used for readiness/balance checks. Override if your stack
|
||||
# names containers differently.
|
||||
REGTEST_LND="${REGTEST_LND:-lnbits-lnd-4-1}"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Status helpers
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
_regtest_is_running() {
|
||||
docker ps --filter "name=$REGTEST_LND" --format '{{.Names}}' 2>/dev/null \
|
||||
| grep -q "$REGTEST_LND"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Start
|
||||
#-------------------------------------------------------------------------------
|
||||
# Usage: regtest-start [worktree|pr:<branch>] [--path <dir>] [--seed|--keep]
|
||||
#
|
||||
# worktree an lnbits worktree name (under ~/dev/lnbits/<name>)
|
||||
# pr:<branch> ~/dev/upstream-prs/lnbits-<branch>
|
||||
# --path <dir> arbitrary lnbits directory
|
||||
# --seed copy lnbits-seed/ to lnbits/ before starting
|
||||
# --keep keep existing data
|
||||
regtest-start() {
|
||||
_devenv_load_config
|
||||
|
||||
local env="" data_mode="fresh" custom_path=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--seed) data_mode="seed" ;;
|
||||
--keep) data_mode="keep" ;;
|
||||
--path) shift; custom_path="$1" ;;
|
||||
pr:*)
|
||||
local pr_branch="${1#pr:}"
|
||||
custom_path="$UPSTREAM_PRS_DIR/lnbits-$pr_branch"
|
||||
;;
|
||||
-*)
|
||||
cat <<USAGE
|
||||
Unknown argument: $1
|
||||
Usage: regtest-start [worktree|pr:<branch>] [--path <dir>] [--seed|--keep]
|
||||
|
||||
worktree an lnbits worktree name (under ~/dev/lnbits/<name>)
|
||||
pr:<branch> ~/dev/upstream-prs/lnbits-<branch>
|
||||
--path <dir> arbitrary lnbits directory
|
||||
USAGE
|
||||
return 1
|
||||
;;
|
||||
*) env="$1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ ! -d "$REGTEST_DIR" ]]; then
|
||||
echo "Regtest environment not found at $REGTEST_DIR"
|
||||
echo "Run dev-env-bootstrap to clone it."
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Determine lnbits source dir
|
||||
local lnbits_path=""
|
||||
if [[ -n "$custom_path" ]]; then
|
||||
lnbits_path="$custom_path"
|
||||
[[ -d "$lnbits_path" ]] || { echo "lnbits dir not found: $lnbits_path"; return 1; }
|
||||
else
|
||||
[[ -z "$env" ]] && env="dev"
|
||||
lnbits_path="$LNBITS_DIR/$env"
|
||||
[[ -d "$lnbits_path" ]] || { echo "lnbits worktree not found: $env"; return 1; }
|
||||
fi
|
||||
|
||||
local lnbits_data="$REGTEST_DIR/data/lnbits"
|
||||
local lnbits_seed="$REGTEST_DIR/data/lnbits-seed"
|
||||
|
||||
case "$data_mode" in
|
||||
fresh)
|
||||
if [[ -d "$lnbits_data" ]] && [[ -n "$(ls -A "$lnbits_data" 2>/dev/null)" ]]; then
|
||||
echo "Existing lnbits data at $lnbits_data"
|
||||
read -r -p "Wipe it? [y/N] " -n 1 REPLY; echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; return 1; }
|
||||
docker run --rm -v "$lnbits_data":/data alpine sh -c "rm -rf /data/* /data/.*" 2>/dev/null || true
|
||||
rmdir "$lnbits_data" 2>/dev/null || true
|
||||
fi
|
||||
mkdir -p "$lnbits_data"
|
||||
;;
|
||||
seed)
|
||||
[[ -d "$lnbits_seed" ]] || { echo "Seed data not found at $lnbits_seed"; return 1; }
|
||||
if [[ -d "$lnbits_data" ]] && [[ -n "$(ls -A "$lnbits_data" 2>/dev/null)" ]]; then
|
||||
read -r -p "Overwrite with seed? [y/N] " -n 1 REPLY; echo
|
||||
[[ $REPLY =~ ^[Yy]$ ]] || { echo "Aborted."; return 1; }
|
||||
docker run --rm -v "$lnbits_data":/data alpine sh -c "rm -rf /data/* /data/.*" 2>/dev/null || true
|
||||
rmdir "$lnbits_data" 2>/dev/null || true
|
||||
fi
|
||||
cp -r "$lnbits_seed" "$lnbits_data"
|
||||
;;
|
||||
keep)
|
||||
mkdir -p "$lnbits_data"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
echo "Starting regtest..."
|
||||
echo " lnbits path: $lnbits_path"
|
||||
echo " data mode: $data_mode"
|
||||
echo ""
|
||||
|
||||
echo "Building lnbits Docker image from $lnbits_path..."
|
||||
docker build -t lnbits/lnbits "$lnbits_path"
|
||||
|
||||
(cd "$REGTEST_DIR" && ./start-regtest)
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Regtest running:
|
||||
LNbits: http://localhost:5001/
|
||||
Mempool: http://localhost:8080/
|
||||
Boltz: http://localhost:9001/
|
||||
|
||||
EOF
|
||||
|
||||
echo "Commands: regtest-stop, regtest-logs, regtest-cli, regtest-status"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Stop
|
||||
#-------------------------------------------------------------------------------
|
||||
regtest-stop() {
|
||||
_devenv_load_config
|
||||
|
||||
if [[ ! -d "$REGTEST_DIR" ]]; then
|
||||
echo "Regtest environment not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Stopping regtest environment..."
|
||||
(cd "$REGTEST_DIR"
|
||||
# shellcheck disable=SC1091
|
||||
source ./docker-scripts.sh
|
||||
docker compose down -v)
|
||||
echo "Regtest stopped"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Status / logs / CLI
|
||||
#-------------------------------------------------------------------------------
|
||||
regtest-status() {
|
||||
_devenv_load_config
|
||||
|
||||
echo "=== Regtest Environment ==="
|
||||
if _regtest_is_running; then
|
||||
echo "Status: RUNNING"
|
||||
echo ""
|
||||
docker ps --filter "name=lnbits-" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | head -15
|
||||
|
||||
local balance
|
||||
balance="$(docker exec "$REGTEST_LND" lncli --network=regtest walletbalance 2>/dev/null \
|
||||
| grep -oP '"total_balance":\s*"\K[0-9]+' || echo 0)"
|
||||
echo ""
|
||||
echo "$REGTEST_LND balance: $balance sats"
|
||||
else
|
||||
echo "Status: STOPPED"
|
||||
echo "Start with: regtest-start"
|
||||
fi
|
||||
}
|
||||
|
||||
regtest-logs() {
|
||||
_devenv_load_config
|
||||
local service="${1:-}"
|
||||
[[ -d "$REGTEST_DIR" ]] || { echo "Regtest not found"; return 1; }
|
||||
if [[ -n "$service" ]]; then
|
||||
(cd "$REGTEST_DIR" && docker compose logs -f "$service")
|
||||
else
|
||||
(cd "$REGTEST_DIR" && docker compose logs -f)
|
||||
fi
|
||||
}
|
||||
|
||||
regtest-cli() {
|
||||
_devenv_load_config
|
||||
[[ -f "$REGTEST_DIR/docker-scripts.sh" ]] || { echo "docker-scripts.sh missing"; return 1; }
|
||||
echo "Sourcing regtest CLI helpers..."
|
||||
# shellcheck disable=SC1091
|
||||
source "$REGTEST_DIR/docker-scripts.sh"
|
||||
cat <<EOF
|
||||
|
||||
Available commands:
|
||||
bitcoin-cli-sim Bitcoin Core CLI
|
||||
lncli-sim <1-4> LND node CLI
|
||||
lightning-cli-sim <1-3> C-Lightning node CLI
|
||||
boltzcli-sim Boltz client CLI
|
||||
elements-cli-sim Elements/Liquid CLI
|
||||
|
||||
Examples:
|
||||
bitcoin-cli-sim -generate 1
|
||||
lncli-sim 1 getinfo
|
||||
EOF
|
||||
}
|
||||
|
||||
regtest() {
|
||||
_devenv_load_config
|
||||
[[ -d "$REGTEST_DIR" ]] && cd "$REGTEST_DIR" || { echo "Regtest not found at $REGTEST_DIR"; return 1; }
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Rebuild / restart the lnbits service (docker-compose.dev.yml workflow)
|
||||
#-------------------------------------------------------------------------------
|
||||
# regtest-lnbits-rebuild build (cached) + recreate the lnbits container
|
||||
# regtest-lnbits-rebuild --clean build --no-cache + recreate (truly fresh image)
|
||||
# regtest-lnbits-restart restart the container without rebuilding
|
||||
#
|
||||
# The cached build is the happy path: docker invalidates the source COPY layer
|
||||
# when LNBITS_SRC content changes, so most rebuilds are fast. Use --clean when
|
||||
# you've been mucking with the image itself.
|
||||
|
||||
_regtest_lnbits_compose_file() {
|
||||
echo "$REGTEST_DIR/docker-compose.dev.yml"
|
||||
}
|
||||
|
||||
regtest-lnbits-rebuild() {
|
||||
_devenv_load_config
|
||||
local compose_file build_args=()
|
||||
compose_file="$(_regtest_lnbits_compose_file)"
|
||||
[[ -f "$compose_file" ]] || { echo "Compose file not found: $compose_file"; return 1; }
|
||||
|
||||
if [[ "${1:-}" == "--clean" ]]; then
|
||||
build_args+=(--no-cache)
|
||||
fi
|
||||
|
||||
(cd "$REGTEST_DIR" \
|
||||
&& docker compose -f "$compose_file" build "${build_args[@]}" lnbits \
|
||||
&& docker compose -f "$compose_file" up -d --force-recreate lnbits)
|
||||
}
|
||||
|
||||
regtest-lnbits-restart() {
|
||||
_devenv_load_config
|
||||
local compose_file
|
||||
compose_file="$(_regtest_lnbits_compose_file)"
|
||||
[[ -f "$compose_file" ]] || { echo "Compose file not found: $compose_file"; return 1; }
|
||||
docker compose -f "$compose_file" restart lnbits
|
||||
}
|
||||
145
modules/dev-env/scripts/status.sh
Normal file
145
modules/dev-env/scripts/status.sh
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-status: show divergence and dirty state of every dev-env worktree
|
||||
#
|
||||
# Reads /etc/dev-env/projects.json and walks every declared worktree,
|
||||
# reporting dirty state and ahead/behind counts vs origin (and upstream,
|
||||
# for projects that declare one).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
|
||||
PROJECTS_JSON="${DEVENV_PROJECTS_JSON:-/etc/dev-env/projects.json}"
|
||||
|
||||
if [[ ! -r "$PROJECTS_JSON" ]]; then
|
||||
echo "projects.json not found at $PROJECTS_JSON" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null; then
|
||||
echo "jq required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
issues=()
|
||||
|
||||
header() {
|
||||
echo ""
|
||||
echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BOLD}${CYAN} $1${NC}"
|
||||
echo -e "${BOLD}${CYAN}═══════════════════════════════════════════════════════${NC}"
|
||||
}
|
||||
|
||||
check_one() {
|
||||
local label="$1" path="$2" has_upstream="${3:-false}"
|
||||
|
||||
if [[ ! -d "$path/.git" ]] && [[ ! -f "$path/.git" ]]; then
|
||||
printf " ${YELLOW}⚠${NC} %s: not present\n" "$label"
|
||||
issues+=("$label: not present")
|
||||
return
|
||||
fi
|
||||
|
||||
git -C "$path" fetch --all --quiet 2>/dev/null || true
|
||||
|
||||
local branch icons="" status_str=""
|
||||
branch="$(git -C "$path" branch --show-current 2>/dev/null || echo '?')"
|
||||
|
||||
[[ -n "$(git -C "$path" status --porcelain)" ]] && {
|
||||
icons+="${YELLOW}●${NC} "
|
||||
status_str+="dirty "
|
||||
}
|
||||
|
||||
local behind ahead
|
||||
behind=$(git -C "$path" rev-list --count "HEAD..origin/$branch" 2>/dev/null || echo 0)
|
||||
ahead=$(git -C "$path" rev-list --count "origin/$branch..HEAD" 2>/dev/null || echo 0)
|
||||
(( behind > 0 )) && icons+="${RED}↓${NC}$behind "
|
||||
(( ahead > 0 )) && icons+="${GREEN}↑${NC}$ahead "
|
||||
|
||||
if [[ "$has_upstream" == "true" ]] && git -C "$path" remote get-url upstream &>/dev/null; then
|
||||
local ub="main"
|
||||
git -C "$path" rev-parse upstream/main &>/dev/null || ub="master"
|
||||
local ub_behind
|
||||
ub_behind=$(git -C "$path" rev-list --count "HEAD..upstream/$ub" 2>/dev/null || echo 0)
|
||||
if (( ub_behind > 0 )); then
|
||||
icons+="${CYAN}⇣${NC}$ub_behind "
|
||||
issues+=("$label: $ub_behind behind upstream/$ub")
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ -z "$icons" ]] && icons="${GREEN}✓${NC}"
|
||||
printf " %-45s %s (%s)\n" "$label" "$icons" "$branch"
|
||||
}
|
||||
|
||||
header "Development Environment Status"
|
||||
echo -e " ${BLUE}Date:${NC} $(date '+%Y-%m-%d %H:%M')"
|
||||
echo -e " ${BLUE}Host:${NC} $(hostname)"
|
||||
echo -e " ${BLUE}Root:${NC} ${DEV_ROOT:-?}"
|
||||
|
||||
# Walk every project and worktree from the JSON.
|
||||
mapfile -t PROJECTS < <(jq -r 'keys[]' "$PROJECTS_JSON")
|
||||
|
||||
for proj in "${PROJECTS[@]}"; do
|
||||
echo ""
|
||||
echo -e "${BLUE}─── $proj ───${NC}"
|
||||
|
||||
is_clone="$(jq -r --arg p "$proj" '.[$p].isClone' "$PROJECTS_JSON")"
|
||||
has_upstream_decl="$(jq -r --arg p "$proj" '.[$p].remotes.upstream != null' "$PROJECTS_JSON")"
|
||||
|
||||
if [[ "$is_clone" == "true" ]]; then
|
||||
clone_path="$(jq -r --arg p "$proj" '.[$p].clonePath' "$PROJECTS_JSON")"
|
||||
check_one "$proj" "$clone_path" "$has_upstream_decl"
|
||||
else
|
||||
while IFS=$'\t' read -r wt_name wt_path; do
|
||||
[[ -z "$wt_name" || "$wt_path" == "null" ]] && continue
|
||||
check_one "$proj/$wt_name" "$wt_path" "$has_upstream_decl"
|
||||
done < <(jq -r --arg p "$proj" '
|
||||
.[$p].worktrees
|
||||
| to_entries[]
|
||||
| "\(.key)\t\(.value.path)"
|
||||
' "$PROJECTS_JSON")
|
||||
fi
|
||||
done
|
||||
|
||||
# Docker
|
||||
echo ""
|
||||
echo -e "${BLUE}─── Docker ───${NC}"
|
||||
if command -v docker &>/dev/null; then
|
||||
running="$(docker ps --format '{{.Names}}' 2>/dev/null | wc -l)"
|
||||
echo -e " ${BLUE}Containers running:${NC} $running"
|
||||
fi
|
||||
|
||||
header "Summary"
|
||||
if (( ${#issues[@]} == 0 )); then
|
||||
echo -e " ${GREEN}✓ All worktrees are clean and in sync!${NC}"
|
||||
else
|
||||
echo -e " ${YELLOW}Issues found:${NC}"
|
||||
for issue in "${issues[@]}"; do
|
||||
echo -e " ${YELLOW}•${NC} $issue"
|
||||
done
|
||||
fi
|
||||
|
||||
cat <<EOF
|
||||
|
||||
${BLUE}Legend:${NC}
|
||||
${GREEN}✓${NC} clean ${YELLOW}●${NC} dirty ${GREEN}↑${NC} ahead origin
|
||||
${RED}↓${NC} behind origin ${CYAN}⇣${NC} behind upstream
|
||||
|
||||
${BLUE}Quick actions:${NC}
|
||||
wts sync all worktrees with origin
|
||||
wtu <repo> fetch upstream + show divergence
|
||||
rebase status which forks need rebasing onto upstream
|
||||
dev-env-bootstrap materialize missing worktrees
|
||||
|
||||
EOF
|
||||
131
modules/dev-env/scripts/tmux-launch.sh
Normal file
131
modules/dev-env/scripts/tmux-launch.sh
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-tm: launch a declared tmux session
|
||||
#
|
||||
# Reads /etc/dev-env/tmux-sessions.json (rendered by config.nix) and
|
||||
# either attaches to an existing session or creates one with the
|
||||
# declared windows. Window cwds are resolved relative to $DEV_ROOT.
|
||||
#
|
||||
# This is a single generic launcher; the window layouts come from Nix
|
||||
# (or a runtime override at ~/.config/dev-env/tmux-sessions.json).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
|
||||
DEV_ROOT="${DEV_ROOT:-$HOME/dev}"
|
||||
|
||||
# Prefer a user override if present
|
||||
SESSIONS_JSON=""
|
||||
if [[ -r "$HOME/.config/dev-env/tmux-sessions.json" ]]; then
|
||||
SESSIONS_JSON="$HOME/.config/dev-env/tmux-sessions.json"
|
||||
elif [[ -r /etc/dev-env/tmux-sessions.json ]]; then
|
||||
SESSIONS_JSON=/etc/dev-env/tmux-sessions.json
|
||||
else
|
||||
echo "No tmux-sessions.json found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
dev-tm — declarative tmux session launcher
|
||||
|
||||
USAGE:
|
||||
dev-tm list available sessions
|
||||
dev-tm <session> start or attach to <session>
|
||||
dev-tm -k <session> kill session
|
||||
dev-tm -k all kill all dev sessions
|
||||
dev-tm -l list available sessions
|
||||
EOF
|
||||
}
|
||||
|
||||
list_sessions() {
|
||||
echo "Defined sessions (from $SESSIONS_JSON):"
|
||||
jq -r 'keys[]' "$SESSIONS_JSON" | sed 's/^/ /'
|
||||
echo ""
|
||||
echo "Active tmux sessions:"
|
||||
tmux list-sessions 2>/dev/null | sed 's/^/ /' || echo " (none)"
|
||||
}
|
||||
|
||||
resolve_path() {
|
||||
local p="$1"
|
||||
[[ -z "$p" || "$p" == "null" ]] && { echo "$DEV_ROOT"; return; }
|
||||
[[ "$p" = /* ]] && { echo "$p"; return; }
|
||||
echo "$DEV_ROOT/$p"
|
||||
}
|
||||
|
||||
start_session() {
|
||||
local name="$1"
|
||||
local session_name="dev-$name"
|
||||
|
||||
if tmux has-session -t "$session_name" 2>/dev/null; then
|
||||
tmux attach-session -t "$session_name"
|
||||
return
|
||||
fi
|
||||
|
||||
local session_cwd
|
||||
session_cwd="$(resolve_path "$(jq -r --arg s "$name" '.[$s].cwd // empty' "$SESSIONS_JSON")")"
|
||||
|
||||
local nwindows
|
||||
nwindows="$(jq --arg s "$name" '.[$s].windows | length' "$SESSIONS_JSON")"
|
||||
if (( nwindows == 0 )); then
|
||||
echo "Session '$name' has no windows defined" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# First window
|
||||
local first_name first_cwd first_cmd
|
||||
first_name="$(jq -r --arg s "$name" '.[$s].windows[0].name' "$SESSIONS_JSON")"
|
||||
first_cwd="$(resolve_path "$(jq -r --arg s "$name" '.[$s].windows[0].cwd // empty' "$SESSIONS_JSON")")"
|
||||
first_cmd="$(jq -r --arg s "$name" '.[$s].windows[0].cmd // empty' "$SESSIONS_JSON")"
|
||||
|
||||
tmux new-session -d -s "$session_name" -n "$first_name" -c "$first_cwd"
|
||||
[[ -n "$first_cmd" && "$first_cmd" != "null" ]] && \
|
||||
tmux send-keys -t "$session_name:0" "$first_cmd" C-m
|
||||
|
||||
# Remaining windows
|
||||
for ((i = 1; i < nwindows; i++)); do
|
||||
local wn wcwd wcmd
|
||||
wn="$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].name' "$SESSIONS_JSON")"
|
||||
wcwd="$(resolve_path "$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].cwd // empty' "$SESSIONS_JSON")")"
|
||||
wcmd="$(jq -r --arg s "$name" --argjson i "$i" '.[$s].windows[$i].cmd // empty' "$SESSIONS_JSON")"
|
||||
tmux new-window -t "$session_name" -n "$wn" -c "$wcwd"
|
||||
[[ -n "$wcmd" && "$wcmd" != "null" ]] && \
|
||||
tmux send-keys -t "$session_name:$i" "$wcmd" C-m
|
||||
done
|
||||
|
||||
tmux select-window -t "$session_name:0"
|
||||
tmux attach-session -t "$session_name"
|
||||
}
|
||||
|
||||
kill_session() {
|
||||
local target="$1"
|
||||
if [[ "$target" == "all" ]]; then
|
||||
tmux list-sessions -F '#{session_name}' 2>/dev/null \
|
||||
| grep '^dev-' \
|
||||
| xargs -r -n1 tmux kill-session -t \
|
||||
|| echo "No dev sessions to kill"
|
||||
return
|
||||
fi
|
||||
tmux kill-session -t "dev-$target" 2>/dev/null || echo "Session 'dev-$target' not found"
|
||||
}
|
||||
|
||||
# --- entry ---
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
-l|--list|"") list_sessions ;;
|
||||
-k|--kill)
|
||||
[[ -z "${2:-}" ]] && { echo "Usage: dev-tm -k <session|all>"; exit 1; }
|
||||
kill_session "$2"
|
||||
;;
|
||||
*)
|
||||
if ! jq -e --arg s "$1" 'has($s)' "$SESSIONS_JSON" >/dev/null; then
|
||||
echo "Unknown session: $1" >&2
|
||||
list_sessions
|
||||
exit 1
|
||||
fi
|
||||
start_session "$1"
|
||||
;;
|
||||
esac
|
||||
275
modules/dev-env/scripts/worktree.sh
Normal file
275
modules/dev-env/scripts/worktree.sh
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
#!/usr/bin/env bash
|
||||
# dev-env: git worktree helpers
|
||||
#
|
||||
# Sourced into interactive shells. Provides:
|
||||
# - wt / wts / wtu generic worktree listing / sync / upstream-fetch
|
||||
# - wtn / worktree-spawn create a new branch + worktree from any project
|
||||
# - lnbits-status show dev/main divergence vs upstream
|
||||
# - lnbits-sync-dev merge upstream/dev into the dev worktree
|
||||
# - lnbits-sync-main merge upstream/main into the main worktree
|
||||
#
|
||||
# The bare-repo path comes from /etc/dev-env/config.sh.
|
||||
|
||||
_devenv_load_config() {
|
||||
if [[ -r /etc/dev-env/config.sh ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/dev-env/config.sh
|
||||
fi
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Generic worktree helpers
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
worktree-list() {
|
||||
_devenv_load_config
|
||||
local repo_name="${1:-}"
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
|
||||
if [[ -n "$repo_name" ]]; then
|
||||
local bare_repo="$repos_dir/${repo_name}.git"
|
||||
if [[ -d "$bare_repo" ]]; then
|
||||
git -C "$bare_repo" worktree list
|
||||
else
|
||||
echo "Repo not found: $repo_name"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
echo "=== All Worktrees ==="
|
||||
for repo in "$repos_dir"/*.git; do
|
||||
[[ -d "$repo" ]] || continue
|
||||
echo ""
|
||||
echo "--- $(basename "$repo" .git) ---"
|
||||
git -C "$repo" worktree list
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
worktree-sync() {
|
||||
_devenv_load_config
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
echo "=== Syncing Worktrees ==="
|
||||
for repo in "$repos_dir"/*.git; do
|
||||
[[ -d "$repo" ]] || continue
|
||||
local name
|
||||
name="$(basename "$repo" .git)"
|
||||
echo ""
|
||||
echo "--- $name ---"
|
||||
git -C "$repo" fetch --all --quiet 2>/dev/null || true
|
||||
git -C "$repo" worktree list | while read -r line; do
|
||||
# format: <path> <sha> [<branch>]
|
||||
local path branch status
|
||||
path="$(awk '{print $1}' <<<"$line")"
|
||||
branch="$(awk '{print $3}' <<<"$line" | tr -d '[]')"
|
||||
if [[ -d "$path" ]]; then
|
||||
status="$(git -C "$path" status -sb 2>/dev/null | head -1)"
|
||||
printf " %-20s %s\n" "${branch:-?}" "$status"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
worktree-update-upstream() {
|
||||
_devenv_load_config
|
||||
local repo_name="${1:-}"
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
|
||||
if [[ -z "$repo_name" ]]; then
|
||||
echo "Usage: wtu <repo-name>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local bare_repo="$repos_dir/${repo_name}.git"
|
||||
if [[ ! -d "$bare_repo" ]]; then
|
||||
echo "Repo not found: $bare_repo"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! git -C "$bare_repo" remote get-url upstream &>/dev/null; then
|
||||
echo "No upstream remote configured for $repo_name"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Fetching upstream..."
|
||||
git -C "$bare_repo" fetch upstream
|
||||
|
||||
echo ""
|
||||
echo "=== Upstream Status: $repo_name ==="
|
||||
|
||||
# Determine upstream base branch (main or master).
|
||||
local base_branch="main"
|
||||
git -C "$bare_repo" show-ref --verify --quiet refs/remotes/upstream/main || base_branch="master"
|
||||
|
||||
# Show each local branch's divergence vs the upstream base.
|
||||
git -C "$bare_repo" for-each-ref --format='%(refname:short)' refs/heads | while read -r br; do
|
||||
local behind ahead
|
||||
behind=$(git -C "$bare_repo" rev-list --count "$br..upstream/$base_branch" 2>/dev/null || echo ?)
|
||||
ahead=$(git -C "$bare_repo" rev-list --count "upstream/$base_branch..$br" 2>/dev/null || echo ?)
|
||||
printf " %-24s behind:%s ahead:%s\n" "$br" "$behind" "$ahead"
|
||||
done
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Spawn a new worktree (== branch) from an existing project.
|
||||
#-------------------------------------------------------------------------------
|
||||
# Usage: worktree-spawn <repo> <new-branch> [base-branch]
|
||||
#
|
||||
# Locates the bare repo, creates <new-branch> from <base-branch> (default
|
||||
# main, else master), and adds a worktree at the project root next to the
|
||||
# base worktree.
|
||||
|
||||
worktree-spawn() {
|
||||
_devenv_load_config
|
||||
local repo_name="${1:-}"
|
||||
local new_branch="${2:-}"
|
||||
local base_branch="${3:-}"
|
||||
|
||||
if [[ -z "$repo_name" || -z "$new_branch" ]]; then
|
||||
echo "Usage: worktree-spawn <repo-name> <new-branch-name> [base-branch]"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local repos_dir="${REPOS_DIR:-$DEV_ROOT/repos}"
|
||||
local bare_repo="$repos_dir/${repo_name}.git"
|
||||
|
||||
[[ -d "$bare_repo" ]] || { echo "Repo not found: $bare_repo"; return 1; }
|
||||
|
||||
if [[ -z "$base_branch" ]]; then
|
||||
if git -C "$bare_repo" show-ref --verify --quiet refs/heads/main; then
|
||||
base_branch="main"
|
||||
elif git -C "$bare_repo" show-ref --verify --quiet refs/heads/master; then
|
||||
base_branch="master"
|
||||
else
|
||||
echo "No main/master in $repo_name; pass base branch explicitly."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local base_path
|
||||
base_path="$(git -C "$bare_repo" worktree list --porcelain | awk -v t="refs/heads/$base_branch" '
|
||||
/^worktree / { wt = $2 }
|
||||
/^branch / && $2 == t { print wt; exit }
|
||||
')"
|
||||
[[ -n "$base_path" ]] || { echo "No worktree for $repo_name@$base_branch"; return 1; }
|
||||
|
||||
local project_root new_path
|
||||
project_root="$(dirname "$base_path")"
|
||||
new_path="$project_root/$new_branch"
|
||||
|
||||
[[ -e "$new_path" ]] && { echo "Path already exists: $new_path"; return 1; }
|
||||
|
||||
echo "Creating branch '$new_branch' from '$base_branch' in $repo_name..."
|
||||
git -C "$bare_repo" branch "$new_branch" "$base_branch" || return 1
|
||||
|
||||
echo "Adding worktree at $new_path..."
|
||||
git -C "$bare_repo" worktree add "$new_path" "$new_branch" || return 1
|
||||
|
||||
echo ""
|
||||
echo "Ready: $new_path"
|
||||
[[ -f "$new_path/package.json" ]] && echo "Next: cd $new_path && pnpm install"
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# lnbits fork workflow helpers
|
||||
#-------------------------------------------------------------------------------
|
||||
# Mental model:
|
||||
# Your `dev` and `main` worktrees track a personal fork of lnbits that
|
||||
# diverges from upstream and carries your own commits on top. The sync
|
||||
# helpers below `merge upstream/<branch>` to vendor upstream changes —
|
||||
# they don't fast-forward. `lnbits-status` reports divergence vs
|
||||
# upstream so you know how stale the vendor base is.
|
||||
#
|
||||
# Adjust the `dev`/`main` worktree names to whatever your lnbits
|
||||
# project declares in devEnv.projects.lnbits.worktrees.
|
||||
|
||||
_lnbits_paths() {
|
||||
_devenv_load_config
|
||||
BARE="${REPOS_DIR:-$DEV_ROOT/repos}/lnbits.git"
|
||||
LNBITS_ROOT="${LNBITS_DIR:-$DEV_ROOT/lnbits}"
|
||||
}
|
||||
|
||||
lnbits-status() {
|
||||
_lnbits_paths
|
||||
if [[ ! -d "$BARE" ]]; then
|
||||
echo "lnbits bare repo not found: $BARE"
|
||||
return 1
|
||||
fi
|
||||
|
||||
git -C "$BARE" fetch upstream --quiet 2>/dev/null || true
|
||||
git -C "$BARE" fetch origin --quiet 2>/dev/null || true
|
||||
|
||||
echo "=== lnbits Branch Status ==="
|
||||
echo ""
|
||||
echo "Branch layout (your branches diverge from upstream; sync via merge):"
|
||||
echo " dev = your staging (vendored from upstream/dev)"
|
||||
echo " main = your stable (vendored from upstream/main)"
|
||||
echo ""
|
||||
|
||||
for env in dev main; do
|
||||
local wt="$LNBITS_ROOT/$env"
|
||||
if [[ ! -d "$wt" ]]; then
|
||||
printf " %-6s: (worktree not created)\n" "$env"
|
||||
continue
|
||||
fi
|
||||
local branch behind_u ahead_u
|
||||
branch="$(git -C "$wt" branch --show-current)"
|
||||
case "$env" in
|
||||
dev)
|
||||
behind_u=$(git -C "$BARE" rev-list --count "$branch..upstream/dev" 2>/dev/null || echo ?)
|
||||
ahead_u=$(git -C "$BARE" rev-list --count "upstream/dev..$branch" 2>/dev/null || echo ?)
|
||||
printf " dev: %-3s behind upstream/dev %-3s ahead\n" "$behind_u" "$ahead_u"
|
||||
;;
|
||||
main)
|
||||
behind_u=$(git -C "$BARE" rev-list --count "$branch..upstream/main" 2>/dev/null || echo ?)
|
||||
ahead_u=$(git -C "$BARE" rev-list --count "upstream/main..$branch" 2>/dev/null || echo ?)
|
||||
printf " main: %-3s behind upstream/main %-3s ahead\n" "$behind_u" "$ahead_u"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " lnbits-sync-dev # merge upstream/dev into dev"
|
||||
echo " lnbits-sync-main # merge upstream/main into main worktree"
|
||||
}
|
||||
|
||||
lnbits-sync-dev() {
|
||||
_lnbits_paths
|
||||
local dev_dir="$LNBITS_ROOT/dev"
|
||||
[[ -d "$BARE" ]] || { echo "bare repo missing: $BARE"; return 1; }
|
||||
[[ -d "$dev_dir" ]] || { echo "dev worktree missing: $dev_dir"; return 1; }
|
||||
|
||||
echo "Fetching upstream..."
|
||||
git -C "$BARE" fetch upstream
|
||||
|
||||
echo "Merging upstream/dev into dev..."
|
||||
git -C "$dev_dir" merge upstream/dev
|
||||
|
||||
echo ""
|
||||
echo "Dev synced. Review, then: lb dev && git push"
|
||||
}
|
||||
|
||||
lnbits-sync-main() {
|
||||
_lnbits_paths
|
||||
local main_dir="$LNBITS_ROOT/main"
|
||||
[[ -d "$BARE" ]] || { echo "bare repo missing: $BARE"; return 1; }
|
||||
[[ -d "$main_dir" ]] || { echo "main worktree missing: $main_dir"; return 1; }
|
||||
|
||||
echo "Fetching upstream..."
|
||||
git -C "$BARE" fetch upstream
|
||||
|
||||
echo "Merging upstream/main into main worktree..."
|
||||
git -C "$main_dir" merge upstream/main
|
||||
|
||||
echo ""
|
||||
echo "Main synced. Review, then: lb main && git push"
|
||||
}
|
||||
|
||||
# Aliases keep the short two-letter shortcuts.
|
||||
alias wt='worktree-list'
|
||||
alias wts='worktree-sync'
|
||||
alias wtu='worktree-update-upstream'
|
||||
alias wtn='worktree-spawn'
|
||||
alias lbs='lnbits-status'
|
||||
alias lbsd='lnbits-sync-dev'
|
||||
alias lbsm='lnbits-sync-main'
|
||||
Reference in a new issue