On this page

Every terminal tutorial assumes one tool before any other: git. Install guides start with “clone the repository”, and sooner or later a terminal is waiting for you to know git. This guide stays offline on purpose: the shared save point later is a folder on your own disk, no hosting service involved. Everything below ran on git 2.53.0 (Ubuntu 26.04) and 2.56.0 (Fedora 44, Arch), identical on both versions.

What git actually does

git is a record-keeper. Point it at a folder and it can snapshot the whole folder, exactly as it sits, whenever you ask. Each snapshot is a commit carrying a short name like bc11b6c, and together the commits let you walk back to any earlier state — save points, in the game sense. A repository is just a folder with a .git directory inside it, and each commit records the whole project, so you can reconstruct Tuesday morning and not just the file you regret touching.

Install and introduce yourself

Check for it with git --version. On Ubuntu this matters: the server image depends on git, so a server always has it, but a fresh desktop or minimal install lacks it. Fedora’s workstation set marks it a default package, and Arch’s small base set leaves it out. If it is missing:

$ sudo apt install git        # Ubuntu, Debian
$ sudo dnf install git        # Fedora
$ sudo pacman -S git          # Arch

Before your first commit, git wants to know who you are. It stamps a name and an address into every commit, so a history still means something once several people have written parts of it:

$ git config --global user.email "[email protected]"
$ git config --global user.name "Vera Flyer"

The address is not checked against anything, and git never mails anyone. It just carves what you typed into every commit. Both settings land in ~/.gitconfig, and --global makes them apply to every repository on this machine.

Commit with no identity on file and git stops you — worth seeing once:

Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email "[email protected]"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'you@mycomputer.(none)')

The last line confesses that git tried to guess an address from the hostname and gave up.

Your first repository

Any folder can become a repository. Make one and turn git on:

$ mkdir projects
$ cd projects
$ git init
hint: Using 'master' as the name for the initial branch. This default branch name
hint: will change to "main" in Git 3.0. To configure the initial branch name
hint: to use in all of your new repositories, which will suppress this warning,
hint: call:
hint:
hint: 	git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: 	git branch -m <name>
hint:
hint: Disable this message with "git config set advice.defaultBranchName false"
Initialized empty Git repository in /home/you/projects/.git/

The hints name the branch master and say this default will change to “main” in Git 3.0. That is git’s wording, not a promise of permanence, so read main wherever this guide prints master when that lands. The line that matters comes last: the repository exists, in the .git directory git just created. Ask it how it is doing:

$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

Every word there is vocabulary — three states, three words. A file is untracked when git sees it but you have not told git to keep records about it, staged once git add has marked its state for the next commit, and committed once a snapshot containing it exists. git status is the map of where every file stands.

The daily loop

The loop you will type a thousand times is add, then commit. Start with a named file, then let the dot take over:

$ echo "backup plan: rsync to the Pi" > notes.txt
$ git add notes.txt
$ git commit -m "First commit"
[master (root-commit) bc11b6c] First commit
 1 file changed, 1 insertion(+)
 create mode 100644 notes.txt
$ mkdir src
$ echo "# Scripts" > README.md
$ git add .
$ git commit -m "Add src and readme"
$ git log --oneline
09aecf2 Add src and readme
bc11b6c First commit
$ git log
commit 09aecf24b6f75067a6453a79752811861fb847d3
Author: Vera Flyer <[email protected]>
Date:   Mon Oct 12 07:15:35 2026 +0000

    Add src and readme

git add stages, moving a file to the map’s middle state. The dot stages whatever is lying around, which is how junk sneaks into history (the ignoring section has the filter). Empty directories are invisible to git, so src enters history only through its files. git commit -m photographs everything staged, and the first commit is a root-commit whose output names the branch and the new short hash. --oneline squeezes each commit to hash and message, newest first, and the full form adds the author and date your config stamped in. The hashes here belong to the test run. Yours differ every time, and that is normal. One more rep, editing an existing file:

$ nano notes.txt
$ git add notes.txt
$ git commit -m "Edit notes"
[master dc6d8fc] Edit notes
 1 file changed, 1 insertion(+), 1 deletion(-)

The summary now counts deletions too. That is the whole loop: git status to look, git add to stage, git commit -m to record, git log to read back.

Looking at changes

Three windows onto change, at three moments. git diff prints the edits you have made but not yet staged, the work a commit right now would leave out. git diff --staged prints the other half — everything a commit right now would include. Before committing, the first tells you what you are leaving behind and the second what you are writing down. git show HEAD prints the newest commit itself, message and changes together, HEAD being git’s shorthand for that newest commit.

Undoing things

The command you will want most is also the one to handle carefully. git restore FILE puts a file back to its last recorded state and throws your uncommitted edits away. This deletes your own work, and there is no trash to fish it out of. On a file git has never seen, it refuses to guess:

$ echo "scrapped plan" > notes.txt
$ git restore notes.txt
$ git restore extra.txt
error: pathspec 'extra.txt' did not match any file(s) known to git

The damage is gone, and notes.txt reads as it did at the last commit. The error is exact: restore works on files known to git, and extra.txt never entered a commit or an add. One piece of fine print: by default restore takes its copy from the staging area, not from the last commit. With nothing staged the two are the same, which is why the plain description holds. A half-staged file makes them differ, and that mix is where the manual starts.

The gentler undo backs out a staging decision. You ran git add TODO.md in haste and decided the file is not ready. git restore --staged TODO.md undoes exactly that. Only the staging goes. The edits stay on disk, and the next commit just stops including them. Where plain restore throws work away, --staged changes your mind.

The third undo repairs a commit itself. Commit a one-item list, then remember the second item:

$ echo "milk" > list.txt
$ git add list.txt
$ git commit -m "Add list with one item"
$ echo "eggs" >> list.txt
$ git add list.txt
$ git commit --amend -m "Add list with two items"
[master 70a66be] Add list with two items
 Date: Mon Oct 12 07:17:57 2026 +0000
 1 file changed, 2 insertions(+)
 create mode 100644 list.txt

--amend replaces the newest commit with a new one built from the old commit plus whatever is staged now. The hash is new, 70a66be, because this is a different commit wearing the corrected message. It even prints a Date: line, which a plain commit never shows. Amending rewrites history, so doing it to something already pushed rewrites history other copies may hold. Amend is for commits that are still yours alone.

Branches without fear

A branch is a name for a line of work, and four commands cover it. git branch feature creates one and says nothing (in git, silence means it went fine), and a bare git branch lists them all, marking your position with a star:

$ git branch feature
$ git branch
  feature
* master

Nothing moved: creating a branch only wrote the name down. Switching is a separate step, and -c creates and switches in one motion:

$ git switch feature
Switched to branch 'feature'
$ git switch -c hotfix
Switched to a new branch 'hotfix'

The messages differ by two words. Older tutorials write git checkout for this. git switch is the modern replacement, and both print Switched to a new branch 'NAME'. Now the payoff. Commit on a branch, return to master, merge:

$ echo "new thing" > feature.txt
$ git add feature.txt
$ git commit -m "Add feature"
$ git switch master
Switched to branch 'master'
$ git merge feature
Updating dc6d8fc..f4354cb
Fast-forward
 feature.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 feature.txt

Fast-forward is the happy case: master made no commits of its own while feature was out, so the merge slides master up to feature’s newest commit. The Updating line names the two positions as hashes.

One wall is real: if both branches changed the same lines of the same file, git stops the merge and asks you to decide what the result should say. Untangling that, a merge conflict, is its own skill and outside this guide.

Ignoring files

Every working folder collects files that are yours but not the project’s: test-run logs, build output, editor droppings. A .gitignore in the repository root lists what git should look past:

# test runs leave these behind
*.log
build/

Lines starting with # are comments, *.log matches every name ending in .log, and build/ with the trailing slash matches the build directory.

Now the trap: .gitignore only affects files git is not already tracking. Commit debug.log, and only then write *.log, and git keeps tracking it as if nothing happened. Edits still show up as changes and sail into every commit, while a brand-new log next to it would be ignored. The list steers what git picks up going forward. It does not un-see anything.

Telling git to forget a tracked file takes git rm --cached FILE, which stops tracking it and leaves the file on disk. For today, knowing the trap exists is enough.

Remotes on your own disk

Real teams keep their shared repository on a hosting service, and these same commands run against a URL instead of a path when you join one. Staying on your own disk is the point: it teaches the moves, and a save point on a second drive is a real backup.

Push into a plain folder and git refuses:

remote: error: refusing to update checked out branch: refs/heads/master
To /tmp/vfygit/demo
 ! [remote rejected] master -> master (branch is currently checked out)
error: failed to push some refs to '/tmp/vfygit/demo'

