Commit graph

180 commits

Author SHA1 Message Date
ClaudiaFang
4fb27854fa chore: update harness state for GraphQL push + post-push status fix
Records feat-014 (GitHub GraphQL batch push/delete, commit 114a575) and
feat-015 (avoid stale remote-tree read after push, commit 7676325) as
done. Also archived feat-011/012/013 and other completed narratives out
of progress.md (had grown past its own 80-line cleanup threshold) into
archive/2026-07.md, and trimmed feature_list.json evidence strings under
300 chars — harness self-audit was 96/100, now 100/100.
2026-07-14 11:54:32 +00:00
ClaudiaFang
7676325088 fix(push): avoid stale remote-tree read right after a batch push
After a push completed, SyncStatusView.executeBatchOperation immediately
re-fetched the remote tree via refreshAllStatuses() to update the UI.
GitHub's tree-by-branch-name read can lag a just-completed write by a
moment, so this could misreport a file that was just pushed correctly
as still "modified" (confirmed live: refreshing again a few seconds
later showed it as synced).

SyncManager.pushAllFiles now also returns syncedPaths (path + new sha,
from data already available at the write site: the batch chunk result,
the sequential-push fallback, or the immediate symlink/rename push).
SyncStatusView uses this to mark those files 'synced' directly instead
of re-fetching the remote tree, sidestepping the eventual-consistency
window entirely. Pull is unaffected, still does a full refresh.
2026-07-14 11:53:20 +00:00
ClaudiaFang
114a5759a7 perf(push): GitHub batch push/delete via GraphQL createCommitOnBranch
GitHubService.pushBatch/deleteBatch previously created each file's blob
with a REST POST .../git/blobs call before the tree/commit/ref sequence,
so an N-file batch still cost N/8 rounds of latency-bound round trips even
with concurrency. Switched to GitHub's createCommitOnBranch GraphQL
mutation, which carries file content directly in the request body,
cutting an N-file batch to 2-3 HTTP calls total.

- Extracted BaseGitService.getLatestCommitSha from resolveGitHubStyleBaseTree,
  which is still used by pushSymlink (GraphQL's FileAddition has no file-mode
  field) and Gitea's batch methods (no GraphQL API).
- Added explicit handling for GraphQL's 200-status-with-errors-array failure
  mode, which REST's status-code check can't catch.
- Verified against a live GitHub repo: pushBatch/deleteBatch each produced
  one commit with correct content and blob shas.
2026-07-14 11:52:58 +00:00
ClaudiaFang
12cce6497e fix(ui): connection status badge text invisible on some themes
Some Obsidian themes define --background-modifier-success/error
identically to --text-success/error, so the badge's colored chip
background exactly matched its own text color, making the label
invisible (confirmed via computed style: both resolved to the same
rgb() value). Use the same neutral --background-secondary as the
checking state for connected/disconnected too, relying only on the
state-colored text and dot for contrast.
2026-07-14 11:20:03 +00:00
ClaudiaFang
c7ae0f6754 perf(push): parallelize blob creation within a batch commit
User reported push-all was still slow on GitHub after the one-
commit-per-run batching landed. Root cause: GitHubService/
GiteaService.pushBatch still created each file's blob with a
sequential for loop -- one POST .../git/blobs awaited fully before
the next started -- so wall-clock time was still O(N) round trips of
pure latency even though only one commit came out the other end.

- Add BaseGitService.mapWithConcurrency<T,R>: an order-preserving,
  bounded-concurrency mapper (worker-pool over a shared cursor).
- Add BLOB_CREATE_CONCURRENCY = 8 alongside MAX_BATCH_PUSH_SIZE.
- GitHubService/GiteaService.pushBatch now create blobs via
  mapWithConcurrency instead of a for loop; the tree/commit/ref
  sequence after it is unchanged since each step genuinely depends on
  the previous one's result.
- GitLab is unaffected -- its pushBatch already sends the whole batch
  in one Commits API request, no per-file blob step to parallelize.

Added a regression-guard test: deferred blob-response promises that
only resolve after every blob POST has been dispatched, so a
reintroduced sequential loop deadlocks the test instead of silently
passing.

