Skip to content
Draft
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
66 changes: 63 additions & 3 deletions pkg/provider/gitea/gitea.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"path"
"regexp"
"strconv"
Expand Down Expand Up @@ -149,9 +150,68 @@ func (v *Provider) SetPacInfo(pacInfo *info.PacOpts) {
v.pacInfo = pacInfo
}

// GetTaskURI TODO: Implement ME.
func (v *Provider) GetTaskURI(_ context.Context, _ *info.Event, _ string) (bool, string, error) {
return false, "", nil
// splitGiteaURL parses a Gitea/Forgejo URL and returns org, repo, path, and ref.
func splitGiteaURL(uri string) (string, string, string, string, error) {
pURL, err := url.Parse(uri)
if err != nil {
return "", "", "", "", fmt.Errorf("URL %s is not a valid provider URL: %w", uri, err)
}
uriPath := pURL.Path
if pURL.RawPath != "" {
uriPath = pURL.RawPath
}
Comment on lines +159 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using pURL.Path for splitting is problematic because it is already unescaped by url.Parse. If a path segment (such as a branch name or a directory) contains an encoded slash (%2F), pURL.Path will contain a literal /, causing strings.Split to break the segment incorrectly. Using pURL.EscapedPath() ensures you split only on the intended delimiters.

Suggested change
uriPath := pURL.Path
if pURL.RawPath != "" {
uriPath = pURL.RawPath
}
uriPath := pURL.EscapedPath()

split := strings.Split(uriPath, "/")
// minimum: /owner/repo/{src|raw}/{branch|tag|commit}/ref/filepath β†’ 7 segments (split[0] is empty)
if len(split) < 7 {
return "", "", "", "", fmt.Errorf("URL %s does not seem to be a proper Gitea URL: not enough path segments", uri)
}

spOrg := split[1]
spRepo := split[2]
action := split[3] // "src" or "raw"
refType := split[4] // "branch", "tag", or "commit"

if action != "src" && action != "raw" {
return "", "", "", "", fmt.Errorf("cannot recognize URL as a Gitea URL to fetch: %s (expected 'src' or 'raw' in path)", uri)
}
if refType != "branch" && refType != "tag" && refType != "commit" {
return "", "", "", "", fmt.Errorf("cannot recognize URL as a Gitea URL to fetch: %s (expected 'branch', 'tag', or 'commit' in path)", uri)
}

spRef := split[5]
spPath := strings.Join(split[6:], "/")

if spRef, err = url.QueryUnescape(spRef); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode ref: %w", err)
}
if spPath, err = url.QueryUnescape(spPath); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode path: %w", err)
}
if spOrg, err = url.QueryUnescape(spOrg); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode org: %w", err)
}
if spRepo, err = url.QueryUnescape(spRepo); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode repo: %w", err)
}
Comment on lines +184 to +195
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When decoding path segments, url.PathUnescape should be used instead of url.QueryUnescape. QueryUnescape incorrectly treats + as a space, which is not the case for URL path segments. Additionally, since the path is now extracted using EscapedPath(), all segments must be explicitly unescaped to restore their original values.

Suggested change
if spRef, err = url.QueryUnescape(spRef); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode ref: %w", err)
}
if spPath, err = url.QueryUnescape(spPath); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode path: %w", err)
}
if spOrg, err = url.QueryUnescape(spOrg); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode org: %w", err)
}
if spRepo, err = url.QueryUnescape(spRepo); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode repo: %w", err)
}
if spRef, err = url.PathUnescape(spRef); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode ref: %w", err)
}
if spPath, err = url.PathUnescape(spPath); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode path: %w", err)
}
if spOrg, err = url.PathUnescape(spOrg); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode org: %w", err)
}
if spRepo, err = url.PathUnescape(spRepo); err != nil {
return "", "", "", "", fmt.Errorf("cannot decode repo: %w", err)
}


return spOrg, spRepo, spPath, spRef, nil
}

func (v *Provider) GetTaskURI(_ context.Context, event *info.Event, uri string) (bool, string, error) {
if ret := provider.CompareHostOfURLS(uri, event.URL); !ret {
return false, "", nil
}

spOrg, spRepo, spPath, spRef, err := splitGiteaURL(uri)
if err != nil {
return false, "", err
}

data, _, err := v.Client().GetFile(spOrg, spRepo, spRef, spPath)
if err != nil {
return false, "", err
}
return true, string(data), nil
Comment on lines +210 to +214
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The v.Client() call returns the underlying Gitea client. If the client has not been initialized, this call will return nil, leading to a panic. According to the repository's rules on lazy initialization, the Client() method should be implemented to return a new instance directly if the internal field is nil, rather than returning nil and requiring the caller to handle the error. This avoids potential panics and ensures thread safety for stateless or lightweight objects.

Suggested change
data, _, err := v.Client().GetFile(spOrg, spRepo, spRef, spPath)
if err != nil {
return false, "", err
}
return true, string(data), nil
client := v.Client()
if client == nil {
return false, "", fmt.Errorf("no gitea client has been initialized")
}
data, _, err := client.GetFile(spOrg, spRepo, spRef, spPath)
if err != nil {
return false, "", err
}
return true, string(data), nil
References
  1. Avoid lazy initialization of stateless and lightweight objects without proper synchronization. Instead, return a new instance directly if the field is nil.

}

