Commit 440e152

bryfry <bryon@fryer.io>
2025-09-27 19:32:19
init with remotes
1 parent 154bba0
Changed files (1)
cmd/init.go
@@ -22,6 +22,7 @@ const (
 func NewInitCmd() *cobra.Command {
 	var issuesDir string
 	var issuesBranch string
+	var remoteName string
 
 	var cmd = &cobra.Command{
 		Use:   "init",
@@ -29,18 +30,215 @@ 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)
+			return initRunE(cmd, args, issuesDir, issuesBranch, remoteName)
 		},
 	}
 
 	// flags
 	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")
 
 	return cmd
 }
 
-func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch string) error {
+// checkRemoteBranches checks all remotes for the existence of a branch
+func checkRemoteBranches(repo *git.Repository, branchName string) (map[string]bool, error) {
+	remoteBranches := make(map[string]bool)
+
+	remotes, err := repo.Remotes()
+	if err != nil {
+		return nil, err
+	}
+
+	for _, remote := range remotes {
+		// List remote references
+		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)
+			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
+				break
+			}
+		}
+	}
+
+	return remoteBranches, 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 ""
+	}
+
+	if numRemotes == 0 {
+		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
+	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)
+	err = remote.Fetch(&git.FetchOptions{
+		RefSpecs: []config.RefSpec{
+			config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s", branchName, remoteName, branchName)),
+		},
+	})
+	if err != nil && err != git.NoErrAlreadyUpToDate {
+		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
+	err = repo.Storer.SetReference(plumbing.NewHashReference(refName, ref.Hash()))
+	if err != nil {
+		return fmt.Errorf("creating local branch: %w", err)
+	}
+
+	// Set up tracking
+	cfg, err := repo.Config()
+	if err != nil {
+		return fmt.Errorf("getting repo config: %w", err)
+	}
+
+	cfg.Branches[branchName] = &config.Branch{
+		Name:   branchName,
+		Remote: remoteName,
+		Merge:  refName,
+	}
+
+	err = repo.Storer.SetConfig(cfg)
+	if err != nil {
+		return fmt.Errorf("setting branch tracking: %w", err)
+	}
+
+	// Setup worktree
+	fmt.Printf("Setting up git worktree at %s...\n", issuesDir)
+	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)
+	}
+
+	// Add issuesDir/ to .gitignore in main branch
+	worktree, err := repo.Worktree()
+	if err != nil {
+		return fmt.Errorf("getting worktree: %w", err)
+	}
+
+	// Get current branch to return to it
+	head, err := repo.Head()
+	if err != nil {
+		return fmt.Errorf("getting HEAD: %w", err)
+	}
+
+	gitignoreMainPath := ".gitignore"
+	var gitignoreMainContent []byte
+
+	// Check if .gitignore exists
+	if file, err := os.Open(gitignoreMainPath); err == nil {
+		defer file.Close()
+		info, _ := file.Stat()
+		gitignoreMainContent = make([]byte, info.Size())
+		file.Read(gitignoreMainContent)
+	}
+
+	// Append issuesDir/ if not already present
+	gitignoreStr := string(gitignoreMainContent)
+	gitignoreEntry := issuesDir + "/"
+	if !contains(gitignoreStr, gitignoreEntry) {
+		if len(gitignoreStr) > 0 && gitignoreStr[len(gitignoreStr)-1] != '\n' {
+			gitignoreStr += "\n"
+		}
+		gitignoreStr += gitignoreEntry + "\n"
+
+		err = os.WriteFile(gitignoreMainPath, []byte(gitignoreStr), 0644)
+		if err != nil {
+			return fmt.Errorf("updating .gitignore: %w", err)
+		}
+
+		// Make sure we're on the main branch to commit .gitignore
+		err = worktree.Checkout(&git.CheckoutOptions{
+			Branch: head.Name(),
+		})
+		if err != nil {
+			fmt.Printf("Warning: Could not checkout main branch for .gitignore update: %v\n", err)
+		}
+
+		// Stage and commit .gitignore update
+		_, err = worktree.Add(gitignoreMainPath)
+		if err != nil {
+			fmt.Printf("Warning: Could not stage .gitignore: %v\n", err)
+		} else {
+			_, err = worktree.Commit(fmt.Sprintf("Add %s worktree to gitignore", issuesDir), &git.CommitOptions{
+				Author: &object.Signature{
+					Name:  "Tissue CLI",
+					Email: "tissue@example.com",
+					When:  time.Now(),
+				},
+			})
+			if err != nil {
+				fmt.Printf("Warning: Could not commit .gitignore update: %v\n", err)
+			}
+		}
+	}
+
+	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")
+
+	return nil
+}
+
+func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch string, remoteName string) error {
 	// Step 1: Open and validate repository
 	fmt.Println("Initializing tissue issue tracking...")
 
@@ -58,66 +256,145 @@ func initRunE(cmd *cobra.Command, args []string, issuesDir string, issuesBranch
 	}
 	originalBranch := head.Name()
 
-	// Check if issues branch already exists
+	// Check if issues branch already exists locally
 	branchRefName := plumbing.ReferenceName("refs/heads/" + issuesBranch)
 	_, err = repo.Reference(branchRefName, false)
-	if err == nil {
-		err = fmt.Errorf("%s branch already exists - use 'tissue status' to check current setup", issuesBranch)
-		return err
+	localBranchExists := err == nil
+
+	// Check if worktree already exists
+	worktreeExists := false
+	if _, err := os.Stat(issuesDir); err == nil {
+		// Check if it's actually a git worktree
+		worktreeCheckCmd := exec.Command("git", "worktree", "list")
+		output, err := worktreeCheckCmd.Output()
+		if err == nil && contains(string(output), issuesDir) {
+			worktreeExists = true
+		}
 	}
 
-	// Step 2: Create orphan branch
-	fmt.Printf("Creating orphan '%s' branch...\n", issuesBranch)
-	orphanRef := plumbing.NewSymbolicReference(
-		plumbing.HEAD,
-		branchRefName,
-	)
-	err = repo.Storer.SetReference(orphanRef)
-	if err != nil {
-		err = fmt.Errorf("creating orphan branch: %w", err)
+	// If both branch and worktree exist, error out
+	if localBranchExists && worktreeExists {
+		err = fmt.Errorf("%s branch and worktree already exist - use 'tissue status' to check current setup", issuesBranch)
 		return err
 	}
 
-	// Step 3: Get worktree and initialize files
-	worktree, err := repo.Worktree()
-	if err != nil {
-		err = fmt.Errorf("getting worktree: %w", err)
-		return err
+	// 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
 	}
 
-	// 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...")
+	if !skipBranchCreation {
+		// Check remote branches
+		remotesWithBranch, 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)
+		}
 
-	// Get the status to find all tracked files
-	status, err := worktree.Status()
-	if err != nil {
-		err = fmt.Errorf("getting worktree status: %w", err)
-		return err
-	}
+		remotes, _ := repo.Remotes()
+		numRemotes := len(remotes)
+
+		// Validate --remote flag if provided
+		if remoteName != "" {
+			found := false
+			for _, remote := range remotes {
+				if remote.Config().Name == remoteName {
+					found = true
+					break
+				}
+			}
+			if !found {
+				return fmt.Errorf("remote '%s' does not exist", remoteName)
+			}
+		}
+
+		// 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)
+			}
+		}
+
+		// Case 5: Multiple remotes, multiple have issues branch
+		if len(remotesWithBranch) > 1 && remoteName == "" {
+			remoteList := []string{}
+			for r := range remotesWithBranch {
+				remoteList = append(remoteList, r)
+			}
+			return fmt.Errorf("branch '%s' exists on multiple remotes: %v. Use --remote flag to specify which remote to use",
+				issuesBranch, remoteList)
+		}
 
