Skip to content

Commit

Permalink
[GithubLastCommit] [GitlabLastCommit] [GiteaLastCommit] Support file …
Browse files Browse the repository at this point in the history
…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 <[email protected]>

* Update Gitea 404 message

* Handle case when no commits are returned for GitHub and GitLab

---------

Co-authored-by: chris48s <[email protected]>
  • Loading branch information
ReubenFrankel and chris48s authored Mar 25, 2024
1 parent 9fd5546 commit 4f141df
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 28 deletions.
2 changes: 1 addition & 1 deletion services/gitea/gitea-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
}

Expand Down
25 changes: 20 additions & 5 deletions services/gitea/gitea-last-commit.service.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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 {
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand All @@ -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(),
})
}
Expand All @@ -123,13 +136,15 @@ export default class GiteaLastCommit extends GiteaBase {
{
gitea_url: baseUrl = 'https://gitea.com',
display_timestamp: displayTimestamp,
path,
},
) {
const body = await this.fetch({
user,
repo,
branch,
baseUrl,
path,
})
return this.constructor.render({
commitDate: body[0].commit[displayTimestamp].date,
Expand Down
53 changes: 51 additions & 2 deletions services/gitea/gitea-last-commit.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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',
})
33 changes: 25 additions & 8 deletions services/github/github-last-commit.service.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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'),
Expand All @@ -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',
Expand All @@ -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',
Expand All @@ -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,
})
}
}
24 changes: 24 additions & 0 deletions services/github/github-last-commit.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,34 @@ 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' })

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' })
32 changes: 21 additions & 11 deletions services/gitlab/gitlab-last-commit.service.js
Original file line number Diff line number Diff line change
@@ -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 = `
Expand Down Expand Up @@ -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.',
}),
],
},
},
Expand All @@ -67,23 +73,27 @@ 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'),
})
}

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 })
}
}
Loading

0 comments on commit 4f141df

Please sign in to comment.