- Root path setting's folder picker now suggests folders from the remote
repo tree (RemoteFolderSuggest) instead of the local vault, since Root
path is a repo-side setting unrelated to local vault structure.
- GitHub/Gitea deleteFile: URL-encode path segments (matching GitLab's
existing behavior) so paths with spaces/non-ASCII don't 404 on the
pre-delete sha lookup; throw a clear error instead of silently sending
a DELETE with an empty sha when that lookup 404s.
- SyncStatusView delete flow now surfaces the real error message instead
of swallowing it into a bare "N failed" notice.
- SyncStatusView.readFileContent: guard against EISDIR when a hidden
local-only symlinked folder (not yet known to the remote) is read as a
file, falling back to the raw symlink target.
Closesfirstsun-dev/blog#78 (misfiled; this is the actual fix location).
Refresh previously fetched each file's remote content via getFile() to
compare against local content — an O(n) network round-trip per file
even after the existing concurrency pool. Implements the two-phase
hybrid design from the issue discussion:
Phase 1 — bulk classify, no content:
- Extend GitTreeEntry with sha, populated from each service's tree
listing (GitHub/Gitea: item.sha, GitLab: item.id — GitLab's tree API
calls the blob SHA "id", not "sha").
- Add gitBlobSha(): sha1("blob " + byteLength + "\0" + contentBytes)
via crypto.subtle, matching `git hash-object` exactly (verified
against real git-produced SHAs in tests, including a multi-byte
UTF-8 case to check byte length vs char length).
- SyncStatusView.refreshFileStatus() now compares a locally-computed
blob SHA against the tree entry's SHA — no getFile call. Falls back
to the previous content-based comparison per-entry when a tree entry
has no SHA or isn't found in the tree at all (new local file).
- Symlink-aware hashing per the issue's follow-up note: a symlink's
blob content is its target path string, not the content it points
at. "real" mode hashes the raw local link target (readLocalSymlinkTarget)
instead of following it; "follow" mode always hashes the followed
content; "skip" entries are already excluded from the tree map.
Phase 2 — fetch content only when needed:
- Add getBlob(sha, path) to GitServiceInterface: GitHub/Gitea use
`git/blobs/{sha}` (base64 JSON, shared via a new
fetchGitHubStyleBlob() base helper); GitLab uses
`repository/blobs/{sha}/raw` (raw bytes, not JSON-wrapped).
- The diff panel no longer requires content prefetched during refresh.
FileListItem's Diff button always renders for a modified file and
fetches remote content on demand via a new onExpandDiff callback,
showing a loading placeholder while the request is in flight and
caching the result on the FileStatus so re-expanding doesn't refetch.
- A modified symlink shows "Symlink target changed" instead of running
a text diff.
Known limitation (pre-existing, not introduced here): comparing raw
file bytes has no CRLF/line-ending normalization, so a repo checked
out elsewhere with core.autocrlf could show a false "modified" status.
The previous content-based comparison (contentsEqual) had the same
exact-byte-equality behavior, so this isn't a regression.
Closes#36
- listFilesDetailed rethrows 404 branch-resolution failures with a
message naming the branch, instead of a bare "Git Service Error (404)"
- testConnection now also checks the configured branch exists and
reports repoOk/branchOk separately so the settings UI can warn when
the repo is reachable but the branch is missing
- fixes settings.ts silently reporting "connection successful" even
when testConnection's result was never checked
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds end-to-end symlink syncing driven by the "Symbolic links" setting
(real / follow / skip; default real), building on the earlier detection.
- Pull: on desktop with "real", a remote symlink is recreated as a real OS
link via Node fs (utils/symlink.ts, guarded by Platform/FileSystemAdapter);
otherwise the target path is written as content.
- Push: GitHubService.pushSymlink commits a real symlink blob (mode 120000)
through the Git Data API (blob -> tree -> commit -> ref). getFile now
reports isSymlink/symlinkTarget.
- Config 防呆: only GitHub offers "real"; on GitLab/Gitea (no API to create
symlinks) "real" resolves to "skip" via getEffectiveSymlinkHandling.
- Safety: a "follow" push never overwrites a detected remote symlink with a
regular file; it is skipped with a notice.
- Docs: docs/symlink-handling.md plus a README settings note.
Lint is satisfied without disables (Electron global require, minimal Node
type shims). Adds tests for getFile detection, the pushSymlink Git Data
sequence, and the remote-symlink push guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Symlinks are Git blobs with mode 120000 whose content is the target path.
They were treated as ordinary files, which caused 404s on fetch and could
corrupt the file on pull. Add detection and let the user choose behavior.
- Settings: new "Symbolic links" option (symlinkHandling) with real
(default) / follow / skip.
- Services: listFilesDetailed() reports each entry's symlink flag from the
tree mode (120000) for GitHub and GitLab; listFiles() now delegates to it.
- Refresh/discovery: with "skip", remote symlinks are excluded from sync;
with "follow"/"real" they are included and synced as the target content.
Note: true OS-level symlink recreation on desktop and pushing files as
symlinks (Git Data API) are not yet implemented — on all platforms "real"
currently syncs the target content like "follow"; mobile has no symlink
API regardless. Tracked as a follow-up.
Add tests for symlink detection on both services and update settings fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Pushing a new file via the single-file push button failed with GitHub
HTTP 422. A 404 lookup returns sha === '' for new files, and the manual
push path (sync-manager pushFile/conflict resolution) forwarded that
empty string into the Contents API request body as "sha":"", which
GitHub rejects. Batch push avoided this via `remote.sha || undefined`.
Fix at the service layer so every caller is safe: only include sha in the
GitHub request body when it is non-empty. Apply the same guard to the
GitLab service's last_commit_id for symmetry.
Add regression tests for blank-sha pushes on both services.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Refresh failed with the cryptic "Unexpected token '<', "<!DOCTYPE ..."
is not valid JSON" whenever the Git server returned an HTML page (login,
SSO redirect, or proxy/error page) on a 2xx/3xx response. safeRequest
only threw on status >= 400, so such bodies slipped through and crashed
at response.json.
Add BaseGitService.parseJson() which detects non-JSON/HTML bodies and
throws an actionable message, and use it for all response.json reads in
the GitHub and GitLab services. Also harden parseErrorResponse so HTML
error pages produce a clear message instead of dumping the raw document.
Add tests covering HTML 2xx responses, HTML detection by leading '<',
malformed JSON, and HTML error pages.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
This commit consolidates several improvements to satisfy SonarCloud Quality Gate and enhance the overall plugin stability:
- Code Quality & Refactoring:
- Reduced duplication to 0% by refactoring SyncManager and SyncStatusView.
- Implemented centralized 'processBatch' logic for multi-file sync operations.
- Fixed security hotspots by replacing legacy atob/btoa with Buffer.
- Added memory safety limits to the diff algorithm to prevent DoS.
- Test Coverage & Reliability:
- Improved SyncManager coverage to 81.8% and GitignoreManager to 92.8%.
- Added comprehensive unit tests for batch operations and complex .gitignore patterns.
- Resolved all ESLint 'unsafe-member-access' and 'unbound-method' warnings in tests.
- Infrastructure & Metadata:
- Consolidated GitHub Actions into a single optimized 'ci.yml' using latest versions (v6-v8).
- Synchronized manifest.json and versions.json to v1.1.0.
- Fixed workflow permission issues identified by CodeQL.