fix: handle autogit author failures
This commit is contained in:
@@ -62,6 +62,9 @@ files:
|
||||
command.git.resolveConflicts: 871ac36968cfca3d2297a89b28b25dee
|
||||
command.git.viewChanges: b2699edf69d8e1031afce2b2f94e5c8d
|
||||
git.repository.select: 33fcf2b3ec4686d9cd06051c726d0ba2
|
||||
git.toast.autoGitFailed: 3e5d7e183a78e1f54c87ad0dba8a302b
|
||||
git.toast.commitFailed: bb50986d29c30042bed1446f8adaa544
|
||||
git.toast.missingAuthor: 0d4e7a7d6b6a3f883146dd86c9cdca09
|
||||
command.view.editorOnly: 8355e056b32086b9190d639be290dda8
|
||||
command.view.editorNoteList: b9604b1609eb6f581cd9570dca72d3f0
|
||||
command.view.fullLayout: 1cebdf839eee2b1713828731aaa3b507
|
||||
|
||||
@@ -968,6 +968,7 @@ function App() {
|
||||
setToastMessage,
|
||||
onPushRejected: autoSync.handlePushRejected,
|
||||
automaticVaultPaths: activeGitRepositoryPaths,
|
||||
locale: appLocale,
|
||||
manualVaultPath: gitSurfaces.commitRepositoryPath,
|
||||
vaultPath: resolvedPath,
|
||||
})
|
||||
|
||||
@@ -2,6 +2,55 @@ import { act, renderHook } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAutoGit } from './useAutoGit'
|
||||
|
||||
type AutoGitOptions = Parameters<typeof useAutoGit>[0]
|
||||
|
||||
function defaultAutoGitOptions(onCheckpoint: AutoGitOptions['onCheckpoint']): AutoGitOptions {
|
||||
return {
|
||||
enabled: true,
|
||||
idleThresholdSeconds: 1,
|
||||
inactiveThresholdSeconds: 1,
|
||||
isGitVault: true,
|
||||
hasPendingChanges: true,
|
||||
hasUnsavedChanges: false,
|
||||
onCheckpoint,
|
||||
}
|
||||
}
|
||||
|
||||
function renderAutoGit(overrides: Partial<AutoGitOptions> = {}) {
|
||||
const onCheckpoint = overrides.onCheckpoint ?? vi.fn().mockResolvedValue(true)
|
||||
const hook = renderHook(() => useAutoGit({
|
||||
...defaultAutoGitOptions(onCheckpoint),
|
||||
...overrides,
|
||||
onCheckpoint,
|
||||
}))
|
||||
|
||||
return { onCheckpoint, ...hook }
|
||||
}
|
||||
|
||||
async function advanceAutoGitBy(ms: number) {
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(ms)
|
||||
})
|
||||
}
|
||||
|
||||
async function expectSingleCheckpointPerActivityBurst(
|
||||
onCheckpoint: AutoGitOptions['onCheckpoint'],
|
||||
recordActivity: () => void,
|
||||
) {
|
||||
await advanceAutoGitBy(1_000)
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(1)
|
||||
|
||||
await advanceAutoGitBy(3_000)
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => {
|
||||
recordActivity()
|
||||
})
|
||||
|
||||
await advanceAutoGitBy(1_000)
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(2)
|
||||
}
|
||||
|
||||
describe('useAutoGit', () => {
|
||||
let hasFocus = true
|
||||
|
||||
@@ -17,39 +66,23 @@ describe('useAutoGit', () => {
|
||||
})
|
||||
|
||||
it('triggers an idle checkpoint after the configured threshold', async () => {
|
||||
const onCheckpoint = vi.fn().mockResolvedValue(true)
|
||||
renderHook(() => useAutoGit({
|
||||
enabled: true,
|
||||
const { onCheckpoint } = renderAutoGit({
|
||||
idleThresholdSeconds: 3,
|
||||
inactiveThresholdSeconds: 2,
|
||||
isGitVault: true,
|
||||
hasPendingChanges: true,
|
||||
hasUnsavedChanges: false,
|
||||
onCheckpoint,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2_999)
|
||||
})
|
||||
|
||||
await advanceAutoGitBy(2_999)
|
||||
expect(onCheckpoint).not.toHaveBeenCalled()
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
})
|
||||
await advanceAutoGitBy(1)
|
||||
expect(onCheckpoint).toHaveBeenCalledWith('idle')
|
||||
})
|
||||
|
||||
it('waits for the app to become inactive before triggering the inactive checkpoint', async () => {
|
||||
const onCheckpoint = vi.fn().mockResolvedValue(true)
|
||||
renderHook(() => useAutoGit({
|
||||
enabled: true,
|
||||
const { onCheckpoint } = renderAutoGit({
|
||||
idleThresholdSeconds: 10,
|
||||
inactiveThresholdSeconds: 2,
|
||||
isGitVault: true,
|
||||
hasPendingChanges: true,
|
||||
hasUnsavedChanges: false,
|
||||
onCheckpoint,
|
||||
}))
|
||||
})
|
||||
|
||||
hasFocus = false
|
||||
await act(async () => {
|
||||
@@ -61,53 +94,27 @@ describe('useAutoGit', () => {
|
||||
})
|
||||
|
||||
it('does not trigger while the editor still has unsaved changes', async () => {
|
||||
const onCheckpoint = vi.fn().mockResolvedValue(true)
|
||||
renderHook(() => useAutoGit({
|
||||
enabled: true,
|
||||
idleThresholdSeconds: 1,
|
||||
inactiveThresholdSeconds: 1,
|
||||
isGitVault: true,
|
||||
hasPendingChanges: true,
|
||||
const { onCheckpoint } = renderAutoGit({
|
||||
hasUnsavedChanges: true,
|
||||
onCheckpoint,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(2_000)
|
||||
})
|
||||
|
||||
await advanceAutoGitBy(2_000)
|
||||
|
||||
expect(onCheckpoint).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('only triggers once per activity burst until activity is recorded again', async () => {
|
||||
const onCheckpoint = vi.fn().mockResolvedValue(true)
|
||||
const { result } = renderHook(() => useAutoGit({
|
||||
enabled: true,
|
||||
idleThresholdSeconds: 1,
|
||||
inactiveThresholdSeconds: 1,
|
||||
isGitVault: true,
|
||||
hasPendingChanges: true,
|
||||
hasUnsavedChanges: false,
|
||||
const { onCheckpoint, result } = renderAutoGit()
|
||||
await expectSingleCheckpointPerActivityBurst(onCheckpoint, result.current.recordActivity)
|
||||
})
|
||||
|
||||
it('does not retry a rejected checkpoint for the same activity burst', async () => {
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const onCheckpoint = vi.fn().mockRejectedValue(new Error('Author identity unknown'))
|
||||
const { result } = renderAutoGit({
|
||||
onCheckpoint,
|
||||
}))
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
})
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(1)
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(3_000)
|
||||
})
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(1)
|
||||
|
||||
act(() => {
|
||||
result.current.recordActivity()
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
})
|
||||
expect(onCheckpoint).toHaveBeenCalledTimes(2)
|
||||
await expectSingleCheckpointPerActivityBurst(onCheckpoint, result.current.recordActivity)
|
||||
expect(warnSpy).toHaveBeenCalledWith('[git] Auto-commit failed:', expect.any(Error))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -125,7 +125,10 @@ export function useAutoGit({
|
||||
|
||||
void onCheckpoint(trigger).then((didRun) => {
|
||||
if (didRun) markTriggerAsHandled(lastTriggeredRef.current, trigger, lastActivityAt)
|
||||
}).catch((err) => console.warn('[git] Auto-commit failed:', err))
|
||||
}).catch((err) => {
|
||||
markTriggerAsHandled(lastTriggeredRef.current, trigger, lastActivityAt)
|
||||
console.warn('[git] Auto-commit failed:', err)
|
||||
})
|
||||
})
|
||||
|
||||
const updateAppActivity = useEffectEvent((active: boolean) => {
|
||||
|
||||
@@ -151,6 +151,26 @@ describe('useCommitFlow', () => {
|
||||
expect(setToastMessage).toHaveBeenCalledWith('Committed and pushed')
|
||||
})
|
||||
|
||||
it('runAutomaticCheckpoint treats commit failures as handled after showing recovery feedback', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
mockInvokeFn.mockImplementation((command: string) => {
|
||||
if (command === 'git_commit') return Promise.reject(new Error('Author identity unknown'))
|
||||
throw new Error(`Unexpected command: ${command}`)
|
||||
})
|
||||
const { result } = renderCommitFlow()
|
||||
let didHandleCheckpoint = false
|
||||
|
||||
await act(async () => {
|
||||
didHandleCheckpoint = await result.current.runAutomaticCheckpoint()
|
||||
})
|
||||
|
||||
expect(didHandleCheckpoint).toBe(true)
|
||||
expect(setToastMessage).toHaveBeenCalledWith(
|
||||
'Set a Git author before AutoGit can commit. Run git config --global user.name "Your Name" and git config --global user.email you@example.com.',
|
||||
)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('runAutomaticCheckpoint commits and pushes all active repositories', async () => {
|
||||
const resolveRemoteStatusForVaultPath = vi.fn().mockResolvedValue({
|
||||
branch: 'main',
|
||||
@@ -191,6 +211,37 @@ describe('useCommitFlow', () => {
|
||||
expect(setToastMessage).toHaveBeenCalledWith('AutoGit checkpointed 2 repositories')
|
||||
})
|
||||
|
||||
it('runAutomaticCheckpoint treats multi-repository commit failures as handled', async () => {
|
||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
const loadModifiedFilesForVaultPath = vi.fn((vaultPath: string) => Promise.resolve([{
|
||||
path: `${vaultPath}/note.md`,
|
||||
relativePath: 'note.md',
|
||||
status: 'modified',
|
||||
}]))
|
||||
mockInvokeFn.mockImplementation((command: string) => {
|
||||
if (command === 'git_commit') return Promise.reject(new Error('Please tell me who you are'))
|
||||
throw new Error(`Unexpected command: ${command}`)
|
||||
})
|
||||
|
||||
const { result } = renderCommitFlow({
|
||||
automaticVaultPaths: ['/vault', '/work'],
|
||||
loadModifiedFilesForVaultPath,
|
||||
})
|
||||
let didHandleCheckpoint = false
|
||||
|
||||
await act(async () => {
|
||||
didHandleCheckpoint = await result.current.runAutomaticCheckpoint()
|
||||
})
|
||||
|
||||
expect(didHandleCheckpoint).toBe(true)
|
||||
expect(loadModifiedFilesForVaultPath).toHaveBeenCalledWith('/vault')
|
||||
expect(loadModifiedFilesForVaultPath).toHaveBeenCalledWith('/work')
|
||||
expect(setToastMessage).toHaveBeenCalledWith(
|
||||
'Set a Git author before AutoGit can commit. Run git config --global user.name "Your Name" and git config --global user.email you@example.com.',
|
||||
)
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('runAutomaticCheckpoint retries push-only when local commits are already ahead', async () => {
|
||||
resolveRemoteStatusForVaultPath.mockResolvedValue({ branch: 'main', ahead: 2, behind: 0, hasRemote: true })
|
||||
loadModifiedFilesForVaultPath.mockResolvedValue([])
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState, type MutableRefObject } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type MutableRefObject } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type { GitPushResult, GitRemoteStatus, ModifiedFile } from '../types'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
import { isTauri, mockInvoke } from '../mock-tauri'
|
||||
import { generateAutomaticCommitMessage } from '../utils/automaticCommitMessage'
|
||||
import { createTranslator, type AppLocale } from '../lib/i18n'
|
||||
|
||||
export type CommitMode = 'push' | 'local'
|
||||
|
||||
@@ -27,6 +28,7 @@ interface CommitFlowConfig {
|
||||
setToastMessage: (msg: string | null) => void
|
||||
onPushRejected?: () => void
|
||||
automaticVaultPaths?: string[]
|
||||
locale?: AppLocale
|
||||
manualVaultPath?: string
|
||||
vaultPath: string
|
||||
}
|
||||
@@ -80,6 +82,7 @@ type FinalizeCheckpointConfig = Pick<
|
||||
CommitFlowConfig,
|
||||
'loadModifiedFiles' | 'resolveRemoteStatusForVaultPath' | 'setToastMessage' | 'onPushRejected'
|
||||
>
|
||||
type Translator = ReturnType<typeof createTranslator>
|
||||
|
||||
interface FinalizeCheckpointArgs extends FinalizeCheckpointConfig {
|
||||
result: CommitResult
|
||||
@@ -132,10 +135,31 @@ function isPushRejected(result: CommitResult): boolean {
|
||||
return result.status === 'rejected'
|
||||
}
|
||||
|
||||
function formatCommitError(error: unknown): string {
|
||||
function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function isMissingGitAuthorIdentityError(message: string): boolean {
|
||||
const normalized = message.toLowerCase()
|
||||
return normalized.includes('author identity unknown')
|
||||
|| normalized.includes('please tell me who you are')
|
||||
|| normalized.includes('unable to auto-detect email address')
|
||||
|| (normalized.includes('user.email') && normalized.includes('not set'))
|
||||
|| (normalized.includes('user.name') && normalized.includes('not set'))
|
||||
}
|
||||
|
||||
function formatCommitFailureToast(error: unknown, t: Translator): string {
|
||||
const message = errorMessage(error)
|
||||
if (isMissingGitAuthorIdentityError(message)) return t('git.toast.missingAuthor')
|
||||
return t('git.toast.commitFailed', { error: message })
|
||||
}
|
||||
|
||||
function formatAutoGitFailureToast(error: unknown, t: Translator): string {
|
||||
const message = errorMessage(error)
|
||||
if (isMissingGitAuthorIdentityError(message)) return t('git.toast.missingAuthor')
|
||||
return t('git.toast.autoGitFailed', { error: message })
|
||||
}
|
||||
|
||||
function shouldRetryPush(remoteStatus: GitRemoteStatus | null): boolean {
|
||||
return remoteStatus?.hasRemote === true && remoteStatus.ahead > 0
|
||||
}
|
||||
@@ -227,15 +251,15 @@ async function checkpointRepository(
|
||||
return { action, remoteStatus, result, status: 'executed', vaultPath }
|
||||
}
|
||||
|
||||
function multiRepositoryCheckpointToast(results: RepositoryCheckpointResult[]): string {
|
||||
function multiRepositoryCheckpointToast(results: RepositoryCheckpointResult[], t: Translator): string {
|
||||
const executedCount = results.filter((result) => result.status === 'executed').length
|
||||
const failedCount = results.filter((result) => result.status === 'failed').length
|
||||
const rejectedCount = results.filter((result) => result.result && isPushRejected(result.result)).length
|
||||
|
||||
if (executedCount === 0) {
|
||||
const firstError = results.find((result) => result.status === 'failed')?.error
|
||||
return firstError
|
||||
? `AutoGit failed: ${formatCommitError(firstError)}`
|
||||
return firstError !== undefined
|
||||
? formatAutoGitFailureToast(firstError, t)
|
||||
: 'Nothing to commit or push'
|
||||
}
|
||||
|
||||
@@ -331,6 +355,7 @@ async function checkpointRepositories(
|
||||
async function runMultipleRepositoryCheckpoint(
|
||||
targetVaultPaths: string[],
|
||||
config: AutomaticCheckpointRunConfig,
|
||||
t: Translator,
|
||||
): Promise<boolean> {
|
||||
const results = await checkpointRepositories(targetVaultPaths, config)
|
||||
|
||||
@@ -338,13 +363,13 @@ async function runMultipleRepositoryCheckpoint(
|
||||
config.onPushRejected?.()
|
||||
}
|
||||
|
||||
config.setToastMessage(multiRepositoryCheckpointToast(results))
|
||||
config.setToastMessage(multiRepositoryCheckpointToast(results, t))
|
||||
await runCheckpointRefresh({
|
||||
loadModifiedFiles: config.loadModifiedFiles,
|
||||
resolveRemoteStatusForVaultPath: config.resolveRemoteStatusForVaultPath,
|
||||
vaultPaths: targetVaultPaths,
|
||||
})
|
||||
return results.some((result) => result.status === 'executed')
|
||||
return results.some((result) => result.status === 'executed' || result.status === 'failed')
|
||||
}
|
||||
|
||||
function useAutomaticCheckpointAction({
|
||||
@@ -357,8 +382,10 @@ function useAutomaticCheckpointAction({
|
||||
onPushRejected,
|
||||
automaticVaultPaths,
|
||||
vaultPath,
|
||||
t,
|
||||
}: CommitFlowConfig & {
|
||||
checkpointInFlightRef: MutableRefObject<boolean>
|
||||
t: Translator
|
||||
}) {
|
||||
return useCallback(async ({
|
||||
savePendingBeforeCommit = false,
|
||||
@@ -380,13 +407,13 @@ function useAutomaticCheckpointAction({
|
||||
setToastMessage,
|
||||
vaultPath,
|
||||
}
|
||||
return targetVaultPaths.length === 1
|
||||
return await (targetVaultPaths.length === 1
|
||||
? runSingleRepositoryCheckpoint(targetVaultPaths[0], runConfig)
|
||||
: runMultipleRepositoryCheckpoint(targetVaultPaths, runConfig)
|
||||
: runMultipleRepositoryCheckpoint(targetVaultPaths, runConfig, t))
|
||||
} catch (err) {
|
||||
console.error('Commit failed:', err)
|
||||
setToastMessage(`Commit failed: ${formatCommitError(err)}`)
|
||||
return false
|
||||
setToastMessage(formatCommitFailureToast(err, t))
|
||||
return true
|
||||
} finally {
|
||||
checkpointInFlightRef.current = false
|
||||
}
|
||||
@@ -399,6 +426,7 @@ function useAutomaticCheckpointAction({
|
||||
resolveRemoteStatusForVaultPath,
|
||||
savePending,
|
||||
setToastMessage,
|
||||
t,
|
||||
vaultPath,
|
||||
])
|
||||
}
|
||||
@@ -413,6 +441,7 @@ function useManualCommitPushAction({
|
||||
manualVaultPath,
|
||||
vaultPath,
|
||||
setShowCommitDialog,
|
||||
t,
|
||||
}: Pick<
|
||||
CommitFlowConfig,
|
||||
| 'savePending'
|
||||
@@ -425,6 +454,7 @@ function useManualCommitPushAction({
|
||||
> & {
|
||||
checkpointInFlightRef: MutableRefObject<boolean>
|
||||
setShowCommitDialog: (open: boolean) => void
|
||||
t: Translator
|
||||
}) {
|
||||
return useCallback(async (message: string) => {
|
||||
setShowCommitDialog(false)
|
||||
@@ -453,7 +483,7 @@ function useManualCommitPushAction({
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Commit failed:', err)
|
||||
setToastMessage(`Commit failed: ${formatCommitError(err)}`)
|
||||
setToastMessage(formatCommitFailureToast(err, t))
|
||||
} finally {
|
||||
checkpointInFlightRef.current = false
|
||||
}
|
||||
@@ -466,6 +496,7 @@ function useManualCommitPushAction({
|
||||
savePending,
|
||||
setShowCommitDialog,
|
||||
setToastMessage,
|
||||
t,
|
||||
vaultPath,
|
||||
])
|
||||
}
|
||||
@@ -553,7 +584,7 @@ function useOpenCommitDialog({
|
||||
setShowCommitDialog(true)
|
||||
} catch (err) {
|
||||
console.error('Commit dialog failed:', err)
|
||||
setToastMessage(`Commit dialog failed: ${formatCommitError(err)}`)
|
||||
setToastMessage(`Commit dialog failed: ${errorMessage(err)}`)
|
||||
} finally {
|
||||
dialogOpeningRef.current = false
|
||||
setDialogOpening(false)
|
||||
@@ -582,6 +613,7 @@ export function useCommitFlow({
|
||||
setToastMessage,
|
||||
onPushRejected,
|
||||
automaticVaultPaths,
|
||||
locale,
|
||||
manualVaultPath,
|
||||
vaultPath,
|
||||
}: CommitFlowConfig) {
|
||||
@@ -591,6 +623,7 @@ export function useCommitFlow({
|
||||
const checkpointInFlightRef = useRef(false)
|
||||
const dialogOpeningRef = useRef(false)
|
||||
const commitModeVaultPathRef = useRef<string | null>(null)
|
||||
const t = useMemo(() => createTranslator(locale), [locale])
|
||||
|
||||
const openCommitDialog = useOpenCommitDialog({
|
||||
dialogOpeningRef,
|
||||
@@ -616,6 +649,7 @@ export function useCommitFlow({
|
||||
onPushRejected,
|
||||
automaticVaultPaths,
|
||||
vaultPath,
|
||||
t,
|
||||
})
|
||||
|
||||
const handleCommitPush = useManualCommitPushAction({
|
||||
@@ -628,6 +662,7 @@ export function useCommitFlow({
|
||||
manualVaultPath,
|
||||
vaultPath,
|
||||
setShowCommitDialog,
|
||||
t,
|
||||
})
|
||||
useCommitModeRefresh({
|
||||
commitModeVaultPathRef,
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Konflikte auflösen",
|
||||
"command.git.viewChanges": "Ausstehende Änderungen anzeigen",
|
||||
"git.repository.select": "Repository",
|
||||
"git.toast.autoGitFailed": "AutoGit ist fehlgeschlagen: {error}",
|
||||
"git.toast.commitFailed": "Commit fehlgeschlagen: {error}",
|
||||
"git.toast.missingAuthor": "Legen Sie einen Git-Autor fest, bevor AutoGit einen Commit durchführen kann. Führen Sie git config --global user.name \"Ihr Name\" und git config --global user.email you@example.com aus.",
|
||||
"command.view.editorOnly": "Nur Editor",
|
||||
"command.view.editorNoteList": "Editor + Notizenliste",
|
||||
"command.view.fullLayout": "Vollständiges Layout",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Resolve Conflicts",
|
||||
"command.git.viewChanges": "View Pending Changes",
|
||||
"git.repository.select": "Repository",
|
||||
"git.toast.autoGitFailed": "AutoGit failed: {error}",
|
||||
"git.toast.commitFailed": "Commit failed: {error}",
|
||||
"git.toast.missingAuthor": "Set a Git author before AutoGit can commit. Run git config --global user.name \"Your Name\" and git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Editor Only",
|
||||
"command.view.editorNoteList": "Editor + Note List",
|
||||
"command.view.fullLayout": "Full Layout",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Resolver conflictos",
|
||||
"command.git.viewChanges": "Ver cambios pendientes",
|
||||
"git.repository.select": "Repositorio",
|
||||
"git.toast.autoGitFailed": "Error de AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Error al confirmar: {error}",
|
||||
"git.toast.missingAuthor": "Establezca un autor de Git antes de que AutoGit pueda realizar la confirmación. Ejecute git config --global user.name \"Su nombre\" y git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Solo editor",
|
||||
"command.view.editorNoteList": "Editor + Lista de notas",
|
||||
"command.view.fullLayout": "Diseño completo",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Resolver conflictos",
|
||||
"command.git.viewChanges": "Ver cambios pendientes",
|
||||
"git.repository.select": "Repositorio",
|
||||
"git.toast.autoGitFailed": "Error de AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Error al confirmar: {error}",
|
||||
"git.toast.missingAuthor": "Establezca un autor de Git antes de que AutoGit pueda realizar la confirmación. Ejecute git config --global user.name \"Su nombre\" y git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Solo editor",
|
||||
"command.view.editorNoteList": "Editor + Lista de notas",
|
||||
"command.view.fullLayout": "Diseño completo",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Résoudre les conflits",
|
||||
"command.git.viewChanges": "Afficher les modifications en attente",
|
||||
"git.repository.select": "Dépôt",
|
||||
"git.toast.autoGitFailed": "Échec d'AutoGit : {error}",
|
||||
"git.toast.commitFailed": "Échec de la validation : {error}",
|
||||
"git.toast.missingAuthor": "Définissez un auteur Git avant qu'AutoGit puisse valider. Exécutez git config --global user.name \"Votre nom\" et git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Éditeur uniquement",
|
||||
"command.view.editorNoteList": "Éditeur + liste de notes",
|
||||
"command.view.fullLayout": "Mise en page complète",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Risolvi conflitti",
|
||||
"command.git.viewChanges": "Visualizza modifiche in sospeso",
|
||||
"git.repository.select": "Repository",
|
||||
"git.toast.autoGitFailed": "AutoGit non riuscito: {error}",
|
||||
"git.toast.commitFailed": "Commit non riuscito: {error}",
|
||||
"git.toast.missingAuthor": "Imposta un autore Git prima che AutoGit possa eseguire il commit. Esegui git config --global user.name \"Il tuo nome\" e git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Solo editor",
|
||||
"command.view.editorNoteList": "Editor + Elenco note",
|
||||
"command.view.fullLayout": "Layout completo",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "競合を解決",
|
||||
"command.git.viewChanges": "保留中の変更を表示",
|
||||
"git.repository.select": "リポジトリ",
|
||||
"git.toast.autoGitFailed": "AutoGitに失敗しました:{error}",
|
||||
"git.toast.commitFailed": "コミットに失敗しました:{error}",
|
||||
"git.toast.missingAuthor": "AutoGitがコミットできるようになる前に、Gitの作成者を設定してください。git config --global user.name \"Your Name\" と git config --global user.email you@example.com を実行してください。",
|
||||
"command.view.editorOnly": "エディターのみ",
|
||||
"command.view.editorNoteList": "エディター + メモリスト",
|
||||
"command.view.fullLayout": "フルレイアウト",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "충돌 해결",
|
||||
"command.git.viewChanges": "보류 중인 변경 사항 보기",
|
||||
"git.repository.select": "저장소",
|
||||
"git.toast.autoGitFailed": "AutoGit 실패: {error}",
|
||||
"git.toast.commitFailed": "커밋 실패: {error}",
|
||||
"git.toast.missingAuthor": "AutoGit이 커밋하기 전에 Git 작성자를 설정하세요. git config --global user.name \"Your Name\" 및 git config --global user.email you@example.com을 실행하세요.",
|
||||
"command.view.editorOnly": "편집기만",
|
||||
"command.view.editorNoteList": "편집기 + 노트 목록",
|
||||
"command.view.fullLayout": "전체 레이아웃",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Rozwiąż konflikty",
|
||||
"command.git.viewChanges": "Pokaż oczekujące zmiany",
|
||||
"git.repository.select": "Repozytorium",
|
||||
"git.toast.autoGitFailed": "Błąd AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Zatwierdzenie nie powiodło się: {error}",
|
||||
"git.toast.missingAuthor": "Ustaw autora Git, zanim AutoGit będzie mógł wykonać commit. Uruchom git config --global user.name \"Twoje imię\" i git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Tylko edytor",
|
||||
"command.view.editorNoteList": "Edytor + lista notatek",
|
||||
"command.view.fullLayout": "Pełny układ",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Resolver conflitos",
|
||||
"command.git.viewChanges": "Exibir alterações pendentes",
|
||||
"git.repository.select": "Repositório",
|
||||
"git.toast.autoGitFailed": "Falha no AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Falha no commit: {error}",
|
||||
"git.toast.missingAuthor": "Defina um autor Git antes que o AutoGit possa fazer o commit. Execute git config --global user.name \"Seu Nome\" e git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Somente editor",
|
||||
"command.view.editorNoteList": "Editor + Lista de notas",
|
||||
"command.view.fullLayout": "Layout completo",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Resolver conflitos",
|
||||
"command.git.viewChanges": "Ver alterações pendentes",
|
||||
"git.repository.select": "Repositório",
|
||||
"git.toast.autoGitFailed": "Falha no AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Falha no commit: {error}",
|
||||
"git.toast.missingAuthor": "Defina um autor Git antes de o AutoGit poder fazer o commit. Execute git config --global user.name \"O seu nome\" e git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Apenas editor",
|
||||
"command.view.editorNoteList": "Editor + Lista de notas",
|
||||
"command.view.fullLayout": "Disposição completa",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Разрешить конфликты",
|
||||
"command.git.viewChanges": "Просмотреть ожидающие изменения",
|
||||
"git.repository.select": "Репозиторий",
|
||||
"git.toast.autoGitFailed": "Ошибка AutoGit: {error}",
|
||||
"git.toast.commitFailed": "Ошибка коммита: {error}",
|
||||
"git.toast.missingAuthor": "Укажите автора Git, прежде чем AutoGit сможет выполнить коммит. Выполните git config --global user.name \"Your Name\" и git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Только редактор",
|
||||
"command.view.editorNoteList": "Редактор + список заметок",
|
||||
"command.view.fullLayout": "Полный макет",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "Giải quyết xung đột",
|
||||
"command.git.viewChanges": "Xem các thay đổi đang chờ",
|
||||
"git.repository.select": "Kho lưu trữ",
|
||||
"git.toast.autoGitFailed": "AutoGit không thành công: {error}",
|
||||
"git.toast.commitFailed": "Gửi không thành công: {error}",
|
||||
"git.toast.missingAuthor": "Đặt tác giả Git trước khi AutoGit có thể commit. Chạy git config --global user.name \"Tên của bạn\" và git config --global user.email you@example.com.",
|
||||
"command.view.editorOnly": "Chỉ trình soạn thảo",
|
||||
"command.view.editorNoteList": "Trình soạn thảo + danh sách ghi chú",
|
||||
"command.view.fullLayout": "Bố cục đầy đủ",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "解决冲突",
|
||||
"command.git.viewChanges": "查看待处理更改",
|
||||
"git.repository.select": "仓库",
|
||||
"git.toast.autoGitFailed": "AutoGit 失败:{error}",
|
||||
"git.toast.commitFailed": "提交失败:{error}",
|
||||
"git.toast.missingAuthor": "设置 Git 作者后,AutoGit 才能提交。运行 git config --global user.name \"Your Name\" 和 git config --global user.email you@example.com。",
|
||||
"command.view.editorOnly": "仅编辑器",
|
||||
"command.view.editorNoteList": "编辑器 + 笔记列表",
|
||||
"command.view.fullLayout": "完整布局",
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"command.git.resolveConflicts": "解決衝突",
|
||||
"command.git.viewChanges": "檢視待處理更改",
|
||||
"git.repository.select": "儲存庫",
|
||||
"git.toast.autoGitFailed": "AutoGit 失敗:{error}",
|
||||
"git.toast.commitFailed": "提交失敗:{error}",
|
||||
"git.toast.missingAuthor": "設定 Git 作者後,AutoGit 才能提交。執行 git config --global user.name \"Your Name\" 和 git config --global user.email you@example.com。",
|
||||
"command.view.editorOnly": "僅編輯器",
|
||||
"command.view.editorNoteList": "編輯器 + 筆記列表",
|
||||
"command.view.fullLayout": "完整佈局",
|
||||
|
||||
@@ -7,16 +7,19 @@ type MockHandler = (args?: Record<string, unknown>) => unknown
|
||||
function installAutoGitMocks() {
|
||||
type BrowserWindow = Window & typeof globalThis & {
|
||||
__getDirtyPathCount?: () => number
|
||||
__gitCommitAttempts?: number
|
||||
__gitCommitMessages?: string[]
|
||||
__gitPushCalls?: number
|
||||
__mockHandlers?: Record<string, MockHandler>
|
||||
__setMockAppActive?: (active: boolean) => void
|
||||
__setMockGitCommitFailure?: (message: string | null) => void
|
||||
}
|
||||
|
||||
const browserWindow = window as BrowserWindow
|
||||
const dirtyPaths = new Set<string>()
|
||||
let appActive = true
|
||||
let ahead = 0
|
||||
let gitCommitFailure: string | null = null
|
||||
|
||||
const createModifiedFiles = () => [...dirtyPaths].map((path) => ({
|
||||
path,
|
||||
@@ -40,6 +43,11 @@ function installAutoGitMocks() {
|
||||
handlers.get_modified_files = () => createModifiedFiles()
|
||||
|
||||
handlers.git_commit = (args?: Record<string, unknown>) => {
|
||||
browserWindow.__gitCommitAttempts = (browserWindow.__gitCommitAttempts ?? 0) + 1
|
||||
if (gitCommitFailure) {
|
||||
throw new Error(gitCommitFailure)
|
||||
}
|
||||
|
||||
const message = typeof args?.message === 'string' ? args.message : ''
|
||||
browserWindow.__gitCommitMessages?.push(message)
|
||||
dirtyPaths.clear()
|
||||
@@ -84,7 +92,11 @@ function installAutoGitMocks() {
|
||||
browserWindow.__setMockAppActive = (active: boolean) => {
|
||||
appActive = active
|
||||
}
|
||||
browserWindow.__setMockGitCommitFailure = (message: string | null) => {
|
||||
gitCommitFailure = message
|
||||
}
|
||||
browserWindow.__getDirtyPathCount = () => dirtyPaths.size
|
||||
browserWindow.__gitCommitAttempts = 0
|
||||
browserWindow.__gitCommitMessages = []
|
||||
browserWindow.__gitPushCalls = 0
|
||||
|
||||
@@ -133,6 +145,12 @@ async function expectCommitMessageCount(page: Page, expectedCount: number) {
|
||||
).toBe(expectedCount)
|
||||
}
|
||||
|
||||
async function expectCommitAttemptCount(page: Page, expectedCount: number) {
|
||||
await expect.poll(async () =>
|
||||
page.evaluate(() => (window as Window & { __gitCommitAttempts?: number }).__gitCommitAttempts ?? 0),
|
||||
).toBe(expectedCount)
|
||||
}
|
||||
|
||||
async function expectCommitMessage(page: Page, index: number, expectedMessage: string) {
|
||||
await expect.poll(async () =>
|
||||
page.evaluate(
|
||||
@@ -166,13 +184,21 @@ async function setMockAppActive(page: Page, active: boolean) {
|
||||
}, active)
|
||||
}
|
||||
|
||||
async function triggerQuickCommit(page: Page) {
|
||||
const commitButton = page.getByTestId('status-commit-push')
|
||||
await commitButton.focus()
|
||||
await page.keyboard.press('Enter')
|
||||
async function setMockGitCommitFailure(page: Page, message: string | null) {
|
||||
await page.evaluate((failureMessage) => {
|
||||
;(window as Window & {
|
||||
__setMockGitCommitFailure?: (message: string | null) => void
|
||||
}).__setMockGitCommitFailure?.(failureMessage)
|
||||
}, message)
|
||||
}
|
||||
|
||||
test('@smoke AutoGit checkpoints on idle and inactive, and the bottom bar reuses the same message', async ({ page }) => {
|
||||
async function triggerQuickCommit(page: Page) {
|
||||
const commitButton = page.getByTestId('status-commit-push')
|
||||
await expect(commitButton).toBeEnabled({ timeout: 5_000 })
|
||||
await commitButton.click()
|
||||
}
|
||||
|
||||
test('@smoke AutoGit checkpoints on idle, and the bottom bar reuses the same message', async ({ page }) => {
|
||||
await page.addInitScript(installAutoGitMocks)
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
@@ -183,14 +209,43 @@ test('@smoke AutoGit checkpoints on idle and inactive, and the bottom bar reuses
|
||||
await seedSavedChange(page)
|
||||
await expectCheckpoint(page, 1)
|
||||
|
||||
await seedSavedChange(page)
|
||||
await setMockAppActive(page, false)
|
||||
await expectCheckpoint(page, 2)
|
||||
|
||||
await setMockAppActive(page, true)
|
||||
await seedSavedChange(page)
|
||||
await triggerQuickCommit(page)
|
||||
await expectCheckpoint(page, 3)
|
||||
await expectCheckpoint(page, 2)
|
||||
|
||||
await expect(page.locator('.fixed.bottom-8')).toContainText('Committed and pushed', { timeout: 5_000 })
|
||||
})
|
||||
|
||||
test('AutoGit checkpoints when the app is inactive', async ({ page }) => {
|
||||
await page.addInitScript(installAutoGitMocks)
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
await openAutoGitSettings(page)
|
||||
await openFirstNote(page)
|
||||
await setMockAppActive(page, false)
|
||||
|
||||
await seedSavedChange(page)
|
||||
await expectCheckpoint(page, 1)
|
||||
})
|
||||
|
||||
test('AutoGit does not retry a failed checkpoint until the next saved change', async ({ page }) => {
|
||||
await page.addInitScript(installAutoGitMocks)
|
||||
await page.goto('/')
|
||||
await page.waitForLoadState('networkidle')
|
||||
|
||||
await openAutoGitSettings(page)
|
||||
await openFirstNote(page)
|
||||
await setMockGitCommitFailure(page, 'Author identity unknown')
|
||||
|
||||
await seedSavedChange(page)
|
||||
await expectCommitAttemptCount(page, 1)
|
||||
await expectDirtyPathCount(page, 1)
|
||||
await expect(page.locator('.fixed.bottom-8')).toContainText('Set a Git author before AutoGit can commit', { timeout: 5_000 })
|
||||
|
||||
await page.waitForTimeout(3_500)
|
||||
await expectCommitAttemptCount(page, 1)
|
||||
|
||||
await seedSavedChange(page)
|
||||
await expectCommitAttemptCount(page, 2)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user