Git and GitHub#
This chapter introduces Git and GitHub, two powerful tools that allow developers to track the full history of a project and efficiently manage version control, including the ability to revert to previous versions.
Table of Contents#
What is Git?
What is GitHub
What is Git?#
Git is a version control system. It helps you:
track changes in your files
save project history
collaborate with others
restore older versions when needed
make new branches of your project and work there without affecting the main files of project.
In this section we learn about history and snapshots, then later commands such as log, branch, merge, rebase, and revert.These are fundamental commands in daily usage of git.
Git runs on your own computer. GitHub is an online platform that hosts Git repositories. In order to turn a floder into a Git repository, we do
as follows:
Configure Git; git config#
Git uses configuration settings such as your name, email, editor, pager behavior, and aliases. The following commands uses to setup the configuration.
Note
Set your identity
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
These values appear in your commits.
View all configuration
git config --list
View one specific value
git config user.name
Set the default editor
git config --global core.editor "code --wait"
Enable color output
git config --global color.ui auto
Create aliases
Git aliases let you define shorter names for longer commands.
git config --global alias.co checkout
git config --global alias.br branch
git config --global alias.cm commit
git config --global alias.st status
git config --global alias.lg "log --oneline --graph --all"
Then you can use:
git co main
git br
git cm -m "Fix typo"
git st
git lg
Git Configuration Levels
Git supports three configuration levels:
--system→ applies to the whole system--global→ applies to the current user--local→ applies only to the current repositoryPriority
If the same setting exists in multiple places, the order is:
local > global > system
This means local settings override global settings, and global settings override system settings.
Show Configuration Sources
To see both the configuration values and where they come from, use:
git config --list --show-origin
Example output
file:/etc/gitconfig core.editor=vim
file:/home/user/.gitconfig user.name=Ali
file:.git/config core.repositoryformatversion=0
Disabling Git Pagers
Git often uses a pager such as less to display long output. For example, git log may open inside a scrollable interface.
You can disable the pager for a specific command:
git config --global pager.log off
This makes git log print directly in the terminal.
Other examples
git config --global pager.diff off
git config --global pager.show off
git config --global pager.config off
git config --global pager.stash off
git config --global pager.help off
git config --global pager.blame off
git config --global pager.branch off
git config --global pager.annotate off
A Better Pager Setup
Instead of disabling all pagers, many developers prefer a smarter pager configuration:
git config --global core.pager "less -FRX"
This keeps the benefits of a pager while making it less intrusive.
Initialize a Repository; git init#
To turn a folder into a Git repository, go to the folder path and type the following command in the terminal:
git init
If you want the default branch to be main immediately, use:
git init -b main
The above command:
creates a new Git repository
creates the initial branch with the name
main
This is cleaner than using git init first and renaming the branch later.
Stage and Commit Changes; git add, git commit#
A common Git workflow looks like this:
git add .
git commit -m "your message"
Note
git add .stages the changesgit commit -m "..."records those staged changes into Git history
Example
git add app.js
git commit -m "Fix login bug"
This means the changes in app.js are now stored as a commit with the message Fix login bug.
View Commit History with git log#
To see commit history, use:
git log
What it shows
For each commit, Git usually shows:
commit hash
author
date
commit message
There are several ways to show the history of a project as follows:
Note
One-line history
git log --oneline
Graph view
git log --oneline --graph
Last 5 commits
git log -n 5
Show full patch with each commit
git log -p
Show history for one file
git log file.txt
Compare Changes with git diff#
To compare changes, use:
git diff
Note
There are git diff variations and options:
Basic meaning; unstaged changes
This shows changes that are not staged yet.
git diff
Example
- console.log("Hello");
+ console.log("Hello World");
Staged changes
git diff --staged
or
git diff --cached
Compare with the latest commit
git diff HEAD
Compare two commits
git diff commit1 commit2
Compare one file
git diff file.txt
Show only file names
git diff --name-only
Branching; git branch and git switch#
Why Branches Matter
A branch gives you a separate line of development. Instead of putting every change directly onto main, you can isolate work.
Why Branches Are Important Branches let you:
keep main stable
work on features or fixes separately
experiment without damaging the main line
create clean Pull Requests later
integrate work step by step
Core Idea A branch is not a second full copy of the project. It is a movable pointer into commit history. Here is the most commons commands for branching and switiching
Note
List available branches on your project
git branch
Create a new branch
git branch feature/login
Switch to another branch
git switch main feature/login
Create and switch in one step
git switch -c feature/login
Why This Matters
Branching should be understood before Fork and Pull Request workflows, because PRs are usually built from branches.
A cleane everyday local workflow is as follows:
⭐ Summary
main → create branch → make changes → commit → switch back when needed
Example:
git switch main
git pull
git switch -c fix/readme-typo
git add README.md
git commit -m "Fix typo in README"
Key Takeaway
Before learning collaboration on GitHub, a learner should already be comfortable with:
creating a branch
switching branches
committing on a branch
understanding that work stays isolated until integrated
When a Git repository is not connected to a remote (e.g., GitHub), the entire workflow happens locally on your machine. You create or modify files, stage changes using git add, and record them with git commit. You can organize work using branches (git switch -c <branch>), move between them (git switch), and integrate changes via merging (git merge). All history, experimentation, and version control remain isolated within your local repository, and no synchronization commands like git push or git pull are required since there is no external repository involved.
In the next chapter we learn how to create a remote repository and how to communicate with the local version on our machine. A clean workfolw often looks likes this:
git switch main # Go to the main branch on your local machine
git pull # Get all changes on the main branch of remote repo and update your main local
git switch -c fix/readme-typo # make a new branch and switch on it and then make your edition on README.md
git add README.md # stage your changes on the fix/readme-typo branch
git commit -m "Fix typo in README" # save changes with history
What is GitHub#
A local repository is enough for solo experimentation, but collaboration requires a shared remote repository. GitHub provides that shared remote space.
Why Remotes Matter
They allow you to:
back up your project online
collaborate with teammates
open Pull Requests
fetch and push shared history
Important Distinction
local Git = history on your machine
remote GitHub repo = shared history online
📌 Goal: Connecting an Existing Local Git Repository to GitHub
Note
You already have a local Git repository and want to:
Create a repository on GitHub
Link (connect) it to your local repo
Push your code to GitHub
Create a GitHub Account
To use GitHub effectively, the first step is to create a user account.
Steps
Go to GitHub.
Click Sign up.
Enter:
your email address
a password
a username
Verify your email address.
Complete the sign-up process.
Connect Git to GitHub#
After creating your account, you can create repositories, upload code, and connect Git on your computer to GitHub. This is the standard pipeline for creating a repository and working on it on a daily basis.
Note
🧱 Step 1 — Create a Repository on GitHub
Go to GitHub
Click New repository
Choose a name (e.g.,
my-project)Do NOT initialize with README,
.gitignore, or licenseClick Create repository
🔗 Step 2 — Add Remote to Your Local Repository
In your terminal (inside your project folder):
git remote add origin git@github.com:USERNAME/REPOSITORY.git
Example:
git remote add origin git@github.com:john/my-project.git
✔ This connects your local repo → GitHub repo
🔍 Step 3 — Verify Remote Connection
git remote -v
Expected output:
origin git@github.com:USERNAME/REPOSITORY.git (fetch)
origin git@github.com:USERNAME/REPOSITORY.git (push)
🚀 Step 4 — Push Your Code to GitHub
If your main branch is main:
git push -u origin main
If it’s master:
git push -u origin master
✔ -u sets upstream so future pushes are simpler:
git push
Warning
⚠️ Common Issues
❌ Permission denied (publickey) → SSH key not set up
❌ Repository not found → Wrong URL or repo name
❌ Branch mismatch → Use correct branch (
mainvsmaster)
HTTPS vs SSH for GitHub#
There are two common ways to connect Git to GitHub:
HTTPS
git clone https://github.com/user/repo.git
simpler to start with
commonly available by default
often uses a token for authentication
SSH
git clone git@github.com:user/repo.git
very common on Linux and macOS
excellent for repeated use
avoids typing credentials repeatedly
useful beyond Git, for server administration as well
SSH is widely considered worth learning.
What Is SSH?
SSH stands for Secure Shell. It is a secure network protocol used to connect to remote systems over the internet.
Note
Important clarification
SSH is not something separate from the internet. It is a secure way of communicating over the internet.
Why SSH matters
Without encryption, data may be exposed. With SSH, communication is encrypted and authenticated.
Generate SSH Keys
To use SSH with GitHub, you normally create an SSH key pair on your own computer using the following commands:
ssh-keygen -t ed25519 -C "your_email@example.com"
Then your operating system creates two files:
private key
public key
Usually they are stored in:
~/.ssh/
For example:
~/.ssh/id_ed25519→ private key~/.ssh/id_ed25519.pub→ public key
Which Part Stays Private and Which Part Goes to GitHub?
Private key
stays on your computer
must never be shared
is used to prove your identity
Public key
can be shared
is copied to GitHub
allows GitHub to recognize your computer
You do not copy or upload the private key to GitHub.
How GitHub Verifies That You Have the Correct Private Key
A common question is:
If GitHub only has my public key, how does it know I have the right private key?
The answer is cryptographic authentication.
Simplified process
GitHub already has your public key
During login/authentication, GitHub sends a challenge
Your computer uses the private key to sign that challenge
GitHub checks the signature using the public key
If the signature is valid, authentication succeeds
Warning
Key point
The private key never leaves your computer. You do not paste it anywhere. Only the cryptographic proof is sent.
Add the Public Key to GitHub
To display your public key so you can copy it:
cat ~/.ssh/id_ed25519.pub
Copy the output and add it to your GitHub account under SSH keys.
Only the .pub file should be copied to GitHub.
Test the SSH Connection
After adding the public key to GitHub, test the connection with:
ssh -T git@github.com
Meaning of -T
-T means:
do not open an interactive shell
only test authentication
Successful output
Hi username! You've successfully authenticated, but GitHub does not provide shell access.
This means:
SSH is configured correctly
GitHub recognizes your key
Git operations over SSH should work
Common SSH Test Errors
First-time host verification
You may see:
Are you sure you want to continue connecting (yes/no)?
Type:
yes
This stores GitHub’s host fingerprint on your machine.
Permission denied
You may see:
Permission denied (publickey).
This usually means one of the following:
the public key was not added to GitHub
the wrong key is being used
the key was not loaded by the SSH agent
the file path is wrong
Check Whether Your SSH Key Exists
To see files in your SSH directory:
ls ~/.ssh
You should usually see files like:
id_ed25519
id_ed25519.pub
Check Whether the Key Is Loaded
To list keys currently loaded in the SSH agent:
ssh-add -l
If your key is not loaded, add it:
ssh-add ~/.ssh/id_ed25519
Do You Need to Run
ssh -T git@github.comEvery Time?
No.
You usually run:
ssh -T git@github.com
only for:
initial setup
troubleshooting
verifying that authentication works
Note
In normal daily use
You do not run the SSH test every time.
Instead, you just use Git normally:
git clone git@github.com:user/repo.git
git pull
git push
SSH authentication happens automatically in the background.
Note
Recommended Mental Model
You can think of the SSH process like this:
your computer keeps the private key
GitHub stores the public key
when needed, your computer proves ownership of the private key
GitHub verifies the proof with the public key
This is why SSH is secure and convenient.
Warning
never share the private key
only upload the public key
protect the private key with a passphrase if possible
Why learning SSH is valuable
SSH is useful not only for GitHub, but also for:
remote server access
cloud systems
DevOps workflows
secure file transfer
Note
These are all the commands you have learned so far. Try to explain what each of them does.
# Initialize a repository
git init
git init -b main
# Stage and commit
git add .
git commit -m "message"
# View history
git log
git log --oneline
git log --oneline --graph
git log -n 5
git log -p
# View differences
git diff
git diff --staged
git diff --cached
git diff HEAD
git diff commit1 commit2
git diff file.txt
git diff --name-only
# Git configuration
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
git config --list
git config --list --show-origin
# Disable pagers for selected commands
git config --global pager.log off
git config --global pager.diff off
git config --global pager.show off
git config --global pager.config off
git config --global pager.stash off
git config --global pager.help off
git config --global pager.blame off
git config --global pager.branch off
git config --global pager.annotate off
# Better pager setup
git config --global core.pager "less -FRX"
# Generate SSH keys
ssh-keygen -t ed25519 -C "your_email@example.com"
# Show public key
cat ~/.ssh/id_ed25519.pub
# Test SSH
ssh -T git@github.com
# Check SSH files
ls ~/.ssh
# Check/add SSH key in agent
ssh-add -l
ssh-add ~/.ssh/id_ed25519
Git Clone#
If you have a remote repository on GitHub and want to create a copy of it on your local machine, this process is called cloning.
Cloning downloads the entire repository, including its history, branches, and files, to your local environment. It also automatically connects your local repository to the remote repository.
The basic command to clone a repository is:
git clone git@github.com:OWNER/PROJECT.git
After Cloning
Once you clone a repository, Git automatically sets up a connection to the remote repository. You can verify this using:
git remote -v
What Does origin Mean?
After cloning, Git assigns a default name to the remote repository:
origin
originis simply a conventional name for the remote repositoryIt refers to the source you cloned from
You can rename it, but most developers keep it as
origin
Note
Common Commands
Push (send changes to GitHub)
git push origin main
Sends your local commits to the main branch on the remote repository
Pull (get updates from GitHub)
git pull
Fetches and merges changes from the remote repository into your local branch Key Concepts
GitHub Collaboration Models#
There are two major GitHub collaboration models: Fork workflow and collaboration workfolw. Fork Workflow used when you do not have write access to the main repository and Contributor Workflow used when you do have write access to the main repository.
Fork#
A fork is a copy of a GitHub repository that is created under your own GitHub account. This is different from a local copy on your computer. A fork lives on GitHub, not just on your machine.
Mental Model
Suppose there is an original repository:
original repository:
github.com/original-owner/projectyour fork:
github.com/your-username/project
The original repository is often called:
upstream
Your personal copy is usually connected as:
origin
So in a typical fork workflow:
upstream= the original repositoryorigin= your fork on GitHub
When Do We Use a Fork?
Forks are commonly used when you do not have direct write access to the original repository.
Typical Cases
Open-source contribution You want to improve a public project, but you are not part of the core team.
Safe experimentation You want to try changes freely without affecting the original repository.
Personal customization You want to maintain your own version of a project with custom changes.
External collaboration You are contributing from outside the main organization or team.
When a Fork Is Usually Not Needed
If you are already a member of the project team and have write access, the team may prefer this workflow instead:
clone the main repository directly
create a branch inside that repository
push your branch
open a Pull Request from that branch
Note
Fork vs Clone
These two concepts are related, but they are not the same.
Fork: A fork creates a copy of a repository on GitHub under your account.
Clone: A clone creates a copy of a repository on your local computer.
Standard Order
In a fork-based contribution workflow, the normal order is:
Fork on GitHub → Clone to your computer
So first you create the fork online, and then you clone your fork locally.
Do We Get the Full History After Forking?
Yes. In normal GitHub usage, a fork preserves the repository history. That means your fork contains:
commits
branches and references relevant to the forked project
tags (depending on repository state and hosting behavior)
the complete evolution of the project
So conceptually, you inherit the project history. However, having the history does not automatically make you a contributor to the original project.
Are You a Contributor Just Because You Forked?
No. Forking a repository means:
you now have your own copy of the project
you can work on it independently
you can propose changes
But it does not mean that you are already a contributor to the original repository.
When Are You Usually Considered a Contributor?
In practice, you become a contributor when your changes are accepted into the original project, usually by:
opening a Pull Request
having it reviewed
and getting it merged
So this statement is accurate:
You may have the full repository history in your fork, but you are not yet a contributor to the original project until your contribution is accepted there.
The Full Fork Workflow
Here is the standard end-to-end workflow:
Fork → Clone → Add upstream → Create branch → Edit → Commit → Push → Pull Request → Review → Merge
We will now go through each part carefully.
Step 1: Fork the Repository on GitHub
On GitHub:
open the original repository
click Fork
GitHub creates a copy under your own account
Example:
original:
github.com/original-owner/projectfork:
github.com/your-username/project
At this point, you have your own GitHub-hosted copy of the project.
Step 2: Clone Your Fork Locally
After forking, clone your fork, not the upstream repository.
git clone git@github.com:YOUR_USERNAME/PROJECT.git
cd PROJECT
Now your local repository is connected to your fork as origin.
Step 3: Add the Original Repository as
upstream
This is a very important step. You usually want two remotes:
origin→ your forkupstream→ the original repository
Add upstream like this:
git remote add upstream git@github.com:ORIGINAL_OWNER/PROJECT.git
Then verify your remotes:
git remote -v
Expected result:
origin git@github.com:YOUR_USERNAME/PROJECT.git
upstream git@github.com:ORIGINAL_OWNER/PROJECT.git
Why Is upstream Important?
Because the original project keeps moving forward. If you only work with your fork and never sync from upstream:
your fork becomes outdated
your branch may drift away from the current project state
your Pull Request may become harder to review or merge
Step 4: Create a New Branch for Your Change
You should not usually work directly on main for a contribution. Instead, create a branch for each separate change.
Why?
Because branches help you:
isolate your work
keep
maincleanopen focused Pull Requests
revise one change without mixing it with another
Recommended Command
git switch -c fix-readme-typo
This command does two things at once:
creates a new branch named
fix-readme-typoswitches to that branch immediately
Step 5: Make Your Changes
Now edit the project files.
Examples:
fix a typo
improve documentation
add a small feature
refactor a function
update tests
At this point, your changes are only on your local branch.
Stage and Commit the Changes
After editing, stage and commit your work.
git add .
git commit -m "Fix typo in installation guide"
Good Commit Message Advice
A good commit message is:
specific
short
action-oriented
Examples:
Fix typo in READMEAdd validation for email fieldImprove setup instructions for macOS
Avoid vague messages like:
updatechange stufffixStep 6: Push the Branch to Your Fork
Now push your branch to origin, which is your fork.
git push origin fix-readme-typo
This sends your branch to GitHub under your fork.
Step 7: Open a Pull Request
This is the key step. A Pull Request (PR) is a request asking the maintainers of the original project to review and potentially merge your changes.
Meaning of a Pull Request
A Pull Request is basically saying:
I made these changes in my branch. Please review them and merge them into the main project if they are acceptable.
Typical Direction
your fork / your branch → upstream / main
Example:
source:
your-username:fix-readme-typotarget:
original-owner:main
How to Create a Pull Request on GitHub
Typical steps:
push your branch to your fork
go to your fork on GitHub
GitHub often shows a Compare & pull request button
click it
confirm the base repository and base branch
confirm your source branch
write a clear title
write a helpful description
submit the Pull Request
Good PR Title
Fix typo in installation guideAdd missing null check in login flow
Good PR Description Should Explain
Problem: What issue are you solving?
Change: What did you modify?
Reasoning: Why is this the right change?
Testing: How did you verify it?
Scope: Is this a small focused change or a broader change?
Example PR Description
## Summary
This PR fixes a typo in the installation section of the README.
## Details
The command name was missing a dash, which could confuse new users.
## Testing
No code changes were made. Documentation reviewed manually.
This helps maintainers review quickly and confidently.
What Happens After You Open a PR? After you submit the Pull Request, several things may happen:
Maintainers review it
Automated checks may run
tests
formatting
linting
CI pipelines
Reviewers may approve it
Reviewers may request changes
Maintainers may merge it
In some cases, they may close it without merging
This is normal. A closed PR is not necessarily a failure. Sometimes it simply means the project chose a different direction.
What If Reviewers Request Changes?
This is very common and completely normal. You do not usually create a new Pull Request for small requested changes.
Instead, you:
stay on the same branch
make the requested updates
commit the new changes
push again to the same branch
Example:
git add .
git commit -m "Address review comments"
git push origin fix-readme-typo
When you push to the same branch, the existing Pull Request updates automatically.
Why Branches Matter So Much for Pull Requests
Branches are central to PR workflows. A Pull Request is typically tied to:
one source branch
one target branch
Because of this, branches give you:
clean separation of work
easier review
better rollback options
less risk of mixing unrelated changes
Warning
Keeping Your Fork Up to Date The upstream repository changes over time. If you do not sync your fork, you can end up working on an old base.
Common Sync Workflow is
git fetch upstream
git switch main
git merge upstream/main
git push origin main
git fetch upstream
downloads the latest state from the original projectgit switch main
returns you to your local main branchgit merge upstream/main
brings upstream changes into your local maingit push origin main
updates your fork’smainon GitHub
Alternative: Rebase Instead of Merge
Some projects prefer a cleaner linear history and may encourage rebase instead of merge.
Example:
git fetch upstream
git switch my-feature
git rebase upstream/main
Why Rebase?
Rebase rewrites your branch so it appears to start from the latest upstream state. This can make history cleaner, but it is conceptually more advanced than merge.
Merge, Rebase, and Conflict Resolution#
This section adresses three critical Git topics:
merge
rebase
conflict resolution
These concepts are essential after learning branching, forks, and Pull Requests, because real collaboration almost always involves integrating changes from multiple branches. By the end of this section, you should be able to:
explain what merge does
explain what rebase does
distinguish the workflows and history produced by each
understand when a team may prefer merge or rebase
detect and resolve merge conflicts
continue or abort a merge or rebase safely
understand why conflicts happen
use practical commands in real project scenarios
Why These Topics Matter
As soon as more than one branch exists, integration becomes necessary.
Examples:
you created a feature branch and now want to bring it into
mainthe upstream repository moved forward while you were working
your Pull Request is out of date
two developers changed the same file in incompatible ways
At that point, Git must combine histories and file changes. That is where merge, rebase, and conflict resolution become central.
The Big Picture
There are two common ways to integrate branch histories:
Merge: preserves the branching history and adds a merge commit
Rebase: rewrites the branch so it appears to start from a new base
Neither is universally “better”. The right choice depends on workflow, team preference, and whether the branch has already been shared.
Imagine this simplified history.
Before Integration
A---B---C main
\
D---E feature
After Merge
A---B---C--------M main
\ /
D---E----/ feature
M is a merge commit.
After Rebase
A---B---C---D'---E' feature
The rebased commits D' and E' are new versions of the original commits D and E.
What Is a Merge?
A merge combines the histories of two branches. Suppose you have:
mainfeature
You worked on feature, and now you want those changes in main. A common workflow is:
git switch main
git merge feature
Git then attempts to combine the histories.
If it succeeds without conflict:
your work is integrated into
mainGit may create a merge commit
the branch structure remains visible in history
What Is a Rebase?
A rebase moves a branch so that it is replayed on top of another base.
Suppose this happened:
mainmoved aheadyour
featurebranch was created earlieryou now want your branch to sit on top of the latest
main
You might run:
git switch feature
git fetch origin
git rebase origin
Git then takes the commits from feature and reapplies them one by one on top of the newer base.
Typical Rebase Workflow for a Feature Branch
Suppose your branch is behind main and you want to update it before opening or finalizing a PR.
git fetch origin
git switch feature
git rebase origin/main
Then, if the branch was already pushed before, you may need:
git push --force-with-lease origin feature
Because after rebase, the branch history changed. A normal push may be rejected.
--force-with-lease is safer than plain --force because it checks that the remote state is what you expect before overwriting it.
git fetch origin
git switch feature
git rebase origin/main
git push --force-with-lease origin feature
What Is a Conflict?
A conflict happens when Git cannot automatically decide how to combine changes. This usually happens when:
two branches changed the same lines
one branch deleted a file that another branch edited
structural changes overlap in incompatible ways
two developers edited the same code block
branch A renamed or deleted something that branch B still uses
the code evolved in two directions simultaneously
a long-lived branch fell far behind
main
Git is very good at automatic merging, but it cannot safely guess human intent in every case. The longer a branch lives without syncing, the more likely conflicts become.
Example of a Conflict
Imagine main has:
def greet():
return "Hello"
And your feature branch changed it to:
def greet():
return "Hello, user"
But meanwhile main changed it to:
def greet():
return "Hi"
Git now sees that the same lines were changed differently. It cannot confidently choose one, so it reports a conflict.
When a conflict happens, Git inserts markers into the file, such as:
<<<<<<< HEAD
return "Hi"
=======
return "Hello, user"
>>>>>>> feature/login
That means
<<<<<<< HEAD
the current branch’s version=======
separator>>>>>>> feature/login
the incoming branch’s version
You must edit the file manually and remove these markers. If a conflict happens during merge:
git switch main
git merge feature
Git may stop and tell you which files are conflicted.
Then the normal process is:
open conflicted files
decide the correct final content
remove conflict markers
stage the resolved files
complete the merge
Conflict During merge Here is the standard sequence.
Step 1:Attempt the merge
git switch main
git merge feature/login
Step 2: Check status
git status
Git shows which files are unmerged.
Step 3: Open each conflicted file
Look for markers like:
<<<<<<<
=======
>>>>>>>
Step 4: Edit to the correct final result
Choose:
your version
their version
or a combination
Step 5: Stage resolved files
git add path/to/file
Step 6: Finish the merge
If needed:
git commit
Sometimes Git prepares the merge commit automatically after staging, depending on the conflict path and tooling.
Abort a Merge
Sometimes you decide the current integration attempt is not worth continuing right now.
git merge --abort
This attempts to return the repository to the pre-merge state.
Conflict During Rebase
If a conflict happens during rebase:
git switch feature/login
git rebase origin/main
Git stops at the conflicting commit.
Then you:
open the conflicted files
resolve the conflict
stage the fixed files
continue the rebase
Command:
git add .
git rebase --continue
If more conflicts appear, repeat the same process. Optional: Skip a problematic commit
git rebase --skip
Optional: Abort the whole rebase
git rebase --abort
Useful Commands During Conflict Resolution
See current status
git status
See differences
git diff
See staged differences
git diff --staged
Conflict Prevention Strategies
You cannot eliminate all conflicts, but you can reduce them.
Good Practices
Keep branches short-lived Long-lived branches drift and conflict more.
Sync frequently with main or upstream
merge from main regularly
or rebase regularly if that is the team workflow
Make smaller Pull Requests Small focused changes are easier to integrate.
Communicate with teammates If two people plan to edit the same subsystem, coordinate early.
Avoid giant unrelated commits Big mixed commits make conflict analysis much harder.
What Is Squash Merge?
A related concept is squash merge. Instead of preserving all commits from the feature branch, the branch is merged as one single commit.
This is often offered in GitHub Pull Requests.
Why Teams Use It
cleaner history on
mainless noise from many small “work in progress” commits
easier project history reading
Important Distinction
squash merge is not the same as rebase
rebase rewrites branch history before integration
squash merge compresses branch history at integration time
Contributor#
This section is for the case where you are already a contributor or team member and have write access to the main repository. In this situation, the workflow is usually different from the fork-based open-source workflow. Instead of:
Fork → Clone → Branch → Push to your fork → Pull Request
you often use:
Clone main repository → Create branch → Commit → Push branch → Pull Request
or, in some teams:
Clone main repository → Create branch → Commit → Push branch → Merge directly
depending on team policy.
By the end of this section, you should be able to:
understand how contributor workflow differs from fork workflow
know when a fork is unnecessary
clone the main repository directly
create and manage contribution branches inside the main repository
push branches to the shared repository
open Pull Requests from a branch in the same repository
understand when direct pushes to
mainare discouragedwork safely in a collaborative team environment
What Changes When You Are a Contributor?
When you are a contributor with write access, you no longer need your own fork in order to propose changes. That is the key difference.
Fork-Based Workflow
Used when:
you do not have write access
you contribute from outside the core team
you submit changes through your own fork
Contributor Workflow
Used when:
you do have write access
you are part of the organization or trusted team
you can push branches directly to the main repository
So if you are already a contributor, the repository itself can usually act as the shared collaboration space.
Do Contributors Still Use Pull Requests?
Very often, yes.
Having write access does not always mean:
push directly to
mainmerge without review
skip collaboration rules
In many professional teams, even contributors with write access still:
create branches
push those branches to the shared repository
open Pull Requests
wait for review
merge only after approval
Why?
Because Pull Requests are not only about permissions. They are also about:
review quality
design discussion
CI checks
documentation of why a change happened
protecting the stability of
main
High-Level Contributor Workflow
A common contributor workflow looks like this:
Clone main repository → Create feature branch → Edit → Commit → Push branch → Open Pull Request → Review → Merge
This is similar to the fork workflow, but the difference is where the branch lives:
in fork workflow, the branch lives in your fork
in contributor workflow, the branch lives in the main shared repository
Step 1: Clone the Main Repository
Since you already have access, you normally clone the main repository directly.
git clone git@github.com:ORGANIZATION/PROJECT.git
cd PROJECT
There is no need to fork first.
Your origin now points directly to the shared repository.
Understanding origin in Contributor Workflow
In a fork-based workflow:
originusually points to your forkupstreampoints to the original repository
But in a contributor workflow:
originusually points directly to the shared repository
Example:
git remote -v
Possible output:
origin git@github.com:ORGANIZATION/PROJECT.git
In many cases, there is no need for an upstream remote at all, because you are already working directly with the main repository.
Step 2: Update Your Local Main Branch
Before starting new work, make sure your local main is up to date.
git switch main
git pull
This is important because you usually want to branch from the latest project state.
Step 3: Create a Feature Branch
Even when you are a contributor, it is usually best not to work directly on main. Instead, create a focused branch:
git switch -c feature/improve-login-validation
or:
git switch -c fix/readme-typo
Why Branches Still Matter
Branches let you:
isolate one change
keep
mainstablemake review easier
avoid mixing unrelated work
simplify rollback and debugging
Should Contributors Push Directly to main?
Usually, no.
Even if technically allowed, many teams discourage or forbid direct
pushes to main.
Why Direct Pushes Are Risky
They can:
bypass code review
skip discussion
introduce unstable code
make auditing harder
break CI expectations
surprise teammates
Better Practice
Use:
short-lived feature branches
Pull Requests
review gates
protected branch rules
In modern team workflows, main is often protected specifically to prevent accidental direct pushes.
Protected Branches
Many GitHub repositories use protected branches.
A protected branch may require:
Pull Request before merge
at least one approval
passing CI checks
resolved conversations
linear history
no force-pushes
no direct pushes
This means that even contributors with write access still follow a formal workflow. So contributor status gives you access, but not necessarily unrestricted freedom.
Step 4: Make Changes and Commit
After creating your branch, edit the necessary files and commit your work.
git add .
git commit -m "Improve login validation for empty email input"
Good Commit Message Advice
Strong commit messages are:
clear
focused
action-based
easy for reviewers to understand
git add .
git commit -m "Improve login validation for empty email input"
Step 5: Push Your Branch to the Shared Repository
Now push your branch to the main shared repository:
git push origin feature/improve-login-validation
This is a major difference from the fork workflow.
Note
In Fork Workflow
You push to:
your fork
In Contributor Workflow
You push to:
the main repository itself
Step 6: Open a Pull Request from the Same Repository
Now create a Pull Request. In this case, both the source branch and target branch are usually in the same repository.
Example direction:
ORGANIZATION/PROJECT:feature/improve-login-validation
→
ORGANIZATION/PROJECT:main
This is different from the fork model, where the source branch lives in your fork.
Why PRs Still Matter for Contributors
A Pull Request is useful even when both branches are in the same repository.
PRs provide:
peer review
architectural feedback
CI validation
historical record of discussion
visibility for teammates
a checkpoint before code reaches
main
In strong engineering teams, the Pull Request is a collaboration tool, not just a permission workaround.
Note
Same-Repository PR vs Fork PR
Fork PR
your-username/PROJECT:my-branch
→
ORIGINAL_OWNER/PROJECT:main
Contributor PR
ORGANIZATION/PROJECT:my-branch
→
ORGANIZATION/PROJECT:main
Key Difference
The branch source lives in a different place:
fork PR → branch in your fork
contributor PR → branch in the main repository
Team Naming Conventions for Branches
Professional teams often use naming conventions such as:
feature/add-export-buttonfix/login-timeoutdocs/update-installation-guiderefactor/auth-servicetest/add-user-service-tests
These names help reviewers understand branch intent quickly.
Typical Contributor Workflow Example
Let us imagine you are part of the team and want to fix a bug.
Steps
clone the main repository
switch to
mainpull latest changes
create branch
fix/login-timeoutmake your changes
commit
push branch to
originopen Pull Request
address review comments
merge after approval
git clone git@github.com:ORGANIZATION/PROJECT.git
cd PROJECT
git switch main
git pull
git switch -c fix/login-timeout
git add .
git commit -m "Fix login timeout handling"
git push origin fix/login-timeout
What If Reviewers Request Changes?
Just like in fork workflow, you usually stay on the same branch.
Example:
git add .
git commit -m "Address review comments"
git push origin fix/login-timeout
The Pull Request updates automatically because it is tracking that same branch.
Syncing Your Branch with Main
While your Pull Request is open, main may move forward. You may need to update your branch.
Option A: Merge
maininto your branch
git switch fix/login-timeout
git fetch origin
git merge origin/main
Option B: Rebase onto
main
git switch fix/login-timeout
git fetch origin
git rebase origin/main
Which option to use depends on team policy.
What If the Team Allows Direct Pushes?
Some small teams or personal team projects do allow direct pushes to main. That workflow might look like:
git switch main
git pull
git add .
git commit -m "Small update"
git push origin main
Warning
Even if allowed, direct pushes are usually best reserved for:
tiny low-risk changes
emergency hotfixes
trusted internal workflows
solo-maintainer repositories
For most collaborative work, feature branches and PRs are still safer.
Warning
Common Mistakes Contributors Make
Working directly on
main. This increases risk and reduces review quality.Pushing half-finished work to shared branches without clarity. Teammates may review unstable code too early.
Opening giant PRs. Smaller changes are easier to review and merge.
Ignoring branch naming conventions. This makes the shared repository harder to navigate.
Forgetting to pull before creating a branch. You may branch from outdated
main.Rebasing shared team branches carelessly. If others use the same branch, history rewriting becomes dangerous.
Correction using Git reset, revert, restore, amend, and cherry-pick#
This section help to answer real-world questions such as:
How do I undo something safely?
How do I fix the last commit?
How do I restore a file?
How do I move one specific commit to another branch?
When should I rewrite history, and when should I preserve it?
By the end of this section, you should be able to:
explain the difference between reset and revert
understand the purpose of restore
amend the most recent commit safely
move a specific commit with cherry-pick
choose the correct tool depending on whether history should be rewritten or preserved
avoid common mistakes when undoing or reusing work
Why These Commands Matter
In real Git usage, people often need to correct mistakes.
Examples:
you committed too early
you forgot to include one file in the last commit
you want to undo a bad commit without deleting history
you accidentally staged the wrong file
you want to bring one bug fix from one branch into another
you want to discard local edits and go back to the committed state
These are not rare situations. They are part of normal Git work. That is why reset, revert, restore, amend, and cherry-pick are core professional tools. A useful mental model is this:
reset → move branch pointers and optionally unstage or discard changes
revert → create a new commit that undoes an earlier commit
restore → restore file contents or unstage changes
amend → modify the most recent commit
cherry-pick → apply one specific commit onto another branch
Before using above undo commands well, remember that:
Working Tree is the actual files in your project directory.
Staging Area is the set of changes prepared for the next commit.
Commit History is the already-recorded snapshots in Git.
Git reset#
git reset is one of the most powerful and potentially dangerous Git commands. Its core purpose is to move HEAD and sometimes also affect:
the staging area
the working tree
reset says: Move my current branch to a different commit, and possibly adjust staged or working changes to match.
Because of that, reset can be:
very helpful
very destructive if used carelessly
Three common forms of git reset are:
--soft--mixed--hard
git reset --soft: Moves the branch pointer, but keeps changes staged.
Example:
git reset --soft HEAD~1
This means:
move the current branch back by one commit
keep the changes from that commit staged
Use Case
You made a commit, but now want to:
rewrite the message
split the work differently
recommit in a cleaner way
This is useful when the commit itself was premature, but the content is still correct.
git reset --mixed: Moves the branch pointer and unstages changes, but keeps file modifications in the working tree. This is the default if you do not specify a mode.
Example:
git reset HEAD~1
or explicitly:
git reset --mixed HEAD~1
This means:
move back one commit
keep file changes in your working directory
unstage them
Use Case
You want to undo a commit, but then re-stage the files more selectively.
git reset --hard: Moves the branch pointer, unstages changes, and resets files in the working tree. This is the most dangerous form because it can discard local work.
Example:
git reset --hard HEAD~1
This means:
move back one commit
reset the staging area
reset the working tree
discard local changes that were part of that state transition
Warning
--hard can permanently destroy local work that is not otherwise recoverable through reflog or other advanced recovery methods.
Use it only when you are sure.
Note
When Is
resetAppropriate?
reset is most appropriate when:
you are cleaning up local history
you have not safely shared the commits yet
you want to rewrite or remove recent local commits
you want to unstage files
you want to discard local changes intentionally
reset is usually best for local correction. It is more dangerous when the commits were already pushed and other
people may rely on them.
Why
resetCan Be Dangerous on Shared Branches
If you use reset on commits that were already pushed to a shared branch:
history changes
commit IDs may disappear from the visible branch
collaborators may still have the old history
force-pushing may become necessary
confusion and integration problems can follow
Use reset freely on your own local mistakes. Use it very carefully on anything already shared.
git revert#
git revert is different from reset. Instead of moving history backward, revert creates a new commit that undoes the effect of an earlier commit. Actually, it keeps the history, but add a new commit that reverses the change. This makes revert much safer for shared history.
Example:
git revert abc1234
This tells Git:
find commit
abc1234compute the inverse of its effect
create a new commit applying that inverse
Result:
The original commit remains in history. The new revert commit records that its effects were undone.
Note
When Should You Prefer revert?
Prefer revert when:
the bad commit was already pushed
the branch is shared
you want a clear audit trail
you need to undo a change safely without rewriting history
Professional Rule:
local mistake not yet shared → often
resetshared bad commit → often
revert
git restore#
git restore was introduced to make certain file-level operations clearer.
It is mainly used to:
restore file content in the working tree
unstage files from the index
It helps separate these actions from older overloaded checkout behavior.
Example:
git restore app.py
This means:
discard local modifications in
app.pyrestore it from the current
HEADstate
Use Case
You edited a file locally and want to abandon those uncommitted edits. You accidentally staged a file and want to unstage it without losing the edit.
Unstage a File with
restore
Example:
git restore --staged app.py
This means:
remove
app.pyfrom the staging areakeep the file changes in the working tree
Restore from Another Source
You can also restore a file from a particular commit.
Example:
git restore --source=abc1234 README.md
This means:
take
README.mdas it existed in commitabc1234restore it into the working tree
This is useful when you want one file from an earlier point without changing the whole branch history.
Note
Why restore Is Useful
restore is useful because it lets you think in terms of file state, not only commit history.
It is especially helpful for:
undoing accidental local edits
unstaging files cleanly
restoring a single file from a chosen commit
It is often safer and clearer than reaching for reset when your goal is only file-level correction.
amend#
git commit --amend: Changes the most recent commit. It is commonly used when:
the last commit message is wrong
you forgot to include one file
you want to slightly adjust the most recent commit
Example:
git commit --amend
Git opens the commit message editor and lets you replace the last commit with a new version.
Amend the Last Commit Message
Example:
git commit --amend -m "Fix login validation for empty input"
This replaces the latest commit message with a new one.
Note
Even changing only the message rewrites the commit. That means the commit hash changes.
Amend to Add a Forgotten File
Suppose you made a commit but forgot one file.
git add missing_file.py
git commit --amend
Now Git rebuilds the latest commit to include that file. This is a very common and useful correction pattern.
Note
When Is Amend Safe?
Amend is safest when:
the commit is still local
you have not pushed it yet
nobody else is depending on that exact commit hash
Warning
If you amend a commit that was already pushed:
the commit hash changes
force-push may be needed
collaborators can be confused if they already used the old commit
git cherry-pick#
git cherry-pick applies the effect of one specific commit onto your current branch. Take that commit over there, and replay its effect here. This is very useful when you do not want to merge a whole branch, but only want one particular change.
Example:
Suppose commit abc1234 on another branch contains an important bug fix. You are on your current branch and run:
git cherry-pick abc1234
Git then attempts to apply that commit here as a new commit.
Result:
You get the effect of the original commit, but the new branch history remains separate.
Note
When Is cherry-pick Useful?
Cherry-pick is useful when:
one branch contains a bug fix you need elsewhere
you want a single commit, not the whole branch history
you are backporting a fix to a release branch
you want to reuse a precise change selectively
A bug is fixed on main, and the same fix is needed on a maintenance branch:
git switch release/1.2
git cherry-pick abc1234
Warning
Cherry-Pick Can Also Conflict
Cherry-pick is not magic. If the target branch differs too much, the commit may not apply cleanly. Then Git may stop with conflicts, and you resolve them similarly to merge or rebase conflicts:
git status
git add .
git cherry-pick --continue
Or, if needed:
git cherry-pick --abort
Warning
Common Mistakes
Using
reset --hardtoo casually
This can destroy local work.
Using
resetinstead ofreverton shared branches
This can make collaboration messy.
Forgetting that
amendrewrites history
Even a message-only change creates a new commit hash.
Cherry-picking large sequences without thinking
This can create duplicated or confusing history if done carelessly.
Using history-rewriting commands after public sharing without coordination
This is one of the most common professional Git mistakes.
Practical Scenarios#
Undo the Last Local Commit, Keep the Changes
You committed too early, but want to keep working. A good option is:
git reset --mixed HEAD~1
Now:
the commit is removed from visible branch history
the file changes remain in your working tree
nothing is staged
You can now stage files more selectively and recommit properly.
Undo a Shared Commit Safely
A bad commit is already on main and must be undone.
A safer option is:
git revert abc1234
This creates a new commit that reverses the earlier one. This is the professional-safe pattern for shared history.
Remove a File from Staging
You accidentally staged config.local.json, but you do not want it in the next commit.
Use:
git restore --staged config.local.json
Now:
the file is unstaged
your local edits remain
Fix the Last Commit
You committed, then realized the message is weak or one file is missing. A common correction is:
git add forgotten_file.py
git commit --amend -m "Add validation and tests for empty email input"
This replaces the previous final commit with a better version.
Backport a Fix to Another Branch
Suppose main has a bug fix, but you also need it on release/1.2.
git switch release/1.2
git cherry-pick abc1234
This applies only that selected fix to the release branch, without merging unrelated work from main.
Exercises:
make two commits in a test repository
use
git reset --soft HEAD~1and observe the staged staterepeat with
git reset --mixed HEAD~1create a new commit and undo it with
git revertmodify a file and discard changes using
git restorestage a file and unstage it using
git restore --stagedmake a commit with a bad message and fix it using
git commit --amendcreate two branches and use
git cherry-pickto move one fix from one branch to anothercompare histories using:
git log --oneline --graph --all
These exercises build intuition much faster than memorizing definitions.
git bisect#
This is the professional way when you know:
one commit that is definitely good
one commit that is definitely bad
Git then performs a binary search through history to find the first bad commit much faster.
Step 1: Start bisect
git bisect start
Step 2: Mark the current state as bad
git bisect bad
This usually means: “the version I am on right now is broken.”
Step 3: Mark an older known-good commit
git bisect good <commit-id>
Now Git chooses a commit in the middle.
Step 4: Test that commit
Run your program or your tests.
If it works:
git bisect good
If it fails:
git bisect bad
Git keeps narrowing the search until it prints something like:
<commit-id> is the first bad commit
Schematic example of git bisect
Suppose your history is:
A - B - C - D - E - F
You know:
Bwas still goodFis bad
You run:
git bisect start
git bisect bad
git bisect good B
Git may test D first.
If
Dis good, then the bug must be inEorFIf
Dis bad, then the bug must be inCorD
So instead of checking every commit one by one, Git cuts the search space in half each time.
If you already have automated tests, bisect becomes much stronger
If your project has a command that returns success/failure, Git can automate the search.
Examples:
git bisect run pytest
git bisect run npm test
git bisect run python -m unittest
In this mode:
test passes → Git treats commit as good
test fails → Git treats commit as bad
This is one of the best tools for tracking down regressions.
How this connects to reset, revert, restore, amend, and cherry-pick
After you find the problematic commit, you can decide what to do:
If the bad commit was already shared
Use:
git revert <bad-commit>
If the bad commit is only local and you want to rewrite history
Use one of:
git reset --soft <target>
git reset --mixed <target>
git reset --hard <target>
If the problem is just one file
Use:
git restore path/to/file
If the issue is only in the latest commit message or contents
Use:
git commit --amend
If you want to bring a good fix from another branch
Use:
git cherry-pick <good-commit>
So the missing skill is often not the command itself, but finding the correct commit first.