This page is an overview. The Git & CI/CD topic goes much deeper: how Git stores your work, merging and rebasing, undoing mistakes, collaborating on GitHub and automating with pipelines.

Why version control?

Without a system, people keep versions by copying files: report-final.docx, report-final-2.docx, report-final-really.docx. Nobody knows which copy is current, what changed between them, or how to combine the edits of two people.

Version control is the practice, and the software, of recording changes to a set of files over time, so that every earlier state can be retrieved and every change can be traced to its author and purpose. For programmers it provides:

  • History — a complete log of every change, with the date, the author and a message explaining why.
  • Undo — go back to any earlier version of a file, or of the whole project, after a mistake.
  • Collaboration — several people can work on the same code at the same time and combine their changes safely.
  • Accountability — for every line you can ask who last changed it and in which commit; Git's command for this is called git blame. The point is not to blame anyone, but to find the context of a change.
  • Experimentation — try an idea on a separate branch; if it fails, throw the branch away.

Centralized and distributed

In a centralized version control system, such as Subversion (SVN), the history lives on one server. You check out the current files, and every commit goes directly to that server — no network, no commits. In a distributed system, every developer has a full copy of the entire history on their own machine. You commit, browse history and create branches locally and offline, and exchange commits with other copies when you choose.

Git is distributed. Linus Torvalds wrote it in 2005 to manage the development of the Linux kernel, and it has since become the standard for almost all software development. Every copy being complete also helps in an emergency: if a server is lost, a developer's clone can often restore it — but it is no substitute for real backups.

Core concepts

Repository

A repository (or repo) is the database in which a version control system stores a project's complete history — all commits, branches and their metadata. In Git, it is the hidden .git folder at the top of your project. The rest of the project folder, the files you actually see and edit, is the working tree.

The working tree, the staging area and the repository

Git does not save every change automatically. Between your files and the history sits a third area: the staging area (also called the index). It holds what will go into your next commit. This lets you choose: if you changed five files but only three belong to one logical change, you stage those three and commit them, and commit the other two separately.

Three boxes from left to right: working tree, staging area and repository. git add moves changes from the working tree to the staging area; git commit moves them into the repository. git restore brings files back into the working tree from the staging area (which matches the last commit if nothing was staged). git switch, or git restore --source, brings files from a commit in the repository back into the working tree. Working tree files you edit Staging area the next commit Repository all commits (.git) git add git commit git restore git switch · git restore --source
Changes travel from your files to the staging area to the repository; older versions can come back the other way.

Commits

A commit is a saved snapshot of the complete project at one moment, stored in the repository together with the author, a date, a message describing the change, and a reference to its parent — the commit that came before it. Following the parents from any commit leads all the way back to the first one, so the commits form the project's history.

Each commit is identified by a hash: a 40-character hexadecimal fingerprint (SHA-1 by default) computed from its content, metadata and parent. Change anything, and the hash changes. You rarely type the whole hash; the first seven characters, such as cd3a63e, are usually enough. Because each hash also covers the parent, history cannot be altered silently: rewriting an old commit changes the hash of every commit after it.

The basic workflow

Once per computer, tell Git who you are; this name and email address go into every commit you make:

git config --global user.name "Ada Lovelace"
git config --global user.email "ada@example.com"

Turn a folder into a repository with git init. Here, a folder of recipes:

cd recipes
git init
Initialized empty Git repository in /home/ada/recipes/.git/

git status is the command you will type most. It tells you which branch you are on and what state each changed file is in. After creating a first file, pancakes.md:

git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	pancakes.md

nothing added to commit but untracked files present (use "git add" to track)

Stage the file with git add, then record the commit with git commit. The -m option gives the message directly; without it, Git opens an editor for you to write one.

git add pancakes.md
git commit -m "Add pancake recipe"
[main (root-commit) cd3a63e] Add pancake recipe
 1 file changed, 4 insertions(+)
 create mode 100644 pancakes.md

After editing the file again, git diff shows the changes that are not staged yet: removed lines start with -, added lines with +. git diff --staged shows what is staged, and git diff HEAD everything since the last commit.

git diff
diff --git a/pancakes.md b/pancakes.md
index 248d55d..61fb7e4 100644
--- a/pancakes.md
+++ b/pancakes.md
@@ -2,3 +2,4 @@ Pancakes

 - 200 g flour
 - 2 eggs
+- 300 ml milk

Stage and commit that change too, then look at the history. git log shows each commit with its full hash, author, date and message; --oneline gives a compact summary, newest first:

git add pancakes.md
git commit -m "Add milk to pancakes"
git log --oneline
[main 477d340] Add milk to pancakes
 1 file changed, 1 insertion(+)
477d340 Add milk to pancakes
cd3a63e Add pancake recipe

That is the everyday rhythm: edit, git status, git add, git commit, repeat. Commit often, in small steps that each do one thing.

Branches

A branch is an independent line of development. In Git, a branch is nothing more than a movable, named pointer to a commit. When you make a commit on a branch, the pointer moves forward to the new commit. A special pointer, HEAD, marks the branch you are on right now.

Because a branch is only a pointer, creating one is instant and costs nothing. The usual practice is to create a branch for every feature or fix, so the main branch always stays in a working state:

git switch -c add-syrup     # create a new branch and switch to it
git switch main             # switch back to an existing branch
git branch                  # list branches; * marks the current one

Older tutorials use git checkout -b add-syrup and git checkout main; they do the same thing. git switch was added in Git 2.23 because checkout did too many different jobs.

Commit graph: the main branch has commits A, B, C and a merge commit F. A feature branch starts at B with commits D and E, and is merged back into main by F. A B C F D E merge commit main feature time
A feature branch splits off at commit B. Work continues on both branches until the merge commit F combines them. The branch names are pointers to the latest commit of each line.

Merging

A merge combines the changes of one branch into another. You switch to the branch that should receive the changes and name the branch to merge in. Git handles two situations differently.

Fast-forward merge

If the receiving branch has not changed since the other branch split off, there is nothing to combine: Git just moves the pointer forward to the newest commit. This is a fast-forward, and no new commit is created.

git switch main
git merge add-syrup
Updating 477d340..5502123
Fast-forward
 pancakes.md | 1 +
 1 file changed, 1 insertion(+)

Merge commit

If both branches have new commits, as in the diagram above, Git performs a three-way merge: it compares both branch tips with their common ancestor (commit B in the diagram), combines the changes, and records the result as a merge commit — a commit with two parents. When the branches changed different files, or different parts of the same file, this happens automatically.

Merge conflicts

A merge conflict occurs when two branches changed the same part of the same file in different ways, so Git cannot decide automatically which version to keep. Git stops the merge and asks you to decide:

git merge metric
Auto-merging pancakes.md
CONFLICT (content): Merge conflict in pancakes.md
Automatic merge failed; fix conflicts and then commit the result.

In the file, Git has marked the conflicting region with conflict markers. The part between <<<<<<< and ======= is your current branch (HEAD); the part between ======= and >>>>>>> comes from the branch you are merging:

Pancakes

- 200 g flour
<<<<<<< HEAD
- 2 large eggs
=======
- 3 eggs
>>>>>>> metric
- 300 ml milk
- maple syrup, to serve

To resolve the conflict:

  1. Edit the file so it contains what it should contain — one version, the other, or a combination such as - 3 large eggs — and delete all three marker lines.
  2. Mark the file as resolved with git add pancakes.md.
  3. Finish the merge with git commit. (Or, if you would rather start over, cancel it with git merge --abort.)

Conflicts are a normal part of teamwork, not an error. They stay small and easy when branches live briefly and are merged often.

Remotes and collaboration

Remotes

A remote is another copy of the same repository, usually on a server, that your local repository knows by a short name and URL and exchanges commits with. Teams share work through a common remote, typically hosted on a platform such as GitHub, GitLab or Bitbucket. These hosts add a web interface, access control, issue tracking and code review on top of Git itself.

Command What it does
git clone URL Copy a remote repository, with its complete history, into a new local folder. The remote is registered under the name origin.
git fetch Download new commits from the remote, without changing your own branches or files. You can inspect them first.
git pull Fetch, then merge the remote branch into your current branch — bring your branch up to date. If both sides have new commits, Git 2.33 and later first ask how to combine them.
git push Upload your new local commits to the remote branch.
git remote -v List the remotes this repository knows.

