aboutsummaryrefslogtreecommitdiffstats
path: root/README.md
blob: 8ac68015cee364e28d423f546253ace6b14cb981 (plain)
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