Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 3 additions & 55 deletions electron/git/gitHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// Everything else this app does to git changes something. This is the half
// that only looks: the log, what a commit touched, what is changed right now,
// a file as it was, and which worktrees exist. Nothing here writes.
// and which worktrees exist. Nothing here writes.
//
// It is a separate module from gitBranches.js for the same reason that one
// exists — so it can be tested against real repositories (test/electron/git/gitHistory.test.js)
Expand All @@ -14,7 +14,7 @@
// each one is the kind that produces a plausible wrong answer rather than an
// error:
//
// - `git show --name-status` on a MERGE commit prints NOTHING. Not an error,
// - `--name-status` on a MERGE commit prints NOTHING. Not an error,
// not a diff — an empty list, because a diff against several parents is
// ambiguous and git declines to pick. A history panel reading it straight
// would draw every merge as a commit that changed no files.
Expand All @@ -27,7 +27,6 @@
// "Home" needs the project's routing, which lives in project/ — so that
// translation happens at the IPC edge, and this module stays about git.

import { toRecord } from '../../shared/core/record';
import { gitErrorDetail } from './git';
import type { Git } from './git';

Expand Down Expand Up @@ -169,21 +168,6 @@ async function log(
return { commits, atEnd: commits.length < limit };
}

/** What one commit changed: `{status, path, from}` per file. */
async function commitFiles(
git: Git,
{ projectPath, ref }: { readonly projectPath: string; readonly ref: string },
): Promise<FileChange[]> {
const { stdout } = await git(projectPath, [
'show',
'--name-status',
'--format=',
...DIFF_FLAGS,
ref,
]);
return parseNameStatus(stdout);
}

function parseNameStatus(stdout: string): FileChange[] {
const files: FileChange[] = [];
for (const line of stdout.split('\n')) {
Expand Down Expand Up @@ -320,32 +304,6 @@ async function allFiles(
});
}

/** A file's contents as they were at `ref`, or undefined when it wasn't there. */
async function fileAt(
git: Git,
{
projectPath,
ref,
path: filePath,
}: { readonly projectPath: string; readonly ref: string; readonly path: string },
): Promise<string | undefined> {
try {
const { stdout } = await git(projectPath, ['show', `${ref}:${filePath}`]);
return stdout;
} catch (error: unknown) {
// Not an error worth raising: a file that did not exist yet is a normal
// answer to "what did this look like then", and the caller wants to say
// "this page didn't exist yet" rather than show a failure.
const stderr = toRecord(error)?.['stderr'];
if (
/does not exist|exists on disk, but not in/i.test(typeof stderr === 'string' ? stderr : '')
) {
return undefined;
}
throw error;
}
}

export interface WorktreeInfo {
readonly path: string;
readonly head: string | undefined;
Expand Down Expand Up @@ -506,14 +464,4 @@ const describeFiles = <T extends { readonly path: string; readonly from?: string
): (T & FileDescription & { readonly from: string | undefined })[] =>
(files ?? []).map((file) => ({ ...file, ...describeFile(file.path), from: file.from }));

export {
allFiles,
log,
commitFiles,
status,
fileAt,
worktrees,
parseNameStatus,
describeFile,
describeFiles,
};
export { allFiles, log, status, worktrees, parseNameStatus, describeFile, describeFiles };
8 changes: 0 additions & 8 deletions electron/handlers/gitHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,6 @@ function registerGitGhStatusHandlers({ ipcMain }: Pick<MainHost, 'ipcMain'>): vo
ipcMain.handle('git:log', async (_event, payload) => readGitLog(payload));
ipcMain.handle('git:commitDiff', async (_event, payload) => readCommitDiff(git, payload));

ipcMain.handle('git:commitFiles', async (_event, { projectPath, ref }) =>
gitHistory.describeFiles(await gitHistory.commitFiles(git, { projectPath, ref })),
);

// Every file in the project, with what has happened to each — the file
// browser's list, and the same status the commit picker reads.
ipcMain.handle('git:allFiles', async (_event, { projectPath }) =>
Expand All @@ -183,10 +179,6 @@ function registerGitGhStatusHandlers({ ipcMain }: Pick<MainHost, 'ipcMain'>): vo
gitHistory.describeFiles(await gitHistory.status(git, { projectPath })),
);

ipcMain.handle('git:fileAt', async (_event, { projectPath, ref, path: filePath }) =>
gitHistory.fileAt(git, { projectPath, ref, path: filePath }),
);

ipcMain.handle('git:worktrees', async (_event, { projectPath }) =>
gitHistory.worktrees(git, { projectPath }),
);
Expand Down
16 changes: 5 additions & 11 deletions electron/handlers/thumbnails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { serverAlive } from '../preview/devServerChecks';
import type { DevServers } from './devServers';
import type { MainHost } from './mainHost';

export interface ThumbnailHost extends Pick<MainHost, 'send'> {
export interface ThumbnailHost {
readonly devServers: Pick<DevServers, 'current'>;
/** Electron's userData folder, where the pictures are kept. */
readonly userDataPath: () => string;
Expand Down Expand Up @@ -78,16 +78,10 @@ export class Thumbnails {
if (!thumbs.isStale(this.host.userDataPath(), projectPath)) {
return;
}
this.capture(projectPath)
.then((result) => {
if (result?.ok) {
this.host.send('recents:thumb', { projectPath });
}
})
.catch((error: unknown) => {
// A background refresh: the old picture stays, and the log says why.
console.error('[thumbs] capture failed:', error);
});
this.capture(projectPath).catch((error: unknown) => {
// A background refresh: the old picture stays, and the log says why.
console.error('[thumbs] capture failed:', error);
});
}, delay);
if (this.thumbTimer.unref) {
this.thumbTimer.unref();
Expand Down
1 change: 0 additions & 1 deletion electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ let mainWindow: Window | undefined = undefined;
// kept by the objects that own it (handlers/devServers.ts, thumbnails.ts).
const devServers = new DevServers({ send });
const thumbnails = new Thumbnails({
send,
devServers,
userDataPath: () => app.getPath('userData'),
});
Expand Down
7 changes: 0 additions & 7 deletions electron/preload/preloadBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,6 @@ function dataBridge() {
addRecent: invoke('recents:add'),
removeRecent: invoke('recents:remove'),
refreshThumb: invoke('recents:refreshThumb'),
onThumbUpdated: (callback: (payload: unknown) => void) => {
const listener = (_event: unknown, payload: unknown) => callback(payload);
ipcRenderer.on('recents:thumb', listener);
return () => ipcRenderer.removeListener('recents:thumb', listener);
},
} satisfies Partial<PreloadBridge>;
}

Expand Down Expand Up @@ -197,11 +192,9 @@ function pageBridge() {
previewStop: invoke('preview:stop'),
checkPreview: invoke('preview:check'),
gitLog: invoke('git:log'),
gitCommitFiles: invoke('git:commitFiles'),
gitCommitDiff: invoke('git:commitDiff'),
gitAllFiles: invoke('git:allFiles'),
gitStatus: invoke('git:status'),
gitFileAt: invoke('git:fileAt'),
gitWorktrees: invoke('git:worktrees'),
gitPark: invoke('git:park'),
gitUnpark: invoke('git:unpark'),
Expand Down
2 changes: 0 additions & 2 deletions shared/ipc/ipcPayloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,8 @@ export const IPC_PAYLOADS = {
'git:allFiles': object({ ...project }),
'git:checkout': object({ ...branch, create: maybeFlag, parkFirst: maybeFlag }),
'git:commit': object({ ...project, message: text, paths: optional(list(pathText)) }),
'git:commitFiles': object({ ...reference }),
'git:commitDiff': parseCommitDiffRequest,
'git:deleteBranch': object({ ...branch, force: maybeFlag }),
'git:fileAt': object({ ...reference, path: pathText }),
'git:ghStatus': pathText,
'git:info': pathText,
'git:init': pathText,
Expand Down
8 changes: 0 additions & 8 deletions shared/ipc/ipcResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
type WireFileModel,
type WireProjectFile,
type WireFileDescription,
type WireFileChange,
type WireDeleteOutcome,
type WireGitInfo,
type WireCommitInfo,
Expand Down Expand Up @@ -392,14 +391,7 @@ export interface IpcResults {
readonly ok: true;
readonly files: undefined | number;
};
readonly 'git:commitFiles': ReadonlyArray<
WireFileChange &
WireFileDescription & {
readonly from: undefined | string;
}
>;
readonly 'git:deleteBranch': WireDeleteOutcome;
readonly 'git:fileAt': undefined | string;
readonly 'git:ghStatus':
| {
readonly installed: false;
Expand Down
3 changes: 0 additions & 3 deletions shared/ipc/preloadApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,9 @@ interface InvokeChannels {
readonly previewStop: 'preview:stop';
readonly checkPreview: 'preview:check';
readonly gitLog: 'git:log';
readonly gitCommitFiles: 'git:commitFiles';
readonly gitCommitDiff: 'git:commitDiff';
readonly gitAllFiles: 'git:allFiles';
readonly gitStatus: 'git:status';
readonly gitFileAt: 'git:fileAt';
readonly gitWorktrees: 'git:worktrees';
readonly gitPark: 'git:park';
readonly gitUnpark: 'git:unpark';
Expand Down Expand Up @@ -159,7 +157,6 @@ export type PreloadBridge = InvokeSurface & {
readonly onCssChanged: (callback: () => void) => () => void;
readonly onCmsChanged: (callback: () => void) => () => void;
readonly onPageMaybeChanged: (callback: (data: unknown) => void) => () => void;
readonly onThumbUpdated: (callback: (data: unknown) => void) => () => void;
readonly onTerminalData: (callback: (data: unknown) => void) => () => void;
readonly onTerminalExit: (callback: (data: unknown) => void) => () => void;
readonly onTerminalProcess: (callback: (data: unknown) => void) => () => void;
Expand Down
2 changes: 1 addition & 1 deletion test/electron/app/mainBoundaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('the complete channel inventory matches real main and terminal registration
// Account sign-in adds one project-scoped channel for start, status, and cancellation.
// Published-site discovery has its own channel so remote lookups cannot block local status.
// Git pull and its background incoming check each have a project-scoped channel.
assert.equal(harness.handlers.size, 133);
assert.equal(harness.handlers.size, 131);
assert.equal(terminalChannels.length, 4);
assert.deepEqual(
[...Object.keys(IPC_PAYLOADS)].sort(),
Expand Down
45 changes: 8 additions & 37 deletions test/electron/git/gitHistory.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// git's output rather than a behaviour, and each one fails by producing a
// plausible wrong answer instead of an error:
//
// - `git show --name-status` on a merge commit prints nothing at all. A
// - `--name-status` on a merge commit prints nothing at all. A
// history panel reading it straight draws every merge as a commit that
// changed no files, and nothing anywhere reports a problem. That is the
// single most important case in this file.
Expand Down Expand Up @@ -133,17 +133,15 @@ const commit = async (directory, subject) => {
JSON.stringify(commits[1].parents),
);

const files = await history.commitFiles(git, { projectPath: directory, ref: commits[0].hash });
const listed = (await history.log(git, { projectPath: directory, withFiles: true })).commits;
const files = listed[0].files;
check('a commit lists what it changed', files.length === 1, JSON.stringify(files));
check('with the path', files[0].path === 'src/pages/about.astro', JSON.stringify(files[0]));
check('and the letter for a new file', files[0].status === 'A', JSON.stringify(files[0]));

// The root commit: no parent to diff against, and git handles it, but only
// if the flags do not assume there is one.
const rootFiles = await history.commitFiles(git, {
projectPath: directory,
ref: commits[1].hash,
});
const rootFiles = listed[1].files;
check(
'the very first commit lists its files too',
rootFiles.length === 1,
Expand Down Expand Up @@ -271,7 +269,8 @@ const commit = async (directory, subject) => {
);
check('so the whole history is there', commits.length === 4, String(commits.length));

const files = await history.commitFiles(git, { projectPath: directory, ref: merge.hash });
const listed = (await history.log(git, { projectPath: directory, withFiles: true })).commits;
const files = listed.find((item) => item.hash === merge.hash)?.files ?? [];
check('a merge reports the files it brought in', files.length > 0, JSON.stringify(files));
check(
'namely the branch’s work',
Expand All @@ -293,7 +292,8 @@ const commit = async (directory, subject) => {
await sh(directory, 'mv', 'src/pages/about.astro', 'src/pages/contact.astro');
const ref = await commit(directory, 'renamed');

const files = await history.commitFiles(git, { projectPath: directory, ref });
const listed = (await history.log(git, { projectPath: directory, withFiles: true })).commits;
const files = listed.find((item) => item.hash === ref)?.files ?? [];
check(
'a rename is one entry, not a delete and an add',
files.length === 1,
Expand Down Expand Up @@ -350,35 +350,6 @@ const commit = async (directory, subject) => {
);
}

// --- A file as it was ----------------------------------------------------
{
const directory = await repo('fileat');
cleanup.push(directory);
write(directory, 'src/pages/index.astro', 'the old words\n');
const first = await commit(directory, 'first');
write(directory, 'src/pages/index.astro', 'the new words\n');
await commit(directory, 'second');

const then = await history.fileAt(git, {
projectPath: directory,
ref: first,
path: 'src/pages/index.astro',
});
check('a file can be read as it was', then.trim() === 'the old words', JSON.stringify(then));
// "It did not exist yet" is a real answer to the question, and the panel
// wants to say so rather than show a failure.
const missing = await history.fileAt(git, {
projectPath: directory,
ref: first,
path: 'src/pages/later.astro',
});
check(
'a file that did not exist yet reads as undefined',
missing === undefined,
JSON.stringify(missing),
);
}

// --- Paging --------------------------------------------------------------
{
const directory = await repo('paging');
Expand Down
2 changes: 0 additions & 2 deletions test/electron/preview/thumbnailStartup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ test('opening a project cancels active and queued thumbnail startups', async (co
}
});
const thumbnails = new Thumbnails({
send: () => {},
devServers: { current: () => undefined },
userDataPath: () => '/unused',
});
Expand Down Expand Up @@ -76,7 +75,6 @@ test('closing during thumbnail port lookup prevents server startup', async (cont
throw new Error('A cancelled thumbnail must not start a server');
});
const thumbnails = new Thumbnails({
send: () => {},
devServers: { current: () => undefined },
userDataPath: () => '/unused',
});
Expand Down
Loading