This is the most important lesson in the module and, most likely, in the first third of the course. Everything Git does — committing, branching, merging, rewriting history — follows directly from how it stores information. Once you understand the data model you stop memorising commands and start deducing them: you know what is possible, what is dangerous, and why a command has the effect it has.
The central idea is surprisingly simple: Git is a hash-addressable content store, a key-value table where the key is the cryptographic fingerprint of the content and the value is the content itself. On that minimal foundation sit four object types and a handful of references, and that is the whole thing. In this lesson we open the box: we will look inside the objects with low-level commands, explore the .git folder and work out why the history is immutable and what "rewriting" it really means.
Contents
- Hash-addressable content storage
- The four object types
- How they connect: the commit graph
- SHA-1 and the move to SHA-256
- What lives inside
.git/ - Hands-on exploration with plumbing commands
- Immutability and rewriting history
- Why this model explains everything
- Hash-addressable content storage
The idea in one sentence
Git stores objects in a database where each object's key is the SHA hash of its own content.
A hash is the output of a cryptographic function that turns any amount of data into a fixed-length string. It has three properties Git takes advantage of:
- Determinism. The same content always produces the same hash.
- Diffusion. Changing a single bit produces a completely different hash.
- Collision resistance. Finding two different contents with the same hash is infeasible.
The first property has two enormous consequences:
- Automatic deduplication. If
styles.cssdoes not change across twenty commits, its content is stored once. All twenty commits point at the same object. - Integrity checking. To confirm an object has not been corrupted you recompute its hash and compare it against its key. If they differ, there is corruption.
The second property gives us another: any alteration of the content changes the identifier, so nothing can be modified silently.
Snapshots, not differences
Many version control systems store, for each version, the list of differences against the previous one. Reconstructing an old file means applying the chain of differences from the very beginning.
Git works the other way round: each commit records a complete snapshot of the project's state. Conceptually, it is as if it kept a photograph of every file at that moment.
graph TD
subgraph "Difference model (SVN, CVS)"
D1["v1: complete file"] --> D2["v2: +3 lines, -1 line"]
D2 --> D3["v3: +8 lines"]
end
subgraph "Snapshot model (Git)"
S1["c1: full picture"] --> S2["c2: full picture"]
S2 --> S3["c3: full picture"]
end
The obvious objection is space: surely storing the entire project every time takes up an enormous amount? It does not, for two reasons:
- Reuse by hash. Files that do not change are not stored again; the snapshot simply references them. If Ana only touches
styles.cssin a commit, new content is created for that file alone. - Packing. Periodically, Git compresses loose objects into packfiles, which do use delta compression between similar objects. But that is a storage optimisation invisible to the model: conceptually they are still snapshots.
- The four object types
Git's database contains exactly four object types. All of them are identified by their hash and all of them are immutable.
| Type | Represents | Contains |
|---|---|---|
| blob | A file's content | The file's bytes, and nothing else |
| tree | A directory | A list of names with their mode, type and hash |
| commit | A commit | Hash of the root tree, parents, author, date and message |
| annotated tag | A tag with metadata | Object pointed at, name, tagger, date, message and optional signature |
The blob
A blob (binary large object) stores the content of a file. Nothing else: not the name, not the permissions, not the path, not the date. Just the bytes.
This has an elegant consequence: if Ana copies styles.css to styles-backup.css without changing anything, Git stores no new content. There is one blob and two directory entries referencing it. The file name lives in the tree, not in the blob.
The tree
A tree represents a directory. It is a list of entries, each carrying four pieces of data: mode (permissions), type (blob or tree), hash and name.
Here is what the root tree of task-manager would look like in its first commit:
100644 blob a3f5c9e2b1d4... README.md 100644 blob 7b2e8f1a5c3d... app.js 100644 blob 2c9d4e6f8a1b... styles.css 100644 blob e1f3a7b9c2d5... index.html
You will only ever meet a handful of modes in practice:
| Mode | Meaning |
|---|---|
100644 |
Ordinary file |
100755 |
Executable file |
120000 |
Symbolic link |
040000 |
Subdirectory (another tree) |
If the project had an img/ folder, the root tree would include an entry of type tree pointing at that folder's tree. Trees nest inside one another and build up the complete directory structure.
The commit
A commit is the object that gives the history its meaning. It contains:
- The hash of the root tree — that is, the complete snapshot of the project.
- The hash of its parent or parents: none if it is the first commit, one in the normal case, two or more if it is a merge.
- The author (whoever wrote the change) with their date.
- The committer (whoever recorded it) with their date. Usually they are the same person; they part company in scenarios such as a rebase or the application of someone else's patches.
- The message.
A real commit, shown raw, looks like this:
tree 9f4c2a8e1b7d3f5a6c9e2b4d8f1a3c5e7b9d2f4a parent 4d8f1a3c5e7b9d2f4a6c8e1b3d5f7a9c2e4b6d8f author Ana Ferrer <[email protected]> 1751328000 +0200 committer Ana Ferrer <[email protected]> 1751328000 +0200 Add task deletion to the list
Notice what is not there: not a single change, no difference, no list of modified files. A commit does not store "what changed", it stores "how everything ended up". When Git shows you a commit's differences, it computes them on the spot by comparing its tree against its parent's. That detail explains why git show is a computation and not a simple read.
The annotated tag
An annotated tag is an object that points at another object (almost always a commit) and adds metadata: the tag's name, who created it, when, a message and optionally a GPG signature.
It is the only one of the four types with a lightweight alternative: a lightweight tag creates no object, it is just a file in .git/refs/tags/ with a hash inside. That is why annotated tags are recommended for released versions: they leave an auditable trail. Module 5 develops the point.
- How they connect: the commit graph
The four object types link to one another by hash and form a directed acyclic graph (DAG). This diagram shows two consecutive commits of task-manager, with the second one modifying only app.js:
graph RL
C2["commit c2<br/>'Add task deletion'"] --> C1["commit c1<br/>'Initial version'"]
C2 --> T2["root tree (c2)"]
C1 --> T1["root tree (c1)"]
T2 --> B_HTML["blob index.html"]
T2 --> B_CSS["blob styles.css"]
T2 --> B_JS2["blob app.js (v2)"]
T2 --> B_MD["blob README.md"]
T1 --> B_HTML
T1 --> B_CSS
T1 --> B_JS1["blob app.js (v1)"]
T1 --> B_MD
Three fundamental things to notice in the diagram:
- The blobs for
index.html,styles.cssandREADME.mdare shared between the two commits. Nothing has been duplicated. Onlyapp.jshas two distinct blobs, because its content changed. - The arrows point backwards. A commit knows its parent, but a parent does not know its children. That is why Git walks the history from the present into the past, and why
git logstarts with the most recent entry. - Two different root trees were created even though three of the four files are identical: the tree contains the hash of
app.js, which changed, so the tree's content changed and so did its hash.
The references we met in the lesson Basic Git Terminology rest on this graph:
graph RL
HEAD["HEAD<br/>(.git/HEAD file)"] -.points at.-> MAIN["refs/heads/main"]
MAIN -.points at.-> C2["commit c2"]
TAG["refs/tags/v1.0"] -.points at.-> C1["commit c1"]
C2 --> C1
A branch is nothing more than a name pointing at a node of the graph. Committing means creating a new node and moving that name. That is all. Which is why branches are cheap, as we said up front in the lesson What Is Git?.
- SHA-1 and the move to SHA-256
SHA-1: the classic identifier
Git has historically used SHA-1, which produces 160-bit hashes written as 40 hexadecimal characters:
In practice, abbreviated forms of 7 to 12 characters (a3f5c9e) are used, enough to identify an object unambiguously in a normal repository. Git lengthens the abbreviation automatically if it detects a risk of ambiguity.
One important detail: the hash is not computed over the raw content, but over a header concatenated with the content. For a blob, what actually goes through SHA-1 is literally:
That \0 is a null separator byte. Thanks to the header, a blob and a tree with the same bytes produce different hashes.
The SHA-1 collision and Git's response
In 2017, the SHAttered attack demonstrated a practical SHA-1 collision: two different PDF files with the same hash. This did not break Git overnight — exploiting it against a repository demands very specific conditions, and the graph's cross-references make it far harder — but it did set off the alarms.
The response came in two phases:
- Collision detection. Since version 2.13, Git ships a hardened SHA-1 implementation (SHA-1 collision detection) that recognises the known attack's patterns and aborts if it finds them. That is a mitigation, not a solution.
- Migration to SHA-256. Since version 2.29, Git supports repositories using 64-hex-character SHA-256 hashes. You choose at repository creation time and cannot change afterwards.
| Aspect | SHA-1 | SHA-256 |
|---|---|---|
| Hash length | 40 hex characters (160 bits) | 64 hex characters (256 bits) |
| Status in Git | The default | Experimental but working |
| Collision resistance | Compromised in 2017 | No known practical attacks |
| Interoperability | Universal | Limited: cannot talk to SHA-1 repositories |
| Platform support | Complete | Partial or non-existent in 2026 |
The transition is slow precisely because of interoperability: a SHA-256 repository cannot synchronise with a SHA-1 one, and the whole ecosystem — GitHub, GitLab, CI tooling — would have to support it simultaneously. A design for dual-hash repositories exists to allow the two to coexist, but it is not finished yet.
For this course and for your daily work: use SHA-1, which is what Git does by default. What matters is that you know the reasons behind the transition and understand that the data model does not depend on the particular algorithm.
- What lives inside
.git/
.git/A freshly created repository has this structure. Let us walk through it:
.git/
├── HEAD # where I am: ref: refs/heads/main
├── config # the repository's local configuration
├── description # description, used only by GitWeb
├── index # the staging area (binary)
├── hooks/ # automatic scripts (sample templates)
├── info/
│ └── exclude # files ignored locally only
├── logs/ # the reflog: history of every ref movement
├── objects/ # THE DATABASE: every object
│ ├── a3/
│ │ └── f5c9e2b1d4...
│ ├── info/
│ └── pack/ # packed and compressed objects
└── refs/
├── heads/ # local branches: one file per branch
├── tags/ # tags
└── remotes/ # remote-tracking branchesThe pieces that genuinely matter:
| Item | What it is | Practical consequence |
|---|---|---|
objects/ |
The object database | This is the real repository; lose it and you lose the history |
refs/heads/ |
One file per branch, with a hash inside | See with your own eyes that a branch is 41 bytes |
HEAD |
A text file holding ref: refs/heads/main |
Switching branches means rewriting this line |
index |
The staging area | Explains why staging is a separate operation |
config |
Local-level configuration | Covered in lesson 01-05 |
logs/ |
The reflog | The safety net for recovering lost work (module 9) |
hooks/ |
Scripts Git runs automatically | Module 6 |
How loose objects are stored
Look at objects/a3/f5c9e2b1d4...: the first two characters of the hash name the directory and the remaining thirty-eight name the file. It is a simple optimisation: it avoids putting hundreds of thousands of files in a single directory, which badly degrades performance on many filesystems.
Every object file is zlib-compressed, so opening one in a text editor shows nothing but binary noise. To read them you need Git's own commands, which is exactly what we will do now.
- Hands-on exploration with plumbing commands
Git separates its commands into two layers:
- Porcelain: the everyday user interface (
git status,git commit,git log). Its output is designed for humans and may change between versions. - Plumbing: low-level commands operating directly on the database (
git hash-object,git cat-file,git rev-parse). Their output is stable and designed for scripts.
Day to day you will use the porcelain. Here we are going to use the plumbing precisely because it shows the model without decoration.
git hash-object: computing the hash of some content
Output:
Breaking it down:
echo "Task manager"sends that text to standard output.|pipes it into the next command.git hash-object --stdinreads standard input and computes the hash that content would have as a blob. It stores nothing: it only computes.
Check determinism for yourself: run the command twice, then change a single letter of the text.
The two hashes will be completely different, not similar. That is the diffusion property in action.
To store the object in the database, add -w (write):
The object now exists in .git/objects/. This command has to be run inside a repository.
git cat-file: reading an object
This is the inverse command: given a hash, it shows what is inside.
# Object type: blob, tree, commit or tag
git cat-file -t 2b1a3c5
# Object content, in readable form
git cat-file -p 2b1a3c5
# Size in bytes
git cat-file -s 2b1a3c5The three options:
| Option | Stands for | Returns |
|---|---|---|
-t |
type | blob, tree, commit or tag |
-p |
pretty-print | The content formatted according to the type |
-s |
size | Size in bytes |
Applied to a commit, -p produces exactly the tree/parent/author/committer/message block we saw in section 2. Applied to a tree, it produces the list of entries. Applied to a blob, the file's content.
git rev-parse: resolving names into hashes
It translates any way of referring to an object into its full hash.
# Which commit does HEAD point at?
git rev-parse HEAD
# Which commit does the main branch point at?
git rev-parse main
# And the current commit's parent?
git rev-parse HEAD~1
# Expand an abbreviated hash into its full form
git rev-parse a3f5c9eThe reference syntax worth recognising from the start:
| Expression | Means |
|---|---|
HEAD |
The current commit |
HEAD~1, HEAD~2 |
One, two steps back along the first line of parents |
HEAD^ |
The first parent (equivalent to HEAD~1) |
HEAD^2 |
The second parent; it only exists on merge commits |
main |
The commit the main branch points at |
v1.0 |
The commit the v1.0 tag points at |
One very handy command for getting your bearings:
# Absolute path of the .git directory
git rev-parse --git-dir
# Am I inside a Git repository?
git rev-parse --is-inside-work-treeA complete walk through the graph
Putting the three commands together, this is how you navigate the model from the top down. The hashes in your repository will be different: replace them with whatever each command returns.
# 1. Which commit is the current one
git rev-parse HEAD
# → c2a8f1e4b6d9...
# 2. Look inside the commit
git cat-file -p c2a8f1e
# → tree 9f4c2a8...
# parent 4d8f1a3...
# author Ana Ferrer <[email protected]> 1751328000 +0200
# committer Ana Ferrer <[email protected]> 1751328000 +0200
#
# Add task deletion to the list
# 3. Look at the root tree that commit references
git cat-file -p 9f4c2a8
# → 100644 blob e1f3a7b... index.html
# 100644 blob 2c9d4e6... styles.css
# 100644 blob 7b2e8f1... app.js
# 100644 blob a3f5c9e... README.md
# 4. Look at the content of app.js in that commit
git cat-file -p 7b2e8f1
# → the complete JavaScript codeYou have just walked by hand the same path Git walks internally every time you run any command: commit → tree → blob. If that walk feels clear, you have the mental model that holds up the rest of the course.
There is a convenient shortcut that combines a reference and a path:
The <reference>:<path> syntax resolves the tree and descends to the file in a single step.
- Immutability and rewriting history
Why the history is immutable
We now have every piece we need to understand Git's most important property.
A commit's hash is computed over its content, and that content includes its parent's hash. Therefore:
graph RL
C3["c3<br/>hash depends on c2"] --> C2["c2<br/>hash depends on c1"]
C2 --> C1["c1<br/>hash depends on its tree"]
If someone modified c1's message, its hash would change. c2 would then point at a hash that no longer exists, so c2 would have to change too, which would change its hash, which would force c3 to change, and so on to the end of the history.
Three guarantees come out of this:
- The past cannot be altered unnoticed. Any change invalidates the entire chain that follows.
- A commit's hash identifies the whole history leading up to it. If Ana and Bruno have the same hash at the tip of
main, they have exactly the same history up to that point, byte for byte. That is a complete verification in a forty-character comparison. - Objects are never modified, only created. There is no operation in Git that edits an existing object.
This is the source of the "integrity" we listed among Git's virtues in lesson 01-01. It is not a promise: it is a mathematical property of the design.
So what does "rewriting history" mean?
If nothing can be modified, how is it possible to fix a commit message or reorder the history — things Git clearly allows?
The answer is that nothing is modified: new objects are created and the references are moved.
When you say "I'm going to fix the last commit's message", what happens is:
- Git creates a new commit, with the same tree and the same parent, but with the corrected message. It has a different hash.
- Git moves the
mainbranch so that it points at the new commit. - The old commit still exists in the database. There is simply no longer any reference that reaches it.
graph RL
subgraph "Before"
A_MAIN["main"] -.-> A_C2["c2 'Ad task deltion'"]
A_C2 --> A_C1["c1"]
end
subgraph "After"
B_MAIN["main"] -.-> B_C2N["c2' 'Add task deletion'"]
B_C2N --> B_C1["c1"]
B_C2["c2 (dangling)"] --> B_C1
end
Objects with no references pointing at them are called dangling (unreachable). They stay in the database and remain accessible by their hash or through the reflog — the record of every movement of every reference, which lives in .git/logs/ — until garbage collection (git gc) removes them, usually after a couple of weeks.
This has two capital implications:
- Almost nothing is truly lost. If you think you have destroyed committed work, the odds are it is still there. Module 9 devotes a whole lesson to getting it back.
- Rewriting shared history is dangerous. If Ana rewrites commits Bruno already has in his repository, Bruno's hashes stop matching Ana's: their histories diverge and reconciling them is painful. Hence the golden rule we will repeat in module 5:
Do not rewrite history you have already shared with others.
The operations that rewrite history — rebase, commit --amend, reset over published commits, filter-repo — are covered in modules 5, 8 and 9. What you need now is to understand why they differ from the rest: they create new objects and abandon the old ones, instead of adding at the end.
- Why this model explains everything
Let us close with the practical value of all this. These frequently asked questions answer themselves once you know the model:
| Common question | The model's answer |
|---|---|
| Why is creating a branch instantaneous? | It writes 41 bytes into refs/heads/; nothing is copied |
| Why is switching branches so fast? | HEAD is rewritten and the working directory is adjusted to the matching tree |
| Why can't I commit an empty directory? | Trees only contain entries; with no blobs there is nothing to record |
| Why doesn't Git store a file's full permissions? | The tree only distinguishes executable, non-executable and symbolic link |
| Why does renaming a file take no space? | The blob is reused; only the tree entry changes |
| Why does the repository grow with images and video? | Every version of a binary generates a new blob that compresses badly |
| Why do all the hashes change when I rebase? | New commits are created with different parents |
| Why can I recover a deleted branch? | The commits are still in objects/; the reflog keeps their hash |
| Why do two people with the same hash have the same code? | The hash covers the entire tree and the whole chain of parents |
Whenever some Git behaviour strikes you as odd in the coming modules, come back to this model and ask yourself: which objects are being created, and which references are being moved? The answer almost always turns up on its own.
Common Mistakes and Tips
- Believing Git stores differences. This is the most widespread conceptual error and it comes from experience with other systems. Git stores snapshots and computes differences when you ask for them. The differences you see in
git log -por in a code review are not stored anywhere. - Thinking the blob holds the file name. It does not. The name lives in the tree. That is why Git does not record renames explicitly: it detects them by comparing contents between two trees.
- Poking around inside
.git/by hand. Exploring it withlsand readingHEADorrefs/heads/mainis instructive and safe. Editing or deleting files in there is an efficient way to corrupt the repository. For everything else, there are commands. - Trusting dangling objects to last forever. They survive the rewrite, but
git gcremoves them after a while (by default, 30 days for recent unreachable objects and 90 for the reflog). If you need to recover something, do it soon. - Assuming an abbreviated hash is unique forever. Seven characters are enough in your repository today; in a repository with hundreds of thousands of commits they can turn ambiguous. When you record a hash in documentation, use the full form.
- Tip: run this experiment once. In any repository, follow the
git cat-file -p HEAD→ tree → blob walk from section 6. Ten minutes of manual exploration save months of confusion. - Tip: when something goes wrong, think in references. The overwhelming majority of Git scares are not data loss but references pointing somewhere unexpected. The objects are almost always still there.
Exercises
Exercise 1: Predicting hashes and objects
Ana creates a repository with index.html, styles.css, app.js and README.md, and makes a first commit. She then modifies only styles.css and makes a second commit. Finally, she copies styles.css to styles-old.css without changing the content and makes a third commit.
Answer:
- How many distinct blob objects are in the database at the end? List them.
- How many tree objects have been created in total?
- How many commit objects are there?
- Was any new blob created in the third commit? Why?
Exercise 2: Hands-on exploration
Using any Git repository you have to hand (if you do not have one yet, save this exercise for after module 2), run the complete exploration and note the results:
- Get the hash of the current commit.
- Show that commit raw and identify the hash of its tree and of its parent.
- Show the tree and identify the hash of one of its files.
- Show that file's content straight from the object database.
- Check that the tree's type is
treeand the file's isblob. - Find out that blob's size in bytes.
Exercise 3: Reasoning about rewriting
Ana has made three commits on main: c1, c2 and c3. She has pushed all three to the shared repository and Bruno already has them on his machine. Ana now notices a spelling mistake in c2's message and fixes it by rewriting the history.
Answer:
- Which new objects are created and which are left dangling?
- Does
c1's hash change? What aboutc2's tree? Justify your answer. - What will Bruno see when he tries to synchronise?
- Would it have been different if Ana had not pushed anything yet?
Solutions
Solution to Exercise 1
-
Five blobs. One for each distinct content that has existed:
index.html(never changes): 1 blobapp.js(never changes): 1 blobREADME.md(never changes): 1 blobstyles.cssoriginal version: 1 blobstyles.cssmodified version: 1 blob
Total: 5. The content is the key, not the file.
-
Three trees. One per commit. The root tree changes in all three because the second changes the hash of
styles.cssand the third adds a new entry (styles-old.css). Since the project has no subdirectories, there is only a root tree. -
Three commits, one per commit operation.
-
No new blob was created in the third commit. The content of
styles-old.cssis identical to that ofstyles.css, and Git stores content indexed by its hash: it already exists. The only new thing is a tree entry with a different name pointing at the same blob. This is the canonical example of content deduplication.
Solution to Exercise 2
# 1. Hash of the current commit
git rev-parse HEAD
# → 8c4f2a1e9b7d3f5a6c8e0b2d4f6a8c0e2b4d6f8a
# 2. The commit raw
git cat-file -p HEAD
# → tree 3f7a9c1e5b8d2f4a6c9e1b3d5f7a9c1e3b5d7f9a
# parent 1e5b8d2f4a6c9e1b3d5f7a9c1e3b5d7f9a1c3e5b
# author ...
# committer ...
#
# The commit message
# 3. The tree
git cat-file -p 3f7a9c1
# → 100644 blob 9c1e3b5... README.md
# 100644 blob 5f7a9c1... app.js
# ...
# 4. The file's content
git cat-file -p 5f7a9c1
# → (the source code)
# Equivalent shortcut with no hash hunting:
git cat-file -p HEAD:app.js
# 5. Types
git cat-file -t 3f7a9c1 # → tree
git cat-file -t 5f7a9c1 # → blob
# 6. Size
git cat-file -s 5f7a9c1 # → 412 (for example)Solution to Exercise 3
-
A new commit is created, call it
c2', with the same tree and the same parent (c1) but a different message, and therefore a different hash. Sincec3pointed atc2, ac3'pointing atc2'has to be created as well. The originalc2andc3are left dangling. The trees and the blobs do not change: they are reused whole, because the files' content was never touched. -
c1does not change: its hash depends on its own content and on its parent, and neither has been modified.c2's tree does not change either: the message is part of the commit object, not of the tree. It is a good reminder that the four object types are independent of one another and only linked by hash. -
Bruno will see diverged histories. His local
mainpoints atc3, while the shared branch points atc3'. Git will tell him the branches have diverged and that each side has commits the other lacks. If he tries to integrate carelessly, he will end up with duplicated work:c2,c3,c2'andc3'in the same history. Fixing it requires Bruno to align his branch with the rewritten one, which module 9 covers. -
Yes, it would have been completely different. If the commits only existed on Ana's machine, rewriting them would have no consequences at all: nobody else holds the old hashes, so no divergence is possible. This is exactly where the golden rule draws its line: rewriting local history is safe and advisable; rewriting shared history is dangerous.
Conclusion
Git is, at bottom, a key-value database where the key is the hash of the content. On that minimal idea stand four object types: the blob stores a file's content, the tree represents a directory and gives the blobs their names, the commit points at a root tree and at its parents while adding author, date and message, and the annotated tag marks a point in the history with metadata. Linked by hash, they form a directed acyclic graph on which branches and HEAD are nothing but names pointing at nodes.
That design leads directly to the properties that make Git what it is: deduplication (identical content is stored once), verifiable integrity (the hash betrays any corruption) and immutability (changing the past invalidates the entire chain that follows). And immutability gives "rewriting history" its exact meaning: nothing is modified, new objects are created and the old ones are abandoned, surviving as dangling objects until garbage collection. That is the basis of the golden rule — do not rewrite what you have already shared — and also of the good news that almost nothing is truly lost.
You have also seen the distinction between porcelain and plumbing commands, and you have walked the graph by hand with git rev-parse, git cat-file and git hash-object.
With the mental model in place, we return to the surface. Before Ana can create the task-manager repository she needs to adjust how Git behaves on her machine. In Configuring Git we will look at the configuration mechanism: the three levels of git config, where each file lives, which value wins when they conflict and how to keep separate profiles for work and for personal projects.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
