Today we will dig into how git really works. This will be a deep dive and provide a lot of details about how git creates a commit. It will reinforce important concepts, which are of practical when using git.
You could use the basic commands without understanding how it works, or with even an incorrect understanding. However, to really get the most important advantages out of git and use it fluently, know how it works in great detail is a huge advantage.
Today is also an exercise in careful examination of a system. You will do this a lot in your career, whenever you are debugging something, being able to systematically break it down and examine all of the parts is the central skill.
Later, we will build on this more on the practical side, but these concepts are very important for making sense of the more practical aspects of fixing things in git.
This deep dive in git is to help you build a correct, flexbile understanding of git so that you can use it independently and efficiently. The plumbing commands do not need to be a part of your daily use of git, but they are the way that we can dig in and see what actually happens when git creates a commit.
Inspecting a system’s components is a really good way to understand it and correctly understanding it will impact your ability to ask good questions and even look up the right thing to do when you need to fix things.
Outcomes for today¶
Also, looking at the parts of git is a good way to reinforce specific design patterns that are common in CS in a practical way. This means that today we will also:
review and practice with the bash commands we have seen so far
see a practical example of hashing
reinforce through examples what a pointer does
Review¶
Recall: git stores important content in files that it uses like variables.
navigate to your github inclass repo
cd gh-inclass-sp26-brownsarahm/.gitignore is a file in the working direcotry that contains alist of files and patterns to not track.
We can see it is a hidden file in the working directory with ls -a
ls -a. abstract_base_class.py important_classes.py
.. alternative_classes.py LICENSE.md
.git API.md README.md
.github CONTRIBUTING.md scratch.ipynb
.gitignore docs setup.py
.secret example.md tests
about.md helper_functions.pyls -a .git/. description index ORIG_HEAD
.. FETCH_HEAD info packed-refs
COMMIT_EDITMSG HEAD logs REBASE_HEAD
config hooks objects refsSome files in git work like pointers:
cat .git/HEADref: refs/heads/organizationholds the current branch
Creating a repo from scratch¶
We will start in the top level course directory.
cd ..
lsdemo kwl-sp26-brownsarahm
gh-inclass-sp26-brownsarahm tiny-bookYours should also have your kwl repo, gh inclass repo, course website clone, etc.
We can create an empty repo from scratch using git init <path>
Last time we used an existing directory like git init . because we were working in the directory that already existed
Today we will create a new directory called test and initialize it as a repo at the same time:
git init testhint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /Users/brownsarahm/Documents/inclass/systems/test/.git/I will change mine manually so that I keep getting the warning to teach about it.
git branch -m mainfatal: not a git repository (or any of the parent directories): .gitIf you were in a repo and have repos in repos, you should use office hours to get that sorted out
First we need to go into that directory:
cd test/and now we can try that command again:
git branch -m mainTo confirm we will look at the status
git statusOn branch main
No commits yet
nothing to commit (create/copy files and use "git add" to track)Examining an empty repo¶
ls .git/config HEAD info refs
description hooks objectswe can see the basic requirements of an empty repo here.
Searching the file system¶
We can use the bash command find to search the file system note that this does not search the contents of the files, just the names.
find .git/objects/.git/objects/
.git/objects/pack
.git/objects/infowe have a few items in that directory and the directory itself.
We can limit by type, to only files with the -type option set to f
find .git/objects/ -type fNo files at all yet, meaning there are no git objects yet.
Because this is an empty repo
Git Objects¶
Remember our 3 types of objects
blob objects: the content of your files (data)
tree objects: stores file names and groups files together (organization)
Commit Objects: stores information about the sha values of the snapshots
How to create an object¶
All git objects are files stored with the name that is the hash of the content in the file
Remember git is a content-addressable file systsem... so it uses key- value pairs.
Let’s create our first git object. git uses hashes as the key. We give the hashing function some content, it applies the algorithm and returns us the hash as the reference to that object. We can also write to our .git directory with this.
The git hash-object command works on files, but we do not have any files yet. We can create a file, but we do not have to. Remememer, everything is a file.
When we use commands like echo it writes to the stdout file.
echo "test content"test contentwhich shows on our terminal. We can us a pipe to connect the stdout of on command to the stdin of the next.
pipes (|) are an important content too. we’re seeing them in context of real uses, and we will keep seing them. Pipes connect the std out of one command t othe std in of the next.
We can put these together:
echo "test content" | git hash-object --stdinWe can break down this command:
git hash-objectwould take the content you handed to it and return the unique key--stdinoption tellsgit hash-objectto get the content to be processed from stdin instead of a filethe
|is called a pipe (what we saw before was a redirect) it pipes a process output into the next commandechowould write to stdout, with the pipe it passes that to std in of thegit hash-object
we get back the hash:
d670460b4b4aece5915caf5c68d12f560a9fe3e4Now let’s run it again with a slight modification. -w option tells the command to also write that object to the database
echo "test content" | git hash-object --stdin -wd670460b4b4aece5915caf5c68d12f560a9fe3e4We all got the same hash because we all put the same content into the hashing algorithm (test content)
and we can check if it wrote to the directory.
find .git/objects/ -type f.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4and we see a file that it was supposed to have!
There is no file in the working directory though, only a git object inside .git
lsViewing git objects¶
We can try with cat
cat .git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4xK??OR04f(I-.QH??+I?+?K?This is binary output that we cannot understand.
Fortunately, git provides a utility. We can use cat-file to use the object by referencing at least 4 characters that are unique from the full hash, not the file name. (7046 will not work, but d670 will)
cat-file requires an option -p is for pretty print
git cat-file -p d670test contentwhere we see the content we put in to the hashing function
or -t for type:
git cat-file -t d670blobwe see that it is a blob.
We have a git object but no files at this point
Hashing a file¶
let’s create a file, using a redirect
echo "version 1" > test.txtand confirm it exists
lstest.txtand store it, by hashing it
git hash-object -w test.txt83baae61804e65cc73a7201a7252750c76066a30we can look at what we have.
find .git/objects/ -type f.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30we see two objects as expected
Now this is the status of our repo.
We can check the type of files with -t and git cat-file
git cat-file -t 83bablobit is a blob object as expected
git cat-file -p 83baversion 1and has the content expected.
Notice, however, that we only have one file in the working directory.
lstest.txtgit statusOn branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
test.txt
nothing added to commit but untracked files present (use "git add" to track)it is the one test.txt, the first blob we made had no file in the working directory associated to it.
So far, even though we have hashed the object, git still thinks the file is untracked, because it is not in the tree and there are no commits that point to that part of the tree.
the workign directory and the git repo are not strictly the same thing, and can be different like this. Mostly they will stay in closer relationship that we currently have unless we use plumbling commands, but it is good to build a solid understanding of how the .git directory relates to your working directory.
Updating the Index¶
Now, we can add our file as it is to the index.
the index is also called the staging area
git update-index --add --cacheinfo 100644 \
83baae61804e65cc73a7201a7252750c76066a30 test.txtthe
\lets us wrap onto a second line.this the plumbing command
git update-indexupdates (or in this case creates an index, the staging area of our repository)the
--addoption is because the file doesn’t yet exist in our staging area (we don’t even have a staging area set up yet)--cacheinfobecause the file we’re adding isn’t in your directory but is in the database.in this case, we’re specifying a mode of 100644, which means it’s a normal file.
then the hash object we want to add to the index (the content) in our case, we want the hash of the first version of the file, not the most recent one.
finally the file name of that content
this has no output
Again, we check in with status
git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txtWe have the files staged as expected!
Now the file is staged.
Let’s edit it further.
echo "version 2" >> test.txtWe can look at the content to ensure it as expected
cat test.txtversion 1
version 2Now check status again.
git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test.txtWe added the first version of the file to the staging area, so that version is ready to commit but we have changed the version in our working directory relative to the version from the hash object that we put in the staging area so we also have changes not staged.
Lets check the objects
find .git/objects/ -type f.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30still only for version 1 and the “test content”
We can hash and store this version too.
git hash-object -w test.txt0c1e7391ca4e59584f8b773ecdbbb9467eba1547We can then look again at our list of objects.
find .git/objects/ -type f.git/objects/0c/1e7391ca4e59584f8b773ecdbbb9467eba1547
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30So now our repo has 3 items, all blobs
The index is not represented by any git objects, it is a temporary space
we can look at the new one again
git cat-file -t 0c1ebloband its content
git cat-file -p 0c1eversion 1
version 2git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test.txtPreparing to Commit¶
When we work with porcelain commands, we use add then commit. We have staged the file, which we know is what happens when we add. What else has to happen to make a commit?
We know that commits are comprised of:
a message
author and times stamp info
a pointer to a tree
a pointer to the parent (except the first commit)
We do not have any of these items yet, but we know that the message, author and time stamp come at the last bit when we make the commit.
Let’s make a tree next.
Now we can write a tree from the index,
git write-treed8329fc1cc938780ffdd9f94e0d364e0ea74f579Lets examine the tree, first check the type
git cat-file -t d8329treeit is as expected
and now we can look at its contents
git cat-file -p d8329100644 blob 83baae61804e65cc73a7201a7252750c76066a30 test.txtit looks a lot like what we put in the index
Now this is the status of our repo:
We have 3 blobs, and a tree that points to one of them
Again, we will check in with git via git status
git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test.txtNothing has changed, making the tree does not yet make the commit
Creating a commit manually¶
We can echo a commit message through a pipe into the commit-tree plumbing function to commit a particular hashed object.
the git commit-tree command requires a message via stdin and the tree hash. We will use stdin and a pipe for the message
echo "first comit" | git commit-tree d832The pipe puts the commit on stdin, suggesting that this would have waited for us to write a commit if run without
a398f920056d09cf678d6e808305376d01954c66and we get back a hash. But notice that this hash is unique for each of us. Because the commit has information about the time stamp and our user.
we can list the objects again:
find .git/objects/ -type f.git/objects/0c/1e7391ca4e59584f8b773ecdbbb9467eba1547
.git/objects/a3/98f920056d09cf678d6e808305376d01954c66
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30We can also look at its type to confirm
git cat-file -t a398commitVisually, this is what our repo looks like:
What does git status do?¶
compares the working directory to the current state of the active branch
we cansee the working directory with:
lswe can see the active branch in the
HEADfilewhat is its status?
we have seen it is “on main” this is because we set the branch to main, but since we have not written there, we have to do it directly.
Notice that when we use the porcelain command for commit, it does this automatically; the porcelain commands do many things. and check status
git statusOn branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: test.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test.txtNotice, git says we have no commits yet even though we have written a commit.
In our case because we made the commit manually, we did not update the branch.
This is because the main branch does not point to any commit.
We can verify by looking at the HEAD file
cat .git/HEADref: refs/heads/mainas expected
then we look at that file
cat .git/refs/heads/maincat: .git/refs/heads/main: No such file or directoryGit References¶
We can make that file manually
echo a398f920056d09cf678d6e808305376d01954c66 > .git/refs/heads/mainhere you need to use the full hash because we are writing a file directly that git will read, not using a git command.
Also you need to use the hash of your commit not mine.
then we check with git
git statusOn branch main
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: test.txt
no changes added to commit (use "git add" and/or "git commit -a")and git status works!
Visually, this is what our repo looks like:
git cat-file -p a398tree d8329fc1cc938780ffdd9f94e0d364e0ea74f579
author Sarah M Brown <brownsarahm@uri.edu> 1772734687 -0500
committer Sarah M Brown <brownsarahm@uri.edu> 1772734687 -0500
first comitMaking a second commit¶
First we have to write the tree
git write-treed8329fc1cc938780ffdd9f94e0d364e0ea74f579but this is the same as before
we need to add this change first:
git update-index --add --cacheinfo 100644 0c1e7391ca4e59584f8b773ecdbbb9467eba1547 test.txtgit statusOn branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: test.txtnow write the tree
git write-tree258231c1cee8048eef3a8057cfbdab76261277c6and commit
echo "second commit" | git commit-tree 2582 -pa39841636b02cc375291aabdea2941c08d98ed123d03find .git/objects/ -type f.git/objects/0c/1e7391ca4e59584f8b773ecdbbb9467eba1547
.git/objects/a3/98f920056d09cf678d6e808305376d01954c66
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579
.git/objects/41/636b02cc375291aabdea2941c08d98ed123d03
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30
.git/objects/25/8231c1cee8048eef3a8057cfbdab76261277c6Check that you have the same number of objects but your two commit will have different hashes
Prepare for Next Class¶
Take a few minutes to think what you know about hashing and numbers. Create hash_num_prep.md with two sections:
## Hashingwith a few bullet points summarzing key points about hashing, and## Numberswith what types of number representations you know.
Badges¶
Review the notes from today
Make a table in gitplumbingreview.md in your KWL repo that relates the two types of git commands we have seen: plumbing and porcelain. The table should have two columns, one for each type of command (plubming and porcelain). Each row should have one git porcelain command and at least one of the corresponding git plumbing command(s). Include two rows:
addandcommit.
Review the notes from today
Read more details about git internals to review what we did in class in greater detail. Make a file gitplumbingdetail.md and create a a table or mermaid diagram that shows the relationship between at least three porcelain commands and their corresponding plumbing commands (generally more than one each).
Create gitislike.md and explain main git operations we have seen (add, commit, push) in your own words in a way that will either help you remember or how you would explain it to someone else at a high level. This might be analogies or explanations using other programming concepts or concepts from a hobby.