diff --git a/services/bitbucket/bitbucket-last-commit.service.js b/services/bitbucket/bitbucket-last-commit.service.js new file mode 100644 index 0000000000000000000000000000000000000000..ca6f4c241b5d1154de08804481b918b070427d8e --- /dev/null +++ b/services/bitbucket/bitbucket-last-commit.service.js @@ -0,0 +1,81 @@ +import Joi from 'joi' +import { age as ageColor } from '../color-formatters.js' +import { BaseJsonService, NotFound, pathParam, queryParam } from '../index.js' +import { formatDate } from '../text-formatters.js' +import { relativeUri } from '../validators.js' + +const schema = Joi.object({ + values: Joi.array().items({ + date: Joi.string().isoDate().required(), + }), +}).required() + +const queryParamSchema = Joi.object({ + path: relativeUri, +}).required() + +export default class BitbucketLastCommit extends BaseJsonService { + static category = 'activity' + static route = { + base: 'bitbucket/last-commit', + pattern: ':user/:repo/:branch+', + queryParamSchema, + } + + static openApi = { + '/bitbucket/last-commit/{user}/{repo}/{branch}': { + get: { + summary: 'Bitbucket last commit', + parameters: [ + pathParam({ name: 'user', example: 'shields-io' }), + pathParam({ name: 'repo', example: 'test-repo' }), + pathParam({ name: 'branch', example: 'main' }), + queryParam({ + name: 'path', + example: 'README.md', + schema: { type: 'string' }, + description: 'File path to resolve the last commit for.', + }), + ], + }, + }, + } + + static defaultBadgeData = { label: 'last commit' } + + static render({ commitDate }) { + return { + message: formatDate(commitDate), + color: ageColor(Date.parse(commitDate)), + } + } + + async fetch({ user, repo, branch, path }) { + // https://developer.atlassian.com/cloud/bitbucket/rest/api-group-commits/#api-repositories-workspace-repo-slug-commits-get + return this._requestJson({ + url: `https://bitbucket.org/api/2.0/repositories/${user}/${repo}/commits/${branch}`, + options: { + searchParams: { + path, + pagelen: 1, + fields: 'values.date', + }, + }, + schema, + httpErrors: { + 403: 'private repo', + 404: 'user, repo or branch not found', + }, + }) + } + + async handle({ user, repo, branch }, queryParams) { + const { path } = queryParams + const data = await this.fetch({ user, repo, branch, path }) + const [commit] = data.values + + if (!commit) throw new NotFound({ prettyMessage: 'no commits found' }) + + return this.constructor.render({ commitDate: commit.date }) + } +} diff --git a/services/bitbucket/bitbucket-last-commit.tester.js b/services/bitbucket/bitbucket-last-commit.tester.js new file mode 100644 index 0000000000000000000000000000000000000000..8c75524173f45187252c55e9bd50e697b236b5dd --- /dev/null +++ b/services/bitbucket/bitbucket-last-commit.tester.js @@ -0,0 +1,41 @@ +import { isFormattedDate } from '../test-validators.js' +import { ServiceTester } from '../tester.js' + +export const t = new ServiceTester({ + id: 'BitbucketLastCommit', + title: 'Bitbucket last commit', + pathPrefix: '/bitbucket/last-commit', +}) + +t.create('last commit') + .get('/shields-io/test-repo/main.json') + .expectBadge({ label: 'last commit', message: isFormattedDate }) + +t.create('last commit (path)') + .get('/shields-io/test-repo/main.json?path=README.md') + .expectBadge({ label: 'last commit', message: isFormattedDate }) + +t.create('last commit (user not found)') + .get('/not-a-user/test-repo/main.json') + .expectBadge({ + label: 'last commit', + message: 'user, repo or branch not found', + }) + +t.create('last commit (repo not found)') + .get('/shields-io/not-a-repo/main.json') + .expectBadge({ + label: 'last commit', + message: 'user, repo or branch not found', + }) + +t.create('last commit (branch not found)') + .get('/shields-io/test-repo/not-a-branch.json') + .expectBadge({ + label: 'last commit', + message: 'user, repo or branch not found', + }) + +t.create('last commit (path not found)') + .get('/shields-io/test-repo/main.json?path=not/a/dir') + .expectBadge({ label: 'last commit', message: 'no commits found' })