# Forgejo integration

This directory contains assets for the ifcurl preview extension.  The features
split into two tiers by what infrastructure they require.

## Feature overview

| Feature | What it needs | Forgejo rebuild? |
|---|---|---|
| "View in 3D" button on file/history pages | `ifcurl.js` + ifcurl service | No |
| Browser IFC viewer | `viewer.html` static asset | No |
| PR diff 3D render | `ifcurl.js` + ifcurl service + **Nginx proxy** | No |
| `[title](ifc://...)` links in markdown → inline preview | `ifcurl.js` + ifcurl service + **Nginx proxy** | No |
| Bare `ifc://...` text in markdown → inline preview | Go patch + `PREVIEW_SERVICE_URL` in app.ini | **Yes** |

All features except bare-URL markdown rendering work with static asset deployment
only — no Forgejo rebuild required.  `ifcurl.js` detects `<a href="ifc://...">` links
produced by Goldmark's standard link parser and replaces them with preview figures
at page load.

The Go patch is needed **only** if you want bare `ifc://...` text in markdown
(without `[title](...)` syntax) to render as a preview.  The Issue button in the
viewer emits `[title](ifc://...)` link syntax, so normal issue/PR workflows work
without the patch.  Minimising the patch surface is deliberate — every line of Go
code must be re-verified against each Forgejo release.

