Ask HN: What is your Git commit/push flow?

199 points by nineplay ↗ HN
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 ] thread
I do the same, but before pushing, I squash the commits to remove the small intermediate commits. That may be what your coworkers are doing as well.
Yeah.

Consider I have a feature branch that no-one depends on. When my history is messed up I do easy rebase :

  git reset --soft master (or whatever commit up to which I want to UNDO, but no further than commit I branched off from)
Now at this point changes from multiple commits are in my STAGING.

  git commit -m "Feature X implemented"
  git push --force (you can do If no-one depends on your branch, otherwise - don't do this)
(comment deleted)
Ohhh, you trickster! Going to start doing this.
Create branch, I commit stuff always when my stuff isn't actively broken and I can somehow describe what the change did.

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.

At the end you can just do an interactive rebase and squish the commits away that don't clearly explain what functionality you've added.

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 follow your flow, and I don't bother squashing my commits. So the repo history has lots of dumb little commits. We use a branch model for features, though, so I can just filter for those merge commits if I want to see granularity at the feature level.
Easier for you. Harder for who reads you history.
In addition it makes rebasing harder, which might make you use merge commits and then reading your commits gets even worse because changes might hide inside those merge commits.
Probably. Like I said, though, it's easy to just look at the merge commits and ignore the rest. I don't personally read per-commit history almost ever. If I have to follow someone's train of thought that closely to understand what happened, then 1) the docs suck, 2) or there should have been comments and they are missing, or 3) the story was too complicated.

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 ;-).

I commit often, push to branches until I'm satisfied, and for smaller branches often do squash-merges so all of the mess doesn't show up in the history.

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.

Here's how I address this problem.

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.

...what is the golden rule of rebasing

edit: googled, "Never rebase while on a public branch" i.e. a shared branch

It usually works out fine when you do a `git pull --rebase`, but not everyone does this or has it setup so pulling might have some nasty effects. Generally helps to consider a feature branch as a private branch. Don't push to other people's features without asking, don't fuck up other people's work.
Everyone absolutely should configure that. (Git config pull.rebase true.)

Such an annoying mess it leaves otherwise. And CI is building 'merge branch master', on the master branch, great.

Combine with git config --global rebase.autostash for your own protection.
Are there any downsides to doing this? Why isn't it on by default?
I suppose just the usual 'rebase vs. merge' - i.e. if there are conflicts to resolve they will be dealt with commit-by-commit in the usual way when rebasing, whereas without the option set they will be dealt with all at once 'merge-style'. I happen to think that's a feature, but I know some don't like it.

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...

> Why isn't it on by default?

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.

I think it's the usually public vs private branch issue.

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.

> And CI is building 'merge branch master', on the master branch, great.

How does one set this up in GitHub actions?

By default it shows the commit message doesn't it? At least I'm not aware I've done anything for e.g. https://github.com/OJFord/terraform-provider-wireguard/actio...

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.

FWIW I set pull.ff to only, since I don't want merge commits or rebases happening without explicitly calling pull --rebase or --merge.
Yes, if you're going to rebase, rebase your feature branch. Do not do it on the shared staging/dev etc.
The only problem I have with this workflow in the command line is that I would like to be able to split changes to the same file across multiple commits. I think some GUI tools enable this, anyone know about it?
When you're doing your interactive rebase, find the commit you'd like to split - choose "edit" for that one. Then when you reach that point, you can do `git reset HEAD^1` to bring those changes back onto your staging stack, and make as many commits as you need.
I do this via git add -p which breaks your changeset down into atomic patches that you can either add, skip or delete before making a commit. You can turn one file change into many commits this way, if need be.
Have a look at `tig`. It's even included in Git for Windows now and does this reasonably well.

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

"s" is status view, which you can also get at by running "tig status" directly.

"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.

Ah, thanks for the correction :)
I used tig for a bit because it was the nicest way I could find to do line-level staging in a terminal. But was really impressed with gitui https://github.com/extrawurst/gitui so I've switched to that
The way I deal with this is 1. try to make commits small enough so you are less likely to split them after the fact. 2. when I need to split a commit, I use VSCode UI and apply patch by patch. This is one of the rare case were I use a GUI for git. For most other things the command line is fine.
sublime merge handles this really nicely. You can do this in VS code too but the UI is a little more fiddly (right-click->"Stage Selected Range").
Git has a built-in GUI for that. Just run 'git gui'. It's not pretty but it works.

  > git gui
  git: 'gui' is not a git command. See 'git --help'.
  The most similar commands are
   gc
   grep
   init
   pull
   push
It's a separate package in some distros since it's written in Perl and uses Tk. gitk is often in the same package.
Both git-gui and gitk are in Tcl, not Perl.
I can second the recommendation of tig in the sibling comment.

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.

I use the interactive flag on git add for this. It lets you add parts of a file, commit, and then do it all again. I want to say it's git add -i, but I'm not 100% on that. My fingers just do the right thing when I want it to happen.
It's "p" for "partial"
If you're a vim guy, I use `git difftool` setup with `vimdiff` for this. Let's say you have your changes in a branch CHANGES on top of public branch PUBLIC.

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.

If you have a Mac, check out GitUp. It's a simple but fantastic tool for cleaning up git histories: merging commits, splitting commits, reordering...
Gitx is great for that on a mac, but every time I set up a new MacBook I have to hunt down the latest repo / build. It keeps getting abandoned and then forked and continued, and then abandoned again...

Edit: looks like it's back from the dead again :) https://github.com/gitx/gitx

I use Sourcetree [0] for this (on Mac but Win version available)

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

