# GitFlow - Shared Functions

> **Usage:** This file contains reusable functions. Reference with `{{REF:section_name}}` in other files.

---

## DETECT_PLATFORM

Detects the current platform (WSL, Windows, macOS, Linux) and shell environment.
Must be called before any path operations.

```bash
detect_platform() {
  # WSL detection (highest priority - WSL reports as Linux but needs path translation)
  if [ -f /proc/version ] && grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then
    GF_PLATFORM="wsl"
    GF_SHELL="bash"
  elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "mingw"* ]] || [[ "$OSTYPE" == "cygwin" ]]; then
    GF_PLATFORM="windows"
    GF_SHELL="gitbash"
  elif [[ -n "$WINDIR" ]] || [[ -n "$SystemRoot" ]]; then
    GF_PLATFORM="windows"
    GF_SHELL="powershell"
  elif [[ "$OSTYPE" == "darwin"* ]]; then
    GF_PLATFORM="macos"
    GF_SHELL="zsh"
  elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
    GF_PLATFORM="linux"
    GF_SHELL="bash"
  else
    GF_PLATFORM="unknown"
    GF_SHELL="bash"
  fi
}
```

---

## NORMALIZE_PATH

Path translation functions for cross-platform compatibility (Windows ↔ WSL ↔ POSIX).

```bash
# Translates a path to the format expected by the CURRENT platform
normalize_path_for_platform() {
  local input_path="$1"
  case "$GF_PLATFORM" in
    wsl)
      # Windows path → WSL path: D:\foo\bar or D:/foo/bar → /mnt/d/foo/bar
      if [[ "$input_path" =~ ^[A-Za-z]:[\\/] ]]; then
        local drive=$(echo "${input_path:0:1}" | tr '[:upper:]' '[:lower:]')
        local rest="${input_path:2}"
        rest="${rest//\\//}"
        echo "/mnt/$drive$rest"
      else
        echo "$input_path"
      fi
      ;;
    *)
      # WSL path → Windows path: /mnt/d/foo → D:/foo
      if [[ "$input_path" =~ ^/mnt/([a-z])/(.*) ]]; then
        local drive=$(echo "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]')
        echo "${drive}:/${BASH_REMATCH[2]}"
      else
        # Normalize backslashes to forward slashes
        echo "${input_path//\\//}"
      fi
      ;;
  esac
}

# Translates a path to storage-neutral format for config.json (forward slashes, drive letter)
# Always converts WSL paths to Windows-style, regardless of current platform
normalize_path_for_storage() {
  local input_path="$1"
  # WSL path → Windows-style: /mnt/d/foo → D:/foo (always, regardless of platform)
  if [[ "$input_path" =~ ^/mnt/([a-z])/(.*) ]]; then
    local drive=$(echo "${BASH_REMATCH[1]}" | tr '[:lower:]' '[:upper:]')
    echo "${drive}:/${BASH_REMATCH[2]}"
  elif [[ "$input_path" =~ ^[A-Za-z]:[\\/] ]]; then
    # Normalize backslashes only
    echo "${input_path//\\//}"
  else
    echo "$input_path"
  fi
}

# Returns current directory in platform-appropriate format
get_current_dir() {
  local raw_pwd
  raw_pwd="$(pwd)"
  normalize_path_for_platform "$raw_pwd"
}
```

---

## DETECT_PROVIDER

Detects GitHub or Azure DevOps from remote URL.

```bash
detect_git_provider() {
  REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "")

  if [[ "$REMOTE_URL" == *"dev.azure.com"* ]] || [[ "$REMOTE_URL" == *"visualstudio.com"* ]]; then
    GIT_PROVIDER="azuredevops"
    [[ "$REMOTE_URL" =~ dev\.azure\.com/([^/]+)/([^/]+)/_git/([^/]+) ]] && {
      AZURE_ORG="${BASH_REMATCH[1]}"
      AZURE_PROJECT="${BASH_REMATCH[2]}"
      AZURE_REPO="${BASH_REMATCH[3]}"
    }
  elif [[ "$REMOTE_URL" == *"github.com"* ]]; then
    GIT_PROVIDER="github"
  else
    GIT_PROVIDER="unknown"
  fi
}
```

---

## NORMALIZE_NAME

Normalizes a branch name (accents, spaces, special characters).

```bash
normalize_branch_name() {
  local input="$1" max_length="${2:-50}"
  echo "$input" | tr '[:upper:]' '[:lower:]' | \
    sed 'y/àâäéèêëïîôùûüçœæ/aaaeeeeiioouucoa/' | \
    sed "s/[ _']/-/g" | sed 's/[^a-z0-9-]//g' | \
    sed 's/--*/-/g' | sed 's/^-//;s/-$//' | cut -c1-$max_length | sed 's/-$//'
}
```

