Version Control with Git and GitHub/GitLab

Chapter 2
Git Architecture and Repository Model

Diploma in DevOps
Version Control with Git and GitHub/GitLab   •   Diploma

Git Architecture and Repository Model

Introduction

Git is a distributed version control system designed to record the evolution of files and coordinate changes among developers. Its power comes from a relatively small set of architectural ideas: a repository stores objects and references, a working tree contains files being edited, a staging area records the next proposed snapshot, and commits connect project states into a directed acyclic graph (DAG).

Understanding this model is more important than memorising commands. Commands such as add, commit, branch, merge, tag, reset, and checkout are operations on these underlying structures. A developer who understands the structures can predict what a command will do, diagnose problems, and work safely in collaborative DevOps environments.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish a working tree, staging area, local repository, and remote repository;
  • explain how blobs, trees, commits, and annotated tags are stored;
  • describe snapshots and the relationship between commits;
  • explain references, branches, symbolic references, and HEAD;
  • interpret Git’s directed acyclic graph model;
  • use hashes and inspection commands to investigate repository state;
  • apply the model to branching, merging, tagging, and recovery scenarios.

1. The Four Places in a Git Workflow

A normal Git workflow involves four logically distinct locations.

LocationPurposeTypical operation
Working treeFiles currently checked out on diskEdit files, run tests
Staging area (index)Proposed contents of the next commitgit add, git restore --staged
Local repositoryPermanent object database and referencesgit commit, git branch
Remote repositoryRepository hosted elsewhere for collaborationgit push, git fetch

The working tree is not automatically the next commit. After a file is edited, its new contents exist only in the working tree. git add copies the selected contents into the index. git commit then creates a commit from the index, not directly from every current file on disk.

This explains a common situation: a file can be both staged and modified. The staged version is the version selected for the next commit, while the working-tree version contains later edits. A subsequent git add updates the staged version.

The local repository is normally stored in the .git directory. It contains objects, references, configuration, logs, and the index. The working tree is normally the directory surrounding .git; in some configurations, Git can use separate working trees.

Git workflow and repository locations Working treeedited files Indexnext snapshot Local repositoryobjects and refs Remoteshared copy add commit push restore / checkout
Git workflow from working tree through staging area to local and remote repositories
Figure: Git workflow from working tree through staging area to local and remote repositories

Figure: The principal locations through which changes move in a Git workflow.

2. Repositories and the Object Database

A Git repository is an object database plus references and supporting metadata. Git stores content in objects identified by cryptographic object names. In current Git installations, SHA-1 is still common, while repositories can also be configured for SHA-256 object formats. The important architectural principle is that an object’s identifier is derived from its type and contents; it is not simply a sequential record number.

A hash is commonly written as a hexadecimal string. For a repository using SHA-1, the object identifier is 160 bits, conventionally displayed as 40 hexadecimal characters. A shortened hash may identify an object only when it is unambiguous within the repository. Hashes provide content addressing and help detect accidental corruption, but they should not be treated as an absolute guarantee against deliberate modern cryptographic attacks.

Git’s principal object types are:

ObjectRole
BlobStores the contents of a file, without its filename or directory location
TreeRepresents a directory-like listing of names, modes, and references to blobs or subtrees
CommitPoints to a root tree, parent commit(s), and metadata such as author, committer, and message
Tag objectAn optional annotated object pointing to another object, with a name, message, and tagger

Blobs

A blob stores bytes. It does not know whether those bytes came from README.md, src/main.py, or another path. Consequently, two files with identical contents can refer to the same blob. Renaming a file without changing its contents generally does not create a new blob; the tree changes because the name changes.

Trees

A tree maps directory entries to object identifiers. Each entry includes a name, a mode describing the entry type and permissions relevant to Git, and an object identifier. A root tree can point to blobs for files and to other trees for subdirectories. Thus a complete project state is represented by a tree hierarchy.

Commits and snapshots

A commit points to one root tree. That tree, recursively, describes the complete tracked state at the commit. This is why Git is often described as snapshot-based. A commit is not merely a list of line-by-line changes, although Git can calculate and display differences between snapshots.

A commit also records zero or more parent commits. Ordinary commits have one parent; the first commit has none; a merge commit commonly has two or more. Commit metadata includes author identity and time, committer identity and time, and a message. The author may be the person who originally wrote the change, while the committer may be the person who applied or merged it.

A conceptual commit can be represented as:

Commit = (tree, parent(s), author, committer, message)

