D
Using Git for Version Control

Version control software allows you to take snapshots of a project whenever it’s in a working state. When you make changes to a project—for example, when you implement a new feature—you can go back to a previous working state if the project’s current state isn’t functioning well.

Using version control software gives you the freedom to work on improvements and make mistakes without worrying about ruining your project. This is especially critical in large projects, but can also be helpful in smaller projects, even when you’re working on programs contained in a single file.

In this appendix, you’ll learn to install Git and use it for version control in the programs you’re working on now. Git is the most popular version control software in use today. Many of its advanced tools help teams collaborate on large projects, but its most basic features also work well for solo developers. Git implements version control by tracking the changes made to every file in a project; if you make a mistake, you can just return to a previously saved state.

Installing Git

Git runs on all operating systems, but there are different approaches to installing it on each system. The following sections provide specific instructions for each operating system.

Git is included on some systems by default, and is often bundled with other packages that you might have already installed. Before trying to install Git, see if it’s already on your system. Open a new terminal window and issue the command git --version. If you see output listing a specific version number, Git is installed on your system. If you see a message prompting you to install or update Git, follow the onscreen instructions.

If you don’t see any onscreen instructions and you’re using Windows or macOS, you can download an installer from https://git-scm.com. If you’re a Linux user with an apt-compatible system, you can install Git with the command sudo apt install git.

Configuring Git

Git keeps track of who makes changes to a project, even when only one person is working on the project. To do this, Git needs to know your username and email. You must provide a username, but you can make up a fake email address:

$ git config --global user.name "username"
$ git config --global user.email "username@example.com"

If you forget this step, Git will prompt you for this information when you make your first commit.

It’s also best to set the default name for the main branch in each project. A good name for this branch is main:

$ git config --global init.defaultBranch main

This configuration means that each new project you use Git to manage will start out with a single branch of commits called main.

Making a Project

Let’s make a project to work with. Create a folder somewhere on your system called git_practice. Inside the folder, make a simple Python program:

hello_git.py

print("Hello Git world!")

We’ll use this program to explore Git’s basic functionality.

Ignoring Files

Files with the extension .pyc are automatically generated from .py files, so we don’t need Git to keep track of them. These files are stored in a directory called __pycache__. To tell Git to ignore this directory, make a special file called .gitignore—with a dot at the beginning of the filename and no file extension—and add the following line to it:

.gitignore

__pycache__/

This file tells Git to ignore any file in the __pycache__ directory. Using a .gitignore file will keep your project clutter-free and easier to work with.

You might need to modify your file browser’s settings so hidden files (files whose names begin with a dot) will be shown. In Windows Explorer, check the box in the View menu labeled Hidden Items. On macOS, press ⌘-SHIFT-. (dot). On Linux, look for a setting labeled Show Hidden Files.

Initializing a Repository

Now that you have a directory containing a Python file and a .gitignore file, you can initialize a Git repository. Open a terminal, navigate to the git_practice folder, and run the following command:

git_practice$ git init
Initialized empty Git repository in git_practice/.git/
git_practice$

The output shows that Git has initialized an empty repository in git_practice. A repository is the set of files in a program that Git is actively tracking. All the files Git uses to manage the repository are located in the hidden directory .git, which you won’t need to work with at all. Just don’t delete that directory, or you’ll lose your project’s history.

Checking the Status

Before doing anything else, let’s look at the project’s status:

git_practice$ git status
 On branch main
No commits yet

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

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

In Git, a branch is a version of the project you’re working on; here you can see that we’re on a branch named main . Each time you check your project’s status, it should show that you’re on the branch main. You then see that we’re about to make the initial commit. A commit is a snapshot of the project at a particular point in time.

Git informs us that untracked files are in the project , because we haven’t told it which files to track yet. Then we’re told that there’s nothing added to the current commit, but untracked files are present that we might want to add to the repository .

Adding Files to the Repository

Let’s add the two files to the repository and check the status again:

 git_practice$ git add .
 git_practice$ git status
On branch main
No commits yet

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

git_practice$

The command git add . adds to the repository all files within a project that aren’t already being tracked , as long as they’re not listed in .gitignore. It doesn’t commit the files; it just tells Git to start paying attention to them. When we check the status of the project now, we can see that Git recognizes some changes that need to be committed . The label new file means these files were newly added to the repository .

Making a Commit

Let’s make the first commit:

 git_practice$ git commit -m "Started project."
 [main (root-commit) cea13dd] Started project.
 2 files changed, 5 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 hello_git.py
 git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

We issue the command git commit -m "message" to make a snapshot of the project. The -m flag tells Git to record the message that follows (Started project.) in the project’s log. The output shows that we’re on the main branch and that two files have changed .

When we check the status now, we can see that we’re on the main branch, and we have a clean working tree . This is the message you should see each time you commit a working state of your project. If you get a different message, read it carefully; it’s likely you forgot to add a file before making a commit.

Checking the Log

Git keeps a log of all commits made to the project. Let’s check the log:

git_practice$ git log
commit cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main)
Author: eric <eric@example.com>
Date:   Mon Jun 6 19:37:26 2022 -0800

    Started project.
git_practice$

Each time you make a commit, Git generates a unique, 40-character reference ID. It records who made the commit, when it was made, and the message recorded. You won’t always need all of this information, so Git provides an option to print a simpler version of the log entries:

git_practice$ git log --pretty=oneline
cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main) Started project.
git_practice$

The --pretty=oneline flag provides the two most important pieces of information: the reference ID of the commit and the message recorded for the commit.

The Second Commit

To see the real power of version control, we need to make a change to the project and commit that change. Here we’ll just add another line to hello_git.py:

hello_git.py

print("Hello Git world!")
print("Hello everyone.")

When we check the status of the project, we’ll see that Git has noticed the file that changed:

git_practice$ git status
 On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

  modified:   hello_git.py

 no changes added to commit (use "git add" and/or "git commit -a")
git_practice$

We see the branch we’re working on , the name of the file that was modified , and that no changes have been committed . Let’s commit the change and check the status again:

 git_practice$ git commit -am "Extended greeting."
[main 945fa13] Extended greeting.
 1 file changed, 1 insertion(+), 1 deletion(-)
 git_practice$ git status
On branch main
nothing to commit, working tree clean
 git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
git_practice$

We make a new commit, passing the -am flags when we use the command git commit . The -a flag tells Git to add all modified files in the repository to the current commit. (If you create any new files between commits, reissue the git add . command to include the new files in the repository.) The -m flag tells Git to record a message in the log for this commit.

When we check the project’s status, we see that we once again have a clean working tree . Finally, we see the two commits in the log .

Abandoning Changes

Now let’s look at how to abandon a change and go back to the previous working state. First, add a new line to hello_git.py:

hello_git.py

print("Hello Git world!")
print("Hello everyone.")

print("Oh no, I broke the project!")

Save and run this file.

We check the status and see that Git notices this change:

git_practice$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

     modified:   hello_git.py

no changes added to commit (use "git add" and/or "git commit -a")
git_practice$

Git sees that we modified hello_git.py , and we can commit the change if we want to. But this time, instead of committing the change, we’ll go back to the last commit when we knew our project was working. We won’t do anything to hello_git.py: we won’t delete the line or use the Undo feature in the text editor. Instead, enter the following commands in your terminal session:

git_practice$ git restore .
git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

The command git restore filename allows you to abandon all changes since the last commit in a specific file. The command git restore . abandons all changes made in all files since the last commit; this action restores the project to the last committed state.

When you return to your text editor, you’ll see that hello_git.py has changed back to this:

print("Hello Git world!")
print("Hello everyone.")

Although going back to a previous state might seem trivial in this simple project, if we were working on a large project with dozens of modified files, all the files that had changed since the last commit would be restored. This feature is incredibly useful: you can make as many changes as you want when implementing a new feature, and if they don’t work, you can discard them without affecting the project. You don’t have to remember those changes and manually undo them. Git does all of that for you.

Checking Out Previous Commits

You can revisit any commit in your log, using the checkout command, by using the first six characters of a reference ID. After checking out and reviewing an earlier commit, you can return to the latest commit or abandon your recent work and pick up development from the earlier commit:

git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
git_practice$ git checkout cea13d
Note: switching to 'cea13d'.

 You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

 Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at cea13d Started project.
git_practice$

When you check out a previous commit, you leave the main branch and enter what Git refers to as a detached HEAD state . HEAD is the current committed state of the project; you’re detached because you’ve left a named branch (main, in this case).

To get back to the main branch, you follow the suggestion to undo the previous operation:

git_practice$ git switch -
Previous HEAD position was cea13d Started project.
Switched to branch 'main'
git_practice$

This command brings you back to the main branch. Unless you want to work with some more advanced features of Git, it’s best not to make any changes to your project when you’ve checked out a previous commit. However, if you’re the only one working on a project and you want to discard all of the more recent commits and go back to a previous state, you can reset the project to a previous commit. Working from the main branch, enter the following:

 git_practice$ git status
On branch main
nothing to commit, working directory clean
 git_practice$ git log --pretty=oneline
945fa13af128a266d0114eebb7a3276f7d58ecd2 (HEAD -> main) Extended greeting.
cea13ddc51b885d05a410201a54faf20e0d2e246 Started project.
 git_practice$ git reset --hard cea13d
HEAD is now at cea13dd Started project.
 git_practice$ git status
On branch main
nothing to commit, working directory clean
 git_practice$ git log --pretty=oneline
cea13ddc51b885d05a410201a54faf20e0d2e246 (HEAD -> main) Started project.
git_practice$

We first check the status to make sure we’re on the main branch . When we look at the log, we see both commits . We then issue the git reset --hard command with the first six characters of the reference ID of the commit we want to go back to permanently . We check the status again and see we’re on the main branch with nothing to commit . When we look at the log again, we see that we’re at the commit we wanted to start over from .

Deleting the Repository

Sometimes you’ll mess up your repository’s history and won’t know how to recover it. If this happens, first consider asking for help using the approaches discussed in Appendix C. If you can’t fix it and you’re working on a solo project, you can continue working with the files but get rid of the project’s history by deleting the .git directory. This won’t affect the current state of any of the files, but it will delete all commits, so you won’t be able to check out any other states of the project.

To do this, either open a file browser and delete the .git repository or delete it from the command line. Afterward, you’ll need to start over with a fresh repository to start tracking your changes again. Here’s what this entire process looks like in a terminal session:

 git_practice$ git status
On branch main
nothing to commit, working directory clean
 git_practice$ rm -rf .git/
 git_practice$ git status
fatal: Not a git repository (or any of the parent directories): .git
 git_practice$ git init
Initialized empty Git repository in git_practice/.git/
 git_practice$ git status
On branch main
No commits yet

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

nothing added to commit but untracked files present (use "git add" to track)
 git_practice$ git add .
git_practice$ git commit -m "Starting over."
[main (root-commit) 14ed9db] Starting over.
 2 files changed, 5 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 hello_git.py
 git_practice$ git status
On branch main
nothing to commit, working tree clean
git_practice$

We first check the status and see that we have a clean working directory . Then we use the command rm -rf .git/ to delete the .git directory (del .git on Windows) . When we check the status after deleting the .git folder, we’re told that this is not a Git repository . All the information Git uses to track a repository is stored in the .git folder, so removing it deletes the entire repository.

We’re then free to use git init to start a fresh repository . Checking the status shows that we’re back at the initial stage, awaiting the first commit . We add the files and make the first commit . Checking the status now shows us that we’re on the new main branch with nothing to commit .

Using version control takes a bit of practice, but once you start using it, you’ll never want to work without it again.