Repository: ioquatix/relaxo
Branch: main
Commit: 60808b666243
Files: 29
Total size: 33.7 KB
Directory structure:
gitextract_rc85mw7x/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── documentation-coverage.yaml
│ ├── documentation.yaml
│ ├── rubocop.yaml
│ ├── test-coverage.yaml
│ ├── test-external.yaml
│ └── test.yaml
├── .gitignore
├── .rubocop.yml
├── benchmarks/
│ └── performance.rb
├── config/
│ └── sus.rb
├── fixtures/
│ └── relaxo/
│ └── test_records.rb
├── gems.rb
├── lib/
│ ├── relaxo/
│ │ ├── changeset.rb
│ │ ├── database.rb
│ │ ├── dataset.rb
│ │ ├── directory.rb
│ │ ├── logger.rb
│ │ └── version.rb
│ └── relaxo.rb
├── license.md
├── readme.md
├── relaxo.gemspec
├── release.cert
└── test/
├── relaxo/
│ ├── changeset.rb
│ ├── concurrency.rb
│ ├── database.rb
│ └── enumeration.rb
└── relaxo.rb
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
[*]
indent_style = tab
indent_size = 2
[*.{yml,yaml}]
indent_style = space
indent_size = 2
================================================
FILE: .github/workflows/documentation-coverage.yaml
================================================
name: Documentation Coverage
on: [push, pull_request]
permissions:
contents: read
env:
CONSOLE_OUTPUT: XTerm
COVERAGE: PartialSummary
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Validate coverage
timeout-minutes: 5
run: bundle exec bake decode:index:coverage lib
================================================
FILE: .github/workflows/documentation.yaml
================================================
name: Documentation
on:
push:
branches:
- main
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages:
permissions:
contents: read
pages: write
id-token: write
# Allow one concurrent deployment:
concurrency:
group: "pages"
cancel-in-progress: true
env:
CONSOLE_OUTPUT: XTerm
BUNDLE_WITH: maintenance
jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- name: Installing packages
run: sudo apt-get install wget
- name: Generate documentation
timeout-minutes: 5
run: bundle exec bake utopia:project:static --force no
- name: Upload documentation artifact
uses: actions/upload-pages-artifact@v3
with:
path: docs
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{steps.deployment.outputs.page_url}}
needs: generate
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
================================================
FILE: .github/workflows/rubocop.yaml
================================================
name: RuboCop
on: [push, pull_request]
permissions:
contents: read
env:
CONSOLE_OUTPUT: XTerm
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ruby
bundler-cache: true
- name: Run RuboCop
timeout-minutes: 10
run: bundle exec rubocop
================================================
FILE: .github/workflows/test-coverage.yaml
================================================
name: Test Coverage
on: [push, pull_request]
permissions:
contents: read
env:
CONSOLE_OUTPUT: XTerm
COVERAGE: PartialSummary
jobs:
test:
name: ${{matrix.ruby}} on ${{matrix.os}}
runs-on: ${{matrix.os}}-latest
strategy:
matrix:
os:
- ubuntu
- macos
ruby:
- "3.4"
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.ruby}}
bundler-cache: true
- name: Run tests
timeout-minutes: 5
run: bundle exec bake test
- uses: actions/upload-artifact@v4
with:
include-hidden-files: true
if-no-files-found: error
name: coverage-${{matrix.os}}-${{matrix.ruby}}
path: .covered.db
validate:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.4"
bundler-cache: true
- uses: actions/download-artifact@v4
- name: Validate coverage
timeout-minutes: 5
run: bundle exec bake covered:validate --paths */.covered.db \;
================================================
FILE: .github/workflows/test-external.yaml
================================================
name: Test External
on: [push, pull_request]
permissions:
contents: read
env:
CONSOLE_OUTPUT: XTerm
jobs:
test:
name: ${{matrix.ruby}} on ${{matrix.os}}
runs-on: ${{matrix.os}}-latest
strategy:
matrix:
os:
- ubuntu
- macos
ruby:
- "3.1"
- "3.2"
- "3.3"
- "3.4"
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.ruby}}
bundler-cache: true
- name: Run tests
timeout-minutes: 10
run: bundle exec bake test:external
================================================
FILE: .github/workflows/test.yaml
================================================
name: Test
on: [push, pull_request]
permissions:
contents: read
env:
CONSOLE_OUTPUT: XTerm
jobs:
test:
name: ${{matrix.ruby}} on ${{matrix.os}}
runs-on: ${{matrix.os}}-latest
continue-on-error: ${{matrix.experimental}}
strategy:
matrix:
os:
- ubuntu
- macos
ruby:
- "3.1"
- "3.2"
- "3.3"
- "3.4"
experimental: [false]
include:
- os: ubuntu
ruby: truffleruby
experimental: true
- os: ubuntu
ruby: jruby
experimental: true
- os: ubuntu
ruby: head
experimental: true
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{matrix.ruby}}
bundler-cache: true
- name: Run tests
timeout-minutes: 10
run: bundle exec bake test
================================================
FILE: .gitignore
================================================
/.bundle/
/pkg/
/gems.locked
/.covered.db
/external
/tmp
================================================
FILE: .rubocop.yml
================================================
AllCops:
DisabledByDefault: true
Layout/IndentationStyle:
Enabled: true
EnforcedStyle: tabs
Layout/InitialIndentation:
Enabled: true
Layout/IndentationWidth:
Enabled: true
Width: 1
Layout/IndentationConsistency:
Enabled: true
EnforcedStyle: normal
Layout/BlockAlignment:
Enabled: true
Layout/EndAlignment:
Enabled: true
EnforcedStyleAlignWith: start_of_line
Layout/BeginEndAlignment:
Enabled: true
EnforcedStyleAlignWith: start_of_line
Layout/ElseAlignment:
Enabled: true
Layout/DefEndAlignment:
Enabled: true
Layout/CaseIndentation:
Enabled: true
Layout/CommentIndentation:
Enabled: true
Layout/EmptyLinesAroundClassBody:
Enabled: true
Layout/EmptyLinesAroundModuleBody:
Enabled: true
Style/FrozenStringLiteralComment:
Enabled: true
Style/StringLiterals:
Enabled: true
EnforcedStyle: double_quotes
================================================
FILE: benchmarks/performance.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "benchmark/ips" if ENV["BENCHMARK"]
require "ruby-prof" if ENV["PROFILE"]
require "flamegraph" if ENV["FLAMEGRAPH"]
describe "Relaxo Performance" do
let(:database_path) {File.join(__dir__, "test")}
let(:database) {Relaxo.connect(database_path)}
if defined? Benchmark
def benchmark(name = nil)
Benchmark.ips do |benchmark|
# Collect more data for benchmark:
benchmark.time = 20
benchmark.warmup = 10
benchmark.report(name) do |i|
yield i
end
benchmark.compare!
end
end
elsif defined? RubyProf
def benchmark(name)
result = RubyProf.profile do
yield 1000
end
#result.eliminate_methods!([/^((?!Utopia).)*$/])
printer = RubyProf::FlatPrinter.new(result)
printer.print($stderr, min_percent: 1.0)
printer = RubyProf::GraphHtmlPrinter.new(result)
filename = name.gsub("/", "_") + ".html"
File.open(filename, "w") do |file|
printer.print(file)
end
end
elsif defined? Flamegraph
def benchmark(name)
filename = name.gsub("/", "_") + ".html"
Flamegraph.generate(filename) do
yield 1
end
end
else
def benchmark(name)
yield 1
end
end
before(:each) do
FileUtils.rm_rf(database_path)
end
it "single transaction should be fast" do
benchmark("single") do |iterations|
database.commit(message: "Some Documents") do |dataset|
iterations.times do |i|
object = dataset.append("good-#{i}")
dataset.write("#{i%100}/#{i}", object)
end
end
end
end
it "multiple transactions should be fast" do
benchmark("multiple") do |iterations|
iterations.times do |i|
database.commit(message: "Some Documents") do |dataset|
object = dataset.append("good-#{i}")
dataset.write("#{i%100}/#{i}", object)
end
end
end
end
end
================================================
FILE: config/sus.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2024-2025, by Samuel Williams.
require "covered/sus"
include Covered::Sus
================================================
FILE: fixtures/relaxo/test_records.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "relaxo"
require "tmpdir"
module Relaxo
TemporaryDatabase = Sus::Shared("temporary database") do
def around
Dir.mktmpdir do |directory|
@root = directory
super
end
end
let(:database_path) {@root}
let(:database) {Relaxo.connect(database_path)}
end
TestRecords = Sus::Shared("test records") do
include_context Relaxo::TemporaryDatabase
let(:prefix) {"records"}
def before
super
database.commit(message: "Create Sample Data") do |dataset|
20.times do |i|
object = dataset.append("good-#{i}")
dataset.write("#{prefix}/#{i}", object)
end
10.times do |i|
object = dataset.append("bad-#{i}")
dataset.write("#{prefix}/subdirectory/#{i}", object)
end
end
end
end
end
================================================
FILE: gems.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
source "https://rubygems.org"
gemspec
group :maintenance, optional: true do
gem "bake-gem"
gem "bake-modernize"
gem "utopia-project"
end
group :test do
gem "sus"
gem "covered"
gem "decode"
gem "rubocop"
gem "bake-test"
gem "bake-test-external"
gem "msgpack"
end
================================================
FILE: lib/relaxo/changeset.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
require_relative "dataset"
module Relaxo
class Changeset < Dataset
def initialize(repository, tree)
super
@changes = {}
@directories = {}
end
attr :ref
attr :changes
def changes?
@changes.any?
end
def read(path)
if update = @changes[path]
if update[:action] != :remove
@repository.read(update[:oid])
end
else
super
end
end
def append(data, type = :blob)
oid = @repository.write(data, type)
return @repository.read(oid)
end
def write(path, object, mode = 0100644)
root, _, name = path.rpartition("/")
entry = @changes[path] = {
action: :upsert,
oid: object.oid,
object: object,
filemode: mode,
path: path,
root: root,
name: name,
}
fetch_directory(root).insert(entry)
return entry
end
alias []= write
def delete(path)
root, _, name = path.rpartition("/")
entry = @changes[path] = {
action: :remove,
path: path,
root: root,
name: name,
}
fetch_directory(root).delete(entry)
return entry
end
def abort!
throw :abort
end
def write_tree
@tree.update(@changes.values)
end
end
end
================================================
FILE: lib/relaxo/database.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
# Copyright, 2017, by Huba Nagy.
require "rugged"
require_relative "logger"
require_relative "dataset"
require_relative "changeset"
module Relaxo
HEAD = "HEAD".freeze
class Database
def initialize(path, branch, metadata = {})
@path = path
@metadata = metadata
@repository = Rugged::Repository.new(path)
# @repository.config['core.fsyncObjectFiles'] = fsync
@branch = branch
end
def config
@repository.config
end
attr :path
attr :metadata
attr :repository
# @attribute branch [String] The branch that this database is currently working with.
attr :branch
# Completely clear out the database.
def clear!
if head = @repository.branches[@branch]
@repository.references.delete(head)
end
end
def empty?
@repository.empty?
end
def head
@repository.branches[@branch]
end
def [] key
@metadata[key]
end
# During the execution of the block, changes don't get stored immediately, so reading from the dataset (from outside the block) will continue to return the values that were stored in the configuration when the transaction was started.
# @return the result of the block.
def commit(**options)
result = nil
track_time(options[:message]) do
catch(:abort) do
begin
parent, tree = latest_commit
changeset = Changeset.new(@repository, tree)
result = yield changeset
end until apply(parent, changeset, **options)
end
end
return result
end
# Efficient point-in-time read-only access.
def current
_, tree = latest_commit
dataset = Dataset.new(@repository, tree)
yield dataset if block_given?
return dataset
end
# revision history of given object
def history(path)
head, _ = latest_commit
walker = Rugged::Walker.new(@repository) # Sounds like 'Walker, Texas Ranger'...
walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
walker.push(head.oid)
commits = []
old_oid = nil
walker.each do |commit|
dataset = Dataset.new(@repository, commit.tree)
oid = dataset.read(path).oid
if oid != old_oid # modified
yield commit if block_given?
commits << commit
old_oid = oid
end
break if oid.nil? && !old_oid.nil? # deleted or moved
end
return commits
end
private
def track_time(message)
start_time = Time.now
yield
ensure
end_time = Time.now
elapsed_time = end_time - start_time
Console.debug(self) {"#{message.inspect}: %0.3fs" % elapsed_time}
end
def apply(parent, changeset, **options)
return true unless changeset.changes?
options[:tree] = changeset.write_tree
options[:parents] ||= [parent]
options[:update_ref] ||= "refs/heads/#{@branch}"
begin
Rugged::Commit.create(@repository, options)
rescue Rugged::ObjectError
return false
end
end
def latest_commit
if head = self.head
return head.target, head.target.tree
else
return nil, empty_tree
end
end
def empty_tree
@empty_tree ||= Rugged::Tree.empty(@repository)
end
end
end
================================================
FILE: lib/relaxo/dataset.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
require "rugged"
require_relative "directory"
module Relaxo
class Dataset
def initialize(repository, tree)
@repository = repository
@tree = tree
@directories = {}
end
def read(path)
if entry = @tree.path(path) and entry[:type] == :blob and oid = entry[:oid]
@repository.read(oid)
end
rescue Rugged::TreeError
return nil
end
alias [] read
def file?
read(path)
end
def exist?(path)
read(path) or directory?(path)
end
def directory?(path)
@directories.key?(path) or @tree.path(path)[:type] == :tree
rescue Rugged::TreeError
return false
end
def each(path = "", &block)
return to_enum(:each, path) unless block_given?
directory = fetch_directory(path)
directory.each(&block)
end
protected
def fetch_directory(path)
@directories[path] ||= Directory.new(@repository, @tree, path)
end
end
end
================================================
FILE: lib/relaxo/directory.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "rugged"
module Relaxo
class Directory
def initialize(repository, root_tree, path)
@repository = repository
# The root tree, which path is relative to:
@root_tree = root_tree
# The entry and tree for the directory itself:
@entry = nil
@tree = nil
@path = path
@entries = nil
@changes = {}
end
def freeze
@changes.freeze
super
end
def entries
@entries ||= load_entries!
end
def each(&block)
return to_enum(:each) unless block_given?
entries.each do |entry|
entry[:object] ||= @repository.read(entry[:oid])
yield entry[:name], entry[:object]
end
end
def each_entry(&block)
return to_enum(:each_entry) unless block_given?
entries.each(&block)
end
def insert(entry)
_, _, name = entry[:name].rpartition("/")
@changes[name] = entry
# Blow away the cache:
@entries = nil
end
def delete(entry)
_, _, name = entry[:name].rpartition("/")
@changes[name] = nil
# Blow away the cache:
@entries = nil
end
private
# Look up the entry for the given directory `@path`:
def fetch_entry
@entry ||= @root_tree.path(@path)
end
# Load the directory tree for the given `@path`:
def fetch_tree
@tree ||= Rugged::Tree.new(@repository, fetch_entry[:oid])
rescue Rugged::TreeError
return nil
end
# Load the entries from the tree, applying any changes.
def load_entries!
entries = @changes.dup
if tree = fetch_tree
tree.each_blob do |entry|
unless entries.key? entry[:name]
entries[entry[:name]] = entry
end
end
end
return entries.values.compact.sort_by{|entry| entry[:name]}
end
end
end
================================================
FILE: lib/relaxo/logger.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2019-2025, by Samuel Williams.
require "console"
module Relaxo
extend Console
end
================================================
FILE: lib/relaxo/version.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
module Relaxo
VERSION = "1.8.0"
end
================================================
FILE: lib/relaxo.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
require "relaxo/database"
require "etc"
require "socket"
module Relaxo
DEFAULT_BRANCH = "main".freeze
def self.connect(path, branch: nil, sync: nil, create: true, **metadata)
if !File.exist?(path) || create
repository = Rugged::Repository.init_at(path, true)
if branch
repository.head = "refs/heads/#{branch}"
end
if sync || ENV["RELAXO_SYNC"]
repository.config["core.fsyncObjectFiles"] = true
end
else
repository = Rugged::Repository.new(path)
end
# Automatically detect the current branch if `branch` is not provided:
branch ||= self.default_branch(repository)
database = Database.new(path, branch, metadata)
if config = database.config
unless config["user.name"]
login = Etc.getpwuid
hostname = Socket.gethostname
if login
config["user.name"] = login.name
config["user.email"] = "#{login.name}@#{hostname}"
end
end
end
return database
end
private
# Detect the default branch of the repository, taking into account unborn branches.
def self.default_branch(repository)
if head = repository.references["HEAD"]
if target_id = head.target_id
return target_id.sub(/^refs\/heads\//, "")
end
end
return DEFAULT_BRANCH
end
end
================================================
FILE: license.md
================================================
# MIT License
Copyright, 2012-2025, by Samuel Williams.
Copyright, 2017-2018, by Huba Nagy.
Copyright, 2020, by Olle Jonsson.
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: readme.md
================================================
# 
Relaxo is a transactional database built on top of git. It's aim is to provide a robust interface for document storage and sorted indexes. If you prefer a higher level interface, you can try [relaxo-model](https://github.com/ioquatix/relaxo-model).
[](https://github.com/ioquatix/relaxo/actions?workflow=Test)
## Installation
Add this line to your application's Gemfile:
``` ruby
gem 'relaxo'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install relaxo
## Usage
Connect to a local database and manipulate some documents.
``` ruby
require 'relaxo'
require 'msgpack'
DB = Relaxo.connect("test")
DB.commit(message: "Create test data") do |dataset|
object = dataset.append(MessagePack.dump({bob: 'dole'}))
dataset.write("doc1.msgpack", object)
end
DB.commit(message: "Update test data") do |dataset|
doc = MessagePack.load dataset.read('doc1.msgpack').data
doc[:foo] = 'bar'
object = dataset.append(MessagePack.dump(doc))
dataset.write("doc2.msgpack", object)
end
doc = MessagePack.load DB.current['doc2.msgpack'].data
puts doc
# => {"bob"=>"dole", "foo"=>"bar"}
```
### Document Storage
Relaxo uses the git persistent data structure for storing documents. This data structure exposes a file-system like interface, which stores any kind of data. This means that you are free to use JSON, or BSON, or MessagePack, or JPEG, or XML, or any combination of those.
Relaxo has a transactional model for both reading and writing.
#### Authors
By default, Relaxo sets up the repository author using the login name and hostname of the current session. You can explicitly change this by modifying `database.config`. Additionally, you can set this per-commit:
``` ruby
database.commit(message: "Testing Enumeration", author: {user: "Alice", email: "alice@localhost"}) do |dataset|
object = dataset.append("Hello World!")
dataset.write("hello.txt", object)
end
```
#### Reading Files
``` ruby
path = "path/to/document"
DB.current do |dataset|
object = dataset.read(path)
puts "The object id: #{object.oid}"
puts "The object data size: #{object.size}"
puts "The object data: #{object.data.inspect}"
end
```
#### Writing Files
``` ruby
path = "path/to/document"
data = MessagePack.dump(document)
DB.commit(message: "Adding document") do |changeset|
object = changeset.append(data)
changeset.write(path, object)
end
```
### Datasets and Transactions
`Dataset`s and `Changeset`s are important concepts. Relaxo doesn't allow arbitrary access to data, but instead exposes the git persistent model for both reading and writing. The implications of this are that when reading or writing, you always see a consistent snapshot of the data store.
### Suitability
Relaxo is designed to scale to the hundreds of thousands of documents. It's designed around the git persistent data store, and therefore has some performance and concurrency limitations due to the underlying implementation.
Because it maintains a full history of all changes, the repository would continue to grow over time by default, but there are mechanisms to deal with that.
#### Performance
Relaxo can do anywhere from 1000-10,000 inserts per second depending on how you structure the workload.
Relaxo Performance
Warming up --------------------------------------
single 129.000 i/100ms
Calculating -------------------------------------
single 6.224k (±14.7%) i/s - 114.036k in 20.000025s
single transaction should be fast
Warming up --------------------------------------
multiple 152.000 i/100ms
Calculating -------------------------------------
multiple 1.452k (±15.2%) i/s - 28.120k in 20.101831s
multiple transactions should be fast
Reading data is lighting fast as it's loaded directly from disk and cached.
### Loading Data
As Relaxo is unapologetically based on git, you can use git directly with a non-bare working directory to add any files you like. You can even point Relaxo at an existing git repository.
### Durability
Relaxo is based on `libgit2` and asserts that it is a transactional database. We base this assertion on:
- All writes into the object store using `libgit2` are atomic and synchronized to disk.
- All updates to refs are atomic and synchronized to disk.
Provided these two invariants are maintained, the operation of Relaxo will be safe, even if there are unexpected interruptions to the program.
The durability guarantees of Relaxo depend on [`libgit2` calling `fsync`](https://github.com/libgit2/libgit2/pull/4030), and [this being respected by the underlying hardware](http://www.evanjones.ca/intel-ssd-durability.html). Otherwise, durability cannot be guaranteed.
## Contributing
We welcome contributions to this project.
1. Fork it.
2. Create your feature branch (`git checkout -b my-new-feature`).
3. Commit your changes (`git commit -am 'Add some feature'`).
4. Push to the branch (`git push origin my-new-feature`).
5. Create new Pull Request.
### Developer Certificate of Origin
In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
### Community Guidelines
This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
================================================
FILE: relaxo.gemspec
================================================
# frozen_string_literal: true
require_relative "lib/relaxo/version"
Gem::Specification.new do |spec|
spec.name = "relaxo"
spec.version = Relaxo::VERSION
spec.summary = "Relaxo is versioned document database built on top of git."
spec.authors = ["Samuel Williams", "Huba Nagy", "Olle Jonsson"]
spec.license = "MIT"
spec.cert_chain = ["release.cert"]
spec.signing_key = File.expand_path("~/.gem/release.pem")
spec.homepage = "https://github.com/ioquatix/relaxo"
spec.metadata = {
"funding_uri" => "https://github.com/sponsors/ioquatix/",
"source_code_uri" => "https://github.com/ioquatix/relaxo.git",
}
spec.files = Dir.glob(["{lib}/**/*", "*.md"], File::FNM_DOTMATCH, base: __dir__)
spec.required_ruby_version = ">= 3.1"
spec.add_dependency "console"
spec.add_dependency "rugged"
end
================================================
FILE: release.cert
================================================
-----BEGIN CERTIFICATE-----
MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
-----END CERTIFICATE-----
================================================
FILE: test/relaxo/changeset.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "relaxo/test_records"
describe Relaxo::Changeset do
include_context Relaxo::TestRecords
it "should enumerate all documents including writes" do
records = []
database.commit(message: "Testing Enumeration") do |dataset|
5.times do |i|
object = dataset.append("extra-#{i}")
dataset.write("#{prefix}/extra-#{i}", object)
end
expect(dataset.exist?("#{prefix}/extra-0")).to be_truthy
records = dataset.each(prefix).to_a
end
expect(records.count).to be == 25
end
it "should enumerate all documents excluding deletes" do
records = database.commit(message: "Testing Enumeration") do |dataset|
5.times do |i|
dataset.delete("#{prefix}/#{i}")
end
expect(dataset.exist?("#{prefix}/0")).to be_falsey
dataset.each(prefix).to_a
end
expect(records.count).to be == 15
end
let(:author) do
{name: "Testing McTestface", email: "testing@testing.com"}
end
it "can use specified author" do
database.commit(message: "Testing Enumeration", author: author) do |dataset|
object = dataset.append("Hello World!")
dataset.write("hello.txt", object)
end
commit = database.head.target
expect(commit.author).to have_keys(
name: be == "Testing McTestface",
email: be == "testing@testing.com",
)
end
end
================================================
FILE: test/relaxo/concurrency.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "relaxo/test_records"
describe Relaxo::Changeset do
include_context Relaxo::TestRecords
it "should detect conflicts" do
events = []
alice = Fiber.new do
database.commit(message: "Alice Data") do |changeset|
events << :alice
object = changeset.append("sample-data-1")
changeset.write("conflict-path", object)
Fiber.yield
end
end
bob = Fiber.new do
database.commit(message: "Bob Data") do |changeset|
events << :bob
object = changeset.append("sample-data-1")
changeset.write("conflict-path", object)
Fiber.yield
end
end
alice.resume
bob.resume
alice.resume
bob.resume
expect(events).to be == [:alice, :bob, :bob]
end
end
================================================
FILE: test/relaxo/database.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2012-2025, by Samuel Williams.
# Copyright, 2017, by Huba Nagy.
require "relaxo"
require "relaxo/test_records"
describe Relaxo::Database do
include_context Relaxo::TemporaryDatabase
let(:document_path) {"test/document.json"}
let(:sample_json) {"[1, 2, 3]"}
it "should be initially empty" do
expect(database).to be(:empty?)
end
it "prepares user details in config" do
expect(database.config.to_hash).to have_keys(
"user.name", "user.email"
)
end
it "can clear database" do
database.clear!
expect(database).to be(:empty?)
end
it "should not be empty with one document" do
database.commit(message: "Create test document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path, oid)
end
expect(database).not.to be(:empty?)
end
it "should be able to clear the database" do
database.commit(message: "Create test document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path, oid)
end
expect(database).not.to be(:empty?)
database.clear!
expect(database).to be(:empty?)
end
it "should have metadata" do
expect(database.metadata).to be == {}
end
it "should create a document" do
database.commit(message: "Create test document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path, oid)
end
database.current do |dataset|
expect(dataset[document_path].data).to be == sample_json
end
end
it "should erase a document" do
database.commit(message: "Create test document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path, oid)
end
database.commit(message: "Delete test document") do |dataset|
dataset.delete(document_path)
end
database.current do |dataset|
expect(dataset[document_path]).to be_nil
end
end
it "should create multiple documents" do
database.commit(message: "Create first document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path, oid)
end
database.commit(message: "Create second document") do |dataset|
oid = dataset.append(sample_json)
dataset.write(document_path + "2", oid)
end
database.current do |dataset|
expect(dataset[document_path].data).to be == sample_json
expect(dataset[document_path + "2"].data).to be == sample_json
end
end
it "can enumerate documents" do
database.commit(message: "Create first document") do |dataset|
oid = dataset.append(sample_json)
10.times do |id|
dataset.write(document_path + "-#{id}", oid)
end
end
database.current do |dataset|
expect(dataset.each("test").count).to be == 10
end
end
it "can enumerate commit history of a document" do
10.times do |id|
database.commit(message: "revising the document #{id}") do |changeset|
oid = changeset.append("revision \##{id} of this document")
changeset.write("test/doot.txt", oid)
end
end
database.commit(message: "unrelated commit") do |changeset|
oid = changeset.append("unrelated document")
changeset.write("test/unrelated.txt", oid)
end
expect(database.history("test/doot.txt").count).to be == 10
end
end
================================================
FILE: test/relaxo/enumeration.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2017-2025, by Samuel Williams.
require "relaxo/test_records"
describe Relaxo::Dataset do
include_context Relaxo::TestRecords
it "should enumerate all documents" do
records = []
database.current do |dataset|
records = dataset.each(prefix).to_a
end
expect(records.count).to be == 20
end
end
describe Relaxo::Changeset do
include_context Relaxo::TestRecords
it "should enumerate all documents" do
records = []
database.commit(message: "Testing Enumeration") do |dataset|
records = dataset.each(prefix).to_a
end
expect(records.count).to be == 20
end
end
================================================
FILE: test/relaxo.rb
================================================
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2025, by Samuel Williams.
require "relaxo/test_records"
describe Relaxo do
with ".connect" do
include Relaxo::TemporaryDatabase
it "can connect to a new database" do
expect(database).to be_a Relaxo::Database
expect(database.branch).to (be == "main").or(be == "master")
end
it "can connect to a new database with an alternative branch name" do
Relaxo.connect(database_path, branch: "development")
expect(database).to be_a Relaxo::Database
expect(database.branch).to be == "development"
end
end
end
gitextract_rc85mw7x/
├── .editorconfig
├── .github/
│ └── workflows/
│ ├── documentation-coverage.yaml
│ ├── documentation.yaml
│ ├── rubocop.yaml
│ ├── test-coverage.yaml
│ ├── test-external.yaml
│ └── test.yaml
├── .gitignore
├── .rubocop.yml
├── benchmarks/
│ └── performance.rb
├── config/
│ └── sus.rb
├── fixtures/
│ └── relaxo/
│ └── test_records.rb
├── gems.rb
├── lib/
│ ├── relaxo/
│ │ ├── changeset.rb
│ │ ├── database.rb
│ │ ├── dataset.rb
│ │ ├── directory.rb
│ │ ├── logger.rb
│ │ └── version.rb
│ └── relaxo.rb
├── license.md
├── readme.md
├── relaxo.gemspec
├── release.cert
└── test/
├── relaxo/
│ ├── changeset.rb
│ ├── concurrency.rb
│ ├── database.rb
│ └── enumeration.rb
└── relaxo.rb
SYMBOL INDEX (58 symbols across 9 files)
FILE: benchmarks/performance.rb
function benchmark (line 15) | def benchmark(name = nil)
function benchmark (line 29) | def benchmark(name)
function benchmark (line 45) | def benchmark(name)
function benchmark (line 52) | def benchmark(name)
FILE: fixtures/relaxo/test_records.rb
type Relaxo (line 9) | module Relaxo
function around (line 11) | def around
function before (line 27) | def before
FILE: lib/relaxo.rb
type Relaxo (line 11) | module Relaxo
function connect (line 14) | def self.connect(path, branch: nil, sync: nil, create: true, **metadata)
function default_branch (line 52) | def self.default_branch(repository)
FILE: lib/relaxo/changeset.rb
type Relaxo (line 8) | module Relaxo
class Changeset (line 9) | class Changeset < Dataset
method initialize (line 10) | def initialize(repository, tree)
method changes? (line 20) | def changes?
method read (line 24) | def read(path)
method append (line 34) | def append(data, type = :blob)
method write (line 40) | def write(path, object, mode = 0100644)
method delete (line 60) | def delete(path)
method abort! (line 75) | def abort!
method write_tree (line 79) | def write_tree
FILE: lib/relaxo/database.rb
type Relaxo (line 13) | module Relaxo
class Database (line 16) | class Database
method initialize (line 17) | def initialize(path, branch, metadata = {})
method config (line 27) | def config
method clear! (line 39) | def clear!
method empty? (line 45) | def empty?
method head (line 49) | def head
method [] (line 53) | def [] key
method commit (line 59) | def commit(**options)
method current (line 78) | def current
method history (line 89) | def history(path)
method track_time (line 118) | def track_time(message)
method apply (line 129) | def apply(parent, changeset, **options)
method latest_commit (line 143) | def latest_commit
method empty_tree (line 151) | def empty_tree
FILE: lib/relaxo/dataset.rb
type Relaxo (line 10) | module Relaxo
class Dataset (line 11) | class Dataset
method initialize (line 12) | def initialize(repository, tree)
method read (line 19) | def read(path)
method file? (line 29) | def file?
method exist? (line 33) | def exist?(path)
method directory? (line 37) | def directory?(path)
method each (line 43) | def each(path = "", &block)
method fetch_directory (line 53) | def fetch_directory(path)
FILE: lib/relaxo/directory.rb
type Relaxo (line 8) | module Relaxo
class Directory (line 9) | class Directory
method initialize (line 10) | def initialize(repository, root_tree, path)
method freeze (line 26) | def freeze
method entries (line 32) | def entries
method each (line 36) | def each(&block)
method each_entry (line 46) | def each_entry(&block)
method insert (line 52) | def insert(entry)
method delete (line 61) | def delete(entry)
method fetch_entry (line 73) | def fetch_entry
method fetch_tree (line 78) | def fetch_tree
method load_entries! (line 85) | def load_entries!
FILE: lib/relaxo/logger.rb
type Relaxo (line 8) | module Relaxo
FILE: lib/relaxo/version.rb
type Relaxo (line 6) | module Relaxo
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (40K chars).
[
{
"path": ".editorconfig",
"chars": 105,
"preview": "root = true\n\n[*]\nindent_style = tab\nindent_size = 2\n\n[*.{yml,yaml}]\nindent_style = space\nindent_size = 2\n"
},
{
"path": ".github/workflows/documentation-coverage.yaml",
"chars": 450,
"preview": "name: Documentation Coverage\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n CO"
},
{
"path": ".github/workflows/documentation.yaml",
"chars": 1138,
"preview": "name: Documentation\n\non:\n push:\n branches:\n - main\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment "
},
{
"path": ".github/workflows/rubocop.yaml",
"chars": 376,
"preview": "name: RuboCop\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n\njobs:\n check:\n "
},
{
"path": ".github/workflows/test-coverage.yaml",
"chars": 1181,
"preview": "name: Test Coverage\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n COVERAGE: P"
},
{
"path": ".github/workflows/test-external.yaml",
"chars": 632,
"preview": "name: Test External\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n\njobs:\n test"
},
{
"path": ".github/workflows/test.yaml",
"chars": 966,
"preview": "name: Test\n\non: [push, pull_request]\n\npermissions:\n contents: read\n\nenv:\n CONSOLE_OUTPUT: XTerm\n\njobs:\n test:\n nam"
},
{
"path": ".gitignore",
"chars": 58,
"preview": "/.bundle/\n/pkg/\n/gems.locked\n/.covered.db\n/external\n\n/tmp\n"
},
{
"path": ".rubocop.yml",
"chars": 858,
"preview": "AllCops:\n DisabledByDefault: true\n\nLayout/IndentationStyle:\n Enabled: true\n EnforcedStyle: tabs\n\nLayout/InitialIndent"
},
{
"path": "benchmarks/performance.rb",
"chars": 1899,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"b"
},
{
"path": "config/sus.rb",
"chars": 153,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2024-2025, by Samuel Williams.\n\nrequire \"c"
},
{
"path": "fixtures/relaxo/test_records.rb",
"chars": 878,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "gems.rb",
"chars": 391,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n\nsource \"ht"
},
{
"path": "lib/relaxo/changeset.rb",
"chars": 1308,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n\nrequire_re"
},
{
"path": "lib/relaxo/database.rb",
"chars": 3254,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "lib/relaxo/dataset.rb",
"chars": 1018,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "lib/relaxo/directory.rb",
"chars": 1847,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "lib/relaxo/logger.rb",
"chars": 163,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2019-2025, by Samuel Williams.\n\nrequire \"c"
},
{
"path": "lib/relaxo/version.rb",
"chars": 147,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n\nmodule Rel"
},
{
"path": "lib/relaxo.rb",
"chars": 1366,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "license.md",
"chars": 1157,
"preview": "# MIT License\n\nCopyright, 2012-2025, by Samuel Williams. \nCopyright, 2017-2018, by Huba Nagy. \nCopyright, 2020, by Oll"
},
{
"path": "readme.md",
"chars": 5805,
"preview": "# \n\nRelaxo is a transactional database built on top of git. It's aim is to provide a robust interface"
},
{
"path": "relaxo.gemspec",
"chars": 818,
"preview": "# frozen_string_literal: true\n\nrequire_relative \"lib/relaxo/version\"\n\nGem::Specification.new do |spec|\n\tspec.name = \"rel"
},
{
"path": "release.cert",
"chars": 1740,
"preview": "-----BEGIN CERTIFICATE-----\nMIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11\nZWwud2lsbGlhbXMxHTAbBgoJkia"
},
{
"path": "test/relaxo/changeset.rb",
"chars": 1407,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "test/relaxo/concurrency.rb",
"chars": 839,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "test/relaxo/database.rb",
"chars": 3247,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2012-2025, by Samuel Williams.\n# Copyright"
},
{
"path": "test/relaxo/enumeration.rb",
"chars": 675,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2017-2025, by Samuel Williams.\n\nrequire \"r"
},
{
"path": "test/relaxo.rb",
"chars": 614,
"preview": "# frozen_string_literal: true\n\n# Released under the MIT License.\n# Copyright, 2025, by Samuel Williams.\n\nrequire \"relaxo"
}
]
About this extraction
This page contains the full source code of the ioquatix/relaxo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (33.7 KB), approximately 10.4k tokens, and a symbol index with 58 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.