Skip to content
Open
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
48 changes: 48 additions & 0 deletions internal/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,51 @@ func (c *Client) ClosePullRequest(ctx context.Context, number int) error {
})
return err
}

// Gist represents a created GitHub gist.
type Gist struct {
// ID is the guid of the gist.
ID string
// Owner is the GitHub user name that owns the gist.
Owner string
// Url is the HTML Url of the created gist.
Url string
}

// CreateGist creates a new gist with one or more files.
func (c *Client) CreateGist(ctx context.Context, contents map[string]string, isPublic bool) (*Gist, error) {
var files = make(map[github.GistFilename]github.GistFile, len(contents))
for filename, content := range contents {
files[github.GistFilename(filename)] = github.GistFile{
Content: github.Ptr(content),
}
}
gist, _, err := c.Gists.Create(ctx, &github.Gist{
Files: files,
Public: github.Ptr(isPublic),
})
if err != nil {
return nil, err
}
return &Gist{
ID: gist.GetID(),
Owner: gist.GetOwner().GetName(),
Url: gist.GetHTMLURL(),
}, nil
}

// GetGistContent fetches all the file contents for a specific gist.
func (c *Client) GetGistContent(ctx context.Context, gistID string) (map[string]string, error) {
gist, _, err := c.Gists.Get(ctx, gistID)
if err != nil {
return nil, err
}

files := gist.GetFiles()
var contents = make(map[string]string, len(files))
for _, file := range files {
contents[file.GetFilename()] = file.GetContent()
}

return contents, nil
}
2 changes: 2 additions & 0 deletions internal/librarian/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ type GitHubClient interface {
CreateRelease(ctx context.Context, tagName, name, body, commitish string) (*github.RepositoryRelease, error)
CreateIssueComment(ctx context.Context, number int, comment string) error
CreateTag(ctx context.Context, tag, commitish string) error
CreateGist(ctx context.Context, contents map[string]string, isPublic bool) (*github.Gist, error)
GetGistContent(ctx context.Context, gistID string) (map[string]string, error)
}

// ContainerClient is an abstraction over the Docker client.
Expand Down
16 changes: 16 additions & 0 deletions internal/librarian/mocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type mockGitHubClient struct {
createReleaseCalls int
createIssueCalls int
createTagCalls int
createGistCalls int
getGistContentCalls int
createPullRequestErr error
addLabelsToIssuesErr error
getLabelsErr error
Expand All @@ -51,7 +53,11 @@ type mockGitHubClient struct {
createReleaseErr error
createIssueErr error
createTagErr error
createGistError error
getGistError error
createdPR *github.PullRequestMetadata
createdGist *github.Gist
getGistContent map[string]string
labels []string
pullRequests []*github.PullRequest
pullRequest *github.PullRequest
Expand Down Expand Up @@ -115,6 +121,16 @@ func (m *mockGitHubClient) CreateTag(ctx context.Context, tagName, commitish str
return m.createTagErr
}

func (m *mockGitHubClient) CreateGist(ctx context.Context, contents map[string]string, isPublic bool) (*github.Gist, error) {
m.createGistCalls++
return m.createdGist, m.createGistError
}

func (m *mockGitHubClient) GetGistContent(ctx context.Context, gistID string) (map[string]string, error) {
m.getGistContentCalls++
return m.getGistContent, m.getGistError
}

// mockContainerClient is a mock implementation of the ContainerClient interface for testing.
type mockContainerClient struct {
ContainerClient
Expand Down
77 changes: 77 additions & 0 deletions internal/librarian/pull_request_serializer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package librarian

import (
"context"
"fmt"
"log/slog"
"regexp"
)

// GistOverflowHandler handles storing overflow content in a gist.
type GistOverflowHandler struct {
github GitHubClient
maxContentSize int
}

const maxPullRequestBodySize = 65536

var overflowPullRequestRegex = regexp.MustCompile(`See full release notes at: https://gist.github.com/[^\/]+/([0-9a-f]+)`)

// NewGistOverflowHandler returns a handler for storing overflow content in a gist.
func NewGistOverflowHandler(github GitHubClient, maxContentSize int) (*GistOverflowHandler, error) {
if maxContentSize == 0 {
maxContentSize = maxPullRequestBodySize
}
return &GistOverflowHandler{
github: github,
maxContentSize: maxContentSize,
}, nil
}

// SavePullRequestBody stores content in a gist if it's too big and returns a minimized version.
func (g *GistOverflowHandler) SavePullRequestBody(ctx context.Context, body string) (string, error) {
if len(body) > g.maxContentSize {
slog.Info("content is too big, saving to gist", slog.Int("len", len(body)))
contents := map[string]string{
"release-notes.md": body,
}
gist, err := g.github.CreateGist(ctx, contents, true)
if err != nil {
return "", err
}
return fmt.Sprintf("See full release notes at: %s", gist.Url), nil
}
return body, nil
}

// FetchPullRequestBody restores content from a stroed gist.
func (g *GistOverflowHandler) FetchPullRequestBody(ctx context.Context, body string) (string, error) {
matches := overflowPullRequestRegex.FindStringSubmatch(body)
if len(matches) == 2 {
slog.Info("found gist in pull request body", "gistID", matches[1])
contents, err := g.github.GetGistContent(ctx, matches[1])
if err != nil {
return "", err
}
content, found := contents["release-notes.md"]
if found {
return content, nil
}
return "", fmt.Errorf("unable to find release-notes.md from gist %s", matches[1])
}
return body, nil
}
Loading