Bachelors Program in Full Stack Development
Git, GitHub & Version Control
•
Bachelor
Git Architecture and Repository Model
Introduction
Git is often introduced through commands such as git add, git commit, and git push. Command knowledge is useful, but dependable software development requires a deeper mental model. Developers must understand where each version exists, what a commit records, how branches locate commits, and why apparently destructive operations may still be recoverable.
Git is a distributed version control system built around a content-addressed object database. Each clone normally contains the project’s history, object database, references, and configuration. Unlike systems whose primary abstraction is a sequence of file differences, Git models each commit as a project snapshot connected to one or more parent commits. Unchanged content is reused rather than copied unnecessarily, and storage optimizations may later represent objects as deltas without changing this logical model.
This architecture underpins branching, merging, code review, release management, forensic recovery, and collaboration through hosting platforms such as GitHub.
Learning Objectives
After completing this chapter, students should be able to:
Distinguish a repository, working tree, staging area, and commit.
Explain Git’s snapshot-based and content-addressed data model.
Describe blob, tree, commit, and annotated tag objects.
Interpret object identifiers, references, branches, tags, and HEAD.
Model commit history as a directed acyclic graph.
Explain staged, modified, tracked, and untracked file states.
Use plumbing and porcelain commands to inspect repository internals.
Distinguish Git’s logical snapshot model from physical delta compression.
Apply repository architecture when diagnosing or recovering development work.
1. The Repository and Its Associated Workspaces
A Git repository is not simply a directory containing source files. Strictly understood, it consists primarily of an object database, references, and administrative metadata. A conventional non-bare repository combines this metadata with a checked-out working tree.
1.1 Working tree
The working tree is the developer-visible set of checked-out files. Editors, compilers, test runners, and development servers normally operate on these files. Changes made here are not automatically part of a commit.
A file in the working tree may be:
untracked—not represented in the index;
tracked and unmodified—matching the indexed version;
tracked and modified—different from the indexed version;
deleted—expected by the index but absent from the working tree.
A repository can also have additional linked working trees created with git worktree. Therefore, the conceptual relationship is not always one repository to one working tree.
1.2 Staging area or index
The index, commonly called the staging area, is a binary data structure representing the proposed content of the next commit. It records path names, object identifiers, file modes, and caching information used to compare files efficiently.
The index is not merely a list of changed files. It is a complete proposed snapshot. Running:
git add src/app.js
creates or reuses a blob for the current content of src/app.js and updates the index entry for that path. Further working-tree edits do not alter the staged blob until git add is run again.
During an unresolved merge, the index can temporarily hold multiple entries for one path: a common-ancestor version, an “ours” version, and a “theirs” version. This demonstrates that the index is an active architectural component, not a superficial waiting area.
1.3 Repository database
The .git directory of a typical non-bare repository contains administrative data such as:
Component
Purpose
objects/
Stores loose objects and packfiles
refs/
Stores branch and tag references when not packed
HEAD
Identifies the currently checked-out branch or commit
index
Stores the staging-area representation
config
Holds repository-specific configuration
logs/
Commonly stores reference logs, or reflogs
hooks/
Contains optional event-triggered scripts
The exact physical representation may vary. For example, references can be consolidated in packed-refs, and objects can be stored in packfiles rather than as individual files. Programs should use Git commands rather than assuming every logical element has one fixed on-disk form.
1.4 Bare repositories
A bare repository has no ordinary working tree. Its repository metadata appears at the top level rather than inside a .git subdirectory. Central repositories used as collaboration endpoints are usually bare because users do not edit checked-out files there.
Git working tree, index, object database, and references
Figure: Git working tree, index, object database, and references
Figure: The principal repository states and the operations that transfer or locate content.
2. Three Comparisons Behind Everyday Git Status
A useful model of git status is based on two principal comparisons:
HEAD versus index: identifies changes staged for the next commit.
Index versus working tree: identifies tracked changes not yet staged.
Untracked files require an additional determination: Git finds working-tree paths that are neither indexed nor excluded by applicable ignore rules.
Suppose the latest commit contains app.js with content V1. A developer edits it to V2 and runs git add app.js. The index now records V2. If the developer then edits the file to V3, the three states are:
Location
Version
HEAD commit
V1
Index
V2
Working tree
V3
A subsequent git commit records V2, not V3. V3 remains an unstaged working-tree modification. This behavior enables precise commits but can surprise developers who incorrectly imagine staging as placing a filename on a checklist.
Useful inspection commands include:
git diff # working tree versus index
git diff --staged # index versus HEAD
git status # summarizes both comparisons
git show HEAD:path # shows a path from the HEAD commit
For an unborn branch with no first commit, HEAD may name the intended branch even though no commit is yet available for comparison.
3. Content-Addressed Storage
Git stores persistent history as objects. Each object receives an object identifier (OID) derived from its type, byte length, and content.
Conceptually:
OID = H(type ∥ " " ∥ size ∥ NUL ∥ content)
Here, H is the repository’s configured cryptographic hash function, type is an ASCII object type such as blob, size is the content length in bytes, NUL is one zero byte, content is the exact byte sequence, and ∥ denotes byte concatenation. This is a definition of Git object naming, not a guarantee that hashes are mathematically collision-free.
Traditional repositories use SHA-1 object identifiers, commonly displayed as 40 hexadecimal characters. Git also supports repositories using SHA-256, displayed using 64 hexadecimal characters. The hash algorithm is a repository-format property; developers should not hard-code assumptions about identifier length.
3.1 Properties of content addressing
Content addressing has several consequences:
Identical blob content produces the same blob OID within the same object format.
Changing even one byte normally produces a different OID.
An object can be checked against its identifier to detect accidental corruption.
Parent identifiers participate in commit content, so changing an ancestor changes the identifiers of descendant commits recreated from it.
Objects can be shared by multiple commits and paths.
Hash identifiers are not sequential version numbers. They should also not be treated as digital signatures. A hash helps identify content, whereas authenticity requires trusted signatures, controlled references, or other provenance mechanisms.
Abbreviated OIDs are accepted only when sufficiently long to be unique in the repository’s current object set. Their required length can grow as the repository accumulates objects.
4. Git’s Core Object Types
Git has four principal persistent object types: blobs, trees, commits, and annotated tags.
4.1 Blob objects
A blob stores file content as bytes. It does not inherently store:
the filename;
the containing directory;
the author;
the commit message;
ordinary timestamps.
Consequently, two files with identical content can reference the same blob. A changed filename alone does not require a changed blob.
4.2 Tree objects
A tree represents one directory level. Each tree entry includes a name, an object identifier, and a mode/type indicator. An entry may identify a blob, another tree, or, for a submodule entry, a commit in another repository.
Trees provide path structure. A root tree can refer to blobs and nested trees, thereby representing the complete project snapshot for a commit. Git records a limited set of file-mode distinctions relevant to version control; it is not a general archive of every filesystem permission or metadata attribute.
4.3 Commit objects
A commit normally contains:
the identifier of one root tree;
zero or more parent commit identifiers;
author identity and author timestamp;
committer identity and committer timestamp;
a commit message;
optional additional headers, such as signature-related data.
The first commit has no parent. A normal subsequent commit usually has one parent. A merge commit has two or more parents. Because all these fields contribute to the commit’s content, changing metadata or a message produces a different commit OID even when the root tree is unchanged.
The author is the person represented as having created the change. The committer is the person or process that created this commit object. These can differ when patches are applied, rebased, or integrated by another developer.
4.4 Annotated tag objects
An annotated tag object can contain a target object identifier, target type, tag name, tagger information, message, and an optional cryptographic signature. A lightweight tag, by contrast, is only a reference and does not create a tag object.
4.5 Inspecting objects
Git’s lower-level, or plumbing, commands expose the model:
git hash-object path/to/file
git cat-file -t <oid>
git cat-file -p <oid>
git ls-tree HEAD
git rev-parse HEAD
git write-tree
Commands intended primarily for routine user workflows, such as git status and git commit, are commonly called porcelain commands.
5. Snapshots Rather Than a Primary Chain of Diffs
A file-difference model is often imagined as an original file followed by patches:
Version 1 + difference 2 + difference 3 → Version 3
Git’s logical model is different. Every commit points to a root tree that describes a complete project snapshot. If a file remains unchanged, the new tree can reuse its existing blob identifier. Nested trees may also be reused when their entire subdirectories remain unchanged.
Consider this symbolic history:
Commit
README.md blob
src/app.js blob
C1
R1
A1
C2
R1
A2
C3
R1
A2
C2 changes only src/app.js, so it reuses R1. If C3 changes only commit metadata or another path not shown, both displayed blobs can be reused. Each commit still denotes a complete snapshot through its root tree.
Git can compute a diff between any two snapshots. For example, git diff C1 C3 derives differences from the selected trees; it does not require C3 to store “the patch from C2.” This supports comparisons across branches and nonadjacent commits.
Logical model versus physical optimization
Packed Git repositories may delta-compress similar objects to reduce storage and network transfer. One packed object can be represented relative to another selected base object. This does not turn the logical commit model into a patch history:
commits still identify trees;
trees still identify objects;
object identity is based on reconstructed canonical content;
the delta base need not be the previous version in commit history;
repacking can choose a different physical representation without changing any OID.
Therefore, “Git stores snapshots” and “Git uses delta compression” are compatible statements about different architectural layers.
6. References, Branches, Tags, and HEAD
Object identifiers are stable but inconvenient for humans. Git therefore uses references, or refs, as names that resolve to object identifiers.
Common namespaces include:
Reference
Typical meaning
refs/heads/main
Local branch named main
refs/tags/v2.0.0
Tag named v2.0.0
refs/remotes/origin/main
Local remote-tracking reference
6.1 Branches
A branch is a movable reference that normally identifies the latest commit in one line of development. Creating a branch is inexpensive because Git generally creates another reference rather than copying project files or commit history.
When a new commit is created on the current branch, Git creates the object first and then advances the branch reference to the new commit. Reference updates are managed carefully so readers do not observe a partially written identifier.
6.2 Tags
Tags usually identify releases or other important points. A lightweight tag is a direct ref. An annotated tag points to a tag object, which then points to its target. Unlike branches, tags are conventionally not moved after publication.
6.3 Symbolic and detached HEAD
HEAD identifies the current checkout context. Normally it is a symbolic reference such as:
ref: refs/heads/main
In this state, committing advances main. In detached HEAD state, HEAD directly identifies a commit rather than a local branch. New commits can still be created, but no branch automatically preserves them. Before switching away, the developer should create a branch or tag if the work must remain conveniently reachable.
6.4 Remote-tracking references
A name such as origin/main is a local record of the remote branch state observed during the most recent fetch or related network operation. It is not a live pointer into the remote server. A local main branch and origin/main are distinct references.
6.5 Reflogs
A reflog records recent local updates to references such as branch tips and HEAD. Reflogs can help recover commits after an accidental reset or branch movement. They are not ordinary shared project history, may expire, and are not a substitute for branches, tags, backups, or remote publication.
7. Commit History as a Directed Acyclic Graph
A commit history can be modeled as a directed graph G = (V,E), where V is the set of commits and E contains an edge from each commit to each parent commit. Under this convention, edges point backward through history.
The graph is directed because parent relationships have a defined direction. It is acyclic because a commit is constructed using parent identifiers and cannot ordinarily become its own ancestor. It is not necessarily a simple line because development can branch and merge.
Directed acyclic commit graph with merge commit M pointing to parents C and D
Figure: Directed acyclic commit graph with merge commit M pointing to parents C and D
Figure: Commit M is a merge commit with exactly two parent edges, M→C and M→D; C and D both descend from B, while B descends from A.
In the figure, the parent sets are:
parents(A) = ∅
parents(B) = {A}
parents(C) = {B}
parents(D) = {B}
parents(M) = {C, D}
A commit is reachable from a reference if Git can begin at the referenced object and repeatedly follow object links to reach it. The commits reachable from M are M, C, D, B, and A. B and A are shared ancestors rather than duplicated history.
7.1 Merge and rebase
A merge normally preserves both lines of ancestry by creating a commit with multiple parents. A rebase instead creates new commits whose patches or changes are reapplied onto another base. Because the new commits have different parent relationships—and usually different metadata—their OIDs differ from the original commits. Rebase is therefore history reconstruction, not merely branch-label movement.
7.2 Reachability and garbage collection
Objects reachable from references are considered part of preserved history. Unreachable objects may remain temporarily, especially while reflogs still protect recent states. Eventually, maintenance commands may prune sufficiently old unreachable objects. This explains both Git’s short-term recoverability and its limits.
8. Worked Repository Trace
Assume main points to commit C1, whose snapshot contains:
README.md → blob R1
app.js → blob A1
A developer changes app.js and creates test.js.
Step 1: Before staging
HEAD and the index still describe C1.
The working tree contains changed app.js content A2.
test.js is untracked.
Step 2: Stage only app.js
git add app.js
Git stores or reuses blob A2 and updates the index. README.md remains associated with R1. test.js remains untracked.
Step 3: Commit
git commit -m "Refactor application startup"
Conceptually, Git:
writes trees corresponding to the index;
creates commit C2 referencing the new root tree;
records C1 as C2’s parent;
advances refs/heads/main from C1 to C2.
C2 contains the snapshot with R1 and A2. It does not include test.js because that path was not in the index.
Step 4: Consequence
The working tree can contain work that is absent from the latest commit. A successful commit therefore does not mean “everything visible in the project directory was saved.” Developers should inspect git status and the staged diff before committing.
9. Applied Case: Recovering a Detached-HEAD Feature
Illustrative case
Background
A full-stack team maintains an e-commerce application. Developer Maya checks out an earlier release commit to investigate a production defect. This places the repository in detached-HEAD state. She creates two useful commits implementing a fix, then switches to main without creating a branch.
Problem
The files disappear from the working tree after the switch, and main does not contain the two commits. Maya assumes Git deleted the work.
Analysis
The commits were valid objects, but no branch reference named their latest tip. Switching branches moved HEAD and changed the working tree; it did not immediately erase the detached commits. The previous HEAD movements are likely recorded in the local reflog.
Approach and solution
Maya inspects recent HEAD states:
git reflog
After identifying the OID of the latest detached commit, she creates a preserving branch:
The new branch makes the commit and its ancestors reachable by a durable named reference. She reviews the diff, runs tests, publishes the branch, and opens a pull request.
Outcome
The work is recovered without reconstructing files manually. The pull request records review and integration decisions, while the local reflog served only as a recovery mechanism.
Learning points
A detached HEAD permits commits but does not automatically create a branch.
Disappearing from the working tree does not prove immediate object deletion.
Reflogs provide local, time-limited recovery evidence.
Important work should be anchored by a branch or tag and backed up through an approved remote.
Understanding reachability is more reliable than memorizing recovery commands in isolation.
10. Integrity, Security, and Governance
Content-addressed storage improves integrity checking but does not independently establish who created or approved a change. An attacker with write access to references may redirect a branch to different valid objects. Teams therefore combine Git’s data model with governance controls such as:
protected branches and mandatory reviews;
signed commits or signed annotated tags where provenance warrants them;
least-privilege repository access;
continuous integration checks;
immutable release records and deployment attestations;
backup and retention policies;
secret scanning and dependency analysis.
Deleting a secret in a later commit does not remove it from earlier reachable snapshots. Remediation usually requires revoking the credential, rewriting affected history where appropriate, coordinating with collaborators, and cleaning backups or caches according to policy. History rewriting also changes descendant commit identifiers and can disrupt shared work.
AI coding assistants may generate source changes, commit messages, or review suggestions, but they do not alter Git’s architecture. Teams should inspect the staged snapshot, test generated code, avoid exposing confidential repository data, and preserve human accountability for commits and approvals.
11. Best Practices and Common Misconceptions
Best practices
Review git status and git diff --staged before each commit.
Create focused commits whose snapshot and message express one coherent change.
Use branches or tags to preserve important commits.
Avoid depending on reflogs as long-term storage.
Treat published history as shared data; coordinate before rebasing or force-updating it.
Use full object identifiers in high-assurance automation where ambiguity must be excluded.
Use Git commands rather than directly editing .git internals.
Never assume a later deletion removes sensitive content from history.
Common misconceptions
Misconception
Correct interpretation
Staging saves a filename for later.
The index records selected content for a proposed snapshot.
A commit contains only changed lines.
A commit points to a complete snapshot represented by a root tree.
A branch contains separate copies of files.
A branch is primarily a movable reference to a commit.
origin/main is the live remote branch.
It is a locally stored remote-tracking reference.
A hash proves authorship.
An OID identifies content; provenance requires additional controls.
A detached HEAD prevents commits.
Commits are allowed, but no local branch advances automatically.
Rebase moves existing commits unchanged.
Rebase generally creates new commits with new ancestry and OIDs.
Packed deltas mean Git is logically patch-based.
Delta compression is a storage optimization beneath the snapshot model.
Chapter Summary
Git combines a working tree, an index, an object database, and named references. The working tree contains editable files, while the index represents the proposed next snapshot. A commit records a root tree, parent relationships, identities, timestamps, and a message. Blobs store content, trees supply path structure, and annotated tag objects add release-oriented metadata.
Objects are content-addressed using cryptographic hashes, although hashes alone do not prove authorship. Branches and tags provide human-readable access to objects, while HEAD identifies the current checkout context. Commit parent links form a directed acyclic graph, enabling divergent development and merges without duplicating shared ancestry.
Git’s logical data model is snapshot-based. Physical packfiles may delta-compress objects, but this optimization does not change commit semantics. A sound understanding of snapshots, references, reachability, and reflogs enables precise commits, safe collaboration, incident recovery, and effective repository governance.
Key Terms
Repository: The object database, references, configuration, and administrative metadata managed by Git.
Working tree: The checked-out files available for editing and execution.
Index: The data structure representing the proposed content of the next commit.
Staging area: The user-facing name commonly given to the index.
Object database: Storage containing Git’s persistent blob, tree, commit, and annotated tag objects.
Object identifier (OID): A hash-derived identifier for a Git object.
Blob: An object that stores file content without its pathname.
Tree: An object representing directory entries and their names, modes, and object identifiers.
Commit: An object recording a root tree, parents, identities, timestamps, and a message.
Annotated tag: A tag object containing metadata and optionally a cryptographic signature.
Reference: A name that resolves, directly or symbolically, to an object identifier.
Branch: A movable reference normally identifying the tip of a line of development.
HEAD: The reference or commit identifying the current checkout context.
Detached HEAD: A state in which HEAD directly identifies a commit instead of a local branch.
Remote-tracking reference: A local record of a remote reference observed during fetch or a related operation.
Reflog: A local record of recent reference updates.
Snapshot: A complete project state represented through a commit’s root tree.
Directed acyclic graph: A directed graph with no directed cycles; Git commit edges point from commits to parents.
Reachability: The property of being discoverable by following object links from a reference or other root.
Packfile: A compact storage format that may use delta compression while preserving logical object identities.
Review Questions
Short Answer Questions
What information does the Git index represent?
Why does a blob object not determine a file’s pathname?
What is the difference between a local branch and a remote-tracking reference?
What happens to HEAD when Git enters detached-HEAD state?
Why is an object identifier not equivalent to a digital signature?
Long Answer Questions
Explain how the working tree, index, object database, and references participate in creating a commit.
Compare Git’s snapshot model with a conceptual file-difference model, including the role of packfile delta compression.
Describe the contents and relationships of blob, tree, commit, and annotated tag objects.
Explain how branches, tags, HEAD, and reflogs provide different forms of access to repository history.
Discuss the integrity, security, and governance implications of content-addressed version control.
Analytical / Application Questions
A file is edited, staged, and then edited again before committing. Analyze which version is committed and identify the relevant comparisons shown by git status and git diff.
Given commits with parents(B)={A}, parents(C)={B}, parents(D)={B}, and parents(M)={C,D}, determine all commits reachable from M and explain why the graph contains no duplicated copy of B.
A developer loses sight of two commits made in detached-HEAD state. Design a recovery procedure and explain it in terms of references, reflogs, and reachability.
Multiple Choice Questions with Answers
Question 1
Which Git component represents the proposed content of the next commit?
A. Working tree
B. Index
C. Reflog
D. Remote-tracking reference
Correct Answer: B. Index
Question 2
A developer stages version V2 of a file, edits the working-tree file to V3, and immediately commits without staging again. Which version is committed?
A. V1 from the previous commit
B. A merge of V2 and V3
C. V2 from the index
D. V3 from the working tree
Correct Answer: C. V2 from the index
Question 3
Which Git object stores file content without storing the filename?
A. Blob
B. Tree
C. Commit
D. Annotated tag
Correct Answer: A. Blob
Question 4
Which item is normally included in a commit object?
A. The complete .git/config file
B. A root tree object identifier
C. The current working-tree process list
D. Every reflog entry
Correct Answer: B. A root tree object identifier
Question 5
In a graph where edges point from each commit to its parents, M has edges M→C and M→D. What does this establish?
A. M is a root commit
B. C is a tag and D is a branch
C. M has exactly the two stated parent commits
D. C and D were created after M
Correct Answer: C. M has exactly the two stated parent commits
Question 6
What does HEAD normally contain when a local branch named main is checked out?
A. A symbolic reference to refs/heads/main
B. A copy of every object reachable from main
C. A live connection to the hosting server
D. The staged root tree only
Correct Answer: A. A symbolic reference to refs/heads/main
Question 7
Which statement correctly distinguishes Git’s logical and physical storage models?
A. Commits logically store only line-by-line patches
B. Packfile deltas replace all trees in repository history
C. Snapshots are logical, while delta compression may optimize physical storage
D. Delta compression changes object identifiers each time Git repacks
Correct Answer: C. Snapshots are logical, while delta compression may optimize physical storage
Question 8
What is origin/main in a typical local repository?
A. The local main branch under another spelling
B. A live pointer that always matches the remote server
C. A tag created automatically for each push
D. A local remote-tracking reference updated by fetch-related operations
Correct Answer: D. A local remote-tracking reference updated by fetch-related operations
Question 9
Why can a reflog help recover a commit after an accidental branch reset?
A. It records recent local reference updates
B. It permanently duplicates every working-tree file
C. It automatically publishes unreachable commits
D. It prevents all objects from ever being pruned
Correct Answer: A. It records recent local reference updates
Question 10
Which operation generally creates new commit objects because parent relationships and metadata change?