← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 25 — Git and Version Control for Front-End

Section 1 — What version control is

Version control is a system that records every saved change to your project, so you can look back, compare versions, and return to an earlier one. Think of it as an undo history for the whole project, not just for one file in one editing session. When you write a document, you can undo your last few keystrokes, but that history disappears when you close the file. Version control keeps a durable history that survives closing the editor, restarting the computer, and even moving the project to another machine. Each saved state is kept, with a note about what changed and when, so the project's past is not a vague memory but a record you can inspect.

That record changes how you work. Without it, trying a risky idea feels dangerous, because if the idea fails you may not be able to get back to the version that worked. With version control, trying an idea is cheap: save the working version, try the idea, and if it fails, return to the saved version in seconds. That safety encourages experimentation, which is how better designs are found. You can try a new layout, a different colour scheme, or a rewritten script, knowing that the old version is safe. The freedom to experiment without fear is one of the most valuable things version control gives a developer, and it is easy to underestimate until you have worked without it.

Version control also records the story of a project, not just its current state. A list of saved changes, each with a message, shows how the project grew: which features were added, which bugs were fixed, and which decisions were reversed. That story is useful when you return to a project after a break, because it reminds you what you were doing and why. It is useful in a team, because it shows who changed what and when, without anyone needing to remember or to explain in a meeting. And it is useful when something breaks, because you can compare the broken version with the last working version and see exactly what changed in between.

It is important to understand that version control does not save automatically, and it does not save everything. You choose when to save, by making a commit, and you choose what to include in that save, by staging files. That control is a feature, not a flaw. It means each saved state is a deliberate checkpoint, with a clear message, rather than a random snapshot taken every few minutes. Learning to make good checkpoints, with clear messages, is a skill that this chapter will teach step by step. For now, hold the core idea: version control is a durable, inspectable history for a whole project, and that history makes work safer, more experimental, and easier to understand.

Check: In one sentence, what does version control give you?

Section 2 — Why front-end work needs it

Front-end projects change constantly, and they change in ways that are visible and fragile. A layout tweak can shift a whole page. A colour change can make text unreadable against its background. A script fix can break a button that used to work. A change to a stylesheet can affect ten pages at once, because styles are shared. In such a project, the ability to undo a change cleanly, and to see exactly what changed, is not a luxury. It is a daily necessity, because the cost of a mistake is a page that looks wrong or behaves wrongly for every visitor, and the cause of the mistake is often a small edit made hours earlier and already forgotten.

Consider a common front-end story. You adjust the spacing on a navigation menu to make it look better on a phone. The menu looks better, but later you notice that a footer on another page has shifted, because both pages share a style rule you also changed. Without version control, fixing the footer while keeping the menu improvement means remembering exactly what you changed, and unpicking one part of the change without losing the other. With version control, you open the history, look at the change that affected both pages, and see the exact lines that changed. You can revert the part that hurt the footer, keep the part that helped the menu, and commit the corrected version with a clear message. The history turns a vague worry into a concrete diff you can read.

Front-end work also involves many files that depend on each other. HTML gives structure, CSS gives appearance, and JavaScript gives behaviour, and a change in one layer can break an assumption in another. A new class name in HTML, for example, must match the selector in CSS and the query in JavaScript, or the style and the behaviour will both fail silently. Version control lets you save all three layers together, in one commit, so the project is never left in a state where the HTML expects a class that the CSS does not define. That atomicity, saving related changes together, keeps the project coherent at every checkpoint, which is essential when three languages must agree with each other.

Finally, front-end work is rarely solo for long. Even a personal project may later be shared, reviewed, or handed to someone else, and a team project involves several people editing the same files at the same time. Without version control, two people editing the same file will overwrite each other, and the lost work may not even be noticed until much later. With version control, each person's work is recorded separately, and the system helps combine them, flagging conflicts where the same lines were changed differently. That combination, and the history behind it, is what makes front-end collaboration possible at any scale beyond a single person working alone.

Check: Why is version control especially useful when a layout change breaks a page?

Section 3 — What Git is

Git is a version control tool that runs on your own computer. It tracks changes in a project folder, stores them in a hidden history, and can share that history with other computers when you ask it to. It is a command-line tool, which means you interact with it by typing commands, although graphical tools exist that run those commands for you behind a friendly interface. Understanding the commands is valuable even if you later use a graphical tool, because the graphical tool's buttons correspond to the commands, and when something goes wrong, the command-line message usually explains the problem more precisely than a button's error dialog.

A crucial distinction is that Git is not a website. Websites that host Git projects, where you can browse code, open pull requests, and collaborate with others, are separate services that use Git as their underlying system. Git itself lives on your machine and works perfectly well without any website at all. You can create a repository, make commits, create branches, and move through history entirely offline, on a laptop with no internet connection. The website becomes relevant only when you want to share your repository with others, back it up to another machine, or collaborate through its extra features. Keeping that distinction clear prevents a common confusion, where a beginner thinks that using Git means being online, or that losing access to a hosting website means losing their project. Your local repository is the real project, with its full history, and the hosted copy is a copy of it.

Git is also distributed, which means every copy of a repository contains the full history, not just the latest version. When you clone a project, you receive every commit, every branch, and every message that went into it, not just a snapshot of the current files. That completeness is what makes Git robust. If a hosted copy disappears, any developer's local copy can restore the whole project, history included. If your computer fails, a copy you pushed to a remote can restore it. Distributed version control trades a little complexity, because there are local and remote copies to keep in step, for a great deal of resilience, because no single copy is indispensable.

For a front-end developer, Git is the standard tool for version control, and learning it is a professional skill, not just a convenience. Job postings, open-source projects, and team workflows all assume familiarity with it, and the habits it teaches, such as small commits and clear messages, improve your work even when you are working alone. Start with the basic commands in this chapter, practise them on a small project, and build from there. Git rewards steady, daily use far more than occasional, heroic use, and the sooner it becomes part of your routine, the sooner its benefits become obvious.

Check: Is Git a website, or a tool on your computer?

Section 4 — What a repository is

A repository, often shortened to repo, is a project folder that Git is tracking, plus its hidden history. The two parts belong together, and understanding both is essential. The visible part is the working folder: the files you edit every day, the HTML, CSS, JavaScript, images, and configuration that make up the site. The hidden part is a folder that Git creates inside the project, which stores every commit, every branch, and every version of every file that has ever been committed. You do not edit the hidden folder directly. Git manages it for you, and you interact with it through Git commands, which read from it and write to it on your behalf.

When you first create a repository, the hidden history is empty except for the fact that tracking has begun. As you make commits, the history grows, recording each saved state as a snapshot of the files you staged. The working folder always shows one version of the project at a time, usually the latest commit on your current branch, but the hidden history contains all versions, and Git can switch the working folder between them on command. That switching is how you can look at an old version, compare it with the current one, or return to it if a recent change went wrong. The working folder is the present; the hidden history is the past, kept in full, ready to be revisited.

It is worth noting what a repository is not. It is not a backup of your whole computer, and it is not a folder where you throw unrelated files. It is a single project, with its own history, its own branches, and its own purpose. Putting unrelated projects in one repository mixes their histories and makes cloning, sharing, and reviewing harder, a topic covered later in this chapter. A repository should contain the source files for one project, plus the configuration and documentation that project needs, and it should exclude generated files and secrets, which are handled by an ignore file, also covered later.

When you clone a repository from a hosting service, you receive both parts: a working folder with the current files, and a hidden history with the full record. When you copy a project folder without its hidden history, for example by downloading only the files, you receive the present without the past, and you cannot use Git's history features until you create a new repository, which starts a new history from that point. That is why it matters to clone, rather than download, when you want the full project. The repository is the folder plus its history, and the history is what makes version control powerful. If you ever receive only the files without that hidden history, remember that you have received the present without the past, and Git history will not be available until a new repository is created.

Check: What two things make up a repository?

Section 5 — Starting a repository

Inside a project folder, the command git init turns that folder into a repository. It creates the hidden history store, sets up the initial configuration, and tells Git to start tracking changes in that folder from now on. The command itself is short and undramatic, which is fitting, because creating a repository should be a routine first step for a new project, not a ceremony. You open a terminal, move into the project's root folder, type git init, and Git replies with a message confirming that an empty repository has been initialised. From that moment, Git commands work in that folder and in its subfolders, because Git looks upward for the hidden history store and finds it at the root.

Run git init once per project, in the project's root folder, not inside every subfolder. A common beginner mistake is to run it in a subfolder, or to run it repeatedly in different folders, creating nested repositories that confuse Git and make status output hard to read. The rule is simple: one project, one repository, one git init, at the top. If you are unsure whether a folder is already a repository, run git status. If Git reports a status, the folder is already inside a repository, and you do not need to initialise again. If Git reports that the folder is not a repository, then, and only then, is git init the right command.

Timing matters too. It is best to initialise a repository at the very start of a project, before you have written much code, so that the history captures the project from its beginning. Starting late is still better than never starting, but a history that begins halfway through a project cannot tell you about its early decisions, and the first commit will be a large one containing everything written so far, rather than a series of small, readable steps. If you inherit a project that is not yet a repository, initialise it, make a first commit with a clear message, and then continue with small commits from there. The history will be incomplete, but it will be useful from that point onward.

After initialising, the next steps are to check status, stage files, and make a first commit, all covered in the following sections. Do not be tempted to initialise and then immediately push to a hosting service before making a commit, because there is nothing to push yet. A repository with no commits has no history to share. Make a commit first, with the project's initial files and a message such as "Initial commit", and then, when you are ready to share or back up the project, connect it to a remote and push. That order, init, commit, then push, is the natural rhythm for starting a project under Git.

Check: What does git init do, and how often do you run it per project?

Section 6 — The three states of a file

In Git, a file in a repository can be in one of three main states, and understanding these states explains most of the output Git produces. A file is untracked when Git is not watching it yet. This happens for a new file you have just created, which Git has never seen in a commit. Git will list it in status output as untracked, to draw your attention to it, but it will not include it in any commit until you stage it. Untracked does not mean unimportant. It simply means Git does not yet know whether you intend to keep this file as part of the project, or whether it is a scratch file that should be ignored or deleted.

A file is modified when Git is tracking it, because it was included in a previous commit, but its contents have changed since that commit. The change might be a single line in a stylesheet, a new paragraph in HTML, or a rewritten function in JavaScript. Git detects the difference by comparing the working file with the last committed version, and it reports the file as modified. A modified file is not yet saved. Its changes exist only in your working folder, and if you were to switch branches or discard changes, those edits could be lost. Modification is a warning that work exists which has not yet been captured in history.

A file is staged when you have marked its current changes as ready to save, using the staging command covered in a later section. Staging moves the file's changes into a holding area, sometimes called the index, which is the set of changes that the next commit will record. A staged file's changes are still in your working folder, but Git now knows you intend to include them in the next snapshot. You can stage some files and not others, and you can stage some changes within a file and not others, which gives you precise control over what each commit contains. That precision is the point of staging: it lets you craft commits that each tell one clear story, rather than saving everything that happens to have changed.

There is a fourth state worth mentioning, although it is really the absence of change: a file is unmodified, or clean, when its working version matches the last committed version exactly. Clean files do not appear in status output as changed, because there is nothing to report. The goal, at the end of a work session, is often to leave the project with files either clean, because their changes have been committed, or deliberately untracked or ignored, because they are not part of the project. Learning to read these states, untracked, modified, staged, and clean, turns Git's output from a confusing list into a clear map of where your work stands.

Check: What is the difference between a modified file and a staged file?

Section 7 — Checking status first

The command git status shows what has changed, what is staged, and what is untracked, along with which branch you are on and whether your branch is ahead of or behind its remote counterpart. It is the most frequently used Git command, and for good reason. Before you save, before you share, and whenever Git's reply surprises you, running status tells you where you are. It is the map of your working folder, and like a map, it is most useful when you consult it before you act, not after you have already gone astray.

Make status a habit, not an occasional check. A good routine is to run it when you start work, to see whether yesterday's work was left clean or whether there are uncommitted changes waiting. Run it again before you commit, to confirm that the files you intend to save are the ones that are staged, and that no stray file has slipped in. Run it after a merge or a pull, to confirm that the operation completed cleanly and that no conflict remains. Run it whenever a command produces an unexpected message, because status often explains the message by showing a staged file you forgot, a branch you did not realise you were on, or a conflict that is still unresolved. Each of these runs takes a second, and each prevents a mistake that could take much longer to fix.