-	// Remove all files from both the index and working directory
-	for filepath := range status {
-		_, err := worktree.Remove(filepath)
+		// 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,
+		)
+		err = repo.Storer.SetReference(orphanRef)
 		if err != nil {
-			// Continue even if some files fail to be removed
-			fmt.Printf("Warning: could not remove %s: %v\n", filepath, err)
+			err = fmt.Errorf("creating orphan branch: %w", err)
+			return 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)
-	}
+		// Step 3: Get worktree and initialize files
+		worktree, err := repo.Worktree()
+		if err != nil {
+			err = fmt.Errorf("getting worktree: %w", err)
+			return err
+		}
+
+		// 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
+		status, err := worktree.Status()
+		if err != nil {
+			err = fmt.Errorf("getting worktree status: %w", err)
+			return err
+		}
 
-	// Now we have a clean slate for the orphan branch
-	fs := osfs.New(".")
+		// 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)
+		}
+
+		// Now we have a clean slate for the orphan branch
+		fs := osfs.New(".")
 
-	// Create README.md
-	readmePath := "README.md"
+		// Create README.md
+		readmePath := "README.md"
 	readmeContent := `# Issue Tracker
 
 This branch contains all issues for this repository.
@@ -146,95 +423,104 @@ Use the tissue CLI to manage issues:
 - ` + "`tissue view <id>`" + ` - View an issue
 - ` + "`tissue update <id>`" + ` - Update an issue
 `
