final using git
This commit is contained in:
66
CLAUDE.md
66
CLAUDE.md
@ -105,8 +105,55 @@ Background process using fsnotify for continuous snapshot creation.
|
||||
| `onx switch <name>` | Switch workstreams | `git checkout` |
|
||||
| `onx sync` | Update with remote | `git pull --rebase` |
|
||||
| `onx push` | Push workstream | `git push` |
|
||||
| `onx push --stacked` | Push stacked diffs | N/A (advanced) |
|
||||
| `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
|
||||
|
||||
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
|
||||
|
||||
### 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
|
||||
- Follow Go conventions and idioms
|
||||
- Use structured logging (planned: zap or logrus)
|
||||
|
66
README.md
66
README.md
@ -66,6 +66,72 @@ onx push
|
||||
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
|
||||
|
||||
### Building
|
||||
|
@ -3,6 +3,7 @@ package commands
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"git.dws.rip/DWS/onyx/internal/core"
|
||||
"git.dws.rip/DWS/onyx/internal/git"
|
||||
@ -15,28 +16,41 @@ import (
|
||||
func NewPushCmd() *cobra.Command {
|
||||
var remoteName string
|
||||
var force bool
|
||||
var stacked bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push",
|
||||
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,
|
||||
allowing you to share your stacked diff workflow with others or create
|
||||
pull requests for each commit in the stack.`,
|
||||
By default, pushes as a single branch (clean, traditional workflow).
|
||||
Use --stacked to push each commit as a separate branch (advanced stacked diffs).
|
||||
|
||||
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 {
|
||||
return runPush(remoteName, force)
|
||||
return runPush(remoteName, force, stacked)
|
||||
},
|
||||
}
|
||||
|
||||
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().BoolVar(&stacked, "stacked", false, "Push each commit as separate branch (stacked diffs)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
// runPush executes the push command
|
||||
func runPush(remoteName string, force bool) error {
|
||||
func runPush(remoteName string, force, stacked bool) error {
|
||||
// Get current directory
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@ -57,7 +71,10 @@ func runPush(remoteName string, force bool) error {
|
||||
|
||||
// Use ExecuteWithTransaction to capture state
|
||||
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 {
|
||||
@ -68,8 +85,89 @@ func runPush(remoteName string, force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// executePush performs the actual push operation
|
||||
func executePush(repo *core.OnyxRepository, remoteName string, force bool) error {
|
||||
// executePushSingleBranch pushes the workstream as a single branch (default behavior)
|
||||
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()
|
||||
|
||||
// 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")
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
err = remote.Push(&gogit.PushOptions{
|
||||
Auth: authMethod,
|
||||
RefSpecs: refspecs,
|
||||
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))
|
||||
|
||||
// 6. Print summary of pushed branches
|
||||
fmt.Println("\nPushed branches:")
|
||||
// 7. Print summary of pushed branches
|
||||
fmt.Println("\nPushed branches (stacked diffs):")
|
||||
if baseBranch != "" {
|
||||
fmt.Printf(" - %s (base branch)\n", baseBranch)
|
||||
}
|
||||
for i, commit := range currentWorkstream.Commits {
|
||||
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
|
||||
}
|
||||
|
@ -96,7 +96,21 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
|
||||
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)
|
||||
remote, err := remoteHelper.GetRemote(remoteName)
|
||||
if err != nil {
|
||||
@ -104,6 +118,7 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
|
||||
}
|
||||
|
||||
err = remote.Fetch(&gogit.FetchOptions{
|
||||
Auth: authMethod,
|
||||
RefSpecs: []config.RefSpec{
|
||||
config.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s/%s",
|
||||
currentWorkstream.BaseBranch, remoteName, currentWorkstream.BaseBranch)),
|
||||
@ -118,7 +133,7 @@ func executeSync(repo *core.OnyxRepository, repoPath, remoteName string) error {
|
||||
fmt.Println("Already up to date")
|
||||
}
|
||||
|
||||
// 4. Get the updated base branch HEAD
|
||||
// 5. Get the updated base branch HEAD
|
||||
gitBackend := git.NewGitBackend(gitRepo)
|
||||
remoteRef := fmt.Sprintf("refs/remotes/%s/%s", remoteName, currentWorkstream.BaseBranch)
|
||||
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)
|
||||
}
|
||||
|
||||
// 5. Build the commit stack from the workstream
|
||||
// 6. Build the commit stack from the workstream
|
||||
stack := []string{}
|
||||
for _, commit := range currentWorkstream.Commits {
|
||||
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)
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("rebase failed: %w", err)
|
||||
}
|
||||
|
||||
// 8. Handle rebase result
|
||||
// 9. Handle rebase result
|
||||
if !result.Success {
|
||||
if len(result.ConflictingFiles) > 0 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 9. Update workstream commits with new SHAs
|
||||
// 10. Update workstream commits with new SHAs
|
||||
if err := updateWorkstreamCommits(repo, currentWorkstream, result.RebasedCommits); err != nil {
|
||||
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
|
||||
wsCollection, err := storage.LoadWorkstreams(filepath.Join(onyxPath, "workstreams.json"))
|
||||
if err != nil {
|
||||
|
190
internal/git/auth.go
Normal file
190
internal/git/auth.go
Normal 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)
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
## 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 ✓
|
||||
|
||||
34. **Implement stacked rebase** (`internal/git/rebase.go`) ✓
|
||||
|
Reference in New Issue
Block a user