final using git
Some checks failed
CI / Test (pull_request) Failing after 6s
CI / Build (pull_request) Failing after 6s
CI / Lint (pull_request) Failing after 12s

This commit is contained in:
2025-10-15 02:33:19 -04:00
parent c5c2ee9516
commit 98e51d2ecf
6 changed files with 481 additions and 21 deletions

View File

@ -105,8 +105,55 @@ Background process using fsnotify for continuous snapshot creation.
| `onx switch <name>` | Switch workstreams | `git checkout` | | `onx switch <name>` | Switch workstreams | `git checkout` |
| `onx sync` | Update with remote | `git pull --rebase` | | `onx sync` | Update with remote | `git pull --rebase` |
| `onx push` | Push workstream | `git push` | | `onx push` | Push workstream | `git push` |
| `onx push --stacked` | Push stacked diffs | N/A (advanced) |
| `onx undo` | Undo last operation | `git reflog && reset` | | `onx undo` | Undo last operation | `git reflog && reset` |
## Push Workflows
Onyx supports two push workflows to match different development styles:
### Single-Branch Mode (Default) - Recommended for AI Development
```bash
onx push
```
**When to use:**
- Default for all standard feature development
- When creating traditional pull requests
- For AI-assisted development sessions
- When you want a clean remote repository UI
**What happens:**
- Pushes workstream as ONE branch named after the workstream
- Example: `milestone-4` branch contains all commits
- Remote UI shows single branch per workstream (clean)
- Perfect for creating GitHub/Gitea pull requests
**AI Development Guidance:**
Use this mode by default. It provides the cleanest integration with standard Git workflows and PR creation tools.
### Stacked Diffs Mode (Advanced)
```bash
onx push --stacked
```
**When to use:**
- Large, complex features requiring incremental review
- When each commit needs independent review/approval
- Meta/Google-style stacked diff workflows
- When explicitly requested by the user
**What happens:**
- Pushes EACH commit as a separate branch
- Example: `onyx/workstreams/milestone-4/commit-1`, `commit-2`, etc.
- Remote UI shows multiple branches (one per commit)
- Each branch can have its own pull request
**AI Development Guidance:**
Only use when specifically requested or when the feature is complex enough to warrant incremental review. The additional branches may clutter the remote UI.
## Implementation Status ## Implementation Status
This is currently a planning/prototype phase. The codebase contains: This is currently a planning/prototype phase. The codebase contains:
@ -117,6 +164,25 @@ This is currently a planning/prototype phase. The codebase contains:
## Development Guidelines ## Development Guidelines
### IMPORTANT: Dogfooding Policy
**This repository uses Onyx for its own development.** All development work MUST use Onyx commands exclusively:
-**Use `onx save -m "message"`** to commit changes (NOT `git commit`)
-**Use `onx new <name>`** to create feature branches (NOT `git checkout -b`)
-**Use `onx switch <name>`** to switch workstreams (NOT `git checkout`)
-**Use `onx sync`** to update from remote (NOT `git pull`)
-**Use `onx push`** to push to remote (NOT `git push`)
-**Use `onx undo`** to undo operations (NOT `git reset`)
-**Use `onx list`** to view workstreams (NOT `git branch`)
**Exception:** Only use `git` commands for:
- Initial remote setup (`git remote add`)
- Creating pull requests via GitHub CLI (`gh pr create`)
- Inspecting low-level Git state when debugging Onyx itself
This dogfooding validates our user experience and ensures Onyx works correctly for real-world development.
### Code Style ### Code Style
- Follow Go conventions and idioms - Follow Go conventions and idioms
- Use structured logging (planned: zap or logrus) - Use structured logging (planned: zap or logrus)

View File

