Category Archives: Git

How-to use Git on Linux, with GitLab

I had these notes buried in a subdirectory on one of my servers. Originally, I created them for teaching purposes. But I use them myself for reference. Now I’m posting them here in WordPress for convenience. Warning, this is not a small cheatsheet like I originally planned. It keeps expanding. But relatively speaking, I’d say it’s a succinct explanation of the basics of Git.

I resisted Git for almost a decade, mainly because I was a successful solo developer long before Git was created in 2005. I used a version control system at IBM in the 90s, CMVC + TVFS. But for my own business projects, I never even considered using version control. And I still think it’s overkill for many small projects. As a rule of thumb, if you have more than one or two developers, you will probably benefit from Git.

The ultimate reason to use Git, nearly all software developers use it. And to work with them, they’ll want to use Git. As odd as it sounds, the only way to avoid using Git is to create your own software business where you’re the only developer, and even then your designer might complain you’re not using Git.

Git has so many features, it can seem intimidating, like information overload. Fortunately, most of the advanced features are unnecessary for typical day-to-day work. Once you get up to speed with the basic commands, you can start to get work done without much hassle.

The first thing you want to do is create a subdirectory to work in. So let’s create a “go-cheatsheet” directory as an example project, just because I’m working on writing a Go cheatsheet for my own reference.

mkdir go-cheatsheet
cd go-cheatsheet


There’s a good chance you already have git installed, so I’m going to skip the installation step.

Next, a lot of tutorials recommend configuring git globally. However, you might want to use a different email address for different projects, in which case you should use the “–local” setting. So when you set your name and address globally, I would just think of those as defaults and then you can be more specific later on if you have a project that merits a different “local” email address.

Each configuration level overrides values of the previous level. Global overrides System settings and Local overrides Global settings: Local > Global > System. In most cases, you’re the only user and you won’t need “system” defaults.

git config --global user.name "PJ Brunet"
git config --global user.email "pj@pjbrunet.com"


Now we initialize Git which builds a hidden “.git” directory. The .git directory contains all the internal files Git needs to operate. However, in most cases you won’t ever need to look in there.

Warning: Before you init, you probably want to make sure you’re not the root user, otherwise the .git directory will have root permissions and I’m thinking that could cause problems for you later on.

git init
ls -lA


The ‘ls -lA’ just shows you the hidden .git folder.

Or you can do this…

git init --initial-branch=main

Without the –initial-branch …
Notice the woke info about changing the name of your master branch. Since Gitlab uses a “main” branch instead of a “master” branch, it makes sense to rename our branch to match theirs. If you try to use “master” they force you to merge into “main” and it’s a huge pain in the *** to fix. I wasted an hour of my life and finally had to delete everything and start over. Resistance is futile, bow to the wokeness. Or use something else besides GitLab.

git branch -m master main

Now let’s check the status of our project.

git status

The output looks like this, because I have a file created in there already called cheatsheet.go

On branch main

No commits yet

Untracked files:
  (use "git add ..." to include in what will be committed)
	cheatsheet.go

nothing added to commit but untracked files present (use "git add" to track)


Untracked means the file isn’t “staged” yet. Before any code is added to the main branch, it needs to be staged first. In other words, Git sees the file in the directory, but it doesn’t know what you want to do with it yet.

The workflow is something like this: untracked > staged (ready to be committed) > committed (officially in the codebase) > pushed (to a remote server.) The last push isn’t absolutely necessary, unless your job requires it, for example to collaborate with other developers. Or maybe you’re pushing your code to a service like GitLab as a backup, or to share with the public.

Thinking of Git as a backup system, it was designed as a decentralized system for resiliency. So when you push your code to another Git server (like GitLab for example) you can think of that remote server as a “separate but equal” node that’s still fully functional if your laptop or PC is destroyed. In other words, it’s not a hierarchical network, it’s a decentralized network where individual Git servers push and pull code back and forth between each other, and usually the goal of that is to keep everyone up to date with the latest changes.

Moving on, let’s add cheatsheet.go because we want to add it to the main branch. The easiest way was already explained by the “git status” output. You can also use wildcards.

git add '*.go'

You can also add files interactively with the “-i” parameter, but it’s not as cool as it sounds. If you want a more fancy way to add multiple files, I recommend lazygit by Jesse Duffield. It’s super easy, you can stage and unstage files by clicking on them, or pressing the spacebar. Could it be any easier? No. You could spend hours searching for the best GUI Git client, but nothing could be easier than lazygit.

In any case, I’m going to assume we have all of our latest changes staged now. Any time you change a file, Git switches it back to “untracked.” Just remember to “add” the necessary files to the staging area before you “commit” them to the main branch, like when you’re done making a change. I usually type “git status” to see what files I’m about to commit.

Also note, Git doesn’t scan your entire computer for changes. It’s just looking for changes under the directory you ran “git init” which is also called your “working tree.” So for example, you could “git init” in projects/go-cheatsheet and “git init” again in projects/python-cheatsheet and each codebase would be completely independent. If instead you ran “git init” in the “projects” directory, then you would have one codebase for both projects.

Ask yourself, “Should I include everything in Git? Or just the files I need?” Every situation is a little different. It depends on the project, and to a degree it’s up to you. Maybe you want to stage the entire WordPress codebase, including your plugin. Or maybe you would rather go to example.com/wp-content/plugins/my-plugin and initialize Git there to only add files immediately relevant to your plugin.

Now let’s “commit” our first file, the one we just staged. You can think of a commit as a photograph of your stage, a moment frozen in time. And bonus, you get to add a message.

git commit -m "Start of my Go cheatsheet"

The output:

[main (root-commit) d147c9c] Start of my Go cheatsheet
 1 file changed, 78 insertions(+)
 create mode 100644 cheatsheet.go


Let’s talk about commit messages for a moment. Here are some tips and notes I collected.

A commit message should explain:

– Why is this change necessary?
– What problem does this commit solve?
– Briefly describe at a high level what was done.
– Will this change have side-effects?
– This commit will (your message here)

Focus on why, not how. Your code should explain how.

Not everyone agrees on this, but in general you don’t want too many changes per commit.

If you want to undo a change/commit, I don’t recommend using “git revert.” It’s better to revert the change in your code with a comment explaining the whole story of what happened, and the comment could even refer to the commit id that wasn’t needed, just in case somebody attempts the same idea in the future. For example, let’s say you add a feature, it works perfect, but the client decides they don’t want that feature. But next year they change their mind and want the feature back. It happens.

What if you accidentally commit something stupid? I would just leave it alone and add a new commit saying, “This completes the previous commit where I made xyz mistake.” Like a blockchain, think of commits as additive only. Trying to rewrite history to save face is not worth it.

Commit message formatting:

– Capitalize the first letter.
– Don’t end commit messages with a period.
– Keep it under 50 characters.

If one line is not enough, there’s also the “subject / body” format:

– Subject format is the same as a one-line commit message.
– Separate subject from body with a blank line.
– Wrap the body at 72 characters.

Here’s a good quote I found somewhere:

A well-crafted Git commit message is the best way to communicate context about a change to fellow developers (and indeed to their future selves). A diff will tell you what changed, but the commit message can properly tell you why. A commit message shows whether a developer is a good collaborator. Understanding why something happened months or years ago becomes not only possible but efficient. A project’s long-term success rests (among other things) on its maintainability, and a maintainer has few tools more powerful than the project’s log. It’s worth taking the time to learn how to care for one properly. What may be a hassle at first soon becomes habit, and eventually a source of pride and productivity for all involved.

If you want to change your commit message:

git commit --amend

That opens a default editor, probably nano. To change the editor:

git config --global core.editor "micro"

Moving on, let’s look at our log so far:

git log

Here’s a less verbose way:

git shortlog

Preparing to Push

The next step is optional, but typically we want to “push” our code to a remote Git server for safekeeping. I’m using GitLab and it’s telling me: “You won’t be able to pull or push repositories via SSH until you add an SSH key to your profile.” Assuming you already have an account setup there, the link to add your key should be something like https://gitlab.com/-/profile/keys and the GitLab instructions are helpful explaining how to do this.

As of today, GitLab recommends an ED2551 key type:

ssh-keygen -t ed25519 -C "pj@pjbrunet.com"

Now we’ll configure git to add our remote repository. Your location will be different!

git remote add gitlab git@gitlab.com:cts-llc/go-cheatsheet.git

For future reference, we can “pull” from and “push” to this “gitlab” repository. In most tutorials, the name would be called “origin”. But since it’s not an origin and more of a destination, I figure “gitlab” is more specific.

To see the remotes we have saved:

git remote -v

If we want to remove one of them, in this case the origin:

git remote remove origin

There’s also an update option: git remote set-url

