Push-all was up to 3N sequential HTTP calls and N separate commits for N files (getFile check -> pushFile PUT -> optional extra getFile), plus two redundant full remote-tree fetches per run (one discarded in main.ts, one inside GitignoreManager). - Add optional GitServiceInterface.pushBatch. GitHub/Gitea implement it via the git blob->tree->commit->ref Data API, generalizing pushSymlink's existing pattern into shared resolveGitHubStyleBaseTree/commitGitHubStyleTree helpers in git-service-base.ts. GitLab implements it via its native multi-file Commits API (actions array), with a follow-up listFilesDetailed call to recover each file's new blob sha since that endpoint doesn't return them. Symlinks stay on the existing per-file pushSymlink path. - sync-manager.ts's push-all flow now classifies each file by comparing a locally-computed git blob sha (utils/git-blob-sha.ts, already used by the feat-006 status refresh) against a pre-fetched remote tree's per-entry sha, eliminating the per-file getFile call for the common case. Queued files are committed in one grouped pushBatch call, chunked at MAX_BATCH_PUSH_SIZE=200; a failed chunk marks every file in it as failed rather than dropping results silently. Providers without pushBatch fall back to the original sequential path. - main.ts fetches the remote tree once per push/pull-all run and threads it into both GitignoreManager.loadGitignores(tree) and SyncManager.pushAllFiles(files, onProgress, tree), replacing the previously-discarded listFiles() call and gitignore-manager's separate fetch with one shared call. Rename detection and the pull-all path are unchanged (out of scope). Evidence: npx eslint . -> 0 errors; npm run build -> clean; npx vitest run -> 330/330 passed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
13 KiB
Session Progress Log
Completed work is archived in archive/, one file per calendar month — this file only tracks what's still open.
Current State
Last Updated: 2026-07-14 10:10 Session ID: current Active Feature: feat-011 (perf: batch-commit push-all + SHA-based diffing) — done, lint/build/test all green. Previous active item (issue #78 delete-remote-only-file fix) still awaiting user re-test after rebuild.
Status
What's Done
-
feat-001..009 (project setup, settings UX bundle, folder picker, symlink pull fix, tree-SHA refresh, HTML-response error clarity, what's-new modal, i18n/multi-language support) — see archive/2026-07.md
-
All consolidated onto branch
claude/fix-directory-symlink-pull-260713→ PR #51 (open, all CI green as of last check), per user's explicit request to keep the PR count down rather than one PR per issue. -
"Root path" folder picker now suggests folders from the remote repo tree instead of the local vault. Added
src/ui/RemoteFolderSuggest.ts(derives folder paths fromgitService.listFiles(branch, false)); wired into the Root path field insrc/settings.ts. Vault folder field untouched (still uses localFolderSuggest). -
Issue #78 (filed in
firstsun-dev/blogbut actually a git-files-sync bug — user confirmed to fix it here regardless of repo mismatch): "delete remote only file fail". Three distinct root causes found and fixed across this session (iteratively, as the user re-tested against real console errors each time):- URL encoding + silent empty-sha delete:
github-service.ts/gitea-service.tsgetApiUrl()built the Contents API URL withoutencodeURIComponenton path segments (unlikegitlab-service.ts, which already encodes), so paths with spaces/non-ASCII (e.g. Chinese filenames) 404'd on the pre-delete sha lookup; that 404 silently produced an emptysha, and the resulting DELETE withsha: ''was rejected — silently, since the UI swallowed all delete errors. Fixed: encode path segments in both services;deleteFile()now throws a clear error if the pre-delete lookup's sha is empty;SyncStatusView.tsdelete flow now captures{path, message}per failure and surfaces the real message in the Notice (syncStatus.notice.deleteResult.partialWithMessagei18n key, added to bothen.ts/zh-tw.ts) instead of a bare "N failed". - EISDIR + folder/remote-record collision:
SyncStatusView.tsreadFileContent()/identifyExtraFiles()treated any local path thatadapter.exists()returned true for as a readable file — but a real local directory (or a symlink to one) that happens to share a path with a stale remote record (e.g..claude/skills/polish-blog) is not readable content, andadapter.read()on it throwsEISDIR. Fixed: newisLocalFile()helper (adapter.stat().type === 'file') gatesidentifyExtraFilesand the hidden-filerecursiveScan;readFileContent's string-path branch (extracted toreadStringPathContent()) also falls back toreadLocalSymlinkTarget()on read failure as a last resort. - Wrong path passed to deleteFile (the actual reported bug):
performRemoteDeletioncalledgitService.deleteFile(s.path, ...)withs.path— a vault-relative path (carries thevaultFolderprefix, peridentifyExtraFiles'sgetVaultPath()mapping) — butdeleteFile'sgetFullPath()expects a path relative torootPathonly (vaultFolder is a purely local concept, stripped before every othergitServicecall site, e.g.sync-manager.ts'sgetNormalizedPath(file.path)pattern used everywhere else). With a non-emptyvaultFoldersetting, this built the wrong repo path, causingdeleteFile's pre-deletegetFile()lookup to 404 — surfacing as "file was not found on branch main" for a file the UI still listed as remote-only (since refresh's status computation does normalize correctly). Fixed:performRemoteDeletionnow computesthis.plugin.getNormalizedPath(s.path)before callingdeleteFile, matching every other call site.
- Added 4 tests for root cause #1 (
tests/services/github-service.test.ts,tests/services/gitea-service.test.ts) covering the empty-sha throw and non-ASCII/space path encoding. - Added
tests/ui/SyncStatusView.test.ts(5 cases) for root causes #2 and #3, sinceSyncStatusView.tshad no test file at all before this — required adding minimalItemView/WorkspaceLeafmocks totests/setup.ts(constructor setsapp/leaf, fakescontainerEl.children[0..1]; no rendering behavior needed since tests call private methods directly via a cast rather than going throughonOpen()/renderView()). Covers: vaultFolder-prefix stripping beforedeleteFile()(with and without a configured vaultFolder), real error messages surfacing inerrors, andidentifyExtraFilescorrectly classifying a local-folder/remote-record collision as remote-only vs. a genuine local file as checkable. - Evidence:
npx eslint .→ 0 errors;npm run build→ clean;npx vitest run→ 317/317 passed. - All three root causes committed (
896d77b, merge563a28e,fa42fea) and pushed toclaude/fix-directory-symlink-pull-260713. Test coverage for #2/#3 added after that — pending commit as of this note. - Not yet done: comment on/reference issue #78 (in the
blogrepo) noting the fix landed ingit-files-syncinstead — flag to user before doing so since it crosses repos.
- URL encoding + silent empty-sha delete:
-
feat-011: perf(push): batch-commit push-all + SHA-based diffing. User asked "現在外掛的 push功能很慢,可能是什麼原因" then approved doing all three identified fixes (a/b/c) via a full plan-mode design pass. Implementation:
- Batched commit instead of one-commit-per-file: new optional
GitServiceInterface.pushBatch?(items, branch, message).GitHubService/GiteaServiceimplement it via the git blob→tree→commit→ref Data API (generalizingpushSymlink's existing pattern, factored into sharedresolveGitHubStyleBaseTree/commitGitHubStyleTreehelpers ingit-service-base.ts);GitLabServiceimplements it via the native multi-file Commits API (POST .../repository/commitswith anactionsarray), with one follow-uplistFilesDetailedcall afterward to recover each file's new blob sha (that API doesn't return them). Symlinks stay on the existing per-filepushSymlinkpath, not folded into batches. - SHA-based diffing instead of per-file
getFile:sync-manager.ts's push-all flow (processPushBatch/classifyPushCandidate/classifyAgainstTreeEntry) now compares a locally-computed git blob sha (utils/git-blob-sha.ts'sgitBlobSha, already used by feat-006's status refresh) against a pre-fetched remote tree's per-entry sha — no network round trip needed to detect unchanged/new/modified/conflicting files. Rename detection and symlink handling are untouched (still per-file, immediate). - Dedup the remote-tree fetch:
main.ts:runAllFilesnow fetches the tree once (listFilesDetailed(branch, false)) and threads it into bothGitignoreManager.loadGitignores(tree)(new optional param, replacing its owngetRepoGitignorescall when a tree is supplied) andSyncManager.pushAllFiles(files, onProgress, tree)— replacing the old discardedlistFiles()call plus gitignore-manager's separate fetch with a single shared one.
- Batches are chunked at
MAX_BATCH_PUSH_SIZE = 200files per commit call (git-service-base.ts); a failed chunk marks every file in that chunk as failed (not silently dropped), earlier successful chunks stay committed. - Providers without
pushBatch(future Bitbucket) fall back to the original sequential per-file push path, unchanged. - Added
pushBatchtests togithub-service.test.ts/gitea-service.test.ts/gitlab-service.test.ts(happy path, empty-batch short-circuit, base64 encoding, GitLab's create-vs-update action + sha-recovery-miss case), aMAX_BATCH_PUSH_SIZEsanity test, newsync-manager-batch.test.tscases (grouped pushBatch call, mixed binary+text batch, whole-chunk-failure, sha-match skips bothgetFileandpushBatch), and agitignore-manager.test.tscase for the pre-fetched-tree path. Existing conflict/rename/symlink batch tests rewritten to mocklistFilesDetailedinstead ofgetFilefor the equality/conflict check (rename detection's owngetFilecalls are untouched). - Evidence:
npx eslint .→ 0 errors;npm run build→ clean;npx vitest run→ 330/330 passed (313 pre-existing + wording/mock updates + 17 new cases). - Pull path (
pullAllFiles) intentionally untouched — out of scope, still needs full remote content regardless of sha comparison. - Not yet committed as of this note — pending commit onto
claude/fix-directory-symlink-pull-260713.
- Batched commit instead of one-commit-per-file: new optional
What's In Progress
- Nothing else actively in progress.
What's Next
- Get user confirmation that delete now works (or a new detailed error message) after they rebuild/reload with the latest push (issue #78).
- Manually verify feat-011 in Obsidian if possible: push-all on a mixed vault should produce one new commit containing only changed/new files.
- Issue #37 (Bitbucket provider support, feat-010) — large, was deferred until #38 (i18n) landed. Now unblocked. Its
GitServiceInterfaceimplementation should simply omitpushBatchand use the existing per-file fallback. - Re-sync against
gh issue list --repo firstsun-dev/git-files-sync --state open. Remaining genuinely-unstarted issues as of this session: #47 (regex ignore lists), #45 (SonarQube findings), #37 (Bitbucket), #28 (non-engineering: community visibility). - PR #51 is large (8+ issues' worth of changes now). If the user wants to review/merge it before more work piles on, flag this rather than continuing to add commits indefinitely.
Blockers / Risks
- None currently.
Decisions Made
- All work goes into one PR (#51): user explicitly said "不要那麼多pr merge" (don't want so many PRs). Any further issue work should commit directly onto
claude/fix-directory-symlink-pull-260713, not a new branch. - feat-009 scope (i18n): settings.ts + all Notice() messages, done in one pass rather than split into infra-first/settings-only phases.
- Flat key-value i18n dict, not nested namespaces: matches this plugin's small scale; simpler
t(key, vars)lookup, no external i18n library dependency. - Locale detection via
window.moment.locale(): per issue #38's suggested approach; unresolvable/unsupported locales fall back to English. A barezhlocale code maps tozh-tw, the only Chinese variant shipped. - Diff-format markers, changelog release-note text, and proper nouns (GitHub/GitLab/Gitea) were left untranslated — out of scope for UI-chrome i18n.
src/changelog.tsis hand-curated, separate from the auto-generatedCHANGELOG.md: semantic-release already maintainsCHANGELOG.mdfrom Conventional Commit messages, but it's commit-log-level detail; the what's-new modal needs a small, hand-written, user-facing "highlights" list instead.- Fixed two duplication regressions mid-session (SonarCloud gate is 3% on new code, learned the hard way on PR #49 earlier): deduped
TextComponent/TextAreaComponenttest mocks, and deduped the GitHub/GiteagetBlob()bodies into a sharedfetchGitHubStyleBlob()base helper.
Files Modified This Session
- Issue #78 fix:
src/services/github-service.ts,src/services/gitea-service.ts,src/ui/SyncStatusView.ts,src/i18n/locales/en.ts,src/i18n/locales/zh-tw.ts,tests/services/github-service.test.ts,tests/services/gitea-service.test.ts. - Root path picker fix:
src/ui/RemoteFolderSuggest.ts(new),src/settings.ts. - feat-009 (i18n, prior parallel session):
src/i18n/index.ts,src/i18n/locales/en.ts,src/i18n/locales/zh-tw.ts,tests/i18n/index.test.ts(all new);src/settings.ts,src/main.ts,src/ui/SyncStatusView.ts,src/ui/SyncConflictModal.ts,src/ui/WhatsNewModal.ts,src/ui/ConfirmModal.ts,src/ui/components/ActionBar.ts,FileListItem.ts,DiffPanel.ts(modified). - See archive/2026-07.md entries feat-003/005/006/007/008 for earlier features' file lists.
Evidence of Completion
- Tests pass:
npx vitest run→ 306/306 passed (post-merge, this session) - Type check clean:
npm run build(tsc + Obsidian 1.11.0 compat typecheck + esbuild) → clean - Lint clean:
npx eslint .→ 0 errors - Manual verification in Obsidian: in progress — user is re-testing the delete fix live; not yet confirmed resolved.
Notes for Next Session
- Working branch for all further commits:
claude/fix-directory-symlink-pull-260713(PR #51). Do not open a new branch/PR for the next issue unless the user says otherwise. feature_list.json's backlog is a snapshot from this session — re-checkgh issue listbefore trusting it.- If the user reports delete still failing after this push, get the exact new error message (should no longer be a bare "N failed") to find the next root cause.