diff --git a/node-src/git/git.ts b/node-src/git/git.ts index c1db28f69..6e2e25618 100644 --- a/node-src/git/git.ts +++ b/node-src/git/git.ts @@ -90,6 +90,29 @@ export async function getBranch() { } } +// Retrieve the hash of all uncommitted files, which includes staged, unstaged, and untracked files, +// excluding deleted files (which can't be hashed) and ignored files. There is no one single Git +// command to reliably get this information, so we use a combination of commands grouped together. +export async function getUncommittedHash() { + const listStagedFiles = 'git diff --name-only --diff-filter=d --cached'; + const listUnstagedFiles = 'git diff --name-only --diff-filter=d'; + const listUntrackedFiles = 'git ls-files --others --exclude-standard'; + const listUncommittedFiles = [listStagedFiles, listUnstagedFiles, listUntrackedFiles].join(';'); + + const uncommittedHash = ( + await execGitCommand( + // Pass the combined list of filenames to hash-object to retrieve a list of hashes. Then pass + // the list of hashes to hash-object again to retrieve a single hash of all hashes. We use + // stdin to avoid the limit on command line arguments. + `(${listUncommittedFiles}) | git hash-object --stdin-paths | git hash-object --stdin` + ) + ).trim(); + + // In case there are no uncommited changes (empty list), we always get this same hash. + const noChangesHash = 'e69de29bb2d1d6434b8b29ae775ad8c2e48c5391'; + return uncommittedHash === noChangesHash ? '' : uncommittedHash; +} + export async function hasPreviousCommit() { const result = await execGitCommand(`git --no-pager log -n 1 --skip=1 --format="%H"`); return !!result.trim(); diff --git a/node-src/index.ts b/node-src/index.ts index 38d98f37a..0e9212e87 100644 --- a/node-src/index.ts +++ b/node-src/index.ts @@ -14,7 +14,7 @@ import checkPackageJson from './lib/checkPackageJson'; import { writeChromaticDiagnostics } from './lib/writeChromaticDiagnostics'; import invalidPackageJson from './ui/messages/errors/invalidPackageJson'; import noPackageJson from './ui/messages/errors/noPackageJson'; -import { getBranch, getCommit, getSlug } from './git/git'; +import { getBranch, getCommit, getSlug, getUncommittedHash } from './git/git'; /** Make keys of `T` outside of `R` optional. @@ -112,11 +112,12 @@ export async function runAll(ctx, options?: Options) { } } -export type GitInfo = { +export interface GitInfo { branch: string; commit: string; slug: string; -}; + uncommittedHash: string; +} export async function getGitInfo(): Promise { const branch = await getBranch(); @@ -126,5 +127,6 @@ export async function getGitInfo(): Promise { const [ownerName, repoName, ...rest] = slug ? slug.split('/') : []; const isValidSlug = !!ownerName && !!repoName && !rest.length; - return { branch, commit, slug: isValidSlug ? slug : '' }; + const uncommittedHash = await getUncommittedHash(); + return { branch, commit, slug: isValidSlug ? slug : '', uncommittedHash }; } diff --git a/node-src/main.test.ts b/node-src/main.test.ts index 425d4fbc2..e3de1d5a0 100644 --- a/node-src/main.test.ts +++ b/node-src/main.test.ts @@ -264,6 +264,7 @@ jest.mock('./git/git', () => ({ getVersion: () => Promise.resolve('2.24.1'), getChangedFiles: () => Promise.resolve(['src/foo.stories.js']), getRepositoryRoot: () => Promise.resolve(process.cwd()), + getUncommittedHash: () => Promise.resolve('abc123'), })); jest.mock('./git/getParentCommits', () => ({ diff --git a/node-src/tasks/gitInfo.test.ts b/node-src/tasks/gitInfo.test.ts index 8b658991a..a25a4fdaf 100644 --- a/node-src/tasks/gitInfo.test.ts +++ b/node-src/tasks/gitInfo.test.ts @@ -17,6 +17,9 @@ const getChangedFilesWithReplacement = < >getChangedFilesWithReplacementUnmocked; const getSlug = >git.getSlug; const getVersion = >git.getVersion; +const getUncommittedHash = >( + git.getUncommittedHash +); const getBaselineBuilds = >( getBaselineBuildsUnmocked @@ -42,6 +45,7 @@ const client = { runQuery: jest.fn(), setAuthorization: jest.fn() }; beforeEach(() => { getCommitAndBranch.mockResolvedValue(commitInfo); + getUncommittedHash.mockResolvedValue('abc123'); getParentCommits.mockResolvedValue(['asd2344']); getBaselineBuilds.mockResolvedValue([]); getChangedFilesWithReplacement.mockResolvedValue({ changedFiles: [] }); diff --git a/node-src/tasks/gitInfo.ts b/node-src/tasks/gitInfo.ts index 9bdcc08a0..317d2e62c 100644 --- a/node-src/tasks/gitInfo.ts +++ b/node-src/tasks/gitInfo.ts @@ -1,7 +1,7 @@ import picomatch from 'picomatch'; import getCommitAndBranch from '../git/getCommitAndBranch'; -import { getSlug, getVersion } from '../git/git'; +import { getSlug, getUncommittedHash, getVersion } from '../git/git'; import { getParentCommits } from '../git/getParentCommits'; import { getBaselineBuilds } from '../git/getBaselineBuilds'; import { exitCodes, setExitCode } from '../lib/setExitCode'; @@ -62,8 +62,12 @@ export const setGitInfo = async (ctx: Context, task: Task) => { } = ctx.options; ctx.git = { - version: await getVersion(), ...(await getCommitAndBranch(ctx, { branchName, patchBaseRef, ci })), + uncommittedHash: await getUncommittedHash().catch((e) => { + ctx.log.warn('Failed to retrieve uncommitted files hash', e); + return null; + }), + version: await getVersion(), }; if (!ctx.git.slug) { diff --git a/node-src/types.ts b/node-src/types.ts index 96219cb75..23b799c0d 100644 --- a/node-src/types.ts +++ b/node-src/types.ts @@ -144,6 +144,7 @@ export interface Context { committedAt: number; slug?: string; mergeCommit?: string; + uncommittedHash?: string; parentCommits?: string[]; baselineCommits?: string[]; changedFiles?: string[]; diff --git a/package.json b/package.json index dc742ad19..cf5fe93e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chromatic", - "version": "6.21.0", + "version": "6.22.0-canary.5", "description": "Automate visual testing across browsers. Gather UI feedback. Versioned documentation.", "keywords": [ "storybook-addon",