Git is a distributed version control system. Git records the complete state of a set of files at a specific point in time, the relationships between commits, and the author and description of every change. Each developer’s local repository can hold the complete history, so even when temporarily unable to reach a server, you can still view diffs, create commits, and switch branches.

The most confusing part for Git beginners isn’t the commands — it’s figuring out whether a change currently lives in the working tree, the staging area, the local history, or the remote repository. Build the data-flow model first, then learn the commands, and you’ll be able to pick the right tool based on what you’re actually trying to do.

Working tree ──git add──> Staging area ──git commit──> Local commit history ──git push──> Remote repository

1. Installation and Initial Setup

1.1 Installing Git

On Ubuntu or Debian you can run sudo apt update && sudo apt install git; on Fedora, Rocky Linux, and similar systems, sudo dnf install git; on macOS with Homebrew, brew install git. After installation, use git --version to check the binary and its version.

Every commit records the author’s name and email address. The following global settings apply across all of the current user’s repositories:

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

You can read back the configuration with git config --global user.name and git config --global user.email. When a specific repository needs a different identity, omit --global while inside that repository — the repository-level setting overrides the global setting of the same name.

1.2 Understanding Configuration Levels

Git configuration has three common levels:

LevelCommon locationConfig flagScope
System/etc/gitconfig--systemThe whole machine
User~/.gitconfig--globalThe current user
Repository.git/configNo level flagThe current repository

The closer a setting is to the repository, the higher its priority. git config --list --show-origin shows both the setting’s value and which file it came from. Common initial settings look like this:

git config --global init.defaultBranch main
git config --global core.editor "vim"
git config --global core.autocrlf input

Git opens the configured editor when writing a merge message, an interactive rebase instruction sheet, or a tag description. If you need to get comfortable with basic editor operations, see this site’s Vim guide; readers who prefer a graphical interface can refer to Installing and Extending Visual Studio Code.

2. Creating a Repository and Making Your First Commit

2.1 Initializing or Cloning a Repository

To create a project locally, you can run the following commands:

mkdir git-practice
cd git-practice
git init

git init creates a .git directory used to hold the object database, branch references, and repository configuration. You can also run git init on an existing project — Git won’t delete any existing files. After creating your first file, running git status will list it as untracked, since it hasn’t been added to tracking yet.

When a project already exists remotely, you’ll typically use git clone [email protected]:team/project.git. Cloning downloads the commit history, sets up a working tree, adds a remote named origin, and checks out the default branch. git init suits starting a new project locally, while git clone suits joining an existing one.

2.2 Distinguishing the Four Data Areas

Git’s day-to-day operations involve four areas:

  1. Working tree: what’s currently shown in your editor and on the filesystem;
  2. Staging area (the index): the content prepared for the next commit;
  3. Local repository: the commits, branches, and tags that have already been created;
  4. Remote repository: the commits and branches shared with the team.

The main effect of git add is to add a file’s current content to the next commit. An already-tracked file needs to be re-added after every modification too, in order to update its version in the staging area.

2.3 Checking, Staging, and Committing

git status --short is good for a quick look at file status. git diff shows the difference between the working tree and the staging area, while git diff --staged shows the difference between the staging area and the previous commit. Checking both diffs in sequence before committing lets you confirm which changes are still unstaged, and exactly what the next commit will actually contain.

The complete flow of creating a file, selecting changes, and committing looks like this:

printf '# Git practice\n' > README.md
git status --short
git diff
git add README.md
git diff --staged
git commit -m "Add project README"

git add src/ stages a specific directory, git add . stages additions, modifications, and deletions within the current path, and git add --patch lets you select changes hunk by hunk. Staging hunk by hunk is good for splitting up a large change so each commit expresses one coherent change. A commit message should describe the outcome of the change — for example, Fix empty search results on mobile is much easier to review than update files.

.gitignore is used to exclude build artifacts, temporary files, and local configuration:

node_modules/
dist/
.env
*.log

.gitignore doesn’t stop tracking a file that’s already been committed. If .env is already tracked, use git rm --cached .env to remove it from the staging area, then commit the change. Passwords, API tokens, and private keys should never enter the commit history — deleting the file later won’t erase the secret from old commits.

3. Understanding Commit History and Branches

3.1 Commits, Branches, and HEAD

Every commit records a snapshot of the project, the author, the timestamp, the commit message, and its parent commit. Multiple commits form a directed graph through these parent-child relationships. HEAD usually points to the current branch, and a branch name points to that branch’s latest commit. After creating commit D, both main and HEAD move forward:

A──B──C──D  main, HEAD

A branch isn’t a full copy of the project — it’s a movable reference to a commit. Creating a branch is cheap, which is why short-lived feature branches are the common unit of Git collaboration.

A commit ID is a hash value Git computes from the object’s content. Changing a commit’s content, its parent, or its commit information produces a new ID, which is why rebase and amend rewrite commits. Commands usually only need an unambiguous prefix of the ID. HEAD~1 means the previous commit along the first-parent chain, and HEAD~3 means three commits back along that same first-parent chain.

3.2 Creating, Switching, and Merging Branches

New features should be developed on a separate branch. git switch -c feature/search creates and switches to a branch, git switch main goes back to the main branch, and git branch -vv lets you view local branches and their upstream status. Older Git commonly used git checkout for both switching branches and restoring files; modern Git provides git switch and git restore to make the two kinds of operations easier to distinguish.

Once a feature is finished, switch to the branch that will receive the change, then merge the feature branch in:

git switch main
git merge feature/search

If main has no new commits, Git just needs to move the branch reference forward — this is called a fast-forward merge. If both branches have new commits, Git will typically create a merge commit with two parents, preserving the record of the two development lines converging.

3.3 Using Rebase to Clean Up a Personal Branch

Rebase reapplies a set of commits onto a new base. Running git switch feature/search followed by git rebase main rebuilds the feature commits — originally built on an old base — on top of the latest main.

Rebase can produce a linear history, but it changes commit IDs. It’s fine to clean up a personal branch that hasn’t been shared yet; if a branch shared by multiple people gets rewritten arbitrarily, other developers’ histories will diverge. Once a feature has been merged, git branch -d feature/search safely deletes the local branch that’s already merged; the uppercase -D skips the merge check, so you should confirm the commits are still reachable from another reference before using it.

4. Remote Collaboration and Conflict Resolution

4.1 Fetch, Pull, and Push

git remote -v shows the remote names and URLs. origin is the conventional name created by clone — it isn’t a reserved Git keyword. origin/main is a remote-tracking branch, representing what the local repository knows about the remote main’s state as of the last fetch.

git fetch origin downloads the remote’s new commits and branch references, but doesn’t modify the working tree or any local branch. After fetching, you can use git log --oneline main..origin/main to see the commits added on the remote, and git diff main...origin/main to compare the two development lines.

git pull is usually equivalent to a fetch followed by a merge. git pull --ff-only only allows a fast-forward, stopping if there’s a divergence, so the developer can explicitly choose merge or rebase. Teams that prefer rebase can use git pull --rebase. A team should agree on one pull strategy, to avoid each member ending up with a differently shaped history.

The first time you publish a feature branch, run git push --set-upstream origin feature/search, and after that you can just run git push. If the remote branch has commits the local one doesn’t, a plain push will be rejected — you should fetch and integrate the remote commits first. When rewriting a personal remote branch genuinely does require a forced update, git push --force-with-lease checks that the remote is still in the expected state, which does a much better job of avoiding overwriting someone else’s new commits than an unchecked --force.

4.2 Resolving Merge and Rebase Conflicts

When two branches modify the same piece of text with different results, Git can’t determine which version to keep, and inserts conflict markers:

The marker <<<<<<< HEAD begins the current branch’s content, ======= separates the two versions, and >>>>>>> feature/search ends the incoming branch’s content.

Once a merge conflict occurs, first check the list of conflicts with git status. Edit each conflicting file to produce a final result that reflects the intent of both sides, remove the conflict markers, and run your tests, then run git add path/to/resolved-file and git commit. git merge --abort can cancel the entire merge.

When a rebase conflict occurs, fix and stage the files, then run git rebase --continue; git rebase --abort cancels the entire rebase. A conflict is only truly resolved once the code behaves as expected and passes its tests — not just once the conflict markers have disappeared.

4.3 Submitting a Feature Branch for Review

A common collaboration workflow looks like this:

