How do I use git

Recently I was trying to explain to a colleague what my criteria are when creating a pull request – when it is worth merging something into one pool request and when not. And I noticed the phrase “well, except for…” several times and decided to write down how I use git – to understand the features of my approach, how I could improve it and, perhaps, share something useful.

Since this is the internet, let's get this out of the way: the way I use git is based on the last 12 years of working in companies with relatively small (under 50 people) teams. In each of them, we used only git and GitHub; changes were made in separate branches, proposed as pull requests, and merged into the main branch. In the last few years, after the introduction of GitHub squash-mergingwe used it.

I have never used any other version control system. I cannot and will not compare git to Mercurial, jj, Sapling, etc.

So this is how I use git.

Technical details

Everything is in git, all the time. Any side project, big or small, completed or abandoned, everything is in the repository. Running git init is the first thing I do in the new directory. I see no reason not to use git.

git is the most interesting part of my shell industry. Without it I feel like I'm naked. It shows the current branch and the state of the repository, i.e. whether there are unsaved changes:

When someone asks me to help them with something git related and I see that they don't have git information in their prom, this is the first thing I tell them to do.

I use git on the command line 99.9% of the time. I've never used a GUI for git and don't see the point.

The only exception: git blame. To do this, I always use the built-in text editor interface or GitHub UI. Previously, for 10 years I used the blame functionality in vim-fugitive. Now git blame support added to Zed.

I use git aliases and shell aliases as if a possible future arthritis was behind me, whispering “soon” in my ear and waiting for every extra keystroke. They stored in ~/.gitconfig and in my.zshrc. My most used aliases, according to atuin:

gst - for `git status`
gc — for `git commit`
co — for `git checkout`
gaa — for `git add -A`
gd — for `git diff`
gdc — for `git diff —cached`

I them spamming. Direct connection between muscle memory and keyboard, without brain involvement. Especially gst for withdrawal git status – I constantly use it as confirmation that what I did worked. I am adding files git add and I do gst, git add -p and again gst And gdcI am doing git restore And gst, git stash And gst.

For example, this is how I check what changes I just made, add to staging and commit:

~/code/projects/tucanty fix-clippy X φ gst
# [...]
~/code/projects/tucanty fix-clippy X φ gd
# [...]
~/code/projects/tucanty fix-clippy X φ gaa
~/code/projects/tucanty fix-clippy X φ gst
# [...]
~/code/projects/tucanty fix-clippy X φ gdc
# [...]
~/code/projects/tucanty fix-clippy X φ gc -m "Fix clippy warnings"
~/code/projects/tucanty fix-clippy OK φ gst
# [...]

Why? I'm honestly not sure – maybe it's a lack of feedback from the git commands, maybe because promt doesn't tell me all the information, I don't have a UI and gst de facto and there is UI?

I use this function pretty_git_log V~/.githelpers a hundred times a day. I found her in this screencast Gary Bernhardt and haven't changed it for 12 years. It looks like this:

Why git lr and not glr? I'm lazy and would probably never get used to glr after years of using git lr.

Why git lrnot glr? I'm lazy and would probably never get used to glr after years of use git lr.

Committing changes

What and how often I commit is based on what should end up in the main branch of the repository I'm working on. Commit? Squash commit? A series of commits? That's what I adjust to.

What ends up in the master branch should be:

  1. Easily understood by others as an independent change.

  2. Rollbackable. If I make a mistake while making a change and realize it after the merge, can I revert the change using git revert or will that also revert 12 other unrelated changes that are likely not relevant to the problem?

  3. Bisectable. If we notice that a regression slipped into the main branch last week, would it be easy to find if we went through the commits and tested them? Or we will have to say “this appeared in this commit,” and the commit itself is 3 thousand changed lines in which we updated the OpenSSL dependency, changed the advertising text, adjusted the timeout settings in the standard HTTP client, added a database migration, changed the business logic and updated standard logger? This is something I would like to avoid.

I don't think all three can be achieved 100% of the time, but the general message is – is it easy to undo? Is it easy to debug this in case of regression? is something I try to keep in mind when deciding whether to add something to a separate request pool or a separate commit.