func (v *Provider) SetLogger(logger *zap.SugaredLogger) {
Expand Down
180 changes: 180 additions & 0 deletions pkg/provider/gitea/gitea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,186 @@ func TestGetCommitInfoPRLookupPopulatesURLs(t *testing.T) {
assert.Equal(t, "https://gitea.com/owner/repo", event.BaseURL, "BaseURL should be populated from PR lookup")
}

func TestSplitGiteaURL(t *testing.T) {
tests := []struct {
name string
url string
wantOrg string
wantRepo string
wantRef string
wantPath string
wantErr bool
}{
{
name: "src branch URL",
url: "https://gitea.example.com/owner/repo/src/branch/main/path/to/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "main",
wantPath: "path/to/task.yaml",
},
{
name: "raw branch URL",
url: "https://gitea.example.com/owner/repo/raw/branch/main/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "main",
wantPath: "task.yaml",
},
{
name: "src tag URL",
url: "https://gitea.example.com/owner/repo/src/tag/v1.0.0/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "v1.0.0",
wantPath: "task.yaml",
},
{
name: "src commit URL",
url: "https://gitea.example.com/owner/repo/src/commit/abc123def/path/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "abc123def",
wantPath: "path/task.yaml",
},
{
name: "URL encoded branch name",
url: "https://gitea.example.com/owner/repo/src/branch/feature%2Fbranch/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "feature/branch",
wantPath: "task.yaml",
},
{
name: "raw commit URL",
url: "https://gitea.example.com/owner/repo/raw/commit/abc123def/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "abc123def",
wantPath: "task.yaml",
},
{
name: "raw tag URL",
url: "https://gitea.example.com/owner/repo/raw/tag/v2.0/path/to/task.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "v2.0",
wantPath: "path/to/task.yaml",
},
{
name: "URL encoded path",
url: "https://gitea.example.com/owner/repo/src/branch/main/path%2Fto%2Ftask.yaml",
wantOrg: "owner",
wantRepo: "repo",
wantRef: "main",
wantPath: "path/to/task.yaml",
},
{
name: "too short URL",
url: "https://gitea.example.com/owner/repo",
wantErr: true,
},
{
name: "invalid action segment",
url: "https://gitea.example.com/owner/repo/blob/branch/main/task.yaml",
wantErr: true,
},
{
name: "invalid ref type",
url: "https://gitea.example.com/owner/repo/src/invalid/main/task.yaml",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
org, repo, path, ref, err := splitGiteaURL(tt.url)
if (err != nil) != tt.wantErr {
t.Errorf("splitGiteaURL() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.wantOrg, org)
assert.Equal(t, tt.wantRepo, repo)
assert.Equal(t, tt.wantRef, ref)
assert.Equal(t, tt.wantPath, path)
})
}
}

func TestGetTaskURI(t *testing.T) {
tests := []struct {
name string
eventURL string
uri string
wantRet string
wantFound bool
wantErr bool
fileExists bool
}{
{
name: "fetch task from src branch URL",
eventURL: "https://gitea.example.com/owner/repo/pulls/1",
uri: "https://gitea.example.com/owner/repo/src/branch/main/task.yaml",
wantRet: "hello world",
wantFound: true,
fileExists: true,
},
{
name: "fetch task from raw branch URL",
eventURL: "https://gitea.example.com/owner/repo/pulls/1",
uri: "https://gitea.example.com/owner/repo/raw/branch/main/task.yaml",
wantRet: "hello world",
wantFound: true,
fileExists: true,
},
{
name: "fetch task from tag URL",
eventURL: "https://gitea.example.com/owner/repo/pulls/1",
uri: "https://gitea.example.com/owner/repo/src/tag/v1.0/task.yaml",
wantRet: "hello world",
wantFound: true,
fileExists: true,
},
{
name: "different host returns not found",
eventURL: "https://gitea.example.com/owner/repo/pulls/1",
uri: "https://other.example.com/owner/repo/src/branch/main/task.yaml",
wantFound: false,
},
{
name: "bad URI format",
eventURL: "https://gitea.example.com/owner/repo/pulls/1",
uri: "https://gitea.example.com/owner/repo",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
fakeclient, mux, teardown := tgitea.Setup(t)
defer teardown()

if tt.fileExists {
mux.HandleFunc("/repos/owner/repo/raw/", func(rw http.ResponseWriter, _ *http.Request) {
fmt.Fprint(rw, tt.wantRet)
})
}

p := &Provider{giteaClient: fakeclient}
event := info.NewEvent()
event.URL = tt.eventURL

found, content, err := p.GetTaskURI(context.Background(), event, tt.uri)
if (err != nil) != tt.wantErr {
t.Errorf("GetTaskURI() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.wantFound, found)
if tt.wantFound {
assert.Equal(t, tt.wantRet, content)
}
})
}
}

func TestGetCommitStatuses(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading