fix: handle autogit author failures

This commit is contained in:
lucaronin
2026-05-13 14:12:23 +02:00
parent ae7a468310
commit b0c8eff6c7
22 changed files with 286 additions and 86 deletions

View File

@@ -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

View File

@@ -968,6 +968,7 @@ function App() {
setToastMessage,
onPushRejected: autoSync.handlePushRejected,
automaticVaultPaths: activeGitRepositoryPaths,
locale: appLocale,
manualVaultPath: gitSurfaces.commitRepositoryPath,
vaultPath: resolvedPath,
})

View File

@@ -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))
})
})

View File

@@ -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) => {

View File

@@ -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([])

View File

@@ -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,

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "フルレイアウト",

View File

@@ -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": "전체 레이아웃",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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": "Полный макет",

View File

@@ -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 đủ",

View File

@@ -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": "完整布局",

View File

@@ -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": "完整佈局",

View File

@@ -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)
})