AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Version Control with Git: How Professional Developers Work

📚 Software Development Tools⏱️ 22 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 22 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Imagine you are building a small AI project in Python for a school competition — say, a chatbot that answers questions about Indian history. You save your file as chatbot.py. The next day you improve it and save it as chatbot_v2.py. Then you fix a bug and save chatbot_v2_fixed.py. Then your teammate emails you their version, chatbot_v2_fixed_FINAL.py, and you are not sure if it includes your bug fix or not. A week later something breaks and nobody can say which file actually worked, what changed between versions, or how to combine your teammate's edits with yours without deleting something important.

This is not a hypothetical annoyance — it is the single most common way beginning programmers lose work, and it gets exponentially worse the moment more than one person touches the same code. Professional software teams do not solve this problem with filenames. They solve it with a version control system: software that records every change to a set of files over time, lets you go back to any earlier state, tells you exactly what changed and when, and lets multiple people edit the same files without overwriting each other. The version control system used by the overwhelming majority of professional software teams today is Git, created in 2005 by Linus Torvalds (the creator of the Linux operating system kernel) to manage the source code of Linux itself, after the team lost access to the proprietary tool they had been using.

This chapter teaches you Git the way it is actually used: not as a list of commands to memorise, but as a small number of ideas — snapshots, staging, branching, merging — that combine to solve the exact mess described above. Every command shown in this chapter was run for real before being written down, so the output you see here is output real Git actually produces, not an approximation.

A misconception to clear up immediately: Git is not GitHub

Many students who have heard of "uploading code to GitHub" assume Git and GitHub are the same thing. They are not, and confusing them causes real problems later. Git is the version control program itself — it runs entirely on your own computer, keeps its entire history in a hidden folder inside your project, and works perfectly well with no internet connection at all. GitHub (and similar services like GitLab and Bitbucket) is a separate website that stores a copy of a Git project online, so that people can share it, collaborate on it, and back it up. You could use Git every day for years and never touch GitHub. This chapter is entirely about Git itself — the tool that runs on your machine.

What Git actually tracks: three areas, not one

Here is the idea that makes everything else in Git make sense. When you use Git inside a project folder, your files exist in up to three different "areas" at once:

  • The working directory — the actual files on your disk, exactly as you see them in your code editor. Anything you type here is just an edit; Git has not "noticed" it in any permanent way yet.
  • The staging area (Git calls this the "index") — a holding area where you place the exact set of changes you want to include in your next saved snapshot. This is Git's way of letting you save five files but only include changes from three of them in this particular snapshot.
  • The repository — the permanent, timestamped history of snapshots, called commits. Once a change is committed, it is recorded forever in the project's history (unless you deliberately rewrite history, which is an advanced topic).

A change moves through these three areas with two commands: git add moves a change from the working directory into the staging area, and git commit takes everything in the staging area and seals it into a permanent snapshot in the repository, with a message describing what changed and why.

Where a change lives as you save it in Git Working Directory Your actual files on disk right now calculator.py (edited, unsaved by Git) Staging Area Changes marked to go into the next commit calculator.py (staged, ready to commit) Repository Permanent history of sealed commits b7daf00 "Add function to add two numbers" git add git commit

Starting a repository and making the first commit

Let's build this for real, tracing what Git prints at every step, using a tiny calculator.py file. Inside an empty project folder, the first command turns an ordinary folder into a Git repository:

$ git init
Initialized empty Git repository in /path/to/project/.git/

This creates a hidden .git folder that will hold the entire history of the project from now on. Next, create the file:

$ echo 'def add(a, b):' > calculator.py
$ echo '    return a + b' >> calculator.py

At this point the file exists in the working directory only. Git has noticed it exists, but has not been told to track it. Running git status — the command you should run constantly, since it always tells you exactly what state your project is in — confirms this:

$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	calculator.py

nothing added to commit but untracked files present (use "git add" to track)

Three things are worth reading carefully here. "On branch main" tells you which line of history you are on (branches are explained fully in a moment). "No commits yet" means the repository's history is empty. And "Untracked files" means Git can see calculator.py sitting in the folder but has never been asked to track its changes — it is invisible to Git's history until you say otherwise. Now stage it:

$ git add calculator.py
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
	new file:   calculator.py

The file has moved from "untracked" to "changes to be committed" — it is now sitting in the staging area from the diagram above, waiting to be sealed into a snapshot. Now commit it:

$ git commit -m "Add function to add two numbers"
[main (root-commit) b7daf00] Add function to add two numbers
 1 file changed, 2 insertions(+)
 create mode 100644 calculator.py

Git prints a short identifying code, b7daf00. This is not a sequence number — it is the first few characters of a 40-character SHA-1 hash, a fingerprint computed from the exact content of the snapshot (the file contents, the commit message, the author, the timestamp, and the identity of the previous commit). Change any of these by even one character and the hash changes completely. This is what makes Git trustworthy for tracking history: two commits can never accidentally collide, and a commit can never be silently altered without its hash changing too. Confirm the commit is recorded with:

