firstsun-dev_git-files-sync/docs/symlink-handling.md
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

89 lines
4 KiB
Markdown

# Symbolic Link Handling
This document describes how Git File Sync syncs **symbolic links** (symlinks)
between your vault and the remote repository.
## Background
In Git, a symbolic link is not a normal file — it is stored as a *blob with file
mode `120000`* whose **content is the link's target path** (for example
`../shared/note.md`). Because of this, symlinks need special treatment:
- They cannot be fetched/created like normal files through every provider's API.
- Obsidian exposes no symlink API, and **mobile platforms (iOS/Android) cannot
create OS-level symlinks at all**.
To keep behavior predictable, Git File Sync makes symlink handling an explicit,
configurable choice.
## The setting
**Settings → Git File Sync → Symbolic links**
| Mode | What it does |
| :--- | :--- |
| **Real symlink** (default) | Recreates a real OS symlink on **desktop** when pulling, and pushes local symlinks back as real Git symlinks (mode `120000`). Falls back to content on platforms/providers that can't do this (see below). |
| **Follow** | Treats a symlink as the file it points to: syncs the **target's content** as a normal file. Never creates links. |
| **Skip** | Ignores symlinks entirely — they are not listed, pulled, or pushed. |
The default is **Real symlink**.
## Provider support
Creating a symlink on the remote requires writing a `120000` blob, which is only
possible with a provider that exposes the full **Git Data API**.
| Provider | Real symlink | Notes |
| :--- | :---: | :--- |
| **GitHub** | ✅ | Full support via the Git Data API (blob → tree → commit → ref). |
| **GitLab** | ❌ | The Commits API can't set the symlink mode. "Real" is treated as **Skip**. |
| **Gitea** | ❌ | No write access to git data endpoints. "Real" is treated as **Skip**. |
**Config safeguard (防呆):** the "Real symlink" option is only offered in the
settings dropdown when the active provider is GitHub. On other providers a saved
value of `real` is automatically treated as `skip`, so a symlink is never
silently turned into an ordinary file.
## Platform behavior
| | Desktop | Mobile (iOS/Android) |
| :--- | :--- | :--- |
| **Real symlink (GitHub)** | Creates/reads real OS symlinks via Node's `fs`. | No symlink API — falls back: on pull, writes the target *path* as the file content. |
## What happens per direction
### Pull (remote → vault)
- **Real** + GitHub + desktop: a remote symlink is recreated as a real OS link
pointing at its target.
- **Real** on mobile (or if the link can't be created): the link target path is
written into the file as its content, so it still round-trips.
- **Follow**: the target's content is written as a normal file. (For a link whose
target is a normal in-repo file, GitHub's API already returns that content.)
- **Skip**: remote symlinks are excluded from the sync list.
### Push (vault → remote)
- **Real** + GitHub: a local symlink is committed as a real symlink blob
(mode `120000`) using the Git Data API.
- **Skip**: local symlinks are not pushed.
- **Follow**: the content the link points to is pushed as a normal file.
#### Safety: Follow never destroys a remote symlink
A `follow` push reads *through* a local symlink and would otherwise overwrite the
remote with a regular file. To avoid silently converting a remote symlink into an
ordinary file, **if the remote path is detected as a symlink, the push is skipped
with a notice**. Use **Real symlink** (GitHub) if you actually want to manage the
link.
> Note: remote-symlink detection on push relies on the provider reporting the
> symlink type, which currently only GitHub does. On GitLab/Gitea a `follow` push
> cannot detect a remote symlink and may convert it to a regular file.
## Error handling
Earlier versions surfaced confusing errors when a symlinked `.gitignore` (or other
symlink) returned `404` during a refresh. Expected `404`s are now logged at debug
level instead of as errors, and symlinks are detected up front, so a normal
refresh/pull no longer shows spurious "Git Service Request Failed (404)" messages.