2 releases

0.1.1 Feb 25, 2019
0.1.0 Feb 21, 2019

#1679 in Development tools

MIT/Apache

265KB
4.5K SLoC

ojo is a minimal version control system (VCS) based on the same ideas as pijul, as described in the series of blog posts here. This is not a real VCS, and you should not use it for anything important. (For starters, it is only capable of tracking a single file.) I wrote ojo to help me understand the ideas discussed in the blog posts, and I'm making it public in the hope that maybe it will help someone else also.

Installation

ojo is a command line program. It has only been tested on Linux, although it will probably also work on similar operating systems. ojo is written in rust; to install it, you will need a rust toolchain installed. Once you've done that, clone this repository and build with cargo:

$ git clone https://github.com/jneem/ojo.git
$ cd ojo
$ cargo build --release

Then you can find the ojo binary in the target/release/ directory.

Usage

Creating a repository

To start your ojo journey, initialize a repository in the current directory:

$ ojo init
Created empty ojo repository.

This will create a .ojo directory in the current directory, containing the file db. This db file contains all of ojo's internal data. It's plain text (in YAML format), so you can look if you're curious.

Creating and applying patches

Each ojo repository is capable of tracking only one file, and the filename defaults to ojo_file.txt. To create a patch, use the command ojo patch create. For example, let's suppose that you've just created a repository, and then you edit the file ojo_file.txt. To create a patch reflecting your new changes: do

$ ojo patch create --author "My Name" --description "Something something"
Created patch rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=

That long string in the output is the unique identifier of the patch you just created. It was obtained by hashing the contents of the patch (including a timestamp, so you're unlikely to see the same hash twice even if you have exactly the same contents).

One idiosyncracy of ojo is that it doesn't (by default) apply the patches as soon as you create them, as opposed to (for example) git commit, which creates a patch and also applies it to the current branch. If you want to both create and apply a patch at the same time, provide the argument --then-apply to ojo patch create:

$ ojo patch create --author "My Name" --description "Something something" --then-apply
Created and applied patch rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=

Alternatively, you can first create the patch and then apply it with the ojo patch apply command:

$ ojo patch create --author "My Name" --description "Something something"
Created patch rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=
$ ojo patch apply rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=
Applied:
  vDLmQ2m8JnblI0wPq2bTSYusqNtHOLNo1iRt4nWdyLY=

If you want to unapply a patch, use ojo patch apply --revert:

$ ojo patch apply -R rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=
Unapplied:
  vDLmQ2m8JnblI0wPq2bTSYusqNtHOLNo1iRt4nWdyLY=

Outputting a file

Another of ojo's quirks is that it doesn't automatically update the working copy of your file to match changes in the internal repository. To output a file containing the repository's current contents, use the ojo render command. By default, the repository's contents will be outputted to the ojo_file.txt file, but you can change that.

$ ojo render # outputs the repository contents to ojo_file.txt
$ ojo render --path other_file.txt # specify another output path

Putting it together

$ ojo init
Created empty ojo repository.
$ echo "First line" > ojo_file.txt
$ ojo patch create --author Me --description "I wuz here" --then-apply
Created and applied patch rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=

# Now the file stored in the repository consists of the single line
# "First line". The file ojo_file.txt also consists of the single line
# "First line", but that's because we put it there ourselves; ojo hasn't
# touched it.

$ echo "Second line" >> ojo_file.txt
$ ojo patch create --author Me --description "Me again" --then-apply
Created and applied patch xGRnP1j1o9FdJPPJoD6OM4Pxj3qgyN2hKG_0qg54t38=

# Now the file stored in the repository has two lines, and so does the
# file ojo_file.txt.

$ ojo patch apply --reverse
Unapplied:
  xGRnP1j1o9FdJPPJoD6OM4Pxj3qgyN2hKG_0qg54t38=

# The file stored in the repository has just one line. To reflect that change
# in the filesystem, we need to render:
$ ojo render
Successfully wrote file 'ojo_file.txt'
$ cat ojo_file.txt
First line

Conflicts and resolution

The basic theory behind the way that ojo deals with conflicts is described in the series of blog posts here. The main idea is that instead of files, ojo stores "graggles", which are directed graphs of lines. A file is the special case of a graggle in which the directed graph of lines enforces a unique ordering. (More precisely, a graggle is a file if it has a unique topological sort.) Most of the time, you want your repository to represent a file; if it reprsents a graggle that isn't a file, we call it a conflict.

Here's a way to get a conflict:

$ ojo init
Created empty ojo repository.
$ echo "First line" > ojo_file.txt
$ ojo patch create --author Me --description "Starting out" --then-apply
Created and applied patch rLbZ6RjMol8_wV0tW2dnMapcaNVJB25A9uWFXixDU6c=
$ echo "Second line" >> ojo_file.txt
$ ojo patch create --author Me --description "Working hard"
Created patch xGRnP1j1o9FdJPPJoD6OM4Pxj3qgyN2hKG_0qg54t38=

# Notice that we haven't applied the second patch, so the file in the repository
# only has the first patch applied. Now let's edit the file on disk so that
# it consists of "First line" followed by "Alternate second line":

$ echo "First line" > ojo_file.txt
$ echo "Alternate second line" >> ojo_file.txt
$ ojo patch create --author Me --description "Working differently" --then-apply
Created and applied patch y-lgpjY30n5STzqtrMOEkvBM_WUWy0Yji91y9KTzptc=

# And finally, we apply the patch that added "Second line"
$ ojo patch apply xGRnP1j1o9FdJPPJoD6OM4Pxj3qgyN2hKG_0qg54t38=
Applied:
  xGRnP1j1o9FdJPPJoD6OM4Pxj3qgyN2hKG_0qg54t38=

Now, the effect of that long command listing was to create a graggle containing the line "First line" followed by either "Second line" or "Alternate second line", but with no prescribed order between the two possible second lines. In particular, the result isn't a file, because the lines it contains aren't in a linear order. If you try to render the file, it won't work:

$ ojo render
Error: Couldn't render a file, because the data isn't ordered

There are two important commands that you can use to resolve a conflict. The first is to inspect it, by rendering a graph:

$ ojo graph

This will create a "dot" file, which can be rendered using graphviz:

$ dot -o out.pdf -Tpdf out.dot

And now you can look at out.pdf to see a visualization of your graggle that isn't a file.

Once you understand what's going on, you can resolve your conflict using ojo's built-in interactive graggle resolver:

$ ojo resolve --author Me

This interactive utility will guide you through the process of turning the unordered graggle into a totally ordered file. In the example above, this amounts to deciding whether "Second line" should go before or after "Alternate second line". (At some point in the probably-distant future, I hope to create some comprehensive documentation for ojo resolve's user interface. But for now, hopefully it's reasonably explorable. Anyway, you can see all of the currently-active key bindings in the top right.)

When ojo resolve is done, it will produce a patch, which you can then apply to get rid of the conflict:

$ ojo resolve --author Me
# do the interactive thing...
Created patch SfxSwnA2POPHzL4eNNHku7t4Lyl5xW7Ge9pRXr5hV60=
$ ojo patch apply SfxSwnA2POPHzL4eNNHku7t4Lyl5xW7Ge9pRXr5hV60=
Applied:
  SfxSwnA2POPHzL4eNNHku7t4Lyl5xW7Ge9pRXr5hV60=
$ ojo render
Successfully wrote file 'ojo_file.txt'

Dependencies

~7–17MB
~199K SLoC