diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index 6eeb7b1c99a..bb1e2d3a283 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "os/exec" "path/filepath" "strings" @@ -18,115 +17,11 @@ import ( var compileOrchestratorLog = logger.New("cli:compile_orchestrator") -// getRepositorySlug extracts the repository slug (owner/repo) from git config -func getRepositorySlug() string { - // Try to get from git remote URL - cmd := exec.Command("git", "config", "--get", "remote.origin.url") - output, err := cmd.Output() - if err != nil { - return "" - } - - url := strings.TrimSpace(string(output)) - - // Parse GitHub URL patterns: - // - https://github.com/owner/repo.git - // - git@github.com:owner/repo.git - // - https://github.com/owner/repo - - // Remove .git suffix - url = strings.TrimSuffix(url, ".git") - - // Extract owner/repo from URL - if strings.HasPrefix(url, "https://github.com/") { - slug := strings.TrimPrefix(url, "https://github.com/") - return slug - } else if strings.HasPrefix(url, "git@github.com:") { - slug := strings.TrimPrefix(url, "git@github.com:") - return slug - } - - return "" -} - -// getRepositorySlugForPath extracts the repository slug (owner/repo) from the git config -// of the repository containing the specified file path -func getRepositorySlugForPath(path string) string { - // Get absolute path first - absPath, err := filepath.Abs(path) - if err != nil { - return "" - } - - // Use the directory containing the file - dir := filepath.Dir(absPath) - - // Try to get from git remote URL in the file's repository - cmd := exec.Command("git", "-C", dir, "config", "--get", "remote.origin.url") - output, err := cmd.Output() - if err != nil { - return "" - } - - url := strings.TrimSpace(string(output)) - - // Parse GitHub URL patterns: - // - https://github.com/owner/repo.git - // - git@github.com:owner/repo.git - // - https://github.com/owner/repo - - // Remove .git suffix - url = strings.TrimSuffix(url, ".git") - - // Extract owner/repo from URL - if strings.HasPrefix(url, "https://github.com/") { - slug := strings.TrimPrefix(url, "https://github.com/") - return slug - } else if strings.HasPrefix(url, "git@github.com:") { - slug := strings.TrimPrefix(url, "git@github.com:") - return slug - } - - return "" -} - -// getRepositoryRoot returns the absolute path to the git repository root -// It looks for the git repository containing the current directory -func getRepositoryRoot() (string, error) { - cmd := exec.Command("git", "rev-parse", "--show-toplevel") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get repository root: %w", err) - } - return strings.TrimSpace(string(output)), nil -} - -// getRepositoryRootForPath returns the absolute path to the git repository root -// containing the specified file path -func getRepositoryRootForPath(path string) (string, error) { - // Get absolute path first - absPath, err := filepath.Abs(path) - if err != nil { - return "", fmt.Errorf("failed to get absolute path: %w", err) - } - - // Use the directory containing the file - dir := filepath.Dir(absPath) - - // Run git command in the file's directory - cmd := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel") - output, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("failed to get repository root for path %s: %w", path, err) - } - return strings.TrimSpace(string(output)), nil -} - // getRepositoryRelativePath converts an absolute file path to a repository-relative path // This ensures stable workflow identifiers regardless of where the repository is cloned func getRepositoryRelativePath(absPath string) (string, error) { // Get the repository root for the specific file - repoRoot, err := getRepositoryRootForPath(absPath) + repoRoot, err := findGitRootForPath(absPath) if err != nil { // If we can't get the repo root, just use the basename as fallback compileOrchestratorLog.Printf("Warning: could not get repository root for %s: %v, using basename", absPath, err) @@ -324,7 +219,7 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) { compileOrchestratorLog.Print("Created compiler instance") // Set repository slug for schedule scattering - repoSlug := getRepositorySlug() + repoSlug := getRepositorySlugFromRemote() if repoSlug != "" { compiler.SetRepositorySlug(repoSlug) compileOrchestratorLog.Printf("Repository slug set: %s", repoSlug) @@ -532,7 +427,7 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) { compiler.SetWorkflowIdentifier(relPath) // Set repository slug for this specific file (may differ from CWD's repo) - fileRepoSlug := getRepositorySlugForPath(resolvedFile) + fileRepoSlug := getRepositorySlugFromRemoteForPath(resolvedFile) if fileRepoSlug != "" { compiler.SetRepositorySlug(fileRepoSlug) compileOrchestratorLog.Printf("Repository slug for file set: %s", fileRepoSlug) @@ -869,7 +764,7 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) { compiler.SetWorkflowIdentifier(relPath) // Set repository slug for this specific file (may differ from CWD's repo) - fileRepoSlug := getRepositorySlugForPath(file) + fileRepoSlug := getRepositorySlugFromRemoteForPath(file) if fileRepoSlug != "" { compiler.SetRepositorySlug(fileRepoSlug) compileOrchestratorLog.Printf("Repository slug for file set: %s", fileRepoSlug) diff --git a/pkg/cli/compile_orchestrator_stability_test.go b/pkg/cli/compile_orchestrator_stability_test.go index 3f0305e22d3..0f4afb3e970 100644 --- a/pkg/cli/compile_orchestrator_stability_test.go +++ b/pkg/cli/compile_orchestrator_stability_test.go @@ -9,7 +9,7 @@ import ( // TestGetRepositoryRelativePath tests that paths are correctly converted to repository-relative paths func TestGetRepositoryRelativePath(t *testing.T) { // Get the actual repository root - repoRoot, err := getRepositoryRoot() + repoRoot, err := findGitRoot() if err != nil { t.Skipf("Skipping test: not in a git repository: %v", err) } @@ -54,7 +54,7 @@ func TestGetRepositoryRelativePath(t *testing.T) { // produces the same relative path regardless of how it's constructed func TestGetRepositoryRelativePathConsistency(t *testing.T) { // Get the actual repository root - repoRoot, err := getRepositoryRoot() + repoRoot, err := findGitRoot() if err != nil { t.Skipf("Skipping test: not in a git repository: %v", err) } @@ -111,7 +111,7 @@ func TestGetRepositoryRelativePathConsistency(t *testing.T) { // is normalized to forward slashes for cross-platform stability func TestGetRepositoryRelativePathCrossPlatform(t *testing.T) { // Get the actual repository root - repoRoot, err := getRepositoryRoot() + repoRoot, err := findGitRoot() if err != nil { t.Skipf("Skipping test: not in a git repository: %v", err) } diff --git a/pkg/cli/compile_validation.go b/pkg/cli/compile_validation.go index 18e0aae1f05..094fa1eed46 100644 --- a/pkg/cli/compile_validation.go +++ b/pkg/cli/compile_validation.go @@ -27,7 +27,7 @@ func CompileWorkflowWithValidation(compiler *workflow.Compiler, filePath string, compiler.SetWorkflowIdentifier(relPath) // Set repository slug for this specific file (may differ from CWD's repo) - fileRepoSlug := getRepositorySlugForPath(filePath) + fileRepoSlug := getRepositorySlugFromRemoteForPath(filePath) if fileRepoSlug != "" { compiler.SetRepositorySlug(fileRepoSlug) compileValidationLog.Printf("Repository slug for file set: %s", fileRepoSlug) diff --git a/pkg/cli/devcontainer.go b/pkg/cli/devcontainer.go index 80b64a90079..feff32b22f9 100644 --- a/pkg/cli/devcontainer.go +++ b/pkg/cli/devcontainer.go @@ -4,7 +4,6 @@ import ( "encoding/json" "fmt" "os" - "os/exec" "path/filepath" "strings" @@ -240,33 +239,28 @@ func ensureDevcontainerConfig(verbose bool, additionalRepos []string) error { // getCurrentRepoName gets the current repository name from git remote in owner/repo format func getCurrentRepoName() string { - // Try to get the repository name from git remote - cmd := exec.Command("git", "config", "--get", "remote.origin.url") - output, err := cmd.Output() - if err != nil { - // Fallback to directory name - gitRoot, err := findGitRoot() - if err != nil { - return "" - } - return filepath.Base(gitRoot) + // Try to get the repository name from git remote using centralized helper + slug := getRepositorySlugFromRemote() + if slug != "" { + return slug } - remoteURL := strings.TrimSpace(string(output)) - return parseGitHubRepoFromURL(remoteURL) + // Fallback to directory name + gitRoot, err := findGitRoot() + if err != nil { + return "" + } + return filepath.Base(gitRoot) } // getRepoOwner extracts the owner from the git remote URL func getRepoOwner() string { - cmd := exec.Command("git", "config", "--get", "remote.origin.url") - output, err := cmd.Output() - if err != nil { + // Use centralized helper to get full repo slug + fullRepo := getRepositorySlugFromRemote() + if fullRepo == "" { return "" } - remoteURL := strings.TrimSpace(string(output)) - fullRepo := parseGitHubRepoFromURL(remoteURL) - // Extract owner from "owner/repo" format parts := strings.Split(fullRepo, "/") if len(parts) >= 1 { @@ -274,27 +268,3 @@ func getRepoOwner() string { } return "" } - -// parseGitHubRepoFromURL extracts owner/repo from a GitHub URL -func parseGitHubRepoFromURL(url string) string { - // Remove .git suffix if present - url = strings.TrimSuffix(url, ".git") - - // Handle HTTPS URLs: https://github.com/owner/repo - if strings.Contains(url, "github.com/") { - parts := strings.Split(url, "github.com/") - if len(parts) == 2 { - return parts[1] - } - } - - // Handle SSH URLs: git@github.com:owner/repo - if strings.Contains(url, "git@github.com:") { - parts := strings.Split(url, "git@github.com:") - if len(parts) == 2 { - return parts[1] - } - } - - return "" -} diff --git a/pkg/cli/git.go b/pkg/cli/git.go index e23919d0e90..d68e28e7747 100644 --- a/pkg/cli/git.go +++ b/pkg/cli/git.go @@ -31,6 +31,111 @@ func findGitRoot() (string, error) { return gitRoot, nil } +// findGitRootForPath finds the root directory of the git repository containing the specified path +func findGitRootForPath(path string) (string, error) { + gitLog.Printf("Finding git root for path: %s", path) + + // Get absolute path first + absPath, err := filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + // Use the directory containing the file + dir := filepath.Dir(absPath) + + // Run git command in the file's directory + cmd := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get repository root for path %s: %w", path, err) + } + gitRoot := strings.TrimSpace(string(output)) + gitLog.Printf("Found git root for path: %s", gitRoot) + return gitRoot, nil +} + +// parseGitHubRepoSlugFromURL extracts owner/repo from a GitHub URL +// Supports both HTTPS (https://github.com/owner/repo) and SSH (git@github.com:owner/repo) formats +func parseGitHubRepoSlugFromURL(url string) string { + gitLog.Printf("Parsing GitHub repo slug from URL: %s", url) + + // Remove .git suffix if present + url = strings.TrimSuffix(url, ".git") + + // Handle HTTPS URLs: https://github.com/owner/repo + if strings.HasPrefix(url, "https://github.com/") { + slug := strings.TrimPrefix(url, "https://github.com/") + gitLog.Printf("Extracted slug from HTTPS URL: %s", slug) + return slug + } + + // Handle SSH URLs: git@github.com:owner/repo + if strings.HasPrefix(url, "git@github.com:") { + slug := strings.TrimPrefix(url, "git@github.com:") + gitLog.Printf("Extracted slug from SSH URL: %s", slug) + return slug + } + + gitLog.Print("Could not extract slug from URL") + return "" +} + +// getRepositorySlugFromRemote extracts the repository slug (owner/repo) from git remote URL +func getRepositorySlugFromRemote() string { + gitLog.Print("Getting repository slug from git remote") + + // Try to get from git remote URL + cmd := exec.Command("git", "config", "--get", "remote.origin.url") + output, err := cmd.Output() + if err != nil { + gitLog.Printf("Failed to get remote URL: %v", err) + return "" + } + + url := strings.TrimSpace(string(output)) + slug := parseGitHubRepoSlugFromURL(url) + + if slug != "" { + gitLog.Printf("Repository slug: %s", slug) + } + + return slug +} + +// getRepositorySlugFromRemoteForPath extracts the repository slug (owner/repo) from the git remote URL +// of the repository containing the specified file path +func getRepositorySlugFromRemoteForPath(path string) string { + gitLog.Printf("Getting repository slug for path: %s", path) + + // Get absolute path first + absPath, err := filepath.Abs(path) + if err != nil { + gitLog.Printf("Failed to get absolute path: %v", err) + return "" + } + + // Use the directory containing the file + dir := filepath.Dir(absPath) + + // Try to get from git remote URL in the file's repository + cmd := exec.Command("git", "-C", dir, "config", "--get", "remote.origin.url") + output, err := cmd.Output() + if err != nil { + gitLog.Printf("Failed to get remote URL for path: %v", err) + return "" + } + + url := strings.TrimSpace(string(output)) + slug := parseGitHubRepoSlugFromURL(url) + + if slug != "" { + gitLog.Printf("Repository slug for path: %s", slug) + } + + return slug +} + func stageWorkflowChanges() { // Find git root and add .github/workflows relative to it if gitRoot, err := findGitRoot(); err == nil { diff --git a/pkg/cli/git_helpers_test.go b/pkg/cli/git_helpers_test.go new file mode 100644 index 00000000000..a97172de11f --- /dev/null +++ b/pkg/cli/git_helpers_test.go @@ -0,0 +1,100 @@ +package cli + +import ( + "testing" +) + +func TestParseGitHubRepoSlugFromURL(t *testing.T) { + tests := []struct { + name string + url string + expected string + }{ + { + name: "HTTPS URL with .git", + url: "https://github.com/githubnext/gh-aw.git", + expected: "githubnext/gh-aw", + }, + { + name: "HTTPS URL without .git", + url: "https://github.com/githubnext/gh-aw", + expected: "githubnext/gh-aw", + }, + { + name: "SSH URL with .git", + url: "git@github.com:githubnext/gh-aw.git", + expected: "githubnext/gh-aw", + }, + { + name: "SSH URL without .git", + url: "git@github.com:githubnext/gh-aw", + expected: "githubnext/gh-aw", + }, + { + name: "Invalid URL", + url: "not-a-github-url", + expected: "", + }, + { + name: "Empty URL", + url: "", + expected: "", + }, + { + name: "URL with subdirectory", + url: "https://github.com/owner/repo/subfolder", + expected: "owner/repo/subfolder", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := parseGitHubRepoSlugFromURL(tt.url) + if result != tt.expected { + t.Errorf("parseGitHubRepoSlugFromURL(%q) = %q, expected %q", tt.url, result, tt.expected) + } + }) + } +} + +func TestGetRepositorySlugFromRemote(t *testing.T) { + // This test verifies that the function can execute without errors in a git repo + // The actual value will depend on the repository being tested + result := getRepositorySlugFromRemote() + + // In the gh-aw repository, we should get a non-empty slug + // But we can't assert the exact value since it might change + if result != "" { + t.Logf("Repository slug: %s", result) + } else { + t.Log("Repository slug is empty (may be expected if not in a git repo)") + } +} + +func TestFindGitRootForPath(t *testing.T) { + // Test with current file path + gitRoot, err := findGitRootForPath("git_helpers_test.go") + if err != nil { + // This is okay if we're not in a git repository + t.Logf("findGitRootForPath returned error: %v", err) + return + } + + if gitRoot == "" { + t.Error("findGitRootForPath returned empty string without error") + } else { + t.Logf("Git root: %s", gitRoot) + } +} + +func TestGetRepositorySlugFromRemoteForPath(t *testing.T) { + // Test with current file path + slug := getRepositorySlugFromRemoteForPath("git_helpers_test.go") + + // Log the result - we can't assert exact value + if slug != "" { + t.Logf("Repository slug for path: %s", slug) + } else { + t.Log("Repository slug for path is empty (may be expected if not in a git repo)") + } +}