The PR diff feature requires a **reverse proxy** (e.g. Nginx) to expose the
ifcurl service at the same origin as Forgejo.  This is because the diff image is
fetched by the browser (not by Forgejo's server), and browsers block cross-origin
image requests to plain `http://localhost` URLs.  Most production Forgejo
deployments already sit behind Nginx for TLS termination; adding two
`proxy_pass` lines is the only extra step.

---

## Directory layout

```
forgejo/
  go.patch                              ← diff against Forgejo source (apply once)
  modules/markup/markdown/
    ifc_url.go                          ← new Go source file (copy into source tree)
    ifc_url_test.go                     ← Go tests (copy into source tree)
  custom/public/assets/
    viewer.html                         ← browser IFC viewer (no rebuild needed)
    viewer-url.js                       ← ifc:// URL parse/build/resolve logic (ES module)
    viewer-util.js                      ← shared pure utilities: parseCommitHref, isSimpleTypeSelector, etc.
    ifcurl.js                           ← "View in 3D" + PR diff injection (ES module, no rebuild needed)
  templates/custom/
    footer.tmpl                         ← loads ifcurl.js (no rebuild needed)
  server-config/
    ifcurl-api.service                  ← systemd unit for the API service (git + caching)
    ifcurl-render.service               ← systemd unit for the render isolation service
    gitconfig-ifcmerge                  ← git merge driver registration for /etc/gitconfig
    gitattributes                       ← system-wide gitattributes for bare repos
```

---

## Prerequisites

- ifcurl API service + render service running (see [Running the preview service](#running-the-preview-service))
- Nginx (or equivalent) in front of Forgejo if PR diff images are wanted
- Forgejo source tree cloned and building cleanly (`go` 1.21+) — **only if**
  markdown inline preview is wanted

The patch was written against the `v13.0` branch of Forgejo. Check which
Forgejo commit the patch was authored against with:

```bash
cd /path/to/forgejo
git log --oneline modules/markup/markdown/markdown.go | head -3
```

---

## Applying the patch

### 1. Copy new Go source files

```bash
cp forgejo/modules/markup/markdown/ifc_url.go     /path/to/forgejo/modules/markup/markdown/
cp forgejo/modules/markup/markdown/ifc_url_test.go /path/to/forgejo/modules/markup/markdown/
```

### 2. Apply the diff to existing files

```bash
cd /path/to/forgejo
git apply /path/to/ifcurl/forgejo/go.patch
```

This adds one line to `modules/markup/markdown/markdown.go` and six lines to
`modules/setting/markup.go`. If the patch does not apply cleanly (e.g. after
a Forgejo upstream upgrade), the changes are small enough to apply by hand —
see `go.patch` for the exact hunks.

### 3. Run the Go tests

```bash
cd /path/to/forgejo
go test ./modules/markup/markdown/ -run TestIfcURL -v
```

All seven tests should pass before proceeding.

### 4. Build and deploy Forgejo

```bash
cd /path/to/forgejo
go build \
  -tags 'sqlite sqlite_unlock_notify' \
  -ldflags "-X 'forgejo.org/modules/setting.StaticRootPath=/usr/share/forgejo'" \
  -o forgejo .

sudo cp forgejo /usr/bin/forgejo
sudo systemctl restart forgejo
```

Adjust build tags and `-ldflags` to match your existing Forgejo build
configuration.

---

## Building the viewer JavaScript bundle

The viewer loads Three.js, ThatOpen components, and JSZip from locally-bundled
files rather than CDNs.  The bundle files are committed to the repository, so
you only need to re-run the build when upgrading a dependency version.

### Prerequisites

Node.js 18+ and npm (used only at build time; not required on the server).

### Run the build

```bash
cd forgejo/
npm install        # installs pinned versions from package-lock.json
npm run build      # produces the four files below
npm test           # run JavaScript unit tests (viewer-url.js, viewer-util.js)
```

Output files (committed to the repository):

| File | Source | Purpose |
|---|---|---|
| `custom/public/assets/viewer-deps.js` | three + @thatopen/components + @thatopen/components-front + jszip | Main viewer dependencies |
| `custom/public/assets/fragments-worker.js` | @thatopen/fragments worker | IFC parsing web worker |
| `custom/public/assets/web-ifc.wasm` | web-ifc | IFC geometry kernel (single-threaded) |
| `custom/public/assets/web-ifc-mt.wasm` | web-ifc | IFC geometry kernel (multi-threaded) |

The `node_modules/` directory is gitignored and is never deployed.

### Upgrading a dependency

Edit the version number in `package.json`, then:

```bash
cd forgejo/
npm install        # updates package-lock.json
npm run build      # regenerates the bundle from the new version
git add package.json package-lock.json custom/public/assets/
git commit -m "viewer: upgrade <package> to vX.Y.Z"
```

---

## Deploying custom assets (no rebuild required)

These files can be updated at any time without recompiling Forgejo.

### Viewer and URL logic

```bash
sudo mkdir -p /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer.html          /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-url.js        /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-util.js       /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/viewer-deps.js       /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/fragments-worker.js  /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/web-ifc.wasm         /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/web-ifc-mt.wasm      /var/lib/forgejo/custom/public/assets/
sudo cp forgejo/custom/public/assets/ifcurl.js            /var/lib/forgejo/custom/public/assets/
```

`ifcurl.js` is an ES module and imports from `viewer-util.js` at the same path, so both files must be deployed together.

Served at `/assets/viewer.html`, `/assets/viewer-deps.js`, etc.

Note: the correct CustomPath is `/var/lib/forgejo/custom` (Forgejo's default when `FORGEJO_CUSTOM` is
not set). Do not use `/etc/forgejo/public/assets/` — Forgejo does not serve from there.

### "View in 3D" footer template

```bash
sudo mkdir -p /var/lib/forgejo/custom/templates/custom/
sudo cp forgejo/templates/custom/footer.tmpl /var/lib/forgejo/custom/templates/custom/
sudo systemctl restart forgejo
```

---

## Running the preview service

The service is split into two systemd units for security isolation:

- **ifcurl-api** — handles HTTP requests, git fetching, and caching.  Has network
  access and can call git CLI tools.  Holds credentials (`/etc/ifcurl/env`).
  Delegates all IFC rendering to ifcurl-render over a Unix socket.

- **ifcurl-render** — handles IFC parsing and rendering via ifcopenshell and pyvista.
  Has no network access, no credentials, and `execve`/`execveat` are blocked by
  systemd.  Communicates only over the Unix socket.

This split limits the blast radius if a crafted IFC file achieves code execution
in the render process: it cannot reach git hosts or read credentials.

### As systemd services (recommended)

```bash
# Create the two service users
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ifcurl
sudo useradd --system --no-create-home --shell /usr/sbin/nologin ifcurl-render

# Add ifcurl user to ifcurl-render group so it can read the socket
sudo usermod -aG ifcurl-render ifcurl

# Install the units (edit AllowedHosts and port in ifcurl-api.service first)
sudo cp forgejo/server-config/ifcurl-api.service    /etc/systemd/system/
sudo cp forgejo/server-config/ifcurl-render.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now ifcurl-render
sudo systemctl enable --now ifcurl-api
```

Edit the `ExecStart` line in `ifcurl-api.service` to set your Forgejo hostname:

```ini
ExecStart=/usr/local/bin/ifcurl serve \
    --host 127.0.0.1 \
    --port 8000 \
    --allowed-hosts git.example.com
```

Cache limits and other environment variables for the API service go in
`/etc/ifcurl/env` (one `KEY=value` per line):

```bash
IFCURL_CACHE_MAX_GB=10
IFCURL_T2_MAX=16
```

Render-side environment variables (timeouts, memory limits) go in
`/etc/ifcurl/render-env`:

```bash
IFCURL_SANDBOX_TIMEOUT=180
IFCURL_SANDBOX_MEMORY_MB=2048
```

### Single-service mode (testing / simple deployments)

Without `IFCURL_RENDER_SOCKET` set, the API service handles rendering directly
in its own subprocess sandbox.  This is simpler to set up but offers less
isolation.

```bash
ifcurl serve --allowed-hosts git.example.com
```

For a Forgejo instance on a non-standard port (e.g. 3000 for local dev):

```bash
ifcurl serve --allowed-hosts localhost:3000
```

`--allowed-hosts` accepts a comma-separated list. Omitting it allows all
non-private remote hosts, which is unsafe if the service is reachable from
untrusted clients.

## Configuration

Add to `/etc/forgejo/conf/app.ini`:

```ini
[ifcurl]
PREVIEW_SERVICE_URL = http://localhost:8000
```

`PREVIEW_SERVICE_URL` is used **server-side** by Forgejo's markdown renderer to
fetch preview images when rendering ifc:// links in issue descriptions and
comments.  It runs on the Forgejo server itself, so `localhost:8000` is the
right value.

If left empty, ifc:// links in markdown render as plain links with no preview
image.

---

## Nginx reverse-proxy for PR diff images

The PR diff viewer (Case 3 in `footer.tmpl`) injects `<img src="/render_diff?…">`
tags that the **browser** must fetch.  The browser resolves `/render_diff` against
the Forgejo origin, so the ifcurl service must be reachable at the public URL.

Add these proxy locations to the Nginx virtual host that fronts Forgejo:

```nginx
# ifcurl preview service — served under the same origin as Forgejo
location /preview {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}

location /render_diff {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}

location /bcf {
    proxy_pass         http://127.0.0.1:8000;
    proxy_set_header   Host $host;
    proxy_read_timeout 120s;
}
```

Without this proxy the "View in 3D" button (Case 1/2) still works — it opens
the viewer HTML which fetches the IFC file directly.  The PR diff images
(Case 3) will silently fail to load until the proxy is in place.

---

## IFC merge driver (ifcmerge)

Configuring `ifcmerge` as the git merge driver for `.ifc` files lets Forgejo
automatically merge IFC pull requests instead of marking them as conflicted.

### How Forgejo checks mergeability

Forgejo (with git ≥ 2.38) uses `git merge-tree --write-tree` to perform a
trial merge in the bare repository.  This command **does** invoke configured
merge drivers — confirmed by testing.  With ifcmerge set up correctly,
the "can be automatically merged" indicator on a PR reflects ifcmerge's
actual result, not a naive text-conflict check.

**Important:** bare repositories do not read committed `.gitattributes` files
from the tree.  The merge driver must be declared via a system-wide
`core.attributesFile` setting in `/etc/gitconfig`.

### Prerequisites

`ifcmerge` is a single Perl script from
[github.com/brunopostle/ifcmerge](https://github.com/brunopostle/ifcmerge).
Download and install it on the Forgejo server:

```bash
curl -o /usr/local/bin/ifcmerge \
  https://raw.githubusercontent.com/brunopostle/ifcmerge/main/ifcmerge
chmod +x /usr/local/bin/ifcmerge
```

Verify it is on `PATH` for the `forgejo` system user:

```bash
sudo -u forgejo which ifcmerge
```

### Deploy the config files

```bash
# System-wide gitattributes (the critical piece for bare repos)
sudo cp forgejo/server-config/gitattributes /etc/gitattributes

# Append merge driver registration + core.attributesFile to /etc/gitconfig
sudo git config --system include.path /path/to/ifcurl/forgejo/server-config/gitconfig-ifcmerge
```

Or apply the blocks from `server-config/gitconfig-ifcmerge` directly into
`/etc/gitconfig` by hand.

### Merge direction

The invariant is that step-IDs in `main`/`master` must never be renumbered,
because existing cross-references depend on them.  The correct driver depends
on which branch is the "local" (`%A`) side:

| Situation | `%A` (local) | `%B` (remote) | Driver to use |
|---|---|---|---|
| Forgejo "Merge commit" button | `main` | PR branch | `ifcmerge_ours` (`--prioritise-local`) |
| Developer rebasing PR from `main` | PR branch | `main` | `ifcmerge` (default) |

For Forgejo server-side merges, use the **"Merge commit"** strategy (not
rebase or squash) and configure `.gitattributes` to use `ifcmerge_ours`:

```
*.ifc merge=ifcmerge_ours
```

`ifcmerge` (no flag) is for client-side use by developers updating their
working branch from `main`, where `main` arrives as the remote (`%B`) side
and its IDs are preserved by default.

### Client-side `.gitattributes`

For client-side merges (`git merge` on a developer's machine) committed
`.gitattributes` files in the repository work fine.  It is still worth
adding one:

```
*.ifc merge=ifcmerge
```

This ensures developers with ifcmerge installed get automatic IFC merges
locally, and tools that read gitattributes (e.g. diff viewers) can
identify `.ifc` files correctly.

---

## Upgrading Forgejo

After upgrading Forgejo upstream:

1. Re-apply `go.patch` (or apply the two hunks by hand if context has shifted).
2. Copy `ifc_url.go` and `ifc_url_test.go` back in — they are not modified by
   upstream.
3. Run the Go tests to verify compatibility.
4. Rebuild and redeploy.

Custom assets and `footer.tmpl` do not require a rebuild and are unaffected by
Forgejo upgrades.