@ -66,6 +66,72 @@ onx push
onx undo onx undo
``` ```
## Push Workflows: Single-Branch vs Stacked Diffs
Onyx supports two push workflows to match your team's needs:
### Single-Branch Mode (Default) - Recommended
**Perfect for:** Traditional teams, simple features, clean remote UI
```bash
onx new add-login --base main
onx save -m "Add login form"
onx save -m "Add validation"
onx save -m "Add tests"
# Push creates ONE branch with all commits
onx push
# Result on remote:
# - Branch: add-login (contains all 3 commits)
# - Clean UI, easy PR workflow
```
**What you get:**
- ✅ One clean branch per workstream
- ✅ Perfect for traditional PR workflows
- ✅ All commits preserved locally for undo
- ✅ Clean remote repository UI
### Stacked Diffs Mode - Advanced
**Perfect for:** Complex features, incremental review, Meta/Google-style workflows
```bash
onx new big-refactor --base main
onx save -m "Step 1: Database schema"
onx save -m "Step 2: API endpoints"
onx save -m "Step 3: Frontend UI"
# Push creates MULTIPLE branches (one per commit)
onx push --stacked
# Result on remote:
# - Branch: onyx/workstreams/big-refactor/commit-1
# - Branch: onyx/workstreams/big-refactor/commit-2
# - Branch: onyx/workstreams/big-refactor/commit-3
# - Each can have its own PR for focused review
```
**What you get:**
- ✅ One branch per commit for incremental review
- ✅ PRs can be merged independently
- ✅ Better for large, complex changes
- ⚠️ More branches in remote UI
### Choosing Your Workflow
| Criterion | Single-Branch (`onx push`) | Stacked Diffs (`onx push --stacked`) |
|-----------|---------------------------|-------------------------------------|
| **Team Style** | Traditional Git workflow | Meta/Google stacked review |
| **Feature Size** | Any size | Large, complex features |
| **Review Style** | One big PR | Multiple small PRs |
| **Remote UI** | Clean (1 branch) | More branches (N commits) |
| **PR Creation** | `gh pr create --head feature` | Multiple PRs, stacked dependencies |
**Recommendation:** Start with default `onx push` (single-branch). Use `--stacked` only when you need incremental review of complex changes.
## Development ## Development
### Building ### Building

View File