This is a structural description rather than a command or numerical equation. A commit’s object identifier is calculated from its complete serialized representation, including its referenced tree and parent identifiers. Therefore changing a file, parent, message, or relevant metadata produces a different commit identifier.

3. Snapshots, Diffs, and Rename Detection

Suppose commit C2 has a root tree describing files app.py and README.md. Commit C3 has a new root tree in which app.py has additional lines. The unchanged README can continue to refer to the same blob. Git therefore stores shared objects efficiently rather than copying every file into every commit.

A difference between commits is computed by comparing their trees and, where necessary, blob contents. A displayed patch is a derived view, not the fundamental storage model. Git can infer that a deletion and an addition are probably a rename when similarity is sufficiently high, but rename detection is normally a comparison heuristic. Rename history is not stored as an intrinsic property of a blob.

This distinction matters operationally. A commit is a complete project state, while a diff answers a question about two states. Commands such as git show and git diff expose derived comparisons; they do not imply that Git stores commits as ordinary patch files.

4. References, Branches, Tags, and HEAD

Object identifiers are inconvenient to use directly. A reference, or ref, is a human-readable name that resolves to an object identifier.

A branch is ordinarily a movable reference to a commit. If refs/heads/main points to commit C, creating a new commit on that branch advances the reference to the new commit. The older commit remains reachable through its parent relationship. A branch is therefore not a container holding a separate copy of files; it is a name pointing into the commit graph.

A tag is normally intended to identify a significant point, such as a release. A lightweight tag is simply a reference to an object, often a commit. An annotated tag is a tag object containing a target, tagger, message, and optionally a signature. Annotated tags are generally preferable for published releases because they carry explicit release metadata.

HEAD identifies the currently selected position. In the usual case, HEAD is a symbolic reference to a branch, such as refs/heads/main. The branch then points to a commit. This is called an attached HEAD.

In detached-HEAD state, HEAD points directly to a commit rather than to a branch. This is useful for inspecting or testing an historical state. New commits made there are not automatically named by a branch; if they matter, create a branch or tag before the commits become difficult to locate.

Useful inspection commands include:

git status
git log --oneline --decorate --graph --all
git show <commit>
git cat-file -t <object>
git cat-file -p <object>
git rev-parse HEAD
git ls-tree -r HEAD

git cat-file -t reports an object type, while git cat-file -p presents its content in a human-readable form. git rev-parse resolves names such as HEAD, main, or HEAD~2 to identifiers.

5. The Commit Graph as a Directed Acyclic Graph

Git commits form a directed acyclic graph. Each edge points from a child commit to a parent commit. “Directed” means the relationship has a direction. “Acyclic” means following parent links cannot eventually return to the same commit. New commits refer to existing parents, so ordinary commit creation moves backward through history and cannot create a cycle.

A simplified history may look like this:

A---B---C          main
     \
      D---E        feature

Here, B is a common ancestor of C and E. The branches are names pointing to C and E; the letters represent commits. The graph remains one repository history even though two lines of development exist.

A merge may create:

A---B---C---------M    main
     \             /
      D---E-------      feature

M has two parents, C and E. A fast-forward update is different: if main points to an ancestor of E, Git can move main directly to E without creating a merge commit.

Graph concepts support several practical operations:

  • Reachability: a commit is reachable if it can be found by following references and parent links.
  • Common ancestor: a commit reachable from two tips; merge algorithms use suitable common ancestors.
  • Ancestor notation: HEAD~2 follows first-parent links twice, whereas HEAD^2 selects the second parent of a merge commit.
  • Garbage collection: unreachable objects may eventually be pruned, subject to reflogs and repository maintenance policies.

The DAG model explains why deleting a branch does not immediately delete its commits. Other references, reflogs, tags, or remote-tracking references may still keep those objects reachable.

6. The Index as a Proposed Tree

The index is sometimes called the staging area, but “proposed tree” is a useful conceptual description. It contains entries representing paths and the blob identifiers that will be used when the next commit is created. It can also represent removals and selected versions of files.

When git add report.txt is run, Git takes the current contents of report.txt, creates or reuses a blob, and records that blob in the index. When git commit runs, Git constructs a tree from the index and creates a commit pointing to that tree. A commit therefore records exactly what was staged at that moment.

This design supports partial commits. A developer can stage only the completed part of a file, continue editing the remainder, and commit the staged portion separately. Interactive staging can help separate unrelated changes, improving review quality and reducing deployment risk.

The index also explains the three-way status report produced by git status: differences between HEAD and the index are staged changes; differences between the index and the working tree are unstaged changes; untracked files exist in the working tree but are not represented in the index.

