Git Essentials for Sysadmins: Commit, Branch, Share, Undo
- Explain the working tree, the staging area and the repository, and what moves a change between them
- Create a repository, set an author identity, and record commits
- Create a branch, commit on it, and merge it back into main
- Keep build output and secrets out of history with a committed .gitignore
- Clone a shared repository and push commits and annotated tags to it
- Undo a published commit with git revert instead of rewriting history
The mental model: a timeline of snapshots
Picture what Git actually stores before you type a single git command.
Git looks intimidating because it has a lot of commands, but underneath it does one simple thing: it takes snapshots of a directory and remembers them in order. Each snapshot is a commit. A commit records what every tracked file looked like at that moment, who made it, when, why (the message), and which commit came before it. Chain those together and you get a timeline of your project you can read, compare, and go back to.
A useful picture is a save-game system. Every commit is a save point. You can look at any old save, start a side quest from one (a branch), and later fold the side quest back into the main story (a merge). Nothing you saved is ever overwritten by a new save; new saves are added on top.
Three places matter, and almost every confusing Git moment comes from mixing them up:
- Working tree: the files you see and edit in the directory.
- Staging area (also called the index): the set of changes you've picked for the next commit.
- Repository (the hidden
.gitdirectory): every commit ever made, plus labels such as branch names and tags that point at commits.
You edit in the working tree, choose changes with
git add(staging), and record them withgit commit(repository).git statustells you, at any moment, what sits in each place.Check yourself
You edited config.ini but haven't run
git addyet. In which of the three places does that change live?Your first repository: init, identity, commit
Create a repository, tell Git who you are, and record your first two commits.
Let's make a repository for a small piece of server config.
git initturns an ordinary directory into a repository by creating the hidden.gitdirectory inside it. Nothing about your files changes; Git simply starts watching the directory. We'll name the first branchmain, which is the modern default.mkdir -p /tmp/git-lesson/site && cd /tmp/git-lesson/site && git init -b mainInitialized empty Git repository in /tmp/git-lesson/site/.git/
- -b main
- Name the initial branch main instead of relying on the system default.
Git created /tmp/git-lesson/site/.git. That directory is the repository: delete it and the history is gone, but your files stay. Everything else you'll do talks to this directory.Before Git will record a commit it needs to know who is making it, because every commit carries an author name and email. You can set them for your whole account with
--global(stored in~/.gitconfig), or for just one repository by leaving--globaloff (stored in that repository's.git/config). On your own login, global is the convenient choice; on a shared account such as root on a team server, prefer per-repository so your name doesn't end up on everyone's commits.git config --global user.name "Sam Admin" && git config --global user.email sam@example.internal && git config --global --listuser.name=Sam Admin user.email=sam@example.internal
- --global
- Read or write ~/.gitconfig (your account) instead of the current repository's .git/config.
- --list
- Print every setting as key=value.
The identity is saved in ~/.gitconfigand applies to every repository you work in from this account, including the ones later in this lesson.--listprints the settings back so you can confirm exactly what was stored.Now create a file and watch how Git sees it. A brand-new file starts out untracked: Git notices it but won't include it in commits until you say so.
cd /tmp/git-lesson/site && printf '[server]\ntimeout = 30\n' > config.ini && git status --short?? config.ini
??means untracked. The short format prints two columns per file: the left one is the staging area, the right one is the working tree.??in both means Git has never been told about this file.cd /tmp/git-lesson/site && git add config.ini && git status --shortA config.ini
Ain the left column means the file is now staged as added: it will be part of the next commit.git addcopies the file's current content into the staging area. If you edit the file again afterwards, you'd need togit addit again to stage the new version.cd /tmp/git-lesson/site && git commit -q -m "Initial server config" && git log --onelinead56688 Initial server config
- -m
- Give the commit message on the command line instead of opening an editor.
- --oneline
- Show each commit as short id plus subject.
The staged snapshot is now a commit. git log --onelineshows one line per commit: a short commit id (a hash of the snapshot, so yours will differ) and the message. That id is how you'll refer to this exact snapshot from now on.cd /tmp/git-lesson/site && sed -i 's/timeout = 30/timeout = 45/' config.ini && git diff && git commit -q -am "Raise timeout to 45s"diff --git a/config.ini b/config.ini index f1df41d..92b663c 100644 --- a/config.ini +++ b/config.ini @@ -1,2 +1,2 @@ [server] -timeout = 30 +timeout = 45
- -a
- Stage modifications and deletions of tracked files before committing.
- -q
- Quiet: don't print the commit summary.
git diffshows working-tree changes that aren't staged yet: the-line is the old content, the+line the new.commit -astages every change to files Git already tracks and commits in one step. It never picks up untracked files, so a brand-new file still needsgit add.Check yourself
You created a new file notes.txt and ran
git commit -am "Add notes". Is notes.txt in that commit?Branches: movable labels for side quests
Make a change on a branch and merge it back into main.
A branch sounds heavy, but in Git it is just a label that points at a commit and moves forward each time you commit on it.
mainis one label. Creating a branch makes a second label at the same commit; switching to it means new commits move that label and leavemainwhere it was. That's why branches are cheap: Git doesn't copy any files to make one.Teams use branches so an unfinished or unreviewed change never lands on
mainby accident. The usual loop is: branch, commit, review, merge.mkdir -p /tmp/git-lesson/br && cd /tmp/git-lesson/br && git init -q -b main && printf 'timeout = 30\n' > app.conf && git add app.conf && git commit -q -m base && git switch -c fix-timeout && git branchSwitched to a new branch 'fix-timeout' * fix-timeout main
- switch -c NAME
- Create a new branch called NAME and switch to it.
- branch
- List local branches; * marks the current one.
- init -q
- Quiet: skip the 'Initialized empty repository' line.
This builds a fresh one-commit repo, then git switch -ccreates the branchfix-timeoutat the current commit and moves you onto it in one step.git branchlists the branches and stars the one you're on. Older guides usegit checkout -b, which does the same thing asswitch -c.cd /tmp/git-lesson/br && sed -i 's/30/60/' app.conf && git commit -q -am "Raise timeout to 60s" && git log --oneline --all --graph --decorate* 74e55ac (HEAD -> fix-timeout) Raise timeout to 60s * 128761a (main) base
- --all
- Show commits from every branch, not just the current one.
- --graph
- Draw the history as an ASCII graph.
- --decorate
- Label commits with the branches, tags and HEAD that point at them.
--graph --alldraws every branch. The newest commit carries(HEAD -> fix-timeout): HEAD is "where you are", and the label moved forward with your commit.mainstill points at the older commit below it, untouched.To bring the fix into
main, switch tomainand merge the branch. You always merge into the branch you're on.cd /tmp/git-lesson/br && git switch -q main && git merge fix-timeout && cat app.confUpdating 128761a..74e55ac Fast-forward app.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) timeout = 60
- merge BRANCH
- Integrate BRANCH's commits into the current branch.
Because mainhadn't moved since the branch was created, Git simply slid themainlabel forward to the branch's commit. That's a fast-forward merge: no new commit is needed. Ifmainhad gained its own commits in the meantime, Git would create a merge commit with two parents to join the two lines of history.Check yourself
You're on fix-timeout and run
git merge main. Which branch moves?Keeping junk out: .gitignore
Stop build output and secrets from ever being committed, and leave a clean working tree.
Some files should never be in history: build artifacts, logs, editor swap files, and above all secrets. A .gitignore file lists patterns Git should treat as invisible. Ignored files don't show up as untracked and aren't picked up by
git add .. The .gitignore file itself is committed, so everyone who clones the repository gets the same rules.mkdir -p /tmp/git-lesson/ig && cd /tmp/git-lesson/ig && git init -q -b main && echo app > app.py && git add app.py && git commit -q -m app && mkdir build && echo bin > build/app.bin && git status --short?? build/
Here's the problem in miniature: the build output shows up as untracked ( ?? build/). Sooner or later someone runsgit add .and commits a binary, or worse, a.envfile with passwords in it.cd /tmp/git-lesson/ig && echo 'build/' > .gitignore && git status --short && git check-ignore -v build/app.bin?? .gitignore .gitignore:1:build/ build/app.bin
- check-ignore -v
- Report which ignore rule (file:line:pattern) matches a path.
build/vanished from the status and only.gitignoreitself is new. A trailing slash means "this directory and everything in it".git check-ignore -vnames the file, line number and pattern responsible, which is the quickest way to debug "why won't Git see my file?"cd /tmp/git-lesson/ig && git add .gitignore && git commit -q -m "Ignore build output" && git statusOn branch main nothing to commit, working tree clean
"nothing to commit, working tree clean" is the state you want before you switch branches, pull, or hand the server to someone else. Notice build/is still on disk: ignoring a path hides it from Git, it doesn't delete anything.Check yourself
A file is already tracked, and you add its name to .gitignore. Does Git stop tracking changes to it?
Sharing work: clone, push, and tags
Publish commits and a release tag to a shared repository.
So far everything has been local. Teams share work through a remote: another copy of the repository, usually on a server. A server copy is typically a bare repository: just the
.gitcontents, with no working tree, because nobody edits files there directly.git clonecopies a repository and remembers where it came from under the nameorigin.It helps to picture this as a shared notebook in the office: you photocopy it (clone), write your pages at your desk (commit), then add your pages to the shared copy (push).
mkdir -p /tmp/git-lesson/remote /tmp/git-lesson/remote/work && cd /tmp/git-lesson/remote && git init -q --bare -b main shared.git && git clone -q shared.git work && cd work && echo v1 > VERSION && git add VERSION && git commit -q -m "First release" && git push -q origin main && git log --oneline origin/mainwarning: You appear to have cloned an empty repository. 0ed117b First release
- --bare
- Create a repository with no working tree, suitable as a shared remote.
- push origin main
- Send local main's new commits to the remote named origin.
We made a bare shared.gitto stand in for the server, cloned it, committed, and pushed. Cloning an empty repository prints a harmless warning, because there's nothing to copy yet.origin/mainis your clone's record of wheremainis on the server; after the push it points at your commit.A tag is a permanent name for one commit, typically a release:
v1.4.1. A lightweight tag is just a name. An annotated tag (-a) is a full object with its own message, tagger and date, and it's what release tooling expects. Tags behave differently from branches in one important way:git pushdoes not send them unless you ask.cd /tmp/git-lesson/remote/work && git tag -a v1.0 -m "Release 1.0" && git push -q origin v1.0 && git -C ../shared.git cat-file -t v1.0tag
- tag -a NAME -m MSG
- Create an annotated tag with a message.
- push origin NAME
- Push one tag (or branch) by name.
- cat-file -t
- Print the type of a Git object: commit, tag, tree or blob.
cat-file -ton the server copy printstag, which proves two things: the tag reached the remote, and it's annotated (a lightweight tag would reportcommit).git push --follow-tagswould have pushed main and any annotated tags on its commits in one go.Check yourself
You ran
git tag v2.0and thengit push origin main. Does the server now have the v2.0 tag?Undoing a change without rewriting history
Choose between revert and reset, and know why it matters once commits are shared.
Sooner or later a bad commit lands. Git gives you two very different ways to undo it, and picking the wrong one on a shared branch causes real pain.
- `git revert <commit>` adds a new commit that applies the exact opposite of the bad one. History only grows. Everyone who already pulled just pulls again.
- `git reset --hard <commit>` moves the branch label back, so later commits drop out of the branch. That's fine for commits nobody else has seen, but once they're pushed, putting the reset on the server needs
--force, and everyone else's copy suddenly disagrees with the server.
Think of a shared logbook: revert is writing a correction on a new line; reset is tearing the pages out after other people have copied them.
mkdir -p /tmp/git-lesson/undo && cd /tmp/git-lesson/undo && git init -q -b main && printf 'debug = false\n' > app.conf && git add app.conf && git commit -q -m baseline && sed -i 's/false/true/' app.conf && git commit -q -am "Turn on debug" && echo 'contact: ops@example.internal' > README && git add README && git commit -q -m "Add contact" && git log --oneline444b12a Add contact 0e68f01 Turn on debug 0f17682 baseline
Three commits: a baseline, a bad one in the middle that turned debug on, and a good one on top that we must keep. This is exactly where resethurts, because resetting to the baseline would throw away the good commit as well.cd /tmp/git-lesson/undo && git revert --no-edit HEAD~1 >/dev/null && git log --oneline && cat app.confc057ed8 Revert "Turn on debug" 444b12a Add contact 0e68f01 Turn on debug 0f17682 baseline debug = false
- --no-edit
- Accept the default 'Revert "..."' message instead of opening an editor.
- HEAD~1
- The commit one step before HEAD. HEAD~2 is two steps back, and so on.
HEAD~1means "one commit before where I am", which is the bad commit. Revert recorded a fourth commit,Revert "Turn on debug", andapp.confis back todebug = false, while README from the later commit is still there. In real life you'd point revert at the commit id you found ingit log.Check yourself
A bad commit was pushed to main an hour ago and three colleagues have pulled since. Which command undoes it safely?
Now do it — in your live terminal
Run the whole loop yourself on the sandbox to your right.
Reading Git is one thing; it sticks when your fingers have done it. Run this loop in the terminal and check the output at each step against what you learned above. It's the same sequence the LFCS Git labs grade: commit, branch, merge, ignore, tag, push and revert.
mkdir -p ~/practice && cd ~/practice && git init -b main && git config user.name "Your Name" && git config user.email you@example.internalA fresh repository with an identity. Confirm with ls -athat.gitexists.cd ~/practice && echo 'port = 8080' > app.conf && git add app.conf && git commit -m "Add app config" && git log --onelineYour first commit. Note its short id; you'll see it again in the graph. cd ~/practice && git switch -c change-port && sed -i 's/8080/9090/' app.conf && git commit -am "Move to port 9090" && git switch main && git merge change-portBranch, commit, switch back and merge. Run git log --oneline --graph --allafterwards and find both labels.cd ~/practice && git revert --no-edit HEAD && cat app.conf && git log --onelineUndo the port change with a new commit. app.conf should say 8080 again, and the history should now have three commits.
- Git stores snapshots (commits) in a timeline. You edit in the working tree, choose changes with
git add, and record them withgit commit;git statusshows what is where. - A branch is a movable label on a commit, so branching is cheap. Merge into the branch you're on: switch to main, then
git merge feature. - A committed
.gitignorekeeps build output and secrets out of history, but it only affects untracked files. A secret that was already committed must be rotated. git push origin mainsends commits, not tags. Push tags by name or with--follow-tags, and use annotated tags (-a) for releases.- Once commits are shared, undo with
git revert(history grows).reset --hardplus force-push rewrites history everyone else already has.
| Command | What it does |
|---|---|
git init -b main | Turn the current directory into a repository |
git status / git diff | What's changed, staged, or untracked / the line-by-line changes |
git add FILE · git commit -m MSG | Stage a change · record the staged snapshot |
git switch -c NAME · git merge NAME | Create and switch to a branch · merge it into the current branch |
git log --oneline --graph --all | Draw the history of every branch |
git clone URL · git push origin main | Copy a repository · publish commits to it |
git tag -a v1.0 -m MSG · git push origin v1.0 | Create an annotated release tag · publish it |
git revert COMMIT | Undo a commit with a new inverse commit (safe on shared branches) |
