From 51c0b4d568da543a77c62102069193e9700174bf Mon Sep 17 00:00:00 2001 From: Maxim Geraskin Date: Wed, 25 Feb 2026 18:31:05 +0100 Subject: [PATCH] uspecs 0.1.0-a0, 2026-02-25T17:19:55Z --- uspecs/u/actn-uarchive.md | 4 +- uspecs/u/actn-upr.md | 12 ++-- uspecs/u/scripts/_lib/pr.sh | 58 ++++++++----------- uspecs/u/scripts/_lib/utils.sh | 63 +++++++++++++++++++- uspecs/u/scripts/conf.sh | 102 +++++++++++++++++++++------------ uspecs/u/scripts/uspecs.sh | 21 ++++--- uspecs/u/uspecs.yml | 6 +- 7 files changed, 171 insertions(+), 95 deletions(-) diff --git a/uspecs/u/actn-uarchive.md b/uspecs/u/actn-uarchive.md index 71ebb92..e0bb75b 100644 --- a/uspecs/u/actn-uarchive.md +++ b/uspecs/u/actn-uarchive.md @@ -16,7 +16,7 @@ Parameters: - Active Change Folder path - Output - Folder moved to `{changes_folder}/archive` - - If on PR branch and Engineer confirms: git commit and push with message, branch and refs removed + - If on PR branch and Engineer confirms: git commit and push with message, branch and refs removed, deleted branch hash and restore instructions reported Flow: @@ -26,7 +26,7 @@ Flow: 1. Archive + git cleanup (commit, push, delete local branch and remote tracking ref) 2. Archive only (no git operations) 3. Cancel - - On option 1: `bash uspecs/u/scripts/uspecs.sh change archive -d` + - On option 1: `bash uspecs/u/scripts/uspecs.sh change archive -d`; script output includes deleted branch hash and restore instructions - relay to Engineer - On option 2: `bash uspecs/u/scripts/uspecs.sh change archive ` - On option 3: abort, no action taken - Otherwise: `bash uspecs/u/scripts/uspecs.sh change archive ` diff --git a/uspecs/u/actn-upr.md b/uspecs/u/actn-upr.md index 623aaea..640256f 100644 --- a/uspecs/u/actn-upr.md +++ b/uspecs/u/actn-upr.md @@ -24,19 +24,15 @@ Parameters: Flow: -- Validate preconditions: - - Run `bash uspecs/u/scripts/uspecs.sh pr mergedef --validate` +- Merge default branch into change_branch: + - Run `bash uspecs/u/scripts/uspecs.sh pr mergedef` - If script exits with error: report the error and stop - - Parse `change_branch` and `default_branch` from script output + - Parse `change_branch`, `default_branch`, and `change_branch_head` from script output - Read Active Change Folder (change.md) to determine `issue_url` (may be absent) and derive `issue_id` from the URL (last path segment) - Present Engineer with the following options: 1. Create PR (squash-merge `change_branch` into `{change_branch}--pr`, delete `change_branch`, create PR on GitHub) 2. Cancel - On option 2: stop -- Merge default branch into change_branch: - - Run `bash uspecs/u/scripts/uspecs.sh pr mergedef` - - If script exits with error: report the error and stop - - If merge fails (conflicts): report error and ask Engineer to resolve conflicts and re-run - Get specs diff to derive PR title and body: - Run `bash uspecs/u/scripts/uspecs.sh diff specs` - From the diff output identify `draft_title` and `draft_body`; construct `pr_title` and `pr_body` per `{templates_folder}/tmpl-pr.md` @@ -45,4 +41,4 @@ Flow: - Note: `pr_title` is passed on the command line; ensure it contains no shell-special characters (`<`, `>`, `$`, backticks) - If script exits with error: report the error and stop - Parse `pr_url` from script output -- Report `pr_url` to Engineer; inform that Engineer is now on `pr_branch` to address review comments +- Report `pr_url` and `change_branch_head` to Engineer; inform that Engineer is now on `pr_branch` to address review comments and that the deleted change branch can be restored with `git branch {change_branch} {change_branch_head}` diff --git a/uspecs/u/scripts/_lib/pr.sh b/uspecs/u/scripts/_lib/pr.sh index 1725378..e2fc042 100644 --- a/uspecs/u/scripts/_lib/pr.sh +++ b/uspecs/u/scripts/_lib/pr.sh @@ -22,7 +22,11 @@ set -Eeuo pipefail # Fetch pr_remote and create a local branch from its default branch. # # pr.sh mergedef -# Fetch pr_remote and merge pr_remote/default_branch into the current branch. +# Validate preconditions, fetch pr_remote/default_branch, and merge it into the current branch. +# On success outputs: +# change_branch= +# default_branch= +# change_branch_head= (HEAD before the merge) # # pr.sh diff specs # Output git diff of the specs folder between HEAD and pr_remote/default_branch. @@ -44,10 +48,11 @@ set -Eeuo pipefail # If no changes exist, switch to --next-branch and exit cleanly. # # pr.sh ffdefault -# Fetch pr_remote/default and fast-forward current branch to it +# Fetch pr_remote/default_branch and fast-forward the local default branch to it. +# Switches to the default branch if not already on it, and leaves there after completion. # Fail fast if any of the following conditions are true: -# current branch is not default -# current branch is not clean +# working directory is not clean +# branches have diverged (fast-forward not possible) @@ -75,7 +80,7 @@ read_conf_param() { fi local line raw - line=$(grep -E "^- ${param_name}:" "$conf_file" | head -1 || true) + line=$(_grep -E "^- ${param_name}:" "$conf_file" | head -1 || true) raw="${line#*: }" if [ -z "$raw" ]; then @@ -93,7 +98,7 @@ error() { } determine_pr_remote() { - if git remote | grep -q '^upstream$'; then + if git remote | _grep -q '^upstream$'; then echo "upstream" else echo "origin" @@ -120,17 +125,8 @@ gh_create_pr() { check_prerequisites() { # Check if git repository exists - local dir="$PWD" - local found_git=false - while [[ "$dir" != "/" ]]; do - if [[ -d "$dir/.git" ]]; then - found_git=true - break - fi - dir=$(dirname "$dir") - done - if [[ "$found_git" == "false" ]]; then - error "No git repository found" + if ! is_git_repo "$PWD"; then + error "No git repository found at $PWD" fi # Check if GitHub CLI is installed @@ -139,7 +135,7 @@ check_prerequisites() { fi # Check if origin remote exists - if ! git remote | grep -q '^origin$'; then + if ! git remote | _grep -q '^origin$'; then error "'origin' remote does not exist" fi @@ -198,15 +194,16 @@ cmd_ffdefault() { current_branch=$(git symbolic-ref --short HEAD) if [[ "$current_branch" != "$default_branch" ]]; then - error "Current branch '$current_branch' is not the default branch '$default_branch'" + echo "Switching to '$default_branch'..." + git checkout "$default_branch" fi echo "Fetching $pr_remote/$default_branch..." git fetch "$pr_remote" "$default_branch" 2>&1 - echo "Fast-forwarding $current_branch..." + echo "Fast-forwarding $default_branch..." if ! git merge --ff-only "$pr_remote/$default_branch" 2>&1; then - error "Cannot fast-forward '$current_branch' to '$pr_remote/$default_branch'. The branches have diverged." + error "Cannot fast-forward '$default_branch' to '$pr_remote/$default_branch'. The branches have diverged." fi } @@ -275,14 +272,6 @@ cmd_pr() { } cmd_mergedef() { - local validate_only=false - while [[ $# -gt 0 ]]; do - case "$1" in - --validate) validate_only=true; shift ;; - *) error "Unknown flag: $1" ;; - esac - done - check_prerequisites local pr_remote default_branch current_branch @@ -298,17 +287,18 @@ cmd_mergedef() { error "Current branch '$current_branch' ends with '--pr'; cannot create PR from a PR branch" fi - if [[ "$validate_only" == "true" ]]; then - echo "change_branch=$current_branch" - echo "default_branch=$default_branch" - return 0 - fi + local change_branch_head + change_branch_head=$(git rev-parse HEAD) echo "Fetching $pr_remote/$default_branch..." git fetch "$pr_remote" "$default_branch" 2>&1 echo "Merging $pr_remote/$default_branch into $current_branch..." git merge "$pr_remote/$default_branch" 2>&1 + + echo "change_branch=$current_branch" + echo "default_branch=$default_branch" + echo "change_branch_head=$change_branch_head" } cmd_diff() { diff --git a/uspecs/u/scripts/_lib/utils.sh b/uspecs/u/scripts/_lib/utils.sh index 805c93c..4807f03 100644 --- a/uspecs/u/scripts/_lib/utils.sh +++ b/uspecs/u/scripts/_lib/utils.sh @@ -14,15 +14,17 @@ checkcmds() { done } -# get_pr_info +# get_pr_info [project_dir] # Calls pr.sh info and parses the key=value output into the given associative array. # Keys populated: pr_remote, default_branch +# project_dir: directory to run pr.sh from (defaults to $PWD) # Returns non-zero if pr.sh info fails. get_pr_info() { local pr_sh="$1" local -n _pr_info_map="$2" + local project_dir="${3:-$PWD}" local output - output=$(bash "$pr_sh" info) || return 1 + output=$(cd "$project_dir" && bash "$pr_sh" info) || return 1 while IFS='=' read -r key value; do [[ -z "$key" ]] && continue _pr_info_map["$key"]="$value" @@ -35,6 +37,63 @@ is_tty() { [ -t 0 ] } +# is_git_repo +# Returns 0 if is inside a git repository, 1 otherwise. +is_git_repo() { + local dir="$1" + (cd "$dir" && git rev-parse --git-dir > /dev/null 2>&1) +} + +# _GREP_BIN caches the resolved grep binary path for _grep. +_GREP_BIN="" + +# _grep [grep-args...] +# Portable grep wrapper. On Windows (msys/cygwin) resolves grep from the git +# installation and fails fast if not found. On other platforms uses system grep. +_grep() { + if [[ -z "$_GREP_BIN" ]]; then + case "$OSTYPE" in + msys*|cygwin*) + # Use where.exe to get real Windows paths, then pick the grep + # that lives inside the Git for Windows installation. + local git_path git_root candidate + git_path=$(where.exe git 2>/dev/null | head -1 | tr -d $'\r' | tr $'\\\\' / || true) + if [[ -z "$git_path" ]]; then + echo "Error: git not found; cannot locate git's bundled grep" >&2 + exit 1 + fi + git_root=$(dirname "$(dirname "$git_path")") + # Try direct path first (works even if grep is not on PATH). + # Also try one level up to handle mingw64/bin/git.exe layout where + # two dirnames give .../mingw64 instead of the git installation root. + if [[ -x "$git_root/usr/bin/grep.exe" ]]; then + _GREP_BIN="$git_root/usr/bin/grep.exe" + elif [[ -x "$(dirname "$git_root")/usr/bin/grep.exe" ]]; then + git_root=$(dirname "$git_root") + _GREP_BIN="$git_root/usr/bin/grep.exe" + else + # Fall back to where.exe grep, pick the one under git root + while IFS= read -r candidate; do + candidate=$(echo "$candidate" | tr -d $'\r' | tr $'\\\\' /) + if [[ "$candidate" == "$git_root/"* ]]; then + _GREP_BIN="$candidate" + break + fi + done < <(where.exe grep 2>/dev/null || true) + fi + if [[ -z "$_GREP_BIN" ]]; then + echo "Error: grep not found under git root: $git_root" >&2 + exit 1 + fi + ;; + *) + _GREP_BIN="grep" + ;; + esac + fi + "$_GREP_BIN" "$@" +} + # sed_inplace file sed-args... # Portable in-place sed. Uses -i.bak for BSD compatibility. # Restores the original file on failure. diff --git a/uspecs/u/scripts/conf.sh b/uspecs/u/scripts/conf.sh index d867c41..b9a3bdf 100644 --- a/uspecs/u/scripts/conf.sh +++ b/uspecs/u/scripts/conf.sh @@ -47,6 +47,13 @@ get_timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ" } +native_path() { + case "$OSTYPE" in + msys*|cygwin*) cygpath -m "$1" ;; + *) echo "$1" ;; + esac +} + get_project_dir() { local script_path="${BASH_SOURCE[0]}" if [[ -z "$script_path" || ! -f "$script_path" ]]; then @@ -57,7 +64,7 @@ get_project_dir() { # Go up 3 levels: scripts -> u -> uspecs -> project_dir local project_dir project_dir=$(cd "$script_dir/../../.." && pwd) - echo "$project_dir" + native_path "$project_dir" } check_not_installed() { @@ -98,7 +105,7 @@ load_config() { get_latest_tag() { curl -fsSL "$GITHUB_API/repos/$REPO_OWNER/$REPO_NAME/tags" | \ - grep '"name":' | \ + _grep '"name":' | \ sed 's/.*"name": *"v\?\([^"]*\)".*/\1/' | \ head -n 1 } @@ -110,9 +117,9 @@ get_latest_minor_tag() { local result result=$(curl -fsSL "$GITHUB_API/repos/$REPO_OWNER/$REPO_NAME/tags" | \ - grep '"name":' | \ + _grep '"name":' | \ sed 's/.*"name": *"v\?\([^"]*\)".*/\1/' | \ - grep "^$major\.$minor\." | \ + _grep "^$major\.$minor\." | \ head -n 1 || true) echo "${result:-$current_version}" } @@ -125,9 +132,9 @@ get_latest_commit_info() { local response response=$(curl -fsSL "$GITHUB_API/repos/$REPO_OWNER/$REPO_NAME/commits/$ALPHA_BRANCH") local sha - sha=$(echo "$response" | grep '"sha":' | head -n 1 | sed 's/.*"sha": *"\([^"]*\)".*/\1/') + sha=$(echo "$response" | _grep '"sha":' | head -n 1 | sed 's/.*"sha": *"\([^"]*\)".*/\1/') local commit_date - commit_date=$(echo "$response" | grep '"date":' | head -n 1 | sed 's/.*"date": *"\([^"]*\)".*/\1/') + commit_date=$(echo "$response" | _grep '"date":' | head -n 1 | sed 's/.*"date": *"\([^"]*\)".*/\1/') echo "$sha $commit_date" } @@ -301,10 +308,10 @@ show_operation_plan() { local -A pr_info local pr_remote="" default_branch="" target_repo_url="" pr_branch="" - if get_pr_info "$script_dir/_lib/pr.sh" pr_info 2>/dev/null; then + if get_pr_info "$script_dir/_lib/pr.sh" pr_info "$project_dir" 2>/dev/null; then pr_remote="${pr_info[pr_remote]:-}" default_branch="${pr_info[default_branch]:-}" - target_repo_url=$(git remote get-url "$pr_remote" 2>/dev/null) + target_repo_url=$(git -C "$project_dir" remote get-url "$pr_remote" 2>/dev/null) # Use branch-safe version string for PR branch name local version_branch @@ -367,7 +374,7 @@ has_markers() { local file="$1" local begin_marker="$2" local end_marker="$3" - grep -q "$begin_marker" "$file" && grep -q "$end_marker" "$file" + _grep -q "$begin_marker" "$file" && _grep -q "$end_marker" "$file" } inject_instructions() { @@ -473,6 +480,10 @@ write_metadata() { resolve_update_version() { local current_version="$1" local project_dir="$2" + local -n _ruv_target_version="$3" + local -n _ruv_target_ref="$4" + local -n _ruv_commit="$5" + local -n _ruv_commit_timestamp="$6" if is_alpha_version "$current_version"; then echo "Checking for alpha updates..." @@ -480,21 +491,24 @@ resolve_update_version() { load_config "$project_dir" config local current_commit="${config[commit]:-}" local current_commit_timestamp="${config[commit_timestamp]:-}" - read -r commit commit_timestamp <<< "$(get_latest_commit_info)" + local fetched_commit fetched_timestamp + read -r fetched_commit fetched_timestamp <<< "$(get_latest_commit_info)" - if [[ "$current_commit" == "$commit" ]]; then + if [[ "$current_commit" == "$fetched_commit" ]]; then echo "Already on the latest alpha version: $current_version" - echo " Commit: $commit" + echo " Commit: $fetched_commit" echo " Timestamp: $current_commit_timestamp" return 1 fi - target_version=$(get_alpha_version) - target_ref="$commit" + _ruv_target_version=$(get_alpha_version) + _ruv_target_ref="$fetched_commit" + _ruv_commit="$fetched_commit" + _ruv_commit_timestamp="$fetched_timestamp" else echo "Checking for stable updates..." - target_version=$(get_latest_minor_tag "$current_version") + _ruv_target_version=$(get_latest_minor_tag "$current_version") - if [[ "$target_version" == "$current_version" ]]; then + if [[ "$_ruv_target_version" == "$current_version" ]]; then echo "Already on the latest stable minor version: $current_version" local latest_major @@ -507,7 +521,7 @@ resolve_update_version() { return 1 fi - target_ref="v$target_version" + _ruv_target_ref="v$_ruv_target_version" fi return 0 } @@ -515,20 +529,22 @@ resolve_update_version() { resolve_upgrade_version() { local current_version="$1" local project_dir="$2" + local -n _rugv_target_version="$3" + local -n _rugv_target_ref="$4" if is_alpha_version "$current_version"; then error "Only applicable for stable versions. Alpha versions always track the latest commit from $ALPHA_BRANCH branch, use update instead" fi echo "Checking for major upgrades..." - target_version=$(get_latest_major_tag) + _rugv_target_version=$(get_latest_major_tag) - if [[ "$target_version" == "$current_version" ]]; then + if [[ "$_rugv_target_version" == "$current_version" ]]; then echo "Already on the latest major version: $current_version" return 1 fi - target_ref="v$target_version" + _rugv_target_ref="v$_rugv_target_version" return 0 } @@ -547,7 +563,7 @@ cmd_apply() { while [[ $# -gt 0 ]]; do case "$1" in - --project-dir) project_dir="$2"; shift 2 ;; + --project-dir) project_dir=$(native_path "$2"); shift 2 ;; --version) version="$2"; shift 2 ;; --commit) commit="$2"; shift 2 ;; --commit-timestamp) commit_timestamp="$2"; shift 2 ;; @@ -572,6 +588,7 @@ cmd_apply() { local version_string version_string=$(format_version_string "$version" "$commit" "$commit_timestamp") + # Safe version to create branches local version_string_branch version_string_branch=$(format_version_string_branch "$version" "$commit" "$commit_timestamp") @@ -581,9 +598,11 @@ cmd_apply() { error "uspecs is already installed, use update instead" fi - # PR: fast-forward default branch (may update local uspecs.yml) + # PR: remember current branch, fast-forward default branch (may update local uspecs.yml) + local prev_branch="" if [[ "$pr_flag" == "true" ]]; then - bash "$script_dir/_lib/pr.sh" ffdefault + prev_branch=$(git -C "$project_dir" symbolic-ref --short HEAD) + (cd "$project_dir" && bash "$script_dir/_lib/pr.sh" ffdefault) fi local -A config @@ -596,6 +615,13 @@ cmd_apply() { if [[ "${config[version]:-}" != "$current_version" ]]; then error "Installed version '${config[version]:-}' does not match expected '$current_version'. Re-run the command to pick up the current installed version." fi + # After ffdefault the local uspecs.yml may already reflect the incoming version + if [[ -n "$commit" && "${config[commit]:-}" == "$commit" ]] || \ + [[ -z "$commit" && "${config[version]:-}" == "$version" ]]; then + echo "Already up to date" + [[ -n "$prev_branch" ]] && git -C "$project_dir" checkout "$prev_branch" + return 0 + fi fi # Determine invocation methods string for plan display @@ -608,14 +634,15 @@ cmd_apply() { # Show operation plan and confirm show_operation_plan "$command_name" "$current_version" "$version" "$commit" "$commit_timestamp" "$plan_invocation_methods_str" "$pr_flag" "$project_dir" "$script_dir" - confirm_action "$command_name" || return 0 + if ! confirm_action "$command_name"; then + [[ -n "$prev_branch" ]] && git -C "$project_dir" checkout "$prev_branch" + return 0 + fi - # PR: capture current branch, then create feature branch + # PR: create feature branch from default branch local branch_name="${command_name}-uspecs-${version_string_branch}" - local prev_branch="" if [[ "$pr_flag" == "true" ]]; then - prev_branch=$(git symbolic-ref --short HEAD) - bash "$script_dir/_lib/pr.sh" prbranch "$branch_name" + (cd "$project_dir" && bash "$script_dir/_lib/pr.sh" prbranch "$branch_name") fi # Save existing metadata for update/upgrade @@ -662,13 +689,13 @@ cmd_apply() { pr_info_file=$(create_temp_file) # Capture PR info from stderr while showing normal output - bash "$script_dir/_lib/pr.sh" pr --title "$pr_title" --body "$pr_body" \ - --next-branch "$prev_branch" --delete-branch 2> "$pr_info_file" + (cd "$project_dir" && bash "$script_dir/_lib/pr.sh" pr --title "$pr_title" --body "$pr_body" \ + --next-branch "$prev_branch" --delete-branch) 2> "$pr_info_file" # Parse PR info from temp file - pr_url=$(grep '^PR_URL=' "$pr_info_file" | cut -d= -f2-) - pr_branch=$(grep '^PR_BRANCH=' "$pr_info_file" | cut -d= -f2) - pr_base=$(grep '^PR_BASE=' "$pr_info_file" | cut -d= -f2) + pr_url=$(_grep '^PR_URL=' "$pr_info_file" | cut -d= -f2-) + pr_branch=$(_grep '^PR_BRANCH=' "$pr_info_file" | cut -d= -f2) + pr_base=$(_grep '^PR_BASE=' "$pr_info_file" | cut -d= -f2) fi echo "" @@ -705,7 +732,8 @@ cmd_install() { error "At least one invocation method (--nlia or --nlic) is required" fi - local project_dir="$PWD" + local project_dir + project_dir=$PWD check_not_installed "$project_dir" @@ -771,11 +799,11 @@ cmd_update_or_upgrade() { load_config "$project_dir" config local current_version="${config[version]:-}" - local target_version target_ref commit commit_timestamp + local target_version="" target_ref="" commit="" commit_timestamp="" if [[ "$command_name" == "update" ]]; then - resolve_update_version "$current_version" "$project_dir" || return 0 + resolve_update_version "$current_version" "$project_dir" target_version target_ref commit commit_timestamp || return 0 else - resolve_upgrade_version "$current_version" "$project_dir" || return 0 + resolve_upgrade_version "$current_version" "$project_dir" target_version target_ref || return 0 fi local temp_dir diff --git a/uspecs/u/scripts/uspecs.sh b/uspecs/u/scripts/uspecs.sh index 1b6b935..0b6797a 100644 --- a/uspecs/u/scripts/uspecs.sh +++ b/uspecs/u/scripts/uspecs.sh @@ -27,7 +27,8 @@ set -Eeuo pipefail # Requires git repository, clean working tree, PR branch (ending with --pr) # # pr mergedef: -# Validates preconditions and merges pr_remote/default_branch into the current branch. +# Validates preconditions, fetches pr_remote/default_branch, and merges it into the current branch. +# On success outputs: change_branch=, default_branch=, change_branch_head= # # pr create --title --body <body>: # Creates a PR from the current change branch (delegates to _lib/pr.sh changepr). @@ -45,11 +46,6 @@ get_timestamp() { date -u +"%Y-%m-%dT%H:%M:%SZ" } -is_git_repo() { - local dir="$1" - (cd "$dir" && git rev-parse --git-dir > /dev/null 2>&1) -} - get_baseline() { local project_dir="$1" if is_git_repo "$project_dir"; then @@ -67,7 +63,7 @@ get_folder_name() { count_uncompleted_items() { local folder="$1" local count - count=$(grep -r "^\s*-\s*\[ \]" "$folder"/*.md 2>/dev/null | wc -l) + count=$(_grep -r "^[[:space:]]*-[[:space:]]*\[ \]" "$folder"/*.md 2>/dev/null | wc -l) echo "${count:-0}" | tr -d ' ' } @@ -131,7 +127,7 @@ read_conf_param() { fi local line raw - line=$(grep -E "^- ${param_name}:" "$conf_file" | head -1 || true) + line=$(_grep -E "^- ${param_name}:" "$conf_file" | head -1 || true) raw="${line#*: }" if [ -z "$raw" ]; then @@ -346,7 +342,7 @@ cmd_change_archive() { echo "Cannot archive: $uncompleted_count uncompleted todo item(s) found" echo "" echo "Uncompleted items:" - grep -rn "^\s*-\s*\[ \]" "$path_to_change_folder"/*.md 2>/dev/null | sed 's/^/ /' + _grep -rn "^[[:space:]]*-[[:space:]]*\[ \]" "$path_to_change_folder"/*.md 2>/dev/null | sed 's/^/ /' echo "" echo "Complete or cancel todo items before archiving" exit 1 @@ -466,12 +462,19 @@ cmd_change_archive() { (cd "$project_dir" && git checkout "$default_branch" 2>&1) + local deleted_branch_hash="" if (cd "$project_dir" && git show-ref --verify --quiet "refs/heads/$branch_name"); then + deleted_branch_hash=$(cd "$project_dir" && git rev-parse "refs/heads/$branch_name") (cd "$project_dir" && git branch -D "$branch_name" 2>&1) else echo "Warning: branch '$branch_name' not found, skipping branch deletion" >&2 fi (cd "$project_dir" && git branch -dr "origin/$branch_name") 2>/dev/null || true + + if [ -n "$deleted_branch_hash" ]; then + echo "Deleted branch: $branch_name ($deleted_branch_hash)" + echo "To restore: git branch $branch_name $deleted_branch_hash" + fi fi echo "Archived change: $changes_folder_rel/archive/$yymm_prefix/${date_prefix}-${change_name}" diff --git a/uspecs/u/uspecs.yml b/uspecs/u/uspecs.yml index b487687..0d45d8a 100644 --- a/uspecs/u/uspecs.yml +++ b/uspecs/u/uspecs.yml @@ -3,6 +3,6 @@ version: 0.1.0-a0 invocation_methods: [nlia] installed_at: 2026-02-21T18:48:11Z -modified_at: 2026-02-24T14:03:05Z -commit: c4732720f507fa263f7f9623ab52175639607a35 -commit_timestamp: 2026-02-24T14:02:04Z +modified_at: 2026-02-25T17:31:04Z +commit: 5390f2e8c7fe1b4d9e4e73120a623ab38f846ec6 +commit_timestamp: 2026-02-25T17:19:55Z