Skip to content

Commit 77709b0

Browse files
committed
Initial commit
0 parents  commit 77709b0

File tree

21 files changed

+3997
-0
lines changed

21 files changed

+3997
-0
lines changed

.github/workflows/main.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
on:
2+
push:
3+
branches: [main]
4+
pull_request:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
jobs:
9+
main:
10+
runs-on: ${{ matrix.os }}
11+
strategy:
12+
matrix:
13+
os: [ubuntu-latest, macos-latest]
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- uses: goto-bus-stop/setup-zig@v2
19+
with:
20+
version: 0.14.0
21+
22+
- name: Install dependencies (Ubuntu)
23+
if: matrix.os == 'ubuntu-latest'
24+
run: |
25+
sudo apt-get update
26+
sudo apt-get install -y sqlite3 libsqlite3-dev libgit2-dev
27+
28+
- name: Install dependencies (macOS)
29+
if: matrix.os == 'macos-latest'
30+
run: |
31+
brew install sqlite libgit2
32+
33+
- run: zig build
34+
35+
- run: zig build test

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.zig-cache
2+
zig-out
3+
.claude

CLAUDE.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to [Claude Code](https://claude.ai/code) when working with code in [this repository](https://github.com/chrislloyd/git-remote-sqlite).
4+
5+
Refer to:
6+
7+
* @docs/development.md
8+
* @docs/style.md
9+
10+
Some tips:
11+
12+
* Any time you make something consistent, please document the decision in @docs/style.md.
13+
* Always write in a terse, to the point style.
14+
* The tests are fast and easy to run (`zig build test`) - always run them after refactoring.

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright (c) 2025 Chris Lloyd <chris@chrislloyd.net>
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
# git-remote-sqlite
2+
3+
- [Latest release](https://github.com/chrislloyd/git-remote-sqlite/releases/latest)
4+
- [Latest changes](https://github.com/chrislloyd/git-remote-sqlite/commits/main)
5+
- [Source code](https://github.com/chrislloyd/git-remote-sqlite)
6+
7+
---
8+
9+
**git-remote-sqlite(1)** is a [Git protocol helper](https://git-scm.com/docs/gitremote-helpers) that helps you store a Git repository in a [SQLite](https://www.sqlite.org) database. Why would you want to do this?
10+
11+
1. **Simple Git hosting**. Hosting git repositories typically involves using a third-party forge like GitHub or Sourcehut. These provide value by automating hosting NFS mounts and providing a web interface for collaboration.
12+
2. **Self-contained application bundle**. It serves as both a Git repository hosting alternative and enables Lisp/Smalltalk-style development images where code and its runtime state coexist.
13+
3. **(Advanced) More control over automation**. Leveraging Git hooks lets you transactionally automate tasks like migrations.
14+
4. **(Advanced) Build your own workflows**. An application can be built that is self-updating - you don't need to rely on pull-requests or emails etc. to update your application, you can easily build updating itself into your application.
15+
16+
## Installation
17+
18+
### Prerequisites
19+
20+
- Git (version 2.25+)
21+
- SQLite (version 3.30+)
22+
- Zig (version 0.14.0) - only needed if building from source
23+
24+
### Using Pre-built Binaries (Recommended)
25+
26+
1. Download the latest release for your platform:
27+
28+
```bash
29+
# macOS
30+
curl -L https://github.com/chrislloyd/git-remote-sqlite/releases/latest/download/git-remote-sqlite-macos -o git-remote-sqlite
31+
32+
# Linux
33+
curl -L https://github.com/chrislloyd/git-remote-sqlite/releases/latest/download/git-remote-sqlite-linux -o git-remote-sqlite
34+
```
35+
36+
2. Make the binary executable:
37+
38+
```bash
39+
chmod +x git-remote-sqlite
40+
```
41+
42+
3. Move the binary to your `$PATH`:
43+
44+
```bash
45+
sudo mv git-remote-sqlite /usr/local/bin/
46+
```
47+
48+
4. The installation is complete. The binary functions as both a standalone command and as a Git remote helper.
49+
50+
### Building from Source
51+
52+
1. Clone the repository:
53+
54+
```bash
55+
git clone https://github.com/chrislloyd/git-remote-sqlite.git
56+
cd git-remote-sqlite
57+
```
58+
59+
2. Build the binary using Zig:
60+
61+
```bash
62+
zig build
63+
```
64+
65+
3. Copy the binary to your path:
66+
67+
```bash
68+
sudo cp zig-out/bin/git-remote-sqlite /usr/local/bin/
69+
```
70+
71+
4. The installation is complete. The binary functions as both a standalone command and as a Git remote helper.
72+
73+
## Basic Usage
74+
75+
### 1. Configure Repository Settings
76+
77+
You can configure server-side git settings stored in the SQLite database:
78+
79+
```bash
80+
# Set configuration variables (similar to editing server-side git config)
81+
git-remote-sqlite config myapp.db receive.denyDeletes true
82+
git-remote-sqlite config myapp.db receive.denyNonFastForwards true
83+
84+
# List all configured settings
85+
git-remote-sqlite config myapp.db --list
86+
87+
# Get a specific setting value
88+
git-remote-sqlite config myapp.db --get receive.denyDeletes
89+
90+
# Remove a setting
91+
git-remote-sqlite config myapp.db --unset receive.denyDeletes
92+
```
93+
94+
These settings are stored in the `git_config` table and affect how git operations are processed when interacting with the SQLite repository.
95+
96+
### 2. Push Code to the Database (Coming Soon)
97+
98+
Push your code to the SQLite database:
99+
100+
```bash
101+
git push sqlite://myapp.db main
102+
```
103+
104+
### 3. Use as a Git Remote (Coming Soon)
105+
106+
Add the database as a git remote:
107+
108+
```bash
109+
# In your existing git repository
110+
git remote add origin sqlite://myapp.db
111+
112+
# Note: 'origin' is just a name for the remote - you can use any name you prefer
113+
```
114+
115+
### 4. Checkout Code from the Database (Coming Soon)
116+
117+
Checkout a specific commit from the database:
118+
119+
```bash
120+
git checkout sqlite://myapp.db main
121+
```
122+
123+
This will extract the specified commit to the current directory.
124+
125+
## Development Status
126+
127+
Currently implemented:
128+
- [x] Configuration management (set, get, list, unset)
129+
- [ ] Git remote helper protocol (push/pull functionality)
130+
- [ ] Git hooks
131+
- [ ] Pack file management (tables defined but not yet implemented)
132+
133+
## Database Schema
134+
135+
**git_objects**: Stores git objects (blobs, trees, commits, tags)
136+
137+
| Column | Type | Constraints | Description |
138+
|--|--|--|--|
139+
| `sha` | TEXT | PRIMARY KEY, CHECK(length(sha) = 40 AND sha GLOB '[0-9a-f]*') | Object SHA hash |
140+
| `type` | TEXT | NOT NULL, CHECK(type IN ('blob', 'tree', 'commit', 'tag')) | Object type (blob, tree, commit, tag) |
141+
| `data` | BLOB | NOT NULL | Object content |
142+
143+
*Indexes:*
144+
- `CREATE INDEX idx_git_objects_type ON git_objects(type);` - For efficient queries by object type
145+
146+
**git_refs**: Stores git references
147+
148+
| Column | Type | Constraints | Description |
149+
|--|--|--|--|
150+
| `name` | TEXT | PRIMARY KEY, CHECK(name GLOB 'refs/*') | Reference name (e.g., 'refs/heads/main') |
151+
| `sha` | TEXT | NOT NULL, FOREIGN KEY REFERENCES git_objects(sha) | Commit SHA the ref points to |
152+
| `type` | TEXT | NOT NULL, CHECK(type IN ('branch', 'tag', 'remote')) | Reference type (branch, tag, remote) |
153+
154+
*Indexes:*
155+
- `CREATE INDEX idx_git_refs_sha ON git_refs(sha);` - For finding all refs pointing to a specific commit
156+
157+
**git_symbolic_refs**: Stores symbolic references (like HEAD)
158+
159+
| Column | Type | Constraints | Description |
160+
|--|--|--|--|
161+
| `name` | TEXT | PRIMARY KEY | Symbolic reference name (e.g., 'HEAD') |
162+
| `target` | TEXT | NOT NULL, FOREIGN KEY REFERENCES git_refs(name) | Target reference path |
163+
164+
**git_packs**: Stores git pack files *(pending implementation)*
165+
166+
| Column | Type | Constraints | Description |
167+
|--|--|--|--|
168+
| `id` | INTEGER | PRIMARY KEY | Unique pack identifier |
169+
| `name` | TEXT | NOT NULL, UNIQUE | Pack name/identifier |
170+
| `data` | BLOB | NOT NULL | Pack file binary data |
171+
| `index_data` | BLOB | NOT NULL | Pack index binary data |
172+
173+
*Indexes:*
174+
- `CREATE INDEX idx_git_packs_name ON git_packs(name);` - For efficient lookups by pack name
175+
176+
**git_pack_entries**: Maps objects to packs for faster lookups *(pending implementation)*
177+
178+
| Column | Type | Constraints | Description |
179+
|--|--|--|--|
180+
| `pack_id` | INTEGER | NOT NULL, FOREIGN KEY REFERENCES git_packs(id) | Reference to the pack |
181+
| `sha` | TEXT | NOT NULL, FOREIGN KEY REFERENCES git_objects(sha) | Object SHA contained in the pack |
182+
| `offset` | INTEGER | NOT NULL | Offset position within the pack |
183+
| PRIMARY KEY | | (pack_id, sha) | Composite primary key |
184+
185+
*Indexes:*
186+
- `CREATE INDEX idx_git_pack_entries_sha ON git_pack_entries(sha);` - For finding which pack contains a specific object
187+
188+
189+
**git_config**: Stores git configuration settings
190+
191+
| Column | Type | Constraints | Description |
192+
|--|--|--|--|
193+
| `key` | TEXT | PRIMARY KEY | Configuration key |
194+
| `value` | TEXT | NOT NULL | Configuration value |
195+
196+
## Contributing
197+
198+
Contributions are welcome! Please feel free to submit a Pull Request.

build.zig

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
const std = @import("std");
2+
const builtin = @import("builtin");
3+
4+
comptime {
5+
const expected_zig_version = std.SemanticVersion{
6+
.major = 0,
7+
.minor = 14,
8+
.patch = 0,
9+
};
10+
const actual_zig_version = builtin.zig_version;
11+
if (actual_zig_version.order(expected_zig_version).compare(.lt)) {
12+
@compileError(std.fmt.comptimePrint(
13+
"unsupported zig version: expected {}, found {}",
14+
.{ expected_zig_version, actual_zig_version },
15+
));
16+
}
17+
}
18+
19+
pub fn build(b: *std.Build) void {
20+
const steps = .{
21+
.run = b.step("run", "Run git-remote-sqlite"),
22+
.@"test" = b.step("test", "Run tests"),
23+
.test_unit = b.step("test:unit", "Run unit tests"),
24+
.test_integration = b.step("test:integration", "Run integration tests"),
25+
};
26+
27+
const target = b.standardTargetOptions(.{});
28+
const optimize = b.standardOptimizeOption(.{});
29+
30+
build_git_remote_sqlite(b, .{
31+
.run = steps.run,
32+
.install = b.getInstallStep(),
33+
}, .{ .target = target, .optimize = optimize });
34+
35+
build_test(b, .{ .@"test" = steps.@"test", .test_unit = steps.test_unit, .test_integration = steps.test_integration }, .{ .target = target, .optimize = optimize });
36+
}
37+
38+
fn build_git_remote_sqlite(b: *std.Build, steps: struct {
39+
run: *std.Build.Step,
40+
install: *std.Build.Step,
41+
}, options: struct {
42+
target: std.Build.ResolvedTarget,
43+
optimize: std.builtin.OptimizeMode,
44+
}) void {
45+
// Create a module for our main entry point
46+
const main_mod = b.createModule(.{
47+
.root_source_file = b.path("src/main.zig"),
48+
.target = options.target,
49+
.optimize = options.optimize,
50+
});
51+
52+
// Create an executable from our module
53+
const exe = b.addExecutable(.{
54+
.name = "git-remote-sqlite",
55+
.root_module = main_mod,
56+
});
57+
58+
// Add system library linkage
59+
exe.linkSystemLibrary("sqlite3");
60+
exe.linkSystemLibrary("git2");
61+
62+
// This declares intent for the executable to be installed into the
63+
// standard location when the user invokes the "install" step (the default
64+
// step when running `zig build`).
65+
b.installArtifact(exe);
66+
67+
// This *creates* a Run step in the build graph, to be executed when another
68+
// step is evaluated that depends on it. The next line below will establish
69+
// such a dependency.
70+
const run_cmd = b.addRunArtifact(exe);
71+
72+
// By making the run step depend on the install step, it will be run from the
73+
// installation directory rather than directly from within the cache directory.
74+
// This is not necessary, however, if the application depends on other installed
75+
// files, this ensures they will be present and in the expected location.
76+
run_cmd.step.dependOn(b.getInstallStep());
77+
78+
// This allows the user to pass arguments to the application in the build
79+
// command itself, like this: `zig build run -- arg1 arg2 etc`
80+
if (b.args) |args| {
81+
run_cmd.addArgs(args);
82+
}
83+
84+
// This creates a build step. It will be visible in the `zig build --help` menu,
85+
// and can be selected like this: `zig build run`
86+
// This will evaluate the `run` step rather than the default, which is "install".
87+
// const run_step = b.step("run", "Run the app");
88+
steps.run.dependOn(&run_cmd.step);
89+
}
90+
91+
fn build_test(b: *std.Build, steps: struct {
92+
@"test": *std.Build.Step,
93+
test_unit: *std.Build.Step,
94+
test_integration: *std.Build.Step,
95+
}, options: struct {
96+
target: std.Build.ResolvedTarget,
97+
optimize: std.builtin.OptimizeMode,
98+
}) void {
99+
const unit_mod = b.createModule(.{
100+
.root_source_file = b.path("src/tests.zig"),
101+
.target = options.target,
102+
.optimize = options.optimize,
103+
});
104+
const unit = b.addTest(.{
105+
.root_module = unit_mod,
106+
});
107+
unit.linkSystemLibrary("sqlite3");
108+
unit.linkSystemLibrary("git2");
109+
110+
const run_unit = b.addRunArtifact(unit);
111+
steps.test_unit.dependOn(&run_unit.step);
112+
113+
// Integration tests - run the e2e script
114+
const integration_cmd = b.addSystemCommand(&[_][]const u8{ "bash", "src/e2e.sh", b.getInstallPath(.bin, "git-remote-sqlite") });
115+
integration_cmd.step.dependOn(b.getInstallStep()); // Ensure binary is built first
116+
steps.test_integration.dependOn(&integration_cmd.step);
117+
118+
// Main test step runs both unit and integration tests
119+
steps.@"test".dependOn(&run_unit.step);
120+
steps.@"test".dependOn(&integration_cmd.step);
121+
}

0 commit comments

Comments
 (0)