I commit early and often. My approach: quick save in the game. Survived those three zombies hidden around the corner? Preserved. Fixed that nasty bug that required changes you don't really understand, but it worked? Preserved. You save it and then you think about how to do it right.

Commits and their history in my thread I see flexible. I can always reformulate them, combine them, move them – as long as I haven’t submitted them for review, as long as they are “mine”.

Why? Because in almost every repository I've worked on (except the open-source repositories I've contributed to), the merged pull request is what ends up on the master branch, not the commit.

So I commit as often and as much as I want and then make sure that the merged pull request meets my three criteria. Which naturally leads us to…

Pull Requests

Merged pull request is more important than a commit in a branch, since it is the one that ends up in the main branch and is the one worth optimizing.

If we use compressed commits when merging, the merge will result in one commit and I'll be thinking about what that commit should look like and whether it's easy to understand, roll back, and find.

If compressed commits are not used and all commits from the branch to the master are merged, then I will already think about how they should look. In this case, I can use interactive rebase on my branch and merge the commits so that they correspond to the parts of the work done and are easy to find, understand, and roll back if necessary.

Reviews create exceptions to these rules, since the demands of colleagues or reviewers are higher than mine. For example, if PR reviews are carried out on each commit, I will spend more time on their design. If the PR is reviewed as one change where three lines in two files have changed, I see no problem adding a “fixed formatting” commit and ignoring the message.

The general rule, however, remains: what really matters to me is the final PR, how it will be reviewed and what it will turn into after the merger, and not individual commits on the way to review and merger.

I open PR very early. Right from the first commit. I used to mark them as “WIP” by adding that as a prefix to the title, but now we have a draft status on GitHub. I open them early because after pushing the changes, while I continue to work, CI also starts working. I get the results of long-running test suites, linters, style checks, and other things that run in CI while I continue to work.

My approach: small PRs are accepted quickly. Sometimes they are 3 lines of code. Sometimes at 300. Almost never at 3000. If they are open for more than a week, this is already a warning sign.

Example: Let's say I'm working on a feature that changes the display of the user settings interface. While I'm working, I notice that the parameter parsing mechanism needs to be changed. This is a two line change. I'll take this two-line change and put it in a separate place from the UI PR changes, even though it was required as part of the UI work. Why? Because if two days later someone says “there’s something wrong with our settings parser,” I want to be able to quickly determine whether it’s a UI change or a parser change and roll back that change.

Instead of merging the main branch into mine, I rebase my PRs to the main branch. Why? Because when I use git lr (alias for showing git log) I want to see the commits made in my branch. I think it's cleaner to rebase to the latest version of main. I don't like merge commits on my branch. Interactive rebase also allows me to see all my commits and understand what's going on in the branch.

Do I care about the original commit history being destroyed when I do a rebase? Again: the unit of work is the merged PR and I don't care whether the commits on my branch reflect what happened during the work. What matters is what ends up in the main branch, and if we use a squashed commit, all this clean history will be lost in any case.

But again, exceptions arise due to reviews and reviewer demands – sometimes I do an interactive rebase on my branch to merge or edit commits so they are easier to review, even though they will be merged two hours later.

I also use PR in my pet projects even if I am the only one working on it and even if I will always be the only participant. I don't do this for every change, but sometimes I do as I like to track larger changes in the GitHub UI. It seems, some kind Do I still use the UI?

Commit Messages & Pull Request Messages

I pay attention to commit messages, but not too much. I don't care about prefixes, formulas, etc. I care about well-written messages. I read A Note About Git Commit Messages from Tim Pope in 2011 and haven't forgotten since then.

If we merge commits before merging, then the description of the PR is the message for that PR and I spend more time writing it.

The most important thing in a commit message or PR is “Why”. “What” we can see in the diff (although sometimes a short description can be useful), but when I read your commit message I want to see Why you made this change. Because usually, commit messages are read when something happens.

I think things like Conventional Commits It's mostly a waste of time. Teams waste time choosing the right commit prefixes with very little benefit. When I'm trying to find the source of a regression from a commit history, I'll be checking every commit anyway, since we know that yes, there can be a regression even in a commit [chore]: fix formatting.