$ git log --oneline
b7daf00 Add function to add two numbers

Seeing exactly what changed: git diff

Suppose you now add a comment at the top of the file:

$ cat calculator.py
# Simple calculator functions
def add(a, b):
    return a + b

Before staging or committing anything, you can ask Git to show precisely what is different between the working directory and the last commit:

$ git diff
diff --git a/calculator.py b/calculator.py
index 4693ad3..296d58e 100644
--- a/calculator.py
+++ b/calculator.py
@@ -1,2 +1,3 @@
+# Simple calculator functions
 def add(a, b):
     return a + b

Read this line by line. @@ -1,2 +1,3 @@ means "in the old version this region started at line 1 and spanned 2 lines; in the new version it starts at line 1 and spans 3 lines." Every line that follows starting with + was added, every line starting with - would have been removed (there are none here), and lines with neither are unchanged context, shown so a human can see where the change sits. This is the tool professional developers use every single day to review their own work before committing, and to review a teammate's proposed changes before accepting them. Stage and commit this change:

$ git add calculator.py
$ git commit -m "Add a file header comment"
[main 8c388c7] Add a file header comment
 1 file changed, 1 insertion(+)

The repository's history is now two commits deep: b7daf00 followed by 8c388c7.

Branching: working on a new feature without touching what already works

Here is a scenario every real project runs into. You want to add a subtract function to calculator.py. But what if your half-finished work breaks the existing add function while you're experimenting? You don't want your teammates (or your own working version) affected until the new feature is actually ready. Git's answer is branching.

A crucial fact that trips up almost every beginner: a Git branch is not a copy of your files. Creating a branch does not duplicate your project folder or use extra disk space for your source files. A branch is nothing more than a movable label — technically, a pointer to a specific commit. "main" is one such label pointing at the latest commit on the original line of work; when you create a new branch, you are just creating a second label that currently points at the same commit as the first. As you make new commits on that branch, only that branch's label moves forward; the other label stays where it was. This is why Git can create a branch instantly, even in a project with millions of files — it just writes 41 bytes (a commit hash and a newline) to a new file.

$ git branch feature-subtraction
$ git branch
  feature-subtraction
* main

The * marks which branch you currently have "checked out" — i.e., which label the working directory currently reflects. Switch to the new branch and add the feature:

$ git checkout feature-subtraction
Switched to branch 'feature-subtraction'
$ cat calculator.py
# Simple calculator functions
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b
$ git add calculator.py
$ git commit -m "Add subtraction function"
[feature-subtraction 3bd8f4c] Add subtraction function
 1 file changed, 3 insertions(+)

The feature-subtraction label now points to commit 3bd8f4c, while main still points at 8c388c7 — untouched. If this subtraction feature had turned out to be buggy, you could simply abandon the branch and main would never have known anything was attempted.

Diverging: when both branches move forward independently

In a real team, work rarely stays this tidy. While you were writing subtract, imagine a teammate directly improved the add function on main — adding a docstring. Let's simulate exactly that by switching back to main and committing a separate change there:

$ git checkout main
Switched to branch 'main'
$ cat calculator.py
# Simple calculator functions
def add(a, b):
    """Return the sum of a and b."""
    return a + b
$ git add calculator.py
$ git commit -m "Add docstring to add()"
[main 8c6c1b0] Add docstring to add()
 1 file changed, 1 insertion(+)

The two branches have now genuinely diverged: both moved forward from the same starting point (8c388c7) but along different paths, each adding something the other does not have. This is visible with a history graph:

$ git log --oneline --all --graph
* 3bd8f4c Add subtraction function
| * 8c6c1b0 Add docstring to add()
|/
* 8c388c7 Add a file header comment
* b7daf00 Add function to add two numbers
main and feature-subtraction diverge, then merge b7daf00 add() added 8c388c7 branch point 8c6c1b0 fix on main: docstring 3bd8f4c feature-subtraction c228446 merge commit main feature-subtraction

Merging: how Git recombines two lines of history

To bring the feature branch's work into main, switch to main and merge:

$ git checkout main
Switched to branch 'main'
$ git merge feature-subtraction
Auto-merging calculator.py
Merge made by the 'ort' strategy.
 calculator.py | 3 +++
 1 file changed, 3 insertions(+)

Two details in this output matter. First, notice this is not the simplest possible merge outcome. If main had not moved at all since the branch point — if commit 8c6c1b0 did not exist — Git would have done what's called a fast-forward merge: it would simply have slid the main label forward to 3bd8f4c, since main's history is a strict subset of the feature branch's history and no combining is actually needed. You would see a message like Fast-forward and no new commit would be created. But because main genuinely diverged (it gained the docstring commit that feature-subtraction never saw), Git instead performs a true three-way merge: it compares the branch point (8c388c7), the tip of main (8c6c1b0), and the tip of feature-subtraction (3bd8f4c), figures out that the docstring change and the new subtract function touch different parts of the file, combines both automatically, and seals the result as a brand-new merge commitc228446 — which is unusual in having two parent commits instead of one.

Second, the phrase 'ort' strategy names the algorithm Git used to work out how to combine the two histories. "ort" has been Git's default three-way merge strategy since Git 2.33 (2021), replacing the older "recursive" strategy that many tutorials and older textbooks still mention — if you see "recursive" quoted as the current default anywhere, that source is out of date. Confirm the combined file and the final shape of the history:

$ cat calculator.py
# Simple calculator functions
def add(a, b):
    """Return the sum of a and b."""
    return a + b

def subtract(a, b):
    return a - b

$ git log --oneline --all --graph
*   c228446 Merge branch 'feature-subtraction'
|\
| * 3bd8f4c Add subtraction function
* | 8c6c1b0 Add docstring to add()
|/
* 8c388c7 Add a file header comment
* b7daf00 Add function to add two numbers

Both changes are present — the docstring from main and the new function from feature-subtraction — with no work lost from either side, and full history of exactly how they got there.

Merge conflicts: when Git genuinely cannot decide

The merge above worked automatically because the two branches edited different lines of the file. If both branches had edited the same line — say, both changed line 1 of calculator.py to different text — Git has no way to guess which version you want. It stops the merge and marks the file with conflict markers:

<<<<<<< HEAD
# Simple calculator functions
=======
# Calculator utility functions
>>>>>>> feature-subtraction

Everything between <<<<<<< HEAD and ======= is what your current branch has; everything between ======= and >>>>>>> is what the branch being merged in has. Resolving a conflict means editing the file by hand to keep the correct final text, deleting the marker lines, then running git add on the file to mark it resolved and git commit to complete the merge. A conflict is not an error you did something wrong — it is Git correctly refusing to guess, and handing the decision to a human.

A naming detail worth getting right: "main" is not a hard-coded default

Many students assume every Git repository automatically starts with a branch literally called main. That is a convention, not a rule built into Git itself. Plain, unconfigured Git has historically created a first branch called master. What you see as main in this chapter's output (and on GitHub, which switched its own default to main around 2020) depends on the init.defaultBranch setting — many current installations and platforms now pre-configure it to main, but that is a configuration choice layered on top of Git, not a fixed default of the tool. If you ever run git init somewhere and see master instead of main, nothing is broken — it is simply an installation that has not had that setting changed. The name itself has no special meaning to Git; it is a label like any other branch name.

Why this matters beyond this one chapter

Every idea in this chapter — snapshots you can always return to, a staging area that lets you choose exactly what goes into each save, branches that let you experiment without risk, and merges that combine independent work automatically when possible and ask you to decide when it truly can't — is the same machinery that lets teams of hundreds of engineers, at any company that ships software, work on the same codebase simultaneously without stepping on each other. As you move into more substantial software projects in Classes 11 and 12, working with larger programs and, eventually, with other people's code, these are the exact habits that separate code you can trust and roll back safely from code held together by files named final_v2_USE_THIS.py.

Check your understanding

  • You run git add report.py but have not yet run git commit. Your laptop crashes and restarts. Is your change to report.py permanently recorded in the repository's history? Why or why not, in terms of the three areas?
  • Two branches, main and fix-typo, are created from the same commit. No new commits are ever made on main after that point. You then run git checkout main followed by git merge fix-typo. Will Git perform a fast-forward or a three-way merge? Explain using the definitions above.
  • A classmate says, "I made a new branch, so now I have two separate copies of all my files using twice the disk space." What is wrong with this statement, and what is a branch actually made of?
  • In the merge output Merge made by the 'ort' strategy, what does "ort" refer to, and what would you conclude if you saw a tutorial claiming Git's current default strategy is called "recursive"?
  • You see conflict markers <<<<<<< HEAD in a file after a merge. What do the two sections separated by ======= represent, and what two commands do you run after manually fixing the file?

Summary

  • Git tracks changes through three areas: the working directory (your files as edited), the staging area (changes chosen for the next snapshot, via git add), and the repository (permanent history of snapshots, sealed via git commit).
  • Every commit is identified by a SHA-1 hash computed from its content, making history tamper-evident.
  • git status shows the current state of your files; git diff shows exactly which lines changed before you stage them; git log --oneline shows commit history compactly.
  • A branch is a lightweight, movable pointer to a commit — not a copy of your files — which is why creating one is instantaneous.
  • Merging a branch that has not diverged from the target produces a fast-forward (the pointer simply moves; no merge commit). Merging genuinely diverged branches produces a real three-way merge, combining changes from both sides into a new merge commit with two parents — currently done by Git's 'ort' strategy.
  • A merge conflict occurs only when both branches changed the same lines; Git marks the disagreement with <<<<<<< / ======= / >>>>>>> markers and lets a human decide.
  • Git and GitHub are different things: Git is the local version control tool; GitHub is an online hosting service built around it.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where version control with git: how professional developers work is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting version control with git: how professional developers work to at least 3 other topics you have studied.
← Natural Language Processing: Teaching Computers to ReadAdvanced OOP: Inheritance and Polymorphism →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn