summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-06-23 23:38:55 +0300
committerPaul Buetow <paul@buetow.org>2025-06-23 23:38:55 +0300
commit42d3df9fe18663f5c6f7067b964f0b09071910e6 (patch)
treefff4b4708115caa29c4daadc1444e02ab14c41b7
parent006724744a943aad877a92406a5e2b4d5d12acd3 (diff)
Add debugging features and improve error handling
- Add --test-github-token flag to validate GitHub authentication - Improve error messages for 401 authentication failures - Add merge conflict detection before sync attempts - Stop sync on first error for easier debugging - Add GitHub repo creation support for --sync and --sync-all commands - Add detailed token loading debug output - Create test script for GitHub token validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
-rw-r--r--cmd/gitsyncer/main.go31
-rw-r--r--internal/github/github.go25
-rw-r--r--internal/sync/sync.go42
-rwxr-xr-xtest/test_github_token.sh86
4 files changed, 182 insertions, 2 deletions
diff --git a/cmd/gitsyncer/main.go b/cmd/gitsyncer/main.go
index b60fda0..6e5ecac 100644
--- a/cmd/gitsyncer/main.go
+++ b/cmd/gitsyncer/main.go
@@ -6,6 +6,7 @@ import (
"log"
"os"
"path/filepath"
+ "strings"
"github.com/paul/gitsyncer/internal/codeberg"
"github.com/paul/gitsyncer/internal/config"
@@ -26,6 +27,7 @@ func main() {
createGitHubRepos bool
dryRun bool
workDir string
+ testGitHubToken bool
)
// Define command line flags
@@ -41,6 +43,7 @@ func main() {
flag.BoolVar(&createGitHubRepos, "create-github-repos", false, "automatically create missing GitHub repositories")
flag.BoolVar(&dryRun, "dry-run", false, "show what would be synced without actually syncing")
flag.StringVar(&workDir, "work-dir", ".gitsyncer-work", "working directory for cloning repositories")
+ flag.BoolVar(&testGitHubToken, "test-github-token", false, "test GitHub token authentication")
flag.Parse()
// Handle version flag
@@ -48,6 +51,33 @@ func main() {
fmt.Println(version.GetVersion())
os.Exit(0)
}
+
+ // Handle test GitHub token flag
+ if testGitHubToken {
+ fmt.Println("Testing GitHub token authentication...")
+ client := github.NewClient("", "snonux") // Empty token to trigger loading from env/file
+ if !client.HasToken() {
+ fmt.Println("ERROR: No GitHub token found!")
+ fmt.Println("Please set GITHUB_TOKEN environment variable or create ~/.gitsyncer_github_token file")
+ os.Exit(1)
+ }
+
+ // Test the token by checking a known repo
+ exists, err := client.RepoExists("gitsyncer")
+ if err != nil {
+ fmt.Printf("ERROR: Token test failed: %v\n", err)
+ if strings.Contains(err.Error(), "401") {
+ fmt.Println("\nThe token is invalid or expired. Please check:")
+ fmt.Println("1. Token has not expired")
+ fmt.Println("2. Token has 'repo' scope")
+ fmt.Println("3. Token was not revoked")
+ }
+ os.Exit(1)
+ }
+
+ fmt.Printf("SUCCESS: Token is valid! Repository check returned: %v\n", exists)
+ os.Exit(0)
+ }
// Determine config file path
if configPath == "" {
@@ -337,6 +367,7 @@ func main() {
fmt.Println(" gitsyncer --sync-codeberg-public Sync all public Codeberg repositories")
fmt.Println(" gitsyncer --list-orgs List configured organizations")
fmt.Println(" gitsyncer --list-repos List configured repositories")
+ fmt.Println(" gitsyncer --test-github-token Test GitHub token authentication")
fmt.Println(" gitsyncer --version Show version information")
fmt.Println("\nOptions:")
fmt.Println(" --config <path> Path to configuration file")
diff --git a/internal/github/github.go b/internal/github/github.go
index c0d3ffc..38aafa0 100644
--- a/internal/github/github.go
+++ b/internal/github/github.go
@@ -4,6 +4,7 @@ import (
"bytes"
"encoding/json"
"fmt"
+ "io"
"net/http"
"os"
"path/filepath"
@@ -20,20 +21,36 @@ type Client struct {
func NewClient(token, org string) *Client {
// If no token provided, try other sources
if token == "" {
+ fmt.Println(" No token in config, trying environment variable...")
// Try environment variable
token = os.Getenv("GITHUB_TOKEN")
// If still no token, try reading from file
if token == "" {
+ fmt.Println(" No GITHUB_TOKEN env var, trying ~/.gitsyncer_github_token file...")
home, err := os.UserHomeDir()
if err == nil {
tokenFile := filepath.Join(home, ".gitsyncer_github_token")
data, err := os.ReadFile(tokenFile)
if err == nil {
token = strings.TrimSpace(string(data))
+ fmt.Printf(" Loaded token from file (length: %d)\n", len(token))
+ // Check for common issues
+ if strings.Contains(token, "\n") || strings.Contains(token, "\r") {
+ fmt.Println(" Warning: Token contains newline characters")
+ }
+ if strings.HasPrefix(token, " ") || strings.HasSuffix(token, " ") {
+ fmt.Println(" Warning: Token has leading/trailing spaces")
+ }
+ } else {
+ fmt.Printf(" Could not read token file: %v\n", err)
}
}
+ } else {
+ fmt.Printf(" Loaded token from env var (length: %d)\n", len(token))
}
+ } else {
+ fmt.Printf(" Using token from config (length: %d)\n", len(token))
}
return &Client{
token: token,
@@ -76,6 +93,8 @@ func (c *Client) RepoExists(repoName string) (bool, error) {
}
url := fmt.Sprintf("https://api.github.com/repos/%s/%s", c.org, repoName)
+ fmt.Printf(" Checking URL: %s\n", url)
+ fmt.Printf(" Token present: %v (length: %d)\n", c.token != "", len(c.token))
req, err := http.NewRequest("GET", url, nil)
if err != nil {
@@ -95,6 +114,12 @@ func (c *Client) RepoExists(repoName string) (bool, error) {
return true, nil
} else if resp.StatusCode == 404 {
return false, nil
+ } else if resp.StatusCode == 401 {
+ // Read the response body for 401 errors
+ body, _ := io.ReadAll(resp.Body)
+ fmt.Printf(" 401 Unauthorized - Response: %s\n", string(body))
+ fmt.Printf(" Authorization header: %s\n", req.Header.Get("Authorization"))
+ return false, fmt.Errorf("authentication failed (401): %s", string(body))
}
return false, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
diff --git a/internal/sync/sync.go b/internal/sync/sync.go
index 7f9b81e..a22089d 100644
--- a/internal/sync/sync.go
+++ b/internal/sync/sync.go
@@ -250,6 +250,35 @@ func (s *Syncer) getAllBranches() ([]string, error) {
// syncBranch synchronizes a specific branch across all remotes
func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organization) error {
+ // First check if we have unresolved merge conflicts
+ cmd := exec.Command("git", "status", "--porcelain")
+ output, err := cmd.Output()
+ if err == nil && len(output) > 0 {
+ // Check for merge conflicts
+ statusStr := string(output)
+ if strings.Contains(statusStr, "UU ") || strings.Contains(statusStr, "AA ") || strings.Contains(statusStr, "DD ") {
+ // Get the repo name from the work directory
+ repoName := filepath.Base(s.workDir)
+ if repoName == ".gitsyncer-work" || repoName == "" {
+ // If we're in the work directory itself, extract from current directory
+ cmd := exec.Command("git", "rev-parse", "--show-toplevel")
+ if output, err := cmd.Output(); err == nil {
+ repoName = filepath.Base(strings.TrimSpace(string(output)))
+ }
+ }
+ return fmt.Errorf("repository has unresolved merge conflicts - please resolve manually or delete %s", s.workDir)
+ }
+ // If we have uncommitted changes but no conflicts, try to stash them
+ fmt.Println(" Stashing uncommitted changes...")
+ if err := exec.Command("git", "stash", "push", "-m", "gitsyncer-auto-stash").Run(); err != nil {
+ return fmt.Errorf("failed to stash changes: %w", err)
+ }
+ defer func() {
+ // Try to pop the stash at the end
+ exec.Command("git", "stash", "pop").Run()
+ }()
+ }
+
// Create or checkout the branch
if err := s.checkoutBranch(branch); err != nil {
return fmt.Errorf("failed to checkout branch %s: %w", branch, err)
@@ -333,9 +362,14 @@ func (s *Syncer) syncBranch(branch string, remotes map[string]*config.Organizati
func (s *Syncer) checkoutBranch(branch string) error {
// First try to checkout existing branch
cmd := exec.Command("git", "checkout", branch)
- if err := cmd.Run(); err == nil {
+ output, err := cmd.CombinedOutput()
+ if err == nil {
return nil
}
+
+ // If checkout failed, check the error
+ outputStr := string(output)
+ fmt.Printf(" Initial checkout failed: %s\n", strings.TrimSpace(outputStr))
// If that fails, create a new branch tracking the first remote that has it
for i := range s.config.Organizations {
@@ -344,7 +378,11 @@ func (s *Syncer) checkoutBranch(branch string) error {
if s.remoteBranchExists(remoteName, branch) {
cmd = exec.Command("git", "checkout", "-b", branch, fmt.Sprintf("%s/%s", remoteName, branch))
- return cmd.Run()
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("failed to create tracking branch: %s", string(output))
+ }
+ return nil
}
}
diff --git a/test/test_github_token.sh b/test/test_github_token.sh
new file mode 100755
index 0000000..3078f6f
--- /dev/null
+++ b/test/test_github_token.sh
@@ -0,0 +1,86 @@
+#!/bin/bash
+
+# Test script to validate GitHub token
+set -e
+
+echo "Testing GitHub token authentication..."
+
+# Try to load token from different sources
+TOKEN=""
+
+# 1. Environment variable
+if [ -n "$GITHUB_TOKEN" ]; then
+ echo "Found GITHUB_TOKEN environment variable"
+ TOKEN="$GITHUB_TOKEN"
+fi
+
+# 2. Token file
+if [ -z "$TOKEN" ] && [ -f ~/.gitsyncer_github_token ]; then
+ echo "Found ~/.gitsyncer_github_token file"
+ TOKEN=$(cat ~/.gitsyncer_github_token | tr -d '\n\r ')
+fi
+
+if [ -z "$TOKEN" ]; then
+ echo "ERROR: No GitHub token found!"
+ echo "Please set GITHUB_TOKEN environment variable or create ~/.gitsyncer_github_token file"
+ exit 1
+fi
+
+echo "Token length: ${#TOKEN}"
+echo "Token prefix: ${TOKEN:0:10}..."
+
+# Test the token
+echo ""
+echo "Testing token with GitHub API..."
+RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Accept: application/vnd.github.v3+json" \
+ https://api.github.com/user)
+
+HTTP_STATUS=$(echo "$RESPONSE" | grep HTTP_STATUS | cut -d: -f2)
+BODY=$(echo "$RESPONSE" | grep -v HTTP_STATUS)
+
+echo "HTTP Status: $HTTP_STATUS"
+
+if [ "$HTTP_STATUS" = "200" ]; then
+ echo "SUCCESS: Token is valid!"
+ echo "Authenticated as: $(echo "$BODY" | jq -r .login)"
+elif [ "$HTTP_STATUS" = "401" ]; then
+ echo "ERROR: Token is invalid (401 Unauthorized)"
+ echo "Response: $BODY"
+ echo ""
+ echo "Common issues:"
+ echo "1. Token has expired"
+ echo "2. Token doesn't have required scopes (need 'repo' scope)"
+ echo "3. Token was revoked"
+ echo ""
+ echo "To create a new token:"
+ echo "1. Go to https://github.com/settings/tokens"
+ echo "2. Click 'Generate new token (classic)'"
+ echo "3. Select 'repo' scope"
+ echo "4. Save the token to ~/.gitsyncer_github_token"
+else
+ echo "ERROR: Unexpected status code: $HTTP_STATUS"
+ echo "Response: $BODY"
+fi
+
+# Test specific repository access
+echo ""
+echo "Testing access to snonux/dtail repository..."
+REPO_RESPONSE=$(curl -s -w "\nHTTP_STATUS:%{http_code}" \
+ -H "Authorization: Bearer $TOKEN" \
+ -H "Accept: application/vnd.github.v3+json" \
+ https://api.github.com/repos/snonux/dtail)
+
+REPO_STATUS=$(echo "$REPO_RESPONSE" | grep HTTP_STATUS | cut -d: -f2)
+REPO_BODY=$(echo "$REPO_RESPONSE" | grep -v HTTP_STATUS)
+
+echo "Repository check status: $REPO_STATUS"
+if [ "$REPO_STATUS" = "200" ]; then
+ echo "SUCCESS: Can access repository"
+elif [ "$REPO_STATUS" = "404" ]; then
+ echo "Repository does not exist"
+elif [ "$REPO_STATUS" = "401" ]; then
+ echo "ERROR: Authentication failed for repository"
+ echo "Response: $REPO_BODY"
+fi \ No newline at end of file