My Git Workflow

I wanted to post my general git workflow that git beginners may want to use. I ended up with this workflow based on all the issues that I've run into over the years; When I stick to this workflow I end up with as few merge conflicts as possible, and reversing any screw-ups is as trivial as possible.

I always do my local work in a non-main branch. That way I can work on multiple solutions for one problem and switch between them. It also means that if there's a new commit that came from origin/main that I know I will conflict with what I'm currently doing, then I can still pull from origin/main so that local main is up-to-date but work on my separate branch.

git checkout -b [branch name]

If you want to sync your branch with origin/main (described in the next paragraph) but have uncommitted changes, git will complain when trying that sync. This can be like a config or build file that you have changed to enable local running or something similar, so it is not something you would ever want to push to main. So... you have the option to delete the changes manually, but the better solution is to stash your changes, do the sync, then pop those changes. As the naming suggests, you set aside those changed but uncommited files and then bring them back after the sync was safely completed.

git stash
git pull origin main -—rebase   //covered below
git stash pop

Barring the case where I purposely want to be on an older commit, I always keep the branch I'm working on up-to-date with the origin/main branch. Previously, I used git pull origin main. But the downside of this is that this creates a merge of the origin/main branch and your branch. This usually isn't a problem but if you ever want to revert a commit it may get hairy. So instead, I rebase! Rebasing on origin/main, like the command below, essentially takes commits from origin/main and puts them on top of the commits that are common between origin/main and your branch, then finally puts your local commits on top of those origin/main commits. This means your branch's history will be a straight line, which makes understanding the history much easier to decode.

git pull origin main -—rebase

When I'm done editing files or directories, I add them with

git add [file name or directory name]

You can alternatively just do git add -A if you want to add all files, but there are multiple versions of "add all" that I can never remember. And frankly I often don't edit or create enough unique files in separate directories that aren't already covered by git add [directory name].

When I've added all the files I wanted, I'm ready to commit; nothing fancy here.

git commit -m "commit message here"

Let's say I've committed something, but made a small edit in one of the files in the edit - maybe just a couple lines. Instead of making an entirely extra commit on top of the one you already made - which can get unwieldly if you end up committing more than 3 or so times - you can amend your change!

git commit --amend

Warning: don't use --amend on a commit that has already been pushed to a remote branch.

I'm now ready to push my changes to origin/main! I'm still on my local branch, and to make things as smooth as possible, I run the following

git pull origin main -—rebase
git checkout main
git pull origin main -—rebase
This means that both my local main and branch are in sync with origin/main so that the following command will properly put the two local branches on the same commit. (I mentioned earlier you may want a branch to not be synced with origin/main for some reason - but if you ever wanted to push that change to origin/main you would eventually have to run git pull origin main-—rebase)

Then to merge the changes off my local branch

git merge [branch name]

Now if you run

git log --oneline
You should see that both main and [local branch] have the same commit ID. But hold on. Let's say that I ended up with 2 or more commits on local branch due to the changes being different enough to not justify running git commit --amend? Well, after the merge now local main has those multiple commits as well. Could we still push to origin/main? Yup, and I'll go into why you may want to do that in the next section. However I personally often find that it is good practice to push *one commit at a time per package* to main or as a part of your code review. The reason being that once that the code is merged with origin/main, if someone else has to reverse changes due to a bug in Production or any other reason, it is simple to just revert that singular commit causing the problem. So anyways, how do you squash those multiple commits into one?

git rebase -i

The rebase command will take a look at all your local commits and bring up an interace (that's what the -i is for) that lets you write "pick" or "s" (for squash) for each commit. I always do "pick" for the top-most listed commit and "s" for all the ones below. Then once you confirm your choices with :wq, you will get another list, this time of messages from each commit. This is where you can mix and match (or create new) the commit message you want to end up having. You can do whatever you want, just note that prepending a line with # will disregard that comment in the commit message. I typically just choose the one that is most comprehensive and # the other ones, or edit it if need be.

Getting back to why you would want to submit multiple commits without running git rebase -i. This is a good idea if your changes were that drastically different and contain different logic; whether you should is a judgement call based on your situation. A good rule of thumb is answering "if someone reverts the commit because of a problem, will an entirely unrelated set of logic - or even a different feature - be reverted as well?" If the answer is yes, then you may want to keep the commits separate - and possibly even submit them in separate code reviews.

Phew, with all of that out of the way, we push and then rebase so that your git logs show origin/main containg your new commit.

git push origin main
git pull origin main --rebase

And for good measure, you can run git log --all --graph --decorate or git log --oneline to admire your clean code tree/history :)


Some bonuses:

Multiple times I mentioned reverting a commit, how do you do that?

git revert [commit]
"revert" is a bit misleading, it sounds like you are rolling back to that commit. But in reality it is reversing the changes in that commit to be what they were before it; if you run git status after revert, you will see those same files edited, but in the reverse order! What I mean is, if in commit "abc" you added

- System.out.println("foo")
+ System.out.println("bar")

running git revert abc will result in

+ System.out.println("foo")
- System.out.println("bar")

You can then add, commit, and push the reversal. This is great if you are need to reverse a commit that has a ton of files; you're not going to look at the git diff and make the changes file by file and line by line. git revert does it for you!

Now, if you actually do need to roll back/reset to a commit and *permanently delete all commits after the chosen commit* you can run the below (For a lot of more options, refer to this stack overflow post)

git reset --hard [commit id to reset to]
And to reset to a commit but not permanently delete all newer commits, you run
git checkout [commit id]

To bring to HEAD a revision of only a single file from an old commit, run the below. Note that this will overwrite any changes you may have in an uncommitted file of the same path and name combination.

git checkout [commit id] -- path/to/file.txt