I do this (kind of too), but instead of an interactive rebase I just do a `git reset --soft <target_branch>`, where target_branch is the local (and up-to-date) copy of my target branch. That gets me one clean commit that I can force push up to replace my branch @ remote.

(This works for me because auditing commit history is not important where I work, if it were I would organize commits better.)

I first did rebase, and after a while I realized I was not getting much of it since I mostly wanted to merge everything down to a single commit.
This is the ideal workflow for me since I think merge commits clutter up the log, but it falls apart if people aren't consistent in following it.

*glances and $corp git repo and sees 'updates' 'fix' 'updates'. Sigh.

This is very close to my approach. I work in a private branch and make many commits along the way, but then organize it to tell a coherent story for the actual PR.
I do the same. A clean and logical history helps other people understand your code and might consequently increase the chances of getting it merged.
I do the same thing generally. In fact I generally find multiple mistakes or changes to be made, reading over my own code before merging, requiring figuring out which commit to merge my changes into (git absorb helps in the easy cases) and even more rebasing, or giving up and adding an extra commit at the end.

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.

> I also push often because I'm forever aware disks can fail.

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.

My dev box is deep inside a corporate lab. I'd be much happier if I could manage my own backups.
I do exactly as you say: commit early and commit often. However, something I find quite successful when publishing a Pull Request (aka CR/MR), especially large ones, is that I'll use git rebase to group chunk diffs together in a logical flow to create an easy-to-review set of commits.

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.

As long as I'm working on my branch, that I work on alone, I tend to rebase and interactively rebase those relatively often. The mantra is you shouldn't rebase public history, but since it's my working branch, I feel I can do whatever I want with it.

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.

I commit/push to a remote branch often mostly as a backup/easy undo. My commits are squashed for each PR; if I have two commits that I want to stay separate, I send them as separate/chained PRs (I like the idea that each commit in master represents a reviewed point in history).
I commit as clean as possible, each one should be a functioning feature or a part of it. I try to do small ones, so generally they'll just be part of a feature I have on my todo list for the entire feature. That actually helps me to mentally move on as well. If I need to rename some stuff I might squash commits, but no wip commits in the actual history.

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.

Create a feature branch for everything i work on. Then squash all commits on that branch for merge. If that doesn't look clean because there are too many features munged together, then split that feature branch into multiple ones (ideally would have already realized this ahead of time), then squash and merge each of those. So, then the main branch commit history is only "clean, completely functioning features" and then a link back to the PR that then has the full history of commits that got squashed, if you wanted more insight into the development process. The github UI simplifies a lot of this process and makes it a lot nicer to view.
GitHub/Gitlab allow you to rebase your commits when merging. We do that, but has the disadvantage of mucking up things for people who are using that commit. I personally think it’s worth it
I often commit as well to checkpoint, and push those onto a branch organized under my username as a backup (<username>/<branch> to make use of ref folders). When I’m done, I use interactive rebase to clean the branch up, writing meaningful commit messages for each unit of change (answering the question “why does this commit exist?” usually). I force push that cleaned up branch and then open a PR.

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.

>I also push often because I'm forever aware disks can fail.

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.

This is a good example of how scale can bias your view! :-D

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.

Sure, but your anecdote does not refute in any way that they can fail. It’s all about risk vs. overhead of avoiding that risk. The overhead of pushing often is very low, so even with the low probability of a drive failure, there’s not much reason to not do so (or any other form of backup, really).
I've had a Macbook SSD fail (suddenly and completely) within the warranty period. Surprisingly I've only had one hard drive fail before I retired it, maybe when it was about 6 years old.

But those are just the catastrophic failures, I'm pretty sure I've also had hardware-related data corruption here and there as well.

Some non physical failure scenarios I have experienced include catastrophic filesystem failures (NTFS: never again), loss of machine and no handy backup machine capable of reading the disk or filesystem in question (extX, ZFS, etc.), accidental damage to disk, partition or filesystem tree due to bad code (operator error; PEBSAC).
About 30 years using computers, a little over 20 being paid to do it. I've had two fail outright (one internal, one external) and a couple others develop errors that I noticed (one because I finally started using ZFS on my bulk-storage machine and it's very good at telling when a drive is misbehaving, the other, SMART caught, years ago) so they had to be replaced. All were spinning rust. Maybe 50 drives total, over my computer-using life, counting those in game consoles and ones in work machines that were assigned to me. Including SSDs. Give or take 10.

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.

The most important git feature I discovered was `git add -p`, this allows both to select which patchs to stage, but also to do a review of what you are going to stage. Combined with `git commit -v`, this allows you to have plenty of occasions to review your changes before creating your pull request.

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/

For tasks that you have mentioned, there's also built-in `git gui` for those who prefer to click.
Sublime Merge offers a nice GUI for the workflow you're describing with `git add -p`
I do the same as you, get the work done, keep is stable. Being clean and pretty is the _last_ thing I do.

(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.

1. Create feature branch

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.

You need to include a step where you squash commits. For me that was the point where I had to learn vi, since "git rebase HEAD~4" will throw you into vi. So then I ran "vim tutor" daily for a week and learned enough to continue using git.
I do the same as you - commit early and often, push nearly on every or every few commits, but I try and write my commit messages nicely even (and especially) if the code behind them is not so clean.

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.

(comment deleted)
The n1 rule I follow (and push everyone to follow) is: "Don't write shitty commit messages." It helps in general with everything related to Git.

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.

I usually squash my branches to single commits in the end and write a nice message then. There are times when I don’t do this. For example, yesterday I was moving existing code to a separate project and I first committed the original version and then made necessary changes in my next commit. This made it clear where things differed from the original.

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.

I think part of it depends on the tooling that you're using and what your review system supports. Assuming that these are all supported, I would generally:

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.