Handle both tag and commit objects in Stats

Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
Erik Engervall
2021-04-29 17:55:08 +02:00
parent 2b198c540c
commit e892b22c7c
13 changed files with 295 additions and 233 deletions
@@ -492,17 +492,21 @@ ${selectedPatchCommit.commit.message}`,
getAllTags: async ({ owner, repo }) => {
const { octokit } = await this.getOctokit();
const tags = await octokit.paginate(octokit.repos.listTags, {
const tags = await octokit.paginate(octokit.git.listMatchingRefs, {
owner,
repo,
ref: 'tags',
per_page: 100,
...DISABLE_CACHE,
});
return tags.map(tag => ({
tagName: tag.name,
sha: tag.commit.sha,
}));
return tags
.map(tag => ({
tagName: tag.ref.replace('refs/tags/', ''),
tagSha: tag.object.sha,
tagType: tag.object.type as 'tag' | 'commit',
}))
.reverse();
},
getAllReleases: async ({ owner, repo }) => {
@@ -536,6 +540,7 @@ ${selectedPatchCommit.commit.message}`,
date: singleTag.data.tagger.date,
username: singleTag.data.tagger.name,
userEmail: singleTag.data.tagger.email,
objectSha: singleTag.data.object.sha,
};
},
};
@@ -780,7 +785,8 @@ export interface GitReleaseApi {
) => Promise<
Array<{
tagName: string;
sha: string;
tagSha: string;
tagType: 'tag' | 'commit';
}>
>;
@@ -804,6 +810,7 @@ export interface GitReleaseApi {
date: string;
username: string;
userEmail: string;
objectSha: string;
}>;
};
}
@@ -25,11 +25,13 @@ describe('getReleaseCommitPairs', () => {
candidates: [
{
tagName: 'rc-1.0.0',
sha: 'sha-1.0.0',
tagSha: 'sha-1.0.0',
tagType: 'tag' as const,
},
{
tagName: 'rc-1.0.1',
sha: 'sha-1.0.1',
tagSha: 'sha-1.0.1',
tagType: 'tag' as const,
},
],
versions: [],
@@ -42,13 +44,15 @@ describe('getReleaseCommitPairs', () => {
candidates: [
{
tagName: 'rc-2.0.0',
sha: 'sha-2.0.0',
tagSha: 'sha-2.0.0',
tagType: 'tag' as const,
},
],
versions: [
{
tagName: 'version-2.0.0',
sha: 'sha-2.0.0',
tagSha: 'sha-2.0.0',
tagType: 'tag' as const,
},
],
};
@@ -60,17 +64,20 @@ describe('getReleaseCommitPairs', () => {
candidates: [
{
tagName: 'rc-3.0.1',
sha: 'sha-3.0.1',
tagSha: 'sha-3.0.1',
tagType: 'tag' as const,
},
{
tagName: 'rc-3.0.0',
sha: 'sha-3.0.0',
tagSha: 'sha-3.0.0',
tagType: 'tag' as const,
},
],
versions: [
{
tagName: 'version-3.0.1',
sha: 'sha-3.0.1',
tagSha: 'sha-3.0.1',
tagType: 'tag' as const,
},
],
};
@@ -92,14 +99,29 @@ describe('getReleaseCommitPairs', () => {
Object {
"releaseCommitPairs": Array [
Object {
"baseVersion": "3.0",
"baseVersion": "2.0",
"endCommit": Object {
"sha": "sha-3.0.1",
"tagName": "version-3.0.1",
"tagName": "version-2.0.0",
"tagSha": "sha-2.0.0",
"tagType": "tag",
},
"startCommit": Object {
"tagName": "rc-2.0.0",
"tagSha": "sha-2.0.0",
"tagType": "tag",
},
},
Object {
"baseVersion": "3.0",
"endCommit": Object {
"tagName": "version-3.0.1",
"tagSha": "sha-3.0.1",
"tagType": "tag",
},
"startCommit": Object {
"sha": "sha-3.0.0",
"tagName": "rc-3.0.0",
"tagSha": "sha-3.0.0",
"tagType": "tag",
},
},
],
@@ -28,32 +28,19 @@ export function getReleaseCommitPairs({
const endTag = release.versions[0];
// Missing Release Candidate for unknown reason
if (!startTag?.sha) {
if (!startTag) {
return acc;
}
// Missing Release Version (likely prerelease)
if (!endTag?.sha) {
return acc;
}
// First RC tag is pointing towards the same commit as the most
// recent Version tag, meaning there haven't been any patches
// and we thus cannot determine and difference in time
if (startTag?.sha === endTag?.sha) {
if (!endTag) {
return acc;
}
return acc.concat({
baseVersion: release.baseVersion,
startCommit: {
tagName: startTag.tagName,
sha: startTag.sha,
},
endCommit: {
tagName: endTag.tagName,
sha: endTag.sha,
},
startCommit: { ...startTag },
endCommit: { ...endTag },
});
},
[],
@@ -20,20 +20,22 @@ import { DateTime } from 'luxon';
import { useApi } from '@backstage/core';
import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs';
import { getTagDate } from '../../helpers/getTagDate';
import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef';
import { useProjectContext } from '../../../../contexts/ProjectContext';
import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext';
import { getTagDates } from '../../helpers/getTagDates';
export type ReleaseCommitPairs = Array<{
baseVersion: string;
startCommit: {
tagName: string;
sha: string;
tagSha: string;
tagType: 'tag' | 'commit';
};
endCommit: {
tagName: string;
sha: string;
tagSha: string;
tagType: 'tag' | 'commit';
};
}>;
@@ -56,17 +58,17 @@ export function useGetReleaseTimes() {
const [progress, setProgress] = useState(0);
const { releaseCommitPairs } = getReleaseCommitPairs({ releaseStats });
const [fnRes, run] = useAsyncFn(() => {
const [releaseTimeResult, run] = useAsyncFn(() => {
setProgress(0);
return getAndSetReleaseTime(0);
return getAndSetReleaseTime({ pairIndex: 0 });
});
useAsync(async () => {
if (averageReleaseTime.length === 0) return;
if (releaseCommitPairs.length === averageReleaseTime.length) return;
await getAndSetReleaseTime(averageReleaseTime.length);
}, [fnRes.value, averageReleaseTime]);
await getAndSetReleaseTime({ pairIndex: averageReleaseTime.length });
}, [releaseTimeResult.value, averageReleaseTime]);
useEffect(() => {
const unboundedProgress = Math.round(
@@ -77,16 +79,20 @@ export function useGetReleaseTimes() {
setProgress(boundedProgress);
}, [averageReleaseTime.length, releaseCommitPairs.length]);
async function getAndSetReleaseTime(index: number) {
const { baseVersion, startCommit, endCommit } = releaseCommitPairs[index];
async function getAndSetReleaseTime({ pairIndex }: { pairIndex: number }) {
const { baseVersion, startCommit, endCommit } = releaseCommitPairs[
pairIndex
];
const [
{ tagDate: startCommitCreatedAt },
{ tagDate: endCommitCreatedAt },
] = await Promise.all([
getTagDate({ pluginApiClient, project, tagSha: startCommit.sha }),
getTagDate({ pluginApiClient, project, tagSha: endCommit.sha }),
]);
const {
startDate: startCommitCreatedAt,
endDate: endCommitCreatedAt,
} = await getTagDates({
pluginApiClient,
project,
startTag: startCommit,
endTag: endCommit,
});
const releaseTime: ReleaseTime = {
version: baseVersion,
@@ -21,22 +21,31 @@ import { Alert } from '@material-ui/lab';
import { CenteredCircularProgress } from '../../../../components/CenteredCircularProgress';
import { ReleaseStats } from '../../contexts/ReleaseStatsContext';
import { useGetCommit } from '../../hooks/useGetCommit';
import { useAsync } from 'react-use';
import { getTagDates } from '../../helpers/getTagDates';
import { useProjectContext } from '../../../../contexts/ProjectContext';
import { useApi } from '@backstage/core';
import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef';
import { getDecimalNumber } from '../../helpers/getDecimalNumber';
interface ReleaseTimeProps {
releaseStat: ReleaseStats['releases']['0'];
}
export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
const firstCandidateSha = [...releaseStat.candidates].reverse()[0]?.sha;
const { commit: releaseCut } = useGetCommit({ ref: firstCandidateSha });
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { project } = useProjectContext();
const mostRecentVersionSha = releaseStat.versions[0]?.sha;
const { commit: releaseComplete } = useGetCommit({
ref: mostRecentVersionSha,
});
const releaseTimes = useAsync(() =>
getTagDates({
pluginApiClient,
project,
startTag: [...releaseStat.candidates].reverse()[0],
endTag: releaseStat.versions[0],
}),
);
if (releaseCut.loading || releaseComplete.loading) {
if (releaseTimes.loading || releaseTimes.loading) {
return (
<Wrapper>
<CenteredCircularProgress />
@@ -44,19 +53,22 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
);
}
if (releaseCut.error) {
if (releaseTimes.error) {
return (
<Alert severity="error">
Failed to fetch the first Release Candidate commit (
{releaseCut.error.message})
{releaseTimes.error.message})
</Alert>
);
}
const diff =
releaseCut.value?.createdAt && releaseComplete.value?.createdAt
? DateTime.fromISO(releaseComplete.value.createdAt)
.diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours'])
const { days = 0, hours = 0 } =
releaseTimes.value?.startDate && releaseTimes.value?.endDate
? DateTime.fromISO(releaseTimes.value.endDate)
.diff(DateTime.fromISO(releaseTimes.value.startDate), [
'days',
'hours',
])
.toObject()
: { days: -1 };
@@ -71,8 +83,8 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
>
<Typography variant="body1">
{releaseStat.versions.length === 0 ? '-' : 'Release completed '}
{releaseComplete.value?.createdAt &&
DateTime.fromISO(releaseComplete.value.createdAt)
{releaseTimes.value?.endDate &&
DateTime.fromISO(releaseTimes.value.endDate)
.setLocale('sv-SE')
.toFormat('yyyy-MM-dd')}
</Typography>
@@ -85,11 +97,13 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
alignItems: 'center',
}}
>
<Typography variant="h5" color="secondary">
{diff.days === -1 ? (
<>Ongoing: {diff.days} days</>
<Typography variant="h6" color="secondary">
{days === -1 ? (
<>Ongoing</>
) : (
<>Completed in: {diff.days} days</>
<>
Completed in: {days} days {getDecimalNumber(hours, 1)} hours
</>
)}
</Typography>
</Box>
@@ -103,8 +117,8 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
>
<Typography variant="body1">
Release Candidate created{' '}
{releaseCut.value?.createdAt &&
DateTime.fromISO(releaseCut.value.createdAt)
{releaseTimes.value?.startDate &&
DateTime.fromISO(releaseTimes.value.startDate)
.setLocale('sv-SE')
.toFormat('yyyy-MM-dd')}
</Typography>
@@ -27,21 +27,15 @@ export interface ReleaseStats {
baseVersion: string;
createdAt: string | null;
htmlUrl: string;
/**
* Ordered from new to old
*/
candidates: {
tagName: string;
sha: string;
tagSha: string;
tagType: 'tag' | 'commit';
}[];
/**
* Ordered from new to old
*/
versions: {
tagName: string;
sha: string;
tagSha: string;
tagType: 'tag' | 'commit';
}[];
};
};
@@ -44,14 +44,18 @@ describe('getReleaseStats', () => {
unmatchedTags: [],
},
allTags: [
{ sha: 'sha', tagName: 'rc-1.0.0' },
{ sha: 'sha', tagName: 'rc-1.0.1' },
{ sha: 'sha', tagName: 'rc-1.0.2' },
{ sha: 'sha', tagName: 'version-1.0.2' },
{ sha: 'sha', tagName: 'rc-1.1.1' },
{ tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.0' },
{ tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.1' },
{ tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.2' },
{ tagType: 'tag' as const, tagSha: 'sha', tagName: 'version-1.0.2' },
{ tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.1.1' },
{ sha: 'unmatchable', tagName: 'rc-1/2/3' },
{ sha: 'unmappable', tagName: 'rc-123.123.123' },
{ tagType: 'tag' as const, tagSha: 'unmatchable', tagName: 'rc-1/2/3' },
{
tagType: 'tag' as const,
tagSha: 'unmappable',
tagName: 'rc-123.123.123',
},
],
});
@@ -63,24 +67,28 @@ describe('getReleaseStats', () => {
"baseVersion": "1.0",
"candidates": Array [
Object {
"sha": "sha",
"tagName": "rc-1.0.0",
"tagSha": "sha",
"tagType": "tag",
},
Object {
"sha": "sha",
"tagName": "rc-1.0.1",
"tagSha": "sha",
"tagType": "tag",
},
Object {
"sha": "sha",
"tagName": "rc-1.0.2",
"tagSha": "sha",
"tagType": "tag",
},
],
"createdAt": "2021-01-01T10:11:12Z",
"htmlUrl": "html_url",
"versions": Array [
Object {
"sha": "sha",
"tagName": "version-1.0.2",
"tagSha": "sha",
"tagType": "tag",
},
],
},
@@ -88,8 +96,9 @@ describe('getReleaseStats', () => {
"baseVersion": "1.1",
"candidates": Array [
Object {
"sha": "sha",
"tagName": "rc-1.1.1",
"tagSha": "sha",
"tagType": "tag",
},
],
"createdAt": "2021-01-01T10:11:12Z",
@@ -1,40 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GitReleaseApi } from '../../../api/GitReleaseApiClient';
import { Project } from '../../../contexts/ProjectContext';
interface GetTagDate {
pluginApiClient: GitReleaseApi;
project: Project;
tagSha: string;
}
export const getTagDate = async ({
pluginApiClient,
project,
tagSha,
}: GetTagDate) => {
const commitRes = await pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: tagSha,
});
return {
tagDate: commitRes.createdAt,
};
};
@@ -0,0 +1,143 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { GitReleaseApi } from '../../../api/GitReleaseApiClient';
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
import { Project } from '../../../contexts/ProjectContext';
interface GetTagDates {
pluginApiClient: GitReleaseApi;
project: Project;
startTag: {
tagSha: string;
tagType: 'tag' | 'commit';
};
endTag: {
tagSha: string;
tagType: 'tag' | 'commit';
};
}
export const getTagDates = async ({
pluginApiClient,
project,
startTag,
endTag,
}: GetTagDates) => {
if (startTag.tagType === 'tag' && endTag.tagType === 'tag') {
const [{ date: startDate }, { date: endDate }] = await Promise.all([
pluginApiClient.stats.getSingleTag({
owner: project.owner,
repo: project.repo,
tagSha: startTag.tagSha,
}),
pluginApiClient.stats.getSingleTag({
owner: project.owner,
repo: project.repo,
tagSha: endTag.tagSha,
}),
]);
return {
startDate,
endDate,
};
}
if (startTag.tagType === 'commit' && endTag.tagType === 'commit') {
const [
{ createdAt: startDate },
{ createdAt: endDate },
] = await Promise.all([
pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: startTag.tagSha,
}),
pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: endTag.tagSha,
}),
]);
return {
startDate,
endDate,
};
}
if (startTag.tagType === 'tag' && endTag.tagType === 'commit') {
const [{ date: startDate }, { createdAt: endDate }] = await Promise.all([
getCommitFromTag({ pluginApiClient, project, tag: startTag }),
pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: endTag.tagSha,
}),
]);
return {
startDate,
endDate,
};
}
if (startTag.tagType === 'commit' && endTag.tagType === 'tag') {
const [{ createdAt: startDate }, { date: endDate }] = await Promise.all([
pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: startTag.tagSha,
}),
getCommitFromTag({ pluginApiClient, project, tag: endTag }),
]);
return {
startDate,
endDate,
};
}
throw new GitReleaseManagerError(
`Failed to get tag dates for tags with type "${startTag.tagType}" and "${endTag.tagType}"`,
);
};
async function getCommitFromTag({
pluginApiClient,
project,
tag,
}: {
pluginApiClient: GetTagDates['pluginApiClient'];
project: GetTagDates['project'];
tag: GetTagDates['startTag'] | GetTagDates['endTag'];
}) {
const singleTag = await pluginApiClient.stats.getSingleTag({
owner: project.owner,
repo: project.repo,
tagSha: tag.tagSha,
});
const startCommit = await pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref: singleTag.objectSha,
});
return {
date: startCommit.createdAt,
};
}
@@ -1,43 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
import { useProjectContext } from '../../../contexts/ProjectContext';
export const useGetCommit = ({ ref }: { ref?: string }) => {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { project } = useProjectContext();
const commit = useAsync(async () => {
if (!ref) {
throw new GitReleaseManagerError('Missing ref to get commit');
}
return pluginApiClient.getCommit({
owner: project.owner,
repo: project.repo,
ref,
});
});
return {
commit,
};
};
@@ -1,46 +0,0 @@
/*
* Copyright 2021 Spotify AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useAsync } from 'react-use';
import { useApi } from '@backstage/core';
import { getTagDate } from '../helpers/getTagDate';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
import { ReleaseStats } from '../contexts/ReleaseStatsContext';
import { UnboxArray } from '../../../types/helpers';
import { useProjectContext } from '../../../contexts/ProjectContext';
export const useGetTagDate = ({
tag,
}: {
tag: UnboxArray<ReleaseStats['releases']['0']['candidates']>;
}) => {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { project } = useProjectContext();
const tagDate = useAsync(async () => {
if (!tag) {
throw new GitReleaseManagerError('Missing tag details to get tag date');
}
return await getTagDate({ pluginApiClient, project, tagSha: tag.sha });
});
return {
tagDate,
};
};
@@ -25,11 +25,13 @@ export const mockReleaseStats: ReleaseStats = {
candidates: [
{
tagName: 'rc-1.0.1',
sha: 'sha-1.0.1',
tagSha: 'sha-1.0.1',
tagType: 'tag',
},
{
tagName: 'rc-1.0.0',
sha: 'sha-1.0.0',
tagSha: 'sha-1.0.0',
tagType: 'tag',
},
],
versions: [],
@@ -41,25 +43,30 @@ export const mockReleaseStats: ReleaseStats = {
candidates: [
{
tagName: 'rc-1.1.2',
sha: 'sha-1.1.2',
tagSha: 'sha-1.1.2',
tagType: 'tag',
},
{
tagName: 'rc-1.1.1',
sha: 'sha-1.1.1',
tagSha: 'sha-1.1.1',
tagType: 'tag',
},
{
tagName: 'rc-1.1.0',
sha: 'sha-1.1.0',
tagSha: 'sha-1.1.0',
tagType: 'tag',
},
],
versions: [
{
tagName: 'version-1.1.3',
sha: 'sha-1.1.3',
tagSha: 'sha-1.1.3',
tagType: 'tag',
},
{
tagName: 'version-1.1.2',
sha: 'sha-1.1.2',
tagSha: 'sha-1.1.2',
tagType: 'tag',
},
],
},
@@ -292,7 +292,8 @@ export const mockApiClient: GitReleaseApi = {
getAllTags: jest.fn(async () => [
{
tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER,
sha: 'mock_sha',
tagSha: 'mock_sha',
tagType: 'tag' as const,
},
]),
@@ -308,8 +309,9 @@ export const mockApiClient: GitReleaseApi = {
getSingleTag: jest.fn(async () => ({
date: '2021-04-29T12:48:30.120Z',
username: 'mock_usersingle_tag_name',
userEmail: 'mock_userEsingle_tag_mail',
username: 'mock_user_single_tag_name',
userEmail: 'mock_user_single_tag_email',
objectSha: 'mock_single_tag_object_sha',
})),
},
};