Sometimes I add prefixes to commit messages or PR names, like “lsp:” or “cli:” or “migrations: “. But mostly this is done to shorten the message. “lsp: Ensure process is cleaned up” is shorter than “Ensure language server process is cleaned up” and conveys essentially the same meaning.

Whenever possible, I try to include a demo video or screenshot in the PR. A screenshot is worth a thousand words and ten thousand links to other tickets. Screenshot of this proof. Proof that it actually fixes what it claims to fix, proof that you actually ran the code. And it wastes much less time than many people think. Here example:

If necessary, I link to other commits or PRs in posts. Idea: leave crumbs. Instead of “Fixes broken parsing” I try to write “Fixes broken parsing after changes to 3bac3ed introduced a new keyword.”

In Zed, when we work in pairs, we add Co-authored-by: firstname <email> to commit messages so that a commit is tied to multiple people. Like this:

Especially when it comes to commit messages, the most important thing is context, context, context. When I work alone, I use different commit messages than when I work with a team. When we do a review, it's different from working in pairs.

Who are you communicating with with your message, when and why? These are the questions that should shape the message.

When I'm working alone on a personal repository, trying to get CI to work, you'll almost certainly see single-letter commit messages in main. But even if I work alone, if I fix an unpleasant bug, I will write a nice message. When I work with others, I try to write messages that explain to them what I was trying to do and why.

Review

Before asking someone to review my PR, I myself read the diff on the pull request page. For some reason, reading outside of your editor allows you to better notice bugs and remaining print calls.

I try not to ask for a review until CI has worked successfully. Exception: I know how to fix CI and we can work on reviewing and fixing CI in parallel.

When I review someone else's code, I always try to copy the code to myself, run it and make sure that it does what it says in the PR. You'd be surprised how often this isn't the case.

Workflows

The basic work flow is always the same when I'm working with someone: open my branch, start working, commit early and often, push early and often, open the PR as a draft as early as possible, shutdown, make sure the commits are on the branch more or less make sense, I request a review, merge it with main.

When I work alone, 99% of commits happen on the main branch and are pushed immediately.

Sometimes while working on a branch I notice that I need to make a new commit on a separate branch to turn it into a separate PR. There are several approaches that I use in this case.

  • git add -p && git stash what I want to commit in branch A, then I switch to a new branch B from the main one, make a commit in it, and send it.

  • git add ‑p && git commit what I want to leave in this thread. git stash what I want to put in another branch, switch between branches, git stash popI make a commit.

  • git add -p && git commit -m “WIP” what I want to leave in this thread. Then, again, I put into stash what I want to move to a new branch, go there, and make a commit. Then I return to the original branch, undo the “WIP” commit by doing git reset —soft HEAD~1 and go back to work.

  • git add -p what I want to move to another branch then git stash And git reset --hard HEADto throw away what is not worth keeping. I change branches git stash popI make a commit.

  • Sometimes I make changes to two different commits on the same branch, switch to the new one and move one of them to it using git cherry-pick. Then I return to the old thread and do git rebase -i and remove the moved commit.

When do I choose one strategy or another? It depends on the size of the changes I want to push to another branch and how many uncommitted changes are currently in the working directory.

I don't pay much attention to the names of the branches while they carry some meaning. I'm using GitHub UI to get review of the pull requests I opened (this URL is a quick link in Raycast, so I can just type “prs” in Raycast and open the URL). This helps me understand which PRs are currently in progress and which are ready to merge.

I create a PR either by clicking on the link shown after completion git push on GitHub, or by running the command gh pr create -w. This is probably the main thing I use for GitHub CLI.

Another thing I use for gh this is switching between open PR branches. Especially when I watch PR from third party contributors who are on their forks.

I also have these two convenient aliasallowing you to switch between PRs using fzf and I would like to remember about them more often.

It's been many years since I last had to delete and re-clone a repository due to git issues. Now I can solve most problems using git reflogA little git reset and blue electrical tape.

That's it – this is how I use git!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *