Skip to main content

Git Worktree Trick: Merge Without Checkout! πŸͺ„

Β· 12 min read
Mahmut Salman
Software Developer

The Problem πŸ˜•β€‹

Backend Dev: "I want to merge my frontend branch into main, but when I try git checkout main, Git says 'fatal: main is already checked out at ../EcommerceWebsite3-backend'. I can't checkout main because it's being used in my backend worktree!"

Frontend Mentor: "Ah! This is a perfect situation to use one of Git's hidden superpowers - pushing to a local branch without checking it out!"

The Situation πŸ—‚οΈβ€‹

Frontend Mentor: "Let me understand your setup:"

πŸ“ EcommerceWebsite3/                    (Current worktree)
β”œβ”€β”€ Currently on: frontend branch
└── Want to: merge into main

πŸ“ EcommerceWebsite3-backend/ (Backend worktree)
└── Currently on: main branch ← LOCKED!

Problem: Can't checkout main in current worktree!

Backend Dev: "Exactly! How do I merge frontend into main without checking out main?"

Frontend Mentor: "There's a clever trick using git push that most people don't know about!"

The Traditional Problem βŒβ€‹

Frontend Mentor: "Normally, you'd do this:"

# Traditional merge workflow
git checkout main # Switch to main
git merge frontend # Merge frontend into main

But in your case:

git checkout main
# fatal: 'main' is already checked out at '/path/to/EcommerceWebsite3-backend'

Backend Dev: "Right! Git won't let me checkout main because the backend worktree is using it!"

Frontend Mentor: "This is a safety feature - Git prevents you from checking out the same branch in multiple worktrees simultaneously."

The Clever Solution βœ¨β€‹

Frontend Mentor: "Here's the magic command:"

git push . frontend:main

Backend Dev: "Wait, what? We're using push? Aren't we supposed to use merge?"

Frontend Mentor: "Great question! Let me break down what this command does:"

Understanding git push . frontend:main πŸ”β€‹

Breaking Down the Command​

git push . frontend:main
↑ ↑ ↑
β”‚ β”‚ └─ Target branch (main)
β”‚ └────────── Source branch (frontend)
└──────────── Destination repository (. = current repo)

Frontend Mentor: "Each part has a specific meaning:"

  1. git push: The push command
  2. .: Push to the current repository (not a remote!)
  3. frontend:main: Push from local frontend to local main

Backend Dev: "So we're pushing from one branch to another... in the same repository?"

Frontend Mentor: "EXACTLY! It's like a local merge, but without needing to checkout the target branch!"

The Aha Moment πŸ’‘β€‹

Frontend Mentor: "Think of it this way:"

Traditional Merge (Requires Checkout)​

Step 1: Checkout main
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Working Directory: main β”‚
β”‚ HEAD β†’ main β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 2: Merge frontend
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Merge frontend commits into main β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Push to Local Branch (No Checkout!)​

Current State:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Working Directory: frontend β”‚
β”‚ HEAD β†’ frontend β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Push frontend:main
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Update main branch (without checkout)β”‚
β”‚ HEAD stays on frontend β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Backend Dev: "So push updates the main branch without changing my working directory?"

Frontend Mentor: "YES! That's the key insight!"

Visual Demonstration πŸ“Šβ€‹

Frontend Mentor: "Let me show you exactly what happens:"

Before the Push​

Worktree 1 (EcommerceWebsite3/):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HEAD β†’ frontend β”‚
β”‚ β”‚
β”‚ Branches: β”‚
β”‚ frontend: A---B---C---D---E (you are here)β”‚
β”‚ main: A---B---C ← wants to move here β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Worktree 2 (EcommerceWebsite3-backend/):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HEAD β†’ main β”‚
β”‚ β”‚
β”‚ main branch is LOCKED (checked out here) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

After git push . frontend:main​

Worktree 1 (EcommerceWebsite3/):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HEAD β†’ frontend (still here!) β”‚
β”‚ β”‚
β”‚ Branches: β”‚
β”‚ frontend: A---B---C---D---E β”‚
β”‚ main: A---B---C---D---E ← MOVED! β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Worktree 2 (EcommerceWebsite3-backend/):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HEAD β†’ main β”‚
β”‚ β”‚
β”‚ main branch updated automatically! β”‚
β”‚ (no action needed in this worktree) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Backend Dev: "So main moves forward without me having to go to the backend worktree?"

Frontend Mentor: "EXACTLY! The main branch is updated in the shared Git repository, and both worktrees can see the change instantly!"

Step-by-Step Walkthrough πŸšΆβ€‹

Frontend Mentor: "Let's walk through your exact scenario:"

Your Starting Point​

# You are in EcommerceWebsite3/ worktree
pwd
# /Users/you/EcommerceWebsite3

git status
# On branch frontend
# nothing to commit, working tree clean

git log --oneline --all --graph
# * e1f2g3h (frontend) Add product images
# * d4e5f6g Add cart functionality
# * c7h8i9j Update styles
# * a1b2c3d (main) Initial commit

The Command​

git push . frontend:main

What Git does:

1. Takes frontend branch: A---B---C---D---E
2. Pushes to main branch: A---B---C β†’ A---B---C---D---E
3. Updates main without checkout

The Result​

# Still on frontend branch (didn't move!)
git status
# On branch frontend

# But check the branches
git log --oneline --all --graph
# * e1f2g3h (HEAD -> frontend, main) Add product images
# * d4e5f6g Add cart functionality
# * c7h8i9j Update styles
# * a1b2c3d Initial commit
#
# Notice: (main) moved to the same commit as frontend!

Backend Dev: "So main now points to the same commit as frontend, but I'm still on the frontend branch?"

Frontend Mentor: "PERFECT! You've got it!"

Why This Works πŸŽ―β€‹

Frontend Mentor: "This works because of how Git internally handles branches:"

Git Branches Are Just Pointers​

Before:
frontend β†’ [commit E]
main β†’ [commit C]

After git push . frontend:main:
frontend β†’ [commit E]
main β†’ [commit E] ← pointer moved!

Backend Dev: "So git push is just moving the main pointer, just like merge would?"

Frontend Mentor: "EXACTLY! The difference is:

  • merge: Requires you to be ON the target branch
  • push: Can update target branch from anywhere"

Comparing the Approaches πŸ”„β€‹

Frontend Mentor: "Let's compare all the ways to do this:"

Option 1: Traditional Merge (Can't Do - Branch Locked!)​

git checkout main              # ❌ Can't do this!
# fatal: 'main' is already checked out

git merge frontend # Never gets here

Option 2: Push to Local Branch (Our Solution!)​

git push . frontend:main       # βœ… Works perfectly!
# Updating a1b2c3d..e1f2g3h
# Fast-forward

Option 3: Go to Other Worktree (Works but Inconvenient)​

cd ../EcommerceWebsite3-backend  # Change directory
git merge frontend # Merge here
cd ../EcommerceWebsite3 # Change back

Backend Dev: "So Option 2 is the cleanest because I don't have to change directories or checkout branches?"

Frontend Mentor: "EXACTLY! It's one command that does everything!"

The Complete Workflow πŸ“β€‹

Frontend Mentor: "Here's your complete workflow using this technique:"

# STEP 1: Make sure you're on frontend and it's up to date
git checkout frontend
git status
# On branch frontend
# Your branch is up to date with 'origin/frontend'

# STEP 2: (Optional) Rebase on main if needed
git fetch origin main
git rebase main
# Successfully rebased frontend on main

# STEP 3: Push frontend to local main
git push . frontend:main
# Updating a1b2c3d..e1f2g3h
# Fast-forward
# src/components/ProductCard.tsx | 25 +++++++++++++++++++++++++
# src/pages/Products.tsx | 15 +++++++++++++++
# 2 files changed, 40 insertions(+)

# STEP 4: (Optional) Push to remote
git push origin main
# To origin
# a1b2c3d..e1f2g3h main -> main

Backend Dev: "So I can do everything from the frontend worktree without switching?"

Frontend Mentor: "YES! You never leave the frontend worktree!"

What Happens in the Backend Worktree? πŸ”„β€‹

Backend Dev: "What happens in my backend worktree when I do this?"

Frontend Mentor: "Great question! Let's check:"

Before Push​

# In backend worktree (EcommerceWebsite3-backend/)
git status
# On branch main
# Your branch is up to date with 'origin/main'

git log --oneline -1
# a1b2c3d (HEAD -> main) Initial commit

After Push (From Frontend Worktree)​

# Still in backend worktree
git status
# On branch main
# Your branch is ahead of 'origin/main' by 4 commits

git log --oneline -5
# e1f2g3h (HEAD -> main) Add product images
# d4e5f6g Add cart functionality
# c7h8i9j Update styles
# b0a9d8e Fix bugs
# a1b2c3d Initial commit

Backend Dev: "Wait, the main branch updated automatically in the backend worktree?"

Frontend Mentor: "YES! Because both worktrees share the same Git repository. When you updated main with git push . frontend:main, the main branch pointer moved in the shared repository, which both worktrees can see!"

The Key Insight πŸ’‘β€‹

Frontend Mentor: "Here's the crucial understanding:"

Git Repository (Shared):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Branches (pointers): β”‚
β”‚ frontend β†’ commit E β”‚
β”‚ main β†’ commit E (updated!) β”‚
β”‚ β”‚
β”‚ Commits: β”‚
β”‚ A---B---C---D---E β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
↑ ↑
β”‚ β”‚
Worktree 1 Worktree 2
(frontend (main is
worktree) checked out)

Backend Dev: "So git push . frontend:main updates the branch pointer in the shared repository, which both worktrees can see?"

Frontend Mentor: "PERFECT! You've completely understood the concept!"

Advanced: The Syntax Explained πŸ”¬β€‹

Frontend Mentor: "Let's dive deeper into the syntax:"

General Push Syntax​

git push <remote> <source>:<destination>
↑ ↑ ↑
β”‚ β”‚ └─ Remote branch name
β”‚ └───────── Local branch name
└────────────────── Remote repository

Our Specific Case​

git push . frontend:main
↑ ↑ ↑
β”‚ β”‚ └─ Local branch (main)
β”‚ └────────── Local branch (frontend)
└──────────── Current repository (.)

Backend Dev: "So the . means 'current repository' instead of a remote like origin?"

Frontend Mentor: "EXACTLY! The . is saying 'push within this repository, not to a remote'!"

Common Use Cases πŸŽ―β€‹

Frontend Mentor: "This technique is useful in several scenarios:"

1. Worktree Branch Lock (Your Case!)​

# Can't checkout main - it's locked by another worktree
git push . frontend:main
# βœ“ Updates main without checkout

2. Batch Branch Updates​

# Update multiple branches without checking them out
git push . frontend:main
git push . bugfix:develop
git push . hotfix:release

3. Quick Branch Synchronization​

# Make feature-v2 same as feature-v1
git push . feature-v1:feature-v2
# βœ“ No checkout needed

Backend Dev: "So any time I want to update a branch without checking it out, I can use this?"

Frontend Mentor: "EXACTLY! It's a powerful technique for branch management!"

Comparing with Other Commands πŸ”β€‹

Frontend Mentor: "Let's see how this compares to other Git commands:"

git push . frontend:main​

git push . frontend:main

Effect:
- Updates main branch pointer
- No checkout required
- Works when branch is locked
- Fast and clean

git merge​

git checkout main
git merge frontend

Effect:
- Must checkout main first
- ❌ Fails if branch locked
- Traditional approach

git branch -f main frontend​

git branch -f main frontend

Effect:
- Forces main to point to frontend
- ❌ Dangerous (force update)
- ❌ Can lose commits
- Not recommended

Backend Dev: "So git push . frontend:main is the safest way to do this when the branch is locked?"

Frontend Mentor: "YES! It's safe, explicit, and doesn't require force flags!"

Safety Considerations βš οΈβ€‹

Frontend Mentor: "There are some important safety considerations:"

When It's Safe βœ…β€‹

# Safe: Fast-forward merge (main is behind frontend)
git push . frontend:main
# βœ“ No conflicts, clean fast-forward

When It Might Fail βŒβ€‹

# Fails: main has commits that frontend doesn't have
git push . frontend:main
# ! [rejected] frontend -> main (non-fast-forward)
# error: failed to push some refs to '.'

Backend Dev: "So it fails if main has moved ahead of frontend?"

Frontend Mentor: "EXACTLY! Just like a normal push would fail if the remote has commits you don't have."

The Solution: Force Push (Use Carefully!)​

# Force push (overwrites main)
git push . +frontend:main
↑
+ means force

# Or use --force flag
git push --force . frontend:main

Backend Dev: "But force pushing is dangerous, right?"

Frontend Mentor: "YES! Only use force if you're absolutely sure you want to overwrite main's history!"

Real-World Example πŸŒβ€‹

Frontend Mentor: "Let me show you a complete real-world scenario:"

The Setup​

# Morning: You're working on frontend features
cd ~/EcommerceWebsite3
git checkout frontend

# You make several commits
git add .
git commit -m "Add product filtering"
git commit -m "Add search functionality"
git commit -m "Update UI styles"

# Meanwhile, your backend worktree is on main
# ~/EcommerceWebsite3-backend/ has main checked out

The Problem​

# You want to merge to main, but...
git checkout main
# fatal: 'main' is already checked out at '.../backend'

The Solution​

# Instead, use push
git push . frontend:main
# Updating a1b2c3d..e1f2g3h
# Fast-forward
# src/components/ProductFilter.tsx | 45 ++++++++++++++++++++
# src/components/SearchBar.tsx | 30 +++++++++++++
# src/styles/main.css | 25 +++++++++++
# 3 files changed, 100 insertions(+)

# Push to remote
git push origin main
# To origin
# a1b2c3d..e1f2g3h main -> main

# Done! Never left the frontend worktree!

The Result​

# Check your work
git log --oneline --graph --all
# * e1f2g3h (HEAD -> frontend, origin/main, origin/frontend, main)
# * d4e5f6g Add search functionality
# * c7h8i9j Add product filtering
# * a1b2c3d Initial commit

Backend Dev: "So I merged frontend to main, pushed to remote, all without leaving my frontend worktree!"

Frontend Mentor: "EXACTLY! That's the power of git push . frontend:main!"

The Aha Moment πŸ’‘β€‹

Backend Dev: "So git push isn't just for pushing to remotes - I can use it to update local branches too?"

Frontend Mentor: "YES! That's the key insight! Most people only think of git push for remote repositories, but you can push to the current repository with .!"

Backend Dev: "And this works perfectly when the target branch is locked by another worktree?"

Frontend Mentor: "EXACTLY! It's the perfect solution for the worktree branch lock problem!"

Key Takeaways πŸŽ―β€‹

  1. Push to Current Repository

    • Use . as repository to push locally
    • Updates branches without checkout
  2. Perfect for Worktree Branch Locks

    • Can't checkout branch locked by another worktree
    • git push . source:target solves this elegantly
  3. Same Safety as Regular Push

    • Fast-forward only by default
    • Fails if target has diverged
    • Can force with + or --force (use carefully!)
  4. Shared Repository Effect

    • Updates branch in shared Git repository
    • Both worktrees see the change instantly
    • No need to do anything in other worktree
  5. One-Command Solution

    • No directory changes needed
    • No branch checkout required
    • Clean and efficient

Common Questions ❓​

Q: Can I push to any branch this way?​

# Yes! Push any branch to any branch
git push . feature-a:feature-b
git push . main:backup-main
git push . develop:staging

Q: Does this work with remote branches?​

# No! Remote branches need actual remote push
git push . frontend:origin/main # ❌ Won't work

# Use this instead:
git push origin frontend:main # βœ… Correct

Q: What if I want to undo this?​

# Reset main to previous position
git push . main@{1}:main

# Or force main to specific commit
git push . <commit-hash>:main

The Final Understanding πŸŽ“β€‹

Backend Dev: "So to summarize:

  1. I can't checkout main because it's locked by backend worktree
  2. Instead of merging, I use git push . frontend:main
  3. This updates main without checking it out
  4. Both worktrees see the change instantly
  5. I can then push to remote with normal git push origin main"

Frontend Mentor: "PERFECT! You've completely mastered this advanced Git technique! πŸš€"

Backend Dev: "This is so much cleaner than switching directories or unlocking branches!"

Frontend Mentor: "That's exactly why this technique is so valuable! It's one of Git's hidden gems that makes worktrees much more powerful! 🎯"


Have you discovered any hidden Git commands that make your workflow better? Share in the comments below! πŸ’­