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:
- Git documentation — the manual for every command used here
- Pro Git — the full book, free to read online