-	readmeFile, err := fs.Create(readmePath)
-	if err != nil {
-		err = fmt.Errorf("creating README.md: %w", err)
-		return err
-	}
-	_, err = readmeFile.Write([]byte(readmeContent))
-	if err != nil {
+		readmeFile, err := fs.Create(readmePath)
+		if err != nil {
+			err = fmt.Errorf("creating README.md: %w", err)
+			return err
+		}
+		_, err = readmeFile.Write([]byte(readmeContent))
+		if err != nil {
+			readmeFile.Close()
+			err = fmt.Errorf("writing README.md: %w", err)
+			return err
+		}
 		readmeFile.Close()
-		err = fmt.Errorf("writing README.md: %w", err)
-		return err
-	}
-	readmeFile.Close()
 
-	// Create .gitignore
-	gitignorePath := ".gitignore"
+		// Create .gitignore
+		gitignorePath := ".gitignore"
 	gitignoreContent := `# Ignore everything except markdown files and directories
 /*
 !*.md
 !*/
 !.gitignore
 `
-	gitignoreFile, err := fs.Create(gitignorePath)
-	if err != nil {
-		err = fmt.Errorf("creating .gitignore: %w", err)
-		return err
-	}
-	_, err = gitignoreFile.Write([]byte(gitignoreContent))
-	if err != nil {
+		gitignoreFile, err := fs.Create(gitignorePath)
+		if err != nil {
+			err = fmt.Errorf("creating .gitignore: %w", err)
+			return err
+		}
+		_, err = gitignoreFile.Write([]byte(gitignoreContent))
+		if err != nil {
+			gitignoreFile.Close()
+			err = fmt.Errorf("writing .gitignore: %w", err)
+			return err
+		}
 		gitignoreFile.Close()
-		err = fmt.Errorf("writing .gitignore: %w", err)
-		return err
-	}
-	gitignoreFile.Close()
-
-	// Stage files
-	fmt.Println("Creating initial structure...")
-	_, err = worktree.Add(readmePath)
-	if err != nil {
-		err = fmt.Errorf("staging README.md: %w", err)
-		return err
-	}
-	_, err = worktree.Add(gitignorePath)
-	if err != nil {
-		err = fmt.Errorf("staging .gitignore: %w", err)
-		return err
-	}
 
-	// Commit
-	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",
-			When:  time.Now(),
-		},
-	})
-	if err != nil {
-		err = fmt.Errorf("committing initial structure: %w", err)
-		return err
-	}
-	fmt.Printf("Created initial commit: %s\n", commit.String()[:7])
+		// Stage files
+		fmt.Println("Creating initial structure...")
+		_, err = worktree.Add(readmePath)
+		if err != nil {
+			err = fmt.Errorf("staging README.md: %w", err)
+			return err
+		}
+		_, err = worktree.Add(gitignorePath)
+		if err != nil {
+			err = fmt.Errorf("staging .gitignore: %w", err)
+			return err
+		}
 
-	// Step 4: Push to remote if it exists
-	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(refSpec),
+		// Commit
+		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",
+				When:  time.Now(),
 			},
 		})
