A commit message is part of your project's long-term memory. It explains why a change exists and helps later readers understand what happened without opening every file diff first.
Good messages improve:
git blame investigationsA weak message like fix stuff quickly becomes useless. A stronger message like fix(auth): handle expired refresh tokens gives useful context immediately.
Tip: Write commit messages for the person reading them months later, not just for the moment you type them.
A widely used set of guidelines includes these rules:
Example:
Add validation for release tag names
Reject tag names that do not match the release policy. This prevents
invalid CI/CD releases from being published accidentally.Subjects like Add, Fix, and Refactor read naturally after the phrase "If applied, this commit will...".
Bad examples:
stuff
misc fixes
updated filesBetter examples:
docs: clarify GitHub workflow steps
fix(ci): restore cache key for Node 22 builds
feat(api): add pagination to audit log endpointThe better versions are specific and searchable.
Conventional Commits define a standard format that both people and tooling can understand.
Basic pattern:
type(scope): descriptionCommon types include:
feat:fix:docs:chore:refactor:test:build:ci:Examples:
feat(auth): add SSO login flow
fix(ui): prevent navbar overlap on mobile
docs(git): add submodule troubleshooting sectionA breaking change can be marked with ! or with a BREAKING CHANGE: note in the body.
feat(api)!: rename userId field to accountId
BREAKING CHANGE: Clients must send accountId instead of userId.Note: Conventional Commits are most useful when the whole team follows the same format and a hook or CI job validates it.
Conventional Commits connect well to semantic versioning:
A common mapping is:
feat: â minor bumpfix: â patch bumpBREAKING CHANGE: or ! â major bumpThat makes commit history useful for release automation.
commitlint checks whether commit messages follow your rules.
npm install --save-dev @commitlint/cli @commitlint/config-conventionalExample config:
cat > commitlint.config.js <<'EOF'
module.exports = { extends: ['@commitlint/config-conventional'] };
EOFTest a message:
echo "fix: correct cache key" | npx commitlintTeams often run this from a commit-msg hook.
semantic-release reads structured commit history and automates versioning and release notes.
npm install --save-dev semantic-releaseA common pattern is:
commitlint validates themsemantic-release on mainGood commit messages make history easier to search, review, debug, and release. The seven classic rules improve clarity, while Conventional Commits provide a machine-readable format that works well with tools like commitlint and semantic-release. Better commit messages are a small habit with long-term payoff.
<MCQExercise exercise={{ id: 'git-commit-messages-q1', title: 'Exercise 1: Why commit messages matter', description: 'Which is the best reason strong commit messages improve long-term project maintenance?', options: [ { id: 'a', text: 'They make Git repositories smaller on disk' }, { id: 'b', text: 'They help people understand change history, intent, and release information later' }, { id: 'c', text: 'They automatically remove the need for pull requests' }, { id: 'd', text: 'They prevent all merge conflicts by themselves' }, ], correctId: 'b', explanation: 'The lesson emphasized that good commit messages improve history scanning, blame investigations, changelogs, and team understanding. They preserve the purpose behind a change, which is valuable long after the code is written.', wrongExplanations: { a: 'Commit wording does not meaningfully reduce repository size.', c: 'Clear messages help review, but they do not replace review workflows.', d: 'Merge conflicts come from overlapping changes, not from weak commit message wording.', }, }} />
<MCQExercise
exercise={{
id: 'git-commit-messages-q2',
title: 'Exercise 2: Recognizing a Conventional Commit',
description: 'Which option follows the Conventional Commits pattern from the lesson?',
options: [
{ id: 'a', text: 'fixed bug in login page' },
{ id: 'b', text: 'Login bug fix.' },
{ id: 'c', text: 'fix(auth): handle expired refresh tokens' },
{ id: 'd', text: 'CHANGE THINGS NOW' },
],
correctId: 'c',
explanation: 'Conventional Commits use a structured header such as type(scope): description. The option fix(auth): handle expired refresh tokens matches that format and clearly communicates both the kind of change and its area.',
wrongExplanations: {
a: 'It describes a change, but it does not use the structured Conventional Commits format.',
b: 'It is closer to a normal subject line, but it lacks a commit type and scope structure.',
d: 'It is neither descriptive nor formatted according to the convention.',
},
}}
/>
<MCQExercise
exercise={{
id: 'git-commit-messages-q3',
title: 'Exercise 3: Linking commits to version bumps',
description: 'According to the lesson, which type of change most directly signals a MAJOR semantic version bump?',
options: [
{ id: 'a', text: 'A fix: commit that corrects a typo' },
{ id: 'b', text: 'A docs: commit that updates a tutorial example' },
{ id: 'c', text: 'A feat: commit that adds a backward-compatible feature' },
{ id: 'd', text: 'A commit marked with BREAKING CHANGE: or ! because it introduces incompatibility' },
],
correctId: 'd',
explanation: 'The tutorial connected semantic versioning with Conventional Commits and explained that breaking changes map to a MAJOR release bump. Markers like BREAKING CHANGE: or ! signal that downstream users may need to adapt.',
wrongExplanations: {
a: 'Bug fixes normally map to PATCH releases, not MAJOR ones.',
b: 'Documentation changes do not usually require a version bump for released software behavior.',
c: 'New backward-compatible features typically map to a MINOR bump, not a MAJOR one.',
},
}}
/>
Because Git itself does. Its generated messages read "Merge branch 'main'" and "Revert 'Add caching'", so an imperative summary sits consistently alongside them. The practical test is that the message should complete the sentence "If applied, this commit willâ¦", "Add rate limiting" reads correctly, "Added rate limiting" does not. It is a convention rather than a rule, and its real benefit is consistency: a log where every line is phrased the same way is far quicker to scan.
A format that prefixes the summary with a type and optional scope: feat(auth): add password reset, fix: handle empty cart, chore(deps): bump axios. The point is machine readability. Tooling can derive a changelog and the next semantic version from the log without anyone maintaining either by hand. You need them if you want automated releases or generated changelogs. If you do not, the discipline still helps, but plain clear messages are perfectly adequate.
fix: produces a patch bump, feat: a minor bump, and a breaking change a major bump. A breaking change is marked either with ! after the type, feat!: drop Node 18 support, or a BREAKING CHANGE: footer in the body. Tools such as semantic-release read the commits since the last tag, work out the highest bump implied, then tag the release and write the changelog. That is why the format is strict: the version number is derived from it.
git commit --amend rewrites the most recent message, but amending changes the commit hash, so a pushed commit then needs git push --force-with-lease. That is acceptable on your own feature branch and not on a shared one, where everyone who pulled is left on a commit that no longer exists. For older commits, git rebase -i HEAD~n and marking them reword does the same job with the same caveat. On main, the honest answer is to leave it.
Use commitlint with a commit-msg hook, installed through Husky so the hook lives in the repository and arrives with a clone. Raw .git/hooks files are not version-controlled and so never reach anyone else. That rejects a badly formatted message before the commit is created. Back it up with a server-side check in CI, since local hooks can be skipped with --no-verify; a pull request check that validates the commits is what actually makes the convention stick.
2d:["$","$L2e",null,{"tutorial":{"slug":"git-branching","title":"Git Branching","seoTitle":"Git Branching: Create, Switch and Delete Branches Explained","description":"Learn what a Git branch really is, git switch versus checkout, how HEAD and detached HEAD work, and how to delete, rename and name branches well.","content":"$2f","difficulty":"Easy","duration":"18 min","keywords":["git branching","git checkout switch branch","git head detached head","create branch git","delete branch git","git branch","git switch vs checkout","detached HEAD","delete git branch","delete remote branch","rename git branch","git branch naming convention","git HEAD explained","create new branch git","git fetch --prune"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":4,"tableOfContents":[{"id":"what-a-branch-really-is","title":"What a Branch Really Is","level":2},{"id":"why-branching-matters","title":"Why Branching Matters","level":2},{"id":"viewing-branches","title":"Viewing Branches","level":2},{"id":"creating-a-branch","title":"Creating a Branch","level":2},{"id":"switching-between-branches","title":"Switching Between Branches","level":2},{"id":"real-example","title":"Real Example","level":3},{"id":"understanding-head","title":"Understanding HEAD","level":2},{"id":"deleting-a-branch","title":"Deleting a Branch","level":2},{"id":"detached-head-explained","title":"Detached HEAD Explained","level":2},{"id":"safe-way-to-continue-from-detached-head","title":"Safe Way to Continue from Detached HEAD","level":3},{"id":"good-branch-naming-habits","title":"Good Branch Naming Habits","level":2},{"id":"a-practical-branch-workflow","title":"A Practical Branch Workflow","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-creating-isolated-work","title":"Exercise 1: Creating isolated work","level":3},{"id":"exercise-2-reading-head-correctly","title":"Exercise 2: Reading HEAD correctly","level":3},{"id":"exercise-3-escaping-detached-head-safely","title":"Exercise 3: Escaping detached HEAD safely","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git switch and git checkout?","answer":"git checkout does several unrelated jobs, moving between branches, creating them, and restoring files, which made it a common source of accidents, since git checkout <file> silently discarded changes. Git split those responsibilities: git switch changes branches and git restore restores files. git switch -c <name> replaces git checkout -b <name>. Both new commands are stable and preferred in new work; checkout still functions and is not going away, so older documentation remains valid."},{"question":"What does "detached HEAD" mean and how do I fix it?","answer":"HEAD normally points at a branch name, which in turn points at a commit. When you check out a commit, tag, or remote-tracking branch directly, HEAD points straight at that commit with no branch attached, that is detached. You can look around and even commit, but those commits belong to no branch and become unreachable once you leave. If you have made commits you want to keep, git switch -c <new-branch> right where you are captures them. If you have not, git switch main returns you to normal."},{"question":"How do I delete a local and a remote branch?","answer":"git branch -d <name> deletes a local branch and refuses if it holds commits not merged anywhere, which is a safety net worth keeping; -D forces it. For the remote, git push origin --delete <name>. Deleting a remote branch does not remove other people's stale local copies of it. They need git fetch --prune, or git config --global fetch.prune true to have it happen automatically on every fetch."},{"question":"How do I rename a Git branch?","answer":"git branch -m <new-name> renames the branch you are on, or git branch -m <old> <new> for another one. Renaming a branch that exists on the remote takes three steps, because the remote branch is a separate reference: rename locally, push the new name with git push origin -u <new-name>, then delete the old one with git push origin --delete <old-name>. Anyone else working on it needs to re-point their local branch, so agree the change first."},{"question":"What is a good branch naming convention?","answer":"Use a short prefix that says what kind of change it is, then a brief description in kebab-case: feature/user-search, fix/login-redirect, chore/bump-deps. Many teams add the ticket number, feature/PROJ-412-user-search, which lets tooling link branches to issues automatically. Keep them lowercase and avoid spaces and unusual characters, since branch names become part of URLs and filesystem paths. The value is not aesthetic: a consistent scheme makes git branch --list 'fix/' and automated cleanup possible."},{"question":"Does creating a branch in Git copy all my files?","answer":"No, and this is why Git branching is fast where older systems were slow. A branch is a 41-byte file containing a commit hash. A pointer, nothing more. Creating one writes that file and does not touch your working directory at all, which is why git switch -c is instant even in a huge repository. Switching branches then updates only the files that actually differ between the two commits."}]},"allTutorials":[{"slug":"introduction-to-git","title":"Introduction to Git","seoTitle":"What Is Git? Version Control for Beginners Explained","description":"Learn what Git is and why version control matters: distributed versus centralized systems, how Git stores history and the key terms to know first.","content":"$30","difficulty":"Easy","duration":"12 min","keywords":["git tutorial","introduction to git","version control for beginners","distributed version control","git vs svn","what is git","git vs github","version control system","git for beginners","how git stores data","git terminology","centralized vs distributed vcs","why use git","git for devops"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":1,"tableOfContents":[{"id":"what-is-git","title":"What Is Git?","level":2},{"id":"why-version-control-matters","title":"Why Version Control Matters","level":2},{"id":"a-real-example","title":"A Real Example","level":3},{"id":"how-git-stores-history","title":"How Git Stores History","level":2},{"id":"git-vs-other-version-control-systems","title":"Git vs Other Version Control Systems","level":2},{"id":"why-teams-prefer-git","title":"Why Teams Prefer Git","level":3},{"id":"centralized-vs-distributed-version-control","title":"Centralized vs Distributed Version Control","level":2},{"id":"centralized-version-control","title":"Centralized Version Control","level":3},{"id":"distributed-version-control","title":"Distributed Version Control","level":3},{"id":"git-in-everyday-devops-work","title":"Git in Everyday DevOps Work","level":2},{"id":"key-terms-to-remember","title":"Key Terms to Remember","level":2},{"id":"repository","title":"Repository","level":3},{"id":"commit","title":"Commit","level":3},{"id":"branch","title":"Branch","level":3},{"id":"clone","title":"Clone","level":3},{"id":"remote","title":"Remote","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-distributed-history","title":"Exercise 1: Distributed history","level":3},{"id":"exercise-2-snapshots-vs-file-copies","title":"Exercise 2: Snapshots vs file copies","level":3},{"id":"exercise-3-git-in-devops-work","title":"Exercise 3: Git in DevOps work","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is Git and why is it used?","answer":"Git is a distributed version control system that records snapshots of a project over time, so you can see what changed, when, by whom and why, and return to any earlier state. It is used because it makes two otherwise hard things routine: several people editing the same codebase without overwriting each other, and experimenting on a branch without risking the working version. Every modern deployment pipeline is triggered by Git, which is why it is the one tool essentially every developer and operator shares."},{"question":"What is the difference between Git and GitHub?","answer":"Git is the version control software that runs on your machine and does all the actual work. Committing, branching, merging, storing history. GitHub is a commercial website that hosts Git repositories and adds collaboration features Git does not have: pull requests, issues, code review, permissions and CI. Git works completely without GitHub, and GitLab, Bitbucket and self-hosted alternatives provide the same role. Confusing the two is common because most people meet Git through GitHub."},{"question":"What does "distributed" mean in distributed version control?","answer":"Every clone is a complete repository, with the full history and every branch, rather than a thin checkout of a central server. That has three practical consequences: nearly every operation, log, diff, branch, commit, is local and therefore instant, you can work entirely offline and sync later, and there is no single point of failure because every clone is effectively a backup. Centralised systems such as SVN keep history only on the server, which is why they needed a connection for operations Git does in milliseconds."},{"question":"Does Git store a copy of every file in every commit?","answer":"Conceptually yes. Each commit references a complete snapshot of the tree. Physically no: Git stores each unique piece of content once, addressed by its SHA-1 hash, so a file unchanged across a thousand commits is stored once and referenced a thousand times. It later repacks objects into compressed packfiles that store deltas between similar ones. This is why a repository with years of history is often smaller than the working directory it produced."},{"question":"Do I need to know Git to work in DevOps?","answer":"Yes, and not merely to store code. Infrastructure as code, pipeline definitions, Kubernetes manifests and configuration all live in Git, and GitOps goes further by making the repository the declared desired state of production. That means a Git revert can be a rollback, a branch protection rule can be a deployment control, and a commit message can be the audit record for a change. Git fluency is closer to a prerequisite than a skill in this field."},{"question":"What are the main Git terms a beginner needs?","answer":"Six carry most of the weight. A repository is the project plus its complete history. A commit is one recorded snapshot with a message and a parent. A branch is a movable pointer to a commit, which is why creating one is instant. HEAD is where you currently are. A remote is another copy of the repository, conventionally named origin. The staging area is the draft of your next commit, and it is the concept that makes the add-then-commit two-step make sense."}]},{"slug":"installing-git","title":"Installing Git","seoTitle":"How to Install Git on macOS, Linux and Windows","description":"Install Git on macOS, Linux and Windows, then configure what matters: your identity, default branch, editor and .gitattributes for consistent line endings.","content":"$31","difficulty":"Easy","duration":"10 min","keywords":["install git","git config","git setup for beginners","git on windows","git on macos","install git mac","install git windows","install git ubuntu","git config user.name email","please tell me who you are git","git for windows vs wsl","gitattributes line endings","git autocrlf","check git version"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":2,"tableOfContents":[{"id":"why-installation-and-setup-matter","title":"Why Installation and Setup Matter","level":2},{"id":"installing-git-on-macos","title":"Installing Git on macOS","level":2},{"id":"installing-git-on-linux","title":"Installing Git on Linux","level":2},{"id":"ubuntu-or-debian","title":"Ubuntu or Debian","level":3},{"id":"fedora","title":"Fedora","level":3},{"id":"centos-or-rhel","title":"CentOS or RHEL","level":3},{"id":"arch-linux","title":"Arch Linux","level":3},{"id":"installing-git-on-windows","title":"Installing Git on Windows","level":2},{"id":"configuring-your-identity","title":"Configuring Your Identity","level":2},{"id":"why-this-matters","title":"Why This Matters","level":3},{"id":"setting-your-default-editor","title":"Setting Your Default Editor","level":2},{"id":"choosing-a-default-branch-name","title":"Choosing a Default Branch Name","level":2},{"id":"useful-optional-setup","title":"Useful Optional Setup","level":2},{"id":"show-helpful-colors","title":"Show Helpful Colors","level":3},{"id":"cache-credentials-temporarily-on-macos-or-linux","title":"Cache Credentials Temporarily on macOS or Linux","level":3},{"id":"use-the-windows-credential-manager","title":"Use the Windows Credential Manager","level":3},{"id":"local-vs-global-configuration","title":"Local vs Global Configuration","level":2},{"id":"a-good-first-time-setup-checklist","title":"A Good First-Time Setup Checklist","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-verifying-installation","title":"Exercise 1: Verifying installation","level":3},{"id":"exercise-2-choosing-an-editor","title":"Exercise 2: Choosing an editor","level":3},{"id":"exercise-3-global-vs-local-identity","title":"Exercise 3: Global vs local identity","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I check whether Git is already installed?","answer":"git --version prints the version if it is present. On macOS this can be misleading: the command may exist and trigger a prompt to install the Xcode Command Line Tools, or report an Apple-shipped build that lags well behind upstream. which git tells you where it came from, /usr/bin/git is Apple's, /opt/homebrew/bin/git or /usr/local/bin/git is Homebrew's. Install via Homebrew if you want current Git on a Mac."},{"question":"What is the first thing to configure after installing Git?","answer":"Your identity, because Git refuses to commit without it and it is stamped permanently into every commit you make: git config --global user.name "Your Name" and git config --global user.email "you@example.com". Use the email associated with your hosting account, or your commits will not be attributed to you. Then set init.defaultBranch main to match what GitHub and GitLab expect, and core.editor to something you can exit."},{"question":"Why does Git say "Please tell me who you are"?","answer":"No user.name or user.email is configured, so Git cannot author a commit. Set them globally with the two --global commands, or per repository without the flag when a project needs a different address. The error usually appears on a brand new machine, or after cloning into a repository with a local config that cleared the values. Commits already made under the wrong identity can be corrected with git commit --amend --reset-author, though only safely before pushing."},{"question":"Should I install Git with Homebrew or use the version macOS ships?","answer":"Homebrew, if you use Git seriously. The Apple-supplied build is tied to Xcode Command Line Tools updates and is often several minor versions behind, which means missing newer commands and flags, git switch and git restore arrived relatively recently, for example. brew install git gives you current Git that updates with everything else you have installed. Make sure Homebrew's path precedes /usr/bin in your PATH so the new one actually wins."},{"question":"What is the difference between Git for Windows and Git in WSL?","answer":"They are two separate installations that do not share configuration or credentials. Git for Windows provides Git Bash and integrates with Windows tooling; Git inside a WSL distribution is a Linux Git operating on the Linux filesystem. Mixing them causes real problems, line endings, file permissions and credential helpers all differ, so pick one per project and stay with it. If you develop inside WSL, use WSL's Git and keep repositories in the Linux filesystem rather than under /mnt/c, which is also much faster."},{"question":"How should I handle line endings across Windows, macOS and Linux?","answer":"Add a .gitattributes file to the repository with text=auto so Git normalises text files to LF in history while checking them out natively, plus explicit rules for files that must stay LF, .sh text eol=lf in particular, since a shell script with CRLF fails inside a Linux container with a baffling "not found". Committing .gitattributes is better than relying on each person's core.autocrlf setting, because it makes the behaviour a property of the project rather than of whoever cloned it."}]},{"slug":"git-basics","title":"Git Basics","seoTitle":"Git Basics: init, clone, add, commit, status, log and diff","description":"Learn the everyday Git loop with init, clone, status, add, commit, log and diff, understand the staging area and build checks to run before every commit.","content":"$32","difficulty":"Easy","duration":"15 min","keywords":["git basics","git add commit status","git clone tutorial","git log diff","beginner git workflow","git add vs commit","git staging area","git init vs clone","git diff --staged","git unstage file","git log --oneline --graph","git commit -m","git status explained","git commands for beginners"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":3,"tableOfContents":[{"id":"the-everyday-git-workflow","title":"The Everyday Git Workflow","level":2},{"id":"starting-a-repository-with-git-init","title":"Starting a Repository with git init","level":2},{"id":"copying-an-existing-repository-with-git-clone","title":"Copying an Existing Repository with git clone","level":2},{"id":"checking-state-with-git-status","title":"Checking State with git status","level":2},{"id":"staging-changes-with-git-add","title":"Staging Changes with git add","level":2},{"id":"why-staging-is-useful","title":"Why Staging Is Useful","level":3},{"id":"saving-work-with-git-commit","title":"Saving Work with git commit","level":2},{"id":"seeing-history-with-git-log","title":"Seeing History with git log","level":2},{"id":"real-example","title":"Real Example","level":3},{"id":"comparing-changes-with-git-diff","title":"Comparing Changes with git diff","level":2},{"id":"example-workflow","title":"Example Workflow","level":3},{"id":"putting-it-all-together","title":"Putting It All Together","level":2},{"id":"common-beginner-mistakes","title":"Common Beginner Mistakes","level":2},{"id":"committing-without-checking-status","title":"Committing Without Checking Status","level":3},{"id":"using-vague-commit-messages","title":"Using Vague Commit Messages","level":3},{"id":"staging-everything-automatically","title":"Staging Everything Automatically","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-starting-a-repository","title":"Exercise 1: Starting a repository","level":3},{"id":"exercise-2-selective-staging","title":"Exercise 2: Selective staging","level":3},{"id":"exercise-3-reviewing-the-staged-version","title":"Exercise 3: Reviewing the staged version","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git add and git commit?","answer":"git add moves changes into the staging area, which is a draft of your next commit. git commit records whatever is staged as a permanent snapshot in history. The two-step design is the point: you can edit ten files and commit them as three logical commits by staging selectively, which makes history readable and reverting precise. git add -p takes this further, letting you stage individual chunks within a single file."},{"question":"What is the difference between git init and git clone?","answer":"git init turns an existing directory into a new repository with no history and no remote. You use it once, when starting something from scratch. git clone <url> copies an existing remote repository, bringing its full history and automatically configuring origin so git push and git pull work immediately. If you init when you meant to clone, you end up with an empty repository and have to add the remote by hand with git remote add origin <url>."},{"question":"How do I see what changed before committing?","answer":"git diff shows changes you have not staged, and git diff --staged shows what you have staged, that second one is the real pre-commit review, because it is exactly what the commit will contain. git status gives the file-level overview of both. A useful habit is git diff --staged immediately before every commit; it catches stray debug statements and accidentally staged files more reliably than any hook."},{"question":"How do I unstage a file I added by mistake?","answer":"git restore --staged <file> removes it from the staging area while keeping your edits in the working directory. The older equivalent is git reset HEAD <file>, which you will still see in documentation and which does the same thing. If you want to discard the edits as well, git restore <file> does that, and it is not recoverable, since those changes were never recorded anywhere, so check git diff first."},{"question":"What does git log show and how do I make it readable?","answer":"By default it prints every commit with full hash, author, date and message, which is a lot of screen for little information. git log --oneline gives one commit per line, and git log --oneline --graph --all --decorate adds the branch structure, which is the single most useful way to understand what state a repository is in. git log -p <file> shows the actual changes to one file over time, and git log --since="2 weeks ago" narrows the range."},{"question":"Why does git commit open an editor, and how do I avoid it?","answer":"Because no message was supplied, so Git asks for one. Passing -m "your message" writes it inline and skips the editor, which is what most people use day to day. If the editor opens and you are stuck in Vim, :wq saves and :q! aborts the commit. Set something you know with git config --global core.editor "code --wait" for VS Code or "nano" for nano. Reserve the editor for commits that deserve a body explaining why, which -m makes awkward."}]},{"slug":"git-branching","title":"Git Branching","seoTitle":"Git Branching: Create, Switch and Delete Branches Explained","description":"Learn what a Git branch really is, git switch versus checkout, how HEAD and detached HEAD work, and how to delete, rename and name branches well.","content":"$33","difficulty":"Easy","duration":"18 min","keywords":"$2d:props:tutorial:keywords","publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":4,"tableOfContents":[{"id":"what-a-branch-really-is","title":"What a Branch Really Is","level":2},{"id":"why-branching-matters","title":"Why Branching Matters","level":2},{"id":"viewing-branches","title":"Viewing Branches","level":2},{"id":"creating-a-branch","title":"Creating a Branch","level":2},{"id":"switching-between-branches","title":"Switching Between Branches","level":2},{"id":"real-example","title":"Real Example","level":3},{"id":"understanding-head","title":"Understanding HEAD","level":2},{"id":"deleting-a-branch","title":"Deleting a Branch","level":2},{"id":"detached-head-explained","title":"Detached HEAD Explained","level":2},{"id":"safe-way-to-continue-from-detached-head","title":"Safe Way to Continue from Detached HEAD","level":3},{"id":"good-branch-naming-habits","title":"Good Branch Naming Habits","level":2},{"id":"a-practical-branch-workflow","title":"A Practical Branch Workflow","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-creating-isolated-work","title":"Exercise 1: Creating isolated work","level":3},{"id":"exercise-2-reading-head-correctly","title":"Exercise 2: Reading HEAD correctly","level":3},{"id":"exercise-3-escaping-detached-head-safely","title":"Exercise 3: Escaping detached HEAD safely","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git switch and git checkout?","answer":"git checkout does several unrelated jobs, moving between branches, creating them, and restoring files, which made it a common source of accidents, since git checkout <file> silently discarded changes. Git split those responsibilities: git switch changes branches and git restore restores files. git switch -c <name> replaces git checkout -b <name>. Both new commands are stable and preferred in new work; checkout still functions and is not going away, so older documentation remains valid."},{"question":"What does "detached HEAD" mean and how do I fix it?","answer":"HEAD normally points at a branch name, which in turn points at a commit. When you check out a commit, tag, or remote-tracking branch directly, HEAD points straight at that commit with no branch attached, that is detached. You can look around and even commit, but those commits belong to no branch and become unreachable once you leave. If you have made commits you want to keep, git switch -c <new-branch> right where you are captures them. If you have not, git switch main returns you to normal."},{"question":"How do I delete a local and a remote branch?","answer":"git branch -d <name> deletes a local branch and refuses if it holds commits not merged anywhere, which is a safety net worth keeping; -D forces it. For the remote, git push origin --delete <name>. Deleting a remote branch does not remove other people's stale local copies of it. They need git fetch --prune, or git config --global fetch.prune true to have it happen automatically on every fetch."},{"question":"How do I rename a Git branch?","answer":"git branch -m <new-name> renames the branch you are on, or git branch -m <old> <new> for another one. Renaming a branch that exists on the remote takes three steps, because the remote branch is a separate reference: rename locally, push the new name with git push origin -u <new-name>, then delete the old one with git push origin --delete <old-name>. Anyone else working on it needs to re-point their local branch, so agree the change first."},{"question":"What is a good branch naming convention?","answer":"Use a short prefix that says what kind of change it is, then a brief description in kebab-case: feature/user-search, fix/login-redirect, chore/bump-deps. Many teams add the ticket number, feature/PROJ-412-user-search, which lets tooling link branches to issues automatically. Keep them lowercase and avoid spaces and unusual characters, since branch names become part of URLs and filesystem paths. The value is not aesthetic: a consistent scheme makes git branch --list 'fix/' and automated cleanup possible."},{"question":"Does creating a branch in Git copy all my files?","answer":"No, and this is why Git branching is fast where older systems were slow. A branch is a 41-byte file containing a commit hash. A pointer, nothing more. Creating one writes that file and does not touch your working directory at all, which is why git switch -c is instant even in a huge repository. Switching branches then updates only the files that actually differ between the two commits."}]},{"slug":"git-merging","title":"Git Merging","seoTitle":"Git Merge Explained: Fast-Forward, Three-Way and Conflicts","description":"Learn git merge: fast-forward versus three-way merges, resolving conflict markers step by step, --no-ff and squash merges, and aborting a bad merge.","content":"$34","difficulty":"Medium","duration":"20 min","keywords":["git merge tutorial","fast forward merge","three way merge","resolve merge conflicts","git merge no ff","git merge conflict","resolve merge conflict","git merge --no-ff","fast-forward merge","three-way merge git","git merge --abort","squash merge vs merge commit","git already up to date","conflict markers git","git diff --diff-filter=U"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":5,"tableOfContents":[{"id":"what-merging-does","title":"What Merging Does","level":2},{"id":"fast-forward-merge","title":"Fast-Forward Merge","level":2},{"id":"why-it-happens","title":"Why It Happens","level":3},{"id":"three-way-merge","title":"Three-Way Merge","level":2},{"id":"when-teams-use---no-ff","title":"When Teams Use --no-ff","level":2},{"id":"why-use---no-ff","title":"Why Use --no-ff?","level":3},{"id":"what-a-merge-conflict-is","title":"What a Merge Conflict Is","level":2},{"id":"step-by-step-conflict-resolution","title":"Step-by-Step Conflict Resolution","level":2},{"id":"what-the-markers-mean","title":"What the Markers Mean","level":3},{"id":"resolve-the-file","title":"Resolve the File","level":3},{"id":"verifying-the-merge","title":"Verifying the Merge","level":2},{"id":"abort-a-problematic-merge","title":"Abort a Problematic Merge","level":2},{"id":"merge-vs-rebase-at-a-high-level","title":"Merge vs Rebase at a High Level","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-recognizing-a-fast-forward","title":"Exercise 1: Recognizing a fast-forward","level":3},{"id":"exercise-2-preserving-feature-history","title":"Exercise 2: Preserving feature history","level":3},{"id":"exercise-3-finishing-conflict-resolution","title":"Exercise 3: Finishing conflict resolution","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"How do I resolve a Git merge conflict?","answer":"Open each file Git lists as conflicted and find the <<<<<<<, ======= and >>>>>>> markers. The section above ======= is the branch you are on, the section below is the branch you are merging in. Edit the file into the state you actually want, often a combination of both, not a choice between them, delete all three markers, then git add the file and git commit. git status tracks which files are still outstanding, and git diff --name-only --diff-filter=U lists just the unresolved ones."},{"question":"What is the difference between a fast-forward and a three-way merge?","answer":"A fast-forward happens when the target branch has no commits since you branched off, so Git simply moves the pointer forward to your latest commit, no merge commit, and history stays linear. A three-way merge happens when both branches have moved on, so Git combines them using the common ancestor plus each tip, and records the result as a merge commit with two parents. You cannot choose: Git fast-forwards whenever it can, unless you pass --no-ff to force a merge commit."},{"question":"When should I use git merge --no-ff?","answer":"When you want the branch to remain visible in history. --no-ff forces a merge commit even where a fast-forward was possible, so the graph shows that a group of commits belonged to one feature and git log --graph reads as a series of merged branches. That makes reverting a whole feature easy, one git revert -m 1 on the merge commit, and it is why many teams enable it for release branches. The cost is a busier graph, which is why other teams prefer squash merges instead."},{"question":"How do I abort a merge and start over?","answer":"git merge --abort returns you to the state before the merge began, discarding partial resolutions. It works while the merge is in progress, that is, before you commit, which covers the case where the conflicts turn out to be more than you expected. If you have already committed the merge, use git reset --hard HEAD1 to drop it locally, and git revert -m 1 <merge-sha> if it has already been pushed, since resetting a shared branch strands everyone else."},{"question":"What does "Already up to date" mean when merging?","answer":"Every commit on the branch you are merging is already reachable from your current branch, so there is nothing to bring in. The usual cause is not having fetched: your local copy of the remote branch is stale, and Git is telling you the truth about what it knows. Run git fetch first, then merge. The other cause is merging the wrong direction. Merging main into your feature branch when you meant to merge your feature into main."},{"question":"Should my team use merge commits, squash merges, or rebase merges?","answer":"Squash merges suit most teams: each pull request becomes one commit on main, so history reads as a list of shipped changes and reverting is trivial, at the cost of losing the individual commits. Merge commits preserve everything and keep the branch visible, which matters when you need the full record for audit or bisecting inside a feature. Rebase merges give a linear history with every commit kept, which is tidy but rewrites hashes. Pick one and enforce it in the repository settings rather than leaving it per-developer."}]},{"slug":"git-rebasing","title":"Git Rebasing","seoTitle":"Git Rebase Tutorial: Interactive Rebase, Squash and Safety","description":"Use git rebase for a linear history: basic and interactive rebase, squashing, recovering with --abort and reflog, force-with-lease, and when to merge instead.","content":"$35","difficulty":"Medium","duration":"20 min","keywords":["git rebase tutorial","interactive rebase","squash commits git","rebase vs merge","git rebase i","git rebase","git rebase vs merge","git interactive rebase","git rebase --abort","git push force-with-lease","git pull --rebase","git rebase conflict","git rerere","rewrite git history"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":6,"tableOfContents":[{"id":"what-rebase-does","title":"What Rebase Does","level":2},{"id":"why-developers-rebase","title":"Why Developers Rebase","level":2},{"id":"basic-rebase-example","title":"Basic Rebase Example","level":2},{"id":"rebase-conflicts","title":"Rebase Conflicts","level":2},{"id":"interactive-rebase-with--i","title":"Interactive Rebase with n where n is how many commits back you want to go, then change pick to squash (or s) on every commit except the first. The first stays pick because the others are folded into it. Git then opens an editor to combine the messages. If you only want the first commit's message, use fixup instead of squash to discard the others. Many platforms also offer "Squash and merge" on a pull request, which achieves the same result without rebasing locally."},{"question":"How do I get out of a rebase that has gone wrong?","answer":"git rebase --abort returns the branch to exactly where it was before the rebase started, at any point during the process. That is the escape hatch to reach for whenever you are unsure. If the rebase has already completed and you want the old state back, git reflog lists where HEAD has been, and git reset --hard HEAD@{n} moves you to the entry before the rebase. The reflog is why a completed rebase is recoverable and why "I lost my work" is almost never actually true."},{"question":"Why do I keep hitting the same conflict repeatedly during a rebase?","answer":"Because a rebase replays each commit in turn, so a line you changed in several commits conflicts once per commit. Resolve the file, git add it, then git rebase --continue to move to the next one. Two things help: squashing related commits before rebasing so there are fewer replays, and enabling git config --global rerere.enabled true, which records how you resolved a conflict and reapplies that resolution automatically the next time the identical conflict appears."},{"question":"What does git pull --rebase do and should I use it?","answer":"It fetches the remote branch and replays your local commits on top of it, instead of creating a merge commit. Without it, a plain git pull on a branch that has moved on both sides produces a "Merge branch 'main' of ..." commit, and a busy repository fills with them. Setting git config --global pull.rebase true makes it the default and keeps history linear. It is safe for your own unpushed commits, which is exactly what git pull is dealing with."}]},{"slug":"git-remote","title":"Git Remote","seoTitle":"Git Remote: fetch vs pull, Rejected Push, Upstream Tracking","description":"Work with Git remotes confidently: fetch versus pull, fixing a rejected push without force, upstream tracking, pruning branches and origin versus upstream.","content":"$36","difficulty":"Easy","duration":"15 min","keywords":["git remote tutorial","git fetch pull push","tracking branches git","upstream branch git","git remote add origin","git remote","git fetch vs pull","git push rejected non-fast-forward","git push -u origin main","git remote set-url","origin vs upstream git","git branch -r","git fetch --prune","git remote add upstream","git upstream tracking branch"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":7,"tableOfContents":[{"id":"what-a-remote-is","title":"What a Remote Is","level":2},{"id":"adding-and-removing-remotes","title":"Adding and Removing Remotes","level":2},{"id":"downloading-information-with-git-fetch","title":"Downloading Information with -i","level":2},{"id":"common-interactive-actions","title":"Common Interactive Actions","level":3},{"id":"squashing-commits","title":"Squashing Commits","level":2},{"id":"rebase-vs-merge","title":"Rebase vs Merge","level":2},{"id":"simple-rule-of-thumb","title":"Simple Rule of Thumb","level":3},{"id":"a-safe-team-habit","title":"A Safe Team Habit","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-understanding-rebased-commits","title":"Exercise 1: Understanding rebased commits","level":3},{"id":"exercise-2-cleaning-up-tiny-commits","title":"Exercise 2: Cleaning up tiny commits","level":3},{"id":"exercise-3-pushing-a-rebased-branch-safely","title":"Exercise 3: Pushing a rebased branch safely","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git rebase and git merge?","answer":"Both integrate one branch into another; they differ in what history they leave behind. git merge creates a merge commit joining the two lines of development, preserving exactly what happened including the fact that work happened in parallel. git rebase replays your commits on top of the target branch, producing a straight line as though you had started from the latest code. Merge is honest about history and clutters the graph; rebase is tidy and rewrites commit hashes. Most teams rebase local feature branches before opening a pull request, then merge the result."},{"question":"Is it safe to rebase a branch that has been pushed?","answer":"Only if you are the only person working on it. Rebasing creates new commits with new hashes, so the remote branch and your local one diverge and git push is rejected until you force it. Use git push --force-with-lease rather than --force: it refuses the push if someone else has added commits you have not seen, which turns silent data loss into an error message. Never rebase a shared branch such as main. Everyone who has pulled it will be stranded on commits that no longer exist."},{"question":"How do I squash multiple commits into one?","answer":"Run git rebase -i HEADgit fetch","level":2},{"id":"why-fetch-is-safe","title":"Why fetch Is Safe","level":3},{"id":"bringing-changes-in-with-git-pull","title":"Bringing Changes In with git pull","level":2},{"id":"sending-changes-out-with-git-push","title":"Sending Changes Out with git push","level":2},{"id":"tracking-branches-and-upstream","title":"Tracking Branches and Upstream","level":2},{"id":"setting-upstream-manually","title":"Setting Upstream Manually","level":2},{"id":"common-remote-workflow","title":"Common Remote Workflow","level":2},{"id":"troubleshooting-common-confusion","title":"Troubleshooting Common Confusion","level":2},{"id":"why-dont-i-see-remote-changes","title":"âWhy Donât I See Remote Changes?â","level":3},{"id":"why-did-git-pull-change-my-files","title":"âWhy Did git pull Change My Files?â","level":3},{"id":"why-does-git-ask-me-for-an-upstream","title":"âWhy Does Git Ask Me for an Upstream?â","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-inspecting-before-integrating","title":"Exercise 1: Inspecting before integrating","level":3},{"id":"exercise-2-first-push-of-a-new-branch","title":"Exercise 2: First push of a new branch","level":3},{"id":"exercise-3-fork-workflow-naming","title":"Exercise 3: Fork workflow naming","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git fetch and git pull?","answer":"git fetch downloads commits and updates your remote-tracking branches such as origin/main, and changes nothing in your working directory. It is always safe to run. git pull is fetch followed immediately by a merge (or rebase) into your current branch, so it does alter your files and can produce conflicts. The useful habit is git fetch then git log origin/main..main to see what diverged before deciding how to integrate it, rather than letting pull decide for you."},{"question":"How do I fix "rejected, non-fast-forward" when pushing?","answer":"The remote branch has commits you do not have locally, so accepting your push would discard them. Run git pull --rebase to replay your work on top of theirs, resolve any conflicts, then push. Do not reach for git push --force, which deletes their commits. If you genuinely must overwrite, after an intentional rebase of your own feature branch, use git push --force-with-lease, which refuses the push if the remote has moved since you last fetched."},{"question":"What does git push -u origin main actually do?","answer":"-u (short for --set-upstream) records origin/main as the upstream of your local main, in addition to pushing. That association is what lets you type bare git push, git pull and git status afterwards and have Git know which remote branch you mean, including the "your branch is ahead by 2 commits" line in git status. You need it once per branch; after that the plain commands work. git branch -vv shows which branches have an upstream set."},{"question":"Why don't I see a colleague's new branch?","answer":"Your local copy of the remote's branch list is only updated when you fetch. Run git fetch (or git fetch --all for every remote) and then git branch -r lists the remote branches you know about. The reverse problem is stale entries for branches that have been deleted upstream, which git fetch --prune clears; git config --global fetch.prune true makes that automatic on every fetch."},{"question":"What is the difference between origin and upstream?","answer":"They are just remote names, not Git concepts. origin is the conventional name for the repository you cloned from, set automatically by git clone. upstream is the convention in fork-based workflows for the original project you forked: origin is your fork and upstream is the source. That is what makes git fetch upstream && git rebase upstream/main the standard way to bring a fork up to date. Add it with git remote add upstream <url>, and git remote -v lists what you have."},{"question":"How do I change a remote's URL, for example from HTTPS to SSH?","answer":"git remote set-url origin git@github.com:user/repo.git, then confirm with git remote -v. This is the usual fix when HTTPS pushes start asking for a password that no longer works. GitHub stopped accepting account passwords for Git operations, so HTTPS needs a personal access token while SSH uses your key. Switching to SSH avoids the credential prompt entirely once your key is added to the account."}]},{"slug":"git-stash","title":"Git Stash","seoTitle":"Git Stash: Save, Apply and Pop Changes (With Examples)","description":"Park work in progress with git stash: apply versus pop, stash untracked files, name and inspect stashes, and decide when a branch is the better tool instead.","content":"$37","difficulty":"Easy","duration":"12 min","keywords":["git stash tutorial","git stash pop apply","git stash untracked","named stash git","git stash list","git stash","git stash pop vs apply","git stash untracked files","recover dropped stash","git stash message","git stash conflict","git stash to another branch","git stash show -p"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":8,"tableOfContents":[{"id":"what-git-stash-is-for","title":"What Git Stash Is For","level":2},{"id":"creating-a-basic-stash","title":"Creating a Basic Stash","level":2},{"id":"listing-stashes","title":"Listing Stashes","level":2},{"id":"restoring-a-stash-with-apply","title":"Restoring a Stash with apply","level":2},{"id":"restoring-and-removing-with-pop","title":"Restoring and Removing with pop","level":2},{"id":"apply-vs-pop","title":"apply vs pop","level":3},{"id":"naming-a-stash","title":"Naming a Stash","level":2},{"id":"stashing-untracked-files","title":"Stashing Untracked Files","level":2},{"id":"dropping-and-clearing-stashes","title":"Dropping and Clearing Stashes","level":2},{"id":"a-real-workflow-example","title":"A Real Workflow Example","level":2},{"id":"handling-stash-conflicts","title":"Handling Stash Conflicts","level":2},{"id":"when-not-to-use-stash","title":"When Not to Use Stash","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-pausing-work-quickly","title":"Exercise 1: Pausing work quickly","level":3},{"id":"exercise-2-saving-untracked-files-too","title":"Exercise 2: Saving untracked files too","level":3},{"id":"exercise-3-keeping-the-stash-after-restore","title":"Exercise 3: Keeping the stash after restore","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git stash apply and git stash pop?","answer":"Both restore the stashed changes to your working directory. pop then deletes the stash entry; apply leaves it in the list. Use apply when you want to put the same work onto more than one branch, or when you are not yet sure the restore will go cleanly, if it conflicts badly you still have the stash to fall back on. Use pop for the ordinary case of picking work back up once. Note that pop does not delete the entry if the restore hits a conflict, which is deliberate."},{"question":"Does git stash include untracked files?","answer":"No, and this surprises people regularly. A plain git stash leaves new files sitting in your working directory, so switching branches carries them along and a "clean" stash turns out not to be. Use git stash -u (or --include-untracked) to include them, and git stash -a (--all) to include ignored files as well. Because -a sweeps up build output and node_modules, it is rarely what you want."},{"question":"How do I see what is in a stash before restoring it?","answer":"git stash list shows the entries with their references. git stash show stash@{0} gives a summary of changed files, and git stash show -p stash@{0} prints the full diff. You can also restore a single file from a stash without applying the whole thing: git checkout stash@{0} -- path/to/file. Naming stashes as you create them, git stash push -m "half-done search filter", makes the list readable a week later, which the default WIP on main messages do not."},{"question":"I dropped a stash by accident. Can I recover it?","answer":"Usually yes. A dropped stash is an unreachable commit, and git fsck --unreachable | grep commit lists candidates you can inspect with git show <sha>. Once you find it, git stash apply <sha> restores the contents. It works until Git garbage-collects, which is typically not for weeks. The wider lesson is that stashes are the least durable place to keep work. A throwaway commit on a scratch branch is recoverable through the reflog and cannot be dropped by a stray git stash clear."},{"question":"How do I apply a stash to a different branch?","answer":"Just switch branches and apply it. Stashes are not tied to the branch they were created on. git switch other-branch then git stash pop is a standard way to move work you started in the wrong place. Conflicts are possible if the branches have diverged in the same files, and git checkout --theirs/--ours plus normal conflict resolution applies. If you expect trouble, use apply rather than pop so the stash survives a failed attempt."},{"question":"When should I use a branch instead of a stash?","answer":"Whenever the work will outlive the next half hour. A stash is an unnamed, unordered pile that is easy to forget and easy to clear, and git stash list after a busy fortnight is nobody's friend. For anything you intend to return to, commit it on a branch, even as a scrappy "wip" commit you later amend or squash. Stash is at its best for exactly what it was designed for: parking a few minutes of work to pull, switch branch for a quick fix, or check something on a clean tree."}]},{"slug":"git-undoing-changes","title":"Undoing Changes in Git","seoTitle":"How to Undo Changes in Git: restore, reset, revert, clean","description":"Undo almost anything in Git: restore files, reset soft, mixed and hard, revert on shared branches, clean untracked files and recover a bad reset with reflog.","content":"$38","difficulty":"Medium","duration":"18 min","keywords":["git undo changes","git restore reset revert","git clean tutorial","soft mixed hard reset","undo git commit","git undo last commit","git reset vs revert","git reset --hard","git reset soft mixed hard","git restore file","git revert commit","git clean untracked files","git reflog recover","undo git commit keep changes","git discard local changes"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":9,"tableOfContents":[{"id":"why-undoing-changes-feels-confusing","title":"Why Undoing Changes Feels Confusing","level":2},{"id":"git-restore-undo-file-level-changes","title":"git restore: Undo File-Level Changes","level":2},{"id":"discard-unstaged-changes-in-a-file","title":"Discard unstaged changes in a file","level":3},{"id":"unstage-a-file","title":"Unstage a file","level":3},{"id":"practical-example","title":"Practical Example","level":3},{"id":"git-reset-move-head-and-possibly-more","title":"git reset: Move HEAD and Possibly More","level":2},{"id":"soft-reset","title":"Soft Reset","level":3},{"id":"mixed-reset","title":"Mixed Reset","level":3},{"id":"hard-reset","title":"Hard Reset","level":3},{"id":"git-revert-safely-undo-a-commit-in-shared-history","title":"git revert: Safely Undo a Commit in Shared History","level":2},{"id":"why-revert-is-safe-for-teams","title":"Why Revert Is Safe for Teams","level":3},{"id":"git-clean-remove-untracked-files","title":"git clean: Remove Untracked Files","level":2},{"id":"how-the-commands-differ","title":"How the Commands Differ","level":2},{"id":"real-world-scenarios","title":"Real-World Scenarios","level":2},{"id":"scenario-1-you-staged-the-wrong-file","title":"Scenario 1: You staged the wrong file","level":3},{"id":"scenario-2-your-last-local-commit-should-be-redone","title":"Scenario 2: Your last local commit should be redone","level":3},{"id":"scenario-3-a-bad-commit-is-already-on-main","title":"Scenario 3: A bad commit is already on main","level":3},{"id":"scenario-4-build-output-files-are-cluttering-the-repo","title":"Scenario 4: Build output files are cluttering the repo","level":3},{"id":"a-good-safety-habit","title":"A Good Safety Habit","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-unstaging-without-losing-edits","title":"Exercise 1: Unstaging without losing edits","level":3},{"id":"exercise-2-redoing-the-last-local-commit","title":"Exercise 2: Redoing the last local commit","level":3},{"id":"exercise-3-undoing-shared-history-safely","title":"Exercise 3: Undoing shared history safely","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between git reset and git revert?","answer":"git reset moves the branch pointer backwards, rewriting history so the commits are no longer on the branch. git revert leaves history intact and creates a new commit that applies the inverse of an old one. The distinction is about who else has the commits: reset is right for local work nobody has pulled, and revert is the only safe choice on a shared branch, because resetting a pushed branch strands everyone who already fetched those commits. On main, always revert."},{"question":"How do I undo my last commit but keep the changes?","answer":"git reset --soft HEAD1 moves the branch back one commit and leaves everything staged, ready to recommit, which is what you want when you simply mis-worded the message or forgot a file. git reset HEAD1 (mixed, the default) does the same but unstages the changes, leaving them in your working directory. Use git commit --amend instead if you only want to fix the message or add a file to the commit that is already there, since it avoids the reset entirely."},{"question":"What is the difference between git reset --soft, --mixed and --hard?","answer":"All three move HEAD; they differ in how much they touch. --soft moves the pointer only, leaving your staging area and files exactly as they were, so the undone changes are staged. --mixed, the default, also resets the staging area, so the changes sit unstaged in your working directory. --hard resets the working directory too, discarding the changes completely. Only --hard destroys work, which is why it is the one to pause over."},{"question":"I ran git reset --hard and lost my work. Can I get it back?","answer":"Committed work, yes. git reflog lists every position HEAD has held, including the commit you reset away from, and git reset --hard HEAD@{1} puts you back. Reflog entries persist for about 90 days by default, so recovery is realistic long after the mistake. Work that was never committed is genuinely gone, --hard overwrites the working directory and there is no record of it. That asymmetry is the strongest argument for committing early and often, even on messy work-in-progress."},{"question":"How do I discard changes to a single file?","answer":"git restore <file> discards unstaged changes in that file, and git restore --staged <file> unstages it while keeping the edits. Those two replace the overloaded git checkout -- <file> and git reset HEAD <file> forms, which did the same jobs less clearly. To do both, unstage and discard, use git restore --staged --worktree <file>. Discarding is not recoverable, since the changes were never recorded anywhere, so check git diff before running it."},{"question":"How do I remove untracked files and directories?","answer":"git clean -fd deletes untracked files and directories, with -f required because Git refuses to do something this destructive without it, and -d extending it to directories. Always run git clean -nd first: -n is a dry run that lists exactly what would be removed. By default .gitignored files are left alone, and -x includes them. Useful for wiping build output and node_modules, and a fast way to delete a local .env you meant to keep."}]},{"slug":"git-tagging","title":"Git Tagging","seoTitle":"Git Tags: Lightweight vs Annotated, Push and Delete Tags","description":"Mark releases with Git tags: lightweight versus annotated, why push ignores tags, deleting them safely, tagging older commits and semantic-versioned releases.","content":"$39","difficulty":"Easy","duration":"10 min","keywords":["git tags tutorial","annotated vs lightweight tags","push tags git","delete git tag","checkout tag git","git tag","annotated vs lightweight tag","git push tags","git push --follow-tags","delete git tag remote","git tag old commit","checkout git tag","semantic versioning git tags","git describe","git release tag"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":10,"tableOfContents":[{"id":"what-a-tag-is","title":"What a Tag Is","level":2},{"id":"lightweight-vs-annotated-tags","title":"Lightweight vs Annotated Tags","level":2},{"id":"lightweight-tag","title":"Lightweight Tag","level":3},{"id":"annotated-tag","title":"Annotated Tag","level":3},{"id":"listing-tags","title":"Listing Tags","level":2},{"id":"tagging-older-commits","title":"Tagging Older Commits","level":2},{"id":"pushing-tags-to-a-remote","title":"Pushing Tags to a Remote","level":2},{"id":"deleting-tags","title":"Deleting Tags","level":2},{"id":"checking-out-a-tag","title":"Checking Out a Tag","level":2},{"id":"release-example","title":"Release Example","level":2},{"id":"when-to-use-each-tag-type","title":"When to Use Each Tag Type","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-picking-the-right-release-marker","title":"Exercise 1: Picking the right release marker","level":3},{"id":"exercise-2-publishing-the-release-tag","title":"Exercise 2: Publishing the release tag","level":3},{"id":"exercise-3-working-from-a-tag","title":"Exercise 3: Working from a tag","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between a lightweight and an annotated Git tag?","answer":"A lightweight tag is just a name pointing at a commit. Nothing else is stored. An annotated tag is a real Git object with a tagger, a date, a message and optionally a GPG signature. That difference matters in practice: git describe only considers annotated tags by default, and you cannot tell who created a lightweight tag or why. Use annotated tags (git tag -a v1.2.0 -m "...") for anything released, and lightweight tags for private bookmarks."},{"question":"Why don't my tags appear on GitHub after pushing?","answer":"Because git push does not include tags. Push one with git push origin v1.2.0, or all of them with git push origin --tags. If you want annotated tags pushed along with commits automatically, git push --follow-tags sends the annotated tags that are reachable from the commits being pushed, which is usually the behaviour people expect, and is worth setting as a habit for release work."},{"question":"How do I delete a Git tag locally and remotely?","answer":"git tag -d v1.2.0 removes it locally, and git push origin --delete v1.2.0 removes it from the remote. Be careful with released tags: anyone who has already fetched keeps their copy, and if your release tooling or a package registry resolved that tag, deleting it can break builds that referenced it. The safer convention for a bad release is to publish a new tag rather than to remove the old one."},{"question":"Can I tag a commit from the past?","answer":"Yes. Pass the commit hash: git tag -a v1.0.0 9fceb02 -m "Initial release". The tag's own date is when you created it, not the commit's date, which occasionally matters for tooling that sorts by tag date. This is the usual way to add a version tag retrospectively when a project started without them, and git log --oneline gives you the hashes to work from."},{"question":"What happens when I check out a tag?","answer":"You get a detached HEAD, because a tag points at a fixed commit rather than a moving branch. That is fine for inspecting or building a release. If you need to make changes, a hotfix on an old version, create a branch from it: git switch -c hotfix/1.2.1 v1.2.0. Committing while detached produces commits that belong to no branch and become unreachable once you switch away."},{"question":"How do tags relate to releases and semantic versioning?","answer":"A tag is the Git primitive; a "release" on GitHub or GitLab is a layer on top of a tag, adding notes and downloadable artefacts. The convention is a v prefix and semantic versioning, v1.4.2, where the major number changes on a breaking change, the minor on a backwards-compatible feature, and the patch on a fix. Most release automation, including semantic-release, works by reading commits since the last tag and creating the next one, so consistent tagging is what makes the automation possible."}]},{"slug":"git-github-workflow","title":"GitHub Workflow with Git","seoTitle":"GitHub Pull Request Workflow: Fork, Branch, Review, Merge","description":"Follow the GitHub pull request workflow end to end: fork versus clone, keeping a branch current, squash versus merge versus rebase, and reviewing well.","content":"$3a","difficulty":"Medium","duration":"20 min","keywords":["github workflow tutorial","fork clone pull request","feature branch workflow","code review github","squash and merge","github pull request workflow","fork vs clone","keep branch up to date with main","code review best practices","pull request size","git push force-with-lease","rebase vs merge pull request","open source contribution workflow"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":11,"tableOfContents":[{"id":"why-a-workflow-matters","title":"Why a Workflow Matters","level":2},{"id":"forking-vs-cloning","title":"Forking vs Cloning","level":2},{"id":"creating-a-feature-branch","title":"Creating a Feature Branch","level":2},{"id":"making-changes-and-committing-clearly","title":"Making Changes and Committing Clearly","level":2},{"id":"good-commit-practice-in-prs","title":"Good Commit Practice in PRs","level":3},{"id":"pushing-the-branch","title":"Pushing the Branch","level":2},{"id":"opening-a-pull-request","title":"Opening a Pull Request","level":2},{"id":"code-review-best-practices","title":"Code Review Best Practices","level":2},{"id":"keeping-your-branch-updated","title":"Keeping Your Branch Updated","level":2},{"id":"squash-and-merge","title":"Squash and Merge","level":2},{"id":"example-outcome","title":"Example Outcome","level":3},{"id":"a-complete-example-workflow","title":"A Complete Example Workflow","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-keeping-your-fork-current","title":"Exercise 1: Keeping your fork current","level":3},{"id":"exercise-2-protecting-main-with-feature-branches","title":"Exercise 2: Protecting main with feature branches","level":3},{"id":"exercise-3-understanding-squash-and-merge","title":"Exercise 3: Understanding squash and merge","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between forking and cloning?","answer":"Cloning copies a repository to your machine; forking copies it to your own account on the hosting platform, and you then clone your fork. You clone when you have write access to the original. The normal case inside a company. You fork when you do not, which is how open-source contribution works: you push branches to your fork and open a pull request across repositories. In a fork workflow, origin is your fork and upstream is the original project."},{"question":"How do I keep my branch up to date with main?","answer":"git fetch origin then either git rebase origin/main for a linear history or git merge origin/main to keep a merge commit. Rebase is the usual choice on your own feature branch before review, since it makes the pull request read as though written against current code. Remember it needs git push --force-with-lease afterwards, because the hashes change. Merge is the right choice if others are working on the same branch, as rebasing shared work strands them."},{"question":"What is the difference between squash, merge and rebase merge on a pull request?","answer":"Squash merge collapses every commit in the branch into one on main. The tidiest history and the easiest to revert, at the cost of losing the individual commits. Merge commit keeps all commits and adds one joining them, preserving the full record and showing the branch in the graph. Rebase merge replays each commit onto main individually, giving a linear history that keeps them all but rewrites their hashes. Pick one, set it in repository settings, and stop thinking about it."},{"question":"How big should a pull request be?","answer":"Small enough to review properly in one sitting. A few hundred changed lines is a reasonable ceiling. Review quality falls off sharply beyond that, and a large pull request tends to receive approval rather than scrutiny, which is the opposite of the point. If a change is genuinely large, split it: a mechanical refactor as one pull request, the behaviour change as another. Separating a rename or reformat from real logic changes is the single most effective thing you can do for reviewability."},{"question":"Should I force-push to a pull request branch?","answer":"It is fine and often necessary, after a rebase onto main, or after squashing review fixes, provided you use git push --force-with-lease, which refuses if the remote has commits you have not fetched. What it costs is review continuity: a force-push can detach existing review comments from the lines they referred to. A common compromise is to add plain commits during review so reviewers can follow the changes, then squash on merge."},{"question":"What should I actually look for when reviewing code?","answer":"Correctness first, then whether the change is understandable without the author present, then tests covering the behaviour rather than the implementation. Leave formatting to a linter. A review that spends its attention on spacing has spent it on the thing a tool should have handled. Be specific and direct about problems, ask rather than assert when you may have missed context, and distinguish blocking concerns from suggestions so the author knows what actually needs to change."}]},{"slug":"git-advanced","title":"Advanced Git","seoTitle":"Advanced Git: cherry-pick, bisect, reflog and blame","description":"Learn advanced Git: cherry-pick a commit, find a regression with git bisect, recover lost work with reflog and trace changes with blame, in learning order.","content":"$3b","difficulty":"Hard","duration":"25 min","keywords":["advanced git tutorial","git cherry pick bisect reflog","git submodules hooks aliases","hard git concepts","git power user tips","git cherry-pick","git bisect","git reflog","git blame","git recover deleted commit","git bisect run","git log -S pickaxe","cherry-pick vs rebase","git reflog recover branch","advanced git commands"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":12,"tableOfContents":[{"id":"why-learn-advanced-git","title":"Why Learn Advanced Git?","level":2},{"id":"cherry-pick","title":"Cherry-Pick","level":2},{"id":"when-to-use-it","title":"When to Use It","level":3},{"id":"bisect","title":"Bisect","level":2},{"id":"why-it-matters","title":"Why It Matters","level":3},{"id":"reflog","title":"Reflog","level":2},{"id":"submodules-intro","title":"Submodules Intro","level":2},{"id":"hooks-intro","title":"Hooks Intro","level":2},{"id":"aliases","title":"Aliases","level":2},{"id":"putting-advanced-git-to-work","title":"Putting Advanced Git to Work","level":2},{"id":"when-to-reach-for-these-tools","title":"When to Reach for These Tools","level":2},{"id":"use-cherry-pick-when","title":"Use cherry-pick when","level":3},{"id":"use-bisect-when","title":"Use bisect when","level":3},{"id":"use-reflog-when","title":"Use reflog when","level":3},{"id":"use-submodules-when","title":"Use submodules when","level":3},{"id":"use-hooks-when","title":"Use hooks when","level":3},{"id":"use-aliases-when","title":"Use aliases when","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"exercise-1-backporting-one-fix","title":"Exercise 1: Backporting one fix","level":3},{"id":"exercise-2-finding-the-bad-commit-quickly","title":"Exercise 2: Finding the bad commit quickly","level":3},{"id":"exercise-3-recovering-after-a-mistake","title":"Exercise 3: Recovering after a mistake","level":3},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What does git cherry-pick do?","answer":"It applies the changes from one commit onto your current branch as a new commit with a new hash. The classic use is a hotfix: the fix is committed on main and you need the same change on a release branch without bringing everything else along. git cherry-pick <sha> does it; -x appends a line recording the original hash, which is worth using so the duplication is traceable. Cherry-picking the same change into two branches that later merge can produce a conflict, which is the main reason to prefer merging where you can."},{"question":"How does git bisect find a bug?","answer":"By binary search through history. You tell it a commit where the bug exists (git bisect bad) and one where it does not (git bisect good), and it checks out the midpoint for you to test, narrowing by half each round, around ten steps for a thousand commits. Mark each as good or bad and it names the first bad commit. git bisect run ./test.sh automates it entirely when you have a script that exits non-zero on failure, which turns an afternoon of guessing into a couple of minutes."},{"question":"What is git reflog and when does it save you?","answer":"The reflog records every position HEAD has held in your local repository, including states no branch points at any more. That makes it the recovery tool for a bad reset --hard, a rebase that went wrong, a deleted branch, or a commit you amended away: git reflog lists the history, then git reset --hard HEAD@{5} or git switch -c recovered <sha> brings it back. Entries last about 90 days by default. It is local only, which is why committing is the real safety net."},{"question":"What is the difference between git cherry-pick and git rebase?","answer":"Rebase replays a whole series of commits onto a new base; cherry-pick applies one or a few chosen commits. Under the surface they do much the same work, rebase is essentially a scripted sequence of cherry-picks, so the choice is about scope. Rebase for "move my branch on top of the latest main", cherry-pick for "I need just that one fix over here"."},{"question":"How do I find which commit introduced a specific line?","answer":"git blame <file> annotates every line with the commit, author and date that last changed it, and git blame -L 40,60 <file> limits it to a range. When blame only shows a formatting commit, git blame -w --ignore-rev <sha> skips whitespace changes and a specified reformat. For searching by content rather than location, git log -S "functionName" finds commits where the number of occurrences of that string changed, which is the fastest way to locate when something was added or deleted."},{"question":"Which advanced Git commands are actually worth learning first?","answer":"Three, in this order. git reflog, because it turns most disasters into inconveniences and takes five minutes to understand. git bisect, because nothing else finds a regression in a long history nearly as fast. And git log -S, because "when did this string appear" is a question you ask constantly. Cherry-pick and interactive rebase are valuable but situational, and submodules are best avoided until you have a specific need for them."}]},{"slug":"git-hooks","title":"Git Hooks","seoTitle":"Git Hooks: pre-commit, commit-msg and Husky Setup Guide","description":"Automate checks at commit and push time with Git hooks: where they live, why they never reach your team, client versus server hooks, --no-verify and Husky.","content":"$3c","difficulty":"Medium","duration":"20 min","keywords":["git hooks tutorial","pre-commit hook","git husky","commit-msg hook","git hooks","husky git hooks","git hooks not working","server-side git hooks","git commit --no-verify","lint-staged","core.hooksPath","share git hooks with team"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":13,"tableOfContents":[{"id":"what-are-git-hooks","title":"What Are Git Hooks?","level":2},{"id":"client-side-vs-server-side-hooks","title":"Client-Side vs Server-Side Hooks","level":2},{"id":"client-side-hooks","title":"Client-Side Hooks","level":3},{"id":"server-side-hooks","title":"Server-Side Hooks","level":3},{"id":"three-hooks-you-will-use-often","title":"Three Hooks You Will Use Often","level":2},{"id":"pre-commit","title":"pre-commit","level":3},{"id":"commit-msg","title":"commit-msg","level":3},{"id":"pre-push","title":"pre-push","level":3},{"id":"where-hooks-live","title":"Where Hooks Live","level":2},{"id":"installing-hooks-manually","title":"Installing Hooks Manually","level":2},{"id":"the-main-limitation-of-raw-hooks","title":"The Main Limitation of Raw Hooks","level":2},{"id":"using-husky-for-team-wide-hooks","title":"Using Husky for Team-Wide Hooks","level":2},{"id":"best-practices","title":"Best Practices","level":2},{"id":"keep-hooks-fast","title":"Keep Hooks Fast","level":3},{"id":"pair-hooks-with-ci","title":"Pair Hooks with CI","level":3},{"id":"write-useful-errors","title":"Write Useful Errors","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What are Git hooks and where do they live?","answer":"Hooks are executable scripts Git runs automatically at points in its workflow. Before a commit is created, after a message is written, before a push. They live in .git/hooks, which ships with .sample files you can rename to activate. The crucial consequence of that location is that .git is not version-controlled, so a hook you write is yours alone: it does not travel with a clone and nobody else on the team gets it."},{"question":"Why don't my Git hooks work for the rest of the team?","answer":"Because .git/hooks is outside the repository's tracked content, so hooks are never committed, pushed or cloned. Every developer would have to create them by hand, and nothing keeps them in sync. The standard fix is a manager such as Husky or pre-commit, which stores hook definitions in tracked files and points Git's core.hooksPath at a committed directory during install, so a fresh clone plus npm install sets everything up."},{"question":"What is the difference between client-side and server-side hooks?","answer":"Client-side hooks run on a developer's machine, pre-commit, commit-msg, pre-push, and are about fast feedback: catching a lint error in two seconds rather than two minutes of CI. Server-side hooks such as pre-receive and update run on the remote when a push arrives, and are about enforcement, because they cannot be skipped. Hosted platforms generally do not expose server-side hooks; branch protection rules and required status checks fill that role instead."},{"question":"Can Git hooks be bypassed?","answer":"Yes, trivially, git commit --no-verify skips pre-commit and commit-msg, and git push --no-verify skips pre-push. That is a feature, since you sometimes need to commit through a broken hook, but it means client-side hooks are a convenience and not a control. Anything that genuinely must hold has to be checked where the developer cannot reach it: a required CI check on a pull request, or a server-side hook on self-hosted infrastructure."},{"question":"What should I put in a pre-commit hook?","answer":"Only fast checks, and preferably only on the files being committed. Formatting and linting staged files is the classic use, lint-staged exists specifically for this, plus a scan for accidentally committed secrets. Keep it under a couple of seconds: a pre-commit hook that runs the full test suite gets disabled by the team within a week, and rightly so. Put the slow, comprehensive checks in CI, where they belong."},{"question":"How do I set up Husky for a JavaScript project?","answer":"Install it as a dev dependency, run npx husky init, and it creates a .husky directory and a prepare script so the hooks install automatically after npm install. Add a hook by writing the command into .husky/pre-commit, commonly npx lint-staged, and commit that file. Because .husky is tracked, everyone gets the same hooks on their next install, which is the whole problem raw .git/hooks cannot solve."}]},{"slug":"git-workflows","title":"Git Workflows","seoTitle":"Git Workflows: GitFlow vs GitHub Flow vs Trunk-Based","description":"Compare GitFlow, GitHub Flow and trunk-based development honestly, see which suits which team, why long-lived branches hurt and how feature flags help.","content":"$3d","difficulty":"Medium","duration":"22 min","keywords":["git workflow tutorial","gitflow tutorial","trunk based development","github flow","git workflows","gitflow vs github flow","feature branch workflow","git branching strategy","feature flags","branch protection rules","is gitflow still recommended","git workflow for small teams","long lived feature branch"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":14,"tableOfContents":[{"id":"why-git-workflows-matter","title":"Why Git Workflows Matter","level":2},{"id":"gitflow","title":"GitFlow","level":2},{"id":"how-gitflow-works","title":"How GitFlow Works","level":3},{"id":"when-gitflow-fits","title":"When GitFlow Fits","level":3},{"id":"github-flow","title":"GitHub Flow","level":2},{"id":"when-github-flow-fits","title":"When GitHub Flow Fits","level":3},{"id":"trunk-based-development","title":"Trunk-Based Development","level":2},{"id":"feature-flags-in-trunk-based-development","title":"Feature Flags in Trunk-Based Development","level":3},{"id":"when-trunk-based-development-fits","title":"When Trunk-Based Development Fits","level":3},{"id":"choosing-the-right-workflow","title":"Choosing the Right Workflow","level":2},{"id":"small-team","title":"Small Team","level":3},{"id":"enterprise-team","title":"Enterprise Team","level":3},{"id":"cicd-heavy-team","title":"CI/CD-Heavy Team","level":3},{"id":"quick-comparison","title":"Quick Comparison","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is the difference between GitFlow, GitHub Flow and trunk-based development?","answer":"They differ in how many long-lived branches exist. GitFlow keeps main, develop, plus release and hotfix branches. Heavy, but it models versioned software with several supported releases. GitHub Flow keeps only main plus short-lived feature branches, each merged via pull request and deployed on merge. Trunk-based development goes furthest: everyone commits to main at least daily, with incomplete work hidden behind feature flags rather than branches. The trade is branch isolation against integration pain. The longer a branch lives, the worse its merge."},{"question":"Which Git workflow should a small team use?","answer":"GitHub Flow, in almost every case. One protected main, a short-lived branch per change, a pull request for review, deploy on merge. It is enough process to keep main releasable and little enough that nobody spends time on branch bookkeeping. GitFlow's develop and release branches only pay off when you genuinely maintain multiple versions in production simultaneously, and adopting it without that need is the most common case of a workflow costing more than it returns."},{"question":"Is GitFlow still recommended?","answer":"Only for software with real release management. Desktop applications, on-premise products, anything where several versions are supported at once and hotfixes must land on old releases. Its author has said as much: for continuously deployed web services the extra branches add ceremony without benefit. If you deploy from main several times a week, GitHub Flow or trunk-based development fits the reality better, and the release branch you are maintaining is usually just a delay."},{"question":"What are feature flags and why does trunk-based development need them?","answer":"A feature flag is a runtime switch that decides whether new code paths are active, letting you merge unfinished work into main safely because it is simply off. Trunk-based development depends on them: if everyone integrates daily, there is no long branch in which to hide an incomplete feature, so the hiding has to move into the code. The cost is real, flags accumulate, and each one doubles the possible states, so removing them once a feature ships has to be part of the process."},{"question":"How long should a feature branch live?","answer":"A day or two; a week at the outside. Merge difficulty grows with the square of the time, roughly, because both your branch and main keep moving and the overlap compounds. If a change cannot be finished in that window, split it into separately shippable pieces, a refactor, then the behaviour behind a flag, rather than letting the branch run. "Long-lived feature branch" is close to a synonym for "painful merge"."},{"question":"How do branch protection rules fit into a workflow?","answer":"They are what makes the workflow actually happen rather than being a document people mean to follow. On main, typical rules require a pull request rather than a direct push, at least one approving review, passing status checks, and an up-to-date branch before merge. Setting the merge strategy, squash, merge commit or rebase, at the repository level is worth doing too, since leaving it per-developer produces a history with all three. Enforce it centrally; local hooks can be bypassed with --no-verify."}]},{"slug":"git-aliases-config","title":"Git Aliases and Configuration","seoTitle":"Git Config and Aliases: .gitconfig Scopes and Defaults","description":"Configure Git once and stop fighting it: .gitconfig scopes, per-directory identities with includeIf, handy aliases and the defaults worth changing.","content":"$3e","difficulty":"Easy","duration":"12 min","keywords":["git config tutorial","git aliases","gitconfig setup","git global ignore","git config","gitconfig scopes","global gitignore","git config user.email per repository","git includeIf","init.defaultBranch main","pull.rebase true","core.excludesfile","git config --list --show-origin"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":15,"tableOfContents":[{"id":"why-git-configuration-matters","title":"Why Git Configuration Matters","level":2},{"id":"understanding-gitconfig-scopes","title":"Understanding .gitconfig Scopes","level":2},{"id":"setting-name-and-email","title":"Setting Name and Email","level":2},{"id":"default-editor-and-default-branch","title":"Default Editor and Default Branch","level":2},{"id":"reading-a-gitconfig-file","title":"Reading a .gitconfig File","level":2},{"id":"useful-git-aliases","title":"Useful Git Aliases","level":2},{"id":"pretty-log-alias","title":"Pretty Log Alias","level":3},{"id":"workflow-aliases","title":"Workflow Aliases","level":3},{"id":"using-a-global-ignore-file","title":"Using a Global Ignore File","level":2},{"id":"a-simple-starter-setup","title":"A Simple Starter Setup","level":2},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"Where is my .gitconfig file and what are the config scopes?","answer":"There are three, each overriding the one above. System config applies to every user on the machine (/etc/gitconfig, edited with --system). Global config is yours (~/.gitconfig, edited with --global) and is where nearly everything belongs. Local config belongs to one repository (.git/config, the default with no flag) and is what you use to override your identity for a single project. git config --list --show-origin prints every setting alongside the file it came from, which settles most "why is this value set" questions."},{"question":"How do I use a different email for work and personal repositories?","answer":"Set the work identity per repository with git config user.email you@company.com, which writes to that repository's .git/config and overrides your global value. For more than a couple of repositories, use conditional includes: in /.gitconfig, an [includeIf "gitdir:/work/"] section pointing at a second config file applies automatically to anything under that directory. That way a new clone in the right folder gets the right identity without you remembering to set it."},{"question":"How do I create a Git alias?","answer":"git config --global alias.<name> '<command>', so git config --global alias.st status gives you git st. Aliases can chain flags, as with a pretty log: alias.lg "log --oneline --graph --decorate --all". Prefixing the value with ! runs a shell command rather than a Git subcommand, which lets an alias combine several steps. Aliases live in your config, so they are yours alone and do not travel with the repository."},{"question":"Which Git aliases are actually worth setting up?","answer":"Start with the ones you type dozens of times a day, st for status, co for checkout or switch, br for branch, ci for commit. Then add the ones whose full form you would never type: a graph log, last for log -1 HEAD, and unstage for restore --staged. Do not alias destructive commands to short names; a two-character alias for reset --hard is an accident waiting to happen."},{"question":"What is a global gitignore and why use one?","answer":"A .gitignore in the repository describes what that project should not track, and belongs in version control. A global ignore file, set with git config --global core.excludesfile /.gitignoreglobal, covers things specific to you: .DSStore, editor directories such as .idea/ and .vscode/, and local scratch files. The distinction matters as courtesy: your colleagues on other systems should not have to carry ignore rules for your operating system and editor."},{"question":"What Git settings should everyone change from the defaults?","answer":"Four are worth setting immediately. init.defaultBranch main, so new repositories match what hosting platforms expect. pull.rebase true, which stops a plain git pull littering history with merge commits. fetch.prune true, so branches deleted upstream disappear from your local list. And core.editor set to something you can exit, which spares you meeting Vim unexpectedly. Adding rerere.enabled true pays for itself the first time you rebase through the same conflict twice."}]},{"slug":"git-submodules","title":"Git Submodules","seoTitle":"Git Submodules Tutorial: Add, Clone and Update Correctly","description":"Pin one repository inside another with Git submodules: clone with --recurse-submodules, know the two update commands, handle detached HEAD and subtrees.","content":"$3f","difficulty":"Hard","duration":"25 min","keywords":["git submodules tutorial","git subtree vs submodule","git submodules","git submodule update","clone with submodules","git submodule empty directory","git submodule detached HEAD","submodule vs subtree","remove git submodule","gitmodules file","submodule.recurse","git recurse-submodules"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":16,"tableOfContents":[{"id":"what-are-git-submodules","title":"What Are Git Submodules?","level":2},{"id":"when-to-use-submodules","title":"When to Use Submodules","level":2},{"id":"adding-a-submodule","title":"Adding a Submodule","level":2},{"id":"cloning-with-submodules","title":"Cloning with Submodules","level":2},{"id":"updating-submodules","title":"Updating Submodules","level":2},{"id":"syncing-to-the-parent-repositorys-expected-version","title":"Syncing to the Parent Repository's Expected Version","level":3},{"id":"moving-the-submodule-to-a-new-commit","title":"Moving the Submodule to a New Commit","level":3},{"id":"helpful-commands","title":"Helpful Commands","level":2},{"id":"removing-a-submodule","title":"Removing a Submodule","level":2},{"id":"common-pitfalls","title":"Common Pitfalls","level":2},{"id":"forgetting---recurse-submodules","title":"Forgetting n and marking them reword does the same job with the same caveat. On main, the honest answer is to leave it."},{"question":"How do I enforce commit message rules across a team?","answer":"Use commitlint with a commit-msg hook, installed through Husky so the hook lives in the repository and arrives with a clone. Raw .git/hooks files are not version-controlled and so never reach anyone else. That rejects a badly formatted message before the commit is created. Back it up with a server-side check in CI, since local hooks can be skipped with --no-verify; a pull request check that validates the commits is what actually makes the convention stick."}]}],"category":"git","errorPages":[{"slug":"git-local-changes-would-be-overwritten","title":"error: Your local changes to the following files would be overwritten by merge","description":"Fix "Your local changes would be overwritten by merge": run git stash, pull, then git stash pop. Or commit first, or discard with git restore. Pick by intent."},{"slug":"git-authentication-failed-https","title":"fatal: Authentication failed for 'https://github.com/'","description":"Fix Git "Authentication failed" on GitHub: use a personal access token instead of a password, clear the stale keychain entry, or switch the remote to SSH."},{"slug":"git-not-a-git-repository","title":"fatal: not a git repository (or any of the parent directories): .git","description":"Fix "fatal: not a git repository" fast: check pwd, run git rev-parse --show-toplevel, then git init or re-clone. Covers safe.directory, submodules and Docker."},{"slug":"git-merge-conflict","title":"CONFLICT (content): Merge conflict in file","description":"Fix a Git merge conflict: run git status, edit the conflict markers, git add the file, then git commit. Use git merge --abort to start over, or checkout --ours."}],"children":"$L41"}]
41:["$","div",null,{"className":"max-w-none","children":"$L42"}]
42:[["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-a-branch-really-is","children":"What a Branch Really Is"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"branch"}]," in Git is a movable pointer to a commit. Instead of copying your whole project into a new folder, Git lets you create a lightweight branch and continue work there."]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"This is one of Gitâs biggest strengths. You can build a feature, fix a bug, or test an idea without disturbing the main line of development."}],"\n",["$","blockquote",null,{"className":"border-l-4 border-teal-500 bg-teal-50 pl-4 sm:pl-6 py-2 sm:py-3 my-4 italic text-slate-700 text-sm sm:text-base","children":["\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Tip:"}]," A branch is not a second copy of the project. It is a label that points to a specific place in commit history."]}],"\n"]}],"\n",["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"why-branching-matters","children":"Why Branching Matters"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Imagine the ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," branch contains stable application code. You want to add a login page, but the work may take several commits. Instead of changing ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," directly, you create a feature branch."]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Benefits of branching:"}],"\n",["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"isolate work in progress"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"reduce risk to stable code"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"make code reviews easier"}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":"support multiple parallel changes"}],"\n"]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"In real teams, short-lived feature branches are common because they keep the shared branch clean."}],"\n",["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"viewing-branches","children":"Viewing Branches"}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"To list local branches:"}],"\n",["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git branch\n"}]}],"\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["The current branch is marked with ","$L43","."]}],"\n","$L44","\n","$L45","\n","$L46","\n","$L47","\n","$L48","\n","$L49","\n","$L4a","\n","$L4b","\n","$L4c","\n","$L4d","\n","$L4e","\n","$L4f","\n","$L50","\n","$L51","\n","$L52","\n","$L53","\n","$L54","\n","$L55","\n","$L56","\n","$L57","\n","$L58","\n","$L59","\n","$L5a","\n","$L5b","\n","$L5c","\n","$L5d","\n","$L5e","\n","$L5f","\n","$L60","\n","$L61","\n","$L62","\n","$L63","\n","$L64","\n","$L65","\n","$L66","\n","$L67","\n","$L68","\n","$L69","\n","$L6a","\n","$L6b","\n","$L6c","\n","$L6d","\n","$L6e","\n","$L6f","\n","$L70","\n","$L71","\n","$L72","\n","$L73","\n","$L74","\n","$L75","\n","$L76","\n","$L77","\n","$L78","\n","$L79","\n","$L7a","\n","$L7b","\n","$L7c","\n","$L7d","\n","$L7e","\n","$L7f","\n","$L80","\n","$L81","\n","$L82","\n","$L83","\n","$L84","\n","$L85","\n","$L86","\n","$L87","\n","$L88","\n","$L89","\n","$L8a","\n","$L8b","\n","$L8c","\n","$L8d","\n","$L8e","\n","$L8f","\n","$L90","\n","$L91","\n","$L92","\n","$L93","\n","$L94","\n","$L95"]
96:I[18308,["8500","static/chunks/8500-98e13bcce54aa7a0.js","3839","static/chunks/3839-ab8b319d7c7091ea.js","5899","static/chunks/5899-9fb98f224ca30c06.js","2619","static/chunks/app/tutorials/git/%5Bslug%5D/page-886e054855747ff0.js"],"default"]
43:["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":""}]
44:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"To see both local and remote branches:"}]
45:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git branch -a\n"}]}]
46:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A typical result might be:"}]
47:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":" main\n feature/login\n remotes/origin/main\n"}]}]
48:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"creating-a-branch","children":"Creating a Branch"}]
49:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Create a new branch called ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/login"}],":"]}]
4a:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git branch feature/login\n"}]}]
4b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"This creates the branch, but you stay on your current branch until you switch."}]
4c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"To create and switch in one step, use:"}]
4d:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch -c feature/login\n"}]}]
4e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Older tutorials may use ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git checkout -b feature/login"}],", which still works, but ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch"}]," is clearer for branch changes."]}]
4f:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"switching-between-branches","children":"Switching Between Branches"}]
50:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Move to an existing branch:"}]
51:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch main\n"}]}]
52:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Switch back:"}]
53:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch feature/login\n"}]}]
54:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"When you switch branches, Git updates your working directory to match the selected branchâs latest commit."}]
55:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"real-example","children":"Real Example"}]
56:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch -c feature/navbar\necho "Add navigation" >> README.md\ngit add README.md\ngit commit -m "Document navbar plan"\ngit switch main\n"}]}]
57:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["After switching back to ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}],", that change is no longer visible there because it belongs to the feature branch."]}]
58:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"understanding-head","children":"Understanding HEAD"}]
59:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"HEAD"}]," is Gitâs way of saying âyour current position.â Most of the time, HEAD points to the branch you are currently on."]}]
5a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["If you are on ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}],", HEAD points to ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}],", and ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," points to the latest commit on that branch."]}]
5b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"You can inspect recent history with:"}]
5c:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git log --oneline --decorate -5\n"}]}]
5d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"You may see output like:"}]
5e:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"7fa12cd (HEAD -> feature/login) Add login form markup\n1ab93e0 (main) Create app skeleton\n"}]}]
5f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["That means HEAD currently points to ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/login"}],"."]}]
60:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"deleting-a-branch","children":"Deleting a Branch"}]
61:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"After a branch has been merged and is no longer needed, delete it to keep the repository tidy."}]
62:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Delete a fully merged branch safely:"}]
63:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git branch -d feature/login\n"}]}]
64:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Force delete a branch that has unmerged commits:"}]
65:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git branch -D feature/login\n"}]}]
66:["$","blockquote",null,{"className":"border-l-4 border-teal-500 bg-teal-50 pl-4 sm:pl-6 py-2 sm:py-3 my-4 italic text-slate-700 text-sm sm:text-base","children":["\n",["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Note:"}]," Use ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"-D"}]," carefully. It can remove a branch pointer before its work is merged elsewhere."]}],"\n"]}]
67:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"detached-head-explained","children":"Detached HEAD Explained"}]
68:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["A ",["$","strong",null,{"children":"detached HEAD"}]," happens when HEAD points directly to a commit instead of a branch."]}]
69:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"For example:"}]
6a:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git checkout 1ab93e0\n"}]}]
6b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Now you are no longer âonâ ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," or ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/login"}],". You are looking at a specific past commit."]}]
6c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"This can be useful for inspection, testing, or reproducing an old state. But if you make commits here, they are not attached to a branch unless you create one."}]
6d:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"safe-way-to-continue-from-detached-head","children":"Safe Way to Continue from Detached HEAD"}]
6e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"If you decide to keep that work, create a branch immediately:"}]
6f:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch -c investigate-old-release\n"}]}]
70:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"That gives the new commits a proper branch name and keeps them easy to find."}]
71:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"good-branch-naming-habits","children":"Good Branch Naming Habits"}]
72:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Teams usually prefer simple, descriptive names such as:"}]
73:["$","ul",null,{"className":"space-y-2 mb-4 ml-4 sm:ml-6 list-disc marker:text-teal-500","children":["\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/login-page"}]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"fix/pipeline-timeout"}]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"docs/git-tutorial"}]}],"\n",["$","li",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed pl-1","children":["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"hotfix/rollback-script"}]}],"\n"]}]
74:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"A good branch name helps everyone understand the purpose of the work."}]
75:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"a-practical-branch-workflow","children":"A Practical Branch Workflow"}]
76:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Here is a realistic beginner workflow:"}]
77:["$","pre",null,{"className":"bg-slate-950 text-emerald-400 rounded-lg sm:rounded-xl p-3 sm:p-4 overflow-x-auto mb-4 border border-slate-700 shadow-lg text-xs sm:text-sm leading-relaxed [&_code]:bg-transparent [&_code]:p-0 [&_code]:text-inherit [&_code]:rounded-none","children":["$","code",null,{"className":"language-bash text-sm","children":"git switch main\ngit pull\ngit switch -c feature/add-footer\necho "Footer content" >> index.html\ngit add index.html\ngit commit -m "Add footer to homepage"\ngit switch main\n"}]}]
78:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["At this point, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," stays stable while the new work lives on ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/add-footer"}]," until it is reviewed and merged."]}]
79:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"what-you-should-remember","children":"What You Should Remember"}]
7a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"Branches are lightweight and powerful. Use them to isolate work, protect stable code, and make collaboration easier. HEAD represents your current location in history. Most of the time it points to a branch, but during detached HEAD it points directly to a commit."}]
7b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":"If you remember one best practice, remember this: create a branch before doing meaningful new work."}]
7c:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"test-your-understanding","children":"Test Your Understanding"}]
7d:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"exercise-1-creating-isolated-work","children":"Exercise 1: Creating isolated work"}]
7e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Scenario:"}]," ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"main"}]," is stable and you want to build a new navbar without disturbing it."]}]
7f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Question:"}]," Which command both creates and switches to the feature branch in one step?"]}]
80:["$","$L96",null,{"exercise":{"id":"git-branching-q1","title":"Exercise 1: Creating isolated work","description":"Choose the one-step command for creating and switching to a new branch.","options":[{"id":"a","text":"git merge feature/navbar"},{"id":"b","text":"git switch -c feature/navbar"},{"id":"c","text":"git log --oneline --decorate -5"},{"id":"d","text":"git branch -d feature/navbar"}],"correctId":"b","explanation":"The tutorial introduced --recurse-submodules","level":3},{"id":"detached-head-confusion","title":"Detached HEAD Confusion","level":3},{"id":"pointer-drift","title":"Pointer Drift","level":3},{"id":"ci-and-onboarding-friction","title":"CI and Onboarding Friction","level":3},{"id":"alternatives-to-submodules","title":"Alternatives to Submodules","level":2},{"id":"git-subtree","title":"Git Subtree","level":3},{"id":"package-managers","title":"Package Managers","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What is a Git submodule?","answer":"A submodule is a reference from one repository to a specific commit of another. The parent repository records the child's URL in .gitmodules and the exact commit in its tree, not the files themselves. That pinning is the feature: the parent always checks out a known version of the dependency rather than whatever the other repository's main happens to be. It is also the source of most confusion, because the submodule is a separate repository with its own history, branches and remote."},{"question":"Why is my submodule directory empty after cloning?","answer":"Because a plain git clone records the submodule but does not fetch its contents. Clone with git clone --recurse-submodules <url>, or fix an existing clone with git submodule update --init --recursive. Setting git config --global submodule.recurse true makes clone, pull and checkout handle submodules automatically, which removes an entire category of "it works on my machine" from a team using them."},{"question":"How do I update a submodule to the latest commit?","answer":"Two different operations, easily conflated. git submodule update --init --recursive checks out the commit the parent has pinned. This is what you want after pulling someone else's change. git submodule update --remote fetches the submodule's tracked branch and moves the pin to its newest commit, which is a change to the parent repository and must be committed there. Running the second when you meant the first is how a submodule silently jumps versions."},{"question":"Why is my submodule in a detached HEAD state?","answer":"Because that is by design: the parent pins a commit, not a branch, so the submodule is checked out at that exact commit with no branch attached. Committing there produces work that belongs to no branch. If you intend to make changes, cd into the submodule and git switch main (or the relevant branch) first, then commit and push in the submodule, and only afterwards commit the new pin in the parent."},{"question":"What is the difference between Git submodules and subtrees?","answer":"Submodules keep the dependency as a separate repository and store only a pointer, which keeps histories clean and requires every contributor to understand submodule commands. Subtrees copy the dependency's files into the parent repository, so a normal clone gets everything and no extra commands are needed, at the cost of merging the other project's history into yours and a fiddlier update path. Subtree is kinder to contributors; submodule is stricter about separation."},{"question":"Should I use submodules at all?","answer":"Only when you genuinely need to pin another repository at a known commit and cannot use a package manager. A published package, npm, PyPI, Go modules, a Terraform module registry, solves the same problem with far better tooling, versioning and caching. Submodules earn their place for vendored forks, shared configuration repositories, and monorepo-adjacent setups where no packaging exists. If a package manager can express the dependency, use it instead."}]},{"slug":"git-commit-messages","title":"Git Commit Messages","seoTitle":"Git Commit Message Best Practices and Conventional Commits","description":"Write Git commit messages that stay useful: an imperative summary, a body that records why, Conventional Commits and enforcement with commitlint and Husky.","content":"$40","difficulty":"Easy","duration":"15 min","keywords":["git commit message best practices","conventional commits","commitlint tutorial","git commit message","commit message convention","imperative mood commit","semantic versioning commits","commitlint husky","git commit --amend message","change pushed commit message","breaking change commit","good commit message examples"],"publishedDate":"2026-07-14","updatedDate":"2026-09-16","order":17,"tableOfContents":[{"id":"why-good-commit-messages-matter","title":"Why Good Commit Messages Matter","level":2},{"id":"the-7-rules-of-a-great-commit-message","title":"The 7 Rules of a Great Commit Message","level":2},{"id":"imperative-mood","title":"Imperative Mood","level":3},{"id":"good-vs-bad-messages","title":"Good vs Bad Messages","level":2},{"id":"conventional-commits","title":"Conventional Commits","level":2},{"id":"breaking-changes","title":"Breaking Changes","level":3},{"id":"conventional-commits-and-semantic-versioning","title":"Conventional Commits and Semantic Versioning","level":2},{"id":"helpful-tools","title":"Helpful Tools","level":2},{"id":"commitlint","title":"commitlint","level":3},{"id":"semantic-release","title":"semantic-release","level":3},{"id":"what-you-should-remember","title":"What You Should Remember","level":2},{"id":"test-your-understanding","title":"Test Your Understanding","level":2},{"id":"frequently-asked-questions","title":"Frequently Asked Questions","level":2}],"noindex":false,"faqs":[{"question":"What makes a good Git commit message?","answer":"A short imperative summary under about 50 characters, "Fix login redirect loop", not "Fixed" or "Fixes", then a blank line, then a body explaining why the change was needed if it is not obvious. The summary appears in git log --oneline, on pull requests and in git blame, so it has to stand alone. The body is where the value is: the diff already shows what changed, and only the message can record the reasoning, the alternative you rejected, or the ticket that prompted it."},{"question":"Why should commit messages use the imperative mood?","answer":"Because Git itself does. Its generated messages read "Merge branch 'main'" and "Revert 'Add caching'", so an imperative summary sits consistently alongside them. The practical test is that the message should complete the sentence "If applied, this commit willâ¦", "Add rate limiting" reads correctly, "Added rate limiting" does not. It is a convention rather than a rule, and its real benefit is consistency: a log where every line is phrased the same way is far quicker to scan."},{"question":"What are Conventional Commits and do I need them?","answer":"A format that prefixes the summary with a type and optional scope: feat(auth): add password reset, fix: handle empty cart, chore(deps): bump axios. The point is machine readability. Tooling can derive a changelog and the next semantic version from the log without anyone maintaining either by hand. You need them if you want automated releases or generated changelogs. If you do not, the discipline still helps, but plain clear messages are perfectly adequate."},{"question":"How do Conventional Commits map to semantic versioning?","answer":"fix: produces a patch bump, feat: a minor bump, and a breaking change a major bump. A breaking change is marked either with ! after the type, feat!: drop Node 18 support, or a BREAKING CHANGE: footer in the body. Tools such as semantic-release read the commits since the last tag, work out the highest bump implied, then tag the release and write the changelog. That is why the format is strict: the version number is derived from it."},{"question":"How do I change a commit message I already pushed?","answer":"git commit --amend rewrites the most recent message, but amending changes the commit hash, so a pushed commit then needs git push --force-with-lease. That is acceptable on your own feature branch and not on a shared one, where everyone who pulled is left on a commit that no longer exists. For older commits, git rebase -i HEADgit switch -c as the clearer modern way to create a branch and move HEAD to it immediately. That keeps the new work isolated from main.","wrongExplanations":{"a":"Merge combines history from an existing branch; it does not create a new feature branch for you.","c":"Log inspection helps you read history, but it does not create or switch branches.","d":"That deletes a branch instead of creating one."}}}]
81:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"exercise-2-reading-head-correctly","children":"Exercise 2: Reading HEAD correctly"}]
82:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Question:"}]," In ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git log --oneline --decorate"}],", you see ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"(HEAD -> feature/login)"}]," next to the latest commit. What does that mean?"]}]
83:["$","$L96",null,{"exercise":{"id":"git-branching-q2","title":"Exercise 2: Reading HEAD correctly","description":"Interpret a decorated log entry where HEAD points to a branch.","options":[{"id":"a","text":"Git is in detached HEAD state on an unnamed commit."},{"id":"b","text":"The remote branch origin/feature/login has been deleted."},{"id":"c","text":"The feature/login branch is merged and can no longer receive commits."},{"id":"d","text":"Your current position is on the feature/login branch, and that branch points to the shown commit."}],"correctId":"d","explanation":"The tutorial described HEAD as your current position. When the log says (HEAD -> feature/login), HEAD is attached to that branch and the branch points to the displayed commit tip.","wrongExplanations":{"a":"Detached HEAD would mean HEAD points directly to a commit rather than through a named branch.","b":"The decorate output says nothing about deletion of the remote branch.","c":"A branch being checked out does not imply it is merged or closed to new commits."}}}]
84:["$","h3",null,{"className":"text-lg sm:text-xl font-semibold text-slate-800 mb-2 mt-6 first:mt-0 scroll-mt-24","id":"exercise-3-escaping-detached-head-safely","children":"Exercise 3: Escaping detached HEAD safely"}]
85:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Scenario:"}]," You checked out an old commit to investigate a bug and now want to keep new work you started there."]}]
86:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","strong",null,{"children":"Question:"}]," What is the safe next step from the lesson?"]}]
87:["$","$L96",null,{"exercise":{"id":"git-branching-q3","title":"Exercise 3: Escaping detached HEAD safely","description":"Choose the action that preserves new work created from a detached HEAD state.","options":[{"id":"a","text":"Create a branch immediately with git switch -c investigate-old-release."},{"id":"b","text":"Run git branch -D main so the old commit becomes permanent."},{"id":"c","text":"Push directly to origin without creating any branch."},{"id":"d","text":"Use git checkout main and expect Git to carry detached commits automatically."}],"correctId":"a","explanation":"The tutorial explicitly recommended creating a branch right away when you decide detached HEAD work should be kept. That attaches future commits to a named branch so they remain easy to find.","wrongExplanations":{"b":"Deleting main is unrelated and dangerous. It does not solve detached HEAD bookkeeping.","c":"Without a proper branch, the new commits are easy to lose and the workflow stays unclear.","d":"Switching away without creating a branch can leave detached commits difficult to locate later."}}}]
88:["$","hr",null,{}]
89:["$","h2",null,{"className":"text-xl sm:text-2xl font-bold text-slate-900 mb-3 mt-8 first:mt-0 scroll-mt-24 border-l-4 border-teal-500 pl-3 sm:pl-4","id":"frequently-asked-questions","children":"Frequently Asked Questions"}]
8a:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"What is the difference between git switch and git checkout?"}]}]
8b:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git checkout"}]," does several unrelated jobs, moving between branches, creating them, and restoring files, which made it a common source of accidents, since ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git checkout <file>"}]," silently discarded changes. Git split those responsibilities: ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch"}]," changes branches and ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git restore"}]," restores files. ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch -c <name>"}]," replaces ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git checkout -b <name>"}],". Both new commands are stable and preferred in new work; ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"checkout"}]," still functions and is not going away, so older documentation remains valid."]}]
8c:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"What does "detached HEAD" mean and how do I fix it?"}]}]
8d:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"HEAD"}]," normally points at a branch name, which in turn points at a commit. When you check out a commit, tag, or remote-tracking branch directly, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"HEAD"}]," points straight at that commit with no branch attached, that is detached. You can look around and even commit, but those commits belong to no branch and become unreachable once you leave. If you have made commits you want to keep, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch -c <new-branch>"}]," right where you are captures them. If you have not, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch main"}]," returns you to normal."]}]
8e:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"How do I delete a local and a remote branch?"}]}]
8f:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git branch -d <name>"}]," deletes a local branch and refuses if it holds commits not merged anywhere, which is a safety net worth keeping; ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"-D"}]," forces it. For the remote, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git push origin --delete <name>"}],". Deleting a remote branch does not remove other people's stale local copies of it. They need ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git fetch --prune"}],", or ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git config --global fetch.prune true"}]," to have it happen automatically on every fetch."]}]
90:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"How do I rename a Git branch?"}]}]
91:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":[["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git branch -m <new-name>"}]," renames the branch you are on, or ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git branch -m <old> <new>"}]," for another one. Renaming a branch that exists on the remote takes three steps, because the remote branch is a separate reference: rename locally, push the new name with ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git push origin -u <new-name>"}],", then delete the old one with ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git push origin --delete <old-name>"}],". Anyone else working on it needs to re-point their local branch, so agree the change first."]}]
92:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"What is a good branch naming convention?"}]}]
93:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["Use a short prefix that says what kind of change it is, then a brief description in kebab-case: ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/user-search"}],", ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"fix/login-redirect"}],", ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"chore/bump-deps"}],". Many teams add the ticket number, ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"feature/PROJ-412-user-search"}],", which lets tooling link branches to issues automatically. Keep them lowercase and avoid spaces and unusual characters, since branch names become part of URLs and filesystem paths. The value is not aesthetic: a consistent scheme makes ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git branch --list 'fix/*'"}]," and automated cleanup possible."]}]
94:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["$","strong",null,{"children":"Does creating a branch in Git copy all my files?"}]}]
95:["$","p",null,{"className":"text-base sm:text-lg text-slate-700 leading-relaxed mb-4","children":["No, and this is why Git branching is fast where older systems were slow. A branch is a 41-byte file containing a commit hash. A pointer, nothing more. Creating one writes that file and does not touch your working directory at all, which is why ",["$","code",null,{"className":"bg-slate-100 text-rose-600 px-1 sm:px-1.5 py-0.5 rounded font-mono text-sm sm:text-base break-words","children":"git switch -c"}]," is instant even in a huge repository. Switching branches then updates only the files that actually differ between the two commits."]}]
28:[["$","meta","0",{"charSet":"utf-8"}],["$","meta","1",{"name":"viewport","content":"width=device-width, initial-scale=1"}]]
26:null
2a:[["$","title","0",{"children":"Git Branching: Create, Switch and Delete Branches Explained"}],["$","meta","1",{"name":"description","content":"Learn what a Git branch really is, git switch versus checkout, how HEAD and detached HEAD work, and how to delete, rename and name branches well."}],["$","link","2",{"rel":"author","href":"https://devopslesson.com"}],["$","meta","3",{"name":"author","content":"DevOpsLesson Team"}],["$","meta","4",{"name":"keywords","content":"git branching, git checkout switch branch, git head detached head, create branch git, delete branch git, git branch, git switch vs checkout, detached HEAD, delete git branch, delete remote branch, rename git branch, git branch naming convention, git HEAD explained, create new branch git, git fetch --prune, git tutorial, learn git, git for beginners, git branching tutorial, git merging tutorial, git rebase tutorial, git remote tutorial, github workflow tutorial, git commands guide, version control tutorial, git stash tutorial, git cherry-pick, git bisect, git reflog, git beginner guide 2026, git pull request workflow"}],["$","meta","5",{"name":"creator","content":"DevOpsLesson"}],["$","meta","6",{"name":"publisher","content":"DevOpsLesson"}],["$","meta","7",{"name":"robots","content":"index, follow"}],["$","meta","8",{"name":"googlebot","content":"index, follow, max-video-preview:-1, max-image-preview:large, max-snippet:-1"}],["$","link","9",{"rel":"canonical","href":"https://devopslesson.com/tutorials/git/git-branching"}],["$","meta","10",{"name":"google-site-verification","content":"v1hGowZeYvsOScm3hhGtqYv7ZG_yiFG-YbjDK"}],["$","meta","11",{"property":"og:title","content":"Git Branching"}],["$","meta","12",{"property":"og:description","content":"Learn what a Git branch really is, git switch versus checkout, how HEAD and detached HEAD work, and how to delete, rename and name branches well."}],["$","meta","13",{"property":"og:url","content":"https://devopslesson.com/tutorials/git/git-branching"}],["$","meta","14",{"property":"og:site_name","content":"DevOpsLesson"}],["$","meta","15",{"property":"og:image","content":"https://devopslesson.com/api/og?title=Git+Branching&eyebrow=Git+Tutorial"}],["$","meta","16",{"property":"og:image:width","content":"1200"}],["$","meta","17",{"property":"og:image:height","content":"630"}],["$","meta","18",{"property":"og:image:alt","content":"Git Branching"}],["$","meta","19",{"property":"og:type","content":"article"}],["$","meta","20",{"property":"article:published_time","content":"2026-07-14"}],["$","meta","21",{"property":"article:modified_time","content":"2026-09-16"}],["$","meta","22",{"property":"article:author","content":"Vishvesh Patel"}],["$","meta","23",{"property":"article:tag","content":"git branching"}],["$","meta","24",{"property":"article:tag","content":"git checkout switch branch"}],["$","meta","25",{"property":"article:tag","content":"git head detached head"}],["$","meta","26",{"property":"article:tag","content":"create branch git"}],["$","meta","27",{"property":"article:tag","content":"delete branch git"}],["$","meta","28",{"property":"article:tag","content":"git branch"}],["$","meta","29",{"property":"article:tag","content":"git switch vs checkout"}],["$","meta","30",{"property":"article:tag","content":"detached HEAD"}],["$","meta","31",{"property":"article:tag","content":"delete git branch"}],["$","meta","32",{"property":"article:tag","content":"delete remote branch"}],["$","meta","33",{"property":"article:tag","content":"rename git branch"}],["$","meta","34",{"property":"article:tag","content":"git branch naming convention"}],["$","meta","35",{"property":"article:tag","content":"git HEAD explained"}],["$","meta","36",{"property":"article:tag","content":"create new branch git"}],["$","meta","37",{"property":"article:tag","content":"git fetch --prune"}],["$","meta","38",{"name":"twitter:card","content":"summary_large_image"}],["$","meta","39",{"name":"twitter:title","content":"Git Branching"}],["$","meta","40",{"name":"twitter:description","content":"Learn what a Git branch really is, git switch versus checkout, how HEAD and detached HEAD work, and how to delete, rename and name branches well."}],"$L97","$L98","$L99","$L9a","$L9b","$L9c"]
9d:I[86869,[],"IconMark"]
97:["$","meta","41",{"name":"twitter:image","content":"https://devopslesson.com/api/og?title=Git+Branching&eyebrow=Git+Tutorial"}]
98:["$","link","42",{"rel":"shortcut icon","href":"/favicon.svg"}]
99:["$","link","43",{"rel":"icon","href":"/icon","type":"image/png","sizes":"32x32"}]
9a:["$","link","44",{"rel":"icon","href":"/favicon.svg","type":"image/svg+xml"}]
9b:["$","link","45",{"rel":"apple-touch-icon","href":"/icon","sizes":"180x180","type":"image/png"}]
9c:["$","$L9d","46",{}
Learn what Git is and why version control matters: distributed versus centralized systems, how Git stores history and the key terms to know first.
Install Git on macOS, Linux and Windows, then configure what matters: your identity, default branch, editor and .gitattributes for consistent line endings.
Learn the everyday Git loop with init, clone, status, add, commit, log and diff, understand the staging area and build checks to run before every commit.