== Poor man’s git === Git is “free as in freedom”, mine is “poor as in you-poor-thing.”
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.
A minimal version control system doesn’t include branches. 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.
That’s not a legitimate argument for why branches aren’t necessary for version control. I said those things because I just don’t like using them. I’m asserting that branches aren’t necessary.
Commit messages, commit signing, emails, conflict merging, any kind of built-in server are all not present in a minimal version control system. The only thing present is code snapshots, and being able to manipulate the history.
I’m going to lay out the general structure, which can be implemented a variety of ways. Then I’m going to list a simple implementation using common tools.
Trying to not be 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 that result in another file or folder, such that $A = B - C \iff B = A + C$, etc etc. $-$ is supposed to be thought 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$. $C_0 = \emptyset$, the null file. In this theory the null file isn’t an empty file, it’s a nonexistant file.
| 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. |
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 again to the next previous version if you wish. |
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. In most situations this fills 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
There are a couple obvious improvements that need to be made before this is usable.
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.
| With this modification you can scrub the history forwards and backwards. Given $B_n = (C_n | R_n | F_n) = (U_n | F_n)$, going backwards can be done by $B_{n-1} = (C_n + R_n | B_n - (C_n + R_n))$. Going forwards through the source history can be done by $B_{n+1} = U_n + F_n$. |
The second obvious improvement 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.
img3(commit.svg)CommitBackwards in history[Forwards in history]
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
You can implement commit messages by editing a dedicated COMMITMSG file. You could implement a git log by stepping backwards in history in a loop, cating COMMITMSG, and then going forwards again. Something like:
while [ -f $BACK/reverse.patch ]
do
cat $BACK/COMMITMSG
./reverse.sh
done
while [ -f $BACK/forward.patch ]
do
./forward.sh
done
Etc etc.