Evidence: npx eslint . -> 0 errors; npm run build -> clean;
npx vitest run -> 340/340 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 10:40:48 +00:00
ClaudiaFang
83499c92e8 feat(ui): show connection status in the global status bar
Move connection testing/state from the settings tab into a shared
plugin-level store (GitLabFilesPush.connectionStatus,
onConnectionStatusChange, testConnection) so a new status bar item
can reflect the same in-flight test instead of racing a second one.
The settings tab badge now subscribes to the shared state and
unsubscribes on hide(). Status bar shows service name + state,
tooltip carries the error detail, and clicking retests the connection.
2026-07-14 10:40:12 +00:00
ClaudiaFang
d8e3663b8f perf(delete): batch-commit remote-only file deletion
Follow-up to the push-all batching work: batch-deleting remote-only
files via the sync status panel's checkbox multi-select was still N
separate commits (SyncStatusView.performRemoteDeletion looped
gitService.deleteFile once per file).

- Add optional GitServiceInterface.deleteBatch, mirroring pushBatch.
  GitHub/Gitea implement it via the git blob->tree->commit->ref Data
  API, reusing resolveGitHubStyleBaseTree/resolveBaseTree and
  commitGitHubStyleTree (widened its tree-item sha type to
  string | null -- a null sha removes that path from the resulting
  tree, GitHub's way of expressing a delete at the tree level).
  GitLab implements it via the same Commits API endpoint pushBatch
  already uses, with action: 'delete' entries.
- SyncStatusView.performRemoteDeletion now calls deleteBatch once per
  chunk (MAX_BATCH_PUSH_SIZE, reused from the push work) when the
  provider supports it; a failed chunk marks every path in it as
  failed rather than dropping results silently. The original per-file
  loop is preserved verbatim as performRemoteDeletionSequential, the
  fallback for providers without deleteBatch.

Local deletion is unaffected -- it's a local vault operation, not a
git commit.

Evidence: npx eslint . -> 0 errors; npm run build -> clean;
npx vitest run -> 339/339 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 10:24:22 +00:00
ClaudiaFang
c28e0ec09a perf(push): batch-commit push-all files + SHA-based diffing
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>
2026-07-14 09:58:06 +00:00
ClaudiaFang
f1420f0b44 test: add SyncStatusView coverage for the delete-path and folder-collision fixes
Adds minimal ItemView/WorkspaceLeaf mocks to tests/setup.ts (the shared
'obsidian' module mock) -- SyncStatusView had no test file before this.
New tests/ui/SyncStatusView.test.ts covers:
- performRemoteDeletion strips the vaultFolder prefix before calling
  gitService.deleteFile (with and without vaultFolder configured), and
  records the real error message instead of swallowing it.
- identifyExtraFiles classifies a local folder colliding with a stale
  remote record as remote-only rather than a readable file, while still
  treating a genuine local file as checkable.
2026-07-14 09:25:43 +00:00
ClaudiaFang
fa42fea5fd fix: normalize vaultFolder-relative path before gitService.deleteFile
performRemoteDeletion passed the vault-relative path (carrying the
vaultFolder prefix) straight to gitService.deleteFile(), but
getFullPath() expects a path relative to rootPath only -- vaultFolder is
stripped before every other gitService call site (see sync-manager.ts's
getNormalizedPath(file.path) pattern). With a non-empty vaultFolder
setting this built the wrong repo path, so deleteFile's pre-delete
getFile() lookup 404'd -- surfacing as "file was not found on branch
main" for a file the UI still listed as remote-only, since status
refresh already normalizes the path correctly.

Also hardens SyncStatusView against a local directory (or symlink to
one) colliding with a stale remote record at the same path: a new
isLocalFile() helper (adapter.stat().type === 'file') gates
identifyExtraFiles and the hidden-file recursiveScan, and
readFileContent's string-path branch falls back to
readLocalSymlinkTarget() on read failure.
2026-07-14 09:21:32 +00:00
ClaudiaFang
563a28e44f Merge remote-tracking branch 'origin/claude/fix-directory-symlink-pull-260713'
Resolves conflicts between issue #78 (delete-remote-only-file) fixes and
the i18n work landed on this branch in parallel: the delete-result Notice
now uses the i18n t() helper with a new 'partialWithMessage' key (en +
zh-tw) that carries the real error text.
2026-07-14 09:13:45 +00:00
ClaudiaFang
896d77bddf fix: remote-repo root path picker, delete-remote-only-file errors, symlinked-folder EISDIR
- 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.

Closes firstsun-dev/blog#78 (misfiled; this is the actual fix location).
2026-07-14 09:11:26 +00:00
ClaudiaFang
47b5d263a0 Merge remote-tracking branch 'origin/claude/fix-directory-symlink-pull-260713' into claude/i18n-support-260713
# Conflicts:
#	progress.md
#	session-handoff.md
2026-07-13 14:31:15 +00:00
ClaudiaFang
2032dd33ec chore: update harness state for i18n session
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011BNWwPd5zudtW2JQr6ZAUm
2026-07-13 14:28:40 +00:00
ClaudiaFang
144eb286d8 feat: add i18n (multi-language) support (#38)
Add an i18n layer (t(key, vars?)) with English (default) and
Traditional Chinese translations, detecting the active locale via
Obsidian's window.moment.locale() with a fallback to English for
unsupported locales.

Replace ~130 hardcoded UI strings across the settings tab, ribbon/
command labels, Notice messages, and the sync status view/modals with
translated keys. Left untranslated on purpose: service provider names
(GitHub/GitLab/Gitea), diff-format markers, URL placeholders, and
changelog release-note content.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011BNWwPd5zudtW2JQr6ZAUm
2026-07-13 14:28:18 +00:00
ClaudiaFang
671ce93371 chore: update harness state files (progress, handoff, feature list)
Records this session's completed work (#33, #36, #31, #39 consolidated
onto PR #51 alongside the prior session's #40/#41/#42/#48) and the
paused state of feat-009 (#38 i18n), which is blocked on a scope
decision from the user before any code is written.
2026-07-13 14:08:36 +00:00
ClaudiaFang
4eebebc765 feat: show new feature tips after update
Adds a "what's new" modal shown once after the plugin updates to a new
version, so users don't miss what changed:

- New src/changelog.ts: a hand-curated CHANGELOG array (distinct from
  the auto-generated CHANGELOG.md, which lists every commit) where
  entries can be marked `notable` so they're called out separately
  from minor fixes, per the issue's clarification comment.
- New src/utils/version.ts: compareVersions() does numeric per-segment
  comparison (so "1.10.0" sorts after "1.9.0", unlike a plain string
  compare).
- getUnseenReleases() filters+sorts the changelog to what's newer than
  a given "last seen" version, extracted as a pure/testable function
  rather than inlined in main.ts (which isn't unit-tested in this repo).
- New settings field lastSeenVersion, persisted the same way as other
  settings. On a fresh install (empty lastSeenVersion) it's just
  recorded silently — no modal, since there's nothing to compare
  against. On an actual version bump, WhatsNewModal shows the unseen
  releases' highlights, with a "View full changelog" link out to
  CHANGELOG.md and a "Got it" dismiss; the version is recorded either
  way so the tip only ever shows once per upgrade.
- The whole check is wrapped in try/catch so a malformed version
  string can never break plugin startup.

Closes #39
2026-07-13 13:56:38 +00:00
ClaudiaFang
0cec9ad6f8 Merge remote-tracking branch 'origin/claude/fix-html-response-json-error-260713' into claude/fix-directory-symlink-pull-260713 2026-07-13 13:49:48 +00:00
ClaudiaFang
a86721752a fix: surface a clear error when requestUrl() itself rejects with HTML content
parseJson() already handles the case where a response resolves normally
but its .json getter throws on an HTML body (e.g. a login/SSO redirect
or proxy error page). But some Obsidian versions eagerly parse the
response body as JSON inside requestUrl() itself, so the same failure
can instead reject the whole call with a raw SyntaxError — landing in
safeRequest's outer catch, before there's even a response object to
inspect. That catch just rethrew the error verbatim, surfacing the
literal "Unexpected token '<', \"<!DOCTYPE \"... is not valid JSON"
message to the user (as seen in the reported "Failed to refresh" case,
against a self-hosted GitLab instance).

safeRequest's outer catch now recognizes this error's known V8
phrasings and throws the same friendly "received an HTML page" message
parseJson() already produces for the other code path.

Closes #31
2026-07-13 13:46:38 +00:00
ClaudiaFang
2ed5a436b0 perf(refresh): use tree blob SHAs to avoid per-file content fetches
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
2026-07-13 13:30:10 +00:00
ClaudiaFang
29bf3604e3 Merge remote-tracking branch 'origin/claude/settings-ux-improvements-260713' into claude/fix-directory-symlink-pull-260713
# Conflicts:
#	src/settings.ts
#	tests/setup.ts
2026-07-13 13:08:30 +00:00
ClaudiaFang
8f0e6d563c Merge remote-tracking branch 'origin/claude/folder-picker-settings-260713' into claude/fix-directory-symlink-pull-260713 2026-07-13 13:07:47 +00:00
ClaudiaFang
4c8896b6fa fix: symlinked directories no longer break pull discovery
A folder that's actually an OS symlink (e.g. a shared skills folder
linked in from elsewhere) is a single git blob on the remote, not a
real tree. Local scanning code that walked every subfolder returned by
adapter.list() didn't know this, so it recursed straight into whatever
unrelated directory the link pointed at:

- GitignoreManager.scanDir() picked up bogus nested .gitignore paths
  under the linked folder and tried to fetch them from the remote
  repo, spamming 404s on every refresh (this matches the console
  errors in the issue's screenshot).
- SyncStatusView's hidden-file scan recursed the same way instead of
  registering the symlink itself as a trackable path, so the folder
  never resolved to anything pullable — it just showed up as
  permanently "remote only".

Both now check readLocalSymlinkTarget() before recursing into a
folder and treat it as a single link entry instead, matching how file
symlinks are already handled by the existing push/pull machinery.

Also fixes createLocalSymlink() to pass the correct `type` hint to
Node's symlinkSync on Windows — omitting it defaults to 'file', which
produces a broken link when the target is actually a directory (a
no-op on macOS/Linux, but required on Windows).

Closes #33
2026-07-13 12:52:09 +00:00
ClaudiaFang
c107979427 feat(settings): add folder picker for root path and vault folder settings
Attach an AbstractInputSuggest to the "Root path" and "Vault folder"
text inputs so typing shows a filtered dropdown of existing vault
folders, sourced from app.vault.getAllFolders(). Selecting a suggestion
fills the field and dispatches an input event so the existing
onChange/save/initializeGitService flow still fires. Free typing is
still accepted for paths that don't exist yet (e.g. a not-yet-created
repo subfolder for "Root path").

Closes #48

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011BNWwPd5zudtW2JQr6ZAUm
2026-07-13 12:51:32 +00:00
ClaudiaFang
f5ae8ef16d refactor(tests): dedupe TextComponent/TextAreaComponent mocks
Extract shared setPlaceholder/setValue/onChange/triggerChange logic
into a BaseTextComponent<T> generic to fix SonarCloud's new-code
duplication gate on PR #49 (8.3% > 3% threshold).
2026-07-13 12:33:11 +00:00
ClaudiaFang
597989b53c chore: add agent harness (state, verification, lifecycle tracking)
Adds the missing harness subsystems so future agent sessions can start,
stay in scope, verify work, and resume reliably:

- feature_list.json: local mirror of active/next-up work (GitHub Issues
  on Project #6 remains the source of truth for the full backlog)
- progress.md / session-handoff.md / archive/2026-07.md: session state,
  restart point, and monthly archive of finished work
- init.sh: install + lint + test + build verification entrypoint
- AGENTS.md: added Startup Workflow, Definition of Done, Stay in Scope,
  and End of Session sections (existing agent-tier content kept as-is)
- CLAUDE.md: added a short Agent Workflow section routing to the above

Validated with the firstsun-harness audit script: 100/100 across all
five subsystems (instructions, state, verification, scope, lifecycle).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYCTyZw7gUmJ7oh1VTmAqh
2026-07-13 12:27:42 +00:00
ClaudiaFang
28f4f8efd0 feat: resize conflict modal, add connection status badge, and local ignore patterns
- Conflict modal (#42): resize to min(1100px,92vw) x min(85vh,800px) flex
  layout, remove the 280px content height cap, and add a Diff/Local/Remote
  tab switcher on narrow screens (defaults to Diff).
- Settings connection status (#41): show a persistent Connected/Not
  connected/Checking badge in the settings tab, auto-tested on open and
  after an 800ms debounce on token/branch/URL/owner/repo edits, updated
  in place (no full re-render, so typing focus is preserved).
- Local ignore patterns (#40): new "Ignore patterns" setting (.gitignore-
  style, multi-line) applied in GitignoreManager.isIgnored() in addition
  to the repo's own .gitignore, covering push/pull/refresh uniformly.

Closes #42, #41, #40.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YYCTyZw7gUmJ7oh1VTmAqh
2026-07-13 12:19:17 +00:00
semantic-release-bot
ef238cea59 chore(release): 1.2.1 [skip ci]
## [1.2.1](https://github.com/firstsun-dev/git-files-sync/compare/1.2.0...1.2.1) (2026-07-07)

### Bug Fixes

* **compat:** support Obsidian down to 1.11.0 ([d896015](d896015765))

### Documentation

* restyle README, host demo videos on R2, use official download stats ([#44](https://github.com/firstsun-dev/git-files-sync/issues/44)) ([0a4cff5](0a4cff5a46))
2026-07-07 02:45:25 +00:00
ClaudiaFang
0fd289f79f
Merge pull request #46 from firstsun-dev/claude/obsidian-version-adoption-56aej4
Lower minAppVersion to 1.11.0 with runtime compatibility guards
2026-07-07 10:44:09 +08:00
Claude
23c78133c4
ci: remove advanced CodeQL workflow in favor of default setup
The repository has CodeQL "default setup" enabled, which rejects SARIF
uploads from an advanced workflow ("CodeQL analyses from advanced
configurations cannot be processed when the default setup is enabled"),
failing the Analyze check on every PR to main. Drop the advanced
.github/workflows/codeql.yml and let GitHub's default setup handle
CodeQL scanning.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
2026-07-07 02:41:00 +00:00
Claude
692fc0f293 build(compat): derive the compat typecheck's target version dynamically
The 1.11 compatibility gate previously hardcoded the target Obsidian
version in two places (an obsidian-1_11 alias devDependency and a static
tsconfig.compat.json), so bumping minAppVersion would silently leave the
check pinned to the old version.

Replace both with scripts/typecheck-compat.mjs, which reads the target from
versions.json (the entry for the current manifest version, falling back to
manifest.minAppVersion), installs the matching Obsidian typings into a
git-ignored cache on demand, and type-checks src against them. The gate now
follows minAppVersion automatically with no code changes.

Also fold `typecheck:compat` into `npm run build` so it runs wherever the
build runs (the husky pre-commit hook and any CI step that builds).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
2026-07-07 01:53:28 +00:00
Claude
d896015765 fix(compat): support Obsidian down to 1.11.0
Release 1.2.0 raised minAppVersion to 1.13.0, pushing users still on
Obsidian 1.11/1.12 back to the older 1.1.2 build via versions.json. Only
three 1.13-era APIs were in use; two were already runtime-guarded, but
ButtonComponent.setDestructive() would throw on older Obsidian.

- Guard setDestructive() behind applyDestructiveStyle() (no-ops on < 1.13).
- Declare SettingDefinitionItem locally and read update() via a cast so the
  forward-compatible settings code type-checks against 1.11 typings.
- Ship 1.2.1 with minAppVersion 1.11.0 (manifest.json + versions.json).
- Add tsconfig.compat.json and `npm run typecheck:compat` to type-check src
  against the 1.11 API, plus regression tests for applyDestructiveStyle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SSikT9ZAfQF8K4SE3DatD3
2026-07-07 01:53:28 +00:00
ClaudiaFang
0a4cff5a46
docs: restyle README, host demo videos on R2, use official download stats (#44)
* docs(readme): restyle README following obsidian-pm conventions

* docs: host demo videos on R2 instead of GitHub attachments

* docs: use official Obsidian download stats badge

---------

Co-authored-by: ClaudiaFang <tianyao.firstsun@gmail.coim>
2026-07-06 10:31:33 +08:00
semantic-release-bot
bbd9bc64ac chore(release): 1.2.0 [skip ci]
## [1.2.0](https://github.com/firstsun-dev/git-files-sync/compare/1.1.2...1.2.0) (2026-07-05)

### Features

* add Gitea support as third-party Git provider ([130bd93](130bd93f84)), closes [#26](https://github.com/firstsun-dev/git-files-sync/issues/26)
* **sync:** detect symbolic links and add a configurable handling setting ([62b475d](62b475d632))
* **sync:** real symbolic link support (GitHub) with configurable handling ([9bcaed6](9bcaed65f4))

### Bug Fixes

* **deprecations:** migrate off deprecated Obsidian APIs ([5c64b96](5c64b96c08))
* **deps:** resolve Dependabot security alerts in dev dependencies ([a47cfcb](a47cfcb708))
* **lint:** resolve Obsidian plugin linter warnings ([d11ca94](d11ca94073))
* **services:** clear error when Git API returns HTML instead of JSON ([bcc5cda](bcc5cda69c))
* **services:** omit blank sha so creating a new file doesn't 422 ([339d5cb](339d5cbe77))
* **services:** stop logging expected 404s as errors during refresh ([c474a7e](c474a7e6c4))
* **settings:** mask personal access token fields ([235d9e0](235d9e0976))
* **sync:** clearer branch-not-found errors and connection test ([2f6859a](2f6859a2f1))
* **sync:** fall back to adapter read for symlinked files ([76405cf](76405cf2f7))
* **sync:** stop batch push/pull from silently overwriting conflicts ([1364b94](1364b94f0a))
* **sync:** stop false-positive rename detection and 422 on rename push ([06953e1](06953e1b12))
* **ui:** keep ribbon/command labels in sync with configured Git service ([acebafd](acebafd94a))
* **ui:** match Open sync status ribbon icon to the view icon ([9bc8ab7](9bc8ab7d08))
* use two-step branch→SHA resolution in GiteaService.listFiles ([7648eef](7648eef279))

### Performance Improvements

* **ui:** parallelize refresh status checks and throttle re-renders ([1e21061](1e21061aa8))

### Documentation

* add video ([#30](https://github.com/firstsun-dev/git-files-sync/issues/30)) ([49bfe9f](49bfe9f4c2))
* fix CLAUDE.md to match actual codebase and remove subagent delegation directives ([#32](https://github.com/firstsun-dev/git-files-sync/issues/32)) ([85bcaba](85bcaba81f))
* update intro video with refreshed content ([df44a96](df44a96c9b))
* update USAGE_zh.md with Gitea support and provider compatibility table ([615819c](615819c39e))

### Code Refactoring

* **ui:** unify Sync Status icons via Lucide setIcon ([d8d6f9d](d8d6f9dcb1))
2026-07-05 07:20:33 +00:00
ClaudiaFang
a20d30844b
Merge pull request #43 from firstsun-dev/claude/git-files-sync-issue-31-54izdm
Gitea support, symlink handling, sync reliability fixes, and dependency security updates
2026-07-05 15:19:32 +08:00
ClaudiaFang
a47cfcb708 fix(deps): resolve Dependabot security alerts in dev dependencies
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.
2026-07-05 07:12:32 +00:00
ClaudiaFang
e89f6bad70 chore(release): bump minAppVersion to 1.13.0 and version to 1.2.0
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>
2026-07-05 05:50:21 +00:00
ClaudiaFang
06953e1b12 fix(sync): stop false-positive rename detection and 422 on rename push
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>
2026-07-05 05:44:54 +00:00
ClaudiaFang
5c64b96c08 fix(deprecations): migrate off deprecated Obsidian APIs
- Bump obsidian dependency to ^1.13.1
- settings.ts: add getSettingDefinitions() alongside display()
  fallback for Obsidian < 1.13.0 (minAppVersion)
- SyncConflictModal.ts: setWarning() -> setDestructive()
2026-07-05 05:28:53 +00:00
ClaudiaFang
235d9e0976 fix(settings): mask personal access token fields
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.
2026-07-05 05:19:46 +00:00
ClaudiaFang
1111308a2d chore(skills): install clean-code, design-taste-frontend, frontend-design skills
Adds skill files pulled from firstsun-dev/skills and their lockfile
entries, used during this session's UX review and fixes.
2026-07-05 05:15:47 +00:00
ClaudiaFang
d11ca94073 fix(lint): resolve Obsidian plugin linter warnings
- 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
2026-07-05 05:14:28 +00:00
ClaudiaFang
acebafd94a fix(ui): keep ribbon/command labels in sync with configured Git service
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".
2026-07-05 05:13:52 +00:00
ClaudiaFang
1364b94f0a fix(sync): stop batch push/pull from silently overwriting conflicts
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.
2026-07-05 05:05:54 +00:00
ClaudiaFang
2f6859a2f1 fix(sync): clearer branch-not-found errors and connection test
- 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>
2026-07-05 04:57:47 +00:00
Claude
1e21061aa8
perf(ui): parallelize refresh status checks and throttle re-renders
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
2026-06-28 05:36:47 +00:00
Claude
9bcaed65f4
feat(sync): real symbolic link support (GitHub) with configurable handling
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
2026-06-28 05:14:35 +00:00
ClaudiaFang
13d802e33e
Merge pull request #35 from firstsun-dev/claude/trusting-volta-qlg8bk
Claude/trusting volta qlg8bk
2026-06-28 13:01:07 +08:00
Claude
90981261a5
Merge remote-tracking branch 'origin/claude/git-files-sync-issue-31-54izdm' into claude/trusting-volta-qlg8bk
# Conflicts:
#	.github/workflows/ci.yml
#	package-lock.json
2026-06-28 04:50:34 +00:00
Claude
62b475d632
feat(sync): detect symbolic links and add a configurable handling setting
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
2026-06-28 04:45:41 +00:00