Status output is designed to be read in sections. The first section tells you the branch and its relationship to the remote. The next sections list staged changes, unstaged changes, and untracked files, each with the file names and, often, a hint about the command to stage or unstage them. Read the sections in order, and do not skim past the branch line, because working on the wrong branch is one of the most common Git mistakes, and status is the earliest place to catch it. If the branch is not the one you intended, switch before you edit, not after, because switching with uncommitted changes can complicate the move.

For a beginner, status can feel verbose, but its verbosity is helpful. Git is telling you, in plain language, what it sees, and it often suggests the next command. Treat those suggestions as hints, not as orders, and learn to interpret them rather than copying them blindly. Over time, you will learn to glance at status and instantly see the state of your project: clean, in progress, ready to commit, or in need of attention after a merge. That instant reading is a professional skill, and it starts with the simple habit of running git status first, every time, before you do anything else. A glance at status before you act is the cheapest insurance against working on the wrong branch or committing a file you did not intend to include.

Check: When should you run git status?

Section 8 — Staging a change

Staging marks a change as ready to save. It is the step between editing a file and committing it, and it exists so that you can choose exactly which changes go into the next commit. The command is git add, followed by a file name or a pattern. To stage one file, you write git add index.html. To stage all changed files in the current folder and below, you write git add ., where the dot means "this folder and everything inside it". Both forms are used daily, and choosing between them is a matter of intention. Staging one file is precise. Staging everything is convenient when you know that all current changes belong together in one commit.

The power of staging becomes clear when you have made several unrelated changes. Suppose you fixed a button style, added a new paragraph, and corrected a typo in a script, all in the same work session. Saving all three together in one commit would create a commit whose message cannot honestly describe all three changes, and whose diff mixes three stories. Staging lets you save them separately: stage the stylesheet change and commit it with a message about the button, then stage the HTML change and commit it with a message about the paragraph, then stage the script change and commit it with a message about the typo. Each commit is now a single, clear story, and each can be reviewed, reverted, or understood on its own. That separation is worth the extra minute staging takes.

Staging also lets you check your work before saving it. After staging, you can run a command to see the staged diff, which shows exactly what the next commit will contain. If the staged diff includes a file you did not intend to save, or a change you meant to keep for later, you can unstage it before committing, without losing the work itself. Unstaging, covered in a later section, removes the ready-to-save mark but keeps your edits in the working folder. That safety net means staging is never a dangerous step. You can stage, inspect, adjust, and only then commit, confident that the commit contains exactly what you inspected.

A caution about git add . is worth remembering. Because it stages everything changed in the folder, it can easily stage a scratch file, a screenshot, or a local configuration file that should not be committed. Before using it, run status and look at the untracked and modified lists. If anything there should not be committed, either stage files individually, or add the unwanted file to the ignore file, so that future uses of git add . skip it automatically. Staging is a choice, and like all choices in Git, it is best made deliberately, with status output in front of you.

Check: How do you stage one file, and how do you stage all changed files in a folder?

Section 9 — Making a commit

A commit is a saved snapshot of the staged changes, with a message. It is the fundamental unit of Git history. When you run git commit -m "Add hero heading and styles", Git takes everything currently staged, records it as a new snapshot, attaches the message you provided, and adds that snapshot to the history of your current branch. The working folder does not change visibly, because the snapshot matches what you already have, but the history now contains a new checkpoint that you can return to, compare with, or share. That checkpoint is permanent, in the sense that it stays in history unless you deliberately rewrite history, which is a topic for later, and it is identified by a unique hash that Git generates for it.

The two parts of a commit, the snapshot and the message, are equally important. The snapshot answers the question "what changed?" by recording the exact contents of the staged files at that moment. The message answers the question "why did it change, and what is it for?" in human language. A snapshot without a message is a record without an explanation, and a message without an accurate snapshot is an explanation for the wrong change. Together, they make history readable. A future developer, or future you, can look at the list of commits, read the messages to find the relevant one, and then look at its snapshot diff to see exactly what changed. Without both parts, that search becomes guesswork.

The -m flag in the example provides the message directly on the command line, which is convenient for short messages. For longer messages, with a summary and a body explaining the reasoning, Git can open an editor for you to write in, if you run commit without -m. Either way, the message should be written for a human reader who was not present when the change was made. It should say what changed and, where the reason is not obvious from the change itself, why it changed. A message that says "Add hero heading and styles" tells a reader what to expect in the diff. A message that says "Fix button overlap on small screens, caused by a fixed width on the container" tells a reader both what and why, which is even better for a fix whose cause is not visible in the diff alone.

After committing, run status again. It should show a clean working folder, or at least no longer show the files you just committed as staged or modified, because their changes are now recorded. That clean state is the signal that the checkpoint is safe. From here, you can continue working, make further commits, switch branches, or push the commit to a remote. The commit is the point at which work becomes history, and history, once made, is the foundation for everything else Git offers.

Check: What two things does a commit contain?

Section 10 — Writing good commit messages

A commit message is a short piece of writing that will be read far more often than it was written. It is read when someone scans history to find when a feature was added, when a bug was introduced, or when a decision was made. It is read during code review, when a reviewer checks whether a change matches its description. It is read by future you, who will have forgotten the details of today's work and will rely on the message to reconstruct them. Writing it well is therefore not a courtesy. It is part of the work itself, and a few seconds spent on a clear message saves minutes or hours of confusion later.

The first rule is to be specific. A message should name what changed, in plain words, so that a reader can guess what the diff contains before opening it. "Fix button overlap on small screens" is specific. "Fix stuff" is not, because it could mean anything, and a reader searching history for a button fix will skip past it. "Update files" is equally vague, because every commit updates files. Specificity does not mean length. A single, well-chosen line is better than a paragraph that wanders. Aim for a summary line that a stranger could understand without opening the diff, and add a body only when the reason for the change is not obvious from the change itself.

The second rule is to write in a consistent style. Many teams write messages as commands, in the present tense, such as "Add hero section" rather than "Added hero section" or "Adding hero section". The command style reads naturally as an instruction for what the commit does when applied, and it keeps a list of commits feeling like a list of actions. Consistency matters more than any particular style, so follow your team's convention if one exists, and if you work alone, pick a style and stick to it. A history with a consistent voice is easier to scan than one where every message follows a different grammar.

The third rule is to explain why when why is not obvious. If you changed a colour, the diff shows the old and new colours, but it does not show why the new colour was chosen. If the reason was accessibility, because the old colour failed a contrast check, say so. If you removed a feature, the diff shows the removal, but it does not show whether the removal was because the feature was unused, broken, or replaced by something else. Say so. The why is the information that cannot be recovered from the diff alone, and it is therefore the most valuable part of a message for a future reader. A good message is short, specific, consistent, and honest about reasons, and it treats the future reader as a colleague who deserves a clear explanation.

Check: Why is "fix stuff" a poor commit message?

Section 11 — Small commits beat huge ones

A commit that changes one thing, one fix, one feature, or one style pass, is easy to review, easy to understand, and easy to undo. A commit that changes twenty unrelated things is hard in all three respects, and the difficulty grows faster than the size. A reviewer faced with a huge diff must hold many unrelated changes in mind at once, and is likely to miss a problem in one of them because attention is spread thin. A future developer searching history for a particular change must wade through unrelated changes to find it. And a developer who needs to undo one part of a huge commit cannot do so without also undoing the other parts, because they are tangled together in a single snapshot.

The undo problem deserves emphasis, because it is where huge commits cause the most pain. Suppose a commit adds a new feature, fixes a bug, and restyles a page, all together. Later, the feature turns out to be unwanted, but the bug fix and the restyle are valuable. Undoing the commit as a whole would throw away the good work along with the unwanted feature. Undoing only the feature part requires manually unpicking its lines from the diff, a slow and error-prone job that is exactly what version control was meant to avoid. If the three changes had been three separate commits, undoing the feature would be a single, clean reversal, leaving the fix and the restyle untouched. Small commits make history reversible in a way that huge commits do not.

Small commits also make collaboration smoother. When two developers work on the same project, small commits are easier to merge, because each commit touches fewer lines and is less likely to conflict with another developer's work. When a conflict does occur, it is smaller and easier to resolve, because the conflicting change is one clear idea, not a mixture of ideas. Small commits also make pull requests easier to review, because a reviewer can check one commit at a time, understanding each idea before moving to the next, rather than facing a wall of changes that must be understood all at once. The review becomes a conversation about ideas, not a struggle with volume.

Making small commits is a habit, not a talent. It requires staging deliberately, as covered earlier, and committing frequently, even when the work feels unfinished. A commit does not need to represent a finished feature. It can represent a step toward a feature, on its own branch, with a message that says so honestly. The branch will hold the series of steps, and the finished feature will be the result of merging the branch, not of a single enormous commit. Practise this habit on small projects, where the stakes are low, and it will feel natural on larger ones, where the stakes are high and the benefit of small, clear commits is greatest.

Check: Why is a commit that changes one thing easier to undo than a huge mixed commit?

Section 12 — Reading the history

The command git log lists past commits, newest first, with their messages, authors, dates, and unique identifiers. It is the way to read the story of a project, and it is the first tool to reach for when you need to know when something changed, who changed it, or what the project looked like at a particular point. Running log without options gives a detailed view, with each commit shown in full, which is useful when you need the complete information about a few recent commits, but can be overwhelming when a project has a long history and you only need an overview.

Adding --oneline gives a short, one-line-per-commit view that is far easier to scan: git log --oneline. Each line shows a shortened identifier and the commit's summary message, which is enough to recognise most commits at a glance. A list of one-line commits reads like a table of contents for the project, and scanning it is often the fastest way to find the commit you are looking for. You can spot the commit that added a feature, the commit that fixed a bug, or the commit after which a page started behaving differently, and then look at that commit in detail with other commands. The one-line view is the default view most developers use day to day, because it balances information with readability.

Reading history is a skill that improves with practice. Start by running log after a work session and reading your own commits back, asking whether the messages make sense in sequence and whether the commits are sensibly sized. Then practise finding answers to specific questions: when was this file last changed, which commit introduced this line, and what did the project look like before a particular feature was added? Each of these questions has a Git command that answers it, and log is the starting point for most of them. The more you read history, the more you will appreciate clear messages and small commits, because you will feel their benefit directly, as a reader, rather than just hearing about it as advice.

History is also a learning resource. Reading the history of a well-maintained project shows how experienced developers break work into steps, how they name those steps, and how they handle fixes and reversals. It is a record of real decisions, made under real constraints, and it teaches more about practical development than many tutorials, because it shows the process, not just the result. Make reading history a regular habit, not just a rescue operation when something breaks, and your own commits will improve as a natural consequence of seeing good history done well. Make log part of your review routine, and your own messages will improve, because you will see them through a reader's eyes.

Check: What does git log --oneline show you?

Section 13 — Seeing exactly what changed

The command git diff shows the line-by-line difference between your current work and the last commit. It does not show whole files, only the lines that changed, with context lines around them so you can see where each change sits. Added lines are marked one way, removed lines another, and unchanged lines are shown as context, so the diff reads like a focused summary of exactly what is different. Learning to read a diff is essential, because the diff is the most precise description of a change that exists. A commit message summarises a change in words. A diff shows it in code, line by line, with nothing hidden.

Always read a diff before committing, as a final check on your own work. The diff often reveals things you forgot: a debugging line left in a script, a test value left in HTML, a style rule you meant to remove, or a file you did not intend to change at all. Catching those things in the diff, before they become part of history, is far easier than catching them afterwards, when they are already committed and possibly shared. The diff is also a chance to check that the change does what you intended. Does the new style rule target the right selector? Does the new script handle the case you were fixing? Does the HTML change keep the structure valid? Reading the diff with those questions in mind turns committing from a mechanical step into a moment of review.

Diffs are also the basis of code review, because a reviewer reads the diff of a pull request, not the whole project, to judge whether a change is correct. A clean diff, with only the lines relevant to the change, is easy to review and likely to be approved quickly. A noisy diff, with unrelated changes, formatting churn, or leftover debugging code, is hard to review and likely to be sent back for tidying. Keeping diffs clean is therefore not just tidiness. It is respect for the reviewer's time, and it increases the chance that your work will be understood and accepted without unnecessary rounds of comment and correction.

For front-end work, a diff deserves a visual check as well as a code read, a point that returns later in this chapter. A small change to a stylesheet can shift a layout in ways that are not obvious from the lines alone, and a small change to HTML can affect selectors and accessibility in ways that only become visible when the page is opened. The diff tells you what changed in code. Opening the page tells you what changed for the user. Both checks together, diff first and page second, give you confidence that a commit is ready to become part of history.

