Create getCommit hook

When opening row, calculate diff in days from creating RC to being done with the Release Version

Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
Erik Engervall
2021-04-23 02:55:10 +02:00
parent c865a349e4
commit 10c9547245
13 changed files with 414 additions and 168 deletions
@@ -60,6 +60,7 @@ describe('PluginApiClient', () => {
"stats": Object {
"getAllReleases": [Function],
"getAllTags": [Function],
"getCommit": [Function],
},
}
`);
@@ -576,6 +576,7 @@ ${selectedPatchCommit.commit.message}`,
return tags.map(tag => ({
tagName: tag.name,
sha: tag.commit.sha,
}));
},
@@ -598,6 +599,20 @@ ${selectedPatchCommit.commit.message}`,
htmlUrl: release.html_url,
}));
},
getCommit: async ({ owner, repo, ref }: { ref: string } & OwnerRepo) => {
const { octokit } = await this.getOctokit();
const { data: commit } = await octokit.repos.getCommit({
owner,
repo,
ref,
});
return {
createdAt: commit.commit.committer?.date,
};
},
};
}
@@ -660,28 +675,6 @@ type GetRecentCommits = (
export type GetRecentCommitsResult = UnboxReturnedPromise<GetRecentCommits>;
export type GetRecentCommitsResultSingle = UnboxArray<GetRecentCommitsResult>;
type GetAllTags = (
args: OwnerRepo,
) => Promise<
Array<{
tagName: string;
}>
>;
export type GetAllTagsResult = UnboxReturnedPromise<GetAllTags>;
type GetAllReleases = (
args: OwnerRepo,
) => Promise<
Array<{
id: number;
name: string | null;
tagName: string;
createdAt: string | null;
htmlUrl: string;
}>
>;
export type GetAllReleasesResult = UnboxReturnedPromise<GetAllReleases>;
type GetLatestRelease = (
args: OwnerRepo,
) => Promise<{
@@ -736,6 +729,9 @@ type GetBranch = (
}>;
export type GetBranchResult = UnboxReturnedPromise<GetBranch>;
/**
* CreateRc
*/
type CreateRef = (
args: {
mostRecentSha: string;
@@ -771,6 +767,9 @@ type CreateRelease = (
}>;
export type CreateReleaseResult = UnboxReturnedPromise<CreateRelease>;
/**
* Patch
*/
type CreateTempCommit = (
args: {
tagParts: SemverTagParts | CalverTagParts;
@@ -876,6 +875,9 @@ type UpdateRelease = (
}>;
export type UpdateReleaseResult = UnboxReturnedPromise<UpdateRelease>;
/**
* PromoteRc
*/
type PromoteRelease = (
args: {
releaseId: NonNullable<GetLatestReleaseResult>['id'];
@@ -888,6 +890,41 @@ type PromoteRelease = (
}>;
export type PromoteReleaseResult = UnboxReturnedPromise<PromoteRelease>;
/**
* Stats
*/
type GetAllTags = (
args: OwnerRepo,
) => Promise<
Array<{
tagName: string;
sha: string;
}>
>;
export type GetAllTagsResult = UnboxReturnedPromise<GetAllTags>;
type GetAllReleases = (
args: OwnerRepo,
) => Promise<
Array<{
id: number;
name: string | null;
tagName: string;
createdAt: string | null;
htmlUrl: string;
}>
>;
export type GetAllReleasesResult = UnboxReturnedPromise<GetAllReleases>;
type GetCommit = (
args: {
ref: string;
} & OwnerRepo,
) => Promise<{
createdAt: string | undefined;
}>;
export type GetCommitResult = UnboxReturnedPromise<GetCommit>;
export interface IPluginApiClient {
getHost: GetHost;
getRepoPath: GetRepoPath;
@@ -920,5 +957,6 @@ export interface IPluginApiClient {
stats: {
getAllTags: GetAllTags;
getAllReleases: GetAllReleases;
getCommit: GetCommit;
};
}
@@ -26,17 +26,17 @@ import {
TableContainer,
TableHead,
TableRow,
Typography,
} from '@material-ui/core';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { getMappedReleases } from './getMappedReleases';
import { getSummary } from './getSummary';
import { getTags } from './getTags';
import { getMappedReleases } from './helpers/getMappedReleases';
import { getSummary } from './helpers/getSummary';
import { getTags } from './helpers/getTags';
import { Row } from './Row';
import { useGetStats } from './hooks/useGetStats';
import { useProjectContext } from '../../contexts/ProjectContext';
import { Warn } from './Warn';
import { Summary } from './Summary';
const useStyles = makeStyles({
table: {
@@ -81,80 +81,9 @@ export function DialogBody() {
});
}
const getDecimalNumber = (n: number) => {
if (isNaN(n)) {
return 0;
}
if (n.toString().includes('.')) {
return n.toFixed(2);
}
return n;
};
return (
<>
<Paper
variant="outlined"
style={{
padding: 20,
marginLeft: 25,
marginRight: 25,
marginBottom: 25,
}}
>
<Box margin={1}>
<Typography variant="h4">Summary</Typography>
<Typography variant="body2">
Total releases: {summary.totalReleases}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Candidate</Typography>
<Typography variant="body2">
Release Candidate patches: {summary.totalCandidatePatches}
</Typography>
<Typography variant="body2">
Release Candidate patches per release:{' '}
{getDecimalNumber(
summary.totalCandidatePatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Version</Typography>
<Typography variant="body2">
Release Version patches: {summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Release Version patches per release:{' '}
{getDecimalNumber(
summary.totalVersionPatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Total</Typography>
<Typography variant="body2">
Patches:{' '}
{summary.totalCandidatePatches + summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Patches per release:{' '}
{getDecimalNumber(
(summary.totalCandidatePatches + summary.totalVersionPatches) /
summary.totalReleases,
)}
</Typography>
</Box>
</Paper>
<Summary summary={summary} />
<TableContainer component={Paper}>
<Table className={classes.table} size="small">
@@ -15,7 +15,7 @@
*/
import React, { useState } from 'react';
import { DateTime } from 'luxon';
import { DateTime, DurationObject } from 'luxon';
import {
Box,
Collapse,
@@ -29,7 +29,9 @@ import {
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import { getMappedReleases } from './getMappedReleases';
import { getMappedReleases } from './helpers/getMappedReleases';
import { useGetCommit } from './hooks/useGetCommit';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
const useRowStyles = makeStyles({
root: {
@@ -47,9 +49,6 @@ interface RowProps {
export function Row({ baseVersion, mappedRelease }: RowProps) {
const [open, setOpen] = useState(false);
const classes = useRowStyles();
const versions = mappedRelease.versions.reverse();
const candidates = mappedRelease.candidates.reverse();
const isPrerelease = versions.length === 0;
return (
<React.Fragment>
@@ -67,7 +66,7 @@ export function Row({ baseVersion, mappedRelease }: RowProps) {
<TableCell component="th" scope="row">
<Link href={mappedRelease.htmlUrl} target="_blank">
{baseVersion}
{isPrerelease ? ' (prerelease)' : ''}
{mappedRelease.versions.length === 0 ? ' (prerelease)' : ''}
</Link>
</TableCell>
@@ -79,53 +78,152 @@ export function Row({ baseVersion, mappedRelease }: RowProps) {
: '-'}
</TableCell>
<TableCell>{candidates.length}</TableCell>
<TableCell>{mappedRelease.candidates.length}</TableCell>
<TableCell>{Math.max(0, versions.length - 1)}</TableCell>
<TableCell>{Math.max(0, mappedRelease.versions.length - 1)}</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box margin={1}>
<div
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}
>
{!isPrerelease && (
<Box margin={1}>
{versions.map(version => (
<Typography key={version} variant="body1">
{version}
</Typography>
))}
</Box>
)}
{!isPrerelease && (
<Box
margin={1}
style={{ transform: 'rotate(-45deg)', fontSize: 30 }}
>
{' 🚀 '}
</Box>
)}
<Box margin={1}>
{candidates.map(candidate => (
<Typography key={candidate} variant="body1">
{candidate}
</Typography>
))}
</Box>
</div>
</Box>
<CollapsedEl mappedRelease={mappedRelease} />
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
}
function CollapsedEl({
mappedRelease,
}: {
mappedRelease: ReturnType<typeof getMappedReleases>['releases']['0'];
}) {
const reversedCandidates = [...mappedRelease.candidates].reverse();
const { commit: releaseCut } = useGetCommit({
ref: reversedCandidates[0]?.sha,
});
const { commit: releaseComplete } = useGetCommit({
ref: mappedRelease.versions[0]?.sha,
});
console.log('*** releaseCut > ', releaseCut.value); // eslint-disable-line no-console
console.log('*** releaseComplete > ', releaseComplete.value); // eslint-disable-line no-console
let diff = { days: -1 } as DurationObject;
if (releaseCut.value?.createdAt && releaseComplete.value?.createdAt) {
diff = DateTime.fromISO(releaseComplete.value.createdAt)
.diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours'])
.toObject();
}
return (
<Box
margin={1}
style={{
display: 'flex',
alignItems: 'stretch',
paddingLeft: '20%',
paddingRight: '20%',
}}
>
<Box
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
{mappedRelease.versions.length > 0 && (
<Box margin={1} style={{ position: 'relative' }}>
{mappedRelease.versions.map(version => (
<React.Fragment key={version.tagName}>
<Typography variant="body1">{version.tagName}</Typography>
</React.Fragment>
))}
</Box>
)}
{mappedRelease.versions.length > 0 && (
<Box
margin={1}
style={{
position: 'relative',
transform: 'rotate(-45deg)',
fontSize: 30,
}}
>
{' 🚀 '}
</Box>
)}
<Box margin={1} style={{ position: 'relative' }}>
{mappedRelease.candidates.map(candidate => (
<React.Fragment key={candidate.tagName}>
<Typography variant="body1">{candidate.tagName}</Typography>
</React.Fragment>
))}
</Box>
</Box>
<Box
style={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
}}
>
{releaseComplete.loading ? (
<CenteredCircularProgress />
) : (
<>
<Box
style={{
flex: 1,
display: 'flex',
alignItems: 'flex-end',
}}
>
Release completed{' '}
{releaseComplete.value?.createdAt &&
DateTime.fromISO(releaseComplete.value.createdAt)
.setLocale('sv-SE')
.toFormat('yyyy-MM-dd')}
</Box>
<Box
style={{
flex: 1,
display: 'flex',
alignItems: 'center',
}}
>
<Typography variant="h4">
Release time: {diff.days} days
</Typography>
</Box>
<Box
style={{
flex: 1,
display: 'flex',
alignItems: 'flex-start',
}}
>
RC created{' '}
{releaseCut.value?.createdAt &&
DateTime.fromISO(releaseCut.value.createdAt)
.setLocale('sv-SE')
.toFormat('yyyy-MM-dd')}
</Box>
</>
)}
</Box>
</Box>
);
}
@@ -0,0 +1,89 @@
/*
* 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 React from 'react';
import { Box, Paper, Typography } from '@material-ui/core';
import { getDecimalNumber } from './helpers/getDecimalNumber';
import { getSummary } from './helpers/getSummary';
interface SummaryProps {
summary: ReturnType<typeof getSummary>;
}
export function Summary({ summary }: SummaryProps) {
return (
<Paper
variant="outlined"
style={{
padding: 20,
marginLeft: 25,
marginRight: 25,
marginBottom: 25,
}}
>
<Box margin={1}>
<Typography variant="h4">Summary</Typography>
<Typography variant="body2">
Total releases: {summary.totalReleases}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Candidate</Typography>
<Typography variant="body2">
Release Candidate patches: {summary.totalCandidatePatches}
</Typography>
<Typography variant="body2">
Release Candidate patches per release:{' '}
{getDecimalNumber(
summary.totalCandidatePatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Version</Typography>
<Typography variant="body2">
Release Version patches: {summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Release Version patches per release:{' '}
{getDecimalNumber(
summary.totalVersionPatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Total</Typography>
<Typography variant="body2">
Patches: {summary.totalCandidatePatches + summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Patches per release:{' '}
{getDecimalNumber(
(summary.totalCandidatePatches + summary.totalVersionPatches) /
summary.totalReleases,
)}
</Typography>
</Box>
</Paper>
);
}
@@ -17,8 +17,8 @@
import React from 'react';
import { Alert } from '@material-ui/lab';
import { getMappedReleases } from './getMappedReleases';
import { getTags } from './getTags';
import { getMappedReleases } from './helpers/getMappedReleases';
import { getTags } from './helpers/getTags';
import { Project } from '../../contexts/ProjectContext';
interface WarnProps {
@@ -0,0 +1,27 @@
/*
* 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.
*/
export function getDecimalNumber(n: number) {
if (isNaN(n)) {
return 0;
}
if (n.toString().includes('.')) {
return n.toFixed(2);
}
return n;
}
@@ -14,10 +14,10 @@
* limitations under the License.
*/
import { calverRegexp } from '../../helpers/tagParts/getCalverTagParts';
import { GetAllReleasesResult } from '../../api/PluginApiClient';
import { Project } from '../../contexts/ProjectContext';
import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts';
import { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts';
import { GetAllReleasesResult } from '../../../api/PluginApiClient';
import { Project } from '../../../contexts/ProjectContext';
import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts';
export function getMappedReleases({
allReleases,
@@ -33,8 +33,14 @@ export function getMappedReleases({
releases: {
[baseVersion: string]: {
createdAt: string | null;
candidates: string[];
versions: string[];
candidates: {
tagName: string;
sha: string;
}[];
versions: {
tagName: string;
sha: string;
}[];
htmlUrl: string;
};
};
@@ -60,8 +66,10 @@ export function getMappedReleases({
if (!acc.releases[baseVersion]) {
acc.releases[baseVersion] = {
createdAt: release.createdAt,
candidates: prefix === 'rc' ? [release.tagName] : [],
versions: prefix === 'version' ? [release.tagName] : [],
candidates:
prefix === 'rc' ? [{ tagName: release.tagName, sha: '' }] : [],
versions:
prefix === 'version' ? [{ tagName: release.tagName, sha: '' }] : [],
htmlUrl: release.htmlUrl,
};
return acc;
@@ -14,11 +14,11 @@
* limitations under the License.
*/
import { calverRegexp } from '../../helpers/tagParts/getCalverTagParts';
import { GetAllTagsResult } from '../../api/PluginApiClient';
import { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts';
import { GetAllTagsResult } from '../../../api/PluginApiClient';
import { getMappedReleases } from './getMappedReleases';
import { Project } from '../../contexts/ProjectContext';
import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts';
import { Project } from '../../../contexts/ProjectContext';
import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts';
export function getTags({
allTags,
@@ -52,20 +52,34 @@ export function getTags({
return acc;
}
if (
prefix === 'rc' &&
!mappedReleases.releases[baseVersion].candidates.includes(tag.tagName)
) {
mappedReleases.releases[baseVersion].candidates.push(tag.tagName);
return acc;
if (prefix === 'rc') {
const existingEntry = mappedReleases.releases[
baseVersion
].candidates.find(({ tagName }) => tagName === tag.tagName);
if (existingEntry) {
existingEntry.sha = tag.sha;
} else {
mappedReleases.releases[baseVersion].candidates.push({
tagName: tag.tagName,
sha: tag.sha,
});
}
}
if (
prefix === 'version' &&
!mappedReleases.releases[baseVersion].versions.includes(tag.tagName)
) {
mappedReleases.releases[baseVersion].versions.push(tag.tagName);
return acc;
if (prefix === 'version') {
const existingEntry = mappedReleases.releases[
baseVersion
].versions.find(({ tagName }) => tagName === tag.tagName);
if (existingEntry) {
existingEntry.sha = tag.sha;
} else {
mappedReleases.releases[baseVersion].versions.push({
tagName: tag.tagName,
sha: tag.sha,
});
}
}
return acc;
@@ -0,0 +1,37 @@
/*
* 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 { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext';
import { useProjectContext } from '../../../contexts/ProjectContext';
export const useGetCommit = ({ ref }: { ref: string }) => {
const { pluginApiClient } = usePluginApiClientContext();
const { project } = useProjectContext();
const commit = useAsync(() =>
pluginApiClient.stats.getCommit({
owner: project.owner,
repo: project.repo,
ref,
}),
);
return {
commit,
};
};
@@ -52,6 +52,7 @@ describe('testHelpers', () => {
"stats": Object {
"getAllReleases": [MockFunction],
"getAllTags": [MockFunction],
"getCommit": [MockFunction],
},
},
"mockBumpedTag": "rc-2020.01.01_1337",
@@ -287,5 +287,9 @@ export const mockApiClient: IPluginApiClient = {
getAllReleases: jest.fn(async () => {
throw new Error('Not implemented');
}),
getCommit: jest.fn(async () => {
throw new Error('Not implemented');
}),
},
};