7. Worked Example: Tracing a Small Repository

Assume a new repository contains README.md.

  1. git init creates repository metadata. There may be no commit yet.
  2. The developer writes README.md. The file exists in the working tree only.
  3. git add README.md creates or reuses a blob and records it in the index.
  4. git commit -m "Add README" creates a root tree, then a root commit with no parent. HEAD and the current branch point to that commit.
  5. The developer edits README.md. The working tree now differs from the index and HEAD.
  6. git add README.md updates the index to refer to the new blob.
  7. A second commit creates a new tree and commit whose parent is the first commit.

If the second version has one changed line but the rest is identical, Git may reuse blobs for unchanged files. The second commit is still a new snapshot because its root tree and commit metadata differ.

8. Applied Case: Recovering an Unintended Commit

Illustrative case. A DevOps team maintains an infrastructure repository. A developer commits a configuration file containing an incorrect deployment parameter, then notices that the working tree contains additional valid changes. The developer wants to understand the state before changing anything.

Analysis. git status distinguishes staged and unstaged work. git log --oneline --decorate -5 identifies the recent commits and branch position. git show HEAD displays the latest committed snapshot. git diff HEAD compares the complete working tree and index state with the current commit.

Approach. The developer first creates a safety reference with a temporary branch, then separates valid work using interactive staging. If the incorrect commit has not been shared, it may be revised with a history-editing operation according to team policy. If it has been pushed and other people may have based work on it, the safer approach is usually a new corrective commit rather than rewriting the shared branch. Sensitive credentials must be revoked and rotated; deleting the file in a later commit does not guarantee that the credential is absent from repository history.

Outcome. The team preserves a comprehensible graph, isolates the correction, and avoids silently changing history used by colleagues or automation.

Learning points. References are safety handles; commits are immutable objects in normal use; the index permits selective composition; and governance matters as much as technical recovery commands.

9. Professional Practice, Governance, and Limitations

Git records history, but it does not by itself guarantee that history is correct, reviewed, secure, or compliant. Teams should establish branch protection, required reviews, signed commits or tags where appropriate, secret scanning, backup procedures, and retention policies. Access controls should apply to both hosting platforms and local developer environments.

Never commit passwords, private keys, tokens, or regulated personal data. .gitignore prevents selected untracked files from being added conveniently, but it does not remove data already committed. Secret-removal procedures may require history rewriting, coordinated force updates, credential rotation, and communication with all repository users.

History rewriting changes commit identifiers and can disrupt collaborators. It is appropriate in controlled circumstances, such as removing accidentally committed secrets, but should be governed and communicated. A force push should use safer options such as --force-with-lease when policy permits, because it checks that the remote reference has not advanced unexpectedly.

Git’s content-addressed design also has limits. Hash collisions, repository compromise, malicious code, poor commit messages, and inadequate review are not solved solely by using Git. Cryptographic signing can help authenticate authorship or release provenance, but signatures do not prove that the code is safe or functionally correct.

Modern Git hosting platforms add pull requests or merge requests, continuous integration, protected branches, artifact checks, and dependency scanning. These services build on Git’s repository model; they do not replace the underlying objects, references, and graph.

Best-Practice Checklist

  • Keep commits focused and explain their purpose clearly.
  • Inspect git status before staging or committing.
  • Review staged content with git diff --cached.
  • Use branches and protected shared branches appropriately.
  • Prefer annotated, possibly signed, tags for releases.
  • Treat force-push and history rewriting as controlled operations.
  • Never rely on .gitignore as a secret-management system.
  • Use git log --graph --decorate --all to understand history.
  • Back up important repositories and test restoration procedures.
  • Build review, testing, scanning, and approval into the DevOps workflow.

Chapter Summary

Git separates edited files, proposed commit contents, stored objects, and collaboration endpoints. The working tree contains current files; the index specifies the next snapshot; and the repository stores objects and references. Blobs hold file contents, trees describe directory structures, commits identify complete snapshots and their parent commits, and annotated tags provide metadata for named objects.

Branches and tags are references rather than copies of project files. HEAD identifies the current position and may be attached to a branch or detached at a commit. Commits form a directed acyclic graph, enabling parallel development, fast-forward updates, merge commits, ancestry queries, and recovery through references. Understanding this model supports accurate command use, safer collaboration, effective troubleshooting, and sound repository governance.

Key Terms

Repository: The Git object database, references, index, configuration, and related metadata.

