Get started with Git

Learn how to manage a codebase with Git including setting up a repo, using branches and pull requests, and merging changes.

Get started with Git
Thinkstock

No matter what programming language you use, no matter what operating system you run, some software development tools are for everyone. Git falls squarely into that category. The open source distributed version control system gives every kind of developer all the power they need to manage the evolution of their code, and to experiment freely and non-destructively with their projects.

In this article we’ll walk through the basics of using Git: setting up a repository, working with local and remote repositories, and using features like branches and pull requests to manage workflow. Follow along, and see for yourself why Git has become by far the most popular choice for managing codebases, either for solo developers or development teams.

Downloading and installing Git

Setting up Git on one's work system is different depending on what OS you run.

  • Linux: On some breeds of Linux, Git is installed by default. Otherwise, you can follow the installation instructions for your variety of Linux to set it up.
  • Windows: Git binaries for Windows can be downloaded from the official Git website. The portable or thumbdrive edition requires no installation—it unpacks into any directory where you have admin permissions—but will require you to add the ./bin subdirectory to your system PATH to work reliably.
  • MacOS: Mac users can install Git from HomeBrew with brew install git, or use the copy provide with Xcode.

Setting up Git

After you’ve confirmed Git is installed and available from the command line, the first thing you want to do with Git “out of the box” is configure it with your personal information. This allows all of your commits to be “signed” with that info.

To do this, you will use the git config command, like so:

PS D:\Dev\replicant> git config --global user.name Thomas Anderson
PS D:\Dev\replicant> git config --global user.email t_anderson@neocortex.com

Obviously, you will replace the username and user email with your own.

Another thing you may want to do is configure a default editor for Git:

PS D:\Dev\replicant> git config --global core.editor emacs

(This assumes emacs is a valid command.)

On Windows, you may need to provide a full path, in quotes, to the executable file for your editor.

Lastly, you will want to set the name for the default branch used in your code. This is something like main (or master, although in this example we’ll use main):

PS D:\Dev\replicant> git config --global init.defaultBranch main

Initializing a Git repository

When you want to create a Git repository to go with a project, whether a new “repo” or an existing one, you initialize the repository with the git init command.

PS D:\Dev\replicant> git init
Initialized empty Git repository in D:/Dev/replicant/.git/

The git init command creates a .git subdirectory in your project that holds all of the relevant files for the repo. Never place anything manually in this directory; let Git manage it.

If you are creating a repository that you will not interact with except through Git — e.g., it will be strictly an endpoint for code to be stored, like a GitHub repo — use git init --bare. This flag creates a repo that is not designed to be edited directly, but only pulled from and pushed to. (More on this later.)

Next, you add files or directories to be tracked in the repo with the git add command.

PS D:\Dev\replicant> git add readme.md

(If successful, a git add command returns nothing.)

Wildcards can also be used to add files:

PS D:\Dev\replicant> git add **

This would add every file stored in the directory replicant and in all of its children.

You can also create a .gitignore file in your project directory to indicate files or directories that should not be tracked, such as temporary files or build artifacts.

Cloning a Git repository

Another way to work with a repo is to clone, or copy, an existing repo. One does this with the git clone command, which creates a full, separate copy of a repo in the current directory. You can then work on this copy freely, since it’s now your copy.

PS D:\Dev\pp2> git clone https://github.com/syegulalp/pypacker
Cloning into 'pypacker'...
remote: Enumerating objects: 131, done.
remote: Counting objects: 100% (131/131), done.
remote: Compressing objects: 100% (64/64), done.
Receiving objects:  41%
Receiving objects: 100% (131/131), 23.16 KiB | 5.79 MiB/s, done.
Resolving deltas: 100% (60/60), done.

A cloned repo already includes a .git subdirectory initialized for it, so you can start work right away.

Git’s working and staging areas

The files in the directory covered by the Git repo is called the working area. Changes made to these files aren’t immediately applied to the repo; you can edit files freely in the working area without changing anything in the repo.

When you want to apply the changes in the working area to the repository, you stage the changes for each file using git add. If you have edited README.md, for example, you would stage its changes like this:

PS D:\dev\replicant> git add .\README.md

A fast way to stage all changes in the working area to your repo:

PS D:\dev\replicant> git add **

Again, when something is staged, there is no immediate feedback. But you can see the differences between the working and staging areas by using git status:

PS D:\dev\replicant> git status
On branch main

No commits yet

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

Saving changes with Git commit

When you’re satisfied with the changes to be made in the staging area, you commit them to the repository using git commit. This command writes the staged files to the repository and creates a snapshot of the repo at that moment in time.

PS D:\dev\replicant> git commit -m "Initial commit"
[main (root-commit) 9c6a751] Initial commit
 1 file changed, 1 insertion(+)
 create mode 100644 README.md

The -m flag lets you provide a commit message, or a short text description of the commit. If you just type git commit, Git will open an instance of the text editor it uses by default to edit commit messages. This is overkill most of the time, because commit messages should be short.

Each commit is a snapshot of the changes made with a distinct ID associated with it, such as 9c6a751, also called the ref.

Note that any changes made to the working area that are not staged will not be committed to the repository, and will be at risk of being overwritten by Git actions like changing branches.

The most recent commit for a given branch is referred to as the head for that branch.

Using Git branches

So far all the work we’ve done with a repo has been to the main branch. Branches in a repository are essentially alternate timelines or paths for your code’s development. You can create a new branch from any commit, write commits to that branch that are isolated from other branches, switch freely between branches, and merge changes across branches.

Creating a new branch

Initializing a new branch is easy enough:

PS D:\dev\replicant> git branch alphatest

This creates a new branch named alphatest at the current commit in the current branch. (Again, there is no feedback to the console if this command is successful.)

Switching between branches

When you create a new branch, you don’t automatically switch to it. To switch to a different branch, use the git checkout command:

PS D:\dev\replicant> git checkout alphatest
Switched to branch 'alphatest'

Any commits you make from this point on will be recorded in the alphatest branch, until you change branches once again. Only one branch of a repo can be checked out at a time.

You can see which branch you’re currently on by typing git branch (no options):

PS D:\dev\replicant> git branch
* alphatest
  master

The asterisk indicates the currently selected branch.

Note that when you switch branches, any uncommitted changes you made in the previous branch will be overwritten by the new current branch. For instance, if we changed a file but didn’t commit the changes, and then tried to switch back to main, we would get this warning:

PS D:\dev\replicant> git checkout main
error: Your local changes to the following files would be overwritten by checkout:
        README.md
Please commit your changes or stash them before you switch branches.
Aborting

If you don’t want to commit your changes yet, you can stash them in a kind of temporary holding area. Or you could create a new temporary branch and commit your changes there.

Merging branches

When you want to merge changes from one branch into another, follow these steps:

  1. Commit any changes that need to be made to the branch you’re on.
  2. Make a note of the most recent ref for the branch (just the first seven digits, such as 128a7da). You can see this information by typing git log -n 1.
  3. Switch to the branch you want to merge into.
  4. Use the command git merge <ref> to merge the commit from the previous branch into the current one.

If the merge is successful, you will see a message indicating so:

PS D:\dev\replicant> git merge 128a7da
Updating f109105..128a7da
Fast-forward
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Reconciling merges in Git

By default, Git tries to reconcile the two branches by moving the head for the old branch to the head of the new one — a “fast-forward,” as it’s called. If no other commits have been made to the old branch, that’s the easiest and fastest approach, hence the name. The end result is that all of the commits made to the new branch now show up in the old branch as well.

Another way to merge branches is with git merge --squash <ref>, which takes all the commits made to the new branch and flattens or “squashes” them into a single commit to the old branch. This is useful if you have a great many commits and you want to simplify them so that the history of the branch isn’t hard to read.

If branches cannot be merged cleanly, because each branch contains changes that might contradict the other, you need to reconcile the two. One common way to do this is to specify a merge strategy. For instance, one strategy is to have everything merged in take precedence over everything already in place.

Another strategy is to let Git flag what it cannot reconcile, and then perform the reconciliation yourself, by editing the files in question. See the “Basic Merge Conflicts” section of Chapter 3.2 of the Git book for more on how to do this.

Pulling changes from a remote repository

If you’re working on a clone of a repo and you want to synchronize your local copy with the remote copy, you can pull any changes from the remote copy.

When you clone a repository, the source you cloned it from is called a remote branch. Cloned repositories keep references to the remote branches they were cloned from, which you can list by running git remote show.

The default name for a remote branch is “origin,” so you can synchronize changes from that remote branch by typing git pull origin.

PS D:\Dev\r2\replicant> git pull origin
Already up to date.

“Already up to date” means there are no differences between your local repo and the origin.

If there are changes, you will see a summary of those changes and how they were applied. For example:

PS D:\Dev\r2\replicant> git pull origin
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), 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), 285 bytes | 1024 bytes/s, done.
From d:\dev\replicant
   128a7da..f1af831  main     -> origin/main
Updating 128a7da..f1af831
Fast-forward
 README.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

Note that a git pull is just like merging from another branch. If you’re pulling in changes that continue from the head of your codebase, you won’t have a problem. But if you’ve made changes locally, Git will have to reconcile them with the remote branch, and if it can’t, you'll have to do that by hand — as we discussed in the last section.

Pushing changes to a remote repository

When you want to upload changes from your local copy of a repository to a remote one, you push the changes using git push. For example:

PS D:\Dev\r2\replicant> git push origin
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 304 bytes | 304.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0        
To d:\dev\replicant
   e5b801d..db31abc  main -> main

Note that you can only push changes to a repository that no one else has pushed to since you last pulled from it. Otherwise, you will have to git pull and merge those changes with your local repo before you can push.

Pushing has a few limitations:

  • You cannot push to a branch of a repository that is currently checked out.
  • You cannot (by default) push safely to a “non-bare” repository, meaning a repo that was not created with git init --bare.

Using pull requests in Git

As you probably gathered, pushing and pulling doesn’t cut it for active repositories being worked on by multiple developers. For collaborating development teams, the more common way to merge one’s changes to a repo is to perform a pull request.

Let’s say you have a clone of a repo which has a main branch (named “main”) and your own branch (named “dev”) where you’ve created some changes. You want the owner of the original repository to take your changes in dev and apply it to their copy of main.

To do this, you run git request-pull on your branch. This generates a report to send to the maintainer of the repository that will allow them to incorporate your changes if they wish.

PS D:\Dev\packer2\pypacker> git request-pull main https://github.com/syegulalp/pypacker dev
The following changes since commit 679c8c16e6dd0a5e8d6029baf04cc5ea713e48bc:

  additional usage notes (2021-11-04 11:54:02 -0400)

are available in the Git repository at:

  https://github.com/syegulalp/pypacker dev

for you to fetch changes up to 9a6f161f8dfed40b3d9106973eb10ff2f868f6f0:

  cleanup (2021-11-30 11:23:13 -0500)

----------------------------------------------------------------
Serdar Yegulalp (1):
      cleanup

 pypacker/__main__.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

In this example, we have a local clone of the replicant repository. We generate a pull request for everything from the head of the main branch to the head of our local development branch, dev.

Most code hosting sites, like GitHub, have utilities for automatically generating pull requests to simplify this process.

Copyright © 2021 IDG Communications, Inc.