Check: Why should you read a diff before you commit?

Section 14 — Comparing staged and unstaged changes

Git distinguishes between staged changes, which are marked as ready for the next commit, and unstaged changes, which exist in your working folder but have not yet been marked. The diff commands reflect that distinction, and using the right one matters, because looking at the wrong diff can mislead you about what the next commit will contain. Plain git diff shows unstaged changes: the differences between your working files and the staged version, or, if nothing is staged, between your working files and the last commit. It answers the question "what have I changed that I have not yet staged?" and it is the diff to check while you are still working, before you decide what to save.

The command git diff --staged shows staged changes: the differences between the staged version and the last commit. It answers the question "what will the next commit contain, if I commit right now?" and it is the diff to check immediately before committing, as a final verification. If the staged diff surprises you, because it includes a file you did not intend to save, or a change you meant to keep for later, you can unstage that file or that change before committing, without losing the work itself. The work stays in your working folder, unstaged, ready to be staged later when it belongs in a different commit. That ability to inspect and adjust the staged set is the practical benefit of staging, and the staged diff is how you exercise it.

A common beginner confusion is to run plain diff after staging everything, see no output, and conclude that nothing changed. In fact, everything changed, but it is all staged, so plain diff, which shows only unstaged changes, correctly shows nothing. The changes have moved from the unstaged set to the staged set, and only the staged diff will show them. Understanding this movement, from working folder to staged set to commit, is the key to understanding Git's workflow. Work starts unstaged, becomes staged when you mark it ready, and becomes history when you commit it. Each step has a command to inspect it, and each inspection prevents a different kind of mistake.

Make it a habit to check both diffs at the right moments. Check the unstaged diff while working, to see what is still in progress. Check the staged diff before committing, to confirm exactly what will be saved. That two-step check takes seconds, and it ensures that every commit contains exactly what you intended, no more and no less. A commit that contains exactly what its message describes is a commit that future readers can trust, and trust in history is what makes version control useful in the first place.

Check: What is the difference between git diff and git diff --staged?

Section 15 — Undoing an unstaged change

If you have changed a file but not staged it, and you want the last committed version back, you can discard the change for that file. This is a destructive action, in the sense that the edits you discard are gone and cannot be recovered through Git, because they were never committed. Git will warn you, in its own way, by requiring a deliberate command, but it will not stop you if you insist. Discarding is therefore a tool to use with care, only when you are sure the changes are unwanted, and only after you have checked what you are about to discard.

Before discarding, check status and the diff for that file. Status tells you the file is modified and unstaged, confirming that discarding will affect only working changes, not staged ones or committed history. The diff tells you exactly which lines will be lost, so you can confirm that none of them contain work you want to keep. If the diff contains a mixture of wanted and unwanted changes, do not discard the whole file. Instead, edit the file by hand to remove only the unwanted parts, or stage the wanted parts first, if your workflow supports it, and discard only the rest. Discarding is for the case where the whole change is unwanted, not for mixed cases where careful editing is the right approach.

Discarding is safe in one specific sense: it does not affect committed history. The last commit remains intact, and after discarding, your working file matches that commit exactly. The project returns to a known good state, which is often exactly what you want after an experiment that failed. It is unsafe in another sense: the discarded work is gone, with no undo, unless you have a copy elsewhere, such as in an editor's backup or in a separate file. That asymmetry, safe for history but destructive for uncommitted work, is why the check-before-discarding habit matters so much. Never discard on a vague feeling that a file "looks wrong". Discard on the evidence of a diff you have read and understood.

A practical alternative, when you are unsure, is to commit the work first, even on a temporary branch, before discarding it. A committed change can always be recovered, while an uncommitted change cannot. If you later decide the work was actually useful, the commit is there to return to. If you decide it was not, the commit can be discarded from history, or simply left on a branch that is never merged. That safety-first approach costs a minute and saves the regret of losing work that turned out to be valuable after all. When in doubt, commit first on a temporary branch. A committed change can be recovered, while a discarded uncommitted change cannot be brought back.

Check: When is discarding an unstaged change safe, and when is it not?

Section 16 — Unstaging without losing work

Unstaging a file removes it from the next commit but keeps your edits in the working folder. This is the safe way to fix a staging mistake, and it is important to understand why it is safe, because beginners often fear that unstaging will delete their work. It will not. Staging is a mark, not a move. When you stage a file, Git records a copy of its current contents in the staging area, but the working file itself stays exactly where it is, with exactly the same contents. When you unstage, Git removes the staged copy and the mark, but the working file is untouched. Your edits remain, ready to be staged again later, or edited further, or discarded deliberately if you later decide they are unwanted.

The most common use for unstaging is correcting a commit that would have contained too much. Suppose you staged several files with a broad command, intending to commit one feature, and then, on checking the staged diff, you notice that a file belonging to a different feature has slipped in. Unstaging that file removes it from the next commit, while keeping its changes for a later commit, where they belong. The first commit then contains only the intended feature, with a message that honestly describes it, and the second feature waits, intact, for its own commit. Without unstaging, the only alternatives would be to commit the mixed change, which creates a confusing commit, or to discard the extra file's changes, which loses work. Unstaging gives you a third option, which is the correct one: keep the work, change the grouping.

Unstaging is also useful when you staged a file too early, before you had finished editing it. Perhaps you staged a stylesheet, then noticed a further improvement to make, and edited the file again. The staged version now reflects the older edit, while the working version reflects the newer one. You have a choice: stage the file again, updating the staged version to match the working version, or unstage it, so that the next commit does not include a half-finished change. Either choice is safe, because the working edits are preserved in both cases. The decision depends on whether the change is ready to commit or needs more work, and unstaging is the way to say "not yet" without losing anything.

Practise unstaging on a small project until it feels routine. Stage a file, check the staged diff, unstage the file, check status, and confirm that the file is now listed as modified but unstaged, with its edits still present. That experiment teaches, in a minute, what pages of explanation can only describe: staging and unstaging are about grouping changes for commits, not about creating or destroying work. Once that is clear, the whole staging workflow becomes less intimidating, because every step is reversible except discarding, and discarding is the one step you will now approach with deliberate care.

Check: Does unstaging a file delete your edits? Why not?

Section 17 — What a branch is

A branch is a separate line of work inside the same repository. Think of the project's history as a line of commits, stretching from the first commit to the latest. A branch is a pointer to a particular commit on that line, and when you make new commits on a branch, the pointer moves forward along a new line that diverges from the original. The original line remains, untouched, while the new line grows independently. Later, the two lines can be brought back together by merging, which combines their commits into a shared history. Branches are therefore a way to work in parallel within a single project, without interfering with other lines of work.

The practical benefit is isolation. You can build a new feature on a branch while the main line stays stable and working. If the feature takes days, the main line remains in a state that could be shown or deployed at any time, because the unfinished feature is not in it. If the feature turns out to be a bad idea, you can abandon its branch, and the main line is unaffected, because the abandoned work never entered it. If the feature needs to be paused while you fix an urgent bug, you can switch to the main line, fix the bug, and return to the feature branch later, with its work exactly as you left it. That ability to pause, abandon, and resume work independently is what makes branches indispensable, even for solo developers.

Branches are cheap to create and easy to switch between, because a branch is, in Git's internal terms, just a pointer, not a copy of the whole project. Creating a branch does not duplicate files or history. It simply records a new name for a point in history, and switching branches updates your working folder to match that point. That cheapness is a design feature, not an accident. It encourages developers to create branches freely, for every feature, fix, and experiment, rather than hoarding branches for major work only. A project with many short-lived branches is a healthy project, because each branch represents a small, focused piece of work that can be reviewed and merged independently.

To use branches well, name them clearly, keep them focused on one task, and merge or delete them promptly when their work is done. A branch named after its task, such as a menu feature or a footer fix, tells any reader what it contains. A branch that mixes several tasks is harder to review and harder to merge, for the same reasons that a huge commit is harder to handle. Branches, like commits, reward smallness and clarity, and the habits that make good commits also make good branches, because both are about dividing work into understandable, reversible pieces.

Check: Why build a new feature on a branch instead of directly on the main line?

Section 18 — Creating and switching branches

Creating a branch records a new line of work, and switching to a branch moves your working folder to that line. The two actions are often done together, because you usually want to start working on a new branch immediately after creating it. Git provides a command for each action separately, and a shortcut that combines them. To create a branch without switching to it, you write git branch feature-menu, where the name describes the work the branch will hold. To switch to an existing branch, you write git switch feature-menu, and Git updates your working folder to match that branch's latest commit, provided your current changes do not conflict with the switch.

The combined shortcut is git switch -c feature-menu, which creates the branch and switches to it in one step. The -c flag stands for create, and the command fails helpfully if a branch with that name already exists, so you cannot accidentally overwrite an existing branch by reusing its name. This shortcut is the command most developers use day to day, because the usual workflow is to decide on a new task, create a branch for it, and start working, all in one motion. Learning it first, and learning the separate commands as a fallback, gives you both speed and understanding.

Before creating a branch, check which branch you are currently on, and make sure your working folder is in a sensible state. Creating a branch from the main branch is the usual starting point for a new feature, because it ensures the feature builds on the latest stable work. Creating a branch from an unrelated branch, by mistake, can lead to a branch that contains unexpected changes, and merging it later becomes confusing. If you are unsure where you are, run status, which shows the current branch prominently. If you have uncommitted changes, decide whether to commit them, stash them, or discard them before switching, because switching with uncommitted changes can carry those changes into the new branch, which may or may not be what you intended.

After switching, verify the switch by running status again, and confirm that the branch name matches your intention. That two-second check prevents the common mistake of working on the wrong branch and then having to move commits later, which is possible but fiddly. Branch names should be short, descriptive, and consistent, using words separated by hyphens, such as feature-menu or fix-footer-overlap. A clear name helps you, helps reviewers, and helps future readers of history understand what each branch was for, long after the branch itself has been merged and deleted. After switching, run status again to confirm the branch name. That two-second check prevents working on the wrong branch and having to move commits later.

Check: What command creates a branch and switches to it in one step?

Section 19 — The main branch

Most projects have a main branch that holds the stable, working version of the site. It may be called main, or, in older projects, master, depending on when and how the project was created. The name matters less than the role: the main branch is the version you could show someone at any time, the version that could be deployed without embarrassment, and the version that new work builds on. Feature branches are created from it, developed independently, and merged back into it when their work is done and checked. The main branch therefore grows only through merges of finished, reviewed work, not through direct, unreviewed edits, although solo projects may allow direct commits for small fixes.

Treating the main branch as stable requires discipline. It means not committing experimental or half-finished work directly to it, because that work would then be part of the stable version, even though it is not ready. It means testing and reviewing feature work before merging, so that the main branch does not accumulate broken states. And it means fixing urgent bugs either on a dedicated fix branch that is merged quickly, or, in a solo project, with a small, careful commit directly to main, followed by a test to confirm the fix. The discipline is not about bureaucracy. It is about keeping one line of history that you can always trust, which is invaluable when a deadline arrives or a visitor reports a problem.

The main branch also serves as a shared reference point for a team. When a developer starts a new feature, they start from the latest main branch, so their work builds on the same foundation as everyone else's. When they finish, they merge back into main, and the next developer starts from that updated foundation. That cycle, branch from main, work, merge to main, repeat, is the basic rhythm of collaborative development, and it works only if main is kept stable and up to date. If main is allowed to become broken or stale, every new branch starts from a broken or stale foundation, and the problems compound.

If you work alone, the main branch still matters, for the same reason. It is your own stable reference, the version you know works, to which you can always return. Feature branches let you experiment without risking that reference, and merges let you promote successful experiments into it, one at a time, with a clear record of each promotion. Keep main stable, branch for everything else, and merge only when ready. That simple rule, followed consistently, gives a solo project the same reliability that it gives a team project, and it scales without needing to be relearned. Keep main stable, branch for everything else, and merge only when work is tested. That simple rule gives a solo project the same reliability it gives a team.

Check: What should the main branch always represent?

Section 20 — Merging a branch

Merging brings one branch's commits into another, combining their histories into a shared line. The mechanics are straightforward, but the direction matters, and getting it backwards is a common beginner mistake. To merge a feature branch into the main branch, you first switch to the branch that should receive the work, which is the main branch, and then you run the merge command naming the branch whose work you want to bring in. For a feature branch called feature-menu, the sequence is: switch to main, then run git merge feature-menu. The command reads, in plain language, as "into the current branch, merge the work from feature-menu", and keeping that reading in mind prevents the direction error.