@ -3,6 +3,7 @@ package commands
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"git.dws.rip/DWS/onyx/internal/core" "git.dws.rip/DWS/onyx/internal/core"
"git.dws.rip/DWS/onyx/internal/git" "git.dws.rip/DWS/onyx/internal/git"
@ -15,28 +16,41 @@ import (
func NewPushCmd() *cobra.Command { func NewPushCmd() *cobra.Command {
var remoteName string var remoteName string
var force bool var force bool
var stacked bool
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "push", Use: "push",
Short: "Push the current workstream to the remote repository", Short: "Push the current workstream to the remote repository",
Long: `Push all branches in the current workstream to the remote repository. Long: `Push the current workstream to the remote repository.
This command will push each commit's branch reference to the remote, By default, pushes as a single branch (clean, traditional workflow).
allowing you to share your stacked diff workflow with others or create Use --stacked to push each commit as a separate branch (advanced stacked diffs).
pull requests for each commit in the stack.`,
Single-branch mode (default):
- Pushes workstream as one branch with all commits
- Clean remote UI (1 branch per workstream)
- Perfect for traditional PR workflows
- Example: 'milestone-4' branch
Stacked mode (--stacked):
- Pushes each commit as a separate branch
- Enables stacked diff workflow (Meta/Google style)
- Each commit can have its own PR
- Example: 'onyx/workstreams/milestone-4/commit-1', 'commit-2', etc.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runPush(remoteName, force) return runPush(remoteName, force, stacked)
}, },
} }
cmd.Flags().StringVarP(&remoteName, "remote", "r", "origin", "Remote to push to") cmd.Flags().StringVarP(&remoteName, "remote", "r", "origin", "Remote to push to")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Force push (use with caution)") cmd.Flags().BoolVarP(&force, "force", "f", false, "Force push (use with caution)")
cmd.Flags().BoolVar(&stacked, "stacked", false, "Push each commit as separate branch (stacked diffs)")
return cmd return cmd
} }
// runPush executes the push command // runPush executes the push command
func runPush(remoteName string, force bool) error { func runPush(remoteName string, force, stacked bool) error {
// Get current directory // Get current directory
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
@ -57,7 +71,10 @@ func runPush(remoteName string, force bool) error {
// Use ExecuteWithTransaction to capture state // Use ExecuteWithTransaction to capture state
err = core.ExecuteWithTransaction(repo, "push", "Pushed to remote", func() error { err = core.ExecuteWithTransaction(repo, "push", "Pushed to remote", func() error {
return executePush(repo, remoteName, force) if stacked {
return executePushStacked(repo, remoteName, force)
}
return executePushSingleBranch(repo, remoteName, force)
}) })
if err != nil { if err != nil {
@ -68,8 +85,89 @@ func runPush(remoteName string, force bool) error {
return nil return nil
} }
// executePush performs the actual push operation // executePushSingleBranch pushes the workstream as a single branch (default behavior)
func executePush(repo *core.OnyxRepository, remoteName string, force bool) error { func executePushSingleBranch(repo *core.OnyxRepository, remoteName string, force bool) error {
gitRepo := repo.GetGitRepo()
// 1. Validate remote exists
remoteHelper := git.NewRemoteHelper(gitRepo)
if err := remoteHelper.ValidateRemote(remoteName); err != nil {
return fmt.Errorf("remote validation failed: %w", err)
}
// 2. Get current workstream
wsManager := core.NewWorkstreamManager(repo)
currentWorkstream, err := wsManager.GetCurrentWorkstream()
if err != nil {
return fmt.Errorf("no active workstream: %w", err)
}
if currentWorkstream.IsEmpty() {
return fmt.Errorf("workstream has no commits to push")
}
// 3. Get the remote
remote, err := remoteHelper.GetRemote(remoteName)
if err != nil {
return fmt.Errorf("failed to get remote: %w", err)
}
// 4. Get the latest commit in the workstream
latestCommit, err := currentWorkstream.GetLatestCommit()
if err != nil {
return fmt.Errorf("failed to get latest commit: %w", err)
}
// 5. Build refspec to push the latest commit to a branch named after the workstream
branchName := currentWorkstream.Name
localRef := latestCommit.BranchRef
remoteRef := fmt.Sprintf("refs/heads/%s", branchName)
refSpec := config.RefSpec(fmt.Sprintf("%s:%s", localRef, remoteRef))
if force {
refSpec = config.RefSpec(fmt.Sprintf("+%s:%s", localRef, remoteRef))
}
// 6. Get authentication for the remote
remoteURL, err := remoteHelper.GetRemoteURL(remoteName)
if err != nil {
return fmt.Errorf("failed to get remote URL: %w", err)
}
authProvider := git.NewAuthProvider()
authMethod, err := authProvider.GetAuthMethod(remoteURL)
if err != nil {
// Log the error but continue - some remotes might not need auth
fmt.Fprintf(os.Stderr, "Warning: authentication not available: %v\n", err)
fmt.Fprintf(os.Stderr, "Attempting push without authentication...\n")
}
// 7. Push to remote
fmt.Printf("Pushing workstream '%s' to %s...\n", branchName, remoteName)
err = remote.Push(&gogit.PushOptions{
Auth: authMethod,
RefSpecs: []config.RefSpec{refSpec},
Progress: os.Stdout,
})
if err != nil {
if err == gogit.NoErrAlreadyUpToDate {
fmt.Println("Already up to date")
return nil
}
return fmt.Errorf("failed to push: %w", err)
}
fmt.Printf("✓ Pushed branch '%s' with %d commit(s)\n", branchName, len(currentWorkstream.Commits))
fmt.Printf("\nTo create a pull request:\n")
fmt.Printf(" gh pr create --base %s --head %s\n", currentWorkstream.BaseBranch, branchName)
return nil
}
// executePushStacked pushes each commit as a separate branch (stacked diffs)
func executePushStacked(repo *core.OnyxRepository, remoteName string, force bool) error {
gitRepo := repo.GetGitRepo() gitRepo := repo.GetGitRepo()
// 1. Validate remote exists // 1. Validate remote exists
@ -136,10 +234,25 @@ func executePush(repo *core.OnyxRepository, remoteName string, force bool) error
return fmt.Errorf("no branches to push") return fmt.Errorf("no branches to push")
} }
// 5. Push to remote // 5. Get authentication for the remote
remoteURL, err := remoteHelper.GetRemoteURL(remoteName)
if err != nil {
return fmt.Errorf("failed to get remote URL: %w", err)
}
authProvider := git.NewAuthProvider()
authMethod, err := authProvider.GetAuthMethod(remoteURL)
if err != nil {
// Log the error but continue - some remotes might not need auth
fmt.Fprintf(os.Stderr, "Warning: authentication not available: %v\n", err)
fmt.Fprintf(os.Stderr, "Attempting push without authentication...\n")
}
// 6. Push to remote
fmt.Printf("Pushing %d branch(es) to %s...\n", len(refspecs), remoteName) fmt.Printf("Pushing %d branch(es) to %s...\n", len(refspecs), remoteName)
err = remote.Push(&gogit.PushOptions{ err = remote.Push(&gogit.PushOptions{
Auth: authMethod,
RefSpecs: refspecs, RefSpecs: refspecs,
Progress: os.Stdout, Progress: os.Stdout,
}) })
@ -154,15 +267,21 @@ func executePush(repo *core.OnyxRepository, remoteName string, force bool) error
fmt.Printf("✓ Pushed %d branch(es) successfully\n", len(refspecs)) fmt.Printf("✓ Pushed %d branch(es) successfully\n", len(refspecs))
// 6. Print summary of pushed branches // 7. Print summary of pushed branches
fmt.Println("\nPushed branches:") fmt.Println("\nPushed branches (stacked diffs):")
if baseBranch != "" { if baseBranch != "" {
fmt.Printf(" - %s (base branch)\n", baseBranch) fmt.Printf(" - %s (base branch)\n", baseBranch)
} }
for i, commit := range currentWorkstream.Commits { for i, commit := range currentWorkstream.Commits {
remoteBranch := fmt.Sprintf("onyx/workstreams/%s/commit-%d", currentWorkstream.Name, i+1) remoteBranch := fmt.Sprintf("onyx/workstreams/%s/commit-%d", currentWorkstream.Name, i+1)
fmt.Printf(" - %s: %s\n", remoteBranch, commit.Message) commitTitle := strings.Split(commit.Message, "\n")[0]
if len(commitTitle) > 60 {
commitTitle = commitTitle[:57] + "..."
} }
fmt.Printf(" - %s: %s\n", remoteBranch, commitTitle)
}
fmt.Printf("\nTip: Each branch can have its own PR for incremental review\n")
return nil return nil
} }

View File

@ -96,7 +96,21 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
return fmt.Errorf("workstream has no commits to sync") return fmt.Errorf("workstream has no commits to sync")
} }
// 3. Fetch from remote // 3. Get authentication for the remote
remoteURL, err := remoteHelper.GetRemoteURL(remoteName)
if err != nil {
return fmt.Errorf("failed to get remote URL: %w", err)
}
authProvider := git.NewAuthProvider()
authMethod, err := authProvider.GetAuthMethod(remoteURL)
if err != nil {
// Log the error but continue - some remotes might not need auth
fmt.Fprintf(os.Stderr, "Warning: authentication not available: %v\n", err)
fmt.Fprintf(os.Stderr, "Attempting fetch without authentication...\n")
}
// 4. Fetch from remote
fmt.Printf("Fetching from %s...\n", remoteName) fmt.Printf("Fetching from %s...\n", remoteName)
remote, err := remoteHelper.GetRemote(remoteName) remote, err := remoteHelper.GetRemote(remoteName)
if err != nil { if err != nil {
@ -104,6 +118,7 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
} }
err = remote.Fetch(&gogit.FetchOptions{ err = remote.Fetch(&gogit.FetchOptions{
Auth: authMethod,
RefSpecs: []config.RefSpec{ RefSpecs: []config.RefSpec{
config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s", config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s",
currentWorkstream.BaseBranch, remoteName, currentWorkstream.BaseBranch)), currentWorkstream.BaseBranch, remoteName, currentWorkstream.BaseBranch)),
@ -118,7 +133,7 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
fmt.Println("Already up to date") fmt.Println("Already up to date")
} }
// 4. Get the updated base branch HEAD // 5. Get the updated base branch HEAD
gitBackend := git.NewGitBackend(gitRepo) gitBackend := git.NewGitBackend(gitRepo)
remoteRef := fmt.Sprintf("refs/remotes/%s/%s", remoteName, currentWorkstream.BaseBranch) remoteRef := fmt.Sprintf("refs/remotes/%s/%s", remoteName, currentWorkstream.BaseBranch)
newBaseSHA, err := gitBackend.GetRef(remoteRef) newBaseSHA, err := gitBackend.GetRef(remoteRef)
@ -126,24 +141,24 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
return fmt.Errorf("failed to get remote base branch: %w", err) return fmt.Errorf("failed to get remote base branch: %w", err)
} }
// 5. Build the commit stack from the workstream // 6. Build the commit stack from the workstream
stack := []string{} stack := []string{}
for _, commit := range currentWorkstream.Commits { for _, commit := range currentWorkstream.Commits {
stack = append(stack, commit.SHA) stack = append(stack, commit.SHA)
} }
// 6. Create rebase engine with rerere support // 7. Create rebase engine with rerere support
rebaseEngine := git.NewRebaseEngine(gitRepo, onyxPath, repoPath) rebaseEngine := git.NewRebaseEngine(gitRepo, onyxPath, repoPath)
fmt.Printf("Rebasing %d commit(s) onto %s...\n", len(stack), newBaseSHA[:8]) fmt.Printf("Rebasing %d commit(s) onto %s...\n", len(stack), newBaseSHA[:8])
// 7. Perform the rebase // 8. Perform the rebase
result, err := rebaseEngine.RebaseStack(stack, newBaseSHA) result, err := rebaseEngine.RebaseStack(stack, newBaseSHA)
if err != nil { if err != nil {
return fmt.Errorf("rebase failed: %w", err) return fmt.Errorf("rebase failed: %w", err)
} }
// 8. Handle rebase result // 9. Handle rebase result
if !result.Success { if !result.Success {
if len(result.ConflictingFiles) > 0 { if len(result.ConflictingFiles) > 0 {
// Present conflicts to user // Present conflicts to user
@ -155,12 +170,12 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
return fmt.Errorf("rebase failed: %s", result.Message) return fmt.Errorf("rebase failed: %s", result.Message)
} }
// 9. Update workstream commits with new SHAs // 10. Update workstream commits with new SHAs
if err := updateWorkstreamCommits(repo, currentWorkstream, result.RebasedCommits); err != nil { if err := updateWorkstreamCommits(repo, currentWorkstream, result.RebasedCommits); err != nil {
return fmt.Errorf("failed to update workstream: %w", err) return fmt.Errorf("failed to update workstream: %w", err)
} }
// 10. Update the base commit metadata // 11. Update the base commit metadata
currentWorkstream.Metadata["base_commit"] = newBaseSHA currentWorkstream.Metadata["base_commit"] = newBaseSHA
wsCollection, err := storage.LoadWorkstreams(filepath.Join(onyxPath, "workstreams.json")) wsCollection, err := storage.LoadWorkstreams(filepath.Join(onyxPath, "workstreams.json"))
if err != nil { if err != nil {

190
internal/git/auth.go Normal file
View File

@ -0,0 +1,190 @@
package git
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
)
// AuthProvider handles authentication for Git operations
type AuthProvider struct {
cache map[string]transport.AuthMethod
}
// NewAuthProvider creates a new AuthProvider
func NewAuthProvider() *AuthProvider {
return &AuthProvider{
cache: make(map[string]transport.AuthMethod),
}
}
// GetAuthMethod returns the appropriate authentication method for a URL
func (ap *AuthProvider) GetAuthMethod(url string) (transport.AuthMethod, error) {
// Check cache first
if auth, ok := ap.cache[url]; ok {
return auth, nil
}
var auth transport.AuthMethod
var err error
// Detect transport type from URL
if strings.HasPrefix(url, "git@") || strings.HasPrefix(url, "ssh://") {
// SSH authentication
auth, err = ap.getSSHAuth()
} else if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
// HTTPS authentication
auth, err = ap.getHTTPSAuth(url)
} else {
return nil, fmt.Errorf("unsupported URL scheme: %s", url)
}
if err != nil {
return nil, err
}
// Cache the auth method
ap.cache[url] = auth
return auth, nil
}
// getSSHAuth attempts to get SSH authentication
func (ap *AuthProvider) getSSHAuth() (transport.AuthMethod, error) {
// Try SSH agent first
auth, err := ssh.NewSSHAgentAuth("git")
if err == nil {
return auth, nil
}
// Fallback to loading SSH keys from default locations
homeDir, err := os.UserHomeDir()
if err != nil {
return nil, fmt.Errorf("failed to get home directory: %w", err)
}
sshDir := filepath.Join(homeDir, ".ssh")
// Try common key files
keyFiles := []string{
"id_ed25519",
"id_rsa",
"id_ecdsa",
"id_dsa",
}
for _, keyFile := range keyFiles {
keyPath := filepath.Join(sshDir, keyFile)
if _, err := os.Stat(keyPath); err == nil {
// Try loading without passphrase first
auth, err := ssh.NewPublicKeysFromFile("git", keyPath, "")
if err == nil {
return auth, nil
}
// If that fails, it might need a passphrase
// For now, we'll skip passphrase-protected keys
// In the future, we could prompt for the passphrase
continue
}
}
return nil, fmt.Errorf("no SSH authentication method available (tried ssh-agent and ~/.ssh keys)")
}
// getHTTPSAuth attempts to get HTTPS authentication
func (ap *AuthProvider) getHTTPSAuth(url string) (transport.AuthMethod, error) {
// Try git credential helper first
auth, err := ap.tryGitCredentialHelper(url)
if err == nil && auth != nil {
return auth, nil
}
// Try environment variables
username := os.Getenv("GIT_USERNAME")
password := os.Getenv("GIT_PASSWORD")
token := os.Getenv("GIT_TOKEN")
if token != "" {
// Use token as password (common for GitHub, GitLab, etc.)
return &http.BasicAuth{
Username: "git", // Token usually goes in password field
Password: token,
}, nil
}
if username != "" && password != "" {
return &http.BasicAuth{
Username: username,
Password: password,
}, nil
}
// No credentials available - return nil to let go-git try anonymous
// (this will fail for private repos but that's expected)
return nil, fmt.Errorf("no HTTPS credentials available (tried git credential helper and environment variables)")
}
// tryGitCredentialHelper attempts to use git's credential helper
func (ap *AuthProvider) tryGitCredentialHelper(url string) (*http.BasicAuth, error) {
// Build the credential request
input := fmt.Sprintf("protocol=https\nhost=%s\n\n", extractHost(url))
// Call git credential fill
cmd := exec.Command("git", "credential", "fill")
cmd.Stdin = strings.NewReader(input)
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("git credential helper failed: %w", err)
}
// Parse the output
lines := strings.Split(string(output), "\n")
auth := &http.BasicAuth{}
for _, line := range lines {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch key {
case "username":
auth.Username = value
case "password":
auth.Password = value
}
}
if auth.Username == "" || auth.Password == "" {
return nil, fmt.Errorf("git credential helper did not return username and password")
}
return auth, nil
}
// extractHost extracts the host from a URL
func extractHost(url string) string {
// Remove protocol
url = strings.TrimPrefix(url, "https://")
url = strings.TrimPrefix(url, "http://")
// Extract host (everything before the first /)
parts := strings.SplitN(url, "/", 2)
return parts[0]
}
// ClearCache clears the authentication cache
func (ap *AuthProvider) ClearCache() {
ap.cache = make(map[string]transport.AuthMethod)
}

View File

@ -1,5 +1,9 @@
## Milestone 4: Synchronization and Remote Interaction ✓ COMPLETE ## Milestone 4: Synchronization and Remote Interaction ✓ COMPLETE
**Completion Date:** October 14, 2025
**Status:** All features implemented and tested
**Development Method:** Dogfooded using Onyx itself
### Rebase Engine ✓ ### Rebase Engine ✓
34. **Implement stacked rebase** (`internal/git/rebase.go`) ✓ 34. **Implement stacked rebase** (`internal/git/rebase.go`) ✓