Ask HN: What is your Git commit/push flow?
I've long been in the practice of "commit early, commit often". If one use case works I commit, if the unit tests pass I commit. The code may be a mess, the variables may have names like 'foo' and 'bar' but I commit to have a last known good state. If I start mass refactoring and break the unit tests, I can revert everything and start over.
I also push often because I'm forever aware disks can fail. I'm not leaving a day's worth of work on my local drive and hoping it's there the next morning.
I've become increasingly aware that my coworkers have nice clean commit histories. When I look at their PRs, there are 2-4 commits and each is a clean, completely functioning feature. No "fix misspellings and whitespace" comments.
What flow do you follow?
200 comments
[ 2.7 ms ] story [ 232 ms ] threadConsider I have a feature branch that no-one depends on. When my history is messed up I do easy rebase :
Now at this point changes from multiple commits are in my STAGING.I push often just to be sure. (This is the modern equivalent of constantly hitting Ctrl-S to save).
Squash and clean up before merging to main branch after PR is reviewed and accepted. This makes rolling back the change a lot easier and keeps the main branch log clean from "let's see if this crap works" -style messages.
I believe you can also configure your PR software to attempt to squish for you? I'm not sure about that, but I think I've seen an option in the settings somewhere.
I will stipulate in advance that this is very much going to vary by project, language, and experience. It is entirely legitimate to have a different point of view ;-).
Also, I too used "foo" and "bar" as variable names, but stopped doing that. If I find myself using nonsense names it means that my mental model of the problem (or the solution) is too vague. Then I stop and think about it instead of going ahead at full speed.
When I'm developing, but before I create a PR, I'll create a bunch of stream-of-consciousness commits. This is stuff like "Fix typo" or "Minor formatting changes" mixed in with actual functional changes.
Right before I create the PR, or push up a shared branch, I do an interactive rebase (git rebase -i).
This allows me to organize my commits. I can squash commits, amend commits, move commits around, rewrite the commit messages, etc.
Eventually I end up with the 2-4 clean commits that your coworkers have. Often I design my commits around "cherry-pick" suitability. The commit might not be able to stand on its own in a PR, but does it represent some reasonably contained portion of the work that could be cherry-picked onto another branch if needed?
Granted, all of the advice above requires you to adhere to a "prefer rebase over merge" workflow, and that has some potential pitfalls, e.g. you need to be aware of the Golden Rule of Rebasing:
https://www.atlassian.com/git/tutorials/merging-vs-rebasing#...
But I vastly prefer this workflow to both "merge only," where you can never get rid of those stream-of-consciousness commits, and "squash everything," where every PR ends up with a single commit, even if it would be more useful to have multiple commits that could be potentially cherry-picked.
edit: googled, "Never rebase while on a public branch" i.e. a shared branch
Such an annoying mess it leaves otherwise. And CI is building 'merge branch master', on the master branch, great.
I think there's less argument in favour of merge than usual with pulls though - since it's much less like a semantic merge to begin with for git-merging to preserve a history of.
I'm certainly not aware of any objective downside/gotcha/'oh but it doesn't work when...', no.
The docs only add that you might further want to set it to `interactive` or `merges` rather than `true`, for the effect of rebasing with those options: https://git-scm.com/docs/git-config#Documentation/git-config...
Because git was written primarily for the Linux kernel and the defaults reflect that. The workflow of the Linux kernel is completely different from what most people outside of it do with git. "git pull" is used for opposite purposes in both worlds.
If you're merging a public branch with another public branch, then if you a rebase, rather than a merge commit, then you mess up the history for anyone has pulled that branch.
For private branches that isn't an issue.
How does one set this up in GitHub actions?
The annoyance I'm describing is that when the commit message is 'merge branch master' (and especially if, as the label next to it shows, it is the master branch) this is crap and useless, and hiding the 'real' commits behind it that the committer had locally while behind the remote. If they had `git pull --rebase`d (or `git pull` with the config option set) the commit message would be that of the latest 'real' one.
Only the keybindings are a bit weird if you're not accustomed to Vim bindings:
- Open tig
- Change into the staging view with `s`
- Select your file using the arrow or `j` and `k` keys
- Press Return to show the diff
- Navigate to the line(s) in question with `j` and `k` (arrow keys will switch files)
- Stage parts with `1` (single line), `2` (chunk parts), `u` (chunks) or split chunks with `\`
- "Leave" the diff with `q`
- You can find the keybindings with `h` in the help screen, which also uses Vim keys -- like manpages usually do
"c" is staging view.
> - Stage parts with `1` (single line), `2` (chunk parts), `u` (chunks) or split chunks with `\`
You can also revert a chunk or file by using "!". Sometimes this is very useful.
Additionally, git itself comes with a simple `git gui` command that allows you to do partial commits on a line by line basis. It also has a nice "amend last commit" mode.
1. I `git checkout PUBLIC -b CLEANUP` to a new branch.
2. Do a `git difftool CHANGES`, which opens each changed file in vimdiff one at a time.
3. For each file, I use :diffput/:diffget or just edit in changes I want.
4. Commit these changes on the CLEANUP branch.
5. Use `git difftool CHANGES` again to see the remaining diff.
6. Repeat until the diff comes back empty!
My unstructured changes tend to contain a handful of small typo fixes, white spacing, localized refactors, and 1 or 2 larger refactors and a behavioral change. Once they're all broken out, It's usually easy enough to use `git rebase -i` and reorder the smaller changes first, put out PRs for just those first, etc.
Edit: looks like it's back from the dead again :) https://github.com/gitx/gitx
I've tried most Git clients on Mac over the years and kept gravitating back to Sourcetree.
I only tend to use it for this particular workflow (picking out very granular changes on a line-by-line basis). Otherwise, 90% of my git stuff is via IDE integrations or command line.
[0] https://www.sourcetreeapp.com
(This works for me because auditing commit history is not important where I work, if it were I would organize commits better.)
*glances and $corp git repo and sees 'updates' 'fix' 'updates'. Sigh.
I see some people whose projects (Furnace Tracker, PipeWire, previously Famitudio) seem to make progress very quickly without getting noticeably slowed down by technical debt, despite sloppy programming and unorganized commit logs (push-to-head). Meanwhile I move slowly, dread reviewing hundreds of lines of my own code, and produce technical debt (regrets) anyway, not as many surface-level lintable errors but plenty of entrenched mistakes. I wish I could move faster, but instead struggle to make progress.
You could consider using a tool other than git push for protecting your work against disk failures. I rely on Apple Time Machine for that for example.
If I have 5 commits that are all whitespace changes, I'll use git rebase to group them into one commit before I push.
If it's a large, complex PR, then I'll reorder my commits and pick-and-choose chunks from the many commits so that they make sense in a progression. For example, I might just add one new component in the first commit. Add a second component in the second commit. Third commit might be integrating those two components into the system.
If my PR consists of both "cleanup" like running black on python, then I'll make that its own commit instead of littering actual functional changes within cosmetic ones.
I try to make it as easy as possible for my reviewers to review my code and actually catch mistakes. I also tell the reviewers to look at the commits separately and not to look at the entire diff which can sometimes be hundreds of thousands of lines of code. Even a large PR with 500+ lines of changes can be easy to review if you structure the commits right. (It's really like many PRs within one PR.)
You may ask, why not have separate PRs then for each commit? Sometimes, that makes sense when it's so large that it's worth having an integration branch that lasts weeks or months. Sometimes, it's not possible to just merge that one commit due to problems it'll cause with compilation or linting or other issues. Also, with separate PRs, it can be more costly (in both CICD resources and the dev's time) to do automated testing on each commit separately.
With some practice, the git rebasing and reordering or restructuring commits is not as difficult as it sounds, especially with the aid of a good git GUI (I like Fork.app) that lets me choose exactly the chunk to commit from a particular diff.
Once I got really comfortable with this workflow, what I'll actually starting doing is... when I'm done with something and ready to put up a PR, I'll flatten (fixup) all my commits into one, and then I'll revert it and commit that. Then I revert the revert but instead of committing that revert-the-revert, I'll stage it. This gives me all of my changes in a staged state. Then I'll pick-and-choose chunks from that entire diff to craft a "narrative" like I was describing before. Once I have everything in the right order, I'll drop the first two commits to get of the initial and revert commits.
Nice thing about small, atomic commits that still pass tests, is it works well with `git bisect` to find issues.
I also split commits defining a function etc from using it in other code. So if I `git reset --hard` because I screwed up some caller, I still have the function.
End of day I usually commit a "temp" commit with a few comments to myself and push that to the remote branch, revert that the next day and force push over it for the next commit.
My view is that final commits are a form of communication, and deserve some intention. I’ve thanked myself when I’ve looked years back at work I’ve done and been able to figure out not only the change, but also my own state of mind.
In the 20+ years that I've been using computers, and ≈15 or so that I've been writing software, I've never experienced a drive failure.
Personally I share the same experience: In the 30 years I've been using personal computers at home and work, only 2 disks failed on me, one was an error on my part during an OS upgrade and the second was an external disk that physically fell off my bed while I had it plugged into my notebook. So both of these weren't really those random failures.
On the other hand, while working in an IT infra team of a 60 developer company with 4 racks full of servers for 3 years, we'd get to see about one failed SSD per 2 months and 2-3 failed disks or SSDs per year in servers.
During the 1 year I worked for the IT infra team at a smaller hospital of a medium sized city with a large VDI environment and two HP EVAs in a two datacenter configuration each, we'd get 3-4 disk failures per week. Those had over 140 disk per storage each and were approaching the end of their support life, being around 5 years old, so the failure ratio started to get higher.
But those are just the catastrophic failures, I'm pretty sure I've also had hardware-related data corruption here and there as well.
With the experience of the last 3 or so years of using ZFS, I'd bet most of my "still working" drives over the years were erroring and losing data by the time I replaced them, and I just didn't know it.
Shameless plug, but here are other efficiency tips I wrote about, for working in a high demanding environment: https://dimtion.fr/blog/average-engineer-tips/
(based on years of experience with errors, failures, data loss, etc...)
edit: But I also now like to be messy in a dev branch, so it matters less in the grand scheme.
2. Make changes
3. Commit changes at whatever point and write a good commit message about the feature.
4. Commit more.
5. Create feature/branch_rebase
6. Squash all the commits.
7. Push to fork
8. Make pull request
9. Make fixes and push those commits per fix.
10. Squash when merging back to that first commit.
I like having a separate rebase branch because then I have all my work in one branch and the history of what I did. Then I squash it and get rid of the that history, then maybe rebase main back into it if there we changes since I started the feature.
I care less about the code quality during development, so long as the commits make sense and the end PR is clean. Commits that may have placeholder names, disorganized code could include:
"fix: failing test case" "feat: support new user flow" "cleanup: variable names and helper functions" "cleanup: organize files" etc
If the commit messages are clear as to why the change was committed, reviewing is a lot easier even with less than ideal code along the way. So long as the end state is cleaned up.
Otherwise I decide ahead of time and focus on one area at a time that I know is a small chunk of functionality (like adding boilerplate for a small script, unit testing a piece of functionality, implementing an endpoint) and as long as I'm working on it I just use git add -A ; git commit --amend --no-edit . Once I move on to new self-contained batch of functionality I make a new commit and repeat the amending. It requires a bit of discipline to keep commits small enough but I like it since it's an easy process to follow.
Before I make a review request I usually do an interactive rebase and reword commit messages, sometimes reorder them and squash small stuff if it makes sense. After a review is in process I generally no longer rebase (to keep review transparent) and commit each fix separately.
Overall I think commits that land in main should be worthy of landing in main. Random crap you tried isn’t important. Larger self-contained changes are.
1. Push very often, essentially every time I'm going to context switch or take a break
2. Iterate often, get things working, then clean them up, pushing at each step along the way
3. Interactive rebase to squash into meaningful stacked commits for review
4. Mark the change as ready for review by my team
I have found stacked commits to be the best way to both perform and author reviews by a wide margin. It's so much easier to review 4 200 line semantic changes than a single 800 line change. You'll get better feedback from others, and thinking about development in this way can also lead to better results on its own.