Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,6 @@ jobs:

- name: Run staticcheck
run: staticcheck ./...

- name: Install golangci-lint
run: |
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.61.0

- name: Run golangci-lint
run: golangci-lint run --timeout=5m

test:
name: Run Tests
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ ivaldi timeline switch main
### GitHub Integration

```bash
# Authenticate with GitHub (OAuth)
ivaldi auth login

# Check authentication status
ivaldi auth status

# Connect to GitHub repository
ivaldi portal add owner/repo

Expand Down Expand Up @@ -252,6 +258,9 @@ ivaldi fuse --abort # Abort current merge

### Remote Operations
```bash
ivaldi auth login # Authenticate with GitHub (OAuth)
ivaldi auth status # Check authentication status
ivaldi auth logout # Log out
ivaldi portal add <owner/repo> # Add GitHub connection
ivaldi portal list # List connections
ivaldi download <url> [dir] # Clone repository
Expand Down
151 changes: 151 additions & 0 deletions cli/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/javanhut/Ivaldi-vcs/internal/auth"
"github.com/spf13/cobra"
)

// authCmd represents the auth command
var authCmd = &cobra.Command{
Use: "auth",
Short: "Manage GitHub authentication",
Long: `Authenticate with GitHub to access repositories and perform operations`,
}

// authLoginCmd handles OAuth login
var authLoginCmd = &cobra.Command{
Use: "login",
Short: "Authenticate with GitHub",
Long: `Start the OAuth device flow to authenticate with GitHub and obtain an access token`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()

return auth.Login(ctx)
},
}

// authLogoutCmd handles logout
var authLogoutCmd = &cobra.Command{
Use: "logout",
Short: "Log out of GitHub",
Long: `Remove stored GitHub authentication credentials`,
RunE: func(cmd *cobra.Command, args []string) error {
return auth.Logout()
},
}

// authStatusCmd shows authentication status
var authStatusCmd = &cobra.Command{
Use: "status",
Short: "View authentication status",
Long: `Display current GitHub authentication status and user information`,
RunE: func(cmd *cobra.Command, args []string) error {
// Check authentication method
authMethod := auth.GetAuthMethod()

if authMethod == nil {
fmt.Println("Not authenticated with GitHub")
fmt.Println("\nTo authenticate, run:")
fmt.Println(" ivaldi auth login")
fmt.Println("\nAlternatively, you can:")
fmt.Println(" - Set GITHUB_TOKEN environment variable")
fmt.Println(" - Use 'gh auth login' (GitHub CLI)")
fmt.Println(" - Configure git credentials")
return nil
}

// Display authentication method
fmt.Printf("%s\n", authMethod.Description)

// Test the token by making a request to GitHub
user, err := getAuthenticatedUser(authMethod.Token)
if err != nil {
fmt.Println("\nAuthenticated, but token may be invalid")
fmt.Printf("Error: %v\n", err)

if authMethod.Name == "ivaldi" {
fmt.Println("\nTry logging in again:")
fmt.Println(" ivaldi auth login")
} else if authMethod.Name == "gh-cli" {
fmt.Println("\nTry re-authenticating with GitHub CLI:")
fmt.Println(" gh auth login")
} else {
fmt.Println("\nCheck your authentication credentials or use:")
fmt.Println(" ivaldi auth login")
}
return nil
}

fmt.Printf("\nLogged in to GitHub as: %s\n", user.Login)
if user.Name != "" {
fmt.Printf("Name: %s\n", user.Name)
}
if user.Email != "" {
fmt.Printf("Email: %s\n", user.Email)
}
fmt.Printf("Account type: %s\n", user.Type)

// Show additional info based on auth method
if authMethod.Name != "ivaldi" {
fmt.Println("\nNote: You're using an external authentication method.")
fmt.Println("To use Ivaldi's built-in OAuth, run:")
fmt.Println(" ivaldi auth login")
}

return nil
},
}

// GitHubUser represents a GitHub user
type GitHubUser struct {
Login string `json:"login"`
Name string `json:"name"`
Email string `json:"email"`
Type string `json:"type"`
}

// getAuthenticatedUser fetches the authenticated user's information
func getAuthenticatedUser(token string) (*GitHubUser, error) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, "GET", "https://api.github.com/user", nil)
if err != nil {
return nil, err
}

req.Header.Set("Authorization", fmt.Sprintf("token %s", token))
req.Header.Set("Accept", "application/vnd.github.v3+json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}

var user GitHubUser
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}

return &user, nil
}

func init() {
rootCmd.AddCommand(authCmd)
authCmd.AddCommand(authLoginCmd)
authCmd.AddCommand(authLogoutCmd)
authCmd.AddCommand(authStatusCmd)
}
2 changes: 1 addition & 1 deletion cli/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -594,7 +594,7 @@ var sealCmd = &cobra.Command{
// Check if there are staged files
stageFile := filepath.Join(ivaldiDir, "stage", "files")
if _, err := os.Stat(stageFile); os.IsNotExist(err) {
return fmt.Errorf("no files staged for commit. Use 'ivaldi gather' to stage files first.")
return fmt.Errorf("no files staged for commit. Use 'ivaldi gather' to stage files first")
}

// Read staged files
Expand Down
2 changes: 1 addition & 1 deletion cli/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func (cr *ConflictResolver) showConflictAndGetChoice(conflict diffmerge.ChunkCon

// showChunkPreview shows a preview of chunk content.
func (cr *ConflictResolver) showChunkPreview(data []byte) {
if data == nil || len(data) == 0 {
if len(data) == 0 {
fmt.Println(colors.Dim(" (empty)"))
return
}
Expand Down
4 changes: 2 additions & 2 deletions cli/timeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var createTimelineCmd = &cobra.Command{
defer refsManager.Close()

// Get current timeline to branch from
currentTimeline, err := refsManager.GetCurrentTimeline()
currentTimeline, _ := refsManager.GetCurrentTimeline()
var baseHashes [2][32]byte // blake3 and sha256 hashes
var casStore cas.CAS

Expand Down Expand Up @@ -307,7 +307,7 @@ var removeTimelineCmd = &cobra.Command{
// Check if trying to remove current timeline
currentTimeline, err := refsManager.GetCurrentTimeline()
if err == nil && currentTimeline == name {
return fmt.Errorf("cannot remove current timeline '%s'. Switch to another timeline first.", name)
return fmt.Errorf("cannot remove current timeline '%s'. Switch to another timeline first", name)
}

// Remove timeline file
Expand Down
Loading