> Free-climbing it is an incredible endeavor [..] Honnold didn’t just free-climb El Capitan [..] Or, that’s true for the free climb
Minor terminology pedantry: free climbing merely means making progress using only one's hands and feet (or other body parts as appropriate) on the rock. It is in contrast to other styles including aid climbing, where it's totally fine to do something like place an expanding cam in a crack and step into a sling attached to it, thus using the equipment to make progress. You can still place cams and other stuff when free climbing but they're just there to save you if you fall. It's not free climbing if you're using gear to get up the wall.
Free solo climbing is free climbing without any protective equipment. If you fall, you keep falling until something else arrests you (the ground, a ledge, etc).
Honnold practiced freeing all the moves with protection until he was confident he could solo the route.
It's common to refer to free soloing as "free climbing" but it's not the same thing. Though people who aren't climbers likely don't care about the difference anyway, and that's fine. I'm just pointing it out in case anyone here is interested.
If we're going to get into it, coding with git makes you more of a trad climber and less like a free soloist. Sometimes you run it out when the going is easy, sometimes you put in some extra protection when it looks sketchy. It's all about knowing your limits and taking calculated risks.
No one ever forces you to pull out all your gear and free solo the route, just like no one is going to delete your git repo and ask you to rewrite your code from scratch. These are just things a tiny minority of people do, sometimes with disastrous results.
That said I like the analogy. But maybe the take home is that we don't all need to be Alex Honnold, where one mistake means certain death, and can instead settle to be the guy who takes the occasional 15 foot fall.
Why no squash? It's a private dev branch, so it doesn't throw off anyone else's work. Just commit to your heart's content for every little experiment. Write trivial commit messages like “checkpoint” because `git show` will contain so little code it's easy to figure out what's what. If you later decide you've headed down the wrong path, `git checkout -b my_feature_take2 HEAD~n` and start back up from a known point. When you have a functional final result, `git rebase -i master` to squash and groom the commits into more meaningful aggregations. Or just a single commit, if it makes sense for the context.
Interactive rebasing with fixup/squash makes the end result very readable. `git reset` and `git add -p` can also be a huge boon when tidying up a branch before PR.
I can understand they don't want to oversquash - I rebase & squash so that each commit should be able to stand on its own. I think the above link is a good argument for squashing (and rebasing) to the point that each commit should leave the system in a state where everything works, because otherwise you can't use `git bisect` and actually determine if it's good or bad. I will often have more granular commits where I break something and then fix it in the next commit.
Yeah, I think you're asking for trouble if you start squashing commits on code that has been deployed, which is the situation he seems to be describing here. I can't see any net benefit to that, and can see a whole lot of trouble. Any time your code has been shared outside your personal development environment, rebasing is dicey. The only situation I can think of that validates that is if something ended up in a commit that definitely should not, like auth credentials.
> The only situation I can think of that validates that is if something ended up in a commit that definitely should not, like auth credentials.
Even then, you can't unleak auth credentials so you will need to change them anyway. Then leaving the commit, while embarrasing, is no longer an actual issue.
Given most teams operate CI pipelines on their PRs/MRs, I like to make it a requirement to squash before merging. This means that only passing builds make it into the main branch.
The value of this cannot be underestimated, in my experience, as using things like “git bisect” mean you always have a working build.
So editing code is like free climbing now? My IDE has infinite undo, and some editors (like vim) can go back to arbitrary points in time. The relatively unlikely case that the editor crashes (or I accidentally shut it down) and I lose a little bit of undo state (or a few lines of code) is just not worth it in my opinion.
I commit maybe once every few hours. I don't recall my diff exceeding 2000 lines, normally it's a few 100s. I can't recall the last time I lost code. It only happened a few times ever. It was never a problem to quickly redo those changes (and possibly improve along the way).
Is there any other point in doing micro-commits other than being "safe"? If not, I have other things to worry about.
Besides, my editing environments don't play well with checking out an older commit to see what's there - they don't recognize this as "going back". Much better to simply undo.
When I was less experienced, my code was more error-prone and micro-commits saved me often. Either way, VSCode won’t lose your progress regardless of how it quits. It will always drop my undo history on restarts though (which can be a real pain if I’m exploring and haven’t made a commit in hours).
This best practice of committing often never made much sense to me. I find it typical that we as developers come up with such practices that start to control how we work instead of us focusing on getting work done. This practice makes even less sense when we make PRs that once merged are squashed into one commit?!
I try to make my commit history nice but the way people obsess about it, I don't think is productive.
I work in a way similar to the author, treating my micro commits like a videogame quick save. But many videogames separate "quick save" and "real save" in the UI and so do I.
I have a local branch, usually just called "wip" for work-in-progress and commit to it frequently. Then when I want to push to origin I pull all the contents from that branch into my "real branch" and commit it as one commit. Then delete the wip branch and repeat.
If I'm working on multiple tasks at once as my distracted brain loves to do, I'll have several wip branches. wip-new-hook-events, wip-update-sdk-version, wip-refactor-button-state. Sometimes the branches conflict with eachother, but dealing with that is worth being able to drop a task for a few days while I'm stumped or blocked. I also don't tend to micro commit when tests pass, just whenever I feel a smaller idea is either done, or I don't know how to progress yet.
Rebase works if you're comfortable with it, but it would make working on multiple tasks at the same time a bit harder wouldn't it? I'll be honest I'm not great with rebase.
If you do 'git checkout wip .' it pulls in all the changes committed to your wip branch into the staging area of your current branch. Then you can just 'git commit -m "My meaningful commit message."'
That's the cool thing about git though, it has a lot of compatible workflows so you can do what's most comfortable personally.
I consider myself a git noob, but I do find myself using rebase several times weekly. It seems to get easier the more I use it, along with all other git tactics.
Why bundle all changes into a single commit? How can the commit message be meaningful at that point? Why not just submit multiple atomic commits?
Sometimes I do that if it makes sense, but usually in that case those would be new wip branches with my workflow. If I'm at the point where my changes make for a good single atomic commit, they're ready to be committed to the main branch and the existing wip branch deleted.
If I'm working on really small tasks where I know I'll be done quickly and won't need to look back at my earlier changes, I don't bother making a wip branch.
Any chance you still have the link to the micro-commits post you mentioned below? Even better, do you have a post on your blog describing your workflow? I'd love to read a concrete example and apply it in my daily work.
I picked up the process when I first read a blog post about micro commits and wanted to try it but my company standards didn't allow tons of commits to clog the history like that. It feels like the best of both worlds to me.
I'll also add that sometimes I do break them into a few commits on "real branch" if it makes more sense to do so, and I use a GUI tool for merging if I need to solve conflicts. I also don't really worry about commit messages being useful in the wip branches. Oftentimes the commit message is just "wip".
Luckily my company doesn't have much prescription about commit process, so its up to each team to determine what works for them and we've been pushing for small atomic commits (probably what you'd term micro-commits).
The idea is that each commit should be small enough to fit in one "page" of PR review, with source lines and test line diffs <150 each, and everything should be one integrated and independent change with unit tests all passing and deployable to prod, etc. Honestly, I can't see a downside to this, other than maybe taking a while to internalize this process and to properly decompose commits.
Have you considered advocating for a change of company standards? Sometimes thats a sisyphean task, but if the only argument against it is "that's how it's always been done" it might be a worthwhile endeavor.
I think the difference in my case is an atomic commit works in isolation and pass tests. The micro commits don't always do that. That's where I differ from the article author in my workflow.
I have a "micro commit" sitting in a branch right now that's just a single line change, for example. That's not super common, but I don't have any strict rules for myself about when to or not to quicksave.
> Have you considered advocating for a change of company standards?
Oh trust me I did. I'm at a better job now :p My current company's git policy sounds identical to yours. But I still like this workflow for myself.
I've always disliked when code review tools are prescriptive about my commit history (i.e. prefer that each successive patch to a given CR needs to still just be a single commit rather than a separate commit per patch), and I ended up writing a shell function that just wraps whatever command I need to use to submit a CR that first swaps to a branch called `xxx-temp-xxx`, then does an interactive rebase (in which I squash all the commits I've made into one), then invokes the CR command, and finally swaps back and deletes the `xxx-temp-xxx` branch. This was made easier when I found out about `git checkout -`, which switches back to whatever you just swapped from last (like `cd -`).
I think the focus should be on atomic, independently reviewable commits, not time. Sometimes, those commits are obvious and sometimes it takes a while to figure it out. I don't think you should feel compelled to commit on a rigid timeline (though if you go too long, that probably indicates an issue with scope, processes, etc).
It's far easier and less time-consuming to squash commits than it is to tease out logically distinct changes from a big mess into separate commits. The latter is particularly troublesome when you require preserving bisectability, i.e. every final commit builds and functions. Being able to bisect saves tons of time chasing bugs.
This is one of those "ounce of prevention vs. a pound of cure" situations.
For the developer working on the change, or for the reviewer + everyone that comes after them looking at your commit (which can include your future self)?
We tend to discount everyone else's time but our own.
Might also be worth thinking about long term asymptotic effort vs marginal effort at a particular point in time--typically, the more often you do something (decomposing a commit into multiple independent parts, in this instance), the easier and less time-consuming it becomes.
> For the developer working on the change, or for the reviewer + everyone that comes after them looking at your commit (which can include your future self)?
Both. I don't really follow what you're getting at, I think we're in agreement? confused
Yes we are; I thought you were advocating for larger commits since they're too difficult to break apart into logically independent pieces, but it seems I just misunderstood what you wrote the first time I read it.
To me, the ability to bisect is a good reason not to squash (I assume you mean squash merge?).
My commits all stand alone; each is a distinct change that pretty much always is in a working state. Often it will be a series of refactors before the real change. Ironically, though it makes bisect easier, I find it rarely need to use bisect: just looking at commit messages (via either log or blame) is often enough to isolate a problem.
I accomplish this using interactive staging, and within my branch, and before I push, using any combination of rebase, squash, amend and fixup. I also prefix commits with the ticket to make referencing way easier.
My failed experiments (a change that later is undone) don't usually make it to anyone else, unless it's something modified during a PR review. On a small handful of occasions I've changed something radically and will delete the remote branch, modify and interactive rebase ny local one and push it again (or push as a new branch). For a feature I work on for a couple days, I'll commit a couple dozen times or more, rebase/squash to maybe 5-10 commits anyone else sees, and push to origin maybe 2-5 times.
When merging the branch (PR), I use merge --no-ff. This preserves commits and makes it easier to see later during a blame. And since the commit history is clean, it's not "noisy".
One of the massive benefits of this is the ability to take a big PR and split it up for easier review: I can branch off halfway, make a PR there with just the refactors, merge it, then the actual change merges cleanly later, and preserves the parent commit relations, so you can visually see what happened. This also works great when you fix a bug on the way to adding a new feature, or want to ship the early part of a feature early before everything is done (but only decide that after work has already started on stuff you're not ready to ship).
Two concepts are bound up together in a commit. "Publish" and "Save".
In general our OSes aren't storing a complete history of a file, so we rely on git "quick saves" (as someone else put it).
The git log is as much an artifact of the development process as the ticket history or the code itself. You don't want to publish your quick saves for someone else to have to dig through in two years time - give them something a bit more polished than that.
I find that having one commit per logically-grouped set of changes really helps me to focus on those changes and not get distracted. Before I got into that habit, for years I would try to cram so many features in at once, and would have a really hard time keeping track of each of their progress and what's left to be done for each, because I tried to do them simultaneously. No more.
> This practice makes even less sense when we make PRs that once merged are squashed into one commit
Up until the point of merging, other developers reviewing the PR can see your development path, and optionally examine individual commits. In GitHub at least, these individual commits usually make their way into the squashed commit's comments as a bulleted list of changes (after some cleanup), which encourages better final commit messages in general.
I examine commits regularly, sometimes going way back in time. Maybe not daily, but pretty frequently. Usually, I'm trying to figure out why something is the way it is (so I know if it can be changed, removed, or whatever), and a clean history makes this exponentially easier.
Our team has been pushing for and implementing small, atomic commits on mainline. Honestly, I'll keep fighting for it and never go back. PRs are easy to review, commits are easy to reason about, what's not to love?
Why squash commits to begin with? Unless you're getting too many commits per package per time period, then maybe that's an indication that you should modularize your codebase.
I always find practices about commits weird while working strange
I usually have my changes planned out before I start writing code, including which commits I want to make. Figuring out how to go about making a change is a design time problem, rather than implementation time imo
I usually commit early and often locally specifically for the ability to bisect during feature development. Then the squash and merge is for the "public" commit history
I have a guy on me team that commits basically every 5 or so lines. Hell have a 30 line bug fix spread across like 20 commits because he never goes back and cleans up those small commits that get changed afterwards, not to mention he’ll merge the development branch into it numerous times and never rebased. It’s absolutely infuriating reading through git logs that involve his stuff.
His commit messages are equally maddening - “change this variable to 5”, “fix broken spec”, “change variable to 7” ad nauseam…
Not really because none of his commits have any context on what the purpose any of his changes are actually for. He never describes why he does anything, just that he is changing something (which is obvious from the code).
It's the git equivalent of "useless comments" like `int x = 10; // set x to 10`
PRs dont' squashed, nothing gets rebased and only merge commits are allowed for anything non-local. It adds noise to trying to figure out why changes are made because the commit messages only explain what was changed, not the purpose or context.
Because then you don't see all those minor little changes and can view the PR holistically. I don't typically find that viewing the changes in chronologically order buys much, it's not the order I care about, it's the final changes themselves that have an impact on the system, especially when those changes could be transient (changing something in commit #1, change it back in commit #3). There's a larger load to comprehension with a squashed commit, but it shouldn't be THAT large, and it removes all the temporal noise.
It also makes tracing the historical "why" of a change easier to understand. If you don't squash it's a random change in the log, if you do squash then it's "adding feature x".
I've been doing this basically since I started using git ~9 years ago. I always work on dev/myfeature branch, and then commit when some tests pass, or I establish a code structure that I think will last.
It's often a tactic of "make the change easy, and then make the easy change". So the first few changes make the change easy -- they are refactoring to prepare for a behavior change.
Then often the last commit is small and has all the behavior change.
Now it's big enough to describe / review. So I do a git diff master.. , make sure it's what I want, run a wider set of tests, squash the commits, write a description, and merge.
In my mind these tactics makes going forward faster. I don't have to check every box before making progress. I'd say 95% of time I just keep piling on commits in the "happy path".
But 5% of the time, on a particularly tricky bug/feature, I might go backward, and this method saves me time. I don't have to worry about polluting the master branch.
-----
Before git I didn't really have a decoupling of "commits" and "work that could be pushed". I was usually working on one big commit. I appreciate this flexibility in keeping track of "code maturity". The master branch should always be kept working with all tests passing.
But I often want to save code in "less good" states. And it even helps if I'm on a desktop and want to move to a laptop to change locations. I just push to the dev branch and then pull on the laptop, without messing up the master.
Staging can be a helpful alternative to micro commits. I'll write some code, make some progress, revise, and once I get a small changeset that I'm happy with I stage it. Then I'm free to make a bunch of exploratory changes and the diff will show only those changes, and I can back them out easily. I move forward bit by bit like this until the feature is done then commit.
++ to this, beyond committing frequently for your own sanity, doing so in a way that:
1. Runs CI
2. Can be merged independently (to reduce the chance of merge conflicts by preventing drift from master/main)
3. Can be reviewed in isolation (so your reviewers can follow your route)
I think micro-commits would make me overthink my commit messages even more. I'd love to keep snapshots of my code in different stages, but without having to put a label on X part of Y feature at Z stage.
I have a `git snap` command that does this. It creates a commit on a machine-specific snapshots branch with a fixed commit message. It doesn't touch the working directory and restores everything else (HEAD and the index) after making the commit.
One way to help this is to use some sort of tag text at the beginning of the commit info that is useful so you can see this in the sequence of commits that all go together (top is most recent commit):
sql_parser: add new languages now supported by parse_unicode()
sql_parser: switch from using old parse_text() to parse_unicode()
sql_parser: add parse_unicode()
sql_server_health: ....
sql_server_health: ....
...
rather than like:
fix stuff in this random function
updating functions using random function
The ones with the tags help the reader determine what things go together much more easily, so even those lame commits are much more helpful if they have a tag:
sql_parser: fix stuff in this random function
sql_parser: updating functions using random function
Yea the structure matches what you said but not really using the set of words called out in that example like "feat" "chore" etc.. Personally I find that less useful than categorized tags that the developers just choose as they go along and are meant as a thing to help you read the commit, not to actually fit into a perfect index of possible tags that are controlled someplace. Here's an example of the categorization scheme in use (plenty of other examples out there).
https://gitlab.freedesktop.org/mesa/mesa/-/commits/main
I was thinking this article would give me a way to automatically commit every time the compilation succeeds after a write with an automatic timestamp commit message. I think people here would find it horrendous because of the unhelpful commit messages but then nothing would prevent to edit the last commit message when you have implemented something meaningful. Just throwing ideas, don't shoot this post down.
Kent Beck wrote up a workflow along these lines called test && commit || revert. The idea is any change that makes the tests pass is committed, any change that makes the tests fail is reverted. https://medium.com/@kentbeck_7670/test-commit-revert-870bbd7...
I believe Kent has also posted a few videos on YouTube demonstrating this approach.
Something I kind of depend on--which may be a crutch, I'm not entirely sure--is VS Code's "gutter indicators" feature[1] that simply indicates where code was added/edited/deleted. It helps me jump between changes to tweak further or undo them. The normal diff view in VS Code is often useful too, and is essentially also made less useful if you commit too frequently.
In the past this has caused me to commit way too little (i.e. sometimes weeks between commits...), but I've improved on that somewhat.
I still have a bunch of tabs open on my laptop about some Git workflows that should allow me to commit more frequently and then revert back to my 'starting point' for the gutter indicators and other diffing. Must get around to trying some of that stuff out.
I'm probably missing some other useful tool or way of working to help with this kind of thing - let me know if anyone else has thoughts on this. Personally I don't think I could ever get to this 'micro-committing' state, but there is a middle ground somewhere that I'm aiming towards.
> I'm probably missing some other useful tool or way of working to help with this kind of thing - let me know if anyone else has thoughts on this.
The combination of 'git commit --amend' with 'git reflog' enables creation of commits that are hidden from the usual history as shown by 'git log --oneline', but no less fully inspectable later. Likewise recoverable, by passing commit hashes from 'git reflog' to the usual tools.
having mucked around with this in the past, in my experience, it actually isn't very useful and you almost never need any of the micro commits.
I use Jetbrains tools and it actually does this automatically through local history. I've never really needed it, but if you like that kind of thing, I'd suggest finding a tool that does it automatically for you
Git is now my "ctrl-s" in my editor. The autosave is set to 500ms. I never waste keystrokes saving now. maybe not as fine-grained as this article suggests, but whenever 'jumping' from rock to rock while climbing" as the starting metaphor shows, it is an awesome alternative to traditional 'saving' on the filesystem.
FYI if you're treating git as a durable place to save your code you might want to set 'core.fsyncObjectFiles = true'.
I've personally had my git repository become unusable when my laptop lost power too soon after making some commits, before the background periodic sync occurred. (XFS)
We have enough cheap space that we should be able to easily have (non-binary) file changes have a history log in our file system itself and be able to revert local changes without needing the full change graph that git entails.
I'm all for small commits, but committing every time ctrl+s happens would be an anti-pattern because ctrl+s doesn't imply it compiles whereas I'd want any commit to compile. (passing tests optional).
If I'm about to embark on something where PyCharm's local history isn't enough but I don't have an atomic commit ready, I just use PyCharm's Save to Shelf action. Same as a stash but I mapped it to Ctrl+S when I'm in the commit window. 90% of the time I don't screw it up anyway.
I do the opposite. Because VSCode helpfully highlights the bits that were changed, I don't want to lose this indicator because of a commit, so I often postpone commits until I am ready to move to another part of feature. Then I use 'git add -p' to make multiple smaller commits, and finally push to remote origin. I don't treat commits as safelines, instead I try to make them logical explanations of the reasons for the changed code.
My safeguard is "undo", but I rarely need it. If I do, I just copy the contents of the file to a new one (ctrl n, ctrl a, ctrl c, ctrl tab, ctrl v) before undoing the wrong path - because any change on older version will break redo history, unfortunately.
Just my opinion. I'm still looking for a better editor with "auto-save" commits, or maybe a VSCode plugin. It should just wrap around my build command (or let me pass something), and if it builds, it should do this in the background as a snapshot. I think defining a good build in the context of runtime behavior is hard. I don't necessarily need all of the intermediate states it in git, though I don't mind if this tool uses git to power the snapshots. Maybe there's a world where version control is abstracted
Vim's non-linear undo[1] history is hard to beat. Set undofile [2] and you are done with local versioning. For visualisation of the undo tree, install undotree [3]
IntelliJ keeps a running history of all the files on your system so you can always go and show history on a file or directory and see what you’ve done (in addition to git etc).
I use emacs and I really like undo-tree, for basically this reason. Every edit is its own save point, and I can go back to any point in my editing history, even the stuff that would have been overwritten using traditional linear undo/redo.
Vim also has this in gundo. It's one of the killer features of the older editors that doesn't seem to be present in newer ones.
Many (mediocre) programmers see/use git only as a tool for sharing code with collaborators. These people can be easily identified by leaving traces of meaningless commit messages.
The key is to convince these people that the git history can be very worthful if edits are commented appropriately. Rapid committing in small steps comes after.
For other developers: Please don't look at comments like the above and think that you are doing something wrong. Git is a tool, like any others, and you can be a good programmer using it in the way that makes you most productive.
Make small commits, make descriptive commits, whatever.
I don't think it's that simple when you're collaborating with others on a large project. I think Wine [0] is a great example of how small and atomic commits can create a very nice history that allows to you track down weird regression without having to read 1000+ SLoC commits or bunch of "chaff" that is just noise when you're trying to understand the bug. But it requires effort both from the developer to make actually good commits and from the maintainer to enforce these rules.
I'm a _very_ big believer in maintaining an informative Git history. I have some suggestions for practical Git usage patterns in a blog post I wrote last year:
If you see Git as a dumb sharing tool you are correct. It’s also correct to see it as a Global Information Tracker (git) whereby you are able to share the functional state of a program. Let’s say you make a commit of a program and tag it with a version, and also leave a message to yourself which explains what that commit or version accomplishes. By doing so, you’re going beyond “saving the text” and into “saving the state” which is valuable!
Git has abhorrent user interface / experience, thank God some people with fine taste in design made Magit, only way I can keep my sanity if I have to use Git.
Git is trivial to use if you don't try to actually understand it, follow the rules, and do stuff through a GUI.
If you have something more advanced than that, there's a bit of essential complexity there that's kind of hard to get rid of anyway.
There might be some better VCS out there, but I'd only want to switch if everyone else does. Having to stay familiar with 2 different systems sounds worse than 1 mediocre one.
Out of all the VCS systems I used, Git was the best. I used and (meh) and a bunch of horrendous stuff (mks, Serena, a couple more that I am trying to forget). So, of course, I'm weary of trying out new things. Got world and I grokk it and it is well suited for my messy mind.
Minor terminology pedantry: free climbing merely means making progress using only one's hands and feet (or other body parts as appropriate) on the rock. It is in contrast to other styles including aid climbing, where it's totally fine to do something like place an expanding cam in a crack and step into a sling attached to it, thus using the equipment to make progress. You can still place cams and other stuff when free climbing but they're just there to save you if you fall. It's not free climbing if you're using gear to get up the wall.
Free solo climbing is free climbing without any protective equipment. If you fall, you keep falling until something else arrests you (the ground, a ledge, etc).
Honnold practiced freeing all the moves with protection until he was confident he could solo the route.
It's common to refer to free soloing as "free climbing" but it's not the same thing. Though people who aren't climbers likely don't care about the difference anyway, and that's fine. I'm just pointing it out in case anyone here is interested.