Commit 825c0ef

bryfry <bryon@fryer.io>
2025-09-24 21:14:15
001_init issues-branch
1 parent 74e68a6
Changed files (2)
cmd/init.go
@@ -14,8 +14,14 @@ import (
 	"github.com/spf13/cobra"
 )
 
+const (
+	_issuesBranchDefault = "issues"
+	_issuesDirDefault    = "issues"
+)
+
 func NewInitCmd() *cobra.Command {
 	var issuesDir string
+	var issuesBranch string
 
 	var cmd = &cobra.Command{
 		Use:   "init",
@@ -23,53 +29,60 @@ 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)
+			return initRunE(cmd, args, issuesDir, issuesBranch)
 		},
 	}
 
 	// flags
-	cmd.Flags().StringVar(&issuesDir, "issues-dir", "issues", "Directory name for the issues worktree (git worktree)")
+	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")
 
 	return cmd
 }
 
-func initRunE(cmd *cobra.Command, args []string, issuesDir string) error {
+func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch string) error {
 	// Step 1: Open and validate repository
 	fmt.Println("Initializing tissue issue tracking...")
 
 	repo, err := git.PlainOpen(".")
 	if err != nil {
-		return fmt.Errorf("failed to open repository: %w\nMake sure you're in a git repository", err)
+		err = fmt.Errorf("opening repository: %w", err)
+		return err
 	}
 
 	// Store current branch to return to later
 	head, err := repo.Head()
 	if err != nil {
-		return fmt.Errorf("failed to get current branch: %w", err)
+		err = fmt.Errorf("getting current branch: %w", err)
+		return err
 	}
 	originalBranch := head.Name()
 
 	// Check if issues branch already exists
-	_, err = repo.Reference(plumbing.ReferenceName("refs/heads/issues"), false)
+	branchRefName := plumbing.ReferenceName("refs/heads/" + issuesBranch)
+	_, err = repo.Reference(branchRefName, false)
 	if err == nil {
-		return fmt.Errorf("issues branch already exists. Use 'tissue status' to check current setup")
+		err = fmt.Errorf("%s branch already exists - use 'tissue status' to check current setup", issuesBranch)
+		return err
 	}
 
 	// Step 2: Create orphan branch
-	fmt.Println("Creating orphan 'issues' branch...")
+	fmt.Printf("Creating orphan '%s' branch...\n", issuesBranch)
 	orphanRef := plumbing.NewSymbolicReference(
 		plumbing.HEAD,
-		plumbing.ReferenceName("refs/heads/issues"),
+		branchRefName,
 	)
 	err = repo.Storer.SetReference(orphanRef)
 	if err != nil {
-		return fmt.Errorf("failed to create orphan branch: %w", err)
+		err = fmt.Errorf("creating orphan branch: %w", err)
+		return err
 	}
 
 	// Step 3: Get worktree and initialize files
 	worktree, err := repo.Worktree()
 	if err != nil {
-		return fmt.Errorf("failed to get worktree: %w", err)
+		err = fmt.Errorf("getting worktree: %w", err)
+		return err
 	}
 
 	// Clear the index and working directory for orphan branch
@@ -79,7 +92,8 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string) error {
 	// Get the status to find all tracked files
 	status, err := worktree.Status()
 	if err != nil {
-		return fmt.Errorf("failed to get worktree status: %w", err)
+		err = fmt.Errorf("getting worktree status: %w", err)
+		return err
 	}
 
 	// Remove all files from both the index and working directory
@@ -134,12 +148,14 @@ Use the tissue CLI to manage issues:
 `
 	readmeFile, err := fs.Create(readmePath)
 	if err != nil {
-		return fmt.Errorf("failed to create README.md: %w", err)
+		err = fmt.Errorf("creating README.md: %w", err)
+		return err
 	}
 	_, err = readmeFile.Write([]byte(readmeContent))
 	if err != nil {
 		readmeFile.Close()
-		return fmt.Errorf("failed to write README.md: %w", err)
+		err = fmt.Errorf("writing README.md: %w", err)
+		return err
 	}
 	readmeFile.Close()
 
@@ -153,12 +169,14 @@ Use the tissue CLI to manage issues:
 `
 	gitignoreFile, err := fs.Create(gitignorePath)
 	if err != nil {
-		return fmt.Errorf("failed to create .gitignore: %w", err)
+		err = fmt.Errorf("creating .gitignore: %w", err)
+		return err
 	}
 	_, err = gitignoreFile.Write([]byte(gitignoreContent))
 	if err != nil {
 		gitignoreFile.Close()
-		return fmt.Errorf("failed to write .gitignore: %w", err)
+		err = fmt.Errorf("writing .gitignore: %w", err)
+		return err
 	}
 	gitignoreFile.Close()
 
@@ -166,15 +184,17 @@ Use the tissue CLI to manage issues:
 	fmt.Println("Creating initial structure...")
 	_, err = worktree.Add(readmePath)
 	if err != nil {
-		return fmt.Errorf("failed to stage README.md: %w", err)
+		err = fmt.Errorf("staging README.md: %w", err)
+		return err
 	}
 	_, err = worktree.Add(gitignorePath)
 	if err != nil {
-		return fmt.Errorf("failed to stage .gitignore: %w", err)
+		err = fmt.Errorf("staging .gitignore: %w", err)
+		return err
 	}
 
 	// Commit
-	commit, err := worktree.Commit("Initialize issues branch for issue tracking", &git.CommitOptions{
+	commit, err := worktree.Commit(fmt.Sprintf("Initialize %s branch for issue tracking", issuesBranch), &git.CommitOptions{
 		Author: &object.Signature{
 			Name:  "Tissue CLI",
 			Email: "tissue@example.com",
@@ -182,7 +202,8 @@ Use the tissue CLI to manage issues:
 		},
 	})
 	if err != nil {
-		return fmt.Errorf("failed to commit initial structure: %w", err)
+		err = fmt.Errorf("committing initial structure: %w", err)
+		return err
 	}
 	fmt.Printf("Created initial commit: %s\n", commit.String()[:7])
 
@@ -190,17 +211,18 @@ Use the tissue CLI to manage issues:
 	remotes, err := repo.Remotes()
 	if err == nil && len(remotes) > 0 {
 		fmt.Println("Pushing to remote...")
+		refSpec := fmt.Sprintf("refs/heads/%s:refs/heads/%s", issuesBranch, issuesBranch)
 		err = repo.Push(&git.PushOptions{
 			RemoteName: "origin",
 			RefSpecs: []config.RefSpec{
-				config.RefSpec("refs/heads/issues:refs/heads/issues"),
+				config.RefSpec(refSpec),
 			},
 		})
 		if err != nil && err != git.NoErrAlreadyUpToDate {
 			fmt.Printf("Warning: Could not push to remote: %v\n", err)
-			fmt.Println("You can push manually later with: git push -u origin issues")
+			fmt.Printf("You can push manually later with: git push -u origin %s\n", issuesBranch)
 		} else if err == nil {
-			fmt.Println("Pushed issues branch to remote")
+			fmt.Printf("Pushed %s branch to remote\n", issuesBranch)
 		}
 	}
 
@@ -209,7 +231,8 @@ Use the tissue CLI to manage issues:
 		Branch: originalBranch,
 	})
 	if err != nil {
-		return fmt.Errorf("failed to return to original branch: %w", err)
+		err = fmt.Errorf("returning to original branch: %w", err)
+		return err
 	}
 	fmt.Printf("Returned to branch: %s\n", originalBranch.Short())
 
@@ -218,12 +241,14 @@ Use the tissue CLI to manage issues:
 
 	// 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, "issues")
+	worktreeCmd := exec.Command("git", "worktree", "add", issuesDir, issuesBranch)
 	worktreeCmd.Stdout = os.Stdout
 	worktreeCmd.Stderr = os.Stderr
 	err = worktreeCmd.Run()
 	if err != nil {
-		return fmt.Errorf("failed to create worktree: %w\nYou can manually run: git worktree add %s issues", err, issuesDir)
+		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
@@ -249,7 +274,8 @@ Use the tissue CLI to manage issues:
 
 		err = os.WriteFile(gitignoreMainPath, []byte(gitignoreStr), 0644)
 		if err != nil {
-			return fmt.Errorf("failed to update .gitignore: %w", err)
+			err = fmt.Errorf("updating .gitignore: %w", err)
+			return err
 		}
 
 		// Stage and commit .gitignore update
@@ -285,11 +311,11 @@ Use the tissue CLI to manage issues:
 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)))
