Commit b0351ee
Changed files (3)
001_init.md
@@ -4,7 +4,6 @@ title: Implement init command
status: in-progress
type: feature
created: 2025-09-24
-assignee: crash
---
# Implement init command
002_init-remote.md
@@ -0,0 +1,674 @@
+---
+id: 002
+title: Handle remote branch conflicts in init command
+status: completed
+type: enhancement
+priority: high
+tags: [init, remote, git]
+assignee:
+created: 2025-09-27T10:00:00Z
+updated: 2025-09-27T16:00:00Z
+completed: 2025-09-27T16:00:00Z
+---
+
+# Handle remote branch conflicts in init command
+
+## Problem Description
+
+The current `tissue init` command has several issues with remote branch handling that can lead to conflicts and unexpected behavior in distributed Git environments:
+
+1. **Hardcoded remote assumption**: The command assumes "origin" remote exists and pushes to it without verification (cmd/init.go:216)
+2. **No remote branch detection**: Doesn't check if the issues branch already exists on any remote
+3. **Multiple remotes not handled**: When multiple remotes exist with the same branch name, there's no way to specify which one to use
+4. **Silent push failures**: Push failures are only logged as warnings, potentially leaving users unaware of sync issues
+
+## Current Behavior
+
+```go
+// cmd/init.go lines 210-227
+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", // <-- Hardcoded assumption
+ RefSpecs: []config.RefSpec{
+ config.RefSpec(refSpec),
+ },
+ })
+ // ...
+}
+```
+
+## Edge Cases to Consider
+
+### Case 1: No remotes configured
+- **Current**: Skips push (correct behavior)
+- **Expected**: Continue as-is ✓
+
+### Case 2: Single remote (not named "origin")
+- **Current**: Push fails silently
+- **Expected**: Should detect and use the single remote
+
+### Case 3: Multiple remotes, none with issues branch
+- **Current**: Pushes to "origin" if it exists
+- **Expected**:
+ - if --remote flag set
+ - should check if that remote exists, and then push to it
+ - if --remote flag not set
+ - Should not push to any remote
+ - print output saying that no pushing has occured
+ - print how to tissue init with a remote (--remote flag)
+ - print how the user can set and push the issues branch/worktree with normal git commands
+
+### Case 4: Multiple remotes, one has issues branch
+- **Current**: May create conflicting local branch
+- **Expected**: Should detect and:
+ - Log that the dectected matching remote branch was found
+ - Checkout and track the existing remote branch
+
+### Case 5: Multiple remotes, multiple have issues branch
+- **Current**: Creates local branch, pushes to "origin"
+- **Expected**: Should require explicit remote selection (--remote flag)
+
+## Proposed Solution
+
+### 1. Add `--remote` flag
+```go
+var remoteName string
+cmd.Flags().StringVar(&remoteName, "remote", "", "Remote to use for pushing the issues branch")
+```
+
+### 2. Implement remote branch detection
+```go
+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 {
+ refs, err := remote.List(&git.ListOptions{})
+ if err != nil {
+ continue // Skip unreachable remotes
+ }
+
+ for _, ref := range refs {
+ if ref.Name() == plumbing.ReferenceName("refs/heads/"+branchName) {
+ remoteBranches[remote.Config().Name] = true
+ }
+ }
+ }
+
+ return remoteBranches, nil
+}
+```
+
+### 3. Improve initialization flow
+```go
+func initRunE(cmd *cobra.Command, args []string, issuesDir, issuesBranch, remoteName string) error {
+ // ... existing validation ...
+
+ // 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)
+ }
+
+ remotes, _ := repo.Remotes()
+ numRemotes := len(remotes)
+
+ // 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)
+ }
+
+ // 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)
+ }
+ }
+
+ // No existing remote branch - create new orphan branch
+ // ... continue with branch creation ...
+
+ // Determine which remote to use for push
+ targetRemote := determineTargetRemote(repo, remoteName, numRemotes)
+
+ if numRemotes > 1 && targetRemote == "" && 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)
+ return nil
+ }
+
+ // ... continue with push if targetRemote is set ...
+}
+
+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 ""
+}
+
+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)
+ }
+
+ 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)
+ }
+
+ // Set up worktree
+ fmt.Printf("Setting up git worktree at %s...\n", issuesDir)
+ // ... continue with worktree setup ...
+
+ return nil
+}
+```
+
+## Implementation Plan
+
+1. **Phase 1: Detection** (Priority: High)
+ - Add remote branch detection function
+ - Warn user if remote branch exists
+ - Fail gracefully with informative error
+
+2. **Phase 2: Remote Selection** (Priority: High)
+ - Add `--remote` flag
+ - Implement smart remote selection logic
+ - Update push logic to use selected remote
+
+3. **Phase 3: Conflict Resolution** (Priority: Medium)
+ - Add `--track` flag to track existing remote branch
+ - Add `--force` flag to override remote branch
+ - Implement interactive mode for conflict resolution
+
+4. **Phase 4: Better Error Handling** (Priority: Medium)
+ - Improve error messages with actionable suggestions
+ - Add `--dry-run` flag to preview actions
+ - Log all remote operations clearly
+
+## Test Scenarios
+
+### Test 1: No remotes configured
+**Setup:**
+```bash
+# Create a new local repository
+mkdir test-tissue-1
+cd test-tissue-1
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should create orphan issues branch locally
+- Should set up worktree
+- No push attempts
+- Success message without any remote warnings
+
+---
+
+### Test 2: Single remote (not named "origin")
+**Setup:**
+```bash
+# Create a bare remote repository
+mkdir -p /tmp/test-remotes/upstream.git
+cd /tmp/test-remotes/upstream.git
+git init --bare
+
+# Create local repository with non-origin remote
+mkdir test-tissue-2
+cd test-tissue-2
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add upstream /tmp/test-remotes/upstream.git
+git push -u upstream main
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should create orphan issues branch
+- Should automatically detect and use "upstream" remote
+- Should push to upstream
+- Success message showing "Pushed issues branch to remote 'upstream'"
+
+---
+
+### Test 3: Multiple remotes, no issues branch
+**Setup:**
+```bash
+# Create two bare remote repositories
+mkdir -p /tmp/test-remotes/origin.git
+mkdir -p /tmp/test-remotes/upstream.git
+cd /tmp/test-remotes/origin.git
+git init --bare
+cd /tmp/test-remotes/upstream.git
+git init --bare
+
+# Create local repository with multiple remotes
+mkdir test-tissue-3
+cd test-tissue-3
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+git remote add upstream /tmp/test-remotes/upstream.git
+git push -u origin main
+```
+
+**Test A (without --remote flag):**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should create orphan issues branch locally
+- Should NOT push to any remote
+- Should display message about multiple remotes
+- Should show instructions for using --remote flag
+- Should show manual git push command
+
+**Test B (with --remote flag):**
+```bash
+tissue init --remote origin
+```
+
+**Expected Result:**
+- Should create orphan issues branch
+- Should push to specified "origin" remote
+- Success message
+
+---
+
+### Test 4: Multiple remotes, one has issues branch
+**Setup:**
+```bash
+# Create two bare remotes
+mkdir -p /tmp/test-remotes/origin.git
+mkdir -p /tmp/test-remotes/upstream.git
+cd /tmp/test-remotes/origin.git
+git init --bare
+cd /tmp/test-remotes/upstream.git
+git init --bare
+
+# Create first repo and push issues branch to origin
+mkdir test-tissue-4a
+cd test-tissue-4a
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+git push -u origin main
+
+# Create and push issues branch
+git checkout --orphan issues
+git rm -rf .
+echo "# Issues" > README.md
+git add README.md
+git commit -m "Initialize issues"
+git push -u origin issues
+git checkout main
+
+# Now create second repo with both remotes but issues only on origin
+cd ..
+mkdir test-tissue-4b
+cd test-tissue-4b
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+git remote add upstream /tmp/test-remotes/upstream.git
+git fetch origin
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should detect issues branch on origin
+- Should display "Found existing 'issues' branch on remote 'origin'"
+- Should fetch and track the existing branch
+- Should set up worktree
+- Success message
+
+---
+
+### Test 5: Multiple remotes, multiple have issues branch
+**Setup:**
+```bash
+# Create two bare remotes
+mkdir -p /tmp/test-remotes/origin.git
+mkdir -p /tmp/test-remotes/upstream.git
+cd /tmp/test-remotes/origin.git
+git init --bare
+cd /tmp/test-remotes/upstream.git
+git init --bare
+
+# Create first repo and push issues to both remotes
+mkdir test-tissue-5a
+cd test-tissue-5a
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+git remote add upstream /tmp/test-remotes/upstream.git
+
+# Create and push issues branch to both
+git checkout --orphan issues
+git rm -rf .
+echo "# Issues Origin" > README.md
+git add README.md
+git commit -m "Initialize issues on origin"
+git push -u origin issues
+
+# Modify and push to upstream (different content)
+echo "# Issues Upstream" > README.md
+git add README.md
+git commit -m "Initialize issues on upstream"
+git push -u upstream issues
+git checkout main
+
+# Create second repo that will see both
+cd ..
+mkdir test-tissue-5b
+cd test-tissue-5b
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+git remote add upstream /tmp/test-remotes/upstream.git
+git fetch --all
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should detect issues branch on both remotes
+- Should display error: "Branch 'issues' exists on multiple remotes: [origin, upstream]. Use --remote flag to specify which remote to use"
+- Should exit with error
+
+**Test with --remote:**
+```bash
+tissue init --remote origin
+```
+
+**Expected Result:**
+- Should track issues branch from origin
+- Success message
+
+---
+
+### Test 6: Unreachable remote
+**Setup:**
+```bash
+# Create local repository with unreachable remote
+mkdir test-tissue-6
+cd test-tissue-6
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin ssh://nonexistent.example.com/repo.git
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should display warning about unable to check remote branches
+- Should continue with local branch creation
+- Should attempt push and show warning when it fails
+- Should still complete successfully with worktree setup
+
+---
+
+### Test 7: Invalid --remote flag
+**Setup:**
+```bash
+# Create repository with one remote
+mkdir test-tissue-7
+cd test-tissue-7
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git remote add origin /tmp/test-remotes/origin.git
+```
+
+**Test:**
+```bash
+tissue init --remote nonexistent
+```
+
+**Expected Result:**
+- Should fail with error: "Remote 'nonexistent' does not exist"
+- Should suggest available remotes
+
+---
+
+### Test 8: Local issues branch already exists
+**Setup:**
+```bash
+# Create repository with existing local issues branch
+mkdir test-tissue-8
+cd test-tissue-8
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git checkout -b issues
+echo "# Existing Issues" > issues.md
+git add issues.md
+git commit -m "Existing issues branch"
+git checkout main
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should fail with error: "issues branch already exists - use 'tissue status' to check current setup"
+
+---
+
+### Test 9: Local issues branch exists but worktree is missing
+**Setup:**
+```bash
+# Create repository with existing local issues branch but no worktree
+mkdir test-tissue-9
+cd test-tissue-9
+git init
+echo "# Test Repo" > README.md
+git add README.md
+git commit -m "Initial commit"
+git checkout -b issues
+echo "# Existing Issues" > issues.md
+git add issues.md
+git commit -m "Existing issues branch"
+git checkout main
+# Remove any existing worktree if present
+rm -rf issues/
+```
+
+**Test:**
+```bash
+tissue init
+```
+
+**Expected Result:**
+- Should detect existing local issues branch
+- Should checkout the existing branch (not create a new orphan)
+- Should set up worktree pointing to the existing branch
+- Success message: "Using existing 'issues' branch and setting up worktree"
+
+**Current Bug:**
+- Currently fails to checkout/use the existing local branch
+- May create a new orphan branch instead
+
+## Expected User Experience
+
+### Scenario 1: Fresh repository, single remote
+```bash
+$ tissue init
+Initializing tissue issue tracking...
+Creating orphan 'issues' branch...
+Pushing to remote 'origin'...
+✓ Tissue initialized successfully!
+```
+
+### Scenario 2: Multiple remotes, no issues branch
+```bash
+$ tissue init
+Initializing tissue issue tracking...
+Creating orphan 'issues' branch...
+✓ Tissue initialized successfully!
+
+Multiple remotes detected. Not pushing to any remote.
+To push to a specific remote, run:
+ tissue init --remote <remote-name>
+
+Or push manually with git:
+ git push -u <remote-name> issues
+```
+
+### Scenario 3: Multiple remotes, one has issues branch
+```bash
+$ tissue init
+Initializing tissue issue tracking...
+Found existing 'issues' branch on remote 'origin'
+Fetching and tracking remote branch...
+✓ Tissue initialized successfully!
+```
+
+### Scenario 4: Multiple remotes, multiple have issues branch
+```bash
+$ tissue init
+Initializing tissue issue tracking...
+Error: Branch 'issues' exists on multiple remotes: [origin, upstream]. Use --remote flag to specify which remote to use
+```
+
+## Notes
+
+- Consider using go-git's native remote listing capabilities instead of exec'ing git commands
+- May need to handle authentication for remote operations more gracefully
+- Should respect user's git configuration for default push behavior
+- Consider adding a `tissue init --from-remote` command to explicitly pull existing issues branch
+
+## References
+
+- Current implementation: cmd/init.go:210-227
+- go-git remote handling: https://github.com/go-git/go-git/blob/master/remote.go
+- Git worktree documentation: https://git-scm.com/docs/git-worktree
003_init-nits.md
@@ -0,0 +1,7 @@
+
+## Nitpicky things
+
+- Init is hundreds of lines long - let's decompose it into smaller functions
+ - consider using internal/git as a package for git operations that we may need to use often
+- Init the issues dir with 000_README.md to make the list look nice - symlink README.md to it
+