Git’s distributed architecture means every clone is a complete repository with full history, and remotes are directly pointers to other repositories. Push uploads your commits to a remote, fetch downloads new commits without merging, and pull combines fetch with merge. The distinction between bare repositories (server-side, no working directory) and non-bare repositories (developer workstations) explains why you cannot push to a non-bare repo by default. Understanding these operations is fundamental to collaborative development workflows.
Git’s distributed architecture means there is no intrinsic client-server relationship. Any repository can act as a “remote” for any other. In practice, one repository is designated as the “canonical” or “origin” repository, and all others sync with it.
flowchart LR
subgraph "Developer A"
A1["Local repo<br/>(.git/)"]
end
subgraph "Remote (origin)"
R["Bare repo<br/>(no working directory)"]
end
subgraph "Developer B"
B1["Local repo<br/>(.git/)"]
end
subgraph "CI/CD"
C1["Runner<br/>(clone + build)"]
end
A1 -- "git push" --> R
R -- "git fetch" --> A1
B1 -- "git push" --> R
R -- "git fetch" --> B1
R -- "git clone" --> C1
style R fill:#e3f2fd Type Working Directory Purpose Non-bare (default)Yes Developer workstation — edit, commit, push Bare (--bare)No Server-side repository — receives pushes only
A bare repository is just the .git/ directory without a working tree. It is the standard format for remote servers (GitHub, GitLab, Gitea):
## Create a bare repository
$ git init --bare project.git
## Clone from a bare repository
$ git clone user@server:/path/to/project.git
remote's branch pointer, but the remote's working directory and index will not be updated, causing inconsistencies. If you need a server-side repo with a working tree, use a **post-receive hook** to check out the files.# Add a remote named "origin" (conventional name for the primary remote)
$ git remote add origin https://github.com/user/repo.git
$ git remote add upstream https://github.com/org/repo.git
# Add with a custom fetch refspec
$ git remote add origin https://github.com/user/repo.git
origin https://github.com/user/repo.git (fetch)
origin https://github.com/user/repo.git (push)
upstream https://github.com/org/repo.git (fetch)
upstream https://github.com/org/repo.git (push)
# Show details for a specific remote
$ git remote rename old-name new-name
$ git remote remove upstream
$ git remote set-url origin git@github.com:user/repo.git
Protocol URL Format Authentication Use Case HTTPS https://github.com/user/repo.gitToken / password Public repos, CI/CD, firewalled environments SSH git@github.com:user/repo.gitSSH key Frequent pushes, personal development
to inject tokens as environment variables). GitHub recommends HTTPS for all new repositories.git fetch downloads objects and references from a remote repository without modifying your working directory or branches . It updates your remote-tracking branches (e.g., origin/main) but does not merge anything.
# Fetch all remotes and all branches
# Fetch from a specific remote
# Fetch a specific branch
# Fetch and prune deleted remote branches
# Equivalent to: git fetch --all && git remote prune origin
sequenceDiagram
participant Local as Local Repo
participant Remote as Remote (origin)
Local->>Remote: git fetch origin
Remote->>Local: Send new objects (commits, trees, blobs)
Remote->>Local: Update refs/remotes/origin/*
Note over Local: Working directory UNCHANGED<br/>Local branches UNCHANGED After fetching, you can inspect the differences:
# See what commits are on origin/main that you don't have
$ git log HEAD..origin/main
# See what commits you have that origin/main doesn't
$ git log origin/main..HEAD
# See all differences (both directions)
$ git log HEAD...origin/main
git pull is a shorthand for git fetch followed by git merge (or git rebase):
# Fetch + rebase (recommended for clean history)
$ git pull --rebase origin main
# Configure rebase as default pull behavior
$ git config --global pull.rebase true
Without --rebase``git pull creates a merge commit for every pull, cluttering the history:
gitGraph
commit id: "A"
checkout origin/main
commit id: "B"
checkout main
commit id: "C (your local work)"
merge origin/main id: "D (merge commit from pull)"
commit id: "E (more local work)"
merge origin/main id: "F (another merge commit from pull)" With --rebaseYour local commits are replayed on top of the remote, maintaining a linear history:
gitGraph
commit id: "A"
checkout origin/main
commit id: "B"
checkout main
commit id: "C'"
commit id: "E'" those commits to a shared branch. If you have, see the [Golden Rule of Rebasing](../03-branching-and-merging/03-rebasing.md#the-golden-rule-of-rebasing).git push uploads local commits to a remote repository and updates the remote’s branch pointer.
# Push current branch to its tracking remote
# Push a specific branch to a specific remote
$ git push origin feature-auth
# Push and set up tracking
$ git push -u origin feature-auth
By default, Git refuses to push a non-fast-forward update:
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: "git pull ...'') before pushing again.
This prevents overwriting commits that other developers may have based their work on.
Force pushing overwrites the remote branch with your local branch, discarding any commits on the remote that are not in your local history:
$ git push --force origin feature-auth
# Safer force push: only if the remote is a fast-forward of your branch
$ git push --force-with-lease origin feature-auth
flowchart LR
subgraph "Before force-with-lease"
R1["origin/feature: A → B → C"]
L1["local/feature: A → B → D"]
end
subgraph "git push --force-with-lease"
C1{"origin/feature still<br/>at C (unchanged since fetch)?"}
C1 -->|Yes| C2["Force push succeeds"]
C1 -->|No| C3["Push rejected<br/>(someone else pushed to C)"]
end
subgraph "git push --force"
D1["Always overwrites remote<br/>(NO SAFETY CHECK)"]
end
style C3 fill:#ffcdd2
style D1 fill:#ffcdd2 - **`--force-with-lease`**: Only overwrites if the remote has not changed since your last fetch. **Always prefer this over `--force`.**Never force push main or any shared branch. The consequences are:
Other developers” histories diverge from the remote. Their next git push will be rejected. They must git pull --rebase or reset their branches, potentially losing their own unpushed commits. git clone creates a local copy of a remote repository:
$ git clone https://github.com/user/repo.git
# Clone into a specific directory
$ git clone https://github.com/user/repo.git my-project
# Shallow clone (only the latest commit, no history)
$ git clone --depth=1 https://github.com/user/repo.git
# Clone a specific branch
$ git clone --branch feature-auth https://github.com/user/repo.git
# Sparse clone (only specific directories)
$ git clone --filter=blob:none --sparse https://github.com/user/repo.git
$ git sparse-checkout set src/docs
$ git clone https://github.com/user/repo.git
This is equivalent to:
$ git remote add origin https://github.com/user/repo.git
$ git checkout -b main origin/main # or whatever the default branch is
The clone creates:
Component Description .git/Full local repository with all objects and refs Working directory Checkout of the default branch origin remotePoints to the source URL main tracking branchTracks origin/main
Variant Command Use Case Full git cloneDevelopment — full history, all branches Shallow git clone --depth=1CI/CD — only latest commit, minimal disk usage Sparse git clone --sparseMonorepos — only specific directories Mirror git clone --mirrorBackup/migration — all refs, bare repository Bare git clone --bareServer setup — no working directory
Remote-tracking branches (e.g., origin/main) are local references that represent the state of a remote branch as of the last git fetch. They are updated automatically by fetch and pullBut never by local commits .
flowchart LR
A["origin/main<br/>(remote-tracking ref)"] -->|"git fetch"| B["Updated to match<br/>remote's main"]
C["main<br/>(local branch)"] -->|"git commit"| D["Moves forward<br/>independently"]
A -->|"git merge origin/main"| C
D -->|"git push"| E["Remote's main updated"]
style A fill:#e3f2fd
style C fill:#e8f5e9 # Difference between local and remote
$ git log HEAD..origin/main # Commits on remote that you don't have
$ git log origin/main..HEAD # Commits you have that remote doesn't
# Or: fetch + rebase in one step
$ git pull --rebase origin main
# 1. Fork the repository on GitHub
$ git clone https://github.com/you/repo.git
# 3. Add the upstream repository
$ git remote add upstream https://github.com/org/repo.git
# 4. Create a feature branch
$ git switch -c feature-auth main
# 5. Keep up-to-date with upstream
$ git rebase upstream/main
$ git push origin feature-auth
# 7. Create a pull request from your fork to the upstream repo
# 1. Clone the shared repository
$ git clone git@github.com:org/repo.git
# 2. Create a feature branch
$ git switch -c feature-auth
# 3. Commit and push frequently
$ git commit -m " WIP: auth module "
$ git push -u origin feature-auth
# 4. Create a pull request
# 5. After review and approval, merge via the PR interface
Neglecting to normalise database designs, leading to data redundancy and update anomalies.
Forgetting edge cases in algorithm design (e.g., empty input, single element, already sorted data).
Misunderstanding the difference between a stack (LIFO) and a queue (FIFO) in data structure applications.
Forgetting that O ( n log n ) O(n \log n) O ( n log n ) average-case for quicksort becomes O ( n 2 ) O(n^2) O ( n 2 ) worst-case on already sorted input.
The key principles covered in this topic are linked in the sub-pages above. Focus on understanding the definitions, applying the formulas or frameworks, and evaluating strengths and limitations of each approach.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
---
Branching covers the local branching operations that determine what gets pushed to remotes.Workflows defines the team conventions for how remote operations are structured and coordinated.Pull Requests extends remote operations into code review and collaborative merge processes.