mirror of
https://github.com/firstsun-dev/git-files-sync.git
synced 2026-07-22 06:54:27 +00:00
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
89 lines
4 KiB
Markdown
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.
|