If the two branches changed different parts of the files, Git combines them automatically, creating a new merge commit on the receiving branch that has two parents, one from each line of history. That merge commit is a record of the combination, and its message usually names the branch that was merged, so history shows when and how the lines came together. After a successful merge, the receiving branch contains both its own previous work and the feature branch's work, and the feature branch itself remains, unchanged, until you delete it. Deleting a merged branch, covered in a later section, does not delete its commits, because those commits are now also part of the receiving branch's history.

If the two branches changed the same lines in different ways, Git cannot combine them automatically and reports a conflict, which is covered in the next sections. A conflict is not a failure of merging in general. It is Git's honest admission that a human decision is needed, because two different versions of the same lines cannot both be correct. Merging is therefore a two-stage process: Git combines what it can, and a human resolves what it cannot, by editing the conflicted files into a single correct version and committing the result. Understanding that division, automatic where possible and human where necessary, makes merging feel less mysterious.

Before merging, it is good practice to make sure both branches are up to date and that the feature branch has been tested. Merge the latest main branch into the feature branch first, if the feature branch has been alive for a while, so that it builds on the current main line and any conflicts are resolved on the feature branch, not on main. Then merge the feature branch into main, test the result, and confirm that main still works. That careful order keeps main stable, even when merging, and it is the order that professional teams follow, because it protects the branch that matters most. Test the merged result before moving on. A merge that looks clean in Git can still break the page, and only opening the page will reveal that.

Check: Which branch do you switch to before running git merge feature-menu?

Section 21 — What a merge conflict is

A merge conflict happens when two branches change the same lines of the same file in different ways, and Git cannot tell which version is correct. This is not a sign that you have broken Git, or that you have done something wrong. It is a normal part of collaborative work, and it happens whenever two lines of development diverge on the same lines and then try to come back together. Git's job in a merge is to combine changes automatically where it can, and to stop and ask a human where it cannot. A conflict is Git stopping and asking, in a clear and structured way, for a decision that only a human can make.

To understand why Git cannot decide, consider a stylesheet where one branch changes a heading's colour to blue, and another branch changes the same heading's colour to green. Both changes are valid on their own, and both branches work correctly in isolation. When merging, Git sees two different new values for the same line, and it has no basis for choosing between them. Choosing blue would discard the green change, and choosing green would discard the blue change, and neither choice is obviously correct without knowing the intention behind each change. Perhaps the blue was for a new brand, and the green was a temporary test. Perhaps both were attempts to fix the same contrast problem, and either would work. Only a human who understands the intention can decide, or can combine the changes into a third version that satisfies both intentions.

Git marks a conflict in the file itself, using special markers that show both versions, along with labels indicating which branch each version came from. The file, in its conflicted state, is not valid code, because the markers are not part of HTML, CSS, or JavaScript. That is deliberate. The markers are meant to be seen, read, and removed by the person resolving the conflict, as part of editing the file into its correct final form. Git also lists the conflicted files in status output, so you can see at a glance which files need attention, and it pauses the merge until all conflicts are resolved and the result is committed. The merge does not complete halfway, and it does not silently pick a winner. It waits for you.

The key attitude toward conflicts is calmness. A conflict is information, not an emergency. It tells you exactly where two lines of work disagreed, and it shows you both versions, side by side, so you can make an informed decision. Developers who have resolved many conflicts learn to read the markers quickly, to check the history behind each version if the intention is unclear, and to test the resolved file before committing. With practice, resolving a conflict becomes a routine part of merging, not a feared event, and the fear itself is often the biggest obstacle for beginners.

Check: When does Git stop and ask a human to decide during a merge?

Section 22 — Resolving a conflict

Resolving a conflict is a manual editing task, followed by staging and committing the result. It begins with opening the conflicted file and reading both versions inside the markers. The markers divide the file into sections, each showing one branch's version of the same lines, with labels that tell you which branch each version came from. Your job is to understand what each version was trying to do, decide on the single correct version, and edit the file to contain that version, removing the markers entirely in the process. The correct version might be one of the two originals, a combination of both, or a third version that neither branch contained but that satisfies both intentions. The choice depends on the intention behind each change, and if that intention is unclear, checking the history or asking the author of the other change is the right next step, not guessing.

After editing, read the resolved file as a whole, not just the conflicted section. A resolution can introduce a new problem, such as a duplicated rule, a missing closing tag, or a variable that is now defined twice, if the two versions overlapped in ways that are not visible in the conflict markers alone. For front-end files, this means checking that the HTML is still valid, that the CSS still targets the right elements, and that the JavaScript still runs without errors. Opening the page in a browser and looking at the affected part is a valuable final check, because a resolution that looks correct in code can still look wrong on the page, if a style or a structure was combined incorrectly. The page is the ultimate test of a front-end resolution.

Once the file is correct, stage it, which tells Git that the conflict for that file is resolved, and repeat for any other conflicted files. When all conflicts are resolved and staged, commit the result, which completes the merge. The commit message for a merge resolution is usually generated by Git, naming the branch that was merged, but you can add a note explaining how a tricky conflict was resolved, if that explanation will help a future reader. Never commit a file with the conflict markers still in it. The markers are not valid HTML, CSS, or JavaScript, and a committed file containing them will break the page and confuse anyone who opens it. Checking for leftover markers, by searching the file for the marker characters, is a simple final safeguard before committing.

Resolving conflicts well is a skill that improves with practice, and it is worth practising deliberately on a small project, by creating a conflict on purpose and resolving it, before you encounter one unexpectedly in a team project. A deliberate practice conflict, where you control both versions and know the intended outcome, teaches the mechanics without the pressure of a real deadline. When a real conflict arrives, the mechanics will feel familiar, and you can focus on the decision itself, which is where your judgement matters most.

Check: What must you remove from a file before committing a resolved conflict?

Section 23 — Deleting a merged branch

Once a feature branch has been merged and you no longer need it, delete it to keep the branch list tidy. The command is git branch -d feature-menu, where -d means delete, and Git will only delete the branch if its work has been fully merged into the current branch. That safety check is important. If you try to delete a branch whose commits are not yet merged anywhere, Git will refuse, warning you that the branch is not fully merged, because deleting it would lose work that exists only on that branch. That refusal is Git protecting you from an accidental loss, and you should not override it without understanding why the branch is unmerged and whether its work is truly unwanted.

Deleting a merged branch does not delete its commits, because those commits now also live on the branch they were merged into. A commit in Git is not owned by a single branch. It is a snapshot in history, and branches are pointers to commits. When you merge a feature branch into main, main's history comes to include the feature branch's commits, either directly or through a merge commit that links to them. Deleting the feature branch's pointer therefore removes only the name, not the history. The work remains, reachable through main, and the history still shows the feature's commits, with their messages, in the order they were made. The branch name was a temporary label for a line of work. Once the work is merged, the label has served its purpose.

Keeping the branch list tidy matters for the same reason that keeping a workspace tidy matters. A list of dozens of stale branches, most of them merged long ago, makes it hard to find the branches that are still active, and it creates uncertainty about which branches are safe to delete and which contain unmerged work. A tidy list, with only active branches and the main branch, tells a clear story about what work is in progress and what work is done. Deleting promptly after merging, as part of the same workflow, keeps that story clear without requiring a periodic cleanup effort, which is easy to postpone and hard to do safely when the list has grown large.

There is a related habit for remote branches. When you push a branch to a remote for review, and that branch is later merged and deleted locally, the remote copy may still exist until it is deleted as well. Many hosting services delete remote branches automatically when a pull request is merged, but if yours does not, deleting the remote branch explicitly keeps the remote list tidy too. The principle is the same in both cases: a branch is a temporary workspace for a piece of work, and when the work is done and merged, the workspace should be cleaned up, leaving only the history, which is the permanent record.

Check: Does deleting a merged branch delete its work? Why not?

Section 24 — What a remote is

A remote is another copy of the repository, usually on a hosting service, that your local repository knows about. The usual name for the main remote is origin, which is the default name Git gives to the remote you cloned from, or the remote you connect to when you set up sharing. A remote is not a different kind of repository. It is the same kind of repository, with the same history, branches, and commits, stored on another machine, and your local Git knows its address and can exchange commits with it. That exchange is the basis of collaboration and backup in Git, because it lets many local copies stay in step through a shared remote copy.

Pushing sends your local commits to the remote. When you push, Git compares your local history with the remote's history, sends any commits the remote does not yet have, and updates the remote's branch pointer to match yours, provided the push does not overwrite work that someone else has already pushed. Pulling brings the remote's commits to you. When you pull, Git fetches the remote's new commits and merges them into your current branch, so your local copy catches up with work that others have shared. Pushing and pulling are therefore opposite directions of the same exchange: push is local to remote, pull is remote to local, and together they keep copies in step.

A project can have more than one remote, although most projects have one. A developer might have a remote for the team's shared repository and another remote for a personal copy, or for a fork used for contributing to an open-source project. Each remote has a name and an address, and commands that involve a remote usually name it explicitly, at least the first time, so Git knows which copy to exchange with. Listing remotes, and checking their addresses, is a useful diagnostic when a push or pull fails, because the failure is often due to a wrong address or a missing remote, rather than a problem with the commits themselves.

Understanding remotes clarifies the earlier distinction between Git and hosting websites. Git is the tool that manages local history and exchanges it with remotes. A hosting website provides a remote, along with a web interface for browsing, reviewing, and discussing that history. You can use Git with a remote that is simply another folder on your own network, with no website at all, and the push and pull mechanics work exactly the same way. The website adds collaboration features on top of Git, but the remote itself is just another Git repository, and the exchange between local and remote is Git's core sharing mechanism. List your remotes when a push or pull fails. A wrong address or a missing remote is a common cause, and it is far easier to fix than a problem with the commits themselves.

Check: What is the difference between pushing and pulling?

Section 25 — Cloning a repository

Cloning copies a remote repository onto your computer, including its history. The command is git clone followed by the repository's address, which the hosting service provides for that project, usually as a web address or a secure shell address. When you run it, Git creates a new folder for the project, downloads the full history from the remote, sets up the remote under the name origin, and checks out the main branch into your working folder, so you can start working immediately. Cloning is the starting point for joining an existing project, and it is the opposite of initialising a new project from scratch, which is for starting a project that does not yet exist anywhere.

You clone once at the start, when you first obtain a project, and then you work locally and push and pull from then on. Cloning again for the same project, in a different folder, would give you a second, independent local copy, with its own working folder and its own uncommitted changes, which is rarely what you want and can lead to confusion about which copy holds the latest work. If you already have a local copy, do not clone again to update it. Pull instead, which brings the remote's new commits into your existing copy, preserving your branches and your uncommitted work, subject to the usual rules about conflicts. Clone is for obtaining, pull is for updating, and keeping that distinction clear prevents a common beginner mistake.

The address you clone from matters, because it determines how Git will authenticate when you later push. A web address usually requires a sign-in or a token for pushing, while a secure shell address usually requires a key set up on your computer. Both can work for cloning public projects without authentication, but pushing always requires proof that you are allowed to change the remote. If cloning succeeds but pushing fails with an authentication error, the problem is usually with the address type or the credentials, not with the project itself, and the hosting service's documentation will explain how to set up the correct form for your account. Do not be discouraged by an authentication error on a first push. It is a setup step, not a sign that the project is broken.

After cloning, take a moment to explore the project before editing. Run status to confirm the branch, run log to see recent history, and open the project's documentation to understand its structure and conventions. That brief exploration, which takes minutes, prevents the mistake of editing without understanding the project's layout, and it helps you choose a sensible branch name and a sensible first task. Cloning gives you the whole project, history included. Exploring it first is how you make that gift useful.

Check: When do you clone, and when do you push and pull instead?

Section 26 — Pushing your work

Pushing sends your local commits to the remote, making them available to others and backing them up on another machine. The basic command is git push, which, when your branch already has a remote counterpart, sends any new local commits on that branch to the remote and updates the remote branch to match. The first push of a new branch is slightly different, because the remote does not yet have a branch with that name, and Git does not know where to send the branch or what to call it remotely. Git handles this by printing a message after a failed plain push, suggesting the exact command to use, which names the remote, the local branch, and the remote branch to create. Running that suggested command establishes the link, called upstream tracking, between your local branch and its remote counterpart, and after that, a plain push works as expected.

That suggested command is worth reading, not just copying blindly, because it teaches the structure of pushing. It shows the remote name, usually origin, the local branch name, and the remote branch name, which is usually the same as the local name. Understanding that structure helps when you later push to a different remote, or push a local branch under a different remote name, because you will know which part of the command to change. It also helps when a push fails for a different reason, such as the remote having new commits you do not yet have, because you will recognise that the failure is not about the branch name, but about history being out of step, and the fix is to pull first, as covered in the next section.

