Foundations: Product, Code and the Working Environment
Git: History, Branches and Safe Changes
How Git moves a change from an edited file through staging, commits, branches, remotes and merges, and why none of those steps on its own proves that a…
Picture two people working on the same booking product on the same day. An engineer changes the rule that calculates which time slots are free; a colleague rewrites the wording of the confirmation message. Both of them save their versions of the files. What has actually been recorded at this point? What can the rest of the team see? And if the two edits contradict each other as business rules, can Git settle the disagreement on its own? The answers depend on several distinct steps that people casually lump together as "I pushed it." An edit in a file is not necessarily part of the history the team sees yet. It passes through the working tree, the index and a commit, and it becomes available to colleagues only after it has been sent to a remote repository. In Git, every one of those steps is a separate state.
What you will be able to do
After this unit you will be able to describe, in plain words, where a change stands in Git: from editing a file, to recording a state, to sending it to a server, to review in a pull request (PR, a request to have changes included), to merging. You will be able to tell local recording apart from sharing, merging apart from releasing a version, and a compensating change apart from rewriting history. You will also be able to read an AI agent's proposal, check the boundaries of a diff (the difference between two versions of files), and notice when an automatic conflict resolution does not match what was intended.
The unit builds on ideas from the anatomy of a code project and the terminal and local environment: a file in a repository, a working folder, a process and a terminal command. "Git" is a name, not an abbreviation. The pull request process is covered more broadly in GitHub as a development control plane, and isolating working folders from each other in worktrees and isolated environments.
Git keeps the history of a project
Git is a distributed version control system. In practice this means that your local repository contains the project's history, and you can prepare and check changes on your own computer. Git helps answer a set of concrete questions: which files changed, what exactly changed in them, in what order, who recorded the change, and which earlier state it was built on. Pro Git describes the local copy as containing a substantial part of the history: clone creates a local repository, fetch retrieves new objects and references, and push sends recorded states to a remote copy (Pro Git, Git Basics and Working with Remotes).
Git is not GitHub. GitHub hosts remote repositories and provides tools for working together on them. Files that live outside the repository, files that are ignored, changes that were never committed and commits that were never pushed may all be missing from your colleagues' history. That is why Git is not a complete backup system.
Three states: a file, a staged change, a history record
To understand what will actually be recorded, keep three states apart.
| State | Plain explanation | What it shows you |
|---|---|---|
| Working tree | The files exactly as your editor sees them right now | Edits and new files not yet saved into history |
| Index, or staging area | The specific set of changes prepared for the next commit | Exactly what will go into the next commit |
| Commit | A recorded state of the tracked files together with its historical context | A saved point you can refer back to |
In Git, a commit is a record of the repository's state. It points to a snapshot of the files and carries historical data such as the author, a message and the parent commit or commits. It is not just "I pressed Save." A commit preserves a point in history, and later records build on top of it. The message is what the team will search through later to find out why something changed. A bare "fix" explains very little; "prevent selling the last unit of stock twice" gives the reader real context. A commit does not certify that the author checked every scenario, that the change has reached GitHub, or that the product already works in the live production environment. Pro Git describes the commit object as a record that ties a snapshot of the files to an author, a message and parent records (Git Internals: Git Objects).
For example, an author edits inventory.md. Until that change is placed in the index, the next commit will not include it. git add inventory.md stages the entire current file; git add -p inventory.md lets you pick individual fragments. Before committing, review the staged difference specifically.
git status summarises the differences between the current commit, the index and the working tree, and it also lists new untracked files. git diff compares the tracked changes in the working tree with the index; an untracked file will not appear in that diff by itself. git diff --cached shows what has been staged for the commit. Do not confuse these. The unstaged diff can look clean even though the index already holds changes, or a new untracked file is sitting right next to it. That is not a bug in the tool: the commands answer different questions (git-status, git-diff).
The commands below are only an illustration, not a list of steps for you to perform:
git status --short
git diff -- inventory.md
git add inventory.md
git diff --cached -- inventory.md
git commit -m "Prevent inventory overselling"
git log --oneline --decorate -5
git log here shows the five most recent records along with the branch pointers and HEAD that point at them. It is an overview of history, not proof that the working tree is clean or that anything has been pushed.
Before committing, read the diff the way a human reviewer would. Does it contain only the intended task? Did someone else's local changes, generated files, secrets or an accidental reformatting of a large file slip in? git add means "include the selected changes in the next commit," not "approve them as correct."
A branch is a movable pointer into history
An ordinary commit points to the one before it, a merge commit can have several parents, and the root commit has none. A branch name such as main or inventory-alert is a movable reference to the latest commit on that branch. Creating a branch gives a new name to the current point; subsequent commits move that pointer forward. HEAD usually identifies the branch you have selected, though sometimes it points directly at a commit.
main is often called the main branch, but that is an agreement about shared history, not a special property built into Git. A team can set up checks and rules for main on GitHub; Git itself has no idea which branch the company treats as production or as accepted. Pro Git explains HEAD as a reference to the current branch and shows how two branches can diverge from a common commit (Branches in a Nutshell).
In the table below, A, B and C are placeholder commits. The last two rows are alternative outcomes of merging from the same starting state, not consecutive steps.
| Step | main points to |
inventory-alert points to |
What happened |
|---|---|---|---|
| Start | A | — | There is an initial shared history |
| Working branch created | A | A | Both names point to the same state |
| Author changed the rule | A | B | A new commit exists only on the working branch |
| Merged by fast-forward | B | B | main moved from A to the already existing commit B; no new merge commit |
| Merged with a separate merge commit | C | B | git merge --no-ff inventory-alert created C with parents A and B; the working branch stayed at B |
How long a branch lives is a team convention: it can be dedicated to a single task or kept going longer. An IT Revolution article adapted from the book Accelerate lists, among the capabilities associated with software delivery performance, version control for all production artifacts, short-lived branches and working in small batches. The authors explain that small batches shorten the time from starting work to a finished result and speed up feedback. This describes findings of the State of DevOps research; it is not evidence that Git itself, or any particular branching strategy, produces fast or high-quality delivery (IT Revolution, "24 Key Capabilities"). No branch format guarantees fast or high-quality delivery on its own.
Local history and the remote repository
Your local copy and the remote copy are separate states, and data moves between them through specific operations.
clonecreates the initial local copy of a repository; options can limit how much history it retrieves.fetchretrieves commits from the remote repository and updates your local knowledge of where the remote branches are. It does not have to merge the new history into your current files.pullfirst retrieves new history and then integrates it into the selected local branch. Exactly how it integrates can depend on settings and arguments.pushsends commits and moves the remote reference forward, if the history and your access rights allow it.
For example, after a local commit the engineer can see the new change on their own machine, but a colleague cannot. The colleague will see it after a push to the shared remote repository, or after some other agreed way of handing it over. Even then, that alone does not mean the branch has become part of main. Pro Git distinguishes fetch, which retrieves data and updates local knowledge of remote branches; pull, which usually performs an integration after retrieving; and push, which sends commits to the remote copy (Working with Remotes).
A remote-tracking branch is your local picture of where a branch in the remote repository was at the moment of your last fetch. That picture can fall behind if other people have since pushed new commits. So a message like "no new changes" refers to the state Git knows about, not to the whole internet: first work out when that information was actually retrieved.
A merge combines histories but does not make business decisions
merge combines two lines of history. If one branch simply moved ahead and nothing diverged, Git can often do a fast-forward: move the main branch's pointer to the newer commit without creating an extra merge commit. If both branches added commits independently, Git can create a merge commit with two parent commits. Either way, the application logic still has to end up correct. The mechanics are described in the git-merge reference.
A conflict appears when Git cannot safely work out the final text by itself. Suppose one branch changed a booking rule from "until 17:00" to "until 18:00," and another changed it to "until 19:00." Git does not know whether the team changed the same schedule twice or whether the two edits represent two different, agreed requirements. A person has to find the right decision in Jira, in the contract, in the product specification, or in a conversation with whoever owns the rule. A conflict does not mean anyone worked incorrectly; it only shows that the change cannot be safely reduced to one answer by looking at the shape of the text. In the pricing example later in this unit, both versions also turn out to be incomplete, so the rule owner has to clarify what they actually mean.
Git may leave the markers <<<<<<<, ======= and >>>>>>> in the file. Once the decision is agreed, remove them and review the full diff; never accept "ours" or "theirs" simply because of its label. Even without a textual conflict, two changes can be incompatible in meaning (GitHub Docs: Merge conflicts).
Rebase and squash change how history is presented
rebase takes the commits from a working branch and re-applies them on top of a different base. The result can be a simpler-looking, linear history, but the re-applied commits get new identifiers, because their parent commit has changed. This is not a cosmetic rename, and it is not a neutral operation for everyone involved. If other people have already built their own changes on the old commits, rewriting published history forces them to reconcile their branches with the new line.
A practical rule: before rebasing a private working branch, check its base, and agree with others if anyone else is already using those commits. Never rewrite a shared branch silently. If the remote state has changed, your push may be rejected; force-pushing history is especially dangerous on a shared branch. GitHub explains separately that its rebase-and-merge option creates new commit identifiers and is not identical in every respect to a local git rebase (GitHub Docs: Pull request merges). The pull request merge options on GitHub and their outcomes are covered in more detail in GitHub as a development control plane.
Squash combines several small changes into one logical commit. For example, a working branch might contain commits called "draft," "typo" and "rename clarification," while the team wants a single clear record of the finished feature. On GitHub, the squash-and-merge strategy adds a PR's changes to the base branch as one commit. That simplifies the base branch's history, but the intermediate commits no longer appear there as separate steps. Each team chooses the format that suits it; there is no single correct style for every repository.
Undoing: a new compensating change or moving a pointer
Git commands with similar-sounding names have very different consequences.
revertcreates a new commit that tries to apply the opposite of the selected commit's changes. The earlier event stays in history, and the new commit records how it was undone.resetmovesHEADor the index to a chosen state. In some modes it changes the working tree as well. Moving the pointer can remove commits from the current history, and the--hardmode also replaces files; this can destroy unsaved edits.restorecopies a chosen state of a file into the index or the working tree. You can use it, for example, to take a file out of the staging area, but restoring content can overwrite unsaved work.
Before running any of these, check git status and git diff, find out whether the history is already shared, and make sure you will not wipe out someone else's uncommitted edits (Git: Reset, restore and revert).
If a wrong change has already landed on the shared main, a new revert is often the safer choice for history, because everyone can see both the original change and the compensating one. If a private commit has never been sent anywhere and nobody depends on it, the team can agree on a different way of fixing the history. The criterion is not how tidy the graph looks; it is who has already received this history and which local changes need to be preserved.
A force push is a push that can replace the remote history pointer with a new line of history, even when a normal push would reject the change. It can push aside commits a colleague is working on. Even the variant that checks the pointer has not moved since you last read it is no substitute for agreement. Shared history is rewritten only after checking authorship, reaching agreement and confirming how to recover.
Cherry-pick, tag, merge, release and deploy are different events
cherry-pick applies the change from one existing commit to the current branch and creates a new commit. It is useful when a single chosen fix needs to be carried over to another branch, but it can leave the fix's dependencies behind. You have to check whether earlier commits, configuration or related changes are also needed. The Git reference defines cherry-pick plainly as applying the changes introduced by existing commits (git-cherry-pick).
A tag is a named reference to a specific object, often a commit. A lightweight tag is just a name; an annotated tag carries a date, the tagger's identity and a message, and sometimes a digital signature. For example, a team might tag a commit v2.4.0 so it can refer to it as a version. A tag by itself does not build a binary and does not put the system in front of users (git-tag).
It is important to keep the final stages apart:
| Event | What happened |
|---|---|
| Commit | A state was saved in local history |
| Push | The commit was sent to the remote repository |
| Pull request | The change was proposed for discussion and review |
| Merge | The history was integrated into the target branch |
| Release | The team announced or prepared a specific version for use |
| Deploy | A build was installed into a chosen environment |
| Confirmation that it works | The required user-facing outcome was checked in that environment |
On GitHub you can choose a merge commit, squash, or rebase-and-merge, and each option has a different effect on history. Merging a PR does not prove that the change has been deployed or that it produced the expected result in the product. Release and deployment depend on team processes that go beyond the mere existence of a commit. GitHub pull requests, checks, and the evidence for releases and deployments are covered in GitHub as a development control plane.
Fully worked examples
Example 1: Two independent changes in an online shop
Situation. Marta is changing the rule that stops the shop from selling the last unit of an item, while Oleh is rewriting the text of the "your order has shipped" email. Both start from commit A on main.
Marta's work. She creates the branch inventory-low-stock, changes the server-side rule, reviews the diff, stages the relevant file and creates commit B. After she pushes, colleagues can see it on that branch, but it is not yet part of main.
Oleh's work. On the branch shipping-email-copy he changes the email template, creates commit C and pushes it too. The two commits descend from the common point A.
Review and the merge decision. A reviewer approves both changes, the PR's automated checks pass, and the product owner accepts the final diff. The team merges the branches according to its agreed process; the details of pull request states, reviews and checks are covered in GitHub as a development control plane.
Checking after the merge. The team opens the up-to-date main and confirms that both changes are in it and that their combined result is consistent. Because they touch different files, there is no textual conflict; but someone still has to make sure the email does not promise to ship an item that is out of stock. If one of the changes needs configuration or an external setting, the mere presence of the commits on main proves neither that they are correct nor that they already work in production.
Example 2: A conflict over a subscription price
Situation. In this teaching example, the basic monthly plan of an analytics service costs five hundred Norwegian kroner. Sales changes it on one branch to five hundred and fifty; the finance team changes it on another branch to six hundred. Both edits touch the same line; the numbers are made up.
What the tool does. During the merge Git does not guess. The file may end up containing something like this:
<<<<<<< sales-price
The monthly plan costs five hundred and fifty Norwegian kroner.
=======
The monthly plan costs six hundred Norwegian kroner.
>>>>>>> operations-price
What the team has to do. First, find out whether one of the edits is simply out of date or whether the two reflect different conditions. Here, sales meant the price for new customers, while finance meant the renewal price for existing customers. The product owner decides to record both rules: five hundred and fifty kroner for new customers and six hundred for renewals.
Result. The author removes the markers, writes down the agreed terms, reviews the diff and asks the owner to confirm the content. The team checks both prices with separate examples: one for a new customer and one for a renewal. The merge now combines two requirements instead of accidentally keeping only one of them.
Why not just accept one side. That would remove the markers, but it could erase the other requirement. A conflict is a signal to reconcile intent with the rule's owner, not to pick a version by its label.
Another option, and its limit. If the product owner is unavailable, the work should stay unmerged, or the team should fall back on a previously agreed policy. A temporary price based on a guess must not quietly become the price in force. Checking the format is no substitute for confirming the business meaning.
Example 3: A bug that is already on main
Situation. After a merge, the team discovers that a new validation in the customer relationship management (CRM) system rejects old customer records that lack an optional phone number. Two colleagues have already pulled the current main and started their next pieces of work.
Action and choice. The product owner decides to restore the previous behaviour temporarily. The team first notes the commit's identifier and checks which later changes depend on it. Because the commit is already shared, they create a separate compensating change with revert and take it through a PR, review and the required checks. The original commit stays in history, and the new record explains exactly what was undone and why.
Result in the product. After the compensating change is merged, the team can build the corrected version and deploy it as a separate step. Then they check that both old and new records import correctly and that no other rules have broken.
Why not reset --hard on the shared branch. It would rewrite the agreed history and could wipe out local edits. For a shared commit, revert keeps both the original change and its reversal visible.
Why rolling back the product is a different operation. revert creates a new change in the repository. Rolling back a deployment may reinstall a previously built version or restore configuration some other way. Neither one performs the other automatically: the team has to decide what the safe target state is, protect the data and verify that state separately.
If the faulty commit is still private and nobody depends on it, the team may agree on a different way of correcting history. But changing files in Git does not undo a database migration that has already run or a change already made to production data: restoring text or code does not automatically restore external state. That has to be checked and restored separately.
Working discipline for an agent
Before making a change, check the repository, the branch, the working tree, the index and the current diff. Protect other people's edits: do not stage everything wholesale and do not overwrite files you do not understand until you have found out who they belong to.
- Before a commit: compare the staged files and the message against the task.
- Before a push: check that this particular branch is meant to go to the shared repository.
- Before a PR: state the target branch, the base commit, which checks were and were not run, the revision they ran against, and any known limitations.
- After new edits: re-run the checks they affect. A local commit proves that history changed, but on its own it does not prove the change was tested, shared, merged or deployed.
This matches the practices Drive Capital has described: splitting a large feature into smaller PRs can speed up review, and the team reverted a failed change through a separate revert PR. This is one team's documented workflow, not a universal Git policy and not proof that a revert button is always safe (Drive Capital, Git Workflow, commit c67fa12).
Summary
- The working tree, the index and a commit are separate states; the diff before and after staging shows different changes.
- A commit records local history, a push shares it with the remote, and a merge integrates it into the target branch. A branch is a movable pointer;
mainis a team convention. - Merge and rebase combine histories in different ways. Conflicts require a decision about meaning;
revert,resetandrestorehave different consequences. - A tag marks a point in history but does not by itself perform a release or a deployment. Git stores changes without confirming that they are correct for the business or that they behave correctly in production.
Each of these events answers a different question: is the change in local history, has it been shared with colleagues, has it been accepted into the target branch, has a version been prepared and installed, and has the required user-facing outcome been achieved? Git helps you track history, but it does not make business decisions and does not guarantee that the deployed system meets the requirement.
Sources and further reading
- Git Project, Git Reference, reference pages as of Git 2.55.0; accessed 23 September 2026. Used for the precise roles of
status,diff,add,commit,merge,rebase,revert,restore,reset,cherry-pickandtag. The reference describes the tool; it does not set team policy. - Scott Chacon and Ben Straub, Pro Git, 2nd Edition, 2014, online edition with community corrections; accessed 23 September 2026. The Git Basics, Working with Remotes and Git Branching sections were used to explain commits, branches,
HEADand the transfer of history. - GitHub Docs, Merge conflicts and Pull request merges, living web pages without a fixed publication date; accessed 23 September 2026. Used for conflicts and for the differences between merge commit, squash and rebase-and-merge on GitHub.
- IT Revolution, 24 Key Capabilities to Drive Improvement in Software Delivery, 1 September 2020; accessed 23 September 2026. The article states that it adapts an excerpt from Accelerate. Used for a cautious link between version control of production artifacts, small batches of work and delivery capabilities; it is not a universal guarantee of any effect from Git.
- Drive Capital, Git Workflow, commit
c67fa12e5c76510e960257c929b5353989b20144; accessed 23 September 2026. Used as a documented description of one team's practices for splitting changes, pull requests and reverting a failed change; it is neither a general standard nor an empirical evaluation.