Compare commits

..

1 Commits

Author SHA1 Message Date
agent-company 4e7b072f82 feat: add GetChangedFiles() method to Gitea client for PR file diffs
Add ChangedFile type and GetChangedFiles() method that calls the Gitea
API endpoint GET /repos/{owner}/{repo}/pulls/{index}/files to retrieve
the list of files changed in a pull request. This is a prerequisite for
displaying changed files in the PR detail view (#189).

Includes unit tests for success and error cases.

Closes leeworks-agents/gitea-mobile#205

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-20 15:06:10 +00:00
3 changed files with 90 additions and 34 deletions
+3 -34
View File
@@ -1,14 +1,10 @@
package main package main
import ( import (
"context"
"log" "log"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"os/signal"
"syscall"
"time"
"gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config" "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/config"
giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea" giteaclient "gitea.leeworks.dev/0xwheatyz/gitea-mobile/internal/gitea"
@@ -40,35 +36,8 @@ func main() {
handler = middleware.Auth(cfg.SessionSecret, cfg.GiteaToken)(handler) handler = middleware.Auth(cfg.SessionSecret, cfg.GiteaToken)(handler)
handler = middleware.Logging()(handler) handler = middleware.Logging()(handler)
srv := &http.Server{ slog.Info("server starting", "addr", cfg.ListenAddr, "gitea_url", cfg.GiteaURL)
Addr: cfg.ListenAddr, if err := http.ListenAndServe(cfg.ListenAddr, handler); err != nil {
Handler: handler, log.Fatalf("server error: %v", err)
} }
// Channel to receive shutdown signals.
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
// Start server in a goroutine.
go func() {
slog.Info("server starting", "addr", cfg.ListenAddr, "gitea_url", cfg.GiteaURL)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Block until a shutdown signal is received.
sig := <-quit
slog.Info("shutdown signal received, draining in-flight requests", "signal", sig.String())
// Give in-flight requests up to 15 seconds to complete.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("server forced to shutdown", "error", err)
os.Exit(1)
}
slog.Info("server stopped gracefully")
} }
+27
View File
@@ -760,6 +760,33 @@ func (c *Client) GetPull(ctx context.Context, token, owner, repo string, index i
return &pr, nil return &pr, nil
} }
// ChangedFile represents a file changed in a pull request.
type ChangedFile struct {
Filename string `json:"filename"`
Status string `json:"status"` // "added", "modified", "removed", "renamed"
Additions int `json:"additions"`
Deletions int `json:"deletions"`
Changes int `json:"changes"`
PreviousFilename string `json:"previous_filename,omitempty"`
}
// GetChangedFiles fetches the list of files changed in a pull request.
func (c *Client) GetChangedFiles(ctx context.Context, token, owner, repo string, index int64) ([]ChangedFile, error) {
path := fmt.Sprintf("/repos/%s/%s/pulls/%d/files?limit=50", owner, repo, index)
resp, err := c.doRequest(ctx, token, http.MethodGet, path, nil)
if err != nil {
return nil, fmt.Errorf("fetching changed files: %w", err)
}
defer resp.Body.Close()
var files []ChangedFile
if err := json.NewDecoder(resp.Body).Decode(&files); err != nil {
return nil, fmt.Errorf("decoding changed files: %w", err)
}
return files, nil
}
// GetIssueComments fetches comments for an issue or pull request. // GetIssueComments fetches comments for an issue or pull request.
func (c *Client) GetIssueComments(ctx context.Context, token, owner, repo string, index int64) ([]Comment, error) { func (c *Client) GetIssueComments(ctx context.Context, token, owner, repo string, index int64) ([]Comment, error) {
path := fmt.Sprintf("/repos/%s/%s/issues/%d/comments?limit=50", owner, repo, index) path := fmt.Sprintf("/repos/%s/%s/issues/%d/comments?limit=50", owner, repo, index)
+60
View File
@@ -1456,3 +1456,63 @@ func TestRetryDelay_ExponentialBackoff(t *testing.T) {
} }
} }
} }
func TestGetChangedFiles(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("expected GET, got %s", r.Method)
}
if r.URL.Path != "/api/v1/repos/owner1/repo1/pulls/5/files" {
t.Errorf("unexpected path: %s", r.URL.Path)
}
if r.Header.Get("Authorization") != "token test-token" {
t.Error("missing or wrong Authorization header")
}
files := []ChangedFile{
{Filename: "main.go", Status: "modified", Additions: 10, Deletions: 3, Changes: 13},
{Filename: "new_file.go", Status: "added", Additions: 25, Deletions: 0, Changes: 25},
{Filename: "old_file.go", Status: "removed", Additions: 0, Deletions: 15, Changes: 15},
}
json.NewEncoder(w).Encode(files)
}))
defer server.Close()
c := NewClient(server.URL)
files, err := c.GetChangedFiles(context.Background(), "test-token", "owner1", "repo1", 5)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(files) != 3 {
t.Fatalf("got %d files, want 3", len(files))
}
if files[0].Filename != "main.go" {
t.Errorf("files[0].Filename = %q, want %q", files[0].Filename, "main.go")
}
if files[0].Status != "modified" {
t.Errorf("files[0].Status = %q, want %q", files[0].Status, "modified")
}
if files[1].Status != "added" {
t.Errorf("files[1].Status = %q, want %q", files[1].Status, "added")
}
if files[2].Status != "removed" {
t.Errorf("files[2].Status = %q, want %q", files[2].Status, "removed")
}
}
func TestGetChangedFiles_Error(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, `{"message":"pull request not found"}`)
}))
defer server.Close()
c := NewClient(server.URL)
_, err := c.GetChangedFiles(context.Background(), "test-token", "owner1", "repo1", 999)
if err == nil {
t.Fatal("expected error for 404 response, got nil")
}
if !strings.Contains(err.Error(), "404") {
t.Errorf("error should contain status code 404, got: %v", err)
}
}