[gitiles] Add DownloadFile function

This change adds a helper function in gitiles.go which can be used to
download a file directly from Gitiles without checking out the full
repo.

Bug: 70425
Change-Id: I673edea581b5078e5b2198fb5b9621bea2a0ccea
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/infra/+/525604
Commit-Queue: Anthony Fandrianto <atyfto@google.com>
Fuchsia-Auto-Submit: Anthony Fandrianto <atyfto@google.com>
Reviewed-by: Oliver Newman <olivernewman@google.com>
diff --git a/gitiles/gitiles.go b/gitiles/gitiles.go
index b3cf600..630cc77 100644
--- a/gitiles/gitiles.go
+++ b/gitiles/gitiles.go
@@ -29,7 +29,7 @@
 	return &gitilesClientWrapper{client: client, project: project}, nil
 }
 
-// LatestCommit returns the revision at refs/heads/master HEAD.
+// LatestCommit resolves a ref to a revision.
 func (c *gitilesClientWrapper) LatestCommit(ctx context.Context, ref string) (string, error) {
 	log, err := c.Log(ctx, ref, 1)
 	if err != nil {
@@ -53,3 +53,17 @@
 	}
 	return resp.Log, nil
 }
+
+// DownloadFile returns the contents of the file at `path` and `ref`.
+func (c *gitilesClientWrapper) DownloadFile(ctx context.Context, path, ref string) (string, error) {
+	resp, err := c.client.DownloadFile(ctx, &gitilespb.DownloadFileRequest{
+		Project:    c.project,
+		Committish: ref,
+		Path:       path,
+		Format:     gitilespb.DownloadFileRequest_TEXT,
+	})
+	if err != nil {
+		return "", fmt.Errorf("failed to download %s: %v", path, err)
+	}
+	return resp.Contents, nil
+}
diff --git a/gitiles/gitiles_test.go b/gitiles/gitiles_test.go
index 57bf740..5a2ff3e 100644
--- a/gitiles/gitiles_test.go
+++ b/gitiles/gitiles_test.go
@@ -48,10 +48,10 @@
 		ctx := context.Background()
 		commit, err := mockGitilesClientWrapper.LatestCommit(ctx, "refs/heads/main")
 		if err != nil {
-			t.Errorf("got unexpected err %v", err)
+			t.Fatal(err)
 		}
 		if test.expected != commit {
-			t.Errorf("expected: %s\nactual: %s\n", test.expected, commit)
+			t.Fatalf("expected: %s\nactual: %s\n", test.expected, commit)
 		}
 	}
 }
@@ -98,7 +98,7 @@
 		ctx := context.Background()
 		log, err := mockGitilesClientWrapper.Log(ctx, test.expected[0].Id, int32(len(test.expected)))
 		if err != nil {
-			t.Errorf("got unexpected err %v", err)
+			t.Fatal(err)
 		}
 		if diff := cmp.Diff(test.expected, log, cmp.Comparer(func(x, y *git.Commit) bool {
 			return x.Id == y.Id
@@ -107,3 +107,48 @@
 		}
 	}
 }
+
+func TestDownloadFile(t *testing.T) {
+	t.Parallel()
+	tests := []struct {
+		project  string
+		path     string
+		ref      string
+		expected string
+	}{
+		{
+			project:  "test-project",
+			path:     "a/b/c.txt",
+			ref:      "foo",
+			expected: "file contents",
+		},
+	}
+	for _, test := range tests {
+		ctrl := gomock.NewController(t)
+		defer ctrl.Finish()
+		mockGitilesClient := mock_gitiles.NewMockGitilesClient(ctrl)
+		req := gitilespb.DownloadFileRequest{
+			Project:    test.project,
+			Committish: test.ref,
+			Path:       test.path,
+			Format:     gitilespb.DownloadFileRequest_TEXT,
+		}
+		// Client returns a successful DownloadFile response.
+		resp := gitilespb.DownloadFileResponse{
+			Contents: test.expected,
+		}
+		mockGitilesClient.EXPECT().DownloadFile(gomock.Any(), &req).Return(&resp, nil)
+		mockGitilesClientWrapper := gitilesClientWrapper{
+			client:  mockGitilesClient,
+			project: test.project,
+		}
+		ctx := context.Background()
+		contents, err := mockGitilesClientWrapper.DownloadFile(ctx, test.path, test.ref)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if contents != test.expected {
+			t.Fatalf("expected: %s\nactual: %s\n", test.expected, contents)
+		}
+	}
+}