Two machines, one shared file, one git pull that refuses to merge. By the end of this lesson you'll see exactly why — and never be confused by the same error again.
Setup: laptop + remote server
Symptom: pull blocked
Cause: uncommitted edit on remote
Fix: two commands
First: the basics
Git is a snapshot recorder, not a save button.
Most file-saving tools ("Save As", Dropbox, Google Drive) overwrite or version-by-timestamp. Git does something different: every time you commit, it takes a frozen snapshot of your project — and you can return to any snapshot, ever, indefinitely.
Dropbox / Save As
You save → it overwrites the previous version (or stores a copy with a timestamp). To go back, you hunt through timestamped folders.
# your folder over timestrategy_v1.py
strategy_v2.py
strategy_v2_FINAL.py
strategy_v2_FINAL_REAL.py
strategy_v2_FINAL_REAL_v3.py← we've all seen this
Each version is a complete copy. No connection between them. No story of *why* each version exists.
Git
One file: strategy.py. Each commit is a labelled snapshot with a hash, an author, and a message. Switch between any of them with one command.
Each line is a complete saved state. The message says why. The hash is a fingerprint.
Every other concept in this lesson builds on this one idea: a commit is a snapshot you can return to. Don't lose that picture.
Vocabulary 1 of 3
A repository is a folder under git's watch.
Any folder on your computer can be turned into a git repository. The folder still holds your code as normal — git just adds a hidden .git/ subdirectory that records every snapshot you've ever taken.
Mac · ~/Documents/ExecutionProject
$ls -ladrwxr-xr-x 18 kapil staff 576 Mar 3 14:00 .drwxr-xr-x 8 kapil staff 256 Mar 3 14:00 .git/← git's storage-rw-r--r-- 1 kapil staff 1234 Mar 3 14:00 main.py
-rw-r--r-- 1 kapil staff 876 Mar 3 14:00 main_rsi_sniper.py
drwxr-xr-x 5 kapil staff 160 Mar 3 14:00 config/
drwxr-xr-x 8 kapil staff 256 Mar 3 14:00 core/$cat .git/HEADref: refs/heads/main← we're on the main branch
git init
turn this folder into a repo (creates .git/)
git clone
download an existing repo from GitHub (copies .git/ too)
rm -rf .git/
remove the repo and lose ALL history (your code files stay)
The ExecutionProject folder on your Mac and on Lightsail are both git repositories — both have their own .git/. They're two independent records of the same project, connected by pushing/pulling through GitHub.
Vocabulary 2 of 3
A commit is one snapshot, named by its hash.
Every commit gets a unique SHA-1 hash (40 hex characters; we usually show the first 7). It records: which files changed, what the new content is, who made the change, when, and a message describing why.
Mac · git show 7116485
commit 7116485e83c4148c4ba5fb56e83c4148c4ba5fb5← the hash (we shorten to 7 chars)Author: Kapil <imkapilagar@gmail.com>← whoDate: Tue Mar 3 14:08:23 +0530← when (IST)
config(rsi_sniper): ramp to 3 lots per tranche; ← message: what + why
pin live values in tests; note I1 deferral
diff --git a/config/strategies/rsi_sniper.py b/config/strategies/rsi_sniper.py- {"id": "T1", ..., "lots": 1, ...}← what was there before+ {"id": "T1", ..., "lots": 3, ...}← what's there now
Commits are immutable — once made, the hash and content never change. (You can make new commits that undo old ones, but you can't edit history.)
Two different snapshots = two different hashes. Change one character in any file → entirely new hash.
A good commit message explains why, not what. The diff already shows what changed; the message has to add the context the diff can't.
Vocabulary 3 of 3
A remote is a copy of your repo, somewhere else.
Your Mac has its own copy of the repo. Lightsail has its own copy. GitHub has another copy. Each is independent. They stay in sync by pushing commits up to GitHub and pulling commits down from it. GitHub is the meeting place — the "remote" that everyone agrees is canonical.
💻 Mac
Your dev machine. Has its own .git/. You commit and push from here.
commits a, b, c, d
☁️ GitHub (remote)
The shared canonical copy. Everyone pulls from and pushes to here.
commits a, b, c, d
☁️ Lightsail
Production box. Has its own .git/. Pulls to get new code.
commits a, b, c, d
git push
Uploads commits from your local repo to the remote. Other machines can now git pull to get them.
# on Mac, after committinggit push origin main# origin = nickname for GitHub
# main = the branch you're pushing
git pull
Downloads commits from the remote into your local repo. Plays them on top of your local history.
# on Lightsail, to get new codegit pull# implicit: pull from "origin", branch "main"
When you run git pull on the server and it errors, you're trying to download commits from GitHub. The error usually isn't about the network — it's about a conflict on disk. (We'll see exactly which one in two slides.)
One last piece before the worked example
On every machine, code lives in four separate places.
This is the mental model that makes the limbo bug obvious. Three of these we've already met — the fourth is where the trap lives.
1. Working dir
The files you edit with nano/vim. Lives on disk.
your edits go here first
2. Staging area
New idea → a holding zone between your edits and your commits.
"I plan to commit this"
3. Local history
The commits you've made on this machine. git log shows these.
past commits
4. GitHub (remote)
The shared copy. Commits land here after git push.
pushed commits
The new concept here is the staging area (slot 2). Most students assume git commit takes your edits directly and saves them — but git actually has a two-step ritual: first git add (which moves into staging), then git commit (which moves from staging into history).
Why two steps? Because a single edit session might span many files; staging lets you pick exactly which ones go into a particular commit. We'll see this matter when we walk through the live case study next.
The puzzle
You edited the file. Why isn't your change "saved"?
On Lightsail you opened config/strategies/rsi_sniper.py, changed lots: 1 → 3, hit Ctrl+S, and walked away. A few days later you ran git pull to get new code from GitHub and saw this:
ubuntu@LIGHTSAIL-PROD ~/ExecutionProject
$git pullUpdating cf26643..7116485error: Your local changes to the following files would be overwritten by merge:
config/strategies/rsi_sniper.py
Please commit your changes or stash them before you merge.
Aborting
The file is sitting on disk. The edits look fine when you cat the file. But git refuses to do anything with it. Why?
Mental model · zoom in
The same four areas, each one explained.
We previewed these three slides ago. Here's each one in detail — what it is, where it lives, what moves things in and out. A single file can exist in any subset of these at any time, and a lot of confusion comes from not knowing which subset it's in right now.
1. Working directory
The files you see when you ls and edit with nano/vim. Lives on disk. Changes here are not tracked by git until you tell it.
rsi_sniper.py
2. Staging area
A holding zone. Files you've told git "I want this in the next commit" via git add. Not yet saved to history.
(empty)
3. Local history
Permanent snapshots. Each git commit appends one. This is what git log shows. Lives in .git/ on your machine.
(empty)
4. GitHub (remote)
A copy on someone else's server. Reachable only after git push. Other machines pull from here.
(empty)
git add
git commit
git push
Each verb moves your file across exactly one boundary. None of them happen automatically. Git doesn't watch the disk.
Interactive · follow the file
Watch a single edit travel through all four areas.
1. Working dir
On disk. Editor changes show up here first.
—
2. Staging
"I want this in my next commit."
—
3. Local history
Permanent snapshot.
—
4. GitHub
Remote, sharable.
—
git add
git commit
git push
Try it →
Start here. Click "1. edit file" to simulate opening rsi_sniper.py in nano and changing lots: 1 → 3. Watch where the change lands.
The trap
What if you stop after step 1?
You're on Lightsail at 14:30 IST. Market's moving. You nano the file, fix the lot count, save, and run. The strategy works. No error, no warning. The file does what it says.
1. Working dir
Your edit lives here.
rsi_sniper.py LIMBO
2. Staging
Empty. git add never ran.
(empty)
3. Local history
Empty for this edit. No commit exists.
(empty)
4. GitHub
Doesn't know your edit exists.
(empty)
The file works. The strategy reads it. The trades fire correctly. But the edit only exists in one of git's four places — and it's the volatile one. Git history doesn't know.GitHub doesn't know.Your Mac doesn't know.
Days later · the collision
Now someone pushes to GitHub. You go to pull.
💻 Mac
✓ Edits → commit → push (disciplined)
✓ Pushed BFO → BSE_FO fix to GitHub
✓ Working tree clean
All four areas hold the same content. Nothing in limbo.
☁️ Lightsail
⚠ Edited rsi_sniper.py 5 days ago
⚠ Never ran git add or git commit
✗ Working dir ≠ local history
One area (working dir) holds an edit the other three don't know about.
git pull on Lightsail tries to update the same file from GitHub. Mac's commit changed line 37. Your uncommitted edit changed line 55. Git sees the collision before it tries to merge — and refuses, because applying the pull would silently overwrite your uncommitted work.
This is a feature, not a bug. Git is protecting you. The error message is git saying: "I won't lose your work without you telling me what to do with it."
The error · word by word
Now the message makes sense.
ubuntu@LIGHTSAIL-PROD ~/ExecutionProject
$git pullUpdating cf26643..7116485# GitHub has new commits. Git wants to apply them.error: Your local changes to the following files would be overwritten by merge: config/strategies/rsi_sniper.py# ← The limbo file. Edit in working dir only.Please commit your changes or stash them before you merge.Aborting# Nothing changes. Pull is rolled back. You're at the old state.
0
commits actually pulled in
10
commits still waiting on GitHub
1
file in limbo, blocking everything
The fix is to resolve the limbo. Either commit the edit (it becomes a real change you can keep), or discard it (admit it was a mistake / test / overtaken by main). After that, the pull goes through.
Recovery · pick one
Resolve the limbo, then pull.
Path A · Keep the edit
If the local edit was real work (e.g. you really did want lots=3 there), commit it first. The pull will then merge cleanly (or conflict if main also touched the same lines — manageable).
# on Lightsailgit add config/strategies/rsi_sniper.py
git commit -m "ops: bump lots to 3"
git pull # may auto-merge
git push # back to GitHub
Path B · Discard the edit
If the local edit was a test / wrong / superseded by main's incoming commit, throw it away. Pull then runs cleanly with zero conflict.
# on Lightsailgit diff config/strategies/rsi_sniper.py
# READ what you're about to losegit checkout -- config/strategies/rsi_sniper.py
# DISCARD — the edit is gonegit pull
Never run Path B without Path A's git diff first. Discarding is irreversible — git doesn't save the edit anywhere before deleting it.
The trap, step by step
A worked example: how the four places fall out of sync.
1
The laptop ships code · main advances
You commit a real change to strategy.py on your laptop and push it to GitHub. The remote is now ahead of the server.
2
The server has a limbo edit · uncommitted tweak
At some point, someone SSH'd in and edited strategy.py directly on the server — a quick parameter bump, never committed. It just sits there in the working dir.
3
Pull blocked · error: would be overwritten
You run git pull on the server. Git refuses: the incoming version of strategy.py collides with the uncommitted edit. Git won't silently throw your edit away, so it stops.
4
Recovery · decide, then act
Path A if the local edit matters: git diff, then commit it, then pull (git merges). Path B if the local edit is junk: git diff to confirm, then git checkout -- <file>, then git pull. Either way: two or three commands, working tree clean, server back in sync.
How to never see this again
One rule: no edit rests in limbo.
Every edit on every machine goes forward (commit + push) or backward (discard). Nothing in between. This is the entire fix.
After every edit on Lightsail
# save it forwardgit add <file>
git commit -m "ops: what + why"
git push origin main
Now the edit lives in all four places. Mac can pull it. Future you can read the commit message and remember why.
If the edit was a test / mistake
# confirm what you're throwing awaygit diff <file># then nuke itgit checkout -- <file>
Working tree clean again. Free to pull, switch branches, do whatever. No phantom edits hanging around.
A clean git status is the goal state. Aim for it after every edit session.
If git status shows modified:, you have a decision to make right now, not later.
Edits made under time pressure are exactly the edits that get forgotten. So have the discipline before the pressure, not during.
The other way pull fails
The file git never knew about.
Last time the limbo was an edit to a tracked file. There's a second flavour, and it's just as common: a whole new file you created on a machine but never git added. Git treats it as a stranger — not a missing commit, but an unknown object sitting in the folder.
💻 Mac
⚠ Created scripts/pnl_total.py directly
⚠git status shows ?? scripts/pnl_total.py
⚠ Never ran git add
☁️ Lightsail
⚠ Same file copied over via scp weeks ago
⚠git status shows ?? scripts/pnl_total.py
⚠ Also never added
Both machines have a working file. Both are running it. Neither has told git the file exists. The two copies are independent strangers that happen to share a name — git on Mac has no idea Lightsail's file is "the same" file, and vice versa.
The ?? prefix in git status is git's way of saying: "I see this thing in the folder, but it's not under my care. I won't track it, won't push it, won't pull it. Until you say otherwise, it's invisible to my history."
Mac commits · Lightsail tries to pull
Now git sees a name collision it can't resolve safely.
1
Mac: git add + commit + push
Now pnl_total.py exists in git history. The remote on GitHub has it. Mac's working dir and history agree. Clean.
2
Lightsail: git pull
Git fetches the new commit. It sees: "a brand-new file called scripts/pnl_total.py should land on the shelf, owned by me." It walks to the slot — and finds a stranger's file already sitting there.
3
Git stops. Refuses to overwrite.
Git has no record of when the existing file appeared, what it contains, or whether you'd want it preserved. So it would rather block the pull than risk destroying unbacked-up work.
ubuntu@LIGHTSAIL-PROD ~/ExecutionProject
$git pullerror: The following untracked working tree files would beoverwritten by merge: scripts/pnl_total.py# ← the strangerPlease move or remove them before you merge.Aborting# pull rolled back, you're stuck
Note the wording: untracked working tree files would be overwritten. This is a different error from the tracked-file version (Your local changes ... would be overwritten by merge). Same protection principle, different cause.
Recovery · the safe move
Diff first. Delete second. Pull third.
Step 1 · Confirm the stranger is safe to discard
The two copies are almost certainly identical (you put both there). But "almost" isn't "is." Diff the local stranger against the incoming version before you delete anything.
# on Lightsailgit fetch origin
git diff origin/main -- scripts/pnl_total.py
# shows what would CHANGE if you accepted the pull
No output = files are byte-identical. Some output = the remote has changes you'd be picking up (which is fine, that's why you're pulling).
Step 2 · Remove the stranger, then pull
Once you've confirmed the diff is harmless (or actively wanted), delete the untracked copy. The slot becomes empty, git happily writes the new tracked version, and from this moment forward the file is under git's care everywhere.
# on Lightsailrm scripts/pnl_total.pygit pull
# now resolves cleanly
Future git pulls for this file will just work — no ??, no collision. The one-time dance is over.
Never rm an untracked file without diffing first. Untracked means git has zero copies — nothing to recover from if you were wrong.
The real fix is preventative: never let a file sit untracked across multiple machines. The moment you create a file you want to keep, git add + commit + push it from one machine. The other machines pick it up via pull from then on.
Lesson · takeaway
Saving the file is not saving the change.
Git's job is to track history, not to watch your disk. An edit on disk is just one of four places it has to be to be safely recorded. The other three only happen when you tell git so. The "limbo state" is when you skip steps 2-3-4 and walk away — and it's always free in the moment, but it always costs you later.
There are two flavours of limbo, and both end in the same error: git pull aborts. One is an edit to a tracked file. The other is a whole new file that was never tracked. Same protection principle: git won't destroy work it can't recover.
Trading systems run on two or three machines. Multiply this trap by the machine count. Every machine you SSH into is another candidate for limbo. The fix is the same on each: edit → add → commit → push, every single time.