Version control with Git
Track every change to your code with Git, work on ideas in branches, merge them back, resolve a conflict and share work through a remote.
What you will learn
- Explain what a repository, a commit, a branch and a remote are
- Use the everyday cycle of status, add, commit and log
- Create a branch, merge it and resolve a merge conflict
- Keep generated files and secrets out of a repository with .gitignore
- Describe how a pull request fits into working with others
On this page
Sooner or later you will break something that worked ten minutes ago, and wish you could go back. Or you will want to try a risky idea without losing the working version. Or two people will edit the same file. Version control solves all three: it records the history of a project so you can inspect, compare, undo and combine changes.
Git is the version control system used almost everywhere. It runs the same way whatever language you write, so this lesson uses no language tabs. You will type commands in the terminal from the tools lesson. Git is installed on most systems; check with git --version.
The core ideas
| Term | Meaning |
|---|---|
| Repository (repo) | A project folder plus its complete history, stored in a hidden .git folder inside it |
| Commit | A saved snapshot of the project, with a message, an author, a time and a pointer to the previous commit |
| Working directory | The files as you see and edit them now |
| Staging area | A waiting room where you choose which changes go into the next commit |
| Branch | A named line of commits. It lets you work on something without disturbing the main line |
| Merge | Combining the changes from one branch into another |
| Remote | Another copy of the repository, usually on a server, that you can push to and pull from |
The staging area surprises beginners. Think of it like packing a box: you edit several files, then git add only the ones that belong together, then git commit seals the box with a label. This lets you make several small, focused commits from a messy afternoon of editing.
Before your first commit, tell Git who you are. This is done once per computer:
git config --global user.name "Your Name"git config --global user.email "you@example.com"The everyday cycle
The following session is real output from a new folder. The $ marks a command you type. Your commit identifiers (the short codes such as f6292ee) will differ from the ones shown, because each is computed from the content and time.
$ git init -b mainInitialized empty Git repository in .../gitdemo/.git/$ git status --short?? greeting.txtgit init turns the current folder into a repository, and -b main names the first branch main. The ?? in git status means Git sees greeting.txt but is not tracking it yet. Stage it and commit:
$ git add greeting.txt$ git commit -m "Add greeting"[main (root-commit) f6292ee] Add greeting 1 file changed, 1 insertion(+) create mode 100644 greeting.txtNow change the file to say Hello, everyone, and look at what changed with git diff:
$ git diffdiff --git a/greeting.txt b/greeting.txtindex a5c1966..991915b 100644--- a/greeting.txt+++ b/greeting.txt@@ -1 +1 @@-Hello, world+Hello, everyoneA line starting with - was removed and one starting with + was added. Reading a diff before you commit is the best habit you can build: it shows what you are about to record, including debug prints you forgot to remove. Commit the change. The -a option stages every change to files Git already tracks, and -m gives the message:
$ git commit -am "Greet everyone"[main a319838] Greet everyone 1 file changed, 1 insertion(+), 1 deletion(-)The four commands you will use most are git status (what has changed?), git add (stage this), git commit (record it) and git log (show the history). Good commit messages say what the change does, in the imperative: “Fix off-by-one in sum_to”, not “stuff” or “fixed it”. Each commit should be one logical change that you can describe in a line.
Branches and merging
A branch lets you work on an idea in isolation. Create one with git switch -c (older Git versions use git checkout -b), make a commit on it, then go back to main:
$ git switch -c friendlySwitched to a new branch 'friendly'$ git commit -am "Greet a friend"[friendly ec4254a] Greet a friend 1 file changed, 1 insertion(+), 1 deletion(-)$ git switch mainSwitched to branch 'main'On friendly, we changed the greeting to Hello, friend. Back on main, the file again says Hello, everyone. Switching branches swaps the files in your folder to match. Merging brings the branches back together: you switch to the branch that should receive the work, and run git merge with the other branch’s name. When the two branches changed different files, or different lines, Git combines them automatically.
Resolving a conflict
Here, though, main also changed the same line, to Hello, everybody, so both branches rewrote the same line differently. Git cannot know which is right, so it stops and asks you:
$ git commit -am "Greet everybody"[main a9472e7] Greet everybody 1 file changed, 1 insertion(+), 1 deletion(-)$ git merge friendlyAuto-merging greeting.txtCONFLICT (content): Merge conflict in greeting.txtAutomatic merge failed; fix conflicts and then commit the result.$ git status --shortUU greeting.txtA conflict is not an error in the repository. It is Git saying “a human must decide”. The file now holds both versions between markers:
<<<<<<< HEADHello, everybody=======Hello, friend>>>>>>> friendlyEverything between <<<<<<< HEAD and ======= is your current branch’s version. Everything between ======= and >>>>>>> friendly is the incoming version. To resolve it, open the file in your editor, decide what the final text should be (one side, the other, or a blend), and delete all three marker lines. Here we write Hello, everyone and friends and then tell Git the conflict is settled:
$ git add greeting.txt$ git commit -m "Merge friendly"[main d838595] Merge friendly$ git log --oneline --graph* d838595 Merge friendly|\| * ec4254a Greet a friend* | a9472e7 Greet everybody|/* a319838 Greet everyone* f6292ee Add greetingThe graph shows the history splitting where friendly began and rejoining at the merge commit. Run the tests from the testing lesson after every merge: a merge can succeed without conflicts and still break the program.
To abandon a merge that has gone wrong, run git merge --abort.
Remotes and pull requests
So far everything lives on your computer. A remote is a copy of the repository elsewhere, typically on a hosting service such as GitHub, GitLab or Codeberg, that acts as the shared meeting point. The commands are:
| Command | What it does |
|---|---|
git clone <url> |
Copy an existing repository to your computer, with its full history |
git remote add origin <url> |
Connect your local repository to a remote, conventionally called origin |
git push -u origin main |
Upload your main branch and remember the link for next time |
git pull |
Download new commits from the remote and merge them into your branch |
git fetch |
Download new commits without merging them, so you can look first |
Teams rarely push straight to main. The usual workflow is:
- Create a branch for one task and make commits on it.
- Push the branch to the remote.
- Open a pull request (PR) on the hosting service. This asks the team to review your branch and merge it. It is not a Git command; it is a feature of the service.
- Teammates read the diff, comment, and ask for changes. Automated tests often run too.
- Once approved, the branch is merged into
main.
Small, focused pull requests are far easier to review than large ones, which is another reason to commit one logical change at a time.
What to keep out: .gitignore
Not every file belongs in the history. Generated files (compiled output, caches), dependency folders, editor settings and above all secrets such as passwords and API keys should stay out. A file named .gitignore in the project root lists patterns Git will skip:
# Python__pycache__/*.pyc
# JavaScriptnode_modules/
# Java and C build output*.class*.obuild/
# Secrets and local settings.envA trailing slash matches a folder, * matches any run of characters, and a line starting with # is a comment. Add .gitignore early: a file that is already tracked stays tracked even if you list it afterwards.
Secrets deserve extra care. History is permanent. If you commit a password and delete it in the next commit, it is still in the old one, and once pushed you must treat it as stolen and change it. The last lesson returns to this.
Common pitfalls
- Huge, vague commits. “Update” with 40 changed files cannot be reviewed or undone cleanly. Commit small and often.
- Committing without looking. Run
git statusandgit difffirst, so that stray files and debug prints do not go in. - Committing secrets or generated files. Set up
.gitignoreat the start. - Panicking at a conflict. Read the markers, decide, remove them,
git add,git commit. Usegit merge --abortif you want to back out. - Leaving conflict markers in the file. If a
<<<<<<<line is still there, the program is probably broken. Search for them before committing. - Working only on
main. Branches are cheap. Use one per task. - Forgetting that a commit is local. Until you push, nobody else has it, and it lives only on your disk.
In an empty folder, repeat the session from this lesson using your own words. Steps and what you should see:
git init -b main, createnotes.txtwith the linefirst, and commit it with the messageAdd notes.- Make two more commits on
main: append the linesecond, commit; appendthird, commit.git log --onelineshould now list 3 commits. - Create a branch
editand change line 1 toFIRST. Commit. - Switch back to
mainand change line 1 toone. Commit. You now have 5 commits in total across the two branches. - On
main, rungit merge edit. Expected:CONFLICT (content): Merge conflict in notes.txt. - Open
notes.txt, and edit the first line so that it readsOne, with the other two lines unchanged and all marker lines removed. Rungit add notes.txtandgit commit -m "Merge edit". - Check the result with
git log --oneline --graph. Expected: a merge commit at the top with two parents, andcat notes.txtshowingOne,second,third.
Show solution
The full sequence of commands:
git init -b mainecho "first" > notes.txtgit add notes.txtgit commit -m "Add notes"echo "second" >> notes.txtgit commit -am "Add second line"echo "third" >> notes.txtgit commit -am "Add third line"git switch -c editsed -i 's/^first$/FIRST/' notes.txtgit commit -am "Capitalise first line"git switch mainsed -i 's/^first$/one/' notes.txtgit commit -am "Rename first line"git merge editThe last command stops with a conflict on line 1 (one against FIRST; lines 2 and 3 are unchanged on both sides, so only line 1 conflicts). Edit the file so it reads:
OnesecondthirdThen finish:
git add notes.txtgit commit -m "Merge edit"git log --oneline --graph(sed -i edits a file in place; -i behaves slightly differently on macOS, where you can simply use your editor.) The graph will show the two lines of work joining in the top commit.
A project has this .gitignore:
*.logbuild/.envFor each path, say whether Git will track it or ignore it: (1) app.log, (2) logs/debug.log, (3) build/output.o, (4) src/build.py, (5) .env, (6) .env.example.
Show solution
- Ignored.
*.logmatches any file ending in.log. - Ignored. A pattern without a slash in the middle matches at any depth.
- Ignored.
build/matches the folder and everything in it. - Tracked.
build/only matches a folder;src/build.pyis a file insrc. - Ignored. The exact name
.env. - Tracked.
.env.exampleis a different name, so the pattern does not match. That is deliberate: a template file listing the setting names (without real values) is safe to commit.
Which Git command does each job? (1) See which files you have changed. (2) Record the staged changes with a message. (3) See the lines you changed. (4) Start a new line of work called fix-typo. (5) Download a teammate’s new commits and merge them.
Show solution
git status.git commit -m "message".git diff.git switch -c fix-typo.git pull.
Recap
- A repository stores a project’s whole history as commits. The cycle is edit,
git statusandgit diff,git add,git commit. - Branches let you work on an idea in isolation.
git mergecombines them. When both sides edit the same lines you get a conflict: choose the final text, remove the markers,git add,git commit. - A remote is a shared copy. You
pushandpull, and a pull request is how a team reviews a branch before it is merged. .gitignorekeeps generated files and secrets out. A committed secret stays in history forever.
You will spend a lot of time in code that is not yours, so the next lesson is about reading documentation and other people’s code.