Trying out Pijul

✍️ Written on 2022-07-26 in 2226 words. Part of cs software-development

Update 2022-07-28: pneumier informed me that pijul is not line-based. I fixed this.

Motivation

My first version control system was mercurial popular in the python community (at around 2008). Github became a big player soon (at around 2010) and I switched to git. I like git since it solves many problems neatly. However, the CLI is not intuitive. Fortunately they started to refactor the CLI with new subcommands like `restore. But in the end, you cannot ask non-technical people to use it. I don’t think Pijul solves this gap, but let us take a look. Pijul praises itself with a different three-way merge algorithm.

Getting started

  • You need to compile it yourself. Thus you need a rust toolchain. This is a caveat and I think pre-built binaries would be nice. I now assume you have the pijul executable available on your PATH.

  • cargo tree for libpijul shows 262 lines. Dependencies include chrono for timestamps, ed25519-dalek and others for cryptographic computations, diff for text difference computation, tokio for concurrency, ignore to consider .gitignore files, encoding_rs to handle text encodings, toml for TOML parsing, path-slash to support forward/backward slashes in paths, sanakirja (blogpost) for COW data structures, and so many more …

  • pijul uses three vocabularies:

    working copy

    the filesystem state, you modify

    changes

    difference between two states (changes between working copy and pristine can be listed with pijul diff)

    tree

    a set of files and folder tracked by Pijul (can be listed with pijul ls)

    pristine

    representation of the current recorded version of the repository

  • A platform to store Pijul repositories and shows them in a web interface is provided at nest.pijul.com.

  • Pijul repositories can be cloned via SSH or locally from folder to folder.

  • The negative connotation of git blame was renamed to pijul credit

The colorful output of ``pijul --help``

Generate keys

pijul identifies authors by their cryptographic key and not the (name, email address) tuple like git. I think their argument makes sense: if the identity is bound to the key, authors can change their name or email address later. The personal configuration on Linux is stored in ~/.config/pijul/config.toml. Key generation is required before any recording.

$ pijul key generate "Lukas Prokop"

Password for the new key (press enter to leave it unencrypted):
Wrote secret key in "/home/meisterluk/.config/pijul/secretkey.json"

$ head -n 5 /home/meisterluk/.config/pijul/secretkey.json
{
  "version": 0,
  "algorithm": "Ed25519",
  "encryption": {
    "Aes128": {

$ cat .config/pijul/config.toml

[author]
name = "meisterluk"
full_name = "Lukas Prokop"
email = "my@personal.mail"

Initialize project

$ pijul init

$ ls -la

totalo 4
drwxrwxr-x 1 meisterluk meisterluk 26 Jul 26 12:56 .
drwxrwxr-x 1 meisterluk meisterluk 26 Jul 26 12:56 ..
-rw-rw-r-- 1 meisterluk meisterluk 15 Jul 26 12:56 .ignore
drwxrwxr-x 1 meisterluk meisterluk 42 Jul 26 12:56 .pijul

$ find .

.
./.pijul
./.pijul/pristine
./.pijul/pristine/db
./.pijul/pristine/db.lock0
./.pijul/pristine/db.lock1
./.pijul/config
./.pijul/changes
./.ignore

$ cat .pijul/config

[hooks]
record = []

$ cat .pijul/.ignore

.git
.DS_Store

So far, we have some empty pristine.

Record a change

Now let us add a default rust project to this folder and then let us create a patch with files Cargo.toml and src/main.rs here:

$ pijul add Cargo.toml src/lib.rs

$ pijul record
Password for "/home/meisterluk/.config/pijul/secretkey.json":
[opens the following file in $EDITOR]
message = ''
timestamp = '2022-07-26T11:13:48.386870872Z'

[[authors]]
key = 'B1dSX4re8eF23y4jQaS3kRCc13eReajei91iX2XfawAj'

# Dependencies
[2] WEWAKG4JWR7WVHJIF6DLS57R7PLGFB5GDEITAZ724NLOZF5WXNBQC

# Hunks

1. File addition: "src" in "" +dx "binary"
  up 2.1, new 2:16

2. File addition: "main.rs" in "src" "UTF-8"
  up 0.1, new 65:96
+ fn main() {
+     println!("Hello, world!");
+ }

3. File addition: "Cargo.toml" in "" "UTF-8"
  up 2.1, new 277:311
+ [package]
+ name = "testproj"
+ version = "0.1.0"
+ edition = "2021"
+
+ # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+ [dependencies]

I just filled in the message, saved and closed the file to complete the record. And this is an actually interesting part: Whereas the content between # Dependencies and # Hunks will be ignored, the changes below # Hunks can be changed/discarded in-place. This makes it much easier than in git to split a larger commit into smaller ones.

Since this is now recorded, we can also see this change in the log:

$ pijul log

Change 5573KACUX7A563NWFVYHGZEU3LZ4IGFDUUQIMR7YK4LAFJ6EUXIQC
Author: Lukas Prokop
Date: 2022-07-26 11:13:48.386870872 UTC

    Initialize rust project

Change WEWAKG4JWR7WVHJIF6DLS57R7PLGFB5GDEITAZ724NLOZF5WXNBQC
Author:
Date: 2022-07-26 11:13:52.490426426 UTC

Storing another change is so easy that I won’t show it: just edit the file and call pijul record.

Channels, not branches

However, channels are different from Git branches, and do not serve the same purpose. In Pijul, independent changes commute, which means that in many cases where branches are used in Git, there is no need to create a channel in Pijul.

The main differences with Git branches are:

  • The identity of a change doesn’t depend on the branch it is on, or in other words, rebase and merge are the same operation in Pijul.

  • This implies that conflicts do not mysteriously come back after you solve them (which is what git rerere is for).

  • Also, conflicts are between changes, so the resolution of a conflict on one channel solves the same conflict in all other channels.

Create channels with pijul fork <name> and list them with pijul channel. You can switch to another channel with pijul channel switch <channel>. If you want to remove a change, you can do so with pijul unrecord, but only if the change is on the current channel. And pijul apply <change> applies all changes until <change> onto the current channel. The documentation comment “all changes this change depends upon will be applied as well” shows that it does not work like git cherry-pick.

One of the main features of Pijul is that its internal representation of repositories fully models conflicts. Patches can even be applied to a conflicting repository, leaving the conflict resolution for later.

In order to create a conflict, I modified src/main.rs in the same line on channel main and channel feature. Then I ran pijul apply <latest-change-from-feature-channel> in the main channel.

$ pijul apply U5B4QNCAARV2B4AWKC7JFTVXPM45Y66F5D4NXSPG4IUM2DBKHC3AC

Outputting repository ↖

There were conflicts:

  - Order conflict in "src/main.rs" starting on line 2
  - Order conflict in "src/main.rs" starting on line 2

Inside src/main.rs, we can see the conflict:

fn main() {
>>>>>>> 1 [CGJNA7DH]
    println!("Change on main channel");
======= 1 [U5B4QNCA]
    println!("Text from feature branch");
<<<<<<< 1
}

Indeed. Even though the conflict is not resolved yet, we can see that the change was already applied:

$ pijul log

Change U5B4QNCAARV2B4AWKC7JFTVXPM45Y66F5D4NXSPG4IUM2DBKHC3AC
Author: Lukas Prokop
Date: 2022-07-26 18:07:02.414063222 UTC

    Change on feature branch

Change CGJNA7DH2UI6P66JPG4LR57TGGO3NIPYNMAA3CQCYI26GM47LHBQC
Author: Lukas Prokop
Date: 2022-07-26 18:07:54.529764850 UTC

    Change on main branch

Change O4AH6HKI37MS5ZSMVPJUYXKDW4TRFS2VPPZPT5FPCY34MYW7IGMAC
Author: Lukas Prokop
Date: 2022-07-26 16:57:17.629465509 UTC

    My second record

Change 5573KACUX7A563NWFVYHGZEU3LZ4IGFDUUQIMR7YK4LAFJ6EUXIQC
Author: Lukas Prokop
Date: 2022-07-26 11:13:48.386870872 UTC

    Initialize rust project

Change WEWAKG4JWR7WVHJIF6DLS57R7PLGFB5GDEITAZ724NLOZF5WXNBQC
Author:
Date: 2022-07-26 11:13:52.490426426 UTC

As you can see, the change has the same identifier on the feature channel as well as the main channel.

message = 'Conflict resolved'
timestamp = '2022-07-26T18:14:17.403138390Z'

[[authors]]
key = 'B1dSX4re8eF23y4jQaS3kRCc13eReajei91iX2XfawAj'

# Dependencies
[2] CGJNA7DH2UI6P66JPG4LR57TGGO3NIPYNMAA3CQCYI26GM47LHBQC
[3] U5B4QNCAARV2B4AWKC7JFTVXPM45Y66F5D4NXSPG4IUM2DBKHC3AC
[4]+5573KACUX7A563NWFVYHGZEU3LZ4IGFDUUQIMR7YK4LAFJ6EUXIQC
[*] 5573KACUX7A563NWFVYHGZEU3LZ4IGFDUUQIMR7YK4LAFJ6EUXIQC

# Hunks

1. Replacement in "src/main.rs":2 4.49 "UTF-8"
B:BD 4.62 -> 2.0:40/2, B:BD 4.62 -> 3.0:42/3
  up 4.62, new 2:26, down 4.93
-     println!("Change on main channel");
-     println!("Text from feature branch");
+     println!("Merged");

The # Dependencies section seems to convey that different channels have been merged. [2] and [3] are the latest changes on the channels whereas [4] and [*] are the same and seem to indicate a merge (not sure how to read it).

Conclusion

  • I like that you can “select” the changes in the pijul record file.

  • I like that the distinction between rebasing and merging does not exist. This distinction means that “we are using git” can mean different things in two companies.

  • I like that identities are bound to some keyfile and not on the conventional (username, email address) tuple.

  • I am not sure about the merge algorithm (just because I didn’t think it through in detail). I only understand that it works with a rolling checksum, deletes are represented as special lines and merges are associative. This introductory blog post might help you.

  • Pijul needs more tools to visualize the progress of channels (similar to git log --graph).

All basic functionality worked flawlessly. The Nest must have been some large effort. Putting it into a large-scale test would yield important, heuristical results. I think this is a nice piece of software and I just need wider adoption of pijul to use it regularly.

As someone interesting in digital typesetting, I recognized that pmeunier is the lead of the Pijul development team. He was also involved in Patoline.