Pushing is safe in the sense that it does not change your local work. It only sends copies of commits that already exist locally, and it does not delete or alter your working folder. It is also, in most setups, non-destructive to the remote, because Git will refuse a push that would overwrite commits on the remote that you do not have locally, unless you explicitly force it, which is a dangerous operation that should not be used on shared branches. That refusal is a protection, not an obstacle. It means that if someone else pushed work while you were working, your push will wait until you have pulled their work and combined it with yours, so that no one's commits are lost. Pushing regularly, in small batches, reduces the chance of that refusal, because your local copy stays close to the remote's state.

Make pushing part of your routine, not a rare event at the end of a long project. Push at the end of each work session, or after each finished feature, so that your work is backed up and visible to teammates promptly. A commit that exists only on your laptop is vulnerable to that laptop's failure, and it is invisible to a team that might be waiting for it. Pushing turns a private checkpoint into a shared, backed-up checkpoint, and that transformation is what makes Git a collaboration tool, not just a personal history tool.

Check: Why does Git print a suggested command on the first push of a new branch?

Section 27 — Pulling other people's work

Pulling brings the remote's new commits into your current branch, keeping your local copy up to date with work that others have shared. The command is git pull, which, in its usual form, fetches the remote's new commits and then merges them into your current branch in one step. If the remote's changes do not conflict with your local changes, the merge completes automatically, and your branch now contains both your work and the remote's work, in a combined history. If there is a conflict, Git pauses, as in any merge, and you resolve it by hand, as covered earlier. Pulling is therefore not just downloading. It is downloading plus merging, and the merging part is where care is needed.

Pull before you start work, not only before you push, because starting from the latest version prevents a whole class of problems. If you start from an old version, you may build a feature on top of code that has already changed, or fix a bug that has already been fixed, or use a function that has already been renamed. Your work will then be harder to merge, because it diverges from the current state by more than it needed to. Pulling first ensures your starting point matches everyone else's starting point, so your changes are made against the current reality, not against a stale memory of it. That habit, pull first, work second, is one of the simplest ways to keep collaboration smooth.

Pull again before you push, even if you pulled at the start of the session, because other people may have pushed while you were working. A push that is rejected because the remote has moved ahead is not a failure, but it is a signal that your local copy is now behind, and pulling will bring it up to date and merge the new work with yours. If you pull frequently, each pull brings a small set of changes, which are easy to merge and easy to understand. If you pull rarely, each pull brings a large set of changes, which are harder to merge and harder to review. Frequent pulling, like frequent pushing, keeps the differences between copies small, and small differences are manageable differences.

There is a related command, fetch, which downloads without merging, covered in the next section. Pull is the convenient combined command for daily use, when you want to update and continue working immediately. Fetch is the cautious command for when you want to inspect remote changes before they touch your work. Knowing both, and choosing pull for routine updates and fetch for careful inspection, gives you control over how remote work enters your local project, rather than leaving it to chance or to habit alone.

Check: Why pull before you start work, not only before you push?

Section 28 — Fetch versus pull

Fetching and pulling are related but different, and confusing them is a common source of surprises. Fetching downloads the remote's new commits into your local repository, updating your knowledge of what the remote contains, but it does not merge those commits into your current branch, and it does not change your working files at all. After a fetch, your working folder looks exactly as it did before, but Git now knows about the remote's new commits, and you can inspect them, compare them with your own work, and decide when and how to bring them in. Fetch is therefore a read-only update to your knowledge, not a change to your work.

Pulling, by contrast, fetches and then merges in one step. It downloads the remote's new commits and immediately merges them into your current branch, updating your working files to reflect the combined result. That convenience is valuable for routine updates, when you trust the remote's changes and want to continue working without delay. It is also the source of surprises, when the merge introduces a conflict or a change you did not expect, and you must deal with it immediately, in the middle of your work, rather than at a moment you chose. Pull trades control for convenience, and that trade is usually worth it, but not always.

Fetch is the cautious option when you want to see what changed before it touches your files. Perhaps you are in the middle of a delicate change, and you do not want a merge to interrupt it. Perhaps you have heard that a large change was pushed, and you want to read its diff before merging it. Perhaps you are about to start a new feature, and you want to check whether the main branch has moved, without yet merging that movement into your feature branch. In each case, fetching first lets you inspect, plan, and choose the right moment to merge, rather than having the merge forced on you by a pull. After fetching, you can merge explicitly, when you are ready, with a clear understanding of what you are merging.

In practice, most developers use pull for daily updates and fetch for inspection, and they switch between the two as the situation demands. The key is to know which one you are running and what it will do to your working files. A fetch never changes your files. A pull always can. Remembering that single distinction prevents the most common fetch-versus-pull confusion, which is running a pull, seeing your files change unexpectedly, and wondering where the change came from. It came from the remote, via the merge that pull performs automatically, and fetching first would have let you see it coming.

Check: What does fetch do that pull does not, and vice versa?

Section 29 — What a pull request is

A pull request is a proposal, on a hosting service, to merge one branch into another, with a place for discussion and review. It is not a Git command, and you will not find it in Git's own documentation, because it is a workflow built on top of Git by hosting services, not a part of Git itself. The name comes from the action it proposes: please pull my branch's commits into the target branch. The request includes the branch to merge from, the branch to merge into, a title and description explaining the change, and a diff showing exactly what would change if the request were accepted. Reviewers read that diff, leave comments on specific lines, ask for changes, and approve the request when they are satisfied.

The value of a pull request is that it creates a structured moment for review before work enters a shared branch. Without it, a developer could push directly to the main branch, and their work would become part of the stable version without anyone else having seen it. With a pull request, the work is visible, in a branch, before it is merged, and at least one other person has a chance to check it, suggest improvements, or catch a problem. That check is especially valuable for front-end work, where a change that looks correct in code can still look wrong on the page, and a reviewer who opens the page and looks at it can catch what a code-only review would miss. Many hosting services also run automated checks on pull requests, such as tests or linting, and report the results in the request, so that a failing check blocks merging until it is fixed.

A good pull request description explains what changed, why it changed, and how to test it. It does not merely repeat the commit messages, although it may summarise them. It gives the reviewer context: what problem this change solves, what alternatives were considered, what parts of the page are affected, and what the reviewer should look at when testing. A description that says "Fix menu overlap on small screens, caused by a fixed width, test by opening the menu on a narrow viewport" gives a reviewer everything they need to review efficiently. A description that says only "Fixes" gives them nothing, and the review will be slower and less thorough as a result.

Pull requests also serve as a record of decisions. After a request is merged, its discussion remains, showing what was debated, what was changed in response to review, and why the final version looks as it does. That record is valuable when a future developer wonders why a particular choice was made, because the discussion often contains the reasoning that the code alone does not show. Treating pull requests as a place for genuine discussion, not just as a gate to pass through, makes them a learning tool for the whole team, not just a quality check.

Check: Is a pull request a Git command? What is it instead?

Section 30 — Reviewing a pull request

Reviewing a pull request means reading its diff carefully, checking it against the project's standards, and leaving specific, helpful comments. A good review starts with the description, to understand what the change is trying to do, and then moves to the diff, reading it line by line, with the project's pages in mind. For front-end work, three checks deserve particular attention. First, does the HTML stay valid and well-structured, with real headings, labelled form fields, and sensible nesting, or has a change broken the structure that styles and scripts rely on? Second, does the CSS affect only the intended pages and elements, or does a broad selector or a changed shared rule shift layouts elsewhere? Third, does the script handle the cases users will actually encounter, including empty input, missing data, and rapid clicks, or does it assume a perfect path that real users will not always follow?

Beyond those three, a reviewer should check for the kinds of problems that are easy to miss in one's own work but easy to spot in another's. Is there leftover debugging code, such as a console log or a test value, that should not be merged? Are there accessibility regressions, such as a removed label, a colour with poor contrast, or a button that is no longer reachable by keyboard? Is the change consistent with the project's existing patterns, or does it introduce a new pattern without a reason? Does the commit history make sense, with clear messages and sensibly sized commits, or is it a single huge commit that should have been split? Each of these questions is a chance to improve the change before it becomes part of the shared branch, where fixing it will be more expensive.

Comments should be specific and tied to lines. A comment that says "This selector is too broad and will affect the footer as well, consider scoping it to the menu" is actionable. A comment that says "This looks wrong" is not, because the author cannot tell what is wrong or how to fix it. Tone matters too. A review is a collaboration, not a judgement, and comments phrased as questions or suggestions, with reasons, are more likely to be received well and to lead to a better result than comments phrased as orders. The goal is a better change, not a demonstration that the reviewer found a flaw.

Finally, approve only when you would be happy for the code to go live. Approval is not a rubber stamp for a colleague's effort. It is a statement that you have checked the change and believe it is ready. If you have not checked it, because you are busy or because the change is outside your expertise, say so, and ask someone else to review it, rather than approving blindly. A careful review, even a brief one, is one of the highest-value activities in a team, because it catches problems while they are still cheap to fix, and it spreads knowledge about the codebase to more than just its author.

Check: Name three things to check when reviewing a front-end pull request.

Section 31 — Keeping branches short-lived

A branch that lives for weeks will drift far from the main branch, and merging it back becomes painful. The reason is simple: while the branch is alive, the main branch keeps moving, as other developers merge their work into it. The longer the branch lives, the more the main branch changes, and the more the branch's starting point differs from the current main line. When the branch is finally merged, the merge must reconcile weeks of divergence, including changes to the same files, the same styles, and the same scripts, made by different people for different reasons. The result is often a large, difficult merge, with many conflicts, and a high chance that something breaks in the combination, even if each branch worked perfectly on its own.

Short-lived branches avoid this drift by design. A branch that is created, worked on for a day or two, reviewed, and merged promptly, diverges only slightly from main, because main has not had time to move far in the meantime. Its merge is small, its conflicts, if any, are few and easy to resolve, and its review is focused, because the change is a single, recent idea. The branch's author still remembers why each change was made, which makes resolving conflicts and answering review comments easier. The team benefits too, because work reaches the main branch quickly, where it can be tested together with other recent work, rather than sitting in isolation until a large, risky merge day arrives.

Keeping branches short requires breaking work into small pieces, which is a planning skill as much as a Git skill. A large feature should be divided into steps, each of which can be merged independently, even if the feature as a whole is not yet complete. For example, a new navigation menu might be built in steps: first the HTML structure, then the basic styles, then the responsive behaviour, then the interactive script, each on its own short branch, merged in order. Each step leaves main in a working state, because the menu is not yet visible or is hidden until the final step, and each step is reviewable on its own. That incremental approach feels slower at first, because it involves more branches and more merges, but it is faster overall, because it avoids the large, painful merge at the end.

If a branch must live longer, because its feature genuinely takes weeks, pull the latest main branch into it regularly, perhaps every day or two, so that it stays close to main and conflicts are resolved in small batches as they arise, rather than all at once at the end. Regular updates turn a large merge into a series of small merges, each manageable on its own. The principle is the same in both cases: keep the distance between a branch and main small, and merging stays easy. Let the distance grow, and merging becomes a project in itself.

Check: Why do long-lived branches make merging harder?

Section 32 — What .gitignore does

A file called .gitignore lists paths that Git should never track, one per line, such as a build output folder, a folder of downloaded libraries, or a file holding local settings. When Git checks status, it skips ignored files entirely, so they do not appear as untracked, and they will not be staged by a broad staging command or committed by accident. The ignore file itself is part of the repository, once you commit it, so every developer who clones the project shares the same ignore rules, and the same files are ignored on every machine. That sharing is important, because ignore rules are project decisions, not personal preferences, and they should be consistent across the team.

The ignore file uses patterns, not just exact file names, which makes it flexible. A pattern for a folder ignores everything inside that folder. A pattern for an extension ignores every file with that extension, wherever it appears. A pattern with a wildcard can ignore files matching a shape, such as all files ending in a particular suffix. Learning the basic pattern forms, one per line, folder with a trailing slash, extension with a leading star, and exact name for a single file, covers most daily needs. More complex patterns exist, but they are rarely needed for a front-end project, and a simple, readable ignore file is better than a clever one that nobody understands.

A common misunderstanding is that ignoring a file deletes it or hides it from the project. It does neither. Ignored files remain in your working folder, fully usable, and Git simply pretends, for tracking purposes, that they are not there. A build output folder, for example, still exists on disk and still contains the built site, which you can open and test. Git just does not track it, because it can be rebuilt from the source files, and tracking it would clutter history with generated files that change on every build. Ignoring is about what belongs in history, not about what belongs on disk.

