Commit 3038f7a

bryfry <bryon@fryer.io>
2025-09-27 22:52:25
cleanup and finish 002
1 parent 440e152
Changed files (3)
cmd/init.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 	"os"
 	"os/exec"
+	"strings"
 	"time"
 
 	"github.com/go-git/go-billy/v5/osfs"
@@ -23,6 +24,7 @@ func NewInitCmd() *cobra.Command {
 	var issuesDir string
 	var issuesBranch string
 	var remoteName string
+	var forceLocal bool
 
 	var cmd = &cobra.Command{
 		Use:   "init",
@@ -30,7 +32,7 @@ func NewInitCmd() *cobra.Command {
 		Long: `Initialize tissue issue tracking by creating an orphan 'issues' branch
 with proper structure and setting up a git worktree for easy access.`,
 		RunE: func(cmd *cobra.Command, args []string) error {
-			return initRunE(cmd, args, issuesDir, issuesBranch, remoteName)
+			return initRunE(cmd, args, issuesDir, issuesBranch, remoteName, forceLocal)
 		},
 	}
 
@@ -38,29 +40,31 @@ with proper structure and setting up a git worktree for easy access.`,
 	cmd.Flags().StringVar(&issuesDir, "issues-dir", _issuesDirDefault, "Directory name for the issues worktree (git worktree)")
 	cmd.Flags().StringVar(&issuesBranch, "issues-branch", _issuesBranchDefault, "Branch name for issues tracking")
 	cmd.Flags().StringVar(&remoteName, "remote", "", "Remote to use for pushing the issues branch")
+	cmd.Flags().BoolVar(&forceLocal, "force-local", false, "Create local branch even if remote checks fail")
 
 	return cmd
 }
 
 // checkRemoteBranches checks all remotes for the existence of a branch
-func checkRemoteBranches(repo *git.Repository, branchName string) (map[string]bool, error) {
+// Returns: remotesWithBranch (map of remotes that have the branch),
+//          failedRemotes (list of remotes that couldn't be checked),
+//          error (if we couldn't get remotes at all)
+func checkRemoteBranches(repo *git.Repository, branchName string) (map[string]bool, []string, error) {
 	remoteBranches := make(map[string]bool)
+	var failedRemotes []string
 
 	remotes, err := repo.Remotes()
 	if err != nil {
-		return nil, err
+		return nil, nil, err
 	}
 
 	for _, remote := range remotes {
-		// List remote references
+		// git ls-remote <remote>
 		refs, err := remote.List(&git.ListOptions{})
 		if err != nil {
-			// Skip unreachable remotes
-			fmt.Printf("Warning: Could not list refs for remote %s: %v\n", remote.Config().Name, err)
+			failedRemotes = append(failedRemotes, remote.Config().Name)
 			continue
 		}
-
-		// Check if the branch exists on this remote
 		for _, ref := range refs {
 			if ref.Name() == plumbing.ReferenceName("refs/heads/"+branchName) {
 				remoteBranches[remote.Config().Name] = true
@@ -69,20 +73,18 @@ func checkRemoteBranches(repo *git.Repository, branchName string) (map[string]bo
 		}
 	}
 
-	return remoteBranches, nil
+	return remoteBranches, failedRemotes, nil
 }
 
 // determineTargetRemote determines which remote to use for push operations
 func determineTargetRemote(repo *git.Repository, requestedRemote string, numRemotes int) string {
 	if requestedRemote != "" {
-		// Validate that requested remote exists
 		remotes, _ := repo.Remotes()
 		for _, remote := range remotes {
 			if remote.Config().Name == requestedRemote {
 				return requestedRemote
 			}
 		}
-		// Remote doesn't exist - will error later
 		return ""
 	}
 
@@ -90,26 +92,23 @@ func determineTargetRemote(repo *git.Repository, requestedRemote string, numRemo
 		return ""
 	}
 
-	// Case 2: Single remote (use it regardless of name)
 	if numRemotes == 1 {
 		remotes, _ := repo.Remotes()
 		return remotes[0].Config().Name
 	}
 
-	// Case 3: Multiple remotes without --remote flag
-	// Don't push automatically
+	// Multiple remotes - require explicit --remote flag
 	return ""
 }
 
 // checkoutAndTrackRemoteBranch fetches and tracks an existing remote branch
 func checkoutAndTrackRemoteBranch(repo *git.Repository, remoteName, branchName, issuesDir string) error {
-	// Fetch the remote branch
 	remote, err := repo.Remote(remoteName)
 	if err != nil {
 		return fmt.Errorf("getting remote %s: %w", remoteName, err)
 	}
 
-	fmt.Printf("Fetching '%s' branch from remote '%s'...\n", branchName, remoteName)
+	// git fetch origin refs/heads/issues:refs/remotes/origin/issues
 	err = remote.Fetch(&git.FetchOptions{
 		RefSpecs: []config.RefSpec{
 			config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s", branchName, remoteName, branchName)),
@@ -119,23 +118,21 @@ func checkoutAndTrackRemoteBranch(repo *git.Repository, remoteName, branchName,
 		return fmt.Errorf("fetching remote branch: %w", err)
 	}
 
-	// Create local branch tracking the remote
 	refName := plumbing.ReferenceName(fmt.Sprintf("refs/heads/%s", branchName))
 	remoteRef := plumbing.ReferenceName(fmt.Sprintf("refs/remotes/%s/%s", remoteName, branchName))
 
-	// Get the remote branch reference
 	ref, err := repo.Reference(remoteRef, true)
 	if err != nil {
 		return fmt.Errorf("getting remote branch reference: %w", err)
 	}
 
-	// Create local branch pointing to same commit
+	// git branch issues origin/issues
 	err = repo.Storer.SetReference(plumbing.NewHashReference(refName, ref.Hash()))
 	if err != nil {
 		return fmt.Errorf("creating local branch: %w", err)
 	}
 
-	// Set up tracking
+	// git branch --set-upstream-to=origin/issues issues
 	cfg, err := repo.Config()
 	if err != nil {
 		return fmt.Errorf("getting repo config: %w", err)
@@ -152,14 +149,12 @@ func checkoutAndTrackRemoteBranch(repo *git.Repository, remoteName, branchName,
 		return fmt.Errorf("setting branch tracking: %w", err)
 	}
 
-	// Setup worktree
-	fmt.Printf("Setting up git worktree at %s...\n", issuesDir)
+	// git worktree add issues issues
 	worktreeCmd := exec.Command("git", "worktree", "add", issuesDir, branchName)
 	worktreeCmd.Stdout = os.Stdout
 	worktreeCmd.Stderr = os.Stderr
 	err = worktreeCmd.Run()
 	if err != nil {
-		fmt.Printf("Manual worktree setup: git worktree add %s %s\n", issuesDir, branchName)
 		return fmt.Errorf("creating worktree: %w", err)
 	}
 
@@ -205,13 +200,13 @@ func checkoutAndTrackRemoteBranch(repo *git.Repository, remoteName, branchName,
 			Branch: head.Name(),
 		})
 		if err != nil {
-			fmt.Printf("Warning: Could not checkout main branch for .gitignore update: %v\n", err)
+			// Could not checkout main branch for .gitignore update
 		}
 
 		// Stage and commit .gitignore update
 		_, err = worktree.Add(gitignoreMainPath)
 		if err != nil {
-			fmt.Printf("Warning: Could not stage .gitignore: %v\n", err)
+			// Could not stage .gitignore
 		} else {
 			_, err = worktree.Commit(fmt.Sprintf("Add %s worktree to gitignore", issuesDir), &git.CommitOptions{
 				Author: &object.Signature{
@@ -221,27 +216,18 @@ func checkoutAndTrackRemoteBranch(repo *git.Repository, remoteName, branchName,
 				},
 			})
 			if err != nil {
-				fmt.Printf("Warning: Could not commit .gitignore update: %v\n", err)
+				// Could not commit .gitignore update
 			}
 		}
 	}
 
-	fmt.Printf("Git worktree created at ./%s\n", issuesDir)
-	fmt.Println("\n✓ Tissue initialized successfully!")
-	fmt.Printf("\nTracking '%s' branch from remote '%s'\n", branchName, remoteName)
-	fmt.Printf("Issues directory: ./%s\n", issuesDir)
-	fmt.Println("\nNext steps:")
-	fmt.Println("  - Create your first issue: tissue new")
-	fmt.Println("  - List issues: tissue list")
-	fmt.Println("  - Check status: tissue status")
+	fmt.Printf("Initialized tissue in %s/\n", issuesDir)
 
 	return nil
 }
 
-func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch string, remoteName string) error {
+func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch string, remoteName string, forceLocal bool) error {
 	// Step 1: Open and validate repository
-	fmt.Println("Initializing tissue issue tracking...")
-
 	repo, err := git.PlainOpen(".")
 	if err != nil {
 		err = fmt.Errorf("opening repository: %w", err)
@@ -281,21 +267,24 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch
 	// If branch exists but worktree doesn't, we'll set up the worktree later
 	var skipBranchCreation bool
 	if localBranchExists && !worktreeExists {
-		fmt.Printf("Found existing local '%s' branch, setting up worktree...\n", issuesBranch)
 		skipBranchCreation = true
 	}
 
 	if !skipBranchCreation {
 		// Check remote branches
-		remotesWithBranch, err := checkRemoteBranches(repo, issuesBranch)
+		remotesWithBranch, failedRemotes, err := checkRemoteBranches(repo, issuesBranch)
 		if err != nil {
-			// Log warning but continue - remotes might be temporarily unavailable
-			fmt.Printf("Warning: Could not check remote branches: %v\n", err)
+			// Could not check remote branches - continue anyway
 		}
 
 		remotes, _ := repo.Remotes()
 		numRemotes := len(remotes)
 
+		// If we have failed remotes and user didn't use --force-local, fail safe
+		if len(failedRemotes) > 0 && !forceLocal {
+			return fmt.Errorf("could not verify remote branches for: %v\n\nPossible causes:\n- SSH connectivity issues\n- Network problems\n- Authentication failures\n\nTo proceed anyway, use: tissue init --force-local\nOr fix the connectivity issue and try again", failedRemotes)
+		}
+
 		// Validate --remote flag if provided
 		if remoteName != "" {
 			found := false
@@ -313,7 +302,6 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch
 		// Case 4: Multiple remotes, one has issues branch
 		if numRemotes > 1 && len(remotesWithBranch) == 1 {
 			for remote := range remotesWithBranch {
-				fmt.Printf("Found existing '%s' branch on remote '%s'\n", issuesBranch, remote)
 				// Track the existing remote branch
 				return checkoutAndTrackRemoteBranch(repo, remote, issuesBranch, issuesDir)
 			}
@@ -331,20 +319,17 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch
 
 		// If --remote was specified and that remote has the branch, track it
 		if remoteName != "" && remotesWithBranch[remoteName] {
-			fmt.Printf("Found existing '%s' branch on remote '%s'\n", issuesBranch, remoteName)
 			return checkoutAndTrackRemoteBranch(repo, remoteName, issuesBranch, issuesDir)
 		}
 
 		// Single remote with issues branch
 		if numRemotes == 1 && len(remotesWithBranch) == 1 {
 			for remote := range remotesWithBranch {
-				fmt.Printf("Found existing '%s' branch on remote '%s'\n", issuesBranch, remote)
 				return checkoutAndTrackRemoteBranch(repo, remote, issuesBranch, issuesDir)
 			}
 		}
 
 		// Step 2: Create orphan branch
-		fmt.Printf("Creating orphan '%s' branch...\n", issuesBranch)
 		orphanRef := plumbing.NewSymbolicReference(
 			plumbing.HEAD,
 			branchRefName,
@@ -363,37 +348,27 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch
 		}
 
 		// Clear the index and working directory for orphan branch
-		// This is essential to create a true orphan branch with no parent commits
-		fmt.Println("Clearing working directory for orphan branch...")
-
-		// Get the status to find all tracked files
+		// git rm -rf . (remove all files from index and working directory)
 		status, err := worktree.Status()
 		if err != nil {
 			err = fmt.Errorf("getting worktree status: %w", err)
 			return err
 		}
 
-		// Remove all files from both the index and working directory
 		for filepath := range status {
 			_, err := worktree.Remove(filepath)
 			if err != nil {
 				// Continue even if some files fail to be removed
-				fmt.Printf("Warning: could not remove %s: %v\n", filepath, err)
 			}
-			// Also physically delete the file from disk
 			os.Remove(filepath)
 		}
 
-		// Use RemoveGlob as a fallback to ensure everything is cleared
 		err = worktree.RemoveGlob("*")
 		if err != nil && err != git.ErrGlobNoMatches {
-			fmt.Printf("Warning: RemoveGlob failed: %v\n", err)
+			// RemoveGlob failed, continue anyway
 		}
 
-		// Now we have a clean slate for the orphan branch
 		fs := osfs.New(".")
-
-		// Create README.md
 		readmePath := "README.md"
 	readmeContent := `# Issue Tracker
 
@@ -457,8 +432,7 @@ Use the tissue CLI to manage issues:
 		}
 		gitignoreFile.Close()
 
-		// Stage files
-		fmt.Println("Creating initial structure...")
+		// git add README.md .gitignore
 		_, err = worktree.Add(readmePath)
 		if err != nil {
 			err = fmt.Errorf("staging README.md: %w", err)
@@ -471,7 +445,7 @@ Use the tissue CLI to manage issues:
 		}
 
 		// Commit
-		commit, err := worktree.Commit(fmt.Sprintf("Initialize %s branch for issue tracking", issuesBranch), &git.CommitOptions{
+		_, err = worktree.Commit(fmt.Sprintf("Initialize %s branch for issue tracking", issuesBranch), &git.CommitOptions{
 			Author: &object.Signature{
 				Name:  "Tissue CLI",
 				Email: "tissue@example.com",
@@ -482,13 +456,11 @@ Use the tissue CLI to manage issues:
 			err = fmt.Errorf("committing initial structure: %w", err)
 			return err
 		}
-		fmt.Printf("Created initial commit: %s\n", commit.String()[:7])
 
-		// Step 4: Push to remote if appropriate
+		// git push <remote> <branch>
 		targetRemote := determineTargetRemote(repo, remoteName, numRemotes)
 
 		if targetRemote != "" {
-			fmt.Printf("Pushing to remote '%s'...\n", targetRemote)
 			refSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", issuesBranch, issuesBranch)
 			err = repo.Push(&git.PushOptions{
 				RemoteName: targetRemote,
@@ -497,21 +469,11 @@ Use the tissue CLI to manage issues:
 				},
 			})
 			if err != nil && err != git.NoErrAlreadyUpToDate {
-				fmt.Printf("Warning: Could not push to remote: %v\n", err)
-				fmt.Printf("You can push manually later with: git push -u %s %s\n", targetRemote, issuesBranch)
-			} else if err == nil {
-				fmt.Printf("Pushed '%s' branch to remote '%s'\n", issuesBranch, targetRemote)
+				// Push failed, user can push manually
 			}
-		} else if numRemotes > 1 && remoteName == "" {
-			// Case 3: Multiple remotes, no issues branch, no --remote flag
-			fmt.Println("\nMultiple remotes detected. Not pushing to any remote.")
-			fmt.Println("To push to a specific remote, run:")
-			fmt.Println("  tissue init --remote <remote-name>")
-			fmt.Println("\nOr push manually with git:")
-			fmt.Printf("  git push -u <remote-name> %s\n", issuesBranch)
 		}
 
-		// Step 5: Return to original branch
+		// git checkout <original-branch>
 		err = worktree.Checkout(&git.CheckoutOptions{
 			Branch: originalBranch,
 		})
@@ -519,12 +481,9 @@ Use the tissue CLI to manage issues:
 			err = fmt.Errorf("returning to original branch: %w", err)
 			return err
 		}
-		fmt.Printf("Returned to branch: %s\n", originalBranch.Short())
 	} // End of !skipBranchCreation
 
-	// Step 6: Setup worktree (always done)
-	fmt.Printf("Setting up git worktree at %s...\n", issuesDir)
-
+	// git worktree add <dir> <branch>
 	// TODO: Remove dependency on git CLI when go-git supports worktrees
 	// Track progress: https://github.com/go-git/go-git/pull/396
 	worktreeCmd := exec.Command("git", "worktree", "add", issuesDir, issuesBranch)
@@ -532,18 +491,16 @@ Use the tissue CLI to manage issues:
 	worktreeCmd.Stderr = os.Stderr
 	err = worktreeCmd.Run()
 	if err != nil {
-		fmt.Printf("Manual worktree setup: git worktree add %s %s\n", issuesDir, issuesBranch)
 		err = fmt.Errorf("creating worktree: %w", err)
 		return err
 	}
 
-	// Add issuesDir/ to .gitignore in main branch (but only if we're not tracking existing branch)
+	// Add issuesDir/ to .gitignore in main branch
 	if !skipBranchCreation {
 		worktree, _ := repo.Worktree()
 		gitignoreMainPath := ".gitignore"
 		var gitignoreMainContent []byte
 
-		// Check if .gitignore exists
 		if file, err := os.Open(gitignoreMainPath); err == nil {
 			defer file.Close()
 			info, _ := file.Stat()
@@ -551,7 +508,6 @@ Use the tissue CLI to manage issues:
 			file.Read(gitignoreMainContent)
 		}
 
-		// Append issuesDir/ if not already present
 		gitignoreStr := string(gitignoreMainContent)
 		gitignoreEntry := issuesDir + "/"
 		if !contains(gitignoreStr, gitignoreEntry) {
@@ -566,11 +522,9 @@ Use the tissue CLI to manage issues:
 				return err
 			}
 
-			// Stage and commit .gitignore update
+			// git add .gitignore && git commit -m "..."
 			_, err = worktree.Add(gitignoreMainPath)
-			if err != nil {
-				fmt.Printf("Warning: Could not stage .gitignore: %v\n", err)
-			} else {
+			if err == nil {
 				_, err = worktree.Commit(fmt.Sprintf("Add %s worktree to gitignore", issuesDir), &git.CommitOptions{
 					Author: &object.Signature{
 						Name:  "Tissue CLI",
@@ -578,40 +532,16 @@ Use the tissue CLI to manage issues:
 						When:  time.Now(),
 					},
 				})
-				if err != nil {
-					fmt.Printf("Warning: Could not commit .gitignore update: %v\n", err)
-				}
+				// Commit may fail if no changes or other reasons
 			}
 		}
 	}
 
-	fmt.Printf("Git worktree created at ./%s\n", issuesDir)
-
-	fmt.Println("\n✓ Tissue initialized successfully!")
-	fmt.Printf("\nIssues directory: ./%s\n", issuesDir)
-	fmt.Println("\nNext steps:")
-	fmt.Println("  - Create your first issue: tissue new")
-	fmt.Println("  - List issues: tissue list")
-	fmt.Println("  - Check status: tissue status")
+	fmt.Printf("Initialized tissue in %s/\n", issuesDir)
 
 	return nil
 }
 
 func contains(s, substr string) bool {
-	return len(s) > 0 && len(substr) > 0 &&
-		(s == substr ||
-			len(s) > len(substr) &&
-				(s[:len(substr)] == substr ||
-					s[len(s)-len(substr):] == substr ||
-					len(s) > len(substr)+1 &&
-						containsInMiddle(s, substr)))
-}
-
-func containsInMiddle(s, substr string) bool {
-	for i := 1; i < len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return true
-		}
-	}
-	return false
+	return strings.Contains(s, substr)
 }
go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
-	github.com/kevinburke/ssh_config v1.2.0 // indirect
+	github.com/kevinburke/ssh_config v1.4.0 // indirect
 	github.com/pjbgf/sha1cd v0.3.2 // indirect
 	github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
 	github.com/skeema/knownhosts v1.3.1 // indirect
go.sum
@@ -28,6 +28,8 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl
 github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
 github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
+github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
+github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=