git switch main
git pull --ff-only
git switch -c feature/search-filter
# edit and test
git add --patch
git diff --staged
git commit -m "Add topic filter to article search"
git push --set-upstream origin feature/search-filter

After pushing the branch, create a merge request in GitLab. New changes produced during review can continue to be committed to the same branch, and the merge request updates automatically. If you need to wire tests into commits and merge requests, continue on to the GitLab CI/CD Guide; if you want to self-host your code platform, see Deploying GitLab with Docker.

5. Safe Recovery and Stashing Work

5.1 Choosing a Tool Based on Which Area the Change Is In

Different recovery commands operate on different areas:

GoalCommandRewrites history?
Discard working tree changesgit restore path/to/fileNo
Unstage while keeping working tree changesgit restore --staged path/to/fileNo
Fix the most recent local commitgit commit --amendYes
Undo an already-shared commitgit revert <commit-id>No, creates a new commit
Move the current branchgit resetYes

Restore can discard unsaved working tree content, so check git diff before running it. Amend creates a new commit ID, which suits fixing the most recent commit as long as it hasn’t been shared. Revert creates an inverse commit while keeping the original one intact, which suits public branches and multi-person collaboration.

Reset moves the current branch, and its mode determines whether the staging area and working tree are also changed:

CommandStaging areaWorking tree
git reset --soft HEAD~1KeptKept
git reset --mixed HEAD~1ResetKept
git reset --hard HEAD~1ResetReset

--hard discards the content of both the working tree and the staging area. Reset suits cleaning up local history that hasn’t been shared yet — undoing a commit on a public branch should generally use revert instead.

5.2 Using Reflog to Recover a Commit

git reflog records how the local HEAD and branch references have moved. If you need to recover an old commit after a reset or rebase, you can find its commit ID in the reflog, then run git branch recovery <commit-id> to create a branch that preserves it. Reflog is a local record with a retention period, and it doesn’t sync to the remote, so it can’t substitute for a remote backup.

5.3 Stashing Unfinished Changes

When you need to temporarily switch branches but your changes aren’t ready to commit yet, you can follow this flow:

git stash push -m "WIP search form"
git switch main
# handle the temporary task
git switch feature/search
git stash pop

git stash apply restores the changes while keeping the stash entry, while git stash pop deletes the entry once it’s successfully restored. By default, stash doesn’t include untracked files; when you need to stash those too, use git stash push --include-untracked -m "WIP full tree". Stash suits a short-term switch — work that needs to be kept long-term or shared should be put on a clearly named branch with proper commits instead.

6. Tags, Keys, and Releases

Tags are typically used to mark official releases. An annotated tag preserves the creator, date, and description:

git tag -a v1.0.0 -m "Release 1.0.0"
git show v1.0.0
git push origin v1.0.0

A branch moves as new commits are added, while a version tag usually stays fixed pointing at a specific commit. If an official release needs a fix, creating a new version tag is clearer than moving an existing one. Before releasing, confirm the target commit has passed testing, the version number matches the changelog, and the build artifacts can be reproduced from the same source code.

When connecting to a remote repository over SSH, you can create a key with ssh-keygen -t ed25519 -C "[email protected]". Add the .pub public key’s content to your Git hosting platform, and keep the private key locally with appropriate permissions set. A private key should never be pasted into a website, a chat message, or a Git repository. For key management and server-side SSH hardening, see Initializing and Hardening a Linux Cloud Server.

7. A Diagnostic Order and Further Practice

When you run into a Git problem, gather status information first, before running any command that modifies history or the working tree:

git status
git branch -vv
git remote -v
git log --oneline --decorate --graph --all -20
git diff
git diff --staged

The status output can answer which branch you’re currently on, which remote that branch tracks, what changes haven’t been committed yet, and how the commit graph branches out. Once you’ve determined whether a change lives in the working tree, the staging area, local history, or the remote, choosing between add, commit, fetch, merge, rebase, push, restore, revert, or reset becomes much easier, and you’ll avoid most accidental mistakes.

Beginners can work through three tasks in a practice repository, in sequence: first create two small commits, then create a feature branch from the first commit and deliberately produce a controlled conflict, and finally use revert and reset separately to observe the commit graph. Pairing every step with git status, git diff, and a graphical git log makes Git’s area model and branch model much easier to grasp than simply memorizing commands.