Another misunderstanding is that adding a file to the ignore file will untrack a file that is already tracked. It will not. Once a file has been committed, Git tracks it regardless of ignore rules, until it is explicitly removed from tracking. To stop tracking a file that should have been ignored, you must remove it from Git's tracking while keeping it on disk, a separate step that should be done carefully, and then the ignore rule will prevent it from being re-added. That distinction, ignore prevents future tracking, removal stops existing tracking, is worth learning early, because it explains why an ignore rule sometimes seems not to work for a file that is already in history.

Check: What problem does .gitignore solve?

Section 33 — Front-end files to ignore

Front-end projects usually ignore three kinds of files, each for a clear reason. The first is generated output: files produced by a build tool from your source files, such as a bundled script, a compiled stylesheet, or a folder containing the built site ready for deployment. Those files can be rebuilt at any time from the source, by running the build, so tracking them in Git adds no information and much noise. Every build would create a large diff, even if the source changed only slightly, and history would fill with generated files that nobody reviews and nobody needs to keep. Ignoring generated output keeps history focused on source, which is the part humans write and review.

The second kind is downloaded libraries: folders managed by a package manager, containing code written by others. Those folders are large, they change when libraries are updated, and they can be recreated exactly from the package file by running the install command. Tracking them would make the repository huge, make cloning slow, and make diffs unreadable, because a library update would appear as thousands of changed lines in files you did not write. Ignoring the downloaded folder, and tracking only the package file that describes it, gives every developer the same libraries, without the cost of storing them in history. The package file is the source of truth. The downloaded folder is a reproducible consequence of it.

The third kind is local and secret files: environment files that hold keys or tokens for your machine, editor configuration that is personal rather than shared, and operating system files that appear in folders automatically. Secrets must never be committed, for reasons covered in the next section, and ignoring them is a first line of defence, although it is not a guarantee, because a file that is already tracked will not be ignored, and a broad staging command can still add a file if its ignore rule is missing. Local files, such as personal editor settings, should be ignored because they differ from developer to developer and have no place in a shared history. A shared editor configuration, if the team agrees on one, can be tracked deliberately, but personal settings should not.

When you start a project, create an ignore file early, with entries for these three kinds, and commit it as one of your first commits. That early step prevents the common problem of accidentally committing a large generated folder or a downloaded library folder in the first, enthusiastic commit, and then having to remove it from history later, which is a more difficult operation. An ignore file is a small, boring file that saves a great deal of trouble, and it is one of the first signs of a well-set-up front-end project.

Check: Why should generated files stay out of the repository?

Section 34 — Never commit a secret

Passwords, private keys, and secret tokens must never be committed to a Git repository, even a private one, because history keeps them and a repository can later be shared, copied, or made public. A commit is not a temporary note. It is a permanent record, stored in the hidden history, copied to every clone, and pushed to every remote. Deleting a file in a later commit does not delete it from history. The earlier commit, containing the secret, remains in the history, reachable by checking out that commit or by searching the log, unless history is deliberately rewritten, which is a difficult and disruptive operation on shared work. A secret that has been committed should therefore be treated as exposed, even if the repository is private today, because private today does not guarantee private tomorrow.

The consequences of an exposed secret depend on what the secret protects, but they are never trivial. A key for a paid service can be used by anyone who finds it, running up charges on your account. A token for a hosting service can be used to change or delete your projects. A password for a database can be used to read or alter data. In each case, the damage is done not by the act of committing, but by the fact that the secret is now in a place where it can be found, copied, and used by someone who was never meant to have it. The longer the secret remains in history, the greater the chance that it will be found, because repositories are copied, backed up, and shared in ways that are hard to track completely.

If a secret is committed by mistake, the correct response has two parts, and the order matters. First, revoke the secret and replace it, so that the exposed value no longer works, regardless of who may have copied it. Contact the service, revoke the key or change the password, and issue a new one, stored only in a safe place, such as a server's protected configuration, never in the repository. Second, remove the secret from the repository's history, if possible, and from any copies you control, understanding that copies you do not control may still contain it, which is why revocation, not just removal, is the essential step. Removing without revoking leaves a working secret in the hands of anyone who already copied it.

Prevention is far easier than recovery. Use an ignore file to exclude secret files from tracking, use environment variables or server-side configuration for secrets in real projects, and use test keys and test accounts during development, where the exposure of a test value causes no harm. Before committing, check status and the staged diff for any file that might contain a secret, and treat any doubt as a reason to pause and check. That brief pause, repeated as a habit, is the cheapest security measure a front-end developer can adopt, and it protects not only your own accounts but also the accounts of anyone whose services your project touches.

Check: Why is deleting a committed secret file not enough?

Section 35 — Stashing work in progress

Stashing temporarily shelves your current changes so you can switch branches with a clean folder, and then brings them back when you are ready. The command git stash takes your modified and staged changes, stores them in a stash, which is a kind of temporary, unnamed commit, and restores your working folder to the last committed state. You can then switch branches, fix an urgent bug, or pull new work, without your in-progress changes getting in the way or being accidentally committed to the wrong branch. When you return, git stash pop restores the stashed changes into your working folder and removes the stash, so you can continue where you left off.

Stashing is for short interruptions, not for storage. A typical use is when you are halfway through a feature, an urgent fix arrives, and you need to switch to the main branch immediately, but your feature work is not ready to commit. Stashing lets you park the feature work, switch, fix, and return, all without committing half-finished work to the wrong branch or losing it. Another use is when you need to pull new work, but your uncommitted changes would conflict with the pull. Stashing, pulling, and then popping the stash lets the pull complete cleanly, and any conflicts between your stashed work and the pulled work can be resolved after the pull, in a controlled way.

The danger of stashing is forgetting. A stash has no branch name, no descriptive message unless you give it one, and no visible presence in status output, except for a count of stashes if you ask for it. A stash left for weeks is easy to forget, and when you finally remember it, you may no longer remember what it contained or why it was stashed, and applying it to a project that has moved on may cause conflicts that are hard to understand. For that reason, treat a stash as a parking spot for an interruption that will last hours, not weeks. Return to it promptly, pop it, and either commit its work properly or discard it deliberately, so that no work lingers in an unnamed, invisible store.

A safer alternative, for work that will take more than a short interruption, is to commit it on its own branch, with a clear message such as "Work in progress", as covered in a later section. A commit on a branch is visible, named, and easy to find, while a stash is invisible and easy to forget. Stashing has its place, for quick switches and urgent interruptions, but it should not become a habit for storing work. Use it briefly, return promptly, and let branches and commits, not stashes, be the home for work that matters.

Check: When would you stash, and why should you not leave a stash for weeks?

Section 36 — Reverting a commit safely

Reverting creates a new commit that undoes an earlier commit, leaving the history intact. It is the safe way to undo work that has already been shared, because it does not rewrite history, it adds to it. Suppose a commit added a feature that turns out to be unwanted, after it has been pushed and other developers have based work on it. Reverting that commit creates a new commit whose changes are the exact opposite of the original: lines added by the original are removed, lines removed are restored, and the project returns to the state it was in before the original, but through a forward step in history, not by erasing the past. The original commit remains in history, visible and intact, and the revert commit sits after it, explaining, in its message, why the original was undone.

The safety of reverting comes from the fact that it does not change any commit that other people already have. In a distributed system, every developer's copy contains the same commits, identified by the same hashes. If you rewrite history by removing or altering a commit, your copy will diverge from everyone else's copy, and combining them again becomes confusing, because the same logical change now has different identities in different copies. Reverting avoids that divergence entirely, because it leaves all existing commits untouched and adds a new one that everyone can receive by pulling, just like any other commit. Other developers pull the revert, their history gains the new commit, and their working folders update to the reverted state, without any conflict over rewritten history.

Reverting is not always automatic. If later commits have changed the same lines that the original commit touched, the revert may conflict, because Git cannot tell how to undo the original without also undoing the later work. In that case, you resolve the conflict by hand, as with a merge, deciding which parts of the later work to keep and which parts of the original to undo. That manual step is a sign that the project's history has moved on since the original, and the revert needs human judgement about what the correct final state should be. It is still safer than rewriting history, because the conflict is resolved in a new commit, visible to everyone, rather than by altering the past in a way that others must then reconcile.

Use reverting as the default undo for shared work, and reserve history rewriting for local work that has not yet been shared, as covered in the next section. When you revert, write a clear message explaining why, not just that you reverted. A message such as "Revert menu animation, which caused flicker on small screens, to be revisited" tells a future reader why the feature disappeared, and prevents someone from re-adding it without addressing the flicker. History, including reverts, is a record of decisions, and a revert with a reason is a decision recorded well.

Check: Why is reverting safer than rewriting history for shared work?

Section 37 — Resetting is for local work only

Resetting moves your branch pointer back to an earlier commit, effectively discarding commits after that point from the branch's history, at least as far as that branch is concerned. The commits themselves are not immediately deleted from Git's storage, but they are no longer reachable from the branch, and they will eventually be cleaned up if nothing else points to them. Resetting is a powerful tool for tidying local work, because it lets you undo a series of commits that turned out to be wrong, and start again from a clean point, without leaving a trail of revert commits in history. For work that exists only on your computer, that tidiness is valuable, because the history you eventually share will be clean and readable, without the false starts that are a normal part of development.

The risk arises when resetting is used on shared work. If you have pushed commits to a remote, and other developers have pulled them and based work on them, resetting your branch and pushing the rewritten history will create a divergence. Other developers' copies still contain the original commits, while your copy now contains a different history, and Git will struggle to combine them, because the same logical work now has different identities. The usual result is confusion, duplicate commits, or lost work, as developers try to reconcile histories that no longer match. For that reason, resetting shared branches is strongly discouraged, and most teams forbid it on the main branch entirely, preferring reverting, which adds history rather than rewriting it.

The distinction is therefore not about the command itself, but about whether the work has been shared. If you made a commit locally, realised it was wrong, and have not yet pushed it, resetting is safe, because no one else has the commit to be confused by its disappearance. You can reset, fix the work, and commit again, and the shared history will never know about the mistake. If you have pushed, the commit is shared, and resetting is risky, because the mistake is now part of other people's histories as well. In that case, revert instead, as covered in the previous section, and accept the extra commit in history as the price of safety.

A practical rule to remember is: reset locally, revert publicly. If in doubt about whether work has been shared, assume it has been, and revert. The cost of an extra revert commit in history is small. The cost of a rewritten shared history, with confused teammates and potentially lost work, is large. That asymmetry makes the conservative choice the right one, even when resetting feels tidier. Tidiness in history is valuable, but it is never worth breaking the shared record that a team relies on.

Check: When is resetting acceptable, and when is it risky?

Section 38 — Tagging a release

A tag is a fixed label on a commit, marking a version you can find again later. Unlike a branch, which moves forward as new commits are added to it, a tag stays pinned to the commit it was created on, forever, unless it is deliberately moved, which is rare and discouraged for release tags. That permanence is the point. A branch answers the question "what is the latest work on this line?" A tag answers the question "what exactly was the version we released, or the version we tested, at that moment?" When a user reports a bug in a particular release, you can check out its tag and see exactly the code that was in that release, without guessing which commits were included.

Tags are commonly used for releases, with names that describe the version, such as a simple release name or a number that increases with each release. The naming convention matters less than consistency, because the purpose is to make releases easy to find and easy to refer to in conversation. A team can say "the bug appeared in the second release, but not the first" and everyone can check out the corresponding tags and compare them, without needing to remember commit hashes or dates. Tags turn a point in history into a named landmark, which is far easier to discuss than an anonymous commit.

Creating a tag is a simple command that names the tag and, usually, the commit it should point to, which defaults to the current commit if no commit is given. Tags can also carry a message, explaining what the release contains, which is useful for annotated tags that are meant to be shared and discussed. Once created, a tag can be pushed to a remote, like a branch, so that other developers and users can see it and check it out. Pushing a tag does not push a branch, and it does not change any branch's history. It simply shares the label, so that the landmark is visible to everyone.

The difference between a tag and a branch is therefore about movement. A branch moves, because it represents ongoing work. A tag does not move, because it represents a fixed moment. Confusing the two leads to mistakes, such as creating a branch when you meant to mark a release, and then watching the branch move away from the release it was meant to mark, or creating a tag when you meant to start new work, and then being surprised that new commits do not appear under the tag. Keep the roles clear: branch for work in progress, tag for a version worth remembering, and history will be both a workspace and a record.