(The remote: lines carry trailing spaces in the real output, stripped here, and git’s refusal message continues past this excerpt.) The reason is in the first line: the target’s branch is checked out, so someone could be standing in that folder editing the very files a push would replace. git protects that working copy.

The fix is a bare repository: no working tree, nobody editing inside it, nothing to protect, pushes welcome. By convention the name ends in .git. Five lines make up the whole offline workflow:

git init --bare /path/to/savepoints.git   # the shared save point (bare = no working tree, accepts pushes)
git remote add origin /path/to/savepoints.git
git push -u origin master
git clone /path/to/savepoints.git
git pull

origin is a nickname for that path, nothing more. The -u on the first push asks git to remember the link, so every push after it can be a bare git push. A first push from the test run, save point empty:

To /tmp/vfygit/savepoints.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

The tracking line is the -u doing its job: origin/master is your record of where master sits on the save point. Push again with nothing new and the answer is short: Everything up-to-date.

A second machine, or a second folder on the same one, starts from the save point with clone, and git has already written down where the copy came from:

$ git clone /tmp/vfygit/savepoints.git laptop
Cloning into 'laptop'...
done.
$ git remote -v
origin	/tmp/vfygit/savepoints.git (fetch)
origin	/tmp/vfygit/savepoints.git (push)

The arriving commit was edited, added, committed, and pushed from the first folder, so this pull has something to fetch. Pull’s From line prints the path without .git, while git remote -v shows it with:

$ git pull
From /tmp/vfygit/savepoints
   347bcf7..9723a2e  master     -> origin/master
Updating 347bcf7..9723a2e
Fast-forward
 notes.txt | 2 ++
 1 file changed, 2 insertions(+)

Pull fetched the newest state and folded it in. A fast-forward again, because this clone had made no commits of its own. With nothing waiting: Already up to date.

Two footnotes. The same commands work against you@server:/path/repo.git over SSH. The SSH guide covers key authentication. And receive.denyCurrentBranch=updateInstead lets a non-bare repository accept pushes and update its working tree.

Capstone — a versioned scripts folder

Run the whole arc in one sitting. The material is the scripts from the bash scripting guide: the grown-up backup.sh, plus the check.sh snippet from its testing section, because scheduled scripts are the first thing worth versioning. The automation chapter is where they meet cron.

Save the final backup.sh from that guide into a new ~/scripts folder first, and introduce yourself as in the install section if this machine has not met you yet. Then:

$ cd ~/scripts
$ git init
$ git add backup.sh
$ git commit -m "Track the backup script"
[master (root-commit) 730adaa] Track the backup script
 1 file changed, 19 insertions(+)
 create mode 100644 backup.sh

New work goes on a branch first, always. Create one, save the guide’s check.sh into the folder, then stage, commit, and fold the branch back into master:

$ git switch -c add-check
Switched to a new branch 'add-check'
$ git add check.sh
$ git commit -m "Add check script"
$ git switch master
Switched to branch 'master'
$ git merge add-check
Updating 730adaa..127d7c0
Fast-forward
 check.sh | 5 +++++
 1 file changed, 5 insertions(+)
 create mode 100644 check.sh

With the scripts in, guard against junk before any exists: a .gitignore with the ignoring section’s patterns, created with nano. Then commit it:

$ git add .gitignore
$ git commit -m "Ignore test logs"
[master dcf3ec5] Ignore test logs
 1 file changed, 3 insertions(+)
 create mode 100644 .gitignore

Last step, the save point itself. A bare repository on the same disk is a photocopy in the same burning building, but the right shape for a second drive or another machine:

$ git init --bare /home/you/savepoints.git
$ git remote add origin /home/you/savepoints.git
$ git push -u origin master
To /home/you/savepoints.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.
$ git push
Everything up-to-date

The second push proves the save point matches the folder. From anywhere else, git clone /home/you/savepoints.git work brings all three commits down, and git pull keeps them current.

Where you are now

You can version a folder end to end: init, add, commit, branch, merge, restore, ignore, and push to a bare repository on your own disk. Four habits carry most of the weight. Run git status when unsure and git diff --staged before committing. Reach for restore --staged before plain restore whenever you meant to keep the work. And never amend a commit that has already been pushed.

Skipped on purpose: merge conflicts, git stash, hosting platforms and everything that comes with them, and git’s deeper history surgery. All of it waits until a real project demands it.

Two references, both official and stable: