Git Internals and Workflow
Git is easier to use once you know it is a content-addressed store of snapshots. This guide covers the object model, everyday branching and history editing, remotes and the pull request workflow.
Objects and hashes
Inside the hidden .git directory, Git is a key-value database. The key is a hash of the content (SHA-1 traditionally, with SHA-256 available in newer setups), so identical content always gets the same ID and any change produces a new one. There are four object types:
| Object | Holds |
|---|---|
| blob | The contents of one file, with no name or permissions |
| tree | A directory listing: names, modes and the hashes of blobs or sub-trees |
| commit | A pointer to a tree, the parent commit(s), author, committer, timestamp and message |
| tag | An annotated, named pointer to another object |
You can poke at them directly:
echo "hello" | git hash-object -w --stdin # store a blob, print its hash
git cat-file -t <hash> # type of an object
git cat-file -p <hash> # pretty-print its content
git ls-tree HEAD # tree of the current commit
Because a commit stores its parent's hash, history is a chain that cannot be altered without changing every later ID. Unchanged files are simply reused by reference between commits, so snapshots stay cheap.
Commits, branches and HEAD
Changes move through three areas: the working directory, the staging area (index) and the repository. The basic cycle:
git config --global user.name "Your Name"
git config --global user.email "user@example.com"
git status
git add src/app.py # stage one file (git add -p stages hunks)
git commit -m "Add request validation"
git commit --amend # rewrite the latest commit (only if not yet shared)
git diff # unstaged changes; --staged for staged ones
git log --oneline --graph --decorate
List patterns you never want tracked in .gitignore. It does not affect files that are already tracked; remove those with git rm --cached.
A branch is nothing more than a movable file containing a commit hash, which is why branching is instant. HEAD is the pointer to where you are, normally the current branch. Checking out a raw commit gives a detached HEAD: commits made there are not on any branch and can be lost unless you create one with git switch -c rescue.
git branch # list
git switch -c feature/login # create and switch
git switch main
git branch -d feature/login # delete if merged (-D forces)
git switch --detach <hash> # inspect an old commit
Merge versus rebase
Both bring one line of work into another, in different ways.
- Merge joins histories. If the target has not moved, Git just fast-forwards the pointer. Otherwise it creates a merge commit with two parents. History stays truthful, at the price of more branching noise.
- Rebase replays your commits on top of another base, creating new commits with new hashes. The result is a straight, readable line, but it rewrites history.
git switch main
git merge feature/login # merge
git switch feature/login
git rebase main # replay your commits onto main
git rebase -i HEAD~4 # squash, reorder, reword recent commits
When two sides edit the same lines, Git stops and marks conflicts with <<<<<<<, ======= and >>>>>>>. Edit the file to the intended result, git add it, then git merge --continue or git rebase --continue. Use --abort to back out.
Golden rule: rebase only commits that nobody else has pulled. Rewriting shared history forces collaborators to untangle diverged copies. If you must push a rebased branch of your own, use git push --force-with-lease rather than a bare --force.
Stash, restore and reset
Stash shelves uncommitted work so you can switch context:
git stash push -m "wip: parser" # add -u to include untracked files
git stash list
git stash pop # re-apply and drop (apply keeps it)
Restore handles files, leaving history alone:
git restore file.txt # discard working-tree edits (irreversible)
git restore --staged file.txt # unstage, keep the edits
git restore --source=HEAD~2 file.txt # bring back an older version
Reset moves the current branch pointer, with three strengths:
| Mode | Branch pointer | Index | Working files |
|---|---|---|---|
| --soft | moved | kept | kept |
| --mixed (default) | moved | reset | kept |
| --hard | moved | reset | overwritten |
To undo a commit that is already shared, prefer git revert <hash>, which adds a new commit that cancels the old one. If you reset too far, git reflog lists recent positions of HEAD so you can usually recover.
Remotes
A remote is a named URL for another copy of the repository; the conventional name for the original is origin. After git clone, Git keeps remote-tracking branches such as origin/main, which record where the remote was the last time you talked to it.
git fetchdownloads new objects and updates tracking branches without touching your work.git pullis fetch plus an integration step: merge by default, or rebase with--rebase.git push -u origin feature/loginpublishes a branch and sets its upstream so laterpushandpullneed no arguments.git push origin --delete feature/loginremoves a remote branch;git fetch --prunecleans stale tracking refs.git remote -vshows configured remotes, andgit remote add upstream <url>adds another, handy for forks.
Fetching first and inspecting with git log main..origin/main is a safe habit before integrating.
Pull requests
A pull request (PR) is a hosting-platform feature, not a Git one. It proposes merging one branch into another and wraps the diff in discussion, review, status checks and approval. To open one, push your branch, choose the base and compare branches, describe the change and what you tested, and request reviewers. Collaborators need appropriate repository access, or you contribute from a fork.
Repositories usually protect the main branch: require a PR, a minimum number of approvals, passing CI checks, up-to-date branches and no force pushes. The merge button typically offers three strategies: a merge commit, a squash into one commit, or a rebase and fast-forward. Squash keeps the main history tidy; merge commits preserve detail.
Feature-branch workflow
- Update:
git switch main && git pull. - Branch with a descriptive name:
git switch -c feature/add-search. - Commit small, focused changes with messages that explain why.
- Periodically sync with main via
git rebase main(or merge main into the branch) to resolve conflicts early. - Push and open a PR; automated checks run, reviewers comment, you push fixes.
- Merge once approved and green, then delete the branch locally and remotely.
Keep branches short-lived, never commit directly to the protected branch, never commit secrets (removing them later does not erase them from history), and use annotated tags such as git tag -a v1.2.0 to mark releases.