Skip to content

Gitea support, symlink handling, sync reliability fixes, and dependency security updates - #43

Merged
ClaudiaFang merged 31 commits into
mainfrom
claude/git-files-sync-issue-31-54izdm
Jul 5, 2026
Merged

ClaudiaFang merged 31 commits into
mainfrom
claude/git-files-sync-issue-31-54izdm

Conversation

@ClaudiaFang

@ClaudiaFang ClaudiaFang commented Jul 5, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Add Gitea as a third-party Git provider alongside GitHub/GitLab
  • Add symbolic link detection with a configurable handling setting
  • Fix Failed to refresh: Unexpected token '<'... (Failed to refresh: Unexpected token '<',< !DOCTYPE ·“, is not valid JSON #31) by handling non-JSON (HTML) responses from Git APIs instead of surfacing raw parse errors
  • Stop false-positive rename detection and the 422 on rename push
  • Stop batch push/pull from silently overwriting conflicts
  • Mask personal access token fields in settings UI
  • Migrate off deprecated Obsidian APIs
  • Match ribbon/command labels and status icons to configured Git service
  • Parallelize refresh status checks and throttle re-renders for UI responsiveness
  • Resolve all open Dependabot alerts (dev-only transitive deps: semantic-release, vitest, jsdom, sigstore, tar, ip-address, js-yaml, undici) — npm audit now reports 0 vulnerabilities; none of the flagged packages ship in the built plugin
  • Various CI updates (Node 24 compatible actions, artifact naming, drop SonarQube)

Test plan

  • npm run build (tsc + esbuild) passes
  • npm run test — 243 tests passing
  • npm run lint passes
  • npm ci succeeds from a clean install (verifies lockfile consistency)
  • Manual smoke test in Obsidian: Gitea provider connection, symlink sync, rename push, conflict handling

🤖 Generated with Claude Code

claude and others added 30 commits June 16, 2026 04:04
- Add GiteaService implementing BaseGitService/GitServiceInterface
  - Uses Gitea API v1 endpoints (/api/v1/repos/{owner}/{repo}/...)
  - POST for file creation, PUT for updates (differs from GitHub)
  - Authorization: token {token} header
- Add giteaToken, giteaBaseUrl, giteaOwner, giteaRepo settings fields
- Add Gitea option to service type dropdown in settings UI
- Add displayGiteaSettings() panel in settings tab
- Update initializeGitService() to instantiate GiteaService
- Update getServiceName() to handle 'gitea' type
- Add comprehensive test suite (gitea-service.test.ts, 15 test cases)
- Update existing test fixtures to include new Gitea settings fields

Closes #26

https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
Gitea's git/trees endpoint requires a tree or commit SHA, not a branch
name, on instances older than ~1.17. Resolve branch to commit SHA via
/branches/{branch} first, then fetch /git/trees/{commitSha}?recursive=1.

Also updates README with provider compatibility table and SVG icons for
GitHub, GitLab, and Gitea.

https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
- actions/setup-node: node 20 → 22 (Node 20 EOL Apr 2026)
- github/codeql-action: v3 → v4 (v3 deprecated Dec 2026)

https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
Replace / with - in branch name and use short SHA (7 chars)
e.g. plugin-claude-trusting-volta-qlg8bk-c2c5dd5

https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
- actions/checkout: v4 → v6
- actions/setup-node: v4 → v6
- actions/upload-artifact: v4 → v5
- actions/checkout (codeql.yml): v4 → v6

https://claude.ai/code/session_019Jbz6HpQvWU1wpm5M6MZpd
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
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
The view mixed hand-picked Unicode glyphs (✓ ⚠ ↑ ↓ ⟳ ↻ ✕ ≡ ⎇ 📁),
duplicated across files, which rendered at inconsistent sizes/weights
across platforms and could drift (e.g. the Refresh button used ↻ while
the "checking" status used ⟳).

Centralize every icon in src/ui/components/icons.ts as Lucide icon ids and
render them with Obsidian's setIcon, so status, action-bar, tab, and
info-strip icons all share one consistent icon set. Tabs now derive their
icon from statusMeta so they can no longer diverge from the file list.
Add CSS to size the SVGs uniformly.

Add setIcon to the obsidian test mock and update statusMeta expectations.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Reading a symlinked file via Obsidian's cached vault.read(TFile) can fail
(notably on mobile), which broke both refresh and push: the status check
swallowed the error and mislabeled the file as 'unsynced', so the user
saw a Push button that then failed re-reading the symlink.

Fall back to vault.adapter.read/readBinary(path) when vault.read throws,
in both the status view and the sync manager. Also stop silently
swallowing the status-check error so the real cause is logged.

Add a regression test covering the adapter fallback on push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
The shared workflow obsidian-plugin-ci.yml@v1 no longer defines the
skip-sonar input or the SONAR_TOKEN secret, which made the caller
workflow invalid ("Invalid input/secret ... is not defined in the
referenced workflow"). Remove both so the workflow validates again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
The ribbon button used 'list-checks' while the Sync Status view itself
uses 'git-compare' (getIcon). Use 'git-compare' for the ribbon too so the
"Open sync status" icon matches the sync status view icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
Loading .gitignore files probes paths that often don't exist remotely.
getFile already treats 404 as "empty file" (handleFileNotFound) and the
gitignore lookup ignores failures, but safeRequest logged every 404 as an
error — twice, because its own catch re-caught the error it had just
thrown. This produced scary "Git Service Request Failed (404): Not Found"
console output during a normal pull/refresh.

Restructure safeRequest so the catch only wraps the network call (no
double-logging of HTTP-status errors), and log an expected 404 at debug
level instead of error. Non-404 statuses are still logged as errors and
all statuses still throw so callers can handle them.

Add a debug level to the logger and regression tests for 404 vs 500.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
…ting

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
…4izdm' into claude/trusting-volta-qlg8bk

# Conflicts:
#	.github/workflows/ci.yml
#	package-lock.json
…dling

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
Refresh checked files one at a time (a sequential network request per
file) and re-rendered the whole view after every file, so a large vault
was slow. Run the per-file checks with a bounded concurrency pool (8) and
re-render on a ~150ms throttle plus a final render. Behavior and results
are unchanged; wall time drops roughly in line with the concurrency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DwioG4CNKUBuKiZdowLFWe
- 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>
Single-file push/pull already detected when both local and remote
changed since the last sync and prompted via SyncConflictModal, but
"Push all"/"Pull all"/multi-select batch actions skipped that check
and force-overwrote. Batch operations now skip conflicting files and
report a conflicts count so nothing is silently clobbered.

Also correct delete-confirmation copy: it claimed local deletes are
recoverable ("moved to trash") unconditionally, but the actual
destination depends on the vault's "Deleted files" setting (which can
be permanent). Wording now defers to that setting instead of
asserting recoverability.
The push ribbon icon's tooltip and the "Push/Pull current file"
command names embedded the configured service name (e.g. "Push to
GitHub") but were only set once at plugin load. Switching service in
Settings afterward left them stale until Obsidian was reloaded.

- Ribbon tooltip is now re-applied via setTooltip() on every
  saveSettings(), so it always reflects the current service.
- Command names have no rename API in Obsidian, so they're now
  generic ("Push current file" / "Pull current file"), matching the
  wording already used by "Push all files" / "Pull all files".
- drop unnecessary type assertion in SyncStatusView tab rendering
- use window.setTimeout instead of global setTimeout
- use window instead of globalThis when resolving Electron's require

docs: list supported Git providers at the top of README
…sign skills

Adds skill files pulled from firstsun-dev/skills and their lockfile
entries, used during this session's UX review and fixes.
Token fields for GitLab, GitHub, and Gitea were plain text inputs, so
tokens were visible in plaintext during screen shares, recordings, or
on shared machines. They're now password-type inputs with a toggle
(eye icon) to reveal them when the user needs to verify what they
pasted.
- Bump obsidian dependency to ^1.13.1
- settings.ts: add getSettingDefinitions() alongside display()
  fallback for Obsidian < 1.13.0 (minAppVersion)
- SyncConflictModal.ts: setWarning() -> setDestructive()
detectRename only checked whether a tracked old path's file was missing
from the vault, with no content verification. Any orphaned syncMetadata
entry (e.g. left behind by a local delete, which never cleared it) would
cause the next unrelated push to be misclassified as a rename from that
stale path, using create-only semantics that 422'd if the target path
already existed remotely.

- detectRename now confirms identity by checking that the remote content
  at the candidate old path still matches what's being pushed.
- handleRename looks up the existing remote file at the new path and
  sends its sha, so pushing onto an existing remote path updates instead
  of failing with "file already exists".
- Local deletes (single and batch) now clear syncMetadata for the
  deleted path so it can't become an orphaned rename source later.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Obsidian APIs used (getSettingDefinitions, PluginSettingTab.update,
setDestructive) all require 1.13.0+, but manifest.json still declared
1.12.7. Bump minAppVersion to match and cut a new release version.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bump semantic-release, vitest, jsdom, and related tooling to latest
patch versions, and add npm overrides for transitive packages bundled
deep inside the npm CLI (sigstore, tar, ip-address) and the eslint
toolchain (js-yaml, undici via @actions/http-client) that npm install
alone could not reach.

brace-expansion needed no override: npm's default resolver already
picks the highest version satisfying each consumer's own semver range
once tar/sigstore/etc. are unpinned, so a nested override there only
forced an incompatible version onto minimatch@3.1.5 and broke lockfile
consistency (`npm ci` failed with EUSAGE).

All flagged packages were dev-only; none ship in the built plugin.
npm audit now reports 0 vulnerabilities, and `npm ci` + build/test/lint
pass from a clean install.
@sonarqubecloud

sonarqubecloud Bot commented Jul 5, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3.1% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@ClaudiaFang
ClaudiaFang merged commit a20d308 into main Jul 5, 2026
18 of 20 checks passed
@ClaudiaFang
ClaudiaFang deleted the claude/git-files-sync-issue-31-54izdm branch July 5, 2026 07:19
@ClaudiaFang

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 1.2.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

ClaudiaFang added a commit that referenced this pull request Aug 30, 2026
- undici >= 6.28.0: fixes cookie attribute injection, response
  desync via retry interceptor, and CRLF injection (alerts #43-45)
- undici ^7.29.0 global override for @semantic-release/github
- ip-address ^10.3.1: fixes IPv4 leading-zero octal confusion (high,
  alert #42) plus NAT64/CIDR SSRF bypasses (alerts #34-35)

npm audit now reports 0 vulnerabilities
ClaudiaFang pushed a commit that referenced this pull request Sep 1, 2026
## [1.6.0](1.5.9...1.6.0) (2026-09-01)

### Features

* **conflict-modal:** wire SyncDiffService.getConflictStat as the batch conflict diff-stat loader ([246a59f](246a59f))
* **conflict-modal:** wire View Diff data through SyncDiffService ([ec9d356](ec9d356))
* **source-control:** add Phase 2 action service ([70f6c9e](70f6c9e))
* **source-control:** add Phase 3 source control UI ([7cec661](7cec661))
* **source-control:** add push-selection and operation-state foundation ([2d72022](2d72022)), closes [#128](#128)
* **source-control:** add remote-only download action and queue upload/download routing ([f9eb81b](f9eb81b))
* **source-control:** add ViewModel foundation layer ([76db082](76db082))
* **source-control:** full-width diff tab and mobile view-title dedup ([f449125](f449125))
* **source-control:** presentation adapter, diff stat, responsive mobile ([dd8ddd5](dd8ddd5))
* **source-control:** selected-section rows, drop show-synced toggle, colored diff-stat ([853793c](853793c))
* **source-control:** split sync queue/repository regions and extract DiffStatProvider + SelectionController ([9fd1789](9fd1789)), closes [#136](#136) [#135](#135) [#136](#136)
* **source-control:** wire Source Control view as the entry and remove legacy UI ([4e647fb](4e647fb))
* **sync-status:** add refresh and operation feedback ([759b717](759b717))
* **sync-status:** add selection workflow and sync action UI ([625fad2](625fad2))
* **sync:** auto-refresh status on local vault changes and distinguish local deletes ([4009f1d](4009f1d)), closes [#66](#66)
* **whats-new:** add onboarding layout for the Source Control workflow ([88e08dd](88e08dd))

### Bug Fixes

* **ci:** continue after intentionally skipped E2E jobs ([18de6e0](18de6e0))
* **ci:** fold E2E suite registration check into run-e2e.sh ([c2bfeb0](c2bfeb0))
* **ci:** isolate manual E2E concurrency ([b5884fc](b5884fc))
* **ci:** run E2E suites through shared runner ([e9f0d28](e9f0d28))
* **ci:** serialize branch validation workflows ([acd2046](acd2046))
* **conflict-modal:** apply modal sizing CSS and add filename-first rows with progressive diff stats ([ac2bd2a](ac2bd2a))
* **deps:** bump undici and ip-address overrides to patched versions ([44c17ba](44c17ba)), closes [#43-45](#43) [#42](#42) [#34-35](#34)
* **e2e:** bound the requestUrl shim to a 30s timeout ([8d09aad](8d09aad))
* **e2e:** constrain generated runtime imports ([5dcd87e](5dcd87e))
* **e2e:** invalidate SourceControlScenario's remote cache on commitResolvedBatch ([257b2a4](257b2a4))
* **e2e:** replace unsafe dynamic imports and align Obsidian lint ([3e9f709](3e9f709))
* **e2e:** resolve SonarCloud quality gate findings on new code ([38a5d9e](38a5d9e))
* **e2e:** scope two-client convergence checks to the run's own namespace ([2c7a473](2c7a473))
* **e2e:** share the manager's SyncStatusService in the two-client fixture ([120dfe5](120dfe5))
* **i18n:** remove duplicate releaseHistory keys from concurrent fixes ([de55653](de55653))
* **settings:** keep release history accessible after dismiss ([c37e37c](c37e37c))
* **settings:** keep release history accessible after dismiss ([d6cdc36](d6cdc36))
* **source-control:** apply keep-remote-only batch plans and harden resolution tests ([2591e05](2591e05))
* **source-control:** correct mobile queue and diff presentation ([b3720f7](b3720f7))
* **source-control:** correct one-sided diff stat direction ([fbe0787](fbe0787))
* **source-control:** correct status grouping and filter semantics ([0bcc800](0bcc800))
* **source-control:** harden scroll, diff-stat and create lifecycles ([05f6628](05f6628))
* **source-control:** key selection and operation state by ChangeId ([4b09425](4b09425))
* **source-control:** make keep-remote resolution authoritative ([c73c9cc](c73c9cc))
* **source-control:** make sync actions actually sequential and clean up on remote delete ([2dfe78b](2dfe78b)), closes [#129](#129)
* **source-control:** make whole view scroll, add clear-selection, click-to-collapse folders ([a9d3e98](a9d3e98))
* **source-control:** pin Checked Changes, independent scroll for Changes tree ([d7606ff](d7606ff))
* **source-control:** preserve Changes tree scroll position on rerender ([10e344f](10e344f))
* **source-control:** preserve mobile list position after diff ([709905a](709905a))
* **source-control:** prevent duplicate sync status views ([2bbd042](2bbd042))
* **source-control:** refresh the open diff tab when its backing status changes ([10f9ead](10f9ead))
* **source-control:** repair diff stat cache lifecycle ([2d6cf91](2d6cf91))
* **source-control:** route local-deleted to delete-remote, not pull-restore ([ebd8cb6](ebd8cb6)), closes [#129](#129)
* **source-control:** track diff stat requests by generation token ([78a78e8](78a78e8))
* **source-control:** unify sync completion notification ([17b361f](17b361f))
* **sync-status:** preserve refreshed state across live modifications ([49f3033](49f3033))
* **sync:** classify remote-only changes as remote-modified, not local-modified ([264cb47](264cb47))
* **sync:** report added/updated counts in push/pull toasts, i18n them ([b5eb1ae](b5eb1ae))
* **sync:** stop remote-op failures from masking each other's outcome ([02bd3e3](02bd3e3))
* **test:** capture post-push head before asserting no-op repeat push ([039588f](039588f))
* **test:** show per-test progress in real-provider E2E CI logs ([54e3fb7](54e3fb7))

### Performance Improvements

* **ci:** gate and tier real-provider E2E ([b1d2208](b1d2208))
* **test:** memoize remote reads in source-control-flows scenarios ([b9a90b2](b9a90b2))

### Documentation

* add source control refactor roadmap ([5d5645e](5d5645e))
* align guides with source control workflow ([d4ce756](d4ce756))
* **claude:** remove session-handoff references from agent workflow ([a4ccf9f](a4ccf9f))
* **imgs:** add sync-status screenshot ([a07a895](a07a895))
* **progress:** record CI run 33358507732 triage ([9528528](9528528))
* record CI green evidence for the whole-run concurrency ([8c07011](8c07011))
* record final-fix round in session handoff and progress ([17d241c](17d241c))
* record Gitea E2E CI verification ([bf33cd2](bf33cd2))
* record lifecycle hardening round in session handoff and progress ([9eb3713](9eb3713))
* record sync-status-workflow-ui completion in progress + handoff ([8aa8fdf](8aa8fdf))
* record unified sync notification verification ([25dfbc8](25dfbc8))
* restore README demo media ([daf7c81](daf7c81))

### Code Refactoring

* **diff:** extract shared DiffViewer, gfs-conflict-modal shell, and gfs-diff-surface tokens ([769f82d](769f82d))
* **source-control:** converge UI to sync-intent workflow ([639840a](639840a)), closes [#135](#135)
* **source-control:** Selected section becomes a read-only action queue ([7538e0a](7538e0a))
* **source-control:** staged/Changes split with collapsible sections ([60790a3](60790a3))
* **source-control:** unify Sync into one plan, one commit ([264ae4b](264ae4b))
* **sync-status:** integrate source control view model ([8c69cc8](8c69cc8))
* **sync:** compact batch-conflict header and drop totalFiles from the interaction port ([a7dcda7](a7dcda7))
* **test:** move real-provider E2E to e2e-tests/provider/, commit static runtime files ([376901f](376901f)), closes [#143](#143) [#142](#142)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants