task-manager is about to have its first stable version. The team wants to be able to say "this is 1.0.0" and, two years from now, when an issue arrives from a client still on that version, for anybody to be able to stand exactly on that code without having to remember a forty-character hash or rummage through git log by date.
That is what tags are for: permanent names pointing at one specific commit. Unlike a branch, which moves on every time you commit, a tag stays put. v1.0.0 means today, tomorrow and a decade from now exactly the same commit.
And there is a second thing to learn here: in lesson 01-04 we saw that Git's database stores four types of object — blob, tree, commit and tag — and about the fourth we said hardly anything. This lesson completes it. Because it turns out there are two very different kinds of tag, and the difference between them is exactly whether that tag object exists or not.
Contents
- What a tag is and how it differs from a branch
- Lightweight versus annotated tags
- Creating tags
- Listing, searching and examining tags
- Tagging a commit from the past
- Semantic versioning
- Pushing tags to the remote
- Deleting tags and why they do not move
- Signed tags
git describe: naming any commit- Working on a tag
- What a tag is and how it differs from a branch
Remember lesson 03-01: a branch is a 41-byte file in .git/refs/heads/ containing a hash. A tag is the same thing, but in .git/refs/tags/:
The difference is not in the format but in the behaviour:
| Branch | Tag | |
|---|---|---|
| Where it lives | refs/heads/ |
refs/tags/ |
| Does it move on its own? | Yes: it advances with every commit | No: never |
Can HEAD point at it? |
Yes | No: it leaves you in detached HEAD |
| Meaning | "This is where I work" | "This is one specific point" |
Sent with git push |
Yes (as per the refspec) | No by default |
| Can have metadata of its own | No | Yes, if it is annotated |
That "does not move on its own" is the whole value of the tool. If you stand on v1.0.0 three years from now, you see exactly what was there when it was released, even if main has taken a thousand commits since.
gitGraph commit id: "1a4c8d6" commit id: "8b6d3c2" commit id: "9f4c2e8" tag: "v1.0.0" commit id: "c5d9b1e" commit id: "7d3a8f4" tag: "v1.0.1" commit id: "e91d4a8"
main carries on moving to the right; v1.0.0 and v1.0.1 stay nailed where they were put.
- Lightweight versus annotated tags
Here is the lesson's central concept.
A lightweight tag is exactly what you have just seen: a file with a hash in it. A bare pointer, nothing more. It creates no new object in the database.
An annotated tag creates a complete tag object in Git's database, with a hash of its own, containing: which commit it points at, who created it, when, a message, and optionally a cryptographic signature. And refs/tags/v1.0.0 points at that object, not at the commit.
Let us look at it with the tools of 01-04:
The reference resolves directly to a commit: there is no intermediate object.
object 9f4c2e8b7d1a5f3c6e9b2d8a4f7c1e5b3d9a6f2c type commit tag v1.0.0 tagger Ana Ferrer <[email protected]> 1754035200 +0200 First stable version of task-manager Includes the task list with persistence in localStorage, the pending counter, the filter and the CSV export.
There it is, the fourth type of object, at last in its natural habitat. Note the structure: object (what it points at), type (what the pointed-at thing is), tag (the name), tagger (who and when) and the message. And like every Git object, it is immutable: its hash is computed over that content.
The full comparison:
| Aspect | Lightweight | Annotated |
|---|---|---|
| How it is created | git tag <name> |
git tag -a <name> -m "..." |
| Object in the database | None | A tag object |
What refs/tags/<n> points at |
The commit | The tag object |
| Tag's author | Not stored | Yes (tagger) |
| Tag's date | Not stored | Yes |
| Message | No | Yes |
| Can be signed (GPG/SSH) | No | Yes |
Appears in git describe |
Only with --tags |
Yes, by default |
Sent by --follow-tags |
No | Yes |
| Recommended use | Local, temporary markers | Published versions, always |
Why annotated tags are always used for releasing versions, in four concrete reasons:
- They record who and when. A
v1.0.0with no author and no date does not answer "who decided this was 1.0?". - They carry a message. That message is the natural place for the release notes, and it travels with the repository.
- They can be signed. A signed tag allows cryptographic verification that the version was released by whoever it claims.
- Git treats them as first-class citizens.
git describeprefers them,push --follow-tagsonly sends those, and several tools in the ecosystem expect them.
The practical rule is simple: lightweight tag = disposable personal marker; annotated tag = everything else.
- Creating tags
# Lightweight, on the current commit
git tag todays-tests
# Annotated, on the current commit (the normal form)
git tag -a v1.0.0 -m "First stable version of task-manager"
# Annotated with a long message: without -m, the editor opens
git tag -a v1.0.0When the editor opens, write a subject on the first line, a blank line and the body. It is exactly the convention of commit messages (lesson 08-01):
task-manager 1.0.0 First stable version, ready for the client deployment. Includes: - Creating, listing and deleting tasks with persistence in localStorage - Pending task counter - Pending filter - Export of the list to CSV
A warning about names: they are Git references, so the same rules as for branches apply (lesson 03-06). No spaces, no .., no ~, ^, :, ?, *, [, no @{, no ending in . or in .lock. The overwhelmingly dominant convention is v followed by the number: v1.0.0, v2.3.1.
And a precaution that avoids a classic problem: do not use the same name for a branch and a tag. If v1.0.0 exists both as a branch and as a tag, git checkout v1.0.0 is ambiguous, Git warns you and applies a precedence rule nobody remembers. We saw it in passing in 04-05 when discussing full refspecs; the solution is not to create the problem.
- Listing, searching and examining tags
Sorted alphabetically, which is the wrong order for versions: v1.10.0 comes out before v1.2.0. Git knows this and has a solution:
v:refname sorts by understanding the semantics of version numbers, and the - reverses it to put the newest at the top. It is worth leaving configured:
Filtering by pattern with -l (or --list), which accepts shell wildcards:
Seeing each tag's information in the listing:
v0.9.0 Version prior to the first stable one v1.0.0 task-manager 1.0.0 v1.0.1 Fix for the focus after deleting a task
Examining a tag thoroughly with git show:
tag v1.0.0 Tagger: Ana Ferrer <[email protected]> Date: Fri Aug 1 10:00:00 2026 +0200 task-manager 1.0.0 First stable version, ready for the client deployment. commit 9f4c2e8b7d1a5f3c6e9b2d8a4f7c1e5b3d9a6f2c Author: Bruno Salas <[email protected]> Date: Thu Jul 31 18:22:41 2026 +0200 Add the export button to the action bar diff --git a/index.html b/index.html ...
With an annotated tag you see two blocks: first the tag (with its tagger, its date and its message) and then the commit it points at with its diff. With a lightweight one you would see only the commit, because there is nothing else to see.
Other useful queries:
# Custom format, as in git branch --format (lesson 03-06)
git tag --format='%(refname:short) %(creatordate:short) %(subject)' --sort=-creatordatev1.0.1 2026-08-05 Fix for the focus after deleting a task v1.0.0 2026-08-01 task-manager 1.0.0 v0.9.0 2026-07-15 Version prior to the first stable one
# Which tags contain a commit (that is, which versions that change went into)
git tag --contains 7d3a8f4That last one is among the most useful in the repertoire: it answers "which version was this fixed in?" without opening anything.
Tags work like any other reference in ranges, diff, log and show. Everything you learned in lesson 02-06 applies here.
- Tagging a commit from the past
It is common to realise that something needs tagging after you have carried on working. You simply pass the commit:
tag v0.9.0 Tagger: Ana Ferrer <[email protected]> Date: Fri Aug 1 10:14:33 2026 +0200 Version prior to the first stable one commit 8b6d3c2...
One important detail is visible there: the tag's date is today's, not the commit's. That is correct: the tag was created today, even though it marks something from weeks ago. If for some reason you need them to match:
And since the commit can be given with any of the expressions you know (lesson 02-06), all of this is valid:
git tag -a v0.9.0 HEAD~5 -m "..."
git tag -a v0.9.0 main~10 -m "..."
git tag -a v0.9.0 feature/csv-export -m "..."
- Semantic versioning
Tagging v1.0.0 means nothing if the team does not share an understanding of what that number says. Semantic versioning (SemVer) is the most widespread convention for giving it meaning.
Format: MAJOR.MINOR.PATCH
| Component | Incremented when… | Effect on the others |
|---|---|---|
| MAJOR | There is a change that is incompatible with the previous version | MINOR and PATCH go back to 0 |
| MINOR | Functionality is added that is backwards compatible | PATCH goes back to 0 |
| PATCH | A bug is fixed without changing the expected behaviour | — |
Applied to the real history of task-manager:
| Version | What changed | Why that number |
|---|---|---|
v0.1.0 |
Basic task list | Before 1.0 there is no commitment to stability |
v0.9.0 |
Everything planned, in testing | Still a pre-release |
v1.0.0 |
First stable version | The compatibility commitment is taken on |
v1.0.1 |
Focus after deleting fixed | Just a fix: PATCH |
v1.1.0 |
Pending filter added | New functionality, nothing breaks: MINOR |
v1.1.1 |
Counter fixed with the filter active | PATCH |
v1.2.0 |
CSV export added | MINOR |
v2.0.0 |
The localStorage format changes and 1.x data cannot be read |
Breaks compatibility: MAJOR |
Two rules that are often overlooked:
- Version
0.x.yis territory with no guarantees. Before1.0.0, the convention explicitly says that anything may change. It is where to be while the design has not settled. - Once released, a version is never touched again. If
v1.0.0had a bug, you releasev1.0.1. You never re-tagv1.0.0(section 8).
SemVer also allows suffixes for pre-releases and build metadata:
git tag -a v2.0.0-alpha.1 -m "First alpha of version 2"
git tag -a v2.0.0-rc.1 -m "Release candidate"
git tag -a v2.0.0 -m "task-manager 2.0.0"In SemVer's ordering, v2.0.0-alpha.1 < v2.0.0-rc.1 < v2.0.0: pre-release suffixes come before the final version. git tag --sort=-v:refname understands this correctly.
How it is decided what goes into each version, who approves it, how the notes are generated and how the release is automated are matters of team process and continuous integration: you will see them in modules 7 and 10. Here we are concerned with the Git mechanism.
- Pushing tags to the remote
In lesson 04-05 we left this open with a sentence that surprises everyone: git push does not send tags. Now we close the subject.
The reason is the default refspec (lesson 04-02): refs/heads/*:refs/heads/*. It only covers branches. refs/tags/* falls outside it.
The tag has been created locally and there it stays. The three ways of sending it:
# 1. One specific tag
git push origin v1.0.0
# 2. ALL local tags
git push origin --tags
# 3. Only the ANNOTATED ones reachable from what is being sent
git push origin --follow-tags| Form | What it sends | When |
|---|---|---|
git push origin <tag> |
Only that one | When releasing a specific version: the most common case |
git push origin --tags |
All of them, lightweight ones included, whatever they point at | Hardly ever: it drags your personal markers along |
git push origin --follow-tags |
Only the annotated ones hanging off the commits being sent | Day to day |
--tags has a real problem: it also sends todays-tests, before-the-rebase and any lightweight marker you happen to have lying around, and once on the server they belong to everyone. That is why --follow-tags is almost always the right option, and why it is worth leaving configured:
From then on, an ordinary git push takes the relevant annotated tags with it and no others.
A nuance about --follow-tags: it only sends annotated tags and only those pointing at commits that are already, or are about to be, on the server. It is exactly what you want, and it is another practical reason for always using annotated tags when releasing versions.
On the receiving side there is nothing special to do: git fetch and git pull bring the tags of the commits they download. If you need to fetch them all explicitly:
git fetch --tags
git fetch --prune --prune-tags # also deletes locally those no longer on the server
- Deleting tags and why they do not move
Deleting locally:
Deleting on the remote (the same branch-deletion syntax from 04-05):
And now the important part: why is moving an already published tag a bad idea?
Technically you can. git tag -f v1.0.0 <another-commit> re-points it, and git push --force origin v1.0.0 re-points it on the server. But:
- Nobody finds out. Whoever already had
v1.0.0downloaded does not get it updated by an ordinarygit fetch. Git is deliberately conservative with existing tags: if the name already exists locally, it does not touch it. Result: for months, yourv1.0.0and Bruno's point at different commits, and neither of you knows. - It breaks the entire premise. A tag is worth something because it is stable. A tag that can change is useless: you can no longer cite it in an issue, in a deployment or in a document.
- Artefacts already released do not change. If
v1.0.0is deployed at a client, moving the tag does not move the client's code. All it achieves is a tag that lies about what was deployed. - It is the same violation of the module's golden rule. Rewriting something others have already downloaded is not a local operation.
Whoever receives a moved tag sees this, if they force it:
And from there on they may struggle to work out which one was the right one.
What to do instead:
| Situation | The correct solution |
|---|---|
| You got the commit wrong and have not published it yet | Delete it locally and create it properly |
| You published it five minutes ago and nobody has fetched | Delete it locally and on the remote, warn the team, and create it again |
| The version has a bug | Release v1.0.1. Never re-tag |
| You got the message wrong | If it is recent and you warn people, delete and recreate. If not, leave it: it is not worth it |
In short: a tag is a commitment. Once sent to the server, it is treated as immutable.
- Signed tags
An annotated tag can carry a cryptographic signature proving who created it:
# Sign with GPG
git tag -s v1.0.0 -m "task-manager 1.0.0"
# Verify
git verify-tag v1.0.0
git tag -v v1.0.0gpg: Signature made Fri Aug 1 10:00:00 2026 CEST gpg: using RSA key 4A7D2F8B... gpg: Good signature from "Ana Ferrer <[email protected]>" [ultimate]
What it is for in practice: if somebody distributes task-manager by downloading the v1.0.0 tag from the server, the signature lets them check that the version really was released by Ana and not by somebody who got access to the server.
It only works with annotated tags — a lightweight one has nowhere to store the signature — and it requires a key to be configured (user.signingkey, gpg.format, tag.gpgSign). The full configuration, including signing with SSH keys and signing commits, is in lesson 08-05: Security Best Practices. Here it is enough to know that the option exists and that it is one more reason to use annotated tags.
git describe: naming any commit
git describe: naming any commitYou have some commit or other, in the middle of development, and you want a readable name for it. git describe builds one from the most recent tag that reaches it:
It reads like this:
| Part | Meaning |
|---|---|
v1.1.0 |
The most recent annotated tag reachable from this commit |
14 |
There are 14 commits between that tag and here |
g8c3e7f1 |
The current commit is 8c3e7f1 (the g stands for "git") |
If you are exactly on a tag, the name is the tag on its own:
Important options:
| Option | What it does |
|---|---|
--tags |
Considers lightweight tags too |
--always |
If there is no tag at all, returns the abbreviated hash instead of failing |
--dirty |
Adds -dirty if the working tree has uncommitted changes |
--abbrev=<n> |
Length of the abbreviated hash (--abbrev=0 omits it: just the tag name) |
--match "<pattern>" |
Only tags matching the pattern |
--contains |
The other way round: the first tag that contains this commit |
The combination used in practice for versioning builds:
That identifier is extraordinarily useful: it is embedded in the application and, when somebody reports a bug from an intermediate version, you know exactly which commit they are talking about and whether it came from a dirty working tree. For example, in task-manager:
// This value is injected by the build script with the output of:
// git describe --tags --always --dirty
const VERSION = 'v1.1.0-14-g8c3e7f1';
function renderFooter() {
const footer = document.getElementById('footer');
footer.textContent = 'task-manager ' + VERSION;
}And --contains, for the inverse question:
"That commit is two commits before v1.0.1", that is: it went into version 1.0.1.
- Working on a tag
Standing on a tag leaves you in detached HEAD (lesson 03-02), because a tag is not a branch and cannot advance:
Note: switching to 'v1.0.0'. You are in 'detached HEAD' state... HEAD is now at 9f4c2e8 Add the export button to the action bar
That is fine for looking: inspecting that version's code, running it, reproducing a bug. But if you are going to work — for example, to fix a bug in 1.0 in order to release a 1.0.1 — you need a branch:
Now we are talking: a maintenance branch starting exactly at the released version. You fix it (or bring the fix over from main with git cherry-pick -x, lesson 05-03), commit, tag the new version and push it:
git cherry-pick -x 7d3a8f4
git tag -a v1.0.1 -m "Fix for the focus after deleting a task"
git push origin maintenance/1.0 v1.0.1That is the complete cycle of a maintenance release, and it uses four things from this module at once.
You can also export a tag's content without cloning anything:
git archive generates a .zip (or .tar.gz) with that tag's tree, without the .git directory. It is the canonical way of producing a release package.
Common Mistakes and Tips
Mistake 1: creating lightweight tags for versions. git tag v1.0.0 without -a records neither who, nor when, nor why, cannot be signed and is not sent by --follow-tags. For versions, always -a.
Mistake 2: believing git push sends tags. It does not. It is the classic surprise: you create v1.0.0, you push, and it is not on the server. git push origin v1.0.0 or push.followTags true.
Mistake 3: using --tags out of habit. It sends all your local tags, test markers included. Once on the server, cleaning them up is awkward and everyone has to be told.
Mistake 4: moving a published tag. Whoever already had it does not get it updated and you end up with two different truths. If there is a bug, you release the next version.
Mistake 5: trusting alphabetical order. v1.10.0 comes before v1.2.0 alphabetically. Configure tag.sort -v:refname.
Mistake 6: using the same name for a branch and a tag. It creates ambiguities in checkout, switch and push that are hard to understand afterwards.
Mistake 7: expecting git describe to see lightweight tags. By default it only looks at annotated ones. If your repository has only lightweight tags, git describe fails with no names found; there you need --tags.
Tip 1: tag from main and only what gets released. One tag per real version. Tagging intermediate commits "just in case" fills the namespace with permanent noise.
Tip 2: write a real message. The tag message is the best place for the release notes: it travels with the repository, does not depend on any external tool and is read with git show.
Tip 3: embed git describe --tags --always --dirty in your builds. It is the cheapest way of ensuring that every bug report includes the exact version.
Tip 4: git tag --contains <sha> to answer "which version did this go into?". It is quicker and more reliable than searching the log by date.
Tip 5: agree SemVer with the team and write it down. The value of MAJOR.MINOR.PATCH lies in everyone understanding the same thing. A version number nobody knows how to interpret is just a number.
Exercises
Exercise 1: lightweight versus annotated, hands on
In a practice repository with at least three commits:
- Create a lightweight tag and an annotated one on the same commit.
- Demonstrate with
git cat-file -tthat one resolves tocommitand the other totag. - Show the content of the
tagobject withgit cat-file -pand identify its five parts. - Compare the output of
git showon each.
Exercise 2: a version history
Simulate the evolution of task-manager:
- Three commits and
v1.0.0(annotated, with a release-notes message). - A fix commit and
v1.0.1. - Two new-functionality commits and
v1.1.0. - List the tags sorted by version, newest to oldest.
- Show what changed between
v1.0.0andv1.1.0. - Work out which version the fix commit went into without looking at the log.
Exercise 3: git describe and a maintenance branch
Starting from exercise 2:
- Add four more commits to
mainand check the output ofgit describe. Interpret each part. - Modify a file without committing and observe the effect of
--dirty. - Create a maintenance branch starting at
v1.0.0. - Take the 1.0.1 fix over to that branch with a cherry-pick, and tag the result as
v1.0.2. - Check with
git describeon each branch that the names are coherent.
Solutions
Solution 1:
mkdir /tmp/practice-tags && cd /tmp/practice-tags
git init -b main
echo "one" > f.txt && git add . && git commit -m "First"
echo "two" >> f.txt && git commit -am "Second"
echo "three" >> f.txt && git commit -am "Third"
# 1. The two tags
git tag lightweight
git tag -a annotated -m "This one is annotated"object 6d3f2a9c8b1e5f7d4a2c9e6b3f8d1a5c7e4b2f9d type commit tag annotated tagger Carla Vidal <[email protected]> 1754035200 +0200 This one is annotated
The five parts: object (the commit pointed at), type (what it is), tag (the name), tagger (authorship and date) and the message.
commit 6d3f2a9... Author: Carla Vidal <[email protected]> Date: Sat Aug 1 11:02:14 2026 +0200
tag annotated Tagger: Carla Vidal <[email protected]> Date: Sat Aug 1 11:03:40 2026 +0200 This one is annotated commit 6d3f2a9...
The annotated one shows one extra block: its own.
Solution 2:
mkdir /tmp/practice-versions && cd /tmp/practice-versions
git init -b main
echo "<h1>Task manager</h1>" > index.html && git add . && git commit -m "Add the initial structure"
echo "body { font-family: sans-serif; }" > styles.css && git add . && git commit -m "Add the base styles"
echo "const tasks = [];" > app.js && git add . && git commit -m "Add the task list"
git tag -a v1.0.0 -m "task-manager 1.0.0
First stable version: task list with base styles."echo "// focus after deleting" >> app.js && git commit -am "Return focus to the field after deleting"
git tag -a v1.0.1 -m "Fix for the focus after deleting a task"
echo "// filter" >> app.js && git commit -am "Add the pending filter"
echo ".filter { margin: 1rem; }" >> styles.css && git commit -am "Add the filter styles"
git tag -a v1.1.0 -m "task-manager 1.1.0
New pending task filter."2c9f4e7 Add the filter styles 8b1d6a3 Add the pending filter 5f7c2e9 Return focus to the field after deleting
It went into v1.0.1 (the first in the list) and, naturally, it is still present in v1.1.0.
Solution 3:
# 1. Four more commits and describe
for i in 1 2 3 4; do echo "// change $i" >> app.js; git commit -am "Change $i"; done
git describeThe most recent reachable annotated tag is v1.1.0, 4 commits have gone by since it, and the current commit is 9e2c6b1.
# 3 and 4. Maintenance branch and cherry-pick
git switch -c maintenance/1.0 v1.0.0
git log --oneline -1Each branch is named with respect to its own line of tags, which is exactly what you expect from a versioning scheme with maintenance branches.
Conclusion
Tags are the repository's stable memory. The essentials:
- A tag is a reference that does not move. It lives in
refs/tags/, marks one specific commit for ever and cannot be the destination ofHEAD(it leaves you in detached HEAD). - There are two kinds. The lightweight one is a bare pointer, with no object of its own. The annotated one creates a
tagobject in the database — the fourth type from lesson 01-04 — with author, date, message and optional signature. For released versions, always annotated. - They are created with
git tag -a <name> -m "...", optionally on a commit from the past; they are listed withgit tag -l "<pattern>",-nand--sort=-v:refname; they are examined withgit show; andgit tag --contains <sha>answers "which version did this go into?". - Semantic versioning (
MAJOR.MINOR.PATCH) gives the number a shared meaning: incompatible / new functionality / fix. Before1.0.0there is no commitment; afterwards, a released version is never touched. - Tags do not travel on their own:
git push origin <tag>for a specific one,--tagsfor all of them (rarely what you want) and--follow-tags— orpush.followTags true— for the reachable annotated ones, which is the right thing day to day. - They are deleted with
git tag -dlocally andgit push origin --deleteon the remote, but a published tag does not move: whoever already has it does not get it updated, and the result is two different truths coexisting. If there is a bug, you release the next version. git describe --tags --always --dirtynames any commit with respect to the last tag and is the canonical way of versioning builds.- To work on a released version, create a branch from the tag; the tag on its own is only good for looking.
What comes next
We now know how to mark the past. What is missing is the complementary operation, and the most delicate of them all: undoing it.
Because it is going to happen. Somebody is going to publish a commit on main that breaks something, and it will already be on the server, and three people will have it downloaded. The golden rule forbids rewriting it. So what then?
Git has an answer for that, and it is an elegant one: not deleting the commit, but adding another one that applies the opposite change. That is git revert, the only safe way of undoing something that is already public, and with it we close the module in lesson 05-06: Reverting Commits.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
