Daniel Taylor

== Poor man’s git === wip

The goal here is to create a very minimal form of source code version control. I’d be shocked if any of this is new. If anything similar to this has been done before, hit me up, I’d love to hear about it.

For personal, non-collaborative use, git is a bit overkill. In my opinion.

I can count on two hands the number of times I’ve used branches in one of my personal solo projects. And I can count on zero hands that amount of times that I’ve needed them.

Not to try to get too needlessly rigorous about it, the setup is as follows:

You have files, named things like $F_1$, $R_n$, etc. You also have collections (folders) of files, written like $D = (F_1 F_2 F_3)$.

You have the $+$ and $-$ operators on these files and folders, such that $A = B - C \iff B = A + C$, etc etc. The output of $-$ is always a file, even if the inputs are folders. You can think of $-$ as a file diff, and $+$ as applying that diff.

You have a number of snapshots of the codebase, $C_n, C_{n-1}, … , C_1, C_0$.

You have the codebase merged differences, $U_n = (C_n U_{n-1} - C_n)$. The $U$ here, very suggestively, stands of “unidirectional”. The second file there, denoted $R_n = U_{n-1} - C_n$, I’m going to be referring below to as a “reverse patch”. For completion’s sake, $R_0 = \emptyset$, the null file.

And that’s the main idea!

If you have a version of the codebase $C_n$, or rather the associated $U_n = (C_n R_n)$, and want to recover the previous version of the codebase $C_{n-1}$, you just do $C_n + R_n = C_n + (U_{n-1} - C_n) = U_{n-1} = (C_{n-1} R_{n-1})$. From there you can rewind to the next previous version if you wish.

Implementation

As a practical matter, to implement this you need two folders. One is your “code” or “working directory,” the one where you’re doing your normal coding in. For most situations this fill the roll of $C_{n+1}$, the next version of your codebase before it gets committed.

You also need your “backup directory,” which corresponds to $U_n$. This is the one that you keep backed up.

You need two folders because at the very least you need to know what files have changed. This is no different from git, except there it’s stored in your .git folder.

You can implement the version backtracking with a couple GNU tools. $-$ corresponds to diff and $+$ with patch.

A commit, or transitioning $U_n$ to $U_{n+1}$ is pretty simple:

# Start: CODE = C(n+1) , BACK = U(n)

diff -rN -u0 $CODE $BACK > reverse.patch
# reverse.patch = R(n+1)

cat reverse.patch | patch -Rd $BACK
# BACK = U(n) + -R(n+1) = U(n) + C(n+1) - U(n) = C(n+1)

mv reverse.patch $BACK/reverse.patch
# BACK = (C(n+1) | R(n+1)) = U(n+1)

To rewind the backup directory:

# Start: BACK = U(n)

mv $BACK/reverse.patch reverse.patch
# BACK = C(n), reverse.patch = R(n)

cat reverse.patch | patch -d $BACK
# BACK = C(n) + R(n) = C(n) + U(n-1) - C(n) = U(n-1)

rm reverse.patch

Bidirectional movement

There are a couple obvious improvements.

First, being able to go forwards as well as reverse in the codebase history would be nice.

Instead of the unidirectional $U_n$, we’re going to be working with the bidirectional $B_n = (C_n R_n B_{n+1} - U_n)$. That last file, denoted $F_n = B_{n+1} - U_n$, is also called a “forward patch.” Note that you can also write $B_n = (U_n F_n)$.

The problem is that $F_n$ can’t be constructed as you’re coding, because you’d need future knowledge of your codebase as you’re committing. So as you’re coding we’re going to adopt the convention that, if $n$ is the version number of the latest source, $F_n = \emptyset$, the null file. Then, when you start scrubbing back and forth through the history the $F_k$’s can be constructed because the final code state is known.

Second is that it’d be nice if the code directory updated when going back and forth through the history, so that you don’t have to switch your editor to a different directory.

The script for commit is the same as before:

# CODE = C(n+1), BACK = B(n) = U(n)

diff -rN -u0 $CODE $BACK > reverse.patch
cat reverse.patch | patch -Rd $BACK
mv reverse.patch $BACK/reverse.patch

To step back a commit

# CODE = C(n) , BACK = B(n)

cat $BACK/reverse.patch | patch -d $CODE
# CODE = C(n) + R(n) = C(n) + U(n-1) - C(n) = U(n-1)

diff -rN -u0 $CODE $BACK > forward.patch
# forward.patch = B(n) - U(n-1) = F(n-1)

cat forward.patch | patch -Rd $BACK
# BACK = B(n) + -F(n-1) = B(n) + U(n-1) - B(n) = U(n-1)

mv forward.patch $BACK/forward.patch
# BACK = B(n-1)

rm $CODE/reverse.patch
# CODE = C(n-1)

To step forward a commit:

# CODE = C(n) , BACK = B(n)

mv $BACK/forward.patch forward.patch
# BACK = U(n)

cp $BACK/reverse.patch $CODE/reverse.patch
# CODE = U(n)

cat forward.patch | patch -d $BACK
# BACK = B(n+1)

cat $BACK/reverse.patch | patch -Rd $CODE
# CODE = U(n) + C(n+1) - U(n) = C(n+1)

rm forward.patch