+			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 {
docs/go-err.md
@@ -0,0 +1,139 @@
+# Golang Error Formatting Style Guide
+
+## Core Principles
+
+### 1. Separate Error Wrapping from Return
+Always put the error wrapping on its own line, then return on the next line.
+
+**Don't do this:**
+```go
+repo, err := git.PlainOpen(".")
+if err != nil {
+    return fmt.Errorf("failed to open repository: %w\nMake sure you're in a git repository", err)
+}
+```
+
+**Do this:**
+```go
+repo, err := git.PlainOpen(".")
+if err != nil {
+    err = fmt.Errorf("opening repository: %w", err)
+    return err
+}
+```
+
+### 2. Error Message Format
+Follow the pattern: `action - context: %w`
+- Always include the action (what was being attempted)
+- Add " - context" when additional context helps
+- Never use newlines (`\n`) in error messages
+
+**Examples:**
+```go
+// Simple action only
+err = fmt.Errorf("opening repository: %w", err)
+
+// Action with helpful context
+err = fmt.Errorf("opening repository - ensure current working directory is a git repository: %w", err)
+
+// Action with specific context
+err = fmt.Errorf("parsing config - check YAML syntax: %w", err)
+```
+
+### 3. Multi-Level Error Wrapping
+Be selective when wrapping errors through multiple levels:
+- Always add context at the top level
+- Only add context at lower levels if the error lacks necessary details
+- Avoid redundant wrapping
+
+```go
+// Lower level - only wrap if adding value
+func readConfig() error {
+    data, err := os.ReadFile("config.yaml")
+    if err != nil {
+        // Only wrap if the error needs more context
+        err = fmt.Errorf("reading config.yaml: %w", err)
+        return err
+    }
+    // ...
+}
+
+// Higher level - add user-facing context
+func initialize() error {
+    err := readConfig()
+    if err != nil {
+        err = fmt.Errorf("initializing application - check configuration: %w", err)
+        return err
+    }
+    // ...
+}
+```
+
+### 4. Variable Reuse
+Prefer reusing the `err` variable rather than creating new variables:
+
+**Preferred:**
+```go
+err = fmt.Errorf("action: %w", err)
+return err
+```
+
+**Avoid:**
+```go
+wrappedErr := fmt.Errorf("action: %w", err)
+return wrappedErr
+```
+
+### 5. Capitalization
+Follow Go convention - error messages should start with lowercase:
+
+✅ **Correct:**
+```go
+err = fmt.Errorf("opening file: %w", err)
+err = fmt.Errorf("parsing JSON: %w", err)
+```
+
+❌ **Incorrect:**
+```go
+err = fmt.Errorf("Opening file: %w", err)
+err = fmt.Errorf("Failed to parse JSON: %w", err)
+```
+
+### 6. When Not to Wrap
+- Don't wrap if there's no error to wrap
+- When creating new errors from scratch, prefer defining custom error types
+
+```go
+// Don't wrap non-existent errors
+if someCondition {
+    // Don't do: err = fmt.Errorf("something went wrong")
+    // Consider creating a custom error type instead:
+    return ErrInvalidState
+}
+
+// Define custom error types for common errors
+var (
+    ErrAlreadyExists = errors.New("resource already exists")
+    ErrNotFound      = errors.New("resource not found")
+    ErrInvalidInput  = errors.New("invalid input provided")
+)
+```
+
+## Additional Guidelines
+
+### Remove "failed to" Prefix
+Instead of "failed to X", just use "X" as the action:
+
+❌ `"failed to open file: %w"`
+✅ `"opening file: %w"`
+
+❌ `"failed to connect to database: %w"`
+✅ `"connecting to database: %w"`
+
+### Benefits of This Style
+
+1. **Clarity**: Separating wrapping from returning makes it clear where errors are being modified
+2. **Maintainability**: Having return on its own line makes it easier to modify function signatures
+3. **Consistency**: Following a standard pattern makes error handling predictable
+4. **Debugging**: Clean error chains without redundant information or formatting issues
+5. **Readability**: No multi-line error messages in the code