9:40 P.M., Two Nights Before the Tech Fest
Aarav, Priya, and Rohit are building a small Python tool for their school's Tech Fest: a Canteen Bill Splitter that takes the total amount of a shared canteen bill and the number of friends splitting it, and prints how much each person owes. Aarav writes the first working version and emails the file, split.py, to the other two. Priya adds a feature for tipping the canteen staff and saves her copy as split_v2.py. At almost the same time, working from Aarav's original attachment — not Priya's update, which she has not seen yet — Rohit fixes how the rupee amount is displayed and saves his copy as split_final.py. By 11:20 p.m., three files sit in three different inboxes: split.py, split_v2.py, and split_final.py. Nobody can say for certain which one, if any, contains both Priya's tip feature and Rohit's formatting fix. When Priya opens split_final.py to rehearse the demo, her tip feature has vanished — Rohit built on top of the older file without knowing her change existed. The team spends the next half hour on a phone call, reading code aloud and pasting pieces between files by hand, and in the confusion someone deletes the one working copy of Aarav's original function.
Nothing here was caused by bad code. Every line any of the three students wrote was correct. What failed was the process of combining three people's work on the same files at the same time, with no record of what had changed, when, or by whom. This is precisely the problem version control was built to solve — and it is why, decades after it was first needed, it is still one of the first tools any working programmer learns.
What Version Control Actually Solves
Version control is a system that records changes to a set of files over time, so that specific versions can be recalled, compared, or combined later. A version control system does not just keep the newest copy of a file — it keeps a searchable history of every saved change, who made it, when, and why.
The most widely used version control system today is Git, created in 2005 by Linus Torvalds, the same engineer who started the Linux operating system kernel in 1991. Torvalds needed a way for a large, worldwide group of contributors to work on the Linux kernel's source code at the same time without stepping on each other's changes, after BitKeeper, the proprietary tool the kernel project had been using, stopped being available to them free of charge. He built Git in a matter of weeks, and within months Git itself was managing the Linux kernel's own enormous codebase — an early proof that it could handle real, high-stakes collaboration at scale.
Git is what is called a distributed version control system, or DVCS. "Distributed" means that every person working on a project has a complete copy of the entire project history on their own machine — not just the latest files, but every past version and every commit ever made. This is different from older, centralized systems such as Subversion, where only one server held the full history and everyone else had to stay connected to it to do meaningful work. With Git, Aarav, Priya, and Rohit could each hold the project's full history on their own laptops and keep working productively even without internet access, then synchronize later.
One distinction worth getting right immediately, because it trips up almost every beginner: Git is not the same thing as GitHub. Git is the version control tool itself — free, open-source software that runs on your own computer. GitHub is a separate company that hosts Git repositories online so that teams can share them; it was founded in 2008, and Microsoft acquired it in 2018 for about $7.5 billion. Git works perfectly well with no internet connection and no GitHub account at all. GitHub, and rivals like GitLab and Bitbucket, simply give Git repositories a shared home on the internet, along with extra collaboration features layered on top, such as the pull requests described further on.
The Three Areas Every Git User Must Know
Before typing a single Git command, it helps to understand the three places a change to a file can live. Confusing these three is the single most common source of beginner mistakes, so picture them as three physical stations on a desk.
- The working directory is the actual folder on your computer where your files live and where you edit them. Change a line of code in your text editor, and that change exists only in the working directory — Git has noticed nothing yet.
- The staging area (Git also calls it the index) is a holding space where you place exactly the changes you have decided are ready to be permanently recorded. You choose what goes here using the command
git add. Nothing is saved to history yet — you are simply building the exact list of changes your next save will contain. - The repository is the permanent, timestamped history itself, stored in a hidden folder named
.git. A change becomes part of this history only when you rungit commit, which takes a snapshot of everything currently in the staging area and seals it with a unique ID, an author, a timestamp, and a message. Each such saved snapshot is called a commit.
Think of it like preparing an answer sheet for evaluation. Your rough notebook, where you experiment and cross things out freely, is the working directory. Copying only your final, chosen answers onto the fair answer sheet is staging. Handing that sheet to the invigilator, who seals it with a timestamp, is the commit — after that point, that exact version is permanently on record, and starting a fresh rough page doesn't change what was already sealed.
Your First Five Git Commands
Five commands cover almost everything a beginner does with a local Git repository:
git init— turns the current folder into a Git repository by creating the hidden.gitfolder that will hold all history.git status— reports what Git currently sees: which files are untracked, which changes are staged, and which are still only in the working directory. This command changes nothing; it only reports, so it is safe to run constantly.git add <filename>— moves a change from the working directory into the staging area, marking it ready to be committed.git commit -m "message"— permanently saves everything currently staged as a new snapshot in the repository's history, labeled with the message you provide.git log— shows the history of commits: who made each one, when, and what the message said.
The next section puts all five to work, with the exact output Git prints at each step.
A Complete Walkthrough: Building the Bill Splitter
Suppose Aarav starts the bill-splitter project over again, this time with Git from the very first line. He creates a folder and turns it into a repository:
$ mkdir bill-splitter
$ cd bill-splitter
$ git init -b main
Initialized empty Git repository in /Users/aarav/bill-splitter/.git/
The -b main tells Git to name the very first branch main, the default branch name most tools and tutorials use today. Aarav then writes a first, simple version of the script:
def split_bill(total, people):
share = total / people
return share
print("Rs.", split_bill(900, 3))
This function divides the total by the number of people and returns each person's share. Running it by hand first confirms it works: 900 divided by 3 is exactly 300, so the script prints Rs. 300.0 — Python's / operator always returns a float, hence the trailing .0 even on an exact result. The file exists on disk, but Aarav has not told Git about it yet, so it sits only in the working directory. Checking status confirms exactly that:
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
split.py
nothing added to commit but untracked files present (use "git add" to track)
"Untracked" is Git's word for "I can see this file exists, but I am not yet watching it for changes." Staging it moves it into Git's care:
$ git add split.py
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: split.py
Notice how the message changed entirely: split.py now appears under "Changes to be committed" instead of "Untracked files," because staging is a real action that moves the file from one area to the next, not just a label Git prints without reason. Committing seals this snapshot into history:
$ git commit -m "Add basic bill splitting function"
[main (root-commit) e625406] Add basic bill splitting function
1 file changed, 5 insertions(+)
create mode 100644 split.py
e625406 is the first seven characters of this commit's full 40-character ID, a hash computed from the file's exact contents, its parent commit, the author, the timestamp, and the commit message together — change any one of those inputs, even a single character of the message, and the ID comes out completely different. "5 insertions(+)" simply counts the five lines Git just saved: the function definition, its two-line body, a blank line, and the print statement.
A few days later, Priya adds tip support. She changes the function to accept an optional tip_percent argument and adds a second, more precisely formatted print statement:
def split_bill(total, people, tip_percent=0):
total_with_tip = total + (total * tip_percent / 100)
share = total_with_tip / people
return share
print("Rs.", split_bill(900, 3))
print("Rs. {:.2f}".format(split_bill(900, 3, 10)))
Trace the new call by hand before trusting it: split_bill(900, 3, 10) sets tip_percent to 10, so total_with_tip = 900 + (900 * 10 / 100) = 900 + 90 = 990, and share = 990 / 3 = 330.0, formatted to two decimal places as 330.00. The original call, split_bill(900, 3), still uses the default tip_percent=0, so it is unaffected and still prints 300.0. Before staging anything, Priya checks exactly what changed:
$ git diff
diff --git a/split.py b/split.py
index 5cdd851..873932d 100644
--- a/split.py
+++ b/split.py
@@ -1,5 +1,7 @@
-def split_bill(total, people):
- share = total / people
+def split_bill(total, people, tip_percent=0):
+ total_with_tip = total + (total * tip_percent / 100)
+ share = total_with_tip / people
return share
print("Rs.", split_bill(900, 3))
+print("Rs. {:.2f}".format(split_bill(900, 3, 10)))
git diff compares the working directory against the last commit and prints only what changed: a leading minus marks a deleted line, a leading plus marks a new one, and lines with no marker are unchanged context shown so the change makes sense on its own. Two lines were removed and four were added — a net gain of two lines, which matches exactly what changed in the function: the signature line was replaced to add a parameter, the one-line share calculation grew into two lines to work the tip in first, and a new print statement was appended at the end. Staging and checking status once more shows a detail worth noticing — the hint text itself has changed now that a first commit already exists:
$ git add split.py
$ git status
On branch main
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: split.py
Before the first commit, Git suggested git rm --cached to unstage a file, because there was no earlier saved version to fall back to. Now that e625406 exists, Git suggests git restore --staged instead, because unstaging can restore the previously committed version. Committing records the second snapshot:
$ git commit -m "Add tip support to bill splitter"
[main 60b7d4a] Add tip support to bill splitter
1 file changed, 4 insertions(+), 2 deletions(-)
$ git log --oneline
60b7d4a Add tip support to bill splitter
e625406 Add basic bill splitting function
git log --oneline confirms both commits now exist, newest first, each identified by its own short hash. Two commits, two clean snapshots, a complete record of exactly how the file grew — nothing like the tangle of split_v2.py and split_final.py the team started with.
Branching: Trying Things Without Breaking Things
Real teams rarely all edit main directly. Instead, each person creates a branch: an independent line of development that starts as an exact copy of the current code and can be worked on without affecting main until it is deliberately merged back in. Rohit wants to add a header line, print("=== Canteen Bill Splitter ==="), as the very first line printed by the program. He creates a branch, switches to it, makes the change, and commits:
$ git switch -c add-header
Switched to a new branch 'add-header'
$ git add split.py
$ git commit -m "Add header message to output"
[add-header 3713452] Add header message to output
1 file changed, 2 insertions(+)
While Rohit was doing this, main did not change at all. Switching back and merging brings his work in:
$ git switch main
Switched to branch 'main'
$ git merge add-header
Updating 60b7d4a..3713452
Fast-forward
split.py | 2 ++
1 file changed, 2 insertions(+)
Git calls this a fast-forward merge because main had not moved since add-header was created — there was nothing to combine, so Git simply slides the main pointer forward to Rohit's latest commit. Fast-forwards are the easy case. The harder, more instructive case happens when both branches change at the same time.
When Two Branches Touch the Same Line: Merge Conflicts
Suppose Priya and Rohit both branch off main at the same point to change the same line — the function's default tip percentage — but choose different values. Priya branches off, changes tip_percent=0 to tip_percent=5, and commits:
$ git switch -c priya-tip-default
Switched to a new branch 'priya-tip-default'
$ git add split.py
$ git commit -m "Default tip to 5 percent"
[priya-tip-default bb7c3f1] Default tip to 5 percent
1 file changed, 1 insertion(+), 1 deletion(-)
Meanwhile, starting from that same commit on main, Rohit changes the identical line to tip_percent=10:
$ git switch main
Switched to branch 'main'
$ git switch -c rohit-tip-default
Switched to a new branch 'rohit-tip-default'
$ git add split.py
$ git commit -m "Default tip to 10 percent"
[rohit-tip-default 645febf] Default tip to 10 percent
1 file changed, 1 insertion(+), 1 deletion(-)
Merging Priya's branch into main first is uneventful, since main still has not diverged from where her branch started:
$ git switch main
$ git merge priya-tip-default
Updating 3713452..bb7c3f1
Fast-forward
split.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
But now main has moved — it carries Priya's tip_percent=5. Rohit's branch still has the original line at its starting point, and his own commit changed that same line to tip_percent=10. When Git tries to merge his branch in, it finds the same line changed two different ways since the branches split apart, and it cannot guess which one the team actually wants:
$ git merge rohit-tip-default
Auto-merging split.py
CONFLICT (content): Merge conflict in split.py
Automatic merge failed; fix conflicts and then commit the result.
This is a merge conflict: not an error in anyone's code, but a genuine disagreement between two histories that only a human can settle. git status confirms the merge is paused, waiting on a decision:
$ git status
On branch main
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: split.py
Opening split.py shows Git has written both versions directly into the file, separated by conflict markers:
print("=== Canteen Bill Splitter ===")
<<<<<<< HEAD
def split_bill(total, people, tip_percent=5):
=======
def split_bill(total, people, tip_percent=10):
>>>>>>> rohit-tip-default
total_with_tip = total + (total * tip_percent / 100)
share = total_with_tip / people
return share
Everything between <<<<<<< HEAD and the ======= divider is what the current branch — main, carrying Priya's change — has; everything between the divider and >>>>>>> rohit-tip-default is what the incoming branch has. Resolving a conflict means editing the file down to what it should actually say and deleting the marker lines entirely — Git will not do this for you, because only the team can decide. Suppose the team discusses it and settles on 10 percent. Aarav edits the file to keep only that line, removing every marker, then stages and commits the resolution:
$ git add split.py
$ git status
On branch main
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: split.py
$ git commit -m "Merge rohit-tip-default, resolve conflict: use 10 percent default tip"
[main ef1ab3c] Merge rohit-tip-default, resolve conflict: use 10 percent default tip
git log --oneline --graph --all now shows the full shape of what happened: two branches splitting off the same commit and rejoining at the merge.
* ef1ab3c Merge rohit-tip-default, resolve conflict: use 10 percent default tip
|\
| * 645febf Default tip to 10 percent
* | bb7c3f1 Default tip to 5 percent
|/
* 3713452 Add header message to output
* 60b7d4a Add tip support to bill splitter
* e625406 Add basic bill splitting function
One detail is easy to miss unless the program is actually re-run rather than trusted on sight: resolving the conflict changed the function's default value, and the very first print statement, print("Rs.", split_bill(900, 3)), relies on that default. Running the file now prints:
=== Canteen Bill Splitter ===
Rs. 330.0
Rs. 330.00
Both lines now show the same amount, because both calls compute with a 10 percent tip — one inherits it silently through the new default, the other still states it explicitly. Git is completely satisfied that the conflict is resolved; it has no way of knowing whether that behavior is what the team actually wants. Resolving a conflict only fixes what Git tracks, which is the text of the file. Confirming the program still behaves correctly is the team's job, every time, which is exactly why the arithmetic was traced by hand above rather than trusted on faith.
Collaborating Beyond One Laptop: Remotes, GitHub, and Pull Requests
Everything so far has happened on one machine. Real teams work from different laptops, so Git supports a remote: a copy of the repository hosted on a server that everyone's local repository can synchronize with. git clone <url> downloads a full copy of a remote repository, history included, onto your own computer. git push origin main uploads your local commits to the remote so teammates can see them — origin is simply the name Git gives the remote a repository was cloned from. git pull origin main does the reverse: it downloads any new commits from the remote and merges them into your current branch, exactly like the merges traced above, except the second branch happens to live on a server instead of a teammate's own laptop.
The most widely used host for Git remotes is GitHub, though it is far from the only one; GitLab and Bitbucket offer similar services. GitHub's most important contribution to how teams actually collaborate is not Git itself but the pull request: a request to merge one branch into another that a teammate reviews, comments on, and approves before the merge actually happens. Instead of Rohit's tip-percentage change landing directly on main, he would push his branch to GitHub and open a pull request; Priya and Aarav would see the diff, discuss the clash with Priya's own branch in writing, and merge only once everyone agreed — the same resolution reached above, but visible to the whole team and preserved as a written discussion instead of settled in a rushed phone call.
Back at the Tech Fest
Redo that first night with Git in place, and the chaos does not happen. Aarav's initial commit is the one shared history everyone builds from. Priya and Rohit each work on their own branch instead of their own renamed file, so there is never a split_v2.py or split_final.py to lose track of. When their changes touch the same line, Git does not silently pick one and discard the other the way copy-pasting between email attachments did — it stops, shows exactly which lines disagree, and waits for a human decision, which is then saved permanently as its own commit. Nothing is ever deleted by accident, because every earlier snapshot still sits in the repository's history, retrievable with git log at any time. The tool does not decide what the code should do; it makes sure that whatever the team decides is recorded precisely, attributed correctly, and never quietly overwritten again.
Think About It
Think about this: How would you explain version control with git: collaboration for young developers to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
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: collaboration for young developers 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: collaboration for young developers to at least 3 other topics you have studied.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind version control with git: collaboration for young developers, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.