Tuesday, August 31, 2021

Introducing: Git Scenarios (also Git Scenarios #1: Revert one file)

Working with git is an infinite loop of small victories followed by crushing defeats. Just when you think you understand the tool's design philosophy, just when you think you get git, just when you let your guard down and attempt the smallest operation outside your comfort zone, disaster strikes. Git-fu simply doesn't extrapolate. The command with the obvious name isn't the one you want, and the command you want doesn't do what you want without including some cryptic option. At best, your attempt does nothing. At worst, it leaves you with a corrupt repo and a missed deadline.

Git is so horrid one can't help but suspect Torvalds released it as a joke. The bad design, the obtuse syntax, the nonsensical command modifiers. I'm here reminded of Richard Feynman lashing out during an interview when a reporter insisted on using buzzwords and machinations to explain QED. You're taking something simple and beautiful and making it ugly and complicated! Once git escaped the lab, rational Software Version Control was forever put out of reach. Programming was no longer about solving problems, it was about sitting at the cool kids table.

What's that, young pup? You disagree? You say git is a paragon of SVC, an exemplar of UX, and if someone doesn't see that, well, they're, like, stupid.

Fine, simple test: Would you be using GitHub if it wasn't free?

Yeah, that's what I thought.



Introducing Git Scenarios

Alas, as Carole King observed, it's too late to go back now. But perhaps we git victims can share our git stories and our git pain and in the process help each other. Pull our comrades to cover from the withering fire of the 1337 H4X0Rs posting on StackExchange who turn every git question into a game of pedagogical Twister.

Ergo does LabKitty present Git Scenarios. A maybe recurring feature in which we confront a git problem and reveal the git horcrux that was its undoing and perhaps detour into an obvious solution that wasn't. Free of ego and pride, free of self-importance. And wrapped in a soothing veneer of snark, at least until we acquire the launch codes and turn Finland into a smoldering vacant lot.

Git Scenarios #1

In this the inaugural episode, our scenario is the following: A wild merge appears. Any kind of merge really, but let's say we're pulling from a remote.

However, trouble is afoot. The new code is perfect except for a single file. For that file, we want to keep the current version. We'd like to do -- for lack of a better therm -- an all-but-one merge. Is such a thing possible?

The answer is: kind of!


Before continuing, a caveat: Caveat emptor. I make no claims my solution will work for you if you find yourself in similiar straits. Git's behavior is impossible to predict (cf. prior ranting). I only claim my solution worked for me, nothing more. A different solution may be required under a different git version, or a different OS, or if the wind has changed direction. Always try out git commands on a toy repo before using it on work you care about. Consider yourself warned.

Like Mission Control deciding Apollo 13 best continue to the moon after the screen doors blew off, we, too, shall press onward. Rather than attempting an all-but-one merge, we will merge normally then revert the one offending file. After all, nary a blessed hour passes without some git nerd crowing about how easy it is to recover a previous version of your work. We shall put that claim to the test.

Footnote: I mean "revert" here in the colloquial sense -- that is, "to return to the previous version." This would ordinarily not require a footnote, but git has so perverted the language of version control that we must carefully define the operation we seek, especially those operations that happen to be literal git commands, which invariably don't do what any normal person would assume they do. ESL, I guess.

Let's invent some names so to write example code snippets. We will assume a branch name of pr123 living on an upstream repo (presumably a pull request -- hence the "pr" prefix) that we're going to merge into our working branch. The offending file is foo.txt.

First, the merge (pull, whatever):

$ git pull upstream pr123

[ git barfs out many lines of blah-blah here ]

For simplicity, we assume the merge (pull, whatever) concludes normally without conflicts or other weirdness (and don't even git (wordplay!) me started on how Torvald's automatic merge algorithm is DANGEROUSLY BROKEN).

After the pull, our local repo has been moved one commit forward. All we need now do is revert foo. One might think the solution involves using "revert" for there is such a git command. Or "reset" for there is also such a command. Or "restore" for this command, too, exists (in recent-ish versions of git).

Nope, nope, and nope.

Instead, remedy is provided by the checkout command. Not kidding.

As a reminder,

$ git checkout branch-name

changes your working branch to branch-name.

However,

$ git checkout commit-hash -- filename

reverts (again, in the colloquial sense) the named file to the version found in the commit identified by the hash.

We're almost home. We just need the hash for the previous commit, aka the one just before the merge, aka the one that has the desired version of foo. You can get the hash using the log command and squinting at the commit messages:

$ git log

commit efedc3160839e57589bd80b98f428d55eed336a8
Author: LabKitty
Date: Fri Aug 13 13:38:45 2021 -0500

  merge of new code

commit 1fa4193cd383c916b0ef7359dfe12ddad6db806e
Author: LabKitty
Date: Thu Aug 12 12:53:53 2021 -0500

   linus torvalds eats worms

commit 6592dc433f70fde40d61fc0119ffad7a0e4953c9
Merge: 588030f 3fb3730
Author: LabKitty
Date: Fri Jun 4 12:10:25 2021 -0500

   linus torvalds eats worms

commit 588030ff944503ab0c7a15b7cc8663d61fd8c34f
Author: LabKitty
Date: Thu Jun 3 15:09:12 2021 -0500

   linus torvalds eats worms

...and so on, back into the mists of time.

The hash we want is in the penultimate commit:

$ git checkout 1fa4193 -- foo.txt

...and we're done. Do a git status and you should see foo.txt is tagged as modifed in the active workspace (or whatever the hell it's called). Examine the contents and you will find the file is indeed the previous version. Add and commit. You may now get on with your life.

NB: You need only enter enough of the hash to uniquely identify it. (You would prolly enter the entire thing because copypasta, but doing so here made the command hard to read because it wrapped). NB: The double-dash preceding the filename in the command above is syntax for "interpret everything from here to the end of the line as a filename." I found this tidbit mentioned in exactly zero of my git books.

Epilogue

If you are paying attention, you will notice the command which changes branches also reverts a file just by using a different switch. That's called metaphor abuse by user interface wonks and git is full-to-bursting with it. If Linux worked like git, pr foo would print a file but pr -d foo would reformat the hard drive. It's the George Lucas effect: Once you get famous and the better angels of your nature get replaced by groupies, your ideas tend to sour.

If you pitched this garbage to your dissertation committee or in a job interview anywhere outside of Microsoft, you'd be flunked. And rightly so. But Linus Torvalds ported unix to the PC that one time so Linus Torvalds gets to tell the rest of us what to do. Forever.

No comments:

Post a Comment