fix(glrepo): make Project() actually cache the fetched project
Description
glrepo.Project() has a value receiver, so the memo write inside it lands on a copy of the struct and is discarded:
func (r glRepo) Project(client *gitlab.Client) (*gitlab.Project, error) { // value receiver
if r.project != nil && ... { return r.project.Project, nil }
p, err := api.GetProject(client, r.fullname)
...
r.project = &Project{...} // written to the copy, thrown awayThe cache has never worked. Every call re-fetches, despite the fullname/hostname cache-invalidation fields on the Project struct showing memoization was intended.
NewWithHost already returns *glRepo into the interface, and every caller is sequential, so a pointer receiver is safe and satisfies glrepo.Interface unchanged.
Knock-on fix
glab ci cancel job and glab ci cancel pipeline call Project() inside their per-item loop, so cancelling N items issued N identical GET /projects/:fullpath calls. Two test expectations encoded that repeat fetch verbatim:
gomock.InOrder(
tc.MockProjects.EXPECT().GetProject("OWNER/REPO", ...), // fetch
tc.MockJobs.EXPECT().CancelJob(int64(123), int64(11111111)),
tc.MockProjects.EXPECT().GetProject("OWNER/REPO", ...), // same fetch again
tc.MockJobs.EXPECT().CancelJob(int64(123), int64(22222222)),
)They now expect a single lookup per batch.
Found while investigating #8526: the telemetry hook calls Project() too, so making telemetry delivery reliable would otherwise have added a redundant API round trip to every in-repo command.
Related Issues
No issue; found during #8526 investigation.
How has this been tested?
New Test_glRepo_Project_cachesTheFetch asserts a single fetch across two Project() calls. Verified it fails without the receiver change (2 calls, not 1) and passes with it.
make test-changed (3515 tests) and make lint pass. The two ci cancel expectations are the only fallout across the tree.