diff --git a/knotserver/git/git.go b/knotserver/git/git.go index 71e73e4e..c419a62e 100644 --- a/knotserver/git/git.go +++ b/knotserver/git/git.go @@ -13,6 +13,7 @@ import ( "time" "github.com/go-git/go-git/v5" + gogit "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/config" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" @@ -281,6 +282,27 @@ func (g *GitRepo) FindMainBranch() (string, error) { return strings.TrimSpace(string(output)), nil } +func (g *GitRepo) Remote() (string, error) { + remote, err := g.r.Remote("origin") + if errors.Is(err, gogit.ErrRemoteNotFound) { + return "", nil + } + if err != nil { + return "", err + } + + if remote == nil { + return "", nil + } + + urls := remote.Config().URLs + if len(urls) == 0 { + return "", nil + } + + return urls[0], nil +} + // WriteTar writes itself from a tree into a binary tar file format. // prefix is root folder to be appended. func (g *GitRepo) WriteTar(w io.Writer, prefix string) error { diff --git a/knotserver/internal.go b/knotserver/internal.go index b7205274..2fc669a6 100644 --- a/knotserver/internal.go +++ b/knotserver/internal.go @@ -8,6 +8,7 @@ import ( "net/http" "net/url" "os" + "path" "path/filepath" "strings" @@ -460,6 +461,11 @@ func (h *InternalHandle) emitPullRequestLink( return err } + remote, err := gr.Remote() + if err != nil { + return fmt.Errorf("checking for upstream remote: %w", err) + } + defaultBranch, err := gr.FindMainBranch() if err != nil { return err @@ -478,16 +484,10 @@ func (h *InternalHandle) emitPullRequestLink( user = userIdent.Handle.String() } - query := url.Values{} - query.Set("source", "branch") - query.Set("sourceBranch", pushedBranch) - query.Set("targetBranch", defaultBranch) - - basePath, err := url.JoinPath(h.c.AppViewEndpoint, user, repoName, "pulls", "new") + pullURL, err := h.createPullURL(h.c.AppViewEndpoint, remote, user, ownerDid, repoName, pushedBranch, defaultBranch) if err != nil { return err } - pullURL := basePath + "?" + query.Encode() ZWS := "\u200B" *clientMsgs = append(*clientMsgs, ZWS) @@ -497,6 +497,66 @@ func (h *InternalHandle) emitPullRequestLink( return nil } +func (h *InternalHandle) createPullURL(appviewURL, remote, user, ownerDID, repoName, pushedBranch, defaultBranch string) (string, error) { + if remote != "" { + return h.createForkPullURL(appviewURL, remote, ownerDID, repoName, pushedBranch, defaultBranch) + } + + query := url.Values{} + + query.Set("source", "branch") + query.Set("sourceBranch", pushedBranch) + query.Set("targetBranch", defaultBranch) + + basePath, err := url.JoinPath(appviewURL, user, repoName, "pulls", "new") + if err != nil { + return "", err + } + pullURL := basePath + "?" + query.Encode() + return pullURL, nil +} + +func (h *InternalHandle) createForkPullURL(appviewURL, remote, ownerDID, repoName, pushedBranch, defaultBranch string) (string, error) { + query := url.Values{} + + query.Set("fork", fmt.Sprintf("%s/%s", ownerDID, repoName)) + query.Set("source", "fork") + query.Set("sourceBranch", pushedBranch) + query.Set("targetBranch", defaultBranch) + + repoPath, err := h.getRemoteOwnerRepoNamePath(remote) + if err != nil { + return "", err + } + + basePath, err := url.JoinPath(appviewURL, repoPath, "pulls", "new") + if err != nil { + return "", err + } + pullURL := basePath + "?" + query.Encode() + return pullURL, nil +} + +func (h *InternalHandle) getRemoteOwnerRepoNamePath(remote string) (string, error) { + u, err := url.Parse(remote) + if err != nil { + return "", fmt.Errorf("invalid remote: %w", err) + } + + if u.Scheme != "file" { + return u.Path, nil + } + + repoDid := path.Base(u.String()) + + owner, name, err := h.db.GetRepoKeyOwner(repoDid) + if err != nil { + return "", err + } + + return fmt.Sprintf("%s/%s", owner, name), nil +} + func Internal(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, n *notifier.Notifier, res *idresolver.Resolver) http.Handler { r := chi.NewRouter() l := log.FromContext(ctx) diff --git a/knotserver/internal_test.go b/knotserver/internal_test.go new file mode 100644 index 00000000..47fd1467 --- /dev/null +++ b/knotserver/internal_test.go @@ -0,0 +1,59 @@ +package knotserver + +import ( + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/stretchr/testify/require" + "tangled.org/core/knotserver/db" +) + +const ( + appviewURL = "https://tangled.org/" + user = "willdot.net" + userDID = "did:plc:dadhhalkfcq3gucaq25hjqon" + pushedBranch = "feature-abc" + defaultBranch = "main" +) + +func TestCreatePullURL(t *testing.T) { + + tt := map[string]struct { + repoName string + remote string + expectedURL string + }{ + "not a fork": { + repoName: "knot-testing", + remote: "", + expectedURL: "https://tangled.org/willdot.net/knot-testing/pulls/new?source=branch&sourceBranch=feature-abc&targetBranch=main", + }, + "is fork": { + repoName: "knot-testing-fork", + remote: "https://knot1.tangled.sh/did:plc:dadhhalkfcq3gucaq25hjqon/knot-testing", + expectedURL: "https://tangled.org/did:plc:dadhhalkfcq3gucaq25hjqon/knot-testing/pulls/new?fork=did%3Aplc%3Adadhhalkfcq3gucaq25hjqon%2Fknot-testing-fork&source=fork&sourceBranch=feature-abc&targetBranch=main", + }, + "is fork on same knot": { + repoName: "knot-testing-fork", + remote: "file:///home/git/repositories/did:plc:ixran6dpypl5lslliiqceshs", + expectedURL: "https://tangled.org/did:plc:dadhhalkfcq3gucaq25hjqon/knot-testing/pulls/new?fork=did%3Aplc%3Adadhhalkfcq3gucaq25hjqon%2Fknot-testing-fork&source=fork&sourceBranch=feature-abc&targetBranch=main", + }, + } + + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + database, err := db.Setup(t.Context(), ":memory:") + require.NoError(t, err) + err = database.StoreRepoKey("did:plc:ixran6dpypl5lslliiqceshs", []byte{}, "did:plc:dadhhalkfcq3gucaq25hjqon", "knot-testing", "at://uri") + require.NoError(t, err) + + h := InternalHandle{ + db: database, + } + res, err := h.createPullURL(appviewURL, tc.remote, user, userDID, tc.repoName, pushedBranch, defaultBranch) + require.NoError(t, err) + + assert.Equal(t, tc.expectedURL, res) + }) + } +}