origin is just the conventional name for the remote you cloned from; nothing about it is special. To publish an existing local repository, create an empty repository on the host, add it as a remote, and push. The -u option links your local branch to the remote one, so later a plain git push or git pull knows where to go:

git remote add origin git@github.com:ada/recipes.git
git push -u origin main
To github.com:ada/recipes.git
 * [new branch]      main -> main
branch 'main' set up to track 'origin/main'.

(Git also prints a few progress lines, which are left out here.) When a colleague has pushed new commits, git pull brings them in:

git pull
From github.com:ada/recipes
   82d721f..6157f29  main       -> origin/main
Updating 82d721f..6157f29
Fast-forward
 pancakes.md | 1 +
 1 file changed, 1 insertion(+)

If someone else pushed first, Git refuses your push until you have pulled and merged their commits. That protects their work from being overwritten.

That example was a fast-forward: only the remote had new commits. When both sides have new commits, current Git (2.33 and later) stops and asks how to combine them. Run git pull --no-rebase to merge them, or git pull --rebase to replay your commits on top of the remote ones; or choose once with git config --global pull.rebase false (merge).

Pull requests and code review

On a team, you usually do not push straight to main. You push your feature branch and open a pull request (GitLab calls it a merge request): a request, made on the hosting platform, to merge a branch into another, where the proposed changes can be discussed, reviewed and tested before they are merged.

The platform shows the diff; teammates leave comments on specific lines, ask questions and approve. Meanwhile, automated checks (continuous integration) run the test suite on the branch. You answer by pushing more commits to the same branch, and the pull request updates itself. When everything is approved and green, someone clicks merge. Code review catches bugs, spreads knowledge through the team and keeps the style consistent — which is why many projects protect main so that changes can only arrive through a reviewed pull request.

Good habits

.gitignore

Not everything in a project folder belongs in the repository. A file named .gitignore at the top of the project lists patterns of files that Git should ignore: they are not shown as untracked and git add . skips them.

# Generated files
__pycache__/
*.pyc
build/

# Dependencies (reinstalled from a list)
node_modules/
.venv/

# Secrets and local settings
.env
*.key

# Editor and OS clutter
.DS_Store
.vscode/

Ignore what can be regenerated (compiled output, installed dependencies), what is personal (editor settings), and above all what is secret. Commit the .gitignore file itself, so the whole team shares it.

.gitignore only affects files that Git is not tracking yet. To stop tracking a file that is already committed, run git rm --cached FILE (the file stays on your disk) and commit. If that file held a secret, rotate the secret too: it stays in the history.

Good commit messages

The history is only as useful as its messages. "fix", "changes" and "asdf" tell your future self nothing. The widely used convention:

  • A short summary line, about 50 characters, in the imperative mood — as if completing "If applied, this commit will…": Add milk to pancakes, Fix crash on empty order list.
  • If more is needed, a blank line, then a body that explains why the change was made. The diff already shows what changed.
  • One logical change per commit. A commit that fixes a bug and renames twenty variables is hard to review and impossible to undo partially.

Undoing changes safely

Almost everything in Git can be undone, but the right command depends on where the change is:

Situation Command Effect
Discard edits to a file you have not staged git restore FILE The file returns to its staged version, or to its last committed state if nothing was staged. The edits are gone for good.
Unstage a file, keep the edits git restore --staged FILE Moves the change out of the staging area only.
Undo a commit that is already shared git revert COMMIT Makes a new commit that reverses the changes of the old one. History is kept.
Remove local commits that nobody else has git reset COMMIT Moves the branch pointer back to an earlier commit. The later commits disappear from the branch.
git revert 6157f29
[main 0f4b945] Revert "Add salt"
 1 file changed, 1 deletion(-)

The key difference is between revert and reset. Revert adds to history, so it is safe, also on a branch other people have pulled (reverting a merge commit needs -m 1 to say which parent to keep). Reset rewrites history: if those commits were already pushed, your colleagues' copies still contain them. Git then refuses your push; forcing it replaces the remote history, and everyone who built on the removed commits has to repair their copy. To force-push a branch that only you work on, use git push --force-with-lease: it refuses if the remote has commits you have not fetched yet. By default git reset keeps your changes as unstaged edits; --soft keeps them staged; --hard throws them away completely.