1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# overleaf-ce-sync
[Español](README.es.md)
Two-way "git-like" sync for a **self-hosted Overleaf Community Edition**.
The native Git integration (`git-bridge`) is a Server Pro / paid feature and is
not shippable with CE — the Snapshot API and OAuth2 server it needs live in
closed-source modules. This tool gives you the next best thing: pull a project
to disk, version it with your own `git`, and push changes back.
It talks to the same HTTP endpoints the web UI uses. The plain-HTTP login that
is broken on overleaf.com (CAPTCHA) works fine on CE because CE has no CAPTCHA.
## Install
With [pipx](https://pipx.pypa.io) (isolated venv, `overleaf-ce-sync` on PATH):
```bash
git clone https://git.all.ar/pub/overleaf-ce-sync.git
cd overleaf-ce-sync
pipx install -e . # -e = editable: source edits apply with no reinstall
```
The only dependency is `requests`; Python 3.8 or newer.
If pipx warns that `~/.local/bin` isn't on PATH, run `pipx ensurepath` and open
a new shell.
## Usage
```bash
# 1. log in once per host (cookie saved to ~/.config/overleaf-ce, 0600)
overleaf-ce-sync login --host overleaf.example.com --email you@example.com
# 2. find your project id
overleaf-ce-sync list
# 3. clone it into a git repo
overleaf-ce-sync clone <project-id> mybook
cd mybook
# 4. normal loop
overleaf-ce-sync pull # bring down remote changes (overwrites local)
# ...edit, git commit...
overleaf-ce-sync push # upload locally-changed files
```
After the first `pull`/`clone`, the host + project id are remembered in
`.overleaf-ce.json`, so inside the project dir you can just run
`overleaf-ce-sync pull` / `overleaf-ce-sync push`.
## How it works
The sync is anchored to **git commits**, so `push` publishes only what you have
committed — never your working-tree scratch edits.
- **pull** → `GET /Project/<id>/download/zip`, extract over the working tree,
then make a "sync commit" and remember it as `last_sync_commit` in
`.overleaf-ce.json`.
- **push** → upload the **committed** state (HEAD). It diffs
`last_sync_commit..HEAD`, uploads each changed file's *committed* bytes
(`git show HEAD:<path>`) via `POST /Project/<id>/upload` (overwrites by name),
then advances `last_sync_commit` to HEAD. Uncommitted edits are ignored.
- The upload endpoint needs the project's **root folder id**. It's discovered
once over socket.io **xhr-polling** (plain HTTP — works behind any reverse
proxy; no websocket upgrade needed) via the `joinProjectResponse` event, and
cached in `.overleaf-ce.json`.
## Limitations / notes
- **push only sends committed changes.** Edit, `git commit`, then `push`. A file
with uncommitted changes is skipped (you'll get a heads-up note).
- **Deletions are not propagated.** Files removed in your commits are reported but
NOT deleted on Overleaf (safer default). Delete them in the web UI; use
`pull --prune` to mirror remote deletions into your working tree.
- **pull overwrites the working tree** and refuses if you have uncommitted
changes (pass `--force` to override). It auto-commits the pulled state, so your
history interleaves "overleaf pull" sync commits with your own.
- **Binary vs doc**: Overleaf decides per file; re-uploading creates new history
versions, so push only sends files whose committed content changed.
- Don't log out in the browser session you used — Overleaf may revoke the cookie.
## Fallback: root folder id without socket.io
If the xhr-polling discovery fails against your setup, fetch the id once from
Mongo on the server and pass it with `--root-folder-id` (it gets cached
afterwards):
```bash
docker compose exec mongo mongosh sharelatex --quiet --eval \
'printjson(db.projects.findOne({_id:ObjectId("<project-id>")},{ "rootFolder._id":1}))'
overleaf-ce-sync push --root-folder-id <root-folder-id>
```
## License
MIT
|