feat: Redesign git changes to split stage/unstaged files. (#1359)
* feat: Redesign git changes to split stage/unstaged files. Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * fixup Signed-off-by: Paolo Insogna <paolo@cowtech.it> * refactor: streamline git changes panel * fix: label staged and working diff tabs * fix: isolate staged and working diff files * fix: scope staged and working diff updates * fix: scope git row revert to working changes --------- Signed-off-by: Paolo Insogna <paolo@cowtech.it> Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
9af0de0056
commit
e16097b05d
@@ -0,0 +1,144 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { createGitIndexMutationQueue, type GitIndexMutationDirection } from './gitIndexMutationQueue';
|
||||
|
||||
type Deferred<T> = {
|
||||
promise: Promise<T>;
|
||||
resolve: (value: T) => void;
|
||||
reject: (error: unknown) => void;
|
||||
};
|
||||
|
||||
const createDeferred = <T>(): Deferred<T> => {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (error: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
const waitMicrotask = async () => {
|
||||
await Promise.resolve();
|
||||
};
|
||||
|
||||
describe('createGitIndexMutationQueue', () => {
|
||||
test('coalesces consecutive mutations with the same directory and direction', async () => {
|
||||
const calls: Array<{ direction: GitIndexMutationDirection; paths: string[] }> = [];
|
||||
const queue = createGitIndexMutationQueue({
|
||||
runMutation: async ({ direction, paths }) => {
|
||||
calls.push({ direction, paths });
|
||||
},
|
||||
onMutationComplete: () => {},
|
||||
onMutationError: () => {},
|
||||
onPathsComplete: () => {},
|
||||
scheduleFlush: () => queue.flush(),
|
||||
});
|
||||
|
||||
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
||||
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['b.ts', 'a.ts']) });
|
||||
queue.flush();
|
||||
await waitMicrotask();
|
||||
|
||||
expect(calls).toEqual([{ direction: 'stage', paths: ['a.ts', 'b.ts'] }]);
|
||||
});
|
||||
|
||||
test('serializes mutations and preserves alternating direction order', async () => {
|
||||
const first = createDeferred<void>();
|
||||
const calls: Array<{ direction: GitIndexMutationDirection; paths: string[] }> = [];
|
||||
let callCount = 0;
|
||||
|
||||
const queue = createGitIndexMutationQueue({
|
||||
runMutation: ({ direction, paths }) => {
|
||||
calls.push({ direction, paths });
|
||||
callCount += 1;
|
||||
return callCount === 1 ? first.promise : Promise.resolve();
|
||||
},
|
||||
onMutationComplete: () => {},
|
||||
onMutationError: () => {},
|
||||
onPathsComplete: () => {},
|
||||
scheduleFlush: () => queue.flush(),
|
||||
});
|
||||
|
||||
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
||||
queue.enqueue({ directory: '/repo', direction: 'unstage', paths: new Set(['a.ts']) });
|
||||
queue.flush();
|
||||
queue.flush();
|
||||
await waitMicrotask();
|
||||
|
||||
expect(calls).toEqual([{ direction: 'stage', paths: ['a.ts'] }]);
|
||||
expect(queue.isRunning()).toBe(true);
|
||||
|
||||
first.resolve();
|
||||
await waitMicrotask();
|
||||
await waitMicrotask();
|
||||
|
||||
expect(calls).toEqual([
|
||||
{ direction: 'stage', paths: ['a.ts'] },
|
||||
{ direction: 'unstage', paths: ['a.ts'] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('reports errors, completes paths, and continues the queue', async () => {
|
||||
const errors: unknown[] = [];
|
||||
const completedPaths: string[][] = [];
|
||||
const completedDirections: GitIndexMutationDirection[] = [];
|
||||
let callCount = 0;
|
||||
|
||||
const queue = createGitIndexMutationQueue({
|
||||
runMutation: async ({ direction }) => {
|
||||
callCount += 1;
|
||||
if (callCount === 1) {
|
||||
throw new Error(`${direction} failed`);
|
||||
}
|
||||
},
|
||||
onMutationComplete: ({ direction }) => {
|
||||
completedDirections.push(direction);
|
||||
},
|
||||
onMutationError: (_mutation, error) => {
|
||||
errors.push(error);
|
||||
},
|
||||
onPathsComplete: (paths) => {
|
||||
completedPaths.push(paths);
|
||||
},
|
||||
scheduleFlush: () => queue.flush(),
|
||||
});
|
||||
|
||||
queue.enqueue({ directory: '/repo', direction: 'stage', paths: new Set(['a.ts']) });
|
||||
queue.enqueue({ directory: '/repo', direction: 'unstage', paths: new Set(['b.ts']) });
|
||||
queue.flush();
|
||||
await waitMicrotask();
|
||||
await waitMicrotask();
|
||||
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(completedDirections).toEqual(['unstage']);
|
||||
expect(completedPaths).toEqual([['a.ts'], ['b.ts']]);
|
||||
});
|
||||
|
||||
test('passes rollback callbacks to error handlers', async () => {
|
||||
let rollbackCalled = false;
|
||||
const queue = createGitIndexMutationQueue({
|
||||
runMutation: async () => {
|
||||
throw new Error('stage failed');
|
||||
},
|
||||
onMutationComplete: () => {},
|
||||
onMutationError: (mutation) => {
|
||||
mutation.rollback?.();
|
||||
},
|
||||
onPathsComplete: () => {},
|
||||
scheduleFlush: () => queue.flush(),
|
||||
});
|
||||
|
||||
queue.enqueue({
|
||||
directory: '/repo',
|
||||
direction: 'stage',
|
||||
paths: new Set(['a.ts']),
|
||||
rollback: () => {
|
||||
rollbackCalled = true;
|
||||
},
|
||||
});
|
||||
queue.flush();
|
||||
await waitMicrotask();
|
||||
|
||||
expect(rollbackCalled).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user