for somewhat experienced computer-ers who want to git good
git is a complicated system, but i hope to give you an understanding that is sensible. this guide is mainly for people who have a grasp on the command line and some fuzzy git concepts in their head, and want to turn that knowledge into a functional understanding.
i don’t care about fully understanding git from head to toe, i am more concerned with the practical usage and application of the system than anything. most guides teach you about how git uses SHA1SUMs under the hood and blah blah blah. i don’t really care, i just want to show you how i use the system and go over some very common road bumps. you can read a 200 page thesis on the inner workings if you want - but i mostly don’t care.
i will teach you only what you must know to use git alone, and then with other people.
git is a distributed version control system. the word distributed here really just means that when you clone a git repository, your local copy is exactly as complete as the repo you cloned it from - the full # of commits, all the refs, every little freckle and scar is there.
when people say the word git, they generally mean one of three things:
you can configure git via a file: $HOME/.git/config or $HOME/.config/git/config
these are normally preferences, but I highly recommend the following bare-bones config before you even start using git, to fix some questionable defaults:
[user] name = Your Name email = firstname.lastname@example.org # to find your SMTP settings, duckduckgo for "email-provider-name SMTP" (for example "gmail SMTP") [sendemail] smtpserver = mail.example.org smtpuser = email@example.com smtpencryption = tls smtpserverport = 587 [advice] detachedhead = false [merge] ff = only
there are many fundamental concepts in git, and they’re often very confusing because they’re not intuitively named. please read these definitions carefully.
a git repository is just a pile of commits, and some refs.
no command used in this document besides
git push or
git fetch affects anything other than our local git repositories - the ones on our own filesystem.
a commit is a simple record of who made the changes, and what & when the changes were made
each git commit gets a unique ID - it looks like this:
or, some people use the first 7 or 8 characters of the commit ID as a shorthand:
example of making a git commit while inside a git repository:
# first, we will make a commit: $ echo "hello world!" > file $ git add file $ git commit -m 'add file' [master (root-commit) 94686b3] add file 1 file changed, 1 insertion(+) create mode 100644 file
example of viewing a full git commit:
$ git log -p -2 commit 94686b38ed654c2a809a68e4e207cc1819c5764d (HEAD -> master) Author: j3s <firstname.lastname@example.org> Date: Mon Jan 4 13:02:09 2021 -0600 add file diff --git a/file b/file new file mode 100644 index 0000000..a042389--- /dev/null +++ b/file @@ -0,0 +1 @@ +hello world!
that’s it, the entire concept of a commit. you can see the who (made the commit), what (was committed), when (was it committed), and why (it was committed).
if the “what” portion (changes made, aka diff) seem a little cryptic at first, don’t worry, it is. know that there are much nicer ways to view git diffs.
you might refer to this commit as
94686b38 for short.
git checkout command simply moves you between commits. you can either reference short commit ids, long commit ids, or refs (which you’ll learn about now-ish).
in order to commit changes, git wants to know which ones to commit. this is so you may break your changes up into little pieces, if you’d like. for example, if you have changed ten files, you may only want to commit files 1 and 3. you may use
git add to “stage” only those changes, followed by
git commit -m "commit message" to commit them.
git status in a repo at any time to get an overview of where you’re at.
ask me questions - email@example.com (email) or @j3s:cyberia.club (matrix)
how are these commit records ordered?
this is quite simple. every commit references only the commit it was placed on top of. you can follow this chain all the way back to commit #1 - it’s a sequential line.
referring to a commit by ID really sucks. so refs are a thing.
you can think of a ref like a CNAME in DNS, or a symbolic link (ln -s) basically, just a human friendly name for a thing.
refs simply point to commit IDs. easy peasy.
i-love-this-commit -> 94686b38
development-test-1 -> 94686b38ed654c2a809a68e4e207cc1819c5764d
there are two super important types of refs - branches and tags.
branches always point to exactly 1 commit.
however when commits are made while you are on a branch, the branch updates the commit it is referencing to the commit you have just made.
# see the commit ID that our current branch is referencing $ git show-branch --sha1-name [94686b3] add file # make a new commit $ echo 'hello world, again??' > file2 $ git add file2 $ git commit -m 'add file2' # see that the commit our branch is referencing has changed auto-magically $ git show-branch --sha1-name [9e379d4] add file2
what is this git checkout thing?
you may use
git checkout to move between commits. you can target either a commit ID or a ref.
for example, these are all valid:
git checkout my-branch
git checkout some-tag
git checkout 94686b38
remember, that if you check out a commit ID or a tag, any commits you make from there will fallinto the aether. you almost always want to be checking out branches.
if i run
git init, what is the default branch name?
master. there is nothing special about branches named master, it just happens to be the default. most people use this branch as the “source of truth”, but varies a shit ton and i wouldn’t rely on that to be true.
what if i don’t commit to a branch?
if you do not commit while you have a branch checked out, your commit will still be valid - it’ll fall into the pile of commits per usual.
however, if you do not eventually reference this commit (via either a branch or a tag) it will be automatically garbage collected and removed from your repository.
git intends for you to use branches while developing.
what is this HEAD thing?
HEAD is just a ref to the currently checkout commit. normally, that’s the latest commit on a branch. sometimes it’s a particular commit that you’ve checked out for whatever reason. If it helps, you can think of it the same way you'd think of "." in the terminal.
wtf is a “detached HEAD”
an unfortunate name, tbh. a detached head just means you’ve checked out a commit that is not the tip of a branch. that’s it.
the only thing you’re “detached” from is the latest commit of branches. so your current HEAD is detached from the branch. get it?
tags are refs that are almost exclusively used to point to commits that are known to be stable, and are used for versioning purposes.
critically, tags never update or change in any way after they’re created.
there are several types of tags - everyone uses annotated tags. almost everyone refers to “annotated tags” as “tags.” you’re unlikely to need to use or learn about other types of tags.
annotated tags have a special property: they can store a message, just like commits can. this can be used to include changelog info, or signoff info, or to draw ascii art.
to make an annotated tag:
# hey! i have a commit i really like. # it's called `9e379d4`. # first, checkout the commit you like so much: $ git checkout 9e379d4 HEAD is now at 9e379d4 add file2 # now, make an annotated tag (the only kind of tag anyone cares about): $ git tag -a i-really-like-this-particular-commit -m "yeah it's lit" # you can now checkout your new funky tag $ git checkout i-really-like-this-particular-commit # note that you will be in the "detached head" state after # checking out your tag. this isn't bad, but git would make # you believe it is.
remember, when you check out a tag, any commits that you make will just fall into the commit abyss, and be deleted unless you reference them!
congrats! you know a lot about git now. take a break pls, relax your shoulders & jaw and grab a cofvefe. then go to sleep. let this stuff sink in, and dream gently of how branches and tags are just refs. and how commits all pile up into a daemonic abyss…
git checkout <tag/branch/commitID>moves around between commits
git statusshows the current branch and any changed files and other misc helpful info
git add <filename>will stage changes
git commit -m "message"will commit changes
And now, for more annoying things: working with people. or: pushing, fetching, cloning, remotes, and merging
If you use git to maintain some personal projects on one machine, stop reading - you’re done!
But if you care to put your work on a server for people to utilize (including yourself), or you want to help work on someone elses project, I recommend reading on.
there are three absolute sources of confusion with working with remote repositories, and i want to address them up front.
ONE: git is almost completely local
people often think that git commands will interact with servers, in reality this is very rare.
no git commands in this guide interact with servers except
git push and
almost all git commands are local.
TWO: remote repositories are tangled with your local repositories.
remote repositories are not tangled with your local repository. even if you git clone a remote repository, the local clone is entirely independent.
the short name for “remote repositories” is “remotes”
remember that repositories include all refs and commits, so fetching a remote means you get all of its refs and commits as well. many people think that “remotes” refers to branches or servers - not so! remotes are simply repositories that are somewhere that isn’t your current repository.
remotes are, quite simply, git repositories that are somewhere else.
you must access remote branches somehow - the typical methods are via HTTPS, FTP, local filesystem, or SSH. therefore, remote URLs will look something like this:
https://git.sequentialread.com/forest/threshold.git firstname.lastname@example.org:~vvesley/terminal_shit # this is ssh in disguise ssh://email@example.com/~starless/fate-discord-bot # this is the full ssh URL /home/j3s/code/shitchat
note that HTTPS and FTP URLs are almost always read-only. if you want to push, use the SSH URL
we can add a remote to our test repository like so:
$ git status On branch master nothing to commit, working tree clean $ git remote add terminalshit https://giit.cyberia.club/~vvesley/terminal_shit $ git remote -v terminalshit https://giit.cyberia.club/~vvesley/terminal_shit (fetch) terminalshit https://giit.cyberia.club/~vvesley/terminal_shit (push) $ git fetch terminalshit warning: no common commits remote: Enumerating objects: 19, done. remote: Total 19 (delta 0), reused 0 (delta 0), pack-reused 19 Unpacking objects: 100% (19/19), 14.73 KiB | 457.00 KiB/s, done. From https://giit.cyberia.club/~vvesley/terminal_shit * [new branch] master -> terminalshit/master
as you can see, there’s nothing very special about remotes at all. they’re very dumb. you just add them and fetch them. fetching just gets the latest version of one of your remotes.
once remotes are fetched, you can use them locally, not so remote after all!
$ ls file file2 $ git branch --all * master remotes/terminalshit/master $ git checkout remotes/terminalshit/master HEAD is now at 8e21382 # HELPFUL j3s TIP! you can just type "git checkout terminalshit/master" here, # remotes/ is implied $ ls LICENSE.md README.md main.go
in practice, you would probably never add the remote URL of an unrelated repository. but i wanted to illustrate a point - that remotes are in no way tied to your own local branches. they are completely, 100% independent.
let’s undo that and add a remote that actually makes sense.
say you’ve added a wrong remote and want to change the URL.
One way is to simply remove the bad remote and add a good one:
$ git checkout master Switched to branch 'master' $ git remote rm terminalshit $ git remote add origin firstname.lastname@example.org:~j3s/git-example # i add an SSH URL because I want to write to this repo. $ git branch -a * master remotes/origin/master $ git remote -v origin email@example.com:~j3s/git-example (fetch) origin firstname.lastname@example.org:~j3s/git-example (push)
i used the Cyberia Forge to make a blank remote git repository, and got the URL from its’ interface.
since we have write access, we can easily push our changes to the remote:
# for reasons that we may never know, # the syntax of this command is `git push <remote-name> <branch-name>` $ git push origin master Enumerating objects: 6, done. Counting objects: 100% (6/6), done. Delta compression using up to 8 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (6/6), 443 bytes | 443.00 KiB/s, done. Total 6 (delta 0), reused 5 (delta 0), pack-reused 0 To giit.cyberia.club:~j3s/git-example * [new branch] master -> master
woo! we’ve stored our changes remotely. we can now do infinite stage, commit, push loops forever and ever… unless???
merging is simple in theory. basically, git takes two branches and attempts to make them one branch by identifying their common history and zipping them together - kind of like if you were to mash two trolli crawler worm snacks together.
merging is best explained by doing. please follow along with this example and get a feel for switching between branches and merging them into each other.
we’ll use the
git merge tool - the syntax is
git merge <branch>.
it will merge whatever branch you target into your current branch.
of course, you may target remote branches.
# first, we will make a git repository, put a file in it, and make our first # commit $ cd /tmp $ git init merge-test Initialized empty Git repository in /tmp/merge-test/.git/ $ cd merge-test $ echo blah > file1 $ git add file1 $ git commit -m 'add file1' [master (root-commit) ab1b602] add file1 1 file changed, 1 insertion(+) create mode 100644 file1 # next, we will flip to a different branch and make a commit over there $ git checkout -b different-branch Switched to a new branch 'different-branch' $ echo blahblah > file2 $ git add file2 $ git commit -m 'add file2' [different-branch d8b4a32] add file2 1 file changed, 1 insertion(+) create mode 100644 file2 # finally, we switch back to our master branch and check out our log - # you can see that file2 is missing. we merge different-branch into # our master branch with "git merge different-branch". $ git checkout master Switched to branch 'master' $ git log commit ab1b6025ea49c40e5ef4acf68272d0dd9e4de963 (HEAD -> master) Author: j3s <email@example.com> Date: Tue Jan 5 16:54:55 2021 -0600 add file1 $ git merge different-branch Merge made by the 'recursive' strategy. file2 | 1 + 1 file changed, 1 insertion(+) create mode 100644 file2 $ git log commit 90c66a1a6fe29073042c1fd2b391640e2b81f4ca (HEAD -> master) Merge: ab1b602 d8b4a32 Author: j3s <firstname.lastname@example.org> Date: Tue Jan 5 16:55:37 2021 -0600 Merge branch 'different-branch' commit d8b4a323726f2dde3e8edfe6cf9d09bc90c6c19f (different-branch) Author: j3s <email@example.com> Date: Tue Jan 5 16:55:23 2021 -0600 add file2 commit ab1b6025ea49c40e5ef4acf68272d0dd9e4de963 Author: j3s <firstname.lastname@example.org> Date: Tue Jan 5 16:54:55 2021 -0600 add file1
You will note that merging branches causes an extra commit to be generating detailing the merge - that is good! it’s mostly clerical.
eventually, possibly even already, you’ll run into the dreaded…
ok, let me stop for a sec and address something quick.
git merge doesn’t always require a nice commit message. sometimes, it can detect a very particular situation.
let’s say that you have cloned a remote repository, like shitchat.
let’s say you did this in the year 1974.
$ date Tue 05 Jan 1974 05:07:25 PM CST $ git clone https://giit.cyberia.club/~j3s/shitchat
and now it’s the year of our lord, $CURRENT_YEAR, and you want to pull in all of the latest shitchat updates! well, you git fetch from your remote of course!
$ date Tue 05 Jan 2077 $ git fetch origin remote: Enumerating objects: 514, done. remote: Counting objects: 100% (444/444), done. remote: Compressing objects: 100% (358/358), done. remote: Total 385 (delta 205), reused 1 (delta 0) Receiving objects: 100% (385/385), 104.62 KiB | 3.87 MiB/s, done. Resolving deltas: 100% (205/205), completed with 17 local objects. From https://giit.cyberia.club/~j3s/shitchat 61ae285..757305d master -> origin/master $ git merge origin/master Updating c1466a6..3994c88 Fast-forward README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 README.md
Fast-forward? why didn’t i get to type a commit message?
in this case, git noticed that the branches you’re merging have common ancestry - and that one of them (your local copy) can simply be sped up, so that it’s up to date with the remote! no merge necessary, since they weren’t merged at all - they’re two identical (but separate) repos!
if you own two copies of
harry potter: the wizard and one is half complete, you wouldn’t tear the ending out of the good copy out to fix the incomplete copy, you’d just finish the incomplete one with a pen. that’s basically how fast-forwards work. no commit message necessary.
git pull has a troubled past.
people will tell you to use
git pull - don’t listen to those people unless you know this wisdom:
I highly recommend using this methodology first. fetch and merge.
by default, git pull can have many dreaded consequences - the worst of all is leaving inconsistent nonlinear commits fucking everywhere.
i recommend using fetch followed by merge to get a grasp on the individual steps (since they can sometimes be confusing) - and then transtioning to
git pull after a few weeks/months once you have dealt with various edge cases.
tl;dr: don’t use
git pull if you’re new to git, or it’ll cost you a lot of time/work.
tldr;tl;dr: use git fetch && git merge, forget git pull.
welp, things were going fine. git seemed easy and intuitive. but then… well…
when git tries to merge and cannot automatically do it, it requires human intervention. this is called a “merge conflict”. let us demystify.
like merging, merge conflicts are easier to demonstrate than describe.
First, we’ll initialize 2 repositories with identical histories.
$ git init repo1 Initialized empty Git repository in /tmp/repo1/.git/ $ cd repo1 $ echo 'blahblahblah' > file1 $ git add file1 $ git commit -m 'add file1' [master (root-commit) 74a9348] add file1 1 file changed, 1 insertion(+) create mode 100644 file1 $ cd .. $ cp -a repo1 repo2
Now, we’ll make their
master branches have different commits.
$ cd repo2 $ echo 'this is shitty code' >> file1 $ git add file1 $ git commit -m 'i have consumed too much alcohol and produced suboptimal *hic* code' [master 3211e8d] i have consumed too much alcohol and produced suboptimal *hic* code 1 file changed, 1 insertion(+) $ cd ../repo1 $ echo 'shit is amazing code' >> file1 $ git add file1 $ git commit -m 'i have consumed stimulants and am soulless but produce good code' [master 9099012] i have consumed stimulants and am soulless but produce good code 1 file changed, 1 insertion(+)
now, we will simply add repo2 as a remote for repo1! and we’ll try and merge their histories, which of course cannot work.
$ git remote add repo2 ../repo2 $ git fetch repo2 remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (3/3), 271 bytes | 271.00 KiB/s, done. From ../repo2 * [new branch] master -> repo2/master $ git merge repo2/master Auto-merging file1 CONFLICT (content): Merge conflict in file1 Automatic merge failed; fix conflicts and then commit the result.
NOW we’re in it. a weird state. HELP! what does
git status say???
$ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: file1 no changes added to commit (use "git add" and/or "git commit -a")
okay, fix conflicts. let’s pop open the file:
$ cat file1 blahblahblah <<<<<<< HEAD shit is amazing code ======= this is shitty code >>>>>>> repo2/master
git inserted a bunch of shit.
basically, everything between
<<<<<<< HEAD and
======= is your current branch. everything between
>>>>>>> repo2/master is
your job is to pick the right stuff, remove the <<<< and >>>> and ==== parts, and then type
git add file1 and
git commit. so, let’s do it.
$ cat file1 blahblahblah shit is amazing code $ git status On branch master You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: file1 no changes added to commit (use "git add" and/or "git commit -a") $ git add file1 $ git commit [master 24d690d] Merge remote-tracking branch 'repo2/master'
voila, merge conflict resolved. if this sort of thing ever gets horribly complicated (and god, it does) - use a good GUI, it helps IMMENSELY.
check out how cute our commit history looks after that though:
* 24d690d - Merge remote-tracking branch 'repo2/master' (HEAD -> master) |\ | * 3211e8d - i have consumed too much alcohol and produced suboptimal *hic* code (repo2/master) * | 9099012 - i have consumed stimulants |/ * 74a9348 - add file1
hopefully, you’re able to reason about the above commit graph!
okay, fine, good. we’ve already cloned a whole bunch but let’s sorta cover it.
first order of business, clone a repo.
git clone https://giit.cyberia.club/~j3s/git-example Cloning into 'git-example'... remote: Enumerating objects: 6, done. remote: Counting objects: 100% (6/6), done. remote: Compressing objects: 100% (3/3), done. remote: Total 6 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (6/6), 423 bytes | 47.00 KiB/s, done.
What happened here, exactly? well, it turns out that git clone does a shit ton of things.
We can see the results:
$ git branch -a * master remotes/origin/HEAD -> origin/master remotes/origin/master
you’ll note that the remote HEAD is pointing at the remote master branch. this is almost always goin to be the case, feel free to read the HEAD section again for why this makes sense.
ALRIGHT. we have covered a lot of confusing ground. let’s summarize.
you might choose this workflow so that you can clone your repo from any computer you work on!
you might choose this workflow because you have to!
this is other useful stuff to know, in no particular order
git diff very often. i use
git diff --staged just as often!
git diff will simply show you any unstaged changes that you’ve made. combined with
git status it’ll give you confidence before you type
git commit and inevitably commit some fucked up stuff.
if you haven’t pushed yet,
git commit --amend can fix up a local commit.
if you have already pushed, live with it. embrace it. maybe Randomly capitalize things to keep people on their toes.
basically, a pull request is the diff of your branch and someone elses branch.
but how do you get this diff to them??? why, register an account on github of course!
pull requests have nothing to do with git. they were invented by github in an effort to proprietarily extend git and make you reliant on their service.
but that could be a lot of changes ?? yeah, well, pull requests are kind of a weird way of git contribution. but they are industry standard, so there’s that.
you normally don’t have write access to repos you want to contribute to, so github/gitlab/gitea/etc will have you:
that’s the basic process. it’s not too bad but tbh but doesn’t make a lot of sense to compare entire branches when you just want to get them a little commit... but the sea knows no sense and the ocean hears no reason.
github was good for free software awhile ago, and they’re still pretty cool i think.
even still, i try not to use github nowadays. github has an industry monopoly on git repositories, which were meant to be distributed. they also invented the concept of a “pull request” and really not much else.
“normal” git repos are called “working repos”. weird git repos are called “bare repos”.
bare repos and working repos are both fine, but normally:
the reasons are pretty boring. read this
the git cli will search all parent directories for a .git directory. if it finds one, it will try and use it.
$ pwd /tmp/garbage $ git init Initialized empty Git repository in /private/tmp/garbage/.git/ $ mkdir some-directory $ cd some-directory/ $ git status On branch master No commits yet nothing to commit (create/copy files and use "git add" to track)
you can make a
.gitignore file in the root of your git repo. this file tells git what to not track. you can use globs. so a
.gitignore file might look something like this:
.DS_STORE huge-compiled-binary .editor-preferences python-libs/*
assuming you didn’t want to store the python libs in git. probably not.
you can, you might, but you probably shouldn’t. if those binaries change, you definitely shouldn’t. but idk, store your family photo albums in git, it’ll be slow but who cares. i’m not ur boss.
cd .. rm -rf your-git-repo git clone remote-git-repo
use git status to find troubleshooting details:
First, set up git-send email. Either use the config at the top of this document or use https://git-send-email.io/ to guide you.
git clone https://giit.cyberia.club/~j3s/git-example cd git-example echo "CHANGES!" >> file1 git add file1 git commit -m 'change file1' git send-email --to="email@example.com" HEAD^ # HEAD^ is a shortcut for "the latest 1 commit"
public repos are scraped constantly, the best possible option is to rotate the credential immediately and leave the old credential in git.the second best option is to force overwrite history lololol but nobody does that, right?