|
| 1 | +# JIP-0002: Git Support for Containers |
| 2 | + |
| 3 | +## Status |
| 4 | + |
| 5 | +Draft |
| 6 | + |
| 7 | +## Abstract |
| 8 | + |
| 9 | +Enable Solid containers to function as Git repositories, allowing standard `git clone`, `git push`, and `git pull` operations for versioned, offline-capable data management. |
| 10 | + |
| 11 | +## Motivation |
| 12 | + |
| 13 | +1. **Familiar workflow** - Developers already know Git |
| 14 | +2. **Offline editing** - Clone pod locally, work offline, push when ready |
| 15 | +3. **Collaboration** - Branch, merge, and collaborate on pod content |
| 16 | +4. **Backup** - Clone provides automatic incremental backup |
| 17 | +5. **Version history** - Built-in audit trail for all resource changes |
| 18 | +6. **Tooling** - Leverage existing Git ecosystem (editors, diff tools, CI/CD) |
| 19 | + |
| 20 | +## Specification |
| 21 | + |
| 22 | +### 1. Git HTTP Smart Protocol |
| 23 | + |
| 24 | +JSS SHALL implement the [Git Smart HTTP Protocol](https://git-scm.com/docs/http-protocol) by delegating to `git http-backend`. |
| 25 | + |
| 26 | +### 2. Request Detection |
| 27 | + |
| 28 | +Git requests are identified by URL patterns: |
| 29 | + |
| 30 | +| Pattern | Operation | |
| 31 | +|---------|-----------| |
| 32 | +| `*/info/refs?service=git-upload-pack` | Clone/fetch negotiation | |
| 33 | +| `*/info/refs?service=git-receive-pack` | Push negotiation | |
| 34 | +| `*/git-upload-pack` | Clone/fetch data transfer | |
| 35 | +| `*/git-receive-pack` | Push data transfer | |
| 36 | + |
| 37 | +The server MAY also recognize `.git` suffix on container URLs. |
| 38 | + |
| 39 | +### 3. Repository Structure |
| 40 | + |
| 41 | +Each container MAY have an associated Git repository: |
| 42 | + |
| 43 | +``` |
| 44 | +/alice/ |
| 45 | +├── .git/ # Git metadata (bare or regular) |
| 46 | +├── inbox/ |
| 47 | +├── public/ |
| 48 | +│ └── notes.ttl |
| 49 | +└── settings/ |
| 50 | +``` |
| 51 | + |
| 52 | +#### 3.1 Bare vs Regular Repositories |
| 53 | + |
| 54 | +The server SHOULD use bare repositories for efficiency: |
| 55 | + |
| 56 | +``` |
| 57 | +/alice/.git/ # Bare repository |
| 58 | +├── HEAD |
| 59 | +├── config |
| 60 | +├── objects/ |
| 61 | +└── refs/ |
| 62 | +``` |
| 63 | + |
| 64 | +The actual container contents serve as the working tree. |
| 65 | + |
| 66 | +### 4. Environment Configuration |
| 67 | + |
| 68 | +The handler SHALL set CGI environment variables for `git http-backend`: |
| 69 | + |
| 70 | +```javascript |
| 71 | +{ |
| 72 | + GIT_PROJECT_ROOT: '<data-root>', |
| 73 | + GIT_HTTP_EXPORT_ALL: '1', |
| 74 | + GIT_HTTP_RECEIVE_PACK: 'true', |
| 75 | + REQUEST_METHOD: request.method, |
| 76 | + PATH_INFO: '/<pod>/.git' + gitPath, |
| 77 | + QUERY_STRING: request.queryString, |
| 78 | + CONTENT_TYPE: request.headers['content-type'], |
| 79 | + CONTENT_LENGTH: request.headers['content-length'], |
| 80 | + HTTP_CONTENT_ENCODING: request.headers['content-encoding'], |
| 81 | + GIT_CONFIG_PARAMETERS: "'uploadpack.allowTipSHA1InWant=true'" |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### 5. Authorization |
| 86 | + |
| 87 | +Git operations SHALL use existing WAC authorization: |
| 88 | + |
| 89 | +| Git Operation | Required Mode | |
| 90 | +|---------------|---------------| |
| 91 | +| `clone` / `fetch` | `acl:Read` on container | |
| 92 | +| `push` | `acl:Write` on container | |
| 93 | + |
| 94 | +The server MUST check authorization before invoking `git http-backend`. |
| 95 | + |
| 96 | +### 6. Repository Initialization |
| 97 | + |
| 98 | +#### 6.1 Explicit Initialization |
| 99 | + |
| 100 | +POST to container with `Link: <http://git-scm.com/>; rel="type"`: |
| 101 | + |
| 102 | +```http |
| 103 | +POST /alice/projects/ HTTP/1.1 |
| 104 | +Link: <http://git-scm.com/>; rel="type" |
| 105 | +``` |
| 106 | + |
| 107 | +Response: |
| 108 | +```http |
| 109 | +HTTP/1.1 201 Created |
| 110 | +Location: /alice/projects/.git/ |
| 111 | +``` |
| 112 | + |
| 113 | +#### 6.2 Auto-Initialization (Optional) |
| 114 | + |
| 115 | +The server MAY auto-initialize a Git repository on first clone attempt if the container exists but has no `.git`. |
| 116 | + |
| 117 | +### 7. Clone URL Format |
| 118 | + |
| 119 | +Containers are cloneable at their LDP URL: |
| 120 | + |
| 121 | +```bash |
| 122 | +git clone https://example.com/alice/projects/ |
| 123 | +``` |
| 124 | + |
| 125 | +Or with explicit `.git` suffix: |
| 126 | + |
| 127 | +```bash |
| 128 | +git clone https://example.com/alice/projects/.git |
| 129 | +``` |
| 130 | + |
| 131 | +### 8. CORS Headers |
| 132 | + |
| 133 | +CORS headers MUST be applied before Git handling to support browser-based Git clients: |
| 134 | + |
| 135 | +```http |
| 136 | +Access-Control-Allow-Origin: * |
| 137 | +Access-Control-Allow-Methods: GET, POST, OPTIONS |
| 138 | +Access-Control-Allow-Headers: Content-Type, Authorization |
| 139 | +``` |
| 140 | + |
| 141 | +### 9. Content Negotiation |
| 142 | + |
| 143 | +When a Git client requests `/info/refs`, the server detects this via: |
| 144 | +- Query parameter: `?service=git-upload-pack` or `?service=git-receive-pack` |
| 145 | +- User-Agent containing "git" |
| 146 | + |
| 147 | +Non-Git requests to the same URL return normal LDP container listing. |
| 148 | + |
| 149 | +## Implementation |
| 150 | + |
| 151 | +### Reference Implementation |
| 152 | + |
| 153 | +Based on [nosdav/server](https://github.com/nosdav/server) git support: |
| 154 | + |
| 155 | +```javascript |
| 156 | +import { spawn } from 'child_process'; |
| 157 | + |
| 158 | +const GIT_PATHS = ['/info/refs', '/git-upload-pack', '/git-receive-pack']; |
| 159 | + |
| 160 | +function isGitRequest(url) { |
| 161 | + return GIT_PATHS.some(p => url.includes(p)) || url.endsWith('.git'); |
| 162 | +} |
| 163 | + |
| 164 | +async function handleGit(request, reply) { |
| 165 | + const repoPath = extractRepoPath(request.url); |
| 166 | + |
| 167 | + // Check authorization |
| 168 | + const canRead = await checkAccess(repoPath, request.webId, 'read'); |
| 169 | + const canWrite = await checkAccess(repoPath, request.webId, 'write'); |
| 170 | + |
| 171 | + if (request.url.includes('receive-pack') && !canWrite) { |
| 172 | + return reply.code(403).send({ error: 'Write access denied' }); |
| 173 | + } |
| 174 | + if (!canRead) { |
| 175 | + return reply.code(403).send({ error: 'Read access denied' }); |
| 176 | + } |
| 177 | + |
| 178 | + // Spawn git http-backend |
| 179 | + const git = spawn('git', ['http-backend'], { |
| 180 | + env: { |
| 181 | + ...process.env, |
| 182 | + GIT_PROJECT_ROOT: DATA_ROOT, |
| 183 | + GIT_HTTP_EXPORT_ALL: '1', |
| 184 | + GIT_HTTP_RECEIVE_PACK: canWrite ? 'true' : 'false', |
| 185 | + REQUEST_METHOD: request.method, |
| 186 | + PATH_INFO: repoPath, |
| 187 | + QUERY_STRING: request.url.split('?')[1] || '', |
| 188 | + CONTENT_TYPE: request.headers['content-type'] || '', |
| 189 | + GIT_CONFIG_PARAMETERS: "'uploadpack.allowTipSHA1InWant=true'" |
| 190 | + } |
| 191 | + }); |
| 192 | + |
| 193 | + // Pipe request body to git stdin |
| 194 | + request.raw.pipe(git.stdin); |
| 195 | + |
| 196 | + // Parse CGI response headers |
| 197 | + let headersParsed = false; |
| 198 | + git.stdout.on('data', (chunk) => { |
| 199 | + if (!headersParsed) { |
| 200 | + const headerEnd = chunk.indexOf('\r\n\r\n'); |
| 201 | + if (headerEnd !== -1) { |
| 202 | + const headers = chunk.slice(0, headerEnd).toString(); |
| 203 | + // Parse and set response headers |
| 204 | + headersParsed = true; |
| 205 | + } |
| 206 | + } |
| 207 | + }); |
| 208 | + |
| 209 | + // Pipe git stdout to response |
| 210 | + git.stdout.pipe(reply.raw); |
| 211 | +} |
| 212 | +``` |
| 213 | + |
| 214 | +Tracking issue: [#5](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/5) |
| 215 | + |
| 216 | +## Security Considerations |
| 217 | + |
| 218 | +1. **Path Traversal**: Validate repository paths to prevent access outside data root |
| 219 | +2. **Resource Exhaustion**: Large pushes could exhaust disk; consider quotas |
| 220 | +3. **Executable Content**: Git hooks are disabled by default on server |
| 221 | +4. **Authentication**: Reuse existing Bearer/DPoP/Nostr auth before git operations |
| 222 | + |
| 223 | +## Compatibility |
| 224 | + |
| 225 | +| Spec | Compatibility | |
| 226 | +|------|---------------| |
| 227 | +| [Solid Protocol](https://solidproject.org/TR/protocol) | Compatible - containers remain LDP-accessible | |
| 228 | +| [LDP](https://www.w3.org/TR/ldp/) | Compatible - Git is additive | |
| 229 | +| [WAC](https://solidproject.org/TR/wac) | Uses for authorization | |
| 230 | +| [Git HTTP Protocol](https://git-scm.com/docs/http-protocol) | Implements | |
| 231 | + |
| 232 | +## Prior Art |
| 233 | + |
| 234 | +- **[nosdav/server](https://github.com/nosdav/server)** - Git support via `git http-backend` |
| 235 | +- **[QuitStore](https://github.com/AKSW/QuitStore)** - "Quads in Git" - Git + RDF/SPARQL versioning |
| 236 | +- **[express-git](https://www.npmjs.com/package/express-git)** - Express middleware for Git HTTP |
| 237 | +- **[git-http-backend npm](https://www.npmjs.com/package/git-http-backend)** - Node.js wrapper |
| 238 | + |
| 239 | +## References |
| 240 | + |
| 241 | +- [Git Smart HTTP](https://git-scm.com/book/en/v2/Git-on-the-Server-Smart-HTTP) |
| 242 | +- [git-http-backend Documentation](https://git-scm.com/docs/git-http-backend) |
| 243 | +- [Git HTTP Protocol](https://git-scm.com/docs/http-protocol) |
| 244 | +- [nosdav git commits](https://github.com/nosdav/server/commits/gh-pages/) |
| 245 | + |
| 246 | +## Changelog |
| 247 | + |
| 248 | +- 2024-12-27: Initial draft |
0 commit comments