This workshop is designed to take you through a Git workflow that is commonly used
for collaborative software development; however, much of this knowledge is applicable
when developing individually. It's not meant to be an exhaustive reference, and
there are a lot of flags and variants on these commands that we don't cover. The aim
is to familiarize you with the logic behind Git and to teach you the most frequently
used commands, so that you have a working knowledge.
You'll be adding your name to the list of participants in index.html, which
you can see here. For those curious, the site is hosted on
Netlify.
Note: code blocks where each line is preceded by $, like:
$ sudo apt-get update
$ sudo apt-get install git
are a stylistic convention used to display terminal commands. Do not enter the $
when copying these commands.
- Setup
- Why Git
- Concept Overview
- Forking and Cloning
- Branching
- Making Changes
- Undoing Changes
- Getting Remote Changes
- Pull Requests
- Conflicts
- Create a GitHub account
- Install Git
$ sudo apt-get update
$ sudo apt-get install git
- Verify that the installation was successful with
git --versionin Terminal or Command Prompt - Configure Git with the following commands in Terminal or Command Prompt. This information will be attached to each commit you make.
$ git config --global user.name "Your Name"
$ git config --global user.username "yourusername"
$ git config --global user.email "youremail@email.com"
First, though, why version control? By using version control, you can progressively save "snapshots" of your work and easily roll back to a "snapshot" if something goes wrong. Additionally, these "snapshots" provide a history of development, which will make the project more maintainable in the long run and can be highly useful when adding new features or onboarding new team members. Lastly, version control provides a way for multiple people, even tens of thousands at the scale of companies like Google and Facebook, to contribute to the same codebase.
Git is one of the most commonly used version control systems, along with Mercurial and SVN. It has become the industry standard for open source projects, which are usually hosted on GitHub. Additionally, learning Git will allow you to share personal projects on GitHub, which is a great way to show off your abilities to potential employers.
Git calls the "snapshots" discussed above "commits". You want to commit frequently such that you have a complete and detailed history of development. However, each commit should represent a unitary change - don't commit the way you save in Microsoft Word. You should never commit half complete or broken code - add the class or complete the function, then commit.
You don't have to add every changed file when you make a new commit. Git uses a "staging area" which files are added to and then committed from. This allows for much more flexibility when committing, and enables you to split your commits up logically.
Now let's dive in! Press the "Fork" button in the top right corner of this repository.
This will produce a copy of the repository on your own GitHub account. This is a
common practice for open source projects, or other projects where you don't have
edit permission on the main repository. You'll be making changes in your own copy,
then submitting a "pull request" to propose changes to the main repository - more
on that later.
Next, we'll create a local copy of the repository on your machine. Navigate to your own copy of the repo on GitHub, press the "Clone or download" button, and copy the resulting URL. Next, open a Command Prompt or Terminal, navigate to the parent directory where you want your repo to be stored, and type the following:
$ git clone https://github.com/your-username/QTMA-git-tutorial.git
$ cd QTMA-git-tutorial
In addition to creating a copy of the repository on your machine, the clone
command also links it to your repository on GitHub. This is called a "remote",
and it has been named "origin". This allows you to send and receive changes to
your copy of the repo on GitHub. But what about changes being made to the original
repository? We can add another remote, which we'll call "upstream", pointing
the main repository.
$ git remote add upstream https://github.com/QTMA/QTMA-git-tutorial.git
If you're working on a personal project or on a centralized team where everyone has edit permission on the main repository, i.e. not open source, you can forego the forking step and addition of a second remote and simply clone the main repo directly to your machine.
When multiple people are working on the same project, conflicts can arise if people try to edit the same lines, someone deletes a file that someone else is using, refactors a function in a breaking manner, etc. Branches allow you to keep a set of commits separate from the main history, so that you don't need to worry about other people's changes breaking yours, and vice versa while a feature is still under development.
The branch command will create a new branch from whichever branch you are currently on.
Generally, we want to branch from master, which is the default branch, although there
are some workflows such as GitFlow
which use more complicated branching schemes. To view a list of branches, with the current
branch highlighted, use the following command:
$ git branch
After verifying that you're on master create a new branch to contain your feature
using branch, then switch to the new branch using checkout.
$ git branch <your-name>
$ git checkout <your-name>
Note that branches can be useful even when developing individually. If you are developing multiple features concurrently, it can be helpful to separate the development of each feature into its own branch in order to keep the main commit history clean and organized.
Now let's add your name to the participants list. In the text editor of your choice,
open index.html. Find this section:
<!-- Copy and paste this section and replace with your name -->
<div class="name-card">
<div class="checkmark-container">
<svg class="checkmark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52">
<circle class="checkmark__circle" cx="26" cy="26" r="25" fill="none"/>
<path class="checkmark__check" fill="none" d="M14.1 27.2l7.1 7.2 16.7-16.8"/>
</svg>
</div>
<p class="name">Your Name Here</p>
</div>
Copy and paste it below and replace the Your Name Here with your name. Now that
you've made your changes locally, you'll want to commit it.
As you're working on more complicated sets of changes that affect multiple files, you may want to check to see what you've touched and how Git perceives your changes.
$ git status
This command will show you which changes are staged for commit (already added to the current commit), and which have been modified but have not been staged.
$ git add .
This command stages all new files, modified files, and deleted files for commit.
$ git add <file name or directory name>
But git add can also be used much more granularly, by specifying individual files
or directories.
Say you didn't follow our advice and weren't committing along the way and now have a file with several modifications that you believe should logically be split up into different commits. Don't fret!
$ git add -p
git add has an interactive mode which allows you to select chunks within files
to commit, rather than having to commit the entire file/set of changes in a file.
It presents one chunk at a time and prompts for a command: y to stage the chunk,
n to ignore the chunk, s to split it into smaller chunks, e to manually edit
the chunk, and q to exit. Alternately, just press ? every time to print the
commands.
So now that you've staged your changes, you're ready to commit.
$ git commit -m "<commit message here>"
The -m flag means "message". If you have a lot to say you might want to omit
the flag. Using git commit without flags will pull up your terminal's default
text editor, usually Vim or Nano, and allow you to write your commit message.
Commit messages should be relatively concise and highly descriptive, so that reading
the commit history chronologically provides a coherent document of development.
While it's not necessary, adhering to some stylistic conventions will make reviewing
commit logs much nicer. A good rule of thumb is to write imperatively in the present
tense, with the first letter capitalized and without a period. This mimics Git's
own style when it automatically generates commit messages. For example, when
merging a branch, git generates the message:
Merge branch 'my-feature'
In instances where you want to commit all changes and add a message inline, you
can omit the separate git add command and simply use:
$ git commit -am "<commit message here"
Now that you've committed your changes, you can push them to your repository on GitHub. Generally, you wouldn't do this after every commit in case you wanted to clean up/squash/amend commits but that's not important for our purposes right now.
$ git push origin <your-name>
The git push command takes two arguments: the name of the remote you're pushing
to (remember that origin is your repo and upstream is the main repo), and
the name of the branch you want to push.
There are a couple of possible scenarios where you may need to undo some questionable commits. The most straightforward is if the shady commit is the most recent one, and it is still local (i.e. it hasn't been pushed to a remote).
$ git commit --amend
This will collapse all of your staged changes into the most recent commit, and allow you to write a new commit message. This is also useful is you notice that there's a typo in your commit message that you'd like to change - invoking the command without any staged changes will simply allow you to rewrite your most recent commit message.
If you have a messed up several local commits and need to find the most recent clean one, you'll have to go digging.
$ git log --oneline
This command will display will display a compact commit history. Copy the hash
(something like bd60c14) of the commit you'd like to step back to.
$ git reset --hard <commit hash>
Invoking the above command will effectively erase all commits after the specified commit, as if they had never happened. You should only use this locally. Obviously, a commit disappearing when others depend on it could be problematic, and Git will block you and complain about the local branch being out of date when you try to push it to a remote, because of the missing local commits.
If you're working on a public repository, the commit has already made it to the remote,
etc., git revert is the preferred option. Unlike git reset, where all subsequent
commits are deleted, git revert undoes a single, specified commit.
$ git revert <commit hash>
Using this command produces a new commit which is the inverse of the changes made
in the offending commit. Because of this, git revert is nondestructive and can
be used safely on public repos.
Remember when you created a new remote called upstream which links back to the
original/main repo? We're going to make use of this now to fetch changes that the
QTMA exec has made to it while everyone was working.
$ git remote
This will list all of the remotes. We know that the one we're looking for is called
upstream, but if for whatever reason you can't remember, this is useful.
There are a couple of options for getting remote changes. git fetch will download
the changes but will not join them together with your local copy - you can git merge
or git rebase manually after. More on that later. Alternately, you can use git pull,
which is functionally equivalent to doing a git fetch then a git merge.
If you wanted to get the upstream changes to master into your local repo, you
could do the following:
$ git pull upstream master
Your local master branch would now be up to date with the upstream master.
The fundamental difference between merging and rebasing is that rebasing will
essentially rewrite your commit history, moving your changes on top of the
missing changes on the different branch you're rebasing on top of. Merging
will create a new commit which encapsulates all of the changes. In the following
example, suppose that there has been a change to the upstream master. You'd
like to get those changes into your local feature branch, without creating a
merge commit. You fetch all upstream changes on all branches, but you don't
combine them with your local branches, so you now have copies of them locally with
the names upstream/<branch-name>. You check out your feature branch, then rebase
on top of upstream/master so that the changes you made come after the changes made
to master in a linear commit history.
$ git fetch upstream
$ git checkout <your-name>
$ git rebase upstream/master
Consistently rebasing changes that make their way to master on to your feature
branch is a good way to address possible conflicts as they occur so that you don't
need to deal with big merge conflicts when you're finished with your feature.
Additionally, it helps keep the commit history clean. Alternately, you could do:
$ git checkout master
$ git pull upstream master
$ git checkout <your-name>
$ git rebase master
In this case, you combine upstream master with your local master, then rebase,
rather than keeping a copy of a upstream branches without merging them into their
local equivalents. Ultimately, consistently rebasing your feature branch on top
of master will prevent the majority of merge conflicts. Either approach works,
but the latter has the benefit of also keeping your local master up to date with
upstream changes.
Pull requests serve as a way to combine your branch with the master branch, when you're finished working on it. More than that, though, pull requests are a place to discuss the code, request changes, ask for better test coverage, etc. Both the forking workflow, which we've covered here and is common in open source projects, and the feature branch workflow, which is common with private teams and has feature-branches directly on the main repo, pull requests are used.
Create a pull request from the GitHub page of your forked repo. Selecting your feature branch as the source branch and the original repo's master branch as the destination branch.
You don't need to use pull requests, especially if you're in a very very small team, working on a small project where you don't want to spend extensive time reviewing each others' changes. This assumes you're not using the forking workflow. In this case, you could just merge your feature branch into master locally, delete your feature branch, then push your changes back to origin. This is not advisable for projects of any size, but might be fine for small things.
Of course, conflicts are hard to avoid entirely. If git complains about an attempted
pull request, you'll have to fix the conflict manually. Open the offending files
in the text editor of your choice, then look for lines like >>>>>> and ======
which mark conflicts. Manually edit the file to resolve the conflict, save,
and commit normally. There's no secret sauce to resolving conflicts and the
multipane view that Git sometimes uses can be more confusing than helpful. All you
need to do is edit the file so that only one set of conflicting changes is kept.