---

## RESOLVE_CONFIG_PATH

Resolves the config.json path regardless of current working directory.
Searches in: pwd, develop worktree, then git root.

```bash
resolve_gitflow_config_path() {
  # Ensure git is accessible (cross-platform worktree repair)
  preflight_git_check 2>/dev/null

  # 1. Try current directory
  [ -f ".claude/gitflow/config.json" ] && { echo ".claude/gitflow/config.json"; return 0; }

  # 2. Try to find develop worktree from git worktree list
  local DEVELOP_PATH
  DEVELOP_PATH=$(git worktree list --porcelain 2>/dev/null | grep -A1 "branch refs/heads/develop" | grep "^worktree " | sed 's/^worktree //')
  [ -n "$DEVELOP_PATH" ] && [ -f "$DEVELOP_PATH/.claude/gitflow/config.json" ] && {
    echo "$DEVELOP_PATH/.claude/gitflow/config.json"
    return 0
  }

  # 3. Try git toplevel
  local GIT_ROOT
  GIT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
  [ -n "$GIT_ROOT" ] && [ -f "$GIT_ROOT/.claude/gitflow/config.json" ] && {
    echo "$GIT_ROOT/.claude/gitflow/config.json"
    return 0
  }

  return 1
}
```

---

## READ_GITFLOW_CONFIG

Reads GitFlow configuration from config.json. Exposes all variables needed by steps and phases.

```bash
read_gitflow_config() {
  # Detect platform first (needed for path translation)
  detect_platform

  local CONFIG_FILE
  CONFIG_FILE=$(resolve_gitflow_config_path) || {
    echo "❌ GitFlow config not found. Run /gitflow init first."
    return 1
  }

  # Config version
  GF_CONFIG_VERSION=$(grep -oP '"version":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Repository
  GF_PROJECT_NAME=$(grep -oP '"name":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_ROOT_FOLDER=$(grep -oP '"rootFolder":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_REMOTE_URL=$(grep -oP '"remoteUrl":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Git provider and branches
  GF_PROVIDER=$(grep -oP '"provider":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_MAIN_BRANCH=$(grep -oP '"main":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_DEVELOP_BRANCH=$(grep -oP '"develop":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Prefixes
  GF_FEATURE_PREFIX=$(grep -oP '"feature":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_RELEASE_PREFIX=$(grep -oP '"release":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_HOTFIX_PREFIX=$(grep -oP '"hotfix":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Worktrees
  GF_WORKTREE_MODE=$(grep -oP '"mode":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_MAIN_PATH=$(grep -A1 '"structure"' "$CONFIG_FILE" | grep -oP '"main":\s*"\K[^"]+' | head -1)
  GF_DEVELOP_PATH=$(grep -A2 '"structure"' "$CONFIG_FILE" | grep -oP '"develop":\s*"\K[^"]+' | head -1)
  GF_FEATURES_PATH=$(grep -oP '"features":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_RELEASES_PATH=$(grep -oP '"releases":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_HOTFIXES_PATH=$(grep -oP '"hotfixes":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Language
  GF_LANG=$(grep -oP '"code":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Versioning
  GF_VERSION=$(grep -oP '"current":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_TAG_PREFIX=$(grep -oP '"tagPrefix":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)

  # Platform (v2.1.0+, optional)
  GF_CONFIG_PLATFORM=$(grep -oP '"detected":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
  GF_CONFIG_PLATFORM=${GF_CONFIG_PLATFORM:-$GF_PLATFORM}

  # Workspace (v2.1.0+, optional)
  GF_WORKSPACE_PATH=$(grep -A1 '"workspace"' "$CONFIG_FILE" | grep -oP '"path":\s*"\K[^"]+' | head -1)
  GF_WORKSPACE_NAME=$(grep -A2 '"workspace"' "$CONFIG_FILE" | grep -oP '"name":\s*"\K[^"]+' | head -1)

  # Defaults for missing values
  GF_LANG=${GF_LANG:-en}
  GF_WORKTREE_MODE=${GF_WORKTREE_MODE:-organized}
  GF_MAIN_BRANCH=${GF_MAIN_BRANCH:-main}
  GF_DEVELOP_BRANCH=${GF_DEVELOP_BRANCH:-develop}
  GF_FEATURE_PREFIX=${GF_FEATURE_PREFIX:-feature/}
  GF_RELEASE_PREFIX=${GF_RELEASE_PREFIX:-release/}
  GF_HOTFIX_PREFIX=${GF_HOTFIX_PREFIX:-hotfix/}
  GF_TAG_PREFIX=${GF_TAG_PREFIX:-v}
  GF_VERSION=${GF_VERSION:-0.0.0}

  # Translate all stored paths to current platform format
  # This ensures WSL paths are /mnt/d/... and Windows paths use forward slashes
  GF_ROOT_FOLDER=$(normalize_path_for_platform "$GF_ROOT_FOLDER")
  GF_MAIN_PATH=$(normalize_path_for_platform "$GF_MAIN_PATH")
  GF_DEVELOP_PATH=$(normalize_path_for_platform "$GF_DEVELOP_PATH")
  GF_FEATURES_PATH=$(normalize_path_for_platform "$GF_FEATURES_PATH")
  GF_RELEASES_PATH=$(normalize_path_for_platform "$GF_RELEASES_PATH")
  GF_HOTFIXES_PATH=$(normalize_path_for_platform "$GF_HOTFIXES_PATH")
  [ -n "$GF_WORKSPACE_PATH" ] && GF_WORKSPACE_PATH=$(normalize_path_for_platform "$GF_WORKSPACE_PATH")

  # Export config file path for steps that need to update it
  GF_CONFIG_FILE="$CONFIG_FILE"
}
```

---

## DETECT_WORKTREE_MODE

Detects worktree mode. Reads config first, falls back to filesystem detection.

```bash
detect_worktree_mode() {
  # 1. Try config first (authoritative)
  local CONFIG_FILE
  CONFIG_FILE=$(resolve_gitflow_config_path 2>/dev/null)
  if [ -n "$CONFIG_FILE" ] && [ -f "$CONFIG_FILE" ]; then
    WORKTREE_MODE=$(grep -oP '"mode":\s*"\K[^"]+' "$CONFIG_FILE" | head -1)
    [ -n "$WORKTREE_MODE" ] && return 0
  fi

  # 2. Fallback: filesystem detection
  if [ -d "../features" ] && [ -d "../releases" ] && [ -d "../hotfixes" ]; then
    WORKTREE_MODE="organized"
  elif [ -d "../01-Main" ] && [ -d "../02-Develop" ]; then
    WORKTREE_MODE="organized"
  elif [ -d "../worktrees" ]; then
    WORKTREE_MODE="adjacent"
  else
    WORKTREE_MODE="organized"
  fi
}
```

---

## CLEANUP_WORKTREE

Cleans up a worktree for a given branch. Uses config paths when available.

```bash
cleanup_worktree_for_branch() {
  local BRANCH=$1 WORKTREE_PATH=""

  # Try config-based paths first (absolute, reliable)
  read_gitflow_config 2>/dev/null
  if [ -n "$GF_FEATURES_PATH" ]; then
    case "$BRANCH" in
      feature/*) WORKTREE_PATH="$GF_FEATURES_PATH/${BRANCH#feature/}" ;;
      release/*) WORKTREE_PATH="$GF_RELEASES_PATH/${BRANCH#release/}" ;;
      hotfix/*)  WORKTREE_PATH="$GF_HOTFIXES_PATH/${BRANCH#hotfix/}" ;;
    esac
  else
    # Fallback: relative paths
    case "$BRANCH" in
      feature/*) WORKTREE_PATH="../features/${BRANCH#feature/}" ;;
      release/*) WORKTREE_PATH="../releases/${BRANCH#release/}" ;;
      hotfix/*)  WORKTREE_PATH="../hotfixes/${BRANCH#hotfix/}" ;;
    esac
  fi

  [ -n "$WORKTREE_PATH" ] && [ -d "$WORKTREE_PATH" ] && {
    git worktree remove "$WORKTREE_PATH" --force 2>/dev/null || rm -rf "$WORKTREE_PATH"
    git worktree prune
    echo "✓ Worktree cleaned: $WORKTREE_PATH"
  }
}
```

---

## VALIDATE_WORKTREE_PATH

Validates that a worktree path follows config conventions. Only `01-Main` and `02-Develop` may use numbered prefixes. All other worktrees (features, releases, hotfixes) MUST use config paths (`features/{name}`, `releases/v{version}`, `hotfixes/{name}`).

```bash
validate_worktree_path() {
  local WORKTREE_PATH="$1"
  local BASENAME=$(basename "$WORKTREE_PATH")

  # Only 01-Main and 02-Develop may use numbered prefixes
  if [[ "$BASENAME" =~ ^[0-9]{2}- ]] && [[ "$BASENAME" != "01-Main" ]] && [[ "$BASENAME" != "02-Develop" ]]; then
    echo "ERROR: Invalid numbered worktree directory: $BASENAME"
    echo "  Path: $WORKTREE_PATH"
    echo "  FORBIDDEN: Numbered directories like 03-Release, 04-Feature, 05-Hotfix"
    echo "  REQUIRED: Use config paths → features/{name}, releases/v{version}, hotfixes/{name}"
    return 1
  fi
  return 0
}
```

---

## REPAIR_WORKTREE_PATHS

Converts worktree metadata paths to **relative paths** for cross-platform compatibility (Windows + WSL).
Absolute paths (Windows `D:/...` or POSIX `/mnt/d/...`) break when switching environments.
Relative paths work everywhere without repair.

Scans `.bare/worktrees/*/gitdir` and the corresponding `{worktree}/.git` files.

```bash
repair_worktree_paths() {
  local BARE_DIR="$1"  # Path to .bare directory
  local WORKTREES_DIR="$BARE_DIR/worktrees"
  [ ! -d "$WORKTREES_DIR" ] && return 0

  local REPAIRED=0
  for wt_dir in "$WORKTREES_DIR"/*/; do
    [ ! -d "$wt_dir" ] && continue
    local wt_name=$(basename "$wt_dir")
    local gitdir_file="$wt_dir/gitdir"
    [ ! -f "$gitdir_file" ] && continue

    # Read stored path (may be Windows absolute, POSIX absolute, or already relative)
    local stored_path=$(cat "$gitdir_file" | tr -d '\n\r')

    # Resolve to current platform absolute path for realpath computation
    local resolved_path="$stored_path"
    local is_abs=false
    if [[ "$stored_path" =~ ^[A-Za-z]:[\\/] ]]; then
      is_abs=true
      resolved_path=$(normalize_path_for_platform "$stored_path")
    elif [[ "$stored_path" =~ ^/ ]]; then
      is_abs=true
      resolved_path=$(normalize_path_for_platform "$stored_path")
    fi
    if [ "$is_abs" = "false" ]; then
      # Already relative - resolve from gitdir_file location to get absolute
      resolved_path=$(realpath -m "$wt_dir/$stored_path")
    fi

    # Get worktree real directory (parent of .git file)
    local wt_real_dir=$(dirname "$resolved_path")
    [ ! -d "$wt_real_dir" ] && continue

    # Compute relative path: .bare/worktrees/{name}/ → worktree/.git
    local bare_wt_abs=$(realpath -m "$wt_dir")
    local wt_abs=$(realpath -m "$wt_real_dir")
    local rel_gitdir=$(realpath -m --relative-to="$bare_wt_abs" "$wt_abs/.git")

    if [ "$stored_path" != "$rel_gitdir" ]; then
      printf '%s\n' "$rel_gitdir" > "$gitdir_file"
      echo "REPAIRED: $wt_name/gitdir → $rel_gitdir"
      REPAIRED=$((REPAIRED + 1))
    fi

    # Fix the worktree's .git pseudo-file → relative path to .bare/worktrees/{name}
    if [ -f "$wt_real_dir/.git" ]; then
      local git_content=$(cat "$wt_real_dir/.git" | tr -d '\n\r')
      local git_path="${git_content#gitdir: }"
      local rel_bare=$(realpath -m --relative-to="$wt_abs" "$bare_wt_abs")

      if [ "$git_path" != "$rel_bare" ]; then
        printf 'gitdir: %s\n' "$rel_bare" > "$wt_real_dir/.git"
        echo "REPAIRED: $wt_name/.git → $rel_bare"
        REPAIRED=$((REPAIRED + 1))
      fi
    fi
  done

  [ "$REPAIRED" -gt 0 ] && echo "Worktree paths: $REPAIRED file(s) repaired"
}
```

---

## PREFLIGHT_GIT_CHECK

Lightweight check to ensure git is accessible in the current worktree. If git fails (cross-platform path issue), locates `.bare` and calls `repair_worktree_paths`. Must be called before any git operation in agents that may run in worktrees created from a different environment (Windows vs WSL).

```bash
preflight_git_check() {
  # Quick test: if git works, nothing to do
  git rev-parse --git-dir >/dev/null 2>&1 && return 0

  echo "Git inaccessible — attempting worktree path repair..."

  # Find .bare directory by walking up
  local BARE_DIR=""
  local DIR="$(pwd)"
  while [ "$DIR" != "/" ]; do
    [ -d "$DIR/.bare" ] && { BARE_DIR="$DIR/.bare"; break; }
    DIR=$(dirname "$DIR")
  done

  if [ -z "$BARE_DIR" ]; then
    echo "ERROR: No .bare directory found. Not a GitFlow worktree."
    return 1
  fi

  # Ensure platform is detected
  [ -z "$GF_PLATFORM" ] && detect_platform

  # Repair all worktree paths to relative format
  repair_worktree_paths "$BARE_DIR"

  # Verify repair
  if git rev-parse --git-dir >/dev/null 2>&1; then
    echo "Worktree paths repaired successfully."
    return 0
  else
    echo "ERROR: Worktree repair failed. Run /gitflow init to reconfigure."
    return 1
  fi
}
```

---

## RESOLVE_WORKSPACE

Resolves the workspace JSON file by walking up the directory tree.

```bash
resolve_workspace_path() {
  local dir="$(pwd)"
  while [ "$dir" != "/" ] && [ "$dir" != "." ]; do
    [ -f "$dir/.smartstack-workspace.json" ] && {
      echo "$dir/.smartstack-workspace.json"
      return 0
    }
    dir=$(dirname "$dir")
  done
  return 1
}
```

---

## SYNC_REMOTE

Syncs with remote and displays status.

```bash
sync_with_remote() {
  git fetch --all --quiet
  local BRANCH=$(git branch --show-current)
  local AHEAD=$(git rev-list --count origin/$BRANCH..HEAD 2>/dev/null || echo "0")
  local BEHIND=$(git rev-list --count HEAD..origin/$BRANCH 2>/dev/null || echo "0")

  [ "$BEHIND" -gt 0 ] && echo "⚠️ $BRANCH: $BEHIND commit(s) behind"
  [ "$AHEAD" -gt 0 ] && echo "ℹ️ $BRANCH: $AHEAD commit(s) not pushed"
}
```

---

## CONFIG_TEMPLATE

GitFlow configuration JSON template v2.1.0 (aligned with `templates/config.json`).

```json
{
  "version": "2.1.0",
  "platform": {
    "detected": "",
    "shell": "",
    "detectedAt": ""
  },
  "workspace": {
    "path": "",
    "name": ""
  },
  "repository": {
    "name": "",
    "rootFolder": "",
    "nameVariants": {
      "pascalCaseDot": "",
      "pascalCase": "",
      "kebabCase": "",
      "snakeCase": "",
      "displayName": ""
    },
    "defaultBranch": "main",
    "remoteUrl": ""
  },
  "git": {
    "provider": "",
    "branches": {
      "main": "main",
      "develop": "develop"
    },
    "prefixes": {
      "feature": "feature/",
      "release": "release/",
      "hotfix": "hotfix/"
    }
  },
  "worktrees": {
    "enabled": true,
    "mode": "organized",
    "structure": {
      "main": "",
      "develop": "",
      "features": "",
      "releases": "",
      "hotfixes": ""
    }
  },
  "versioning": {
    "strategy": "semver",
    "current": "0.0.0",
    "tagPrefix": "v",
    "sources": ["csproj", "package.json", "VERSION"]
  },
  "efcore": {
    "enabled": true,
    "validateOnCommit": true,
    "blockDestructive": true,
    "migrationNaming": "{context}_v{version}_{sequence}_{Description}",
    "migrationNamingSquash": "{context}_v{version}",
    "squashBeforePR": true
  },
  "workflow": {
    "push": {
      "afterCommit": "worktree"
    },
    "pr": {
      "autoLabels": true,
      "requireReview": true
    }
  },
  "language": {
    "code": "en"
  }
}
```

**worktrees.mode options:**
- `organized`: Uses absolute paths from config (`structure.features`, etc.)
- `adjacent`: `{rootFolder}/worktrees/{type}/`
- `disabled`: No worktrees

---

## PR_COMMANDS

PR commands by provider.

### GitHub
```bash
gh pr create --title "$TITLE" --body "$BODY" --base "$BASE"
gh pr view $NUMBER --json state,mergeable,reviews
gh pr merge $NUMBER --squash --delete-branch
```

### Azure DevOps
```bash
az repos pr create --title "$TITLE" --description "$BODY" --source-branch "$SOURCE" --target-branch "$BASE"
az repos pr show --id $NUMBER --query "{status:status,mergeStatus:mergeStatus}"
az repos pr update --id $NUMBER --status completed --squash true --delete-source-branch true
```

---

## OUTPUT_FORMAT

Standard output format (replaces ASCII art).

```bash
output_header() { echo -e "\n## $1\n"; }
output_section() { echo "### $1"; }
output_item() { echo "- $1"; }
output_success() { echo "✓ $1"; }
output_warning() { echo "⚠️ $1"; }
output_error() { echo "❌ $1"; }
output_next() { echo "→ Next: $1"; }
```
