Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: Publish

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: systemic-engineering/ci/actions/nix-setup@main

- name: Download deps
run: nix develop -c gleam deps download

- name: Test
run: nix develop -c gleam test

- name: Publish to Hex.pm
run: nix develop -c gleam hex publish --yes
env:
HEXPM_API_KEY: ${{ secrets.HEX_API_KEY }}
1 change: 1 addition & 0 deletions gleam.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ repository = { type = "github", user = "systemic-engineering", repo = "fragmenta

[dependencies]
gleam_stdlib = ">= 0.44.0 and < 2.0.0"
simplifile = ">= 2.0.0 and < 3.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"
3 changes: 3 additions & 0 deletions manifest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
# You typically do not need to edit this file

packages = [
{ name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
{ name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" },
{ name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" },
{ name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" },
]

[requirements]
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
simplifile = { version = ">= 2.0.0 and < 3.0.0" }
28 changes: 28 additions & 0 deletions src/fragmentation/git.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/// git: content-addressed fragment persistence.
///
/// Writes fragments to disk named by their SHA.
/// The store is a directory. Each fragment becomes a file.
/// File name = SHA-256 of canonical serialization.
/// Idempotent: same SHA, same content, same file.
import fragmentation
import simplifile

// ---------------------------------------------------------------------------
// Operations
// ---------------------------------------------------------------------------

/// Write a fragment to disk under `dir`, named by its content-addressed SHA.
///
/// Computes the SHA via `fragmentation.hash_fragment`, serializes via
/// `fragmentation.serialize`, then writes to `<dir>/<sha>`.
/// Returns Ok(Nil) on success, Error(FileError) on failure.
/// Idempotent: writing the same fragment twice produces the same file.
pub fn write(
fragment: fragmentation.Fragment,
dir: String,
) -> Result(Nil, simplifile.FileError) {
let sha = fragmentation.hash_fragment(fragment)
let content = fragmentation.serialize(fragment)
let path = dir <> "/" <> sha
simplifile.write(path, content)
}
88 changes: 88 additions & 0 deletions test/fragmentation_git_test.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fragmentation
import fragmentation/git
import gleeunit/should
import simplifile

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn test_witnessed() -> fragmentation.Witnessed {
fragmentation.witnessed(
fragmentation.author("alex"),
fragmentation.committer("reed"),
fragmentation.timestamp("2026-03-01T00:00:00Z"),
fragmentation.message("test"),
)
}

fn make_shard(data: String) -> fragmentation.Fragment {
let r = fragmentation.ref(fragmentation.hash(data), "self")
fragmentation.shard(r, test_witnessed(), data)
}

// ---------------------------------------------------------------------------
// write_fragment_creates_file_test
//
// write_fragment writes a file named by SHA containing serialized fragment.
// ---------------------------------------------------------------------------

pub fn write_fragment_creates_file_test() {
let dir = "/tmp/fragmentation_git_test_write"
let _ = simplifile.create_directory(dir)
let frag = make_shard("hello-world")
let sha = fragmentation.hash_fragment(frag)

let result = git.write(frag, dir)
result |> should.be_ok()

let path = dir <> "/" <> sha
simplifile.is_file(path) |> should.equal(Ok(True))
}

// ---------------------------------------------------------------------------
// write_fragment_idempotent_test
//
// write_fragment is idempotent — same SHA, same content, no error on repeat.
// ---------------------------------------------------------------------------

pub fn write_fragment_idempotent_test() {
let dir = "/tmp/fragmentation_git_test_idempotent"
let _ = simplifile.create_directory(dir)
let frag = make_shard("idempotent-shard")
let sha = fragmentation.hash_fragment(frag)

let r1 = git.write(frag, dir)
let r2 = git.write(frag, dir)

r1 |> should.be_ok()
r2 |> should.be_ok()

// Only one file: idempotent means same path, same content
let path = dir <> "/" <> sha
simplifile.is_file(path) |> should.equal(Ok(True))
}

// ---------------------------------------------------------------------------
// write_two_fragments_test
//
// Writing two different fragments creates two files.
// ---------------------------------------------------------------------------

pub fn write_two_fragments_test() {
let dir = "/tmp/fragmentation_git_test_two"
let _ = simplifile.create_directory(dir)
let frag_a = make_shard("fragment-alpha")
let frag_b = make_shard("fragment-beta")
let sha_a = fragmentation.hash_fragment(frag_a)
let sha_b = fragmentation.hash_fragment(frag_b)

git.write(frag_a, dir) |> should.be_ok()
git.write(frag_b, dir) |> should.be_ok()

// SHAs must be distinct
sha_a |> should.not_equal(sha_b)

simplifile.is_file(dir <> "/" <> sha_a) |> should.equal(Ok(True))
simplifile.is_file(dir <> "/" <> sha_b) |> should.equal(Ok(True))
}
Loading