-		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 origin %s\n", issuesBranch)
-		} else if err == nil {
-			fmt.Printf("Pushed %s branch to remote\n", issuesBranch)
+		if err != nil {
+			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
+		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,
+				RefSpecs: []config.RefSpec{
+					config.RefSpec(refSpec),
+				},
+			})
+			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)
+			}
+		} 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
-	err = worktree.Checkout(&git.CheckoutOptions{
-		Branch: originalBranch,
-	})
-	if err != nil {
-		err = fmt.Errorf("returning to original branch: %w", err)
-		return err
-	}
-	fmt.Printf("Returned to branch: %s\n", originalBranch.Short())
+		// Step 5: Return to original branch
+		err = worktree.Checkout(&git.CheckoutOptions{
+			Branch: originalBranch,
+		})
+		if err != nil {
+			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)
@@ -251,47 +537,50 @@ Use the tissue CLI to manage issues:
 		return err
 	}
 
-	// Add issuesDir/ to .gitignore in main branch
-	gitignoreMainPath := ".gitignore"
-	var gitignoreMainContent []byte
-
-	// Check if .gitignore exists
-	if file, err := os.Open(gitignoreMainPath); err == nil {
-		defer file.Close()
-		info, _ := file.Stat()
-		gitignoreMainContent = make([]byte, info.Size())
-		file.Read(gitignoreMainContent)
-	}
-
-	// Append issuesDir/ if not already present
-	gitignoreStr := string(gitignoreMainContent)
-	gitignoreEntry := issuesDir + "/"
-	if !contains(gitignoreStr, gitignoreEntry) {
-		if len(gitignoreStr) > 0 && gitignoreStr[len(gitignoreStr)-1] != '\n' {
-			gitignoreStr += "\n"
+	// Add issuesDir/ to .gitignore in main branch (but only if we're not tracking existing 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()
+			gitignoreMainContent = make([]byte, info.Size())
+			file.Read(gitignoreMainContent)
 		}
-		gitignoreStr += gitignoreEntry + "\n"
 
-		err = os.WriteFile(gitignoreMainPath, []byte(gitignoreStr), 0644)
-		if err != nil {
-			err = fmt.Errorf("updating .gitignore: %w", err)
-			return err
-		}
+		// Append issuesDir/ if not already present
+		gitignoreStr := string(gitignoreMainContent)
+		gitignoreEntry := issuesDir + "/"
+		if !contains(gitignoreStr, gitignoreEntry) {
+			if len(gitignoreStr) > 0 && gitignoreStr[len(gitignoreStr)-1] != '\n' {
+				gitignoreStr += "\n"
+			}
+			gitignoreStr += gitignoreEntry + "\n"
 
-		// Stage and commit .gitignore update
-		_, err = worktree.Add(gitignoreMainPath)
-		if err != nil {
-			fmt.Printf("Warning: Could not stage .gitignore: %v\n", err)
-		} else {
-			_, err = worktree.Commit(fmt.Sprintf("Add %s worktree to gitignore", issuesDir), &git.CommitOptions{
-				Author: &object.Signature{
-					Name:  "Tissue CLI",
-					Email: "tissue@example.com",
-					When:  time.Now(),
-				},
-			})
+			err = os.WriteFile(gitignoreMainPath, []byte(gitignoreStr), 0644)
 			if err != nil {
-				fmt.Printf("Warning: Could not commit .gitignore update: %v\n", err)
+				err = fmt.Errorf("updating .gitignore: %w", err)
+				return err
+			}
+
+			// Stage and commit .gitignore update
+			_, err = worktree.Add(gitignoreMainPath)
+			if err != nil {
+				fmt.Printf("Warning: Could not stage .gitignore: %v\n", err)
+			} else {
+				_, err = worktree.Commit(fmt.Sprintf("Add %s worktree to gitignore", issuesDir), &git.CommitOptions{
+					Author: &object.Signature{
+						Name:  "Tissue CLI",
+						Email: "tissue@example.com",
+						When:  time.Now(),
+					},
+				})
+				if err != nil {
+					fmt.Printf("Warning: Could not commit .gitignore update: %v\n", err)
+				}
 			}
 		}
 	}