Working tree: The checked-out files and directories currently available for editing.

Index: The staging area containing the proposed contents of the next commit.

Object: A content-addressed item stored by Git.

Blob: An object containing file contents without its path name.

Tree: An object describing directory entries and references to blobs or subtrees.

Commit: An object identifying a snapshot, parent commits, metadata, and a message.

Snapshot: The complete tracked project state represented by a commit tree.

Reference (ref): A human-readable name that resolves to an object identifier.

Branch: A normally movable reference to a commit.

Tag: A name identifying an object, either lightweight or annotated.

Annotated tag: A tag object containing target and release-related metadata.

HEAD: The reference or commit identifying the current checkout position.

Detached HEAD: A state in which HEAD points directly to a commit rather than a branch.

Hash: An object identifier derived from the object’s type and contents.

Parent: An earlier commit referenced by a later commit.

Merge commit: A commit with multiple parents, usually combining development lines.

DAG: A directed acyclic graph, the structural model formed by Git commits and parent links.

Reachability: The property of being discoverable from a reference through graph links.

Remote-tracking reference: A local reference recording the observed state of a remote branch.

Review Questions

Short Answer Questions

  1. What is the difference between the working tree and the index?
  2. Why does a blob not identify a file’s pathname?
  3. What information does a commit contain besides file contents?
  4. Explain the difference between a branch and a tag.
  5. What does detached-HEAD state mean?

Long Answer Questions

  1. Explain the complete path of a change from editing a file to creating a commit.
  2. Describe blobs, trees, commits, and annotated tags and explain how they relate.
  3. Explain Git’s directed acyclic graph model, including ordinary commits, branching, and merge commits.
  4. Compare snapshots and diffs as concepts in Git storage and presentation.
  5. Discuss repository governance practices for preventing and responding to committed secrets.

Analytical / Application Questions

  1. A file is edited, staged, edited again, and then committed without another git add. Which version is committed, and why?
  2. A shared branch contains a faulty commit. Compare the risks of rewriting the branch with the approach of adding a corrective commit.
  3. A developer deletes a branch after creating useful commits on it. Identify the factors that determine whether those commits can still be recovered.

Multiple Choice Questions with Answers

Question 1

Which Git location contains the contents selected for the next commit?

A. Working tree

B. Index (staging area)

C. Remote repository

D. Reflog only

Correct Answer: B. Index (staging area)

Question 2

What does a Git blob normally store?

A. A file's contents

B. A branch name

C. A commit's parent list

D. A directory tree and all metadata

Correct Answer: A. A file's contents

Question 3

What does a tree object primarily represent?

A. A commit message

B. A directory listing of names, modes, and object references

C. A remote server

D. A shell command history

Correct Answer: B. A directory listing of names, modes, and object references

Question 4

What is the usual role of a branch?

A. A copy of every file in the repository

B. A movable reference to a commit

C. A blob containing a patch

D. A replacement for the index

Correct Answer: B. A movable reference to a commit

Question 5

In an attached HEAD state, HEAD normally points to what?

A. A branch reference

B. A blob

C. The remote network address

D. The staging area contents

Correct Answer: A. A branch reference

Question 6

Why are Git commits described as forming a directed acyclic graph?

A. They are stored only in alphabetical order

B. Each commit points to parents and parent links cannot form a cycle

C. Every commit has exactly two parents

D. Branches cannot share commits

Correct Answer: B. Each commit points to parents and parent links cannot form a cycle

Question 7

Which statement about a merge commit is correct?

A. It always has no parent

B. It normally has multiple parent commits

C. It contains only untracked files

D. It cannot be referenced by a branch

Correct Answer: B. It normally has multiple parent commits

Question 8

What does git diff --cached primarily review?

A. Changes staged relative to HEAD

B. Only remote branches

C. All unreachable objects

D. The contents of the Git configuration

Correct Answer: A. Changes staged relative to HEAD

Question 9

Why can two different filenames refer to the same blob?

A. Blobs store file contents independently of pathnames

B. Git assigns one blob to every directory

C. Branches force filenames to share objects

D. Tags rename blobs automatically

Correct Answer: A. Blobs store file contents independently of pathnames

Question 10

Which practice is most appropriate after a secret has been committed?

A. Only add it to .gitignore

B. Delete the local repository and take no other action

C. Rotate or revoke the secret and follow an approved remediation process

D. Rename the secret-containing file

Correct Answer: C. Rotate or revoke the secret and follow an approved remediation process

Version Control with Git and GitHub/GitLab
Chapter 2 of 14