Commit 825c0ef
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