Let’s finally do a push. The name of our remote server is “gitlab” and I believe “main” here refers to the name of the branch we want to push to on the remote server. The -u tells Git to remember our remote server name. So that next time we can simply run “git push”.

git push -u gitlab main

Or you can be specific every time, like if you have an account with Github, Bitbucket, or wherever:

git push gitlab main

Since this is our first time pushing, the cryptographic handshake needs our thumbs-up approval. Type “yes” to approve.

Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

Pretend we’ve invited other people to our GitLab project who have pulled our changes, who made their own commits, and pushed them to the main branch. Before we make more changes, it helps to have the latest code commits from everyone else. Why? Maybe some function you’re using has added a new parameter and you need to adapt what you’re working on accordingly.

Generally speaking, other developers are working on other things and you can all work independently without stepping on each other’s toes. And hopefully somebody is leading and coordinating everyone’s efforts. So probably you’re not working on the same files at the same time as other people. But it can happen, it will happen, and it’s not really a big deal if it does happen. That’s why we’re using version control in the first place.

So we can check for changes on our GitLab server and pull down any new changes with:

git pull

Note: If you see a warning client_global_hostkeys_private_confirm: server gave bad signature for RSA key 0 this is apparently a compatibility bug in openssh. The solution is to create or edit ~/.ssh/config and add the following lines:

Host gitlab.com
  UpdateHostKeys no


We could also write git pull gitlab main but we don’t need to be that specific. Because we set a default remote server already, git pull pulls anything new from GitLab.

Bigger projects could have more branches, besides main. Working on your own branch might allow you more independence to create some complicated feature without disrupting the main branch.

Create a new branch and switch to it:

git branch batcave
git checkout batcave


List all branches:

git branch

The current branch will have a * beside it.

Assuming we’re done working in batcave, merge batcave with main:

git checkout main
git merge batcave


The merge doesn’t automatically destroy batcave. To delete a branch:

git branch -d batcave

Note, you can’t delete a branch that you’re currently on.

In my not-so-popular opinion, there’s rarely a good reason to stray from the main branch. If you’re clever, you should have no problem making major modifications without impacting the existing codebase.



The more conventional wisdom sounds like this:

The master branch should represent the ‘stable’ history of your code. Use branches to experiment with new features, implement them, and when they have matured enough you can merge them back to master.

Maybe people want a separate branch for a separate product for a separate market. The sky is the limit as far as how complicated you can make things. Fortunately, for most small projects, you won’t need branches.

Like I was saying though, it’s a good idea to pull in the latest changes, especially before you push new commits. Because like I suggested, there’s always the risk two developers will independently make changes that result in some kind of conflict. If you’re working alone, it doesn’t matter and you won’t need to pull before you push. But if you’re working with other people, you should reconcile new outside changes with your own new code before you push back up to the remote Git server.

This reconciliation is called a merge. For the most part, merges are rare. When they do occur, Git will notify you and the resolution is usually pretty easy. So it’s not something you should stress about.

Working with other people, the workflow changes somewhat: untracked > stage > commit > pull > merge > push

Pre-Merge Checks

via https://git-scm.com/docs/git-merge

Before applying outside changes, you should get your own work in good shape and committed locally, so it will not be clobbered if there are conflicts. Git pull and git merge will stop without doing anything when local uncommitted changes overlap with files that git pull/git merge may need to update.

Hypothetically, let’s say two people commit changes to the same file and Git can’t figure out how to merge the changes. You can see the differences with:

git diff

Or you can just look inside the file that was changed, like with cat read.me. In this example, I committed my change “Another test” first, then pulled in (from GitLab) “Just another test.” So I can decide which change is more important, keep both changes, or whatever makes the most sense.

<<<<<<< HEAD
Another test.
=======
Just another test.
>>>>>>> b998e94b04378af2d4e5084251238a0e1cd316b0


To see what’s different on our stage:

git diff --staged

To unstage a file:

git reset test.txt
# or
git restore --staged test.txt


Files can be changed back to how they were at the last commit:

git checkout example.html

“Checkout” can be used for switching between branches or versions of files.

Checkout the branch “stuff”

git checkout stuff

Checkout out the file or directory “stuff”

git checkout -- stuff

If Git can’t find a branch, then it looks for a file. So you don’t really need the --

Checkout a specific version of a file.txt:

git log --oneline file.txt

To search all branches, you could use --branches

Then copy the (shortened) commit id of the version you want. Example:

git checkout 9a849c6

PS: If you see any errors or have any comments, please email pj@pjbrunet.com