From 4f141dfa84706ad4c91d214a5228619c511f1ad5 Mon Sep 17 00:00:00 2001 From: ReubenFrankel <60552974+ReubenFrankel@users.noreply.github.com> Date: Mon, 25 Mar 2024 12:10:04 +0000 Subject: [PATCH] [GithubLastCommit] [GitlabLastCommit] [GiteaLastCommit] Support file path for last commit (#10041) * Support file path for GitHub last commit * Support file path for GitLab last commit * Support file path for Gitea last commit * Define common `relativeUri` validator * Sort imports * Add more tests for path variations * Fix test name Co-authored-by: chris48s * Update Gitea 404 message * Handle case when no commits are returned for GitHub and GitLab --------- Co-authored-by: chris48s --- services/gitea/gitea-helper.js | 2 +- services/gitea/gitea-last-commit.service.js | 25 +++++++-- services/gitea/gitea-last-commit.tester.js | 53 ++++++++++++++++++- services/github/github-last-commit.service.js | 33 +++++++++--- services/github/github-last-commit.tester.js | 24 +++++++++ services/gitlab/gitlab-last-commit.service.js | 32 +++++++---- services/gitlab/gitlab-last-commit.tester.js | 39 +++++++++++++- services/validators.js | 7 +++ 8 files changed, 187 insertions(+), 28 deletions(-) diff --git a/services/gitea/gitea-helper.js b/services/gitea/gitea-helper.js index 56bba95207891..c3c74e3d8435d 100644 --- a/services/gitea/gitea-helper.js +++ b/services/gitea/gitea-helper.js @@ -8,7 +8,7 @@ To specify another instance like [codeberg](https://codeberg.org/), [forgejo](ht function httpErrorsFor() { return { 403: 'private repo', - 404: 'user or repo not found', + 404: 'user, repo or path not found', } } diff --git a/services/gitea/gitea-last-commit.service.js b/services/gitea/gitea-last-commit.service.js index cb40bb48becb7..bf9c3c28df956 100644 --- a/services/gitea/gitea-last-commit.service.js +++ b/services/gitea/gitea-last-commit.service.js @@ -1,8 +1,8 @@ import Joi from 'joi' +import { age as ageColor } from '../color-formatters.js' import { pathParam, queryParam } from '../index.js' -import { optionalUrl } from '../validators.js' import { formatDate } from '../text-formatters.js' -import { age as ageColor } from '../color-formatters.js' +import { optionalUrl, relativeUri } from '../validators.js' import GiteaBase from './gitea-base.js' import { description, httpErrorsFor } from './gitea-helper.js' @@ -25,10 +25,11 @@ const schema = Joi.array() const displayEnum = ['author', 'committer'] const queryParamSchema = Joi.object({ - gitea_url: optionalUrl, + path: relativeUri, display_timestamp: Joi.string() .valid(...displayEnum) .default('author'), + gitea_url: optionalUrl, }).required() export default class GiteaLastCommit extends GiteaBase { @@ -54,6 +55,12 @@ export default class GiteaLastCommit extends GiteaBase { name: 'repo', example: 'tea', }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), queryParam({ name: 'display_timestamp', example: 'committer', @@ -84,6 +91,12 @@ export default class GiteaLastCommit extends GiteaBase { name: 'branch', example: 'main', }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), queryParam({ name: 'display_timestamp', example: 'committer', @@ -108,12 +121,12 @@ export default class GiteaLastCommit extends GiteaBase { } } - async fetch({ user, repo, branch, baseUrl }) { + async fetch({ user, repo, branch, baseUrl, path }) { // https://gitea.com/api/swagger#/repository return super.fetch({ schema, url: `${baseUrl}/api/v1/repos/${user}/${repo}/commits`, - options: { searchParams: { sha: branch } }, + options: { searchParams: { sha: branch, path } }, httpErrors: httpErrorsFor(), }) } @@ -123,6 +136,7 @@ export default class GiteaLastCommit extends GiteaBase { { gitea_url: baseUrl = 'https://gitea.com', display_timestamp: displayTimestamp, + path, }, ) { const body = await this.fetch({ @@ -130,6 +144,7 @@ export default class GiteaLastCommit extends GiteaBase { repo, branch, baseUrl, + path, }) return this.constructor.render({ commitDate: body[0].commit[displayTimestamp].date, diff --git a/services/gitea/gitea-last-commit.tester.js b/services/gitea/gitea-last-commit.tester.js index bc1e133cbfd68..f77ac6af27b60 100644 --- a/services/gitea/gitea-last-commit.tester.js +++ b/services/gitea/gitea-last-commit.tester.js @@ -8,6 +8,41 @@ t.create('Last Commit (recent)').get('/gitea/tea.json').expectBadge({ message: isFormattedDate, }) +t.create('Last Commit (recent) (top-level file path)') + .get('/gitea/tea.json?path=README.md') + .expectBadge({ + label: 'last commit', + message: isFormattedDate, + }) + +t.create('Last Commit (recent) (top-level dir path)') + .get('/gitea/tea.json?path=docs') + .expectBadge({ + label: 'last commit', + message: isFormattedDate, + }) + +t.create('Last Commit (recent) (top-level dir path with trailing slash)') + .get('/gitea/tea.json?path=docs/') + .expectBadge({ + label: 'last commit', + message: isFormattedDate, + }) + +t.create('Last Commit (recent) (nested dir path)') + .get('/gitea/tea.json?path=docs/CLI.md') + .expectBadge({ + label: 'last commit', + message: isFormattedDate, + }) + +t.create('Last Commit (recent) (path)') + .get('/gitea/tea.json?path=README.md') + .expectBadge({ + label: 'last commit', + message: isFormattedDate, + }) + t.create('Last Commit (recent) (self-managed)') .get('/CanisHelix/shields-badge-test.json?gitea_url=https://codeberg.org') .expectBadge({ @@ -24,9 +59,23 @@ t.create('Last Commit (on-branch) (self-managed)') message: isFormattedDate, }) -t.create('Last Commit (project not found)') +t.create('Last Commit (user not found)') .get('/CanisHelix/does-not-exist.json?gitea_url=https://codeberg.org') .expectBadge({ label: 'last commit', - message: 'user or repo not found', + message: 'user, repo or path not found', + }) + +t.create('Last Commit (repo not found)') + .get('/gitea/not-a-repo.json') + .expectBadge({ + label: 'last commit', + message: 'user, repo or path not found', + }) + +t.create('Last Commit (path not found)') + .get('/gitea/tea.json?path=not/a/dir') + .expectBadge({ + label: 'last commit', + message: 'user, repo or path not found', }) diff --git a/services/github/github-last-commit.service.js b/services/github/github-last-commit.service.js index 58759c13b1b9d..b0fb5d4fc4efc 100644 --- a/services/github/github-last-commit.service.js +++ b/services/github/github-last-commit.service.js @@ -1,7 +1,8 @@ import Joi from 'joi' -import { pathParam, queryParam } from '../index.js' -import { formatDate } from '../text-formatters.js' import { age as ageColor } from '../color-formatters.js' +import { NotFound, pathParam, queryParam } from '../index.js' +import { formatDate } from '../text-formatters.js' +import { relativeUri } from '../validators.js' import { GithubAuthV3Service } from './github-auth-service.js' import { documentation, httpErrorsFor } from './github-helpers.js' @@ -16,14 +17,14 @@ const schema = Joi.array() date: Joi.string().required(), }).required(), }).required(), - }).required(), + }), ) .required() - .min(1) const displayEnum = ['author', 'committer'] const queryParamSchema = Joi.object({ + path: relativeUri, display_timestamp: Joi.string() .valid(...displayEnum) .default('author'), @@ -45,6 +46,12 @@ export default class GithubLastCommit extends GithubAuthV3Service { parameters: [ pathParam({ name: 'user', example: 'google' }), pathParam({ name: 'repo', example: 'skia' }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), queryParam({ name: 'display_timestamp', example: 'committer', @@ -62,6 +69,12 @@ export default class GithubLastCommit extends GithubAuthV3Service { pathParam({ name: 'user', example: 'google' }), pathParam({ name: 'repo', example: 'skia' }), pathParam({ name: 'branch', example: 'infra/config' }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), queryParam({ name: 'display_timestamp', example: 'committer', @@ -82,20 +95,24 @@ export default class GithubLastCommit extends GithubAuthV3Service { } } - async fetch({ user, repo, branch }) { + async fetch({ user, repo, branch, path }) { return this._requestJson({ url: `/repos/${user}/${repo}/commits`, - options: { searchParams: { sha: branch } }, + options: { searchParams: { sha: branch, path } }, schema, httpErrors: httpErrorsFor(), }) } async handle({ user, repo, branch }, queryParams) { - const body = await this.fetch({ user, repo, branch }) + const { path, display_timestamp: displayTimestamp } = queryParams + const body = await this.fetch({ user, repo, branch, path }) + const [commit] = body.map(obj => obj.commit) + + if (!commit) throw new NotFound({ prettyMessage: 'no commits found' }) return this.constructor.render({ - commitDate: body[0].commit[queryParams.display_timestamp].date, + commitDate: commit[displayTimestamp].date, }) } } diff --git a/services/github/github-last-commit.tester.js b/services/github/github-last-commit.tester.js index f1399dc3d1034..fe82cc7425178 100644 --- a/services/github/github-last-commit.tester.js +++ b/services/github/github-last-commit.tester.js @@ -14,6 +14,26 @@ t.create('last commit (on branch)') .get('/badges/badgr.co/shielded.json') .expectBadge({ label: 'last commit', message: 'july 2013' }) +t.create('last commit (by top-level file path)') + .get('/badges/badgr.co.json?path=README.md') + .expectBadge({ label: 'last commit', message: 'september 2013' }) + +t.create('last commit (by top-level dir path)') + .get('/badges/badgr.co.json?path=badgr') + .expectBadge({ label: 'last commit', message: 'june 2013' }) + +t.create('last commit (by top-level dir path with trailing slash)') + .get('/badges/badgr.co.json?path=badgr/') + .expectBadge({ label: 'last commit', message: 'june 2013' }) + +t.create('last commit (by nested file path)') + .get('/badges/badgr.co.json?path=badgr/colors.py') + .expectBadge({ label: 'last commit', message: 'june 2013' }) + +t.create('last commit (on branch) (by top-level file path)') + .get('/badges/badgr.co/shielded.json?path=README.md') + .expectBadge({ label: 'last commit', message: 'june 2013' }) + t.create('last commit (by committer)') .get('/badges/badgr.co/shielded.json?display_timestamp=committer') .expectBadge({ label: 'last commit', message: 'july 2013' }) @@ -21,3 +41,7 @@ t.create('last commit (by committer)') t.create('last commit (repo not found)') .get('/badges/helmets.json') .expectBadge({ label: 'last commit', message: 'repo not found' }) + +t.create('last commit (no commits found)') + .get('/badges/badgr.co/shielded.json?path=not/a/dir') + .expectBadge({ label: 'last commit', message: 'no commits found' }) diff --git a/services/gitlab/gitlab-last-commit.service.js b/services/gitlab/gitlab-last-commit.service.js index c99888305662c..af17127296ea3 100644 --- a/services/gitlab/gitlab-last-commit.service.js +++ b/services/gitlab/gitlab-last-commit.service.js @@ -1,23 +1,23 @@ import Joi from 'joi' -import { pathParam, queryParam } from '../index.js' -import { optionalUrl } from '../validators.js' -import { formatDate } from '../text-formatters.js' import { age as ageColor } from '../color-formatters.js' -import { description, httpErrorsFor } from './gitlab-helper.js' +import { NotFound, pathParam, queryParam } from '../index.js' +import { formatDate } from '../text-formatters.js' +import { optionalUrl, relativeUri } from '../validators.js' import GitLabBase from './gitlab-base.js' +import { description, httpErrorsFor } from './gitlab-helper.js' const schema = Joi.array() .items( Joi.object({ committed_date: Joi.string().required(), - }).required(), + }), ) .required() - .min(1) const queryParamSchema = Joi.object({ ref: Joi.string(), gitlab_url: optionalUrl, + path: relativeUri, }).required() const refText = ` @@ -53,6 +53,12 @@ export default class GitlabLastCommit extends GitLabBase { name: 'ref', example: 'master', }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), ], }, }, @@ -67,13 +73,13 @@ export default class GitlabLastCommit extends GitLabBase { } } - async fetch({ project, baseUrl, ref }) { + async fetch({ project, baseUrl, ref, path }) { // https://docs.gitlab.com/ee/api/commits.html#list-repository-commits return super.fetch({ url: `${baseUrl}/api/v4/projects/${encodeURIComponent( project, )}/repository/commits`, - options: { searchParams: { ref_name: ref } }, + options: { searchParams: { ref_name: ref, path } }, schema, httpErrors: httpErrorsFor('project not found'), }) @@ -81,9 +87,13 @@ export default class GitlabLastCommit extends GitLabBase { async handle( { project }, - { gitlab_url: baseUrl = 'https://gitlab.com', ref }, + { gitlab_url: baseUrl = 'https://gitlab.com', ref, path }, ) { - const data = await this.fetch({ project, baseUrl, ref }) - return this.constructor.render({ commitDate: data[0].committed_date }) + const data = await this.fetch({ project, baseUrl, ref, path }) + const [commit] = data + + if (!commit) throw new NotFound({ prettyMessage: 'no commits found' }) + + return this.constructor.render({ commitDate: commit.committed_date }) } } diff --git a/services/gitlab/gitlab-last-commit.tester.js b/services/gitlab/gitlab-last-commit.tester.js index ecc29fdbd7208..08ab428468737 100644 --- a/services/gitlab/gitlab-last-commit.tester.js +++ b/services/gitlab/gitlab-last-commit.tester.js @@ -8,13 +8,43 @@ t.create('last commit (recent)').get('/gitlab-org/gitlab.json').expectBadge({ message: isFormattedDate, }) -t.create('last commit (on ref and ancient)') +t.create('last commit (on ref) (ancient)') .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee') .expectBadge({ label: 'last commit', message: 'march 2021', }) +t.create('last commit (on ref) (ancient) (by top-level file path)') + .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee&path=README.md') + .expectBadge({ + label: 'last commit', + message: 'december 2020', + }) + +t.create('last commit (on ref) (ancient) (by top-level dir path)') + .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee&path=changelogs') + .expectBadge({ + label: 'last commit', + message: 'march 2021', + }) + +t.create( + 'last commit (on ref) (ancient) (by top-level dir path with trailing slash)', +) + .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee&path=changelogs/') + .expectBadge({ + label: 'last commit', + message: 'march 2021', + }) + +t.create('last commit (on ref) (ancient) (by nested file path)') + .get('/gitlab-org/gitlab.json?ref=v13.8.6-ee&path=changelogs/README.md') + .expectBadge({ + label: 'last commit', + message: 'september 2020', + }) + t.create('last commit (self-managed)') .get('/gitlab-cn/gitlab.json?gitlab_url=https://jihulab.com') .expectBadge({ @@ -28,3 +58,10 @@ t.create('last commit (project not found)') label: 'last commit', message: 'project not found', }) + +t.create('last commit (no commits found)') + .get('/gitlab-org/gitlab.json?path=not/a/dir') + .expectBadge({ + label: 'last commit', + message: 'no commits found', + }) diff --git a/services/validators.js b/services/validators.js index df8d72a4d4696..b4455278668b4 100644 --- a/services/validators.js +++ b/services/validators.js @@ -79,3 +79,10 @@ export const optionalUrl = Joi.string().uri({ scheme: ['http', 'https'] }) export const fileSize = Joi.string() .regex(/^[0-9]+(b|kb|mb|gb|tb)$/i) .required() + +/** + * Joi validator that checks if a value is a relative-only URI + * + * @type {Joi} + */ +export const relativeUri = Joi.string().uri({ relativeOnly: true })