Check: How does a tag differ from a branch?

Section 39 — Reading a diff for styles and markup

Front-end diffs deserve a visual check, not just a code read, because front-end code describes what a user sees, and what a user sees cannot always be judged from lines alone. A small CSS change, such as altering a width, a margin, or a selector, can shift a whole layout, push an element off screen, or change the appearance of pages that share the same stylesheet. A small HTML change, such as renaming a class, moving an element, or removing a wrapper, can break a selector, break a script that queries for that element, or break an accessible label that a screen reader relied on. Reading the diff tells you what changed in code. Opening the page tells you what changed for the user, and both are necessary for a confident review.

The visual check should be deliberate. Open the affected pages, in the viewport sizes that matter, such as a narrow phone width and a wider desktop width, and look at the parts the diff touches. Does the layout still hold together? Does text remain readable, with enough contrast and sensible line lengths? Do interactive elements still work, and are they still reachable by keyboard as well as by mouse? Does the change affect other pages that share the same styles, in ways the diff alone did not make obvious? Those questions cannot be answered by reading lines, because lines do not show layout, contrast, or interaction. They can only be answered by looking at the rendered result, which is what users will actually experience.

A useful habit is to compare before and after visually, not just to look at the after state in isolation. If you have the old version available, through a branch or a stash, open it side by side with the new version, or switch between them, and look for differences beyond the intended change. An unintended shift in spacing, a colour that changed on an unrelated element, or a menu that now behaves differently, are all signs that a shared rule was affected, or that a selector was broader than intended. Catching those unintended changes in review, before merging, is far cheaper than catching them after deployment, when users have already seen them.

For a solo developer, the visual check is self-review, and it is just as important as it is in a team. It is easy, when working alone, to commit a change that looks correct in code and to move on, without opening the page. Resist that temptation. The page is the product, and the diff is only a description of a change to it. A few minutes spent looking at the page, after reading the diff, will catch problems that hours of code reading would miss, and it will build the habit of judging front-end work by its result, not just by its lines.

Check: Why is reading a CSS diff not enough on its own?

Section 40 — Committing a work-in-progress honestly

If you must stop work mid-feature, commit the partial work on its own branch with a clear message, rather than leaving it uncommitted or committing it vaguely to the main branch. A work-in-progress commit is an honest checkpoint: it says, in its message, that the work is not finished, what part is done, and what remains. A message such as "Work in progress: menu layout started, styles and script still to do" tells any reader, including future you, exactly where the work stands, and it prevents the mistake of assuming the branch contains a finished feature when it does not. Honesty in the message is the key, because a vague message on a partial commit misleads in the same way that a vague message on a finished commit does, only worse, because the reader may build on unfinished work believing it to be complete.

Work-in-progress commits belong on their feature branch, not on the main branch, because the main branch should remain stable and working, as covered earlier. A partial feature on the main branch would break that stability, even if the partial code does not crash, because it would present an unfinished feature to anyone who checks out main, and it would make main's history harder to understand. On a feature branch, a partial commit is harmless, because the branch is not merged until the feature is finished, and the series of commits on the branch, including the work-in-progress ones, tells the story of how the feature was built, step by step. When the feature is finished, the branch can be merged, and the work-in-progress commits become part of that story, or they can be tidied into cleaner commits before merging, if the team prefers a tidy history.

The benefit of committing work in progress, rather than leaving it uncommitted, is safety. Uncommitted work exists only in your working folder, vulnerable to accidental discarding, to a mistaken branch switch, or to a computer failure. Committed work, even on a branch, is recorded in history and can be recovered, pushed for backup, and returned to after a break. A developer who stops for the day with uncommitted work risks losing it or forgetting its state. A developer who stops with a work-in-progress commit can return the next day, read the message, check status, and continue confidently, knowing exactly where they left off.

Make work-in-progress commits a routine part of stopping work, not a sign of failure. Stopping mid-feature is normal. Features take longer than a single session, interruptions happen, and priorities change. The professional response is not to force a feature to finish in one session, but to leave it in a clear, recorded state, on its own branch, with an honest message, so that it can be resumed, handed over, or abandoned without confusion. That clarity is a kindness to your future self and to any teammate who might pick up the work.

Check: Where should work-in-progress commits live, and how should they be labelled?

Section 41 — Using history to find a bug

When a page breaks and you do not know why, the history can narrow the search dramatically, turning a vague problem into a specific change. The method is simple in principle: find the last commit where the page worked correctly, then look at the commits after it, in order, until you find the first commit where the page breaks. That commit is the one that introduced the bug, or at least the one after which the bug first appears, and its diff points straight at the cause, or at least at the small set of lines that changed between working and broken. Instead of searching the whole project for a bug, you search a single diff, which is a far smaller and more focused task.

In practice, finding the last working commit can be done by checking out older commits and testing the page, or by reasoning from history: when did you last see the page working, what commits have landed since then, and which of them touched the relevant files? If the history is made of small, clear commits, this reasoning is easy, because each commit's message tells you what it changed, and you can quickly rule out commits that touched unrelated files. If the history is made of huge, mixed commits, the reasoning is harder, because a single commit may have touched many files, and the bug could be in any part of it. That is another reason why small commits matter: they make history a useful debugging tool, not just a record.

Once you have identified the suspect commit, read its diff carefully, and ask what in that diff could cause the symptom you see. A changed selector might have broken a style. A renamed class might have broken a script. A changed calculation might have produced a wrong value. A removed element might have removed a label or a hook that other code relied on. The diff, combined with the symptom, usually suggests a hypothesis, which you can test by reverting the suspect change temporarily, or by fixing it and checking whether the symptom disappears. That hypothesis-and-test loop, guided by history, is far faster than guessing at random or rewriting code in the hope that the bug will vanish.

There are tools that automate this search, by testing commits systematically and narrowing the range automatically, but the manual method teaches the underlying idea, which is what matters for a beginner. History is not just a record of what happened. It is a tool for understanding what went wrong, by comparing working and broken states and finding the change between them. Use it whenever a bug's cause is unclear, before you start changing code at random, and you will often find that the bug was introduced by a change you had forgotten, in a file you had not considered, and that the diff tells you exactly how to fix it.

Check: How does commit history help you find when a bug was introduced?

Section 42 — Keeping your working folder clean

A working folder cluttered with untracked test files, screenshots, stray notes, and experimental copies makes Git harder to use, because status output becomes noisy and real changes become hard to spot. When you run status, you want to see, at a glance, the files you have deliberately changed and intend to commit. If that list is buried among dozens of scratch files, you must read carefully to find the real changes, and you are more likely to miss one, or to stage a scratch file by mistake with a broad staging command. Cleanliness in the working folder is therefore not tidiness for its own sake. It is a practical aid to accurate staging, committing, and reviewing.

The solution is to tidy or delete scratch files promptly, or to ignore the folders they live in, so that status output shows only work that matters. Scratch files that are useful for a short experiment should be deleted when the experiment ends, or moved to a folder that is ignored by Git, so they do not pollute status. Screenshots taken for a bug report should be stored outside the project, or deleted after the report is filed, rather than left in the project folder. Notes about a task should live in the task tracker or in the project's documentation, not as stray text files in the working folder, unless they are deliberately part of the project. Each of these habits keeps the working folder focused on the project's source files, which are the files Git is meant to track.

A related habit is to keep generated files out of the working folder's tracked set, by ignoring their folders, as covered earlier. Generated files are not scratch, because they are produced by the build, but they clutter status in the same way if they are not ignored, because every build changes them and they appear as modified or untracked on every status check. Ignoring them removes that noise entirely, so status shows only source changes, which are the changes a human needs to review. The principle is the same in both cases: status should show human work, not machine output or temporary artefacts.

Make cleaning part of your routine, not a periodic chore. At the end of a work session, before committing, run status, look at the untracked list, and ask of each file whether it belongs in the project, should be ignored, or should be deleted. That minute of tidying, repeated daily, keeps the folder clean without requiring a large cleanup effort later, when the clutter has grown and the decision about each file has become harder to make. A clean working folder makes Git's output clear, and clear output makes every other Git operation easier and safer.

Check: Why does a cluttered working folder make Git harder to use?

Section 43 — One repo per project

Each project should get its own repository, with its own history, branches, and purpose. Putting unrelated projects in one repository mixes their histories, makes cloning heavy, and makes it hard to share one project without sharing the others. Suppose you put a personal blog, a shop site, and a small game in one repository, because they all live on your computer and it felt convenient to track them together. Cloning that repository to work on the blog would also download the shop and the game, with all their histories, even though you only need the blog. Sharing the blog with a collaborator would also share the shop and the game, which you may not want to share, and which the collaborator does not need. The convenience of one repository becomes a cost in every other operation.

Separate repositories also keep histories meaningful. The history of a blog should tell the story of the blog: its posts, its design changes, its fixes. If that history is mixed with the shop's history, scanning it becomes confusing, because blog commits and shop commits interleave, and finding when a blog feature was added requires filtering out shop commits. Branching becomes confusing too, because a branch for a blog feature and a branch for a shop feature live in the same branch list, and merging one project's branch into a shared main branch makes no sense, because there is no shared main line that both projects belong to. Each project deserves its own main branch, its own feature branches, and its own history, unmixed with other projects' stories.

There are cases where projects are genuinely related, such as a front-end site and its shared component library, or a site and its documentation, where a single repository, sometimes called a monorepo, is a deliberate choice with tooling to support it. That choice is different from accidentally putting unrelated projects together out of convenience, because it is made consciously, with a structure that separates the related parts while sharing tooling and history where it helps. For a beginner, and for most personal and team projects, the simple rule is the right one: one project, one repository. If you later encounter a genuine monorepo, you will recognise it as a deliberate, tool-supported exception, not as a default to copy without understanding.

Starting a new repository for a new project takes a minute, with git init and a first commit, and that minute buys clarity for the whole life of the project. It keeps cloning light, sharing safe, history readable, and branching sensible. It also makes it easy to archive or delete a project when it is finished, without affecting other projects, because each project's history lives in its own repository, independent of the others. One repo per project is a small discipline with large, lasting benefits.

Check: Why not put three unrelated projects in one repository?

Section 44 — Collaborating without overwriting

Two people can edit the same project safely if they follow a small set of habits that keep their work separate until it is ready to combine. The first habit is working on separate branches. Each developer creates a branch for their task, works on that branch, and merges it back through a pull request, rather than editing the main branch directly. Separate branches mean that two developers' edits do not immediately collide in the same files, because each branch has its own line of history, and collisions, when they occur, happen at merge time, where they can be resolved deliberately, rather than as silent overwrites in a shared working folder.

The second habit is pulling often. Each developer pulls the latest main branch before starting work, and pulls again before pushing, so their work builds on the current state, not on a stale version. Frequent pulling keeps the distance between a branch and main small, which keeps merges small and conflicts rare, as covered earlier. A developer who pulls once a week will face a large merge at the end of the week. A developer who pulls daily will face a series of small merges, each easy to handle. The habit costs seconds and saves hours.

The third habit is committing small changes, with clear messages. Small commits are easier to merge, easier to review, and easier to revert, as covered throughout this chapter. In collaboration, they have an additional benefit: they make it clear, to a teammate reading history, what each change was for, so that a teammate who encounters a conflict can understand the intention behind the conflicting change and resolve it correctly. A large, vague commit, by contrast, leaves a teammate guessing about intention, which makes conflict resolution slower and more error-prone.

The fourth habit is resolving conflicts promptly. When a merge or pull reports a conflict, resolve it immediately, while the work is fresh in your mind and the other developer is available to discuss it if the intention is unclear. Leaving a conflict unresolved, or working around it by committing a file with markers still in it, creates a broken state that affects everyone who pulls that branch. Prompt resolution, with a test of the resolved result, keeps the shared branches working and keeps the team moving. Together, these four habits, separate branches, frequent pulling, small commits, and prompt conflict resolution, turn collaboration from a source of overwrites into a routine, reliable process. Together, these habits turn collaboration from a source of overwrites into a routine process. They cost little time individually, but they save hours of lost work and difficult merges over the life of a project. Prompt resolution also keeps trust high, because teammates see that shared branches are kept working.

Check: What four habits keep collaboration from turning into overwrites?

Section 45 — A daily Git routine

A steady routine covers most daily Git needs, and following it consistently makes Git feel routine rather than mysterious. The routine begins with pulling the latest work. Open the project, switch to the main branch or to your feature branch, as appropriate, and pull, so you start from the current state. Then check status, to see whether your working folder is clean or whether there are uncommitted changes from a previous session that need attention. Starting with pull and status gives you a clear picture of where you stand before you change anything, which prevents the common mistake of building on a stale or messy foundation.

