Core Fundamentals
OOP concepts, Git basics, and programming fundamentals
What are the four pillars of OOP? Explain each with examples
What are the four pillars of Object-Oriented Programming? Explain each pillar with a practical example.
The four pillars of OOP are:
1. Encapsulation - Bundling data and methods that operate on that data within a single unit (class), and restricting direct access to some components.
public class BankAccount {
private double balance; // Hidden from outside
public void deposit(double amount) {
if (amount > 0) balance += amount;
}
public double getBalance() {
return balance;
}
}
2. Abstraction - Hiding complex implementation details and showing only necessary features.
// User only sees simple interface, not internal complexity
car.start(); // Hides ignition, fuel injection, etc.
3. Inheritance - Creating new classes based on existing classes, inheriting their properties and behaviors.
class Animal { void eat() { } }
class Dog extends Animal { void bark() { } } // Dog inherits eat()
4. Polymorphism - Objects of different classes can be treated as objects of a common parent class; same method behaves differently based on the object.
Animal animal = new Dog();
animal.makeSound(); // Calls Dog's implementation
Key Points to Look For:
- Clear definitions with practical examples
- Understanding that these work together
- Real-world analogies demonstrate deep understanding
Follow-up: How do these pillars help in writing maintainable code?
Difference between abstraction and encapsulation
What is the difference between abstraction and encapsulation? Many candidates confuse these concepts.
Abstraction is about hiding complexity - showing only relevant features to the user while hiding implementation details. It answers "WHAT does it do?"
Encapsulation is about hiding data - bundling data with methods and controlling access through access modifiers. It answers "HOW is it protected?"
Key Differences:
| Aspect | Abstraction | Encapsulation |
|---|---|---|
| Purpose | Hide complexity | Hide data |
| Achieved by | Abstract classes, interfaces | Access modifiers (private, protected) |
| Focus | Design level | Implementation level |
| Example | Remote control (buttons, no circuits) | Capsule (medicine inside coating) |
// Abstraction - interface shows WHAT, hides HOW
interface PaymentProcessor {
void processPayment(double amount);
}
// Encapsulation - data is protected
class CreditCardProcessor implements PaymentProcessor {
private String cardNumber; // Encapsulated
private String cvv; // Encapsulated
public void processPayment(double amount) {
// Implementation hidden (abstraction)
validateCard();
chargeCard(amount);
}
}
Common Pitfall: Candidates often say they're the same thing. While related, abstraction is about design/interface while encapsulation is about data protection.
Follow-up: Can you have one without the other?
Abstract class vs Interface - when to use which?
What are the differences between abstract classes and interfaces? When would you choose one over the other?
Abstract Class:
- Can have both abstract and concrete methods
- Can have instance variables with any access modifier
- Supports constructors
- A class can extend only ONE abstract class
- Use when classes share common implementation
Interface:
- All methods are implicitly abstract (until Java 8 default methods)
- Variables are implicitly public static final
- No constructors
- A class can implement MULTIPLE interfaces
- Use when defining a contract/capability
// Abstract class - shared implementation
abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void breathe() { // Concrete method
System.out.println("Breathing...");
}
abstract void makeSound(); // Must be implemented
}
// Interface - capability contract
interface Flyable {
void fly();
default void land() { // Java 8+
System.out.println("Landing...");
}
}
class Bird extends Animal implements Flyable {
public Bird(String name) { super(name); }
void makeSound() { System.out.println("Chirp!"); }
public void fly() { System.out.println("Flying!"); }
}
When to Use:
| Use Abstract Class When | Use Interface When |
|---|---|
| Classes share code | Defining a capability |
| Need non-public members | Multiple inheritance needed |
| Need constructors | Unrelated classes share behavior |
| Evolving base class | API contract definition |
Key Points to Look For:
- Understanding of "IS-A" (abstract) vs "CAN-DO" (interface)
- Knowledge of Java 8+ default methods
- Practical decision-making ability
Follow-up: How have interfaces evolved in Java 8 and beyond?
Method overloading vs overriding
Explain the difference between method overloading and method overriding. Provide examples.
Method Overloading (Compile-time Polymorphism):
- Same method name, different parameters
- Happens within the SAME class
- Determined at compile time
- Return type alone cannot differentiate
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
int add(int a, int b, int c) { return a + b + c; }
}
Method Overriding (Runtime Polymorphism):
- Same method signature in parent and child
- Happens between PARENT and CHILD class
- Determined at runtime
- Must have same return type (or covariant)
class Animal {
void makeSound() { System.out.println("Some sound"); }
}
class Dog extends Animal {
@Override
void makeSound() { System.out.println("Bark!"); }
}
Key Differences:
| Aspect | Overloading | Overriding |
|---|---|---|
| Classes | Same class | Parent-child |
| Parameters | Must differ | Must be same |
| Return type | Can differ | Must be same/covariant |
| Binding | Compile-time | Runtime |
@Override |
Not used | Recommended |
Common Pitfall: Candidates sometimes think changing only return type is overloading - it's not, it's a compile error.
Follow-up: What is covariant return type?
What is the diamond problem in multiple inheritance?
What is the diamond problem in multiple inheritance? How do different languages handle it?
The diamond problem occurs when a class inherits from two classes that both inherit from a common ancestor, creating ambiguity.
Animal
/ \
Flyer Swimmer
\ /
Duck
The Problem:
If Animal has a method move() and both Flyer and Swimmer override it differently, which version does Duck inherit?
// C++ example (allows multiple inheritance)
class Animal {
virtual void move() { cout << "Moving"; }
};
class Flyer : public Animal {
void move() override { cout << "Flying"; }
};
class Swimmer : public Animal {
void move() override { cout << "Swimming"; }
};
class Duck : public Flyer, public Swimmer {
// Which move() is inherited? AMBIGUOUS!
};
How Languages Handle It:
| Language | Solution |
|---|---|
| Java | No multiple class inheritance; interfaces only |
| C++ | Virtual inheritance, explicit resolution |
| Python | Method Resolution Order (MRO) with C3 linearization |
| Scala | Traits with linearization |
Java's Interface Solution (Java 8+):
interface Flyer {
default void move() { System.out.println("Flying"); }
}
interface Swimmer {
default void move() { System.out.println("Swimming"); }
}
class Duck implements Flyer, Swimmer {
@Override
public void move() {
Flyer.super.move(); // Explicit choice
}
}
Key Points to Look For:
- Understanding of why it's called "diamond"
- Knowledge of how their primary language handles it
- Awareness of trade-offs in different approaches
Follow-up: Why did Java choose not to support multiple class inheritance?
Note: For comprehensive coverage of SOLID principles, see
03-design-principles.md.
How does polymorphism enable extensibility?
How does polymorphism contribute to building extensible software systems?
Polymorphism enables extensibility by allowing code to work with abstractions rather than concrete implementations, making it possible to add new behaviors without modifying existing code.
Without Polymorphism (Rigid):
class PaymentProcessor {
void process(String type, double amount) {
if (type.equals("credit")) {
// Credit card logic
} else if (type.equals("paypal")) {
// PayPal logic
}
// Adding new payment = modifying this class!
}
}
With Polymorphism (Extensible):
interface Payment {
void process(double amount);
}
class CreditCardPayment implements Payment {
public void process(double amount) { /* ... */ }
}
class PayPalPayment implements Payment {
public void process(double amount) { /* ... */ }
}
// Adding new payment type = new class, no modification!
class CryptoPayment implements Payment {
public void process(double amount) { /* ... */ }
}
class PaymentProcessor {
void process(Payment payment, double amount) {
payment.process(amount); // Works with ANY payment type
}
}
Benefits for Extensibility:
1. Open/Closed Principle - Add new types without modifying existing code
2. Reduced coupling - Code depends on interfaces, not implementations
3. Plugin architecture - New implementations can be added at runtime
4. Testing - Easy to mock/stub for unit tests
Real-World Examples:
- JDBC drivers (same interface, different databases)
- Servlet filters (chain of processors)
- Spring beans (dependency injection)
Key Points to Look For:
- Connection to Open/Closed principle
- Understanding of interface-based design
- Practical examples from frameworks
Follow-up: How do dependency injection frameworks leverage polymorphism?
Composition vs Inheritance - when to prefer each?
When should you prefer composition over inheritance? What are the trade-offs?
Inheritance ("IS-A"):
class Bird extends Animal { } // A Bird IS AN Animal
Composition ("HAS-A"):
class Car {
private Engine engine; // A Car HAS AN Engine
}
Prefer Composition When:
- Relationship is "HAS-A" not "IS-A"
// Bad: Stack is NOT a Vector
class Stack extends Vector { } // Leaky abstraction
// Good: Stack HAS a list
class Stack {
private List<E> items;
}
- Need flexibility at runtime
class Duck {
private FlyBehavior flyBehavior; // Can change at runtime
void setFlyBehavior(FlyBehavior fb) {
this.flyBehavior = fb;
}
}
- Avoiding fragile base class problem
// Inheritance: Changes to parent break children
// Composition: Changes are isolated
- Multiple behaviors needed
class Robot {
private MoveBehavior move;
private AttackBehavior attack;
private DefendBehavior defend;
}
Prefer Inheritance When:
1. True "IS-A" relationship exists
2. Need to reuse implementation (not just interface)
3. Leveraging polymorphism for substitutability
4. Framework requires it (e.g., extending Android Activity)
Trade-offs:
| Aspect | Inheritance | Composition |
|---|---|---|
| Coupling | Tight | Loose |
| Flexibility | Compile-time | Runtime |
| Code reuse | Automatic | Explicit delegation |
| Encapsulation | Broken (protected access) | Preserved |
| Testing | Harder | Easier |
Rule of Thumb: "Favor composition over inheritance" - but use inheritance when it truly makes sense.
Key Points to Look For:
- Understanding of both approaches' trade-offs
- Awareness of fragile base class problem
- Practical judgment on when each applies
Follow-up: Can you give an example from a framework where inheritance was the wrong choice?
What are access modifiers and their use cases?
Explain the different access modifiers and when you would use each one.
Java Access Modifiers (from most to least restrictive):
| Modifier | Class | Package | Subclass | World |
|---|---|---|---|---|
private |
Yes | No | No | No |
| (default) | Yes | Yes | No | No |
protected |
Yes | Yes | Yes | No |
public |
Yes | Yes | Yes | Yes |
When to Use Each:
private - Default choice for fields; implementation details
class BankAccount {
private double balance; // Internal state
private void audit() { } // Internal method
}
default (package-private) - Package-internal APIs
class PackageHelper { // Only used within this package
void helperMethod() { }
}
protected - Framework extension points; template method pattern
abstract class Servlet {
protected void doGet(Request req) { } // Subclass override point
}
public - API surface; things external code should use
public class UserService {
public User findById(Long id) { } // Public API
}
Best Practices:
1. Start with most restrictive (private), widen only when needed
2. Fields should almost always be private
3. Use protected sparingly - it breaks encapsulation
4. public = commitment to maintain that API
Key Points to Look For:
- Understanding of encapsulation motivation
- Knowledge of package-private (often forgotten)
- Practical judgment on visibility
Follow-up: Why should fields almost never be public?
Static vs instance members - memory implications
What is the difference between static and instance members? What are the memory implications?
Instance Members:
- Belong to each object instance
- Each object has its own copy
- Accessed via object reference
- Created when object is instantiated
Static Members:
- Belong to the class itself
- Single copy shared by all instances
- Accessed via class name
- Loaded when class is loaded
class Counter {
private static int totalCount = 0; // One copy for all
private int instanceCount = 0; // One copy per object
public Counter() {
totalCount++;
instanceCount++;
}
}
Counter c1 = new Counter(); // totalCount=1, c1.instanceCount=1
Counter c2 = new Counter(); // totalCount=2, c2.instanceCount=1
Memory Layout:
Method Area (Class Data):
┌─────────────────────┐
│ Counter.totalCount │ ← Static: ONE copy
└─────────────────────┘
Heap (Objects):
┌─────────────────────┐ ┌─────────────────────┐
│ c1 │ │ c2 │
│ instanceCount = 1 │ │ instanceCount = 1 │
└─────────────────────┘ └─────────────────────┘
Memory Implications:
| Aspect | Static | Instance |
|---|---|---|
| Memory location | Method area | Heap |
| Lifecycle | Class load to unload | Object creation to GC |
| Memory per instance | None | Size of field |
| GC eligibility | Only on class unload | When object unreachable |
Use Cases:
- Static: Constants, counters, utility methods, factory methods
- Instance: Object state, behavior that varies per instance
Pitfalls:
- Static mutable state = threading issues
- Static collections = memory leaks if not cleared
- Overuse of static = global state anti-pattern
Key Points to Look For:
- Understanding of memory layout
- Awareness of threading implications
- Knowledge of when static is appropriate
Follow-up: How can static fields cause memory leaks?
Version Control (Git)
Basic Git workflow: add, commit, push, pull
Explain the basic Git workflow with add, commit, push, and pull commands.
The Three Areas:
1. Working Directory - Your actual files
2. Staging Area (Index) - Changes marked for commit
3. Repository - Committed history
Basic Workflow:
# 1. Make changes in working directory
echo "new feature" >> file.txt
# 2. Stage changes (working → staging)
git add file.txt
git add . # Stage all changes
# 3. Commit changes (staging → local repo)
git commit -m "Add new feature"
# 4. Push to remote (local → remote)
git push origin main
# 5. Pull updates from remote (remote → local)
git pull origin main
Visual Flow:
Working Dir → Staging → Local Repo → Remote Repo
add commit push
← ← ←
checkout reset pull/fetch
Common Variations:
# Stage and commit in one step
git commit -am "Message" # Only works for tracked files
# Pull with rebase (cleaner history)
git pull --rebase origin main
# Push to set upstream
git push -u origin feature-branch
Key Points to Look For:
- Understanding of three-area model
- Knowledge of staging purpose
- Awareness of remote vs local
Follow-up: What's the difference between git pull and git fetch?
Branching strategies: GitFlow vs Trunk-based
Compare GitFlow and Trunk-based development. When would you use each?
GitFlow:
Multiple long-lived branches with structured workflow.
main ──●────────────●────────────●────────
│ ↑ ↑
develop ──●──●──●──●───●──●──●──●───●────────
│ ↑ │ ↑
feature ─────●──●──● ●──●──●
Branches:
- main - Production releases only
- develop - Integration branch
- feature/* - New features
- release/* - Release preparation
- hotfix/* - Emergency fixes
Trunk-Based Development:
Single branch with short-lived feature branches.
main ──●──●──●──●──●──●──●──●──●──●──
↑ ↑ ↑
feature ─────● ●──── ●
(short) (short) (short)
Comparison:
| Aspect | GitFlow | Trunk-Based |
|---|---|---|
| Branch lifetime | Long | Short (hours/days) |
| Merge frequency | Less frequent | Very frequent |
| Release process | Explicit release branches | Feature flags |
| Team size | Larger teams | Any size |
| CI/CD fit | Traditional releases | Continuous deployment |
| Complexity | Higher | Lower |
When to Use:
GitFlow:
- Scheduled release cycles
- Multiple versions in production
- Less mature CI/CD
- Regulatory requirements for release process
Trunk-Based:
- Continuous deployment
- Feature flags infrastructure
- Strong testing culture
- Small, frequent changes
Key Points to Look For:
- Understanding of trade-offs
- Context-dependent thinking
- Awareness of feature flags in trunk-based
Follow-up: How do feature flags enable trunk-based development?
Git merge vs rebase - pros and cons
What is the difference between git merge and git rebase? When would you use each?
Merge - Creates a merge commit joining two branches:
Before: After merge:
main: A─B─C main: A─B─C───M
\ /
feature: D─E feature: D─E
Rebase - Replays commits on top of another branch:
Before: After rebase:
main: A─B─C main: A─B─C
\ \
feature: D─E feature: D'─E'
Commands:
# Merge
git checkout main
git merge feature
# Rebase
git checkout feature
git rebase main
Comparison:
| Aspect | Merge | Rebase |
|---|---|---|
| History | Non-linear (shows branches) | Linear (cleaner) |
| Commits | Preserves original | Creates new commits |
| Conflicts | Resolve once | Resolve per commit |
| Shared branches | Safe | DANGEROUS |
| Traceability | Shows when merged | Loses branch context |
When to Use:
Merge:
- Public/shared branches
- Preserving complete history is important
- Feature branch history matters
Rebase:
- Local cleanup before pushing
- Keeping history linear
- Updating feature branch with main
Golden Rule:
Never rebase commits that exist outside your local repository
# Safe: Rebase local commits
git pull --rebase origin main
# DANGEROUS: Rebase pushed commits
git rebase main # Then force push = BAD
Key Points to Look For:
- Understanding of commit rewriting
- Awareness of shared branch danger
- Practical judgment on when to use each
Follow-up: What happens if you rebase a shared branch?
How to resolve merge conflicts?
Walk me through how you resolve a merge conflict in Git.
When Conflicts Occur:
When the same lines are modified differently in two branches being merged.
Conflict Markers:
<<<<<<< HEAD
your changes here
=======
their changes here
>>>>>>> feature-branch
Resolution Steps:
# 1. Attempt merge/rebase
git merge feature-branch
# CONFLICT: Merge conflict in file.txt
# 2. Check status
git status
# Both modified: file.txt
# 3. Open file and resolve manually
# Edit to keep correct code, remove markers
# 4. Stage resolved file
git add file.txt
# 5. Complete merge
git commit # For merge
git rebase --continue # For rebase
Tools:
# Use configured merge tool
git mergetool
# VS Code: Click "Accept Current/Incoming/Both"
# Abort if needed
git merge --abort
git rebase --abort
Best Practices:
1. Pull/merge frequently to reduce conflicts
2. Communicate with team about shared files
3. Keep commits small and focused
4. Use meaningful commit messages
Key Points to Look For:
- Understanding of conflict markers
- Knowledge of resolution workflow
- Awareness of abort options
Follow-up: How would you handle a complex conflict across multiple files?
What is a detached HEAD state?
What is a detached HEAD state in Git? When does it occur and how do you handle it?
Normal State:
HEAD points to a branch, which points to a commit.
HEAD → main → commit C
Detached HEAD:
HEAD points directly to a commit, not a branch.
HEAD → commit B (directly)
main → commit C
When It Occurs:
# Checking out a specific commit
git checkout abc1234
# Checking out a tag
git checkout v1.0.0
# During interactive rebase
git rebase -i HEAD~3
The Warning:
You are in 'detached HEAD' state. You can look around, make
experimental changes and commit them, and you can discard any
commits you make in this state without impacting any branches...
What You Can Do:
1. Just browse: Safe to look around
2. Create a branch: Save your position
3. Return to branch: Go back to normal
# Create branch from detached state
git checkout -b new-branch
# Return to existing branch
git checkout main
# If you made commits in detached state
git branch save-my-work # Creates branch at current commit
git checkout main
Danger:
Commits made in detached HEAD are "orphaned" when you checkout something else - they'll eventually be garbage collected.
Key Points to Look For:
- Understanding of HEAD reference
- Knowledge of when it occurs
- Awareness of how to recover work
Follow-up: How would you recover commits made in detached HEAD state?
Cherry-pick vs merge - use cases
When would you use git cherry-pick instead of git merge?
Cherry-pick: Apply specific commits from one branch to another.
Merge: Bring all commits from one branch into another.
# Cherry-pick specific commit
git cherry-pick abc1234
# Merge entire branch
git merge feature-branch
Visual:
Before:
main: A─B─C
\
feature: D─E─F─G
After cherry-pick E:
main: A─B─C─E'
\
feature: D─E─F─G
After merge:
main: A─B─C───────M
\ /
feature: D─E─F─G
Use Cherry-pick When:
1. Hotfixes - Apply fix to multiple release branches
git checkout release-1.0
git cherry-pick <fix-commit>
git checkout release-2.0
git cherry-pick <fix-commit>
- Selective features - Only want some commits
- Reordering commits - For cleaner history
- Backporting - Apply new feature to old version
Use Merge When:
1. Integrating complete feature branches
2. Want to preserve branch history
3. Bringing branch up to date with main
Cherry-pick Pitfalls:
- Creates duplicate commits (different SHAs)
- Can cause conflicts if cherry-picked commits depend on others
- Loses context of original branch
Key Points to Look For:
- Understanding of selective vs complete integration
- Knowledge of hotfix workflow
- Awareness of duplicate commit issue
Follow-up: How do you handle cherry-pick conflicts?
Git reset vs revert - when to use which?
What is the difference between git reset and git revert? When would you use each?
Reset - Moves branch pointer, potentially discarding commits (rewrites history)
Revert - Creates new commit that undoes changes (preserves history)
# Reset - REWRITES history
git reset --hard HEAD~1 # Removes last commit
# Revert - ADDS to history
git revert HEAD # Adds commit that undoes HEAD
Visual:
Original: A─B─C─D (HEAD)
After reset HEAD~1:
A─B─C (HEAD) [D is orphaned]
After revert D:
A─B─C─D─D' (HEAD) [D' undoes D]
Reset Modes:
--soft # Move HEAD only (keep staging and working dir)
--mixed # Move HEAD + reset staging (keep working dir) [DEFAULT]
--hard # Move HEAD + reset staging + reset working dir
When to Use:
| Scenario | Use | Why |
|---|---|---|
| Undo local unpushed commits | reset |
Safe to rewrite local history |
| Undo pushed commits | revert |
Don't rewrite shared history |
| Remove commits from history | reset |
Erases commits |
| Keep record of mistake | revert |
History preserved |
| Unstage files | reset --mixed |
Moves from staging to working |
Golden Rules:
1. Never reset shared/pushed commits - Others have those commits
2. Use revert for public history - Safe and transparent
3. Reset is for local cleanup - Before pushing
Key Points to Look For:
- Understanding of history rewriting danger
- Knowledge of reset modes
- Practical judgment on public vs local branches
Follow-up: What happens if you reset a shared branch?
How to undo the last commit?
You just made a commit and realized it's wrong. How do you undo it?
Depends on whether it's pushed or not:
Unpushed Commits (Safe to rewrite):
# Option 1: Undo commit, keep changes staged
git reset --soft HEAD~1
# Option 2: Undo commit, keep changes unstaged
git reset HEAD~1 # or --mixed
# Option 3: Undo commit AND discard changes (DANGEROUS)
git reset --hard HEAD~1
# Option 4: Amend the commit (fix message or add files)
git commit --amend -m "New message"
git add forgotten-file && git commit --amend --no-edit
Pushed Commits (Preserve history):
# Create new commit that undoes the last one
git revert HEAD
git push
# DON'T DO: Force push after reset (breaks collaborators)
# git reset --hard HEAD~1
# git push --force # BAD!
Decision Tree:
Was commit pushed?
├── No: Use reset
│ ├── Keep changes? → git reset --soft HEAD~1
│ └── Discard changes? → git reset --hard HEAD~1
└── Yes: Use revert
└── git revert HEAD
Quick Reference:
# Last commit message was wrong
git commit --amend -m "Fixed message"
# Forgot to add a file
git add file && git commit --amend --no-edit
# Wrong commit entirely (not pushed)
git reset HEAD~1
# Wrong commit (already pushed)
git revert HEAD
Key Points to Look For:
- Distinguishes pushed vs unpushed
- Knows multiple approaches
- Understands --soft, --mixed, --hard
Follow-up: How do you undo a commit from 3 commits ago?
What is git stash and when to use it?
What is git stash and when would you use it?
Git stash temporarily saves uncommitted changes, giving you a clean working directory.
Common Scenario:
You're working on a feature, but need to switch branches for an urgent fix:
# Working on feature, have uncommitted changes
git status # Shows modified files
# Can't switch branches with dirty working dir
git checkout main # Error!
# Solution: Stash changes
git stash
# Working directory is now clean
# Switch and do urgent work
git checkout main
# ... fix bug ...
git checkout feature-branch
# Restore stashed changes
git stash pop
Key Commands:
# Save changes to stash
git stash
git stash save "WIP: feature description"
# List stashes
git stash list
# stash@{0}: WIP: feature description
# stash@{1}: On main: quick experiment
# Apply most recent stash (and remove from stash)
git stash pop
# Apply stash but keep it
git stash apply
# Apply specific stash
git stash apply stash@{1}
# Delete stash
git stash drop stash@{0}
# Clear all stashes
git stash clear
# See stash contents
git stash show -p stash@{0}
Use Cases:
1. Branch switching - Clean working directory quickly
2. Pull changes - Stash local changes, pull, then restore
3. Experiments - Save work-in-progress before trying something
4. Conflict avoidance - Stash before potentially conflicting operations
Key Points to Look For:
- Understands purpose (temporary storage)
- Knows basic commands (stash, pop, list)
- Recognizes common use cases
Follow-up: What's the difference between git stash pop and git stash apply?
Explain git bisect for debugging
How does git bisect work? When would you use it?
Git bisect uses binary search to find the commit that introduced a bug.
Scenario:
Something broke, but you don't know which of the last 100 commits caused it.
Manually checking each = 100 checks (O(n))
Binary search = ~7 checks (O(log n))
How It Works:
# Start bisect
git bisect start
# Mark current (broken) commit as bad
git bisect bad
# Mark known good commit (where it worked)
git bisect good v1.0.0
# Git checks out middle commit
# Bisecting: 50 revisions left to test
# Test and mark
git bisect good # or git bisect bad
# Repeat until found
# abc1234 is the first bad commit
Visual:
Good Bad
v v
A──B──C──D──E──F──G──H──I──J──K──L──M──N
↑
Git tests here first
(middle)
Automated Bisect:
# Run test script automatically
git bisect start HEAD v1.0.0
git bisect run ./test.sh
# test.sh should exit 0 for good, non-zero for bad
Example test.sh:
#!/bin/bash
make && ./run_tests.sh
When to Use:
1. Bug appeared sometime in history
2. You have a way to test for the bug
3. Large number of commits to search
4. Can't easily trace the bug by code inspection
Commands Summary:
git bisect start
git bisect bad [commit]
git bisect good [commit]
git bisect run <script>
git bisect reset # Exit bisect mode
git bisect log # Show bisect history
Key Points to Look For:
- Understands binary search application
- Knows automated bisect with scripts
- Recognizes appropriate use cases
Follow-up: How would you bisect if some commits don't compile?