feat(hooks): add jujutsu (jj) version control support
Add detection and hook support for jujutsu repositories: - IsJujutsuRepo(): detects .jj directory - IsColocatedJJGit(): detects colocated jj+git repos - GetJujutsuRoot(): finds jj repo root For colocated repos (jj git init --colocate): - Install simplified hooks without staging (jj auto-commits working copy) - Worktree handling preserved for git worktrees in colocated repos For pure jj repos (no git): - Print alias instructions since jj doesn't have native hooks yet Closes: hq-ew1mbr.12 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
committed by
Steve Yegge
parent
e00f013bda
commit
2fe15e2328
@@ -535,31 +535,53 @@ With --stealth: configures per-repository git settings for invisible beads usage
|
|||||||
// Check if we're in a git repo and hooks aren't installed
|
// Check if we're in a git repo and hooks aren't installed
|
||||||
// Install by default unless --skip-hooks is passed
|
// Install by default unless --skip-hooks is passed
|
||||||
// For Dolt backend, install hooks to .beads/hooks/ (uses git config core.hooksPath)
|
// For Dolt backend, install hooks to .beads/hooks/ (uses git config core.hooksPath)
|
||||||
if !skipHooks && isGitRepo() && !hooksInstalled() {
|
// For jujutsu colocated repos, use simplified hooks (no staging needed)
|
||||||
if backend == configfile.BackendDolt {
|
if !skipHooks && !hooksInstalled() {
|
||||||
// Dolt backend: install hooks to .beads/hooks/
|
isJJ := git.IsJujutsuRepo()
|
||||||
embeddedHooks, err := getEmbeddedHooks()
|
isColocated := git.IsColocatedJJGit()
|
||||||
if err == nil {
|
|
||||||
if err := installHooksWithOptions(embeddedHooks, false, false, false, true); err != nil && !quiet {
|
if isJJ && !isColocated {
|
||||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks to .beads/hooks/: %v\n", ui.RenderWarn("⚠"), err)
|
// Pure jujutsu repo (no git) - print alias instructions
|
||||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd hooks install --beads"))
|
if !quiet {
|
||||||
} else if !quiet {
|
printJJAliasInstructions()
|
||||||
fmt.Printf(" Hooks installed to: .beads/hooks/\n")
|
|
||||||
}
|
|
||||||
} else if !quiet {
|
|
||||||
fmt.Fprintf(os.Stderr, "\n%s Failed to load embedded hooks: %v\n", ui.RenderWarn("⚠"), err)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else if isColocated {
|
||||||
// SQLite backend: use traditional hook installation
|
// Colocated jj+git repo - use simplified hooks
|
||||||
if err := installGitHooks(); err != nil && !quiet {
|
if err := installJJHooks(); err != nil && !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", ui.RenderWarn("⚠"), err)
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install jj hooks: %v\n", ui.RenderWarn("⚠"), err)
|
||||||
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
||||||
|
} else if !quiet {
|
||||||
|
fmt.Printf(" Hooks installed (jujutsu mode - no staging)\n")
|
||||||
|
}
|
||||||
|
} else if isGitRepo() {
|
||||||
|
// Regular git repo
|
||||||
|
if backend == configfile.BackendDolt {
|
||||||
|
// Dolt backend: install hooks to .beads/hooks/
|
||||||
|
embeddedHooks, err := getEmbeddedHooks()
|
||||||
|
if err == nil {
|
||||||
|
if err := installHooksWithOptions(embeddedHooks, false, false, false, true); err != nil && !quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks to .beads/hooks/: %v\n", ui.RenderWarn("⚠"), err)
|
||||||
|
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd hooks install --beads"))
|
||||||
|
} else if !quiet {
|
||||||
|
fmt.Printf(" Hooks installed to: .beads/hooks/\n")
|
||||||
|
}
|
||||||
|
} else if !quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n%s Failed to load embedded hooks: %v\n", ui.RenderWarn("⚠"), err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// SQLite backend: use traditional hook installation
|
||||||
|
if err := installGitHooks(); err != nil && !quiet {
|
||||||
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install git hooks: %v\n", ui.RenderWarn("⚠"), err)
|
||||||
|
fmt.Fprintf(os.Stderr, "You can try again with: %s\n\n", ui.RenderAccent("bd doctor --fix"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're in a git repo and merge driver isn't configured
|
// Check if we're in a git repo and merge driver isn't configured
|
||||||
// Install by default unless --skip-merge-driver is passed
|
// Install by default unless --skip-merge-driver is passed
|
||||||
|
// For colocated jj+git repos, merge driver is still useful
|
||||||
|
// For pure jj repos, skip merge driver (no git)
|
||||||
if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() {
|
if !skipMergeDriver && isGitRepo() && !mergeDriverInstalled() {
|
||||||
if err := installMergeDriver(); err != nil && !quiet {
|
if err := installMergeDriver(); err != nil && !quiet {
|
||||||
fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", ui.RenderWarn("⚠"), err)
|
fmt.Fprintf(os.Stderr, "\n%s Failed to install merge driver: %v\n", ui.RenderWarn("⚠"), err)
|
||||||
|
|||||||
@@ -440,6 +440,199 @@ func mergeDriverInstalled() bool {
|
|||||||
return hasCanonical || hasLegacy
|
return hasCanonical || hasLegacy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// installJJHooks installs simplified git hooks for colocated jujutsu+git repos.
|
||||||
|
// jj's model is simpler: the working copy IS always a commit, so no staging needed.
|
||||||
|
// Changes flow into the current change automatically.
|
||||||
|
func installJJHooks() error {
|
||||||
|
hooksDir, err := git.GetGitHooksDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure hooks directory exists
|
||||||
|
if err := os.MkdirAll(hooksDir, 0750); err != nil {
|
||||||
|
return fmt.Errorf("failed to create hooks directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect existing hooks
|
||||||
|
existingHooks := detectExistingHooks()
|
||||||
|
|
||||||
|
// Check if any non-bd hooks exist
|
||||||
|
hasExistingHooks := false
|
||||||
|
for _, hook := range existingHooks {
|
||||||
|
if hook.exists && !hook.isBdHook {
|
||||||
|
hasExistingHooks = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine installation mode
|
||||||
|
chainHooks := false
|
||||||
|
if hasExistingHooks {
|
||||||
|
choice := promptHookAction(existingHooks)
|
||||||
|
switch choice {
|
||||||
|
case "1", "":
|
||||||
|
chainHooks = true
|
||||||
|
// Chain mode - rename existing hooks to .old so they can be called
|
||||||
|
for _, hook := range existingHooks {
|
||||||
|
if hook.exists && !hook.isBdHook {
|
||||||
|
oldPath := hook.path + ".old"
|
||||||
|
if err := os.Rename(hook.path, oldPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to rename %s to .old: %w", hook.name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Renamed %s to %s\n", hook.name, filepath.Base(oldPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "2":
|
||||||
|
// Overwrite mode - backup existing hooks
|
||||||
|
for _, hook := range existingHooks {
|
||||||
|
if hook.exists && !hook.isBdHook {
|
||||||
|
timestamp := time.Now().Format("20060102-150405")
|
||||||
|
backup := hook.path + ".backup-" + timestamp
|
||||||
|
if err := os.Rename(hook.path, backup); err != nil {
|
||||||
|
return fmt.Errorf("failed to backup %s: %w", hook.name, err)
|
||||||
|
}
|
||||||
|
fmt.Printf(" Backed up %s to %s\n", hook.name, filepath.Base(backup))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "3":
|
||||||
|
fmt.Printf("Skipping git hooks installation.\n")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid choice: %s", choice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-commit hook (simplified for jj - no staging)
|
||||||
|
preCommitPath := filepath.Join(hooksDir, "pre-commit")
|
||||||
|
preCommitContent := buildJJPreCommitHook(chainHooks, existingHooks)
|
||||||
|
|
||||||
|
// post-merge hook (same as git)
|
||||||
|
postMergePath := filepath.Join(hooksDir, "post-merge")
|
||||||
|
postMergeContent := buildPostMergeHook(chainHooks, existingHooks)
|
||||||
|
|
||||||
|
// Write pre-commit hook
|
||||||
|
// #nosec G306 - git hooks must be executable
|
||||||
|
if err := os.WriteFile(preCommitPath, []byte(preCommitContent), 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to write pre-commit hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write post-merge hook
|
||||||
|
// #nosec G306 - git hooks must be executable
|
||||||
|
if err := os.WriteFile(postMergePath, []byte(postMergeContent), 0700); err != nil {
|
||||||
|
return fmt.Errorf("failed to write post-merge hook: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if chainHooks {
|
||||||
|
fmt.Printf("%s Chained bd hooks with existing hooks (jj mode)\n", ui.RenderPass("✓"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildJJPreCommitHook generates the pre-commit hook content for jujutsu repos.
|
||||||
|
// jj's model is simpler: no staging needed, changes flow into the working copy automatically.
|
||||||
|
func buildJJPreCommitHook(chainHooks bool, existingHooks []hookInfo) string {
|
||||||
|
if chainHooks {
|
||||||
|
// Find existing pre-commit hook (already renamed to .old by caller)
|
||||||
|
var existingPreCommit string
|
||||||
|
for _, hook := range existingHooks {
|
||||||
|
if hook.name == "pre-commit" && hook.exists && !hook.isBdHook {
|
||||||
|
existingPreCommit = hook.path + ".old"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `#!/bin/sh
|
||||||
|
#
|
||||||
|
# bd (beads) pre-commit hook (chained, jujutsu mode)
|
||||||
|
#
|
||||||
|
# This hook chains bd functionality with your existing pre-commit hook.
|
||||||
|
# Simplified for jujutsu: no staging needed, jj auto-commits working copy.
|
||||||
|
|
||||||
|
# Run existing hook first
|
||||||
|
if [ -x "` + existingPreCommit + `" ]; then
|
||||||
|
"` + existingPreCommit + `" "$@"
|
||||||
|
EXIT_CODE=$?
|
||||||
|
if [ $EXIT_CODE -ne 0 ]; then
|
||||||
|
exit $EXIT_CODE
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
` + jjPreCommitHookBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
return `#!/bin/sh
|
||||||
|
#
|
||||||
|
# bd (beads) pre-commit hook (jujutsu mode)
|
||||||
|
#
|
||||||
|
# This hook ensures that any pending bd issue changes are flushed to
|
||||||
|
# .beads/issues.jsonl before the commit.
|
||||||
|
#
|
||||||
|
# Simplified for jujutsu: no staging needed, jj auto-commits working copy changes.
|
||||||
|
|
||||||
|
` + jjPreCommitHookBody()
|
||||||
|
}
|
||||||
|
|
||||||
|
// jjPreCommitHookBody returns the pre-commit hook logic for jujutsu repos.
|
||||||
|
// Key difference from git: no git add needed, jj handles working copy automatically.
|
||||||
|
// Still needs worktree handling since colocated jj+git repos can use git worktrees.
|
||||||
|
func jjPreCommitHookBody() string {
|
||||||
|
return `# Check if bd is available
|
||||||
|
if ! command -v bd >/dev/null 2>&1; then
|
||||||
|
echo "Warning: bd command not found, skipping pre-commit flush" >&2
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're in a bd workspace
|
||||||
|
# For worktrees, .beads is in the main repository root, not the worktree
|
||||||
|
BEADS_DIR=""
|
||||||
|
if git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
# Check if we're in a worktree
|
||||||
|
if [ "$(git rev-parse --git-dir)" != "$(git rev-parse --git-common-dir)" ]; then
|
||||||
|
# Worktree: .beads is in main repo root
|
||||||
|
MAIN_REPO_ROOT="$(git rev-parse --git-common-dir)"
|
||||||
|
MAIN_REPO_ROOT="$(dirname "$MAIN_REPO_ROOT")"
|
||||||
|
if [ -d "$MAIN_REPO_ROOT/.beads" ]; then
|
||||||
|
BEADS_DIR="$MAIN_REPO_ROOT/.beads"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Regular repo: check current directory
|
||||||
|
if [ -d .beads ]; then
|
||||||
|
BEADS_DIR=".beads"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$BEADS_DIR" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Flush pending changes to JSONL
|
||||||
|
# In jujutsu, changes automatically become part of the working copy commit
|
||||||
|
if ! bd sync --flush-only >/dev/null 2>&1; then
|
||||||
|
echo "Error: Failed to flush bd changes to JSONL" >&2
|
||||||
|
echo "Run 'bd sync --flush-only' manually to diagnose" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# No git add needed - jujutsu automatically includes working copy changes
|
||||||
|
exit 0
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// printJJAliasInstructions prints setup instructions for pure jujutsu repos.
|
||||||
|
// Since jj doesn't have native hooks yet, users need to set up aliases.
|
||||||
|
func printJJAliasInstructions() {
|
||||||
|
fmt.Printf("\n%s Jujutsu repository detected (not colocated with git)\n\n", ui.RenderWarn("⚠"))
|
||||||
|
fmt.Printf("Jujutsu doesn't support hooks yet. To auto-export beads on push,\n")
|
||||||
|
fmt.Printf("add this alias to your jj config (~/.config/jj/config.toml):\n\n")
|
||||||
|
fmt.Printf(" %s\n", ui.RenderAccent("[aliases]"))
|
||||||
|
fmt.Printf(" %s\n", ui.RenderAccent(`push = ["util", "exec", "--", "sh", "-c", "bd sync --flush-only && jj git push \"$@\"", ""]`))
|
||||||
|
fmt.Printf("\nThen use %s instead of %s\n\n", ui.RenderAccent("jj push"), ui.RenderAccent("jj git push"))
|
||||||
|
fmt.Printf("For more details, see: https://github.com/steveyegge/beads/blob/main/docs/JUJUTSU.md\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
// installMergeDriver configures git to use bd merge for JSONL files
|
// installMergeDriver configures git to use bd merge for JSONL files
|
||||||
// Note: This runs during bd init BEFORE .beads exists, so it runs git in CWD.
|
// Note: This runs during bd init BEFORE .beads exists, so it runs git in CWD.
|
||||||
func installMergeDriver() error {
|
func installMergeDriver() error {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package git
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -271,3 +272,45 @@ func ResetCaches() {
|
|||||||
gitCtxOnce = sync.Once{}
|
gitCtxOnce = sync.Once{}
|
||||||
gitCtx = gitContext{}
|
gitCtx = gitContext{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsJujutsuRepo returns true if the current directory is in a jujutsu (jj) repository.
|
||||||
|
// Jujutsu stores its data in a .jj directory at the repository root.
|
||||||
|
func IsJujutsuRepo() bool {
|
||||||
|
_, err := GetJujutsuRoot()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsColocatedJJGit returns true if this is a colocated jujutsu+git repository.
|
||||||
|
// Colocated repos have both .jj and .git directories, created via `jj git init --colocate`.
|
||||||
|
// In colocated repos, git hooks work normally since jj manages the git repo.
|
||||||
|
func IsColocatedJJGit() bool {
|
||||||
|
if !IsJujutsuRepo() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// If we're also in a git repo, it's colocated
|
||||||
|
_, err := getGitContext()
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJujutsuRoot returns the root directory of the jujutsu repository.
|
||||||
|
// Returns empty string and error if not in a jujutsu repository.
|
||||||
|
func GetJujutsuRoot() (string, error) {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to get current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := cwd
|
||||||
|
for {
|
||||||
|
jjPath := filepath.Join(dir, ".jj")
|
||||||
|
if info, err := os.Stat(jjPath); err == nil && info.IsDir() {
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == dir {
|
||||||
|
return "", fmt.Errorf("not a jujutsu repository (no .jj directory found)")
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
187
internal/git/jujutsu_test.go
Normal file
187
internal/git/jujutsu_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package git
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsJujutsuRepo(t *testing.T) {
|
||||||
|
// Save original directory
|
||||||
|
origDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get current directory: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = os.Chdir(origDir)
|
||||||
|
ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("not a jj repo", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
if IsJujutsuRepo() {
|
||||||
|
t.Error("Expected IsJujutsuRepo() to return false for non-jj directory")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jj repo root", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jjDir := filepath.Join(tmpDir, ".jj")
|
||||||
|
if err := os.Mkdir(jjDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create .jj directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
if !IsJujutsuRepo() {
|
||||||
|
t.Error("Expected IsJujutsuRepo() to return true for jj repo root")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jj repo subdirectory", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jjDir := filepath.Join(tmpDir, ".jj")
|
||||||
|
if err := os.Mkdir(jjDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create .jj directory: %v", err)
|
||||||
|
}
|
||||||
|
subDir := filepath.Join(tmpDir, "src", "lib")
|
||||||
|
if err := os.MkdirAll(subDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(subDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
if !IsJujutsuRepo() {
|
||||||
|
t.Error("Expected IsJujutsuRepo() to return true for jj repo subdirectory")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsColocatedJJGit(t *testing.T) {
|
||||||
|
// Save original directory
|
||||||
|
origDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get current directory: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = os.Chdir(origDir)
|
||||||
|
ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("jj only (not colocated)", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
jjDir := filepath.Join(tmpDir, ".jj")
|
||||||
|
if err := os.Mkdir(jjDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create .jj directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
if IsColocatedJJGit() {
|
||||||
|
t.Error("Expected IsColocatedJJGit() to return false for jj-only repo")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not a repo", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
if IsColocatedJJGit() {
|
||||||
|
t.Error("Expected IsColocatedJJGit() to return false for non-repo")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetJujutsuRoot(t *testing.T) {
|
||||||
|
// Save original directory
|
||||||
|
origDir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to get current directory: %v", err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = os.Chdir(origDir)
|
||||||
|
ResetCaches()
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Run("not a jj repo", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
_, err := GetJujutsuRoot()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected GetJujutsuRoot() to return error for non-jj directory")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jj repo root", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
// Resolve symlinks for comparison (macOS /var -> /private/var)
|
||||||
|
tmpDir, _ = filepath.EvalSymlinks(tmpDir)
|
||||||
|
|
||||||
|
jjDir := filepath.Join(tmpDir, ".jj")
|
||||||
|
if err := os.Mkdir(jjDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create .jj directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(tmpDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
root, err := GetJujutsuRoot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetJujutsuRoot() returned error: %v", err)
|
||||||
|
}
|
||||||
|
if root != tmpDir {
|
||||||
|
t.Errorf("Expected root %q, got %q", tmpDir, root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("jj repo subdirectory", func(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
// Resolve symlinks for comparison (macOS /var -> /private/var)
|
||||||
|
tmpDir, _ = filepath.EvalSymlinks(tmpDir)
|
||||||
|
|
||||||
|
jjDir := filepath.Join(tmpDir, ".jj")
|
||||||
|
if err := os.Mkdir(jjDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create .jj directory: %v", err)
|
||||||
|
}
|
||||||
|
subDir := filepath.Join(tmpDir, "src", "lib")
|
||||||
|
if err := os.MkdirAll(subDir, 0750); err != nil {
|
||||||
|
t.Fatalf("Failed to create subdirectory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(subDir); err != nil {
|
||||||
|
t.Fatalf("Failed to chdir: %v", err)
|
||||||
|
}
|
||||||
|
ResetCaches()
|
||||||
|
|
||||||
|
root, err := GetJujutsuRoot()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetJujutsuRoot() returned error: %v", err)
|
||||||
|
}
|
||||||
|
if root != tmpDir {
|
||||||
|
t.Errorf("Expected root %q, got %q", tmpDir, root)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user