From 8776b4549eb18062864f1ed5373708846506f4c2 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 28 Jan 2026 11:22:55 -0800 Subject: [PATCH 1/4] Add a naive attempt at downloading --- .../azure.ai.agents/internal/cmd/init.go | 172 ++++++++++++++---- 1 file changed, 139 insertions(+), 33 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index e44b65fbf94..9688fecb410 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -927,6 +927,15 @@ func (a *InitAction) downloadAgentYaml( } } else if a.isGitHubUrl(manifestPointer) { // Handle GitHub URLs using downloadGithubManifest + // manifestPointer validation: + // - accepts only URLs with the following format: + // - https://raw.///refs/heads///.json + // - This url comes from a user clicking the `raw` button on a file in a GitHub repository (web view). + // - https://///blob///.json + // - This url comes from a user browsing GitHub repository and copy-pasting the url from the browser. + // - https://api./repos///contents//.json + // - This url comes from users familiar with the GitHub API. Usually for programmatic registration of templates. + fmt.Printf("Downloading agent.yaml from GitHub: %s\n", manifestPointer) isGitHubUrl = true @@ -954,20 +963,41 @@ func (a *InitAction) downloadAgentYaml( return nil, "", fmt.Errorf("ensuring gh is installed: %w", err) } - urlInfo, err = a.parseGitHubUrl(ctx, manifestPointer) - if err != nil { - return nil, "", err - } + var contentStr string + // First try naive parsing assuming branch is a single word. The allows users to not have to authenticate + // with gh CLI for public repositories. + urlInfo = a.parseGitHubUrlNaive(manifestPointer) + if urlInfo != nil { + apiPath := fmt.Sprintf("/repos/%s/contents/%s", urlInfo.RepoSlug, urlInfo.FilePath) + if urlInfo.Branch != "" { + fmt.Printf("Downloaded manifest from branch: %s\n", urlInfo.Branch) + apiPath += fmt.Sprintf("?ref=%s", urlInfo.Branch) + } - apiPath := fmt.Sprintf("/repos/%s/contents/%s", urlInfo.RepoSlug, urlInfo.FilePath) - if urlInfo.Branch != "" { - fmt.Printf("Downloaded manifest from branch: %s\n", urlInfo.Branch) - apiPath += fmt.Sprintf("?ref=%s", urlInfo.Branch) + contentStr, err = downloadGithubManifest(ctx, urlInfo, apiPath, ghCli, console) + if err != nil { + fmt.Printf("Warning: naive GitHub URL parsing failed to download manifest: %v\n", err) + fmt.Println("Proceeding with full parsing and download logic...") + } } - contentStr, err := downloadGithubManifest(ctx, urlInfo, apiPath, ghCli, console) - if err != nil { - return nil, "", fmt.Errorf("downloading from GitHub: %w", err) + if contentStr == "" { + // Fall back to complex parsing via azd SDK + urlInfo, err = a.parseGitHubUrl(ctx, manifestPointer) + if err != nil { + return nil, "", err + } + + apiPath := fmt.Sprintf("/repos/%s/contents/%s", urlInfo.RepoSlug, urlInfo.FilePath) + if urlInfo.Branch != "" { + fmt.Printf("Downloaded manifest from branch: %s\n", urlInfo.Branch) + apiPath += fmt.Sprintf("?ref=%s", urlInfo.Branch) + } + + contentStr, err = downloadGithubManifest(ctx, urlInfo, apiPath, ghCli, console) + if err != nil { + return nil, "", fmt.Errorf("downloading from GitHub: %w", err) + } } content = []byte(contentStr) @@ -1342,28 +1372,8 @@ func (a *InitAction) populateContainerSettings(ctx context.Context) (*project.Co func downloadGithubManifest( ctx context.Context, urlInfo *GitHubUrlInfo, apiPath string, ghCli *github.Cli, console input.Console) (string, error) { - // manifestPointer validation: - // - accepts only URLs with the following format: - // - https://raw.///refs/heads///.json - // - This url comes from a user clicking the `raw` button on a file in a GitHub repository (web view). - // - https://///blob///.json - // - This url comes from a user browsing GitHub repository and copy-pasting the url from the browser. - // - https://api./repos///contents//.json - // - This url comes from users familiar with the GitHub API. Usually for programmatic registration of templates. - - authResult, err := ghCli.GetAuthStatus(ctx, urlInfo.Hostname) - if err != nil { - return "", fmt.Errorf("failed to get auth status: %w", err) - } - if !authResult.LoggedIn { - // ensure no spinner is shown when logging in, as this is interactive operation - console.StopSpinner(ctx, "", input.Step) - err := ghCli.Login(ctx, urlInfo.Hostname) - if err != nil { - return "", fmt.Errorf("failed to login: %w", err) - } - console.ShowSpinner(ctx, "Validating template source", input.Step) - } + // This method assumes that either the repo is public, or the user has already been prompted to log in to the github cli + // through our use of the underlying azd logic. content, err := ghCli.ApiCall(ctx, urlInfo.Hostname, apiPath, github.ApiCallOptions{ Headers: []string{"Accept: application/vnd.github.v3.raw"}, @@ -1375,6 +1385,102 @@ func downloadGithubManifest( return content, nil } +// parseGitHubUrlNaive attempts to parse a GitHub URL assuming a simple single-word branch name. +// Returns nil if the URL doesn't match the expected pattern. +// Expected formats: +// - https://github.com/{owner}/{repo}/blob/{branch}/{path} +// - https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}/{path} +func (a *InitAction) parseGitHubUrlNaive(manifestPointer string) *GitHubUrlInfo { + // Try parsing github.com/blob format: https://github.com/{owner}/{repo}/blob/{branch}/{path} + if strings.Contains(manifestPointer, "github.com") && strings.Contains(manifestPointer, "/blob/") { + // Extract hostname + hostname := "github.com" + + // Remove protocol prefix + urlWithoutProtocol := strings.TrimPrefix(manifestPointer, "https://") + urlWithoutProtocol = strings.TrimPrefix(urlWithoutProtocol, "http://") + + // Split by /blob/ + parts := strings.SplitN(urlWithoutProtocol, "/blob/", 2) + if len(parts) != 2 { + return nil + } + + // Extract repo slug (owner/repo) from the first part + repoPath := strings.TrimPrefix(parts[0], hostname+"/") + repoSlug := repoPath + + // The second part is {branch}/{file-path} + branchAndPath := parts[1] + slashIndex := strings.Index(branchAndPath, "/") + if slashIndex == -1 { + return nil + } + + branch := branchAndPath[:slashIndex] + filePath := branchAndPath[slashIndex+1:] + + // Only use naive parsing if branch looks like a simple single word (no slashes) + if strings.Contains(branch, "/") { + return nil + } + + return &GitHubUrlInfo{ + RepoSlug: repoSlug, + Branch: branch, + FilePath: filePath, + Hostname: hostname, + } + } + + // Try parsing raw.githubusercontent.com format: https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}/{path} + if strings.Contains(manifestPointer, "raw.githubusercontent.com") { + hostname := "github.com" // API calls still use github.com + + // Remove protocol prefix + urlWithoutProtocol := strings.TrimPrefix(manifestPointer, "https://") + urlWithoutProtocol = strings.TrimPrefix(urlWithoutProtocol, "http://") + + // Remove raw.githubusercontent.com/ + pathPart := strings.TrimPrefix(urlWithoutProtocol, "raw.githubusercontent.com/") + + // Split path: {owner}/{repo}/refs/heads/{branch}/{file-path} + parts := strings.SplitN(pathPart, "/", 3) // owner, repo, rest + if len(parts) < 3 { + return nil + } + + repoSlug := parts[0] + "/" + parts[1] + rest := parts[2] + + // Check for refs/heads/ prefix + if strings.HasPrefix(rest, "refs/heads/") { + rest = strings.TrimPrefix(rest, "refs/heads/") + slashIndex := strings.Index(rest, "/") + if slashIndex == -1 { + return nil + } + + branch := rest[:slashIndex] + filePath := rest[slashIndex+1:] + + // Only use naive parsing if branch looks like a simple single word + if strings.Contains(branch, "/") { + return nil + } + + return &GitHubUrlInfo{ + RepoSlug: repoSlug, + Branch: branch, + FilePath: filePath, + Hostname: hostname, + } + } + } + + return nil +} + // parseGitHubUrl extracts repository information from various GitHub URL formats using extension framework func (a *InitAction) parseGitHubUrl(ctx context.Context, manifestPointer string) (*GitHubUrlInfo, error) { urlInfo, err := a.azdClient.Project().ParseGitHubUrl(ctx, &azdext.ParseGitHubUrlRequest{ From 3234ac12da7038556dbca59375816689061c6bd2 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 28 Jan 2026 15:30:02 -0800 Subject: [PATCH 2/4] Address PR comments Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/init.go | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 9688fecb410..b92f8ebdcd8 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -9,6 +9,8 @@ import ( "encoding/json" "errors" "fmt" + "io" + "net/http" "net/url" "os" "path/filepath" @@ -964,19 +966,25 @@ func (a *InitAction) downloadAgentYaml( } var contentStr string - // First try naive parsing assuming branch is a single word. The allows users to not have to authenticate + // First try naive parsing assuming branch is a single word. This allows users to not have to authenticate // with gh CLI for public repositories. urlInfo = a.parseGitHubUrlNaive(manifestPointer) if urlInfo != nil { - apiPath := fmt.Sprintf("/repos/%s/contents/%s", urlInfo.RepoSlug, urlInfo.FilePath) - if urlInfo.Branch != "" { - fmt.Printf("Downloaded manifest from branch: %s\n", urlInfo.Branch) - apiPath += fmt.Sprintf("?ref=%s", urlInfo.Branch) + // Construct raw GitHub URL to fetch file directly + rawUrl := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s", urlInfo.RepoSlug, urlInfo.Branch, urlInfo.FilePath) + fmt.Printf("Attempting to download manifest from: %s\n", rawUrl) + + resp, err := http.Get(rawUrl) + if err == nil && resp.StatusCode == http.StatusOK { + defer resp.Body.Close() + bodyBytes, readErr := io.ReadAll(resp.Body) + if readErr == nil { + contentStr = string(bodyBytes) + fmt.Printf("Downloaded manifest from branch: %s\n", urlInfo.Branch) + } } - - contentStr, err = downloadGithubManifest(ctx, urlInfo, apiPath, ghCli, console) - if err != nil { - fmt.Printf("Warning: naive GitHub URL parsing failed to download manifest: %v\n", err) + if contentStr == "" { + fmt.Printf("Warning: naive GitHub URL parsing failed to download manifest\n") fmt.Println("Proceeding with full parsing and download logic...") } } @@ -994,7 +1002,7 @@ func (a *InitAction) downloadAgentYaml( apiPath += fmt.Sprintf("?ref=%s", urlInfo.Branch) } - contentStr, err = downloadGithubManifest(ctx, urlInfo, apiPath, ghCli, console) + contentStr, err = downloadGithubManifest(ctx, urlInfo, apiPath, ghCli) if err != nil { return nil, "", fmt.Errorf("downloading from GitHub: %w", err) } @@ -1371,7 +1379,7 @@ func (a *InitAction) populateContainerSettings(ctx context.Context) (*project.Co } func downloadGithubManifest( - ctx context.Context, urlInfo *GitHubUrlInfo, apiPath string, ghCli *github.Cli, console input.Console) (string, error) { + ctx context.Context, urlInfo *GitHubUrlInfo, apiPath string, ghCli *github.Cli) (string, error) { // This method assumes that either the repo is public, or the user has already been prompted to log in to the github cli // through our use of the underlying azd logic. From ba5b5cceec184a7a430c794ad1c04e42aa5eb254 Mon Sep 17 00:00:00 2001 From: trangevi Date: Wed, 28 Jan 2026 17:23:56 -0800 Subject: [PATCH 3/4] Update download directory as well Signed-off-by: trangevi --- .../azure.ai.agents/internal/cmd/init.go | 109 +++++++++++++++++- 1 file changed, 104 insertions(+), 5 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index b92f8ebdcd8..38e03a59c17 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -877,6 +877,7 @@ func (a *InitAction) downloadAgentYaml( var urlInfo *GitHubUrlInfo var ghCli *github.Cli var console input.Console + var useGhCli bool = false // Check if manifestPointer is a local file path or a URI if a.isLocalFilePath(manifestPointer) { @@ -990,7 +991,8 @@ func (a *InitAction) downloadAgentYaml( } if contentStr == "" { - // Fall back to complex parsing via azd SDK + // Fall back to complex parsing via azd GitHub CLI handling + useGhCli = true urlInfo, err = a.parseGitHubUrl(ctx, manifestPointer) if err != nil { return nil, "", err @@ -1154,7 +1156,7 @@ func (a *InitAction) downloadAgentYaml( if isHostedContainer { // For container agents, download the entire parent directory fmt.Println("Downloading full directory for container agent") - err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console) + err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console, useGhCli) if err != nil { return nil, "", fmt.Errorf("downloading parent directory: %w", err) } @@ -1507,7 +1509,7 @@ func (a *InitAction) parseGitHubUrl(ctx context.Context, manifestPointer string) } func downloadParentDirectory( - ctx context.Context, urlInfo *GitHubUrlInfo, targetDir string, ghCli *github.Cli, console input.Console) error { + ctx context.Context, urlInfo *GitHubUrlInfo, targetDir string, ghCli *github.Cli, console input.Console, useGhCli bool) error { // Get parent directory by removing the filename from the file path pathParts := strings.Split(urlInfo.FilePath, "/") @@ -1520,8 +1522,14 @@ func downloadParentDirectory( fmt.Printf("Downloading parent directory '%s' from repository '%s', branch '%s'\n", parentDirPath, urlInfo.RepoSlug, urlInfo.Branch) // Download directory contents - if err := downloadDirectoryContents(ctx, urlInfo.Hostname, urlInfo.RepoSlug, parentDirPath, urlInfo.Branch, targetDir, ghCli, console); err != nil { - return fmt.Errorf("failed to download directory contents: %w", err) + if useGhCli { + if err := downloadDirectoryContents(ctx, urlInfo.Hostname, urlInfo.RepoSlug, parentDirPath, urlInfo.Branch, targetDir, ghCli, console); err != nil { + return fmt.Errorf("failed to download directory contents: %w", err) + } + } else { + if err := downloadDirectoryContentsWithoutGhCli(ctx, urlInfo.RepoSlug, parentDirPath, urlInfo.Branch, targetDir); err != nil { + return fmt.Errorf("failed to download directory contents: %w", err) + } } fmt.Printf("Successfully downloaded parent directory to: %s\n", targetDir) @@ -1598,6 +1606,97 @@ func downloadDirectoryContents( return nil } +func downloadDirectoryContentsWithoutGhCli( + ctx context.Context, repoSlug string, dirPath string, branch string, localPath string) error { + + // Get directory contents using GitHub API directly + apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/contents/%s", repoSlug, dirPath) + if branch != "" { + apiUrl += fmt.Sprintf("?ref=%s", branch) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiUrl, nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Accept", "application/vnd.github.v3+json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("failed to get directory contents: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to get directory contents: status %d", resp.StatusCode) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read directory contents response: %w", err) + } + + // Parse the directory contents JSON + var dirContents []map[string]interface{} + if err := json.Unmarshal(body, &dirContents); err != nil { + return fmt.Errorf("failed to parse directory contents JSON: %w", err) + } + + // Download each file and subdirectory + for _, item := range dirContents { + name, ok := item["name"].(string) + if !ok { + continue + } + + itemType, ok := item["type"].(string) + if !ok { + continue + } + + itemPath := fmt.Sprintf("%s/%s", dirPath, name) + itemLocalPath := filepath.Join(localPath, name) + + if itemType == "file" { + // Download file using raw.githubusercontent.com + fmt.Printf("Downloading file: %s\n", itemPath) + rawUrl := fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s", repoSlug, branch, itemPath) + + fileResp, err := http.Get(rawUrl) + if err != nil { + return fmt.Errorf("failed to download file %s: %w", itemPath, err) + } + defer fileResp.Body.Close() + + if fileResp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download file %s: status %d", itemPath, fileResp.StatusCode) + } + + fileContent, err := io.ReadAll(fileResp.Body) + if err != nil { + return fmt.Errorf("failed to read file content %s: %w", itemPath, err) + } + + if err := os.WriteFile(itemLocalPath, fileContent, 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", itemLocalPath, err) + } + } else if itemType == "dir" { + // Recursively download subdirectory + fmt.Printf("Downloading directory: %s\n", itemPath) + if err := os.MkdirAll(itemLocalPath, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", itemLocalPath, err) + } + + // Recursively download directory contents + if err := downloadDirectoryContentsWithoutGhCli(ctx, repoSlug, itemPath, branch, itemLocalPath); err != nil { + return fmt.Errorf("failed to download subdirectory %s: %w", itemPath, err) + } + } + } + + return nil +} + // func (a *InitAction) validateResources(ctx context.Context, agentYaml map[string]interface{}) error { // fmt.Println("Reading model name from agent.yaml...") From 63b959ed165f4a0a69e0480f85c8dcad04fd54e6 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 29 Jan 2026 09:53:03 -0800 Subject: [PATCH 4/4] Use net/url parsing in parseGitHubUrlNaive to handle fragments and query params (#6634) * Initial plan * Use net/url parsing to handle query params and fragments in parseGitHubUrlNaive Co-authored-by: trangevi <26490000+trangevi@users.noreply.github.com> * Final review completed Co-authored-by: trangevi <26490000+trangevi@users.noreply.github.com> * Revert unintended changes to other extensions Co-authored-by: trangevi <26490000+trangevi@users.noreply.github.com> * Address PR comments Signed-off-by: trangevi * Update download directory as well Signed-off-by: trangevi * Initial plan * Rebased on latest base branch --------- Signed-off-by: trangevi Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: trangevi <26490000+trangevi@users.noreply.github.com> Co-authored-by: trangevi --- .../azure.ai.agents/internal/cmd/init.go | 27 ++-- .../azure.ai.agents/internal/cmd/init_test.go | 125 ++++++++++++++++++ 2 files changed, 137 insertions(+), 15 deletions(-) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index 38e03a59c17..d680e6f8fd0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -1401,23 +1401,24 @@ func downloadGithubManifest( // - https://github.com/{owner}/{repo}/blob/{branch}/{path} // - https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}/{path} func (a *InitAction) parseGitHubUrlNaive(manifestPointer string) *GitHubUrlInfo { + // Parse URL to properly handle query parameters and fragments + parsedURL, err := url.Parse(manifestPointer) + if err != nil { + return nil + } + // Try parsing github.com/blob format: https://github.com/{owner}/{repo}/blob/{branch}/{path} - if strings.Contains(manifestPointer, "github.com") && strings.Contains(manifestPointer, "/blob/") { - // Extract hostname + if parsedURL.Host == "github.com" && strings.Contains(parsedURL.Path, "/blob/") { hostname := "github.com" - // Remove protocol prefix - urlWithoutProtocol := strings.TrimPrefix(manifestPointer, "https://") - urlWithoutProtocol = strings.TrimPrefix(urlWithoutProtocol, "http://") - // Split by /blob/ - parts := strings.SplitN(urlWithoutProtocol, "/blob/", 2) + parts := strings.SplitN(parsedURL.Path, "/blob/", 2) if len(parts) != 2 { return nil } // Extract repo slug (owner/repo) from the first part - repoPath := strings.TrimPrefix(parts[0], hostname+"/") + repoPath := strings.TrimPrefix(parts[0], "/") repoSlug := repoPath // The second part is {branch}/{file-path} @@ -1444,15 +1445,11 @@ func (a *InitAction) parseGitHubUrlNaive(manifestPointer string) *GitHubUrlInfo } // Try parsing raw.githubusercontent.com format: https://raw.githubusercontent.com/{owner}/{repo}/refs/heads/{branch}/{path} - if strings.Contains(manifestPointer, "raw.githubusercontent.com") { + if parsedURL.Host == "raw.githubusercontent.com" { hostname := "github.com" // API calls still use github.com - // Remove protocol prefix - urlWithoutProtocol := strings.TrimPrefix(manifestPointer, "https://") - urlWithoutProtocol = strings.TrimPrefix(urlWithoutProtocol, "http://") - - // Remove raw.githubusercontent.com/ - pathPart := strings.TrimPrefix(urlWithoutProtocol, "raw.githubusercontent.com/") + // Remove leading slash from path + pathPart := strings.TrimPrefix(parsedURL.Path, "/") // Split path: {owner}/{repo}/refs/heads/{branch}/{file-path} parts := strings.SplitN(pathPart, "/", 3) // owner, repo, rest diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index 9f27f74cff5..3305b663dda 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -196,3 +196,128 @@ func TestFormatDirectoryPreview(t *testing.T) { }) } } + +func TestParseGitHubUrlNaive(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + url string + expected *GitHubUrlInfo + }{ + { + name: "github.com blob URL", + url: "https://github.com/owner/repo/blob/main/path/to/file.yaml", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "main", + FilePath: "path/to/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "github.com blob URL with fragment", + url: "https://github.com/owner/repo/blob/main/path/to/file.yaml#L10", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "main", + FilePath: "path/to/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "github.com blob URL with query parameter", + url: "https://github.com/owner/repo/blob/main/path/to/file.yaml?plain=1", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "main", + FilePath: "path/to/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "github.com blob URL with both fragment and query", + url: "https://github.com/owner/repo/blob/develop/path/file.yaml?plain=1#L20-L30", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "develop", + FilePath: "path/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "raw.githubusercontent.com URL", + url: "https://raw.githubusercontent.com/owner/repo/refs/heads/main/path/to/file.yaml", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "main", + FilePath: "path/to/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "raw.githubusercontent.com URL with query parameter", + url: "https://raw.githubusercontent.com/owner/repo/refs/heads/main/path/to/file.yaml?token=abc123", + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "main", + FilePath: "path/to/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "URL with branch containing slash (naive parsing treats first part as branch)", + url: "https://github.com/owner/repo/blob/feature/my-branch/file.yaml", + // This is a known limitation - the naive parser will incorrectly treat "feature" as the branch + // and "my-branch/file.yaml" as the file path. This is acceptable since the function is designed + // to handle simple cases and fall back to full parsing for complex branch names. + expected: &GitHubUrlInfo{ + RepoSlug: "owner/repo", + Branch: "feature", + FilePath: "my-branch/file.yaml", + Hostname: "github.com", + }, + }, + { + name: "invalid URL", + url: "not a url", + expected: nil, + }, + { + name: "non-github URL", + url: "https://gitlab.com/owner/repo/blob/main/file.yaml", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &InitAction{} + result := a.parseGitHubUrlNaive(tt.url) + + if tt.expected == nil { + if result != nil { + t.Errorf("expected nil, got %+v", result) + } + return + } + + if result == nil { + t.Fatalf("expected non-nil result, got nil") + } + + if result.RepoSlug != tt.expected.RepoSlug { + t.Errorf("RepoSlug = %q, want %q", result.RepoSlug, tt.expected.RepoSlug) + } + if result.Branch != tt.expected.Branch { + t.Errorf("Branch = %q, want %q", result.Branch, tt.expected.Branch) + } + if result.FilePath != tt.expected.FilePath { + t.Errorf("FilePath = %q, want %q", result.FilePath, tt.expected.FilePath) + } + if result.Hostname != tt.expected.Hostname { + t.Errorf("Hostname = %q, want %q", result.Hostname, tt.expected.Hostname) + } + }) + } +}