Git’s magic isn’t in its commands, but in how it lets you rewind time for your code.

Let’s see git init in action. Imagine a new project folder.

$ mkdir my-new-project
$ cd my-new-project
$ git init
Initialized empty Git repository in /path/to/my-new-project/.git/

That .git directory is the brain of your project. It’s where Git stores everything: your history, your branches, your configuration. It’s not something you usually touch directly, but knowing it’s there is key.

Now, what if you want to work on an existing project? That’s where git clone comes in.

$ git clone https://github.com/user/repo.git
Cloning into 'repo'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 204 bytes | 204.00 KiB/s, done.

This pulls down the entire project history from a remote server (like GitHub, GitLab, or Bitbucket) and sets up your local copy to talk back to it. You now have a .git directory in your cloned repo folder.

So, you’ve made some changes. You’ve added a new file, or modified an existing one. Git sees these changes, but it doesn’t automatically track them. You need to tell Git which changes you want to record for your next snapshot. This is the "staging" area, and the command is git add.

Let’s say you created new_feature.py.

$ echo "print('Hello, Git!')" > new_feature.py
$ git status
On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        new_feature.py

nothing added to commit but untracked files present (use "git add" to track)

$ git add new_feature.py
$ git status
On branch main

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   new_feature.py

Notice how git status changed. new_feature.py is now in the "Changes to be committed" list, meaning it’s staged and ready for its first commit.

The git commit command takes everything that’s currently staged and saves it as a permanent snapshot in your project’s history. Each commit gets a unique ID and a message explaining what changed.

$ git commit -m "Add initial feature file"
[main (root-commit) abc1234] Add initial feature file
 1 file changed, 1 insertion(+)
 create mode 100644 new_feature.py

That abc1234 is the commit ID. The -m flag is for the commit message – make it descriptive! Now, if you ever need to go back to this exact state, you can.

Finally, to share your work or get updates from others, you git push. This sends your local commits to a remote repository.

$ git push origin main
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 8 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 268 bytes | 268.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/user/repo.git
 * [new branch]      main -> main

origin is the default name Git gives to the remote repository you cloned from, and main is the branch you’re pushing. This makes your local changes available to anyone else working on the project.

The most surprising thing about Git is how it handles file content. It doesn’t just store diffs; it stores snapshots of your entire project at each commit. When you switch branches or revert to an old commit, Git reconstructs that exact snapshot by recreating the files as they were at that moment. This makes operations like branching and history traversal incredibly fast and reliable because Git is dealing with complete states, not just changes between states.

Understanding the staging area is crucial. It’s Git’s way of letting you craft precise commits. You can stage only some of the changes in your working directory, allowing you to group related modifications into logical commits.

Next, you’ll want to explore how to work with multiple people on the same project using branches.

Want structured learning?

Take the full Git course →