Next, work on a branch. If you are starting a new task, create a branch for it, with a clear name, and switch to it. If you are continuing a task, switch to its existing branch. Work in small steps, editing, testing, and checking the page as you go. When a step is complete, stage and commit it, with a clear message, and read the staged diff before committing, to confirm that the commit contains exactly what you intended. Repeat that cycle, work, stage, commit, for each step, so that history grows as a series of small, understandable checkpoints, rather than as a single large change at the end of the day.

When the work is ready, push it, and open a pull request if you are working in a team, with a description that explains what changed, why, and how to test it. If you are working alone, pushing still backs up your work and makes it available from another machine. After pushing, check status again, to confirm that your working folder is clean or contains only work you deliberately left uncommitted, and that your branch is in step with its remote counterpart. That final check closes the day's work cleanly, so that tomorrow starts, as today did, with pull and status, on a clean foundation.

This routine, pull, status, branch, work, stage, commit, review, push, and, in a team, pull request, may sound long when listed, but in practice each step takes seconds or minutes, and the whole routine becomes automatic within a few weeks of daily use. The value is not in any single step, but in the consistency. A developer who follows the routine daily rarely encounters Git surprises, because surprises come from skipped steps: working on the wrong branch, committing without checking the diff, or pushing without pulling. The routine prevents those skips by making each step a habit. The routine may look long when listed, but each step takes seconds or minutes, and within a few weeks of daily use it becomes automatic, preventing the skipped steps that cause most Git surprises.

Check: List the steps of a daily Git routine in order.

Section 46 — What Git does not do

Git tracks files and history. It does not deploy your site, run your tests, or back up your computer, and expecting it to do those jobs leads to confusion and to gaps in your workflow that Git was never designed to fill. Deployment is the process of taking your site's files and placing them on a server where visitors can reach them, with the right configuration, domain, and security settings. Git can be part of a deployment process, because a deployment tool can pull from a repository, but Git itself does not serve your site to visitors, and pushing to a remote does not, by itself, make a site live. That step requires a hosting or deployment tool, chosen separately.

Running tests is another job Git does not do. Tests are programs that check whether your code behaves correctly, and they are run by a test tool, not by Git. Git can record the test files, and it can record the results indirectly, if you commit a report, but it does not execute the tests or tell you whether they pass. Similarly, Git does not check your code for style or correctness, which is the job of linters and formatters, covered in the previous chapter, and it does not build your site for production, which is the job of build tools, covered in the next chapter. Each of those tools has its own purpose, and Git's purpose is history, not execution.

Backing up your computer is also outside Git's scope, although pushing to a remote does back up your repository's history to another machine. That backup covers only the files tracked in the repository, not your whole computer, not your personal files, not your editor settings, and not the untracked files in your working folder. A repository backup is valuable, but it is not a substitute for a proper backup of your machine, which should cover everything you would miss if the machine failed. Treating a push as a full backup creates a false sense of security, because the files Git does not track would still be lost.

Knowing Git's boundary stops you expecting it to fix problems it was never designed to solve, and it helps you choose the right tool for each job. When a site is not live, check deployment, not Git. When a test fails, check the test tool and the code, not Git, except to see what changed recently. When a file is lost, check backups, not just Git, unless the file was tracked and committed. Git is a powerful tool for history, and history is a powerful aid to all these other jobs, because it tells you what changed, but it is not a replacement for them.

Check: Name three jobs Git does not do.

Section 47 — Backing up a repository

Pushing regularly to a remote is the simplest backup for a Git project, because it copies your commits to another machine, where they survive even if your own computer fails. A commit that exists only on your laptop is vulnerable to that laptop's loss, theft, or failure, and to accidental deletion of the project folder. A commit that has been pushed exists in at least two places, your laptop and the remote, and restoring it is a matter of cloning the remote onto a new machine, which recreates the full history, not just the latest files. That simplicity is why pushing is often described as a backup habit, even though its primary purpose is sharing.

The backup value of pushing depends on regularity. Pushing once at the end of a project backs up only the final state, and a failure during the project would lose all intermediate work, including the history that explains how the final state was reached. Pushing at the end of each work session, or after each finished feature, backs up work in small, frequent batches, so that the most you can lose is a single session's work, not a whole project's. That frequency is easy to achieve, because pushing takes seconds, and it is a habit worth building alongside committing. Commit to record work locally, push to back it up remotely, and do both frequently.

It is important to understand what pushing backs up and what it does not. It backs up tracked, committed files, and their history, including branches that have been pushed and tags that have been pushed. It does not back up uncommitted changes in your working folder, stashes that have not been applied and committed, or branches that exist only locally and have never been pushed. A work-in-progress branch that you have not pushed is not backed up, even if its commits are valuable, because the remote has never received them. To back up a branch, push it, even if it is not ready to merge, because a pushed branch is a backed-up branch, and a local-only branch is a branch at risk.

A remote is not the only backup, and for important projects, additional backups, such as a second remote or a regular backup of your machine, provide extra safety. But for most front-end projects, a single remote, pushed to regularly, is a sufficient and practical backup, because it is simple, automatic in the sense that it takes one command, and integrated into the workflow you already use for sharing. Make pushing a habit, not an afterthought, and your project's history will be safe, even when your computer is not. Make pushing a habit, not an afterthought, and your project's history will be safe even when your computer is not, because a pushed commit lives in at least two places at once.

Check: Why is pushing regularly also a backup habit?

Section 48 — When Git output confuses you

When Git's output confuses you, the first step is to slow down and read the message Git printed, rather than running another command in the hope that it will fix the problem. Git's messages are often precise, even if they use terminology that is unfamiliar at first. A message that says you are not on a branch, that you have uncommitted changes, or that a merge is in progress, is telling you the state of the repository, and that state explains why the command you just ran behaved as it did. Reading the message, and looking up any term you do not understand, is faster than guessing, and it builds the vocabulary that makes future messages easier to read.

The second step is to run git status, which shows the current state in a structured way: the branch, the staged and unstaged changes, the untracked files, and any merge or conflict state. Most Git confusion comes from one of three causes, all of which show up in status. The first is being on the wrong branch, which status shows prominently at the top. The second is having uncommitted changes, which status lists as modified or staged, and which can prevent a branch switch or a pull from completing cleanly. The third is having staged something by mistake, which status shows in the staged section, and which can be corrected by unstaging, without losing work. Checking those three things, branch, uncommitted changes, and staged set, resolves the majority of confusing situations, because it tells you where you are before you decide what to do next.

The third step is to check the diff, if the confusion involves what changed. Running the unstaged diff shows what you have changed but not staged, and running the staged diff shows what the next commit would contain. Those diffs often reveal that a file you thought was unchanged has in fact changed, or that a file you intended to stage is not staged, or that a merge left a conflict you had not noticed. The diff is the evidence that turns a vague feeling of confusion into a concrete fact about what is different, and concrete facts are what you need to decide on a fix.

If, after status and diff, the situation is still unclear, do not run a destructive command, such as discarding changes or resetting, in the hope that it will clear the confusion. Destructive commands make a confusing situation worse by removing information that might have explained it. Instead, save your work, by committing it on a branch if possible, or by copying the working folder elsewhere as a backup, and then seek help, with the status output and the Git message to show. A clear description of the state, with status output attached, lets a teammate or a guide help you quickly, while a vague description of a command that failed does not. Confusion is normal when learning Git. Patience, status, and diff are the tools that turn it into understanding.

Check: What three things do you check first when Git confuses you?

Section 49 — Git for a solo front-end developer

Even working alone, Git pays off, because the benefits of version control do not depend on having teammates. Every experiment gets a branch, so you can try a risky layout or a new interaction without endangering the working version. Every working state gets a commit, so you can return to it if a later change breaks something, without needing to remember what the working version looked like. Every broken layout can be undone, by checking out an earlier commit or reverting a bad change, without reconstructing the broken code from memory. Those benefits are valuable for a solo developer, because a solo developer has no teammate to help reconstruct lost work, and no second pair of eyes to catch a mistake before it becomes permanent.

Solo developers use the same habits as teams, just without pull requests, because the habits themselves are about managing work, not about managing people. Small commits, clear messages, branches for features, and regular pushes for backup, all apply to a solo project, and they all make the project easier to maintain over time. A solo project that is maintained for months or years will be revisited after breaks, when the developer has forgotten details, and history becomes the memory that fills those gaps. A commit message that explains why a style was chosen, or why a feature was removed, is as valuable to future you as it would be to a teammate, because future you is, in effect, a different person, with less context than present you.

Git also prepares a solo project for future collaboration, without requiring any extra work. A project that is already under Git, with a clean history, clear branches, and a remote backup, can be shared with a collaborator in minutes, by giving them access to the remote and letting them clone it. A project that is not under Git, with files scattered across folders and no history, requires a cleanup effort before it can be shared, and that effort is often postponed until collaboration is urgently needed, when it is most disruptive. Starting with Git, even alone, means the project is always ready to grow from solo to team, without a painful transition.

Finally, Git is a professional skill that is expected in front-end work, and practising it solo builds the fluency that team work will later require. The commands, the concepts, and the habits are the same, whether the repository has one contributor or one hundred. A solo developer who uses Git daily will join a team already fluent in its workflow, while a developer who avoids Git solo will need to learn it under the pressure of a team project, where mistakes affect others. Solo Git use is therefore not just useful for the solo project. It is preparation for the team projects that will follow, and it is preparation that costs nothing except the habit of committing regularly.

Check: Why should a solo developer still use branches and commits?

Section 50 — Chapter 25 in one paragraph

Git records a project's history as small, well-named commits on branches, lets you stage exactly what you mean to save, review diffs, resolve conflicts by hand, and share work through a remote with pull requests. Used daily, it turns "the page broke and I don't know why" into a history you can read, search and safely undo. That summary captures the chapter, but it is worth expanding once more, because each part of it represents a habit that, practised together, changes how front-end work feels.

The first habit is saving deliberately. Staging lets you choose what goes into a commit, committing records it with a message, and small commits keep history readable and reversible. The second habit is branching for isolation. Features, fixes, and experiments each get their own line of work, merged back only when ready, so the main branch stays stable and trustworthy. The third habit is sharing carefully. Pushing backs up work and makes it available, pulling keeps your copy up to date, and pull requests create a moment for review before work enters a shared branch. The fourth habit is resolving conflicts by hand, with patience and testing, rather than fearing them or working around them with markers left in files.

Together, those habits give a front-end developer something that no amount of careful editing alone can give: a project whose past is visible, whose present is testable, and whose future changes can be made with confidence, because any change can be inspected, understood, and, if necessary, undone. A page that breaks is no longer a mystery. It is a history to read, a diff to inspect, and a commit to revert or fix. A feature that is wanted is no longer a risky edit to a stable page. It is a branch to build, review, and merge. That transformation, from fragile editing to managed history, is what Git offers, and it is why Git is a core skill for front-end development, not an optional extra.

Carry the daily routine forward: pull, check status, branch, work in small steps, stage, commit with a clear message, review the diff, push, and, in a team, open a pull request. Follow that routine on every project, solo or team, small or large, and Git will stop feeling like a separate subject to learn and start feeling like the natural way to work. The next chapter builds on this foundation, showing how build tools take the source files that Git tracks and prepare them for visitors, but the source itself, and its history, remain the foundation that everything else is built on. That transformation, from fragile editing to managed history, is why Git is a core skill for front-end development, and why the daily routine in this chapter is worth practising until it feels natural.

Check: In your own words, summarise what Git gives a front-end developer.

Practical task

Start a real front-end project under Git. Create a folder with index.html, a stylesheet and a script, run git init, and add a .gitignore that ignores a build output folder and a downloaded-libraries folder. Make a first commit with a clear message, then create a branch called feature-footer, add a footer to the page on that branch, commit it, switch back to the main branch, merge the feature branch, and run git log --oneline to confirm the history. Finally, deliberately change one line on two branches to practise resolving a merge conflict, and commit the resolved version.

Chapter questions

  1. Explain, in plain words, the difference between staging a file and committing it.
  2. Two branches change the same lines of a stylesheet in different ways. What happens when you merge, and how do you resolve it?
  3. Why should generated files and secrets stay out of a Git repository?
  4. When should you revert a commit, and when might you reset instead? Explain the difference in terms of shared work.
  5. Describe the branch workflow you would use, step by step, to add a new navigation menu to a team project without breaking the main branch.