Skip to content

Instantly share code, notes, and snippets.

@rvalenciano
Created January 4, 2021 21:48
Show Gist options
  • Save rvalenciano/c0d7578349c38c60e4cf263cf543e39e to your computer and use it in GitHub Desktop.
Save rvalenciano/c0d7578349c38c60e4cf263cf543e39e to your computer and use it in GitHub Desktop.
Git workflow

Git Exercise

Preparation (First Time Contribution)

This section contains some of the preparation work before being able to start contributing to the repository.

Introduction to Git

The first and more important thing to know is that:

"Git is a distributed version-control system for tracking changes in source code during software development" - Wikipedia

Pay special attention to the highlighted word: distributed because Git is all about having multiple copies of the code (along with the history of changes) in different locations:

  • Your local development environment
  • The company Github account
  • Your Github account
  • ...

Pre-requisites

  1. To have a Github account (can be your personal account)
  2. Have the account configured to work with SSH †
  3. Have a terminal to work on
  4. Have Git installed
  5. Have Git configured:
  • git config --global user.name "Firstname Lastname"
  • git config --global user.email "[email protected]"

† Configuring SSH is beyond the scope of this document but Github has great documentation about this here.

In this section you will be doing the preparation work before you can start contributing to this repository.

Fork

The copy of this repository that lives in the BeenVerifiedInc account is what will be called canonical throughout this document.

The first step is to create a fork of it on your Github account. A fork is the Github term for a copy of a repository. Forking allows you to experiment without affecting the canonical project.

Because it is a Github term (not a Git term), forking has to be done directly in the Github UI:

(screenshot - PENDING)

Clone

Now that you have a fork of this repository, it is time to clone it to your local environment (e.g. your laptop).

$ git clone [email protected]:<your github username>/git-training.git

At this point you are aware of at least 3 copies:

  1. canonical in Github
  2. Your fork in Github
  3. Your local copy

When you cloned from your fork, Git did a few things for you:

  1. Created a directory
  2. Downloaded all the code
  3. Downloaded repository meta information (stored in a .git directory)

As part of the meta information, it also created what's called a remote.

Remote repositories are versions of your project that are hosted on the Internet or network somewhere. - Git Book

Remotes are places where you can pull code from or push code to.

In order to display existing remotes, you can use the following command:

git remote -v

The clone action also made some deliberate decisions for you, like naming this first remote origin and downloading the main branch of the remote repository which in almost every case is called master.

The status of the repository shows it:

git status

As part of downloading the main branch, it had to create one locally and also the respective remote one.

Check it out:

git branch --all

The above branch command can also be run without options to display only the local ones:

git branch

Or with the -r or --remote option to display only the remote ones:

git branch --remote

Add Canonical as a Remote

At this moment your master branch is probably sync-ed with canonical's master but at some point in the future that will no longer be the case. Specially when there is more than one person contributing to the same repository; imagine fellow developers adding changes to that master while you are still working on your feature.

Each time you start to work on something new, it is ideal that you add your work on top of the most recent version of canonical master that there is. Therefore, before doing any new work, you need to pull from that master.

For being able to pull from it, it first needs to be added as a remote:

git remote add canonical [email protected]:BeenVerifiedInc/git-training.git

When you list your remotes once again, the new remote shows up:

git remote -v

You can add more remotes, like each of your co-worker's forks. And the name you give to them is picked by you. When adding the BeenVerifiedInc, it was added with the name "canonical" just as a convention used within BeenVerified so that when one person suggests to another to run a command that involves using this remote, both can run the exact same command without having to adapt it. It is similar to how Git decided to name your fork "origin."

Thus, we currently have:

  • origin = your fork
  • canonical = BeenVerifiedInc's

Working on a New Ticket

Pull from Canonical's Master

As it was mentioned earlier, in order to minimize merge conflicts, new work needs to be based from the most recent version of canonical master.

There are two ways of retrieving it.

Option A: Fetch and Merge

When you cloned from your fork, it left your local copy set on the master branch and its upstream set to point to origin (i.e. your fork). This can be changed, but at the moment it is not required.

The first thing you are going to do is fetch all of what has changed in the canonical remote:

git fetch canonical

This updates all your canonical remote branches, all those named canonical/<branch name>, including canonical/master. What this means is it downloads the content of all those branches from Github's BeenVerifiedInc copy of the repository and stores them in the .git directory of your local copy. Making them available for you to use.

The next thing to do is merge the latest changes of that remote master branch into your master:

git merge canonical/master

If you always work with your local master branch in an organized way and you and your team mates are disciplined when working with the canonical master branch (i.e. not altering it's history), you should always be able to do the above merge without issues.

But if when trying to merge, there are conflicts, then you can instead reset your local master to have exactly the same history that the remote one has but keeps all changes, just uncommitted.

git reset canonical/master

If your local master has changes that were added by mistake and can or should be discarded, you can do a hard reset.

git reset --hard canonical/master

Just beware that reset gets rid of all the local commits that you had. It overwrites the history and leaves you with the code and history identical to the branch you are reseting from, in this case canonical/master.

This information may be a little overwhelming at this point but reset and changes to the history will be covered in more detail later on.

Option B: Pull

The pull command combines fetch and merge in one, but making use of the preconfigured upstream of your local branch.

Currently your upstream is set to origin master, then you need to instruct Git to use another one. We do this by using the -u flag when invoking the pull command †.

git pull -u canonical master

The above command is equivalent to:

git fetch canonical
git merge canonical/master

As you can see, pull is more convenient but less flexible. It doesn't provide the option of doing a reset or a hard reset in case the merge doesn't happen smoothly. For this reason it is important to understand both options.

† The upstream can be set with -u in the same way when using the push command.

Create Feature Branch

What is a branch

Branches have been mentioned a lot throughout this guide but without providing much information of what their meaning is in Git.

Think of them as "parallel universes". Each with their own timeline.

Going back to the definition of Git, it is "a version-control system". It keeps track of all the changes made to files in a repository; it does so by taking snapshots of the repository at different points in time. These snapshots are called commits.

The full set of commits make the history of the repository. Continuing with the analogy, let's say the commit history is the universe's timeline.

But Git is a "multiverse" and it contains multiple copies of a repository (multiple universes), each having it's own timeline.

New "parallel universes" can branch out from a pre-existing one. The new one shares the history of its originator up to the instant it spawned, beyond this point, it starts to have its own history.

Visually explained:

master:     0--1--2--3--4--5--
                    \
new branch:          `--3'--4'--

They share the same history until 2, but then the new branch becomes an "alternate reality" of its originator.

Back to Earth

Before starting to work on a new feature or bug-fix, it is a good practice to create a branch with a descriptive name for it:

git checkout -b sign-guestbook

The git checkout command allows you to bring up another branch, but when passing it the -b flag, it creates a new branch. And that is exactly what you just did.

So you just created a new branch and pulled it out.

Remember you can always check at which branch you are at with the git status command. Please run it and make sure you are at the newly created branch.

Add, Commit, Add, Commit, Add, Commit ...

Make your very first change

This repository has a guestbook/ directory that keeps track of every person that has fulfilled this exercise.

The first change that you will add to this repository is creating a file with your name to it. The name of the file will be as your user name in your BeenVerified email address and the .md extension.

cd guestbook
touch <username>.md

Check the status of the repository:

git status

It informs of the new file added.

In your favorite editor, let's open the newly created file and add the following text:

- [X] Create my guestbook file
- [X] Check the git status
- [X] Add text to it
- [ ] See the diff when only new files are added
- [ ] Modify file after it was staged
- [ ] Check the status when part of the file's changes is staged
- [ ] See the diff when a file is partially staged
- [ ] Add the rest of the changes
- [ ] Create my first commit
- [ ] Create my second commit

Save it and run:

git diff

Shows nothing. Although there are changes, it is only a new file (utracked), so there's nothing to compare it against. The file has never been tracked yet.

Then run:

git status

Again nothing new because the file isn't tracked, so to Git the change is that there is a new file, it doesn't know anything about its contents changes from the moment it was created to the moment it was modified.

Update the file to mark this dull step as done:

- [X] Create my guestbook file
- [X] Check the git status
- [X] Add text to it
- [X] See the diff when only new files are added (it is empty)
- [ ] Modify file after it was staged
- [ ] Check the status when part of the file's changes is staged
- [ ] See the diff when a file is partially staged
- [ ] Add the rest of the changes
- [ ] Create my first commit
- [ ] Create my second commit

Stage it

Git provides a staging area which is very well explained here (in the first two paragraphs).

So instead of just creating a commit straight from what you just changed in the repository (add a new file), the change needs to be staged first:

git add <username>.md

Check the status again:

git status

It now tells us that we have staged changes or "Changes to be committed".

We can check what changed from the original status of the repository to what is staged with git diff but with the --staged flag:

git diff --staged

In this case it does inform us that there is a new file.

Let's modify the file again by marking that task as done.

- [X] Create my guestbook file
- [X] Check the git status
- [X] Add text to it
- [X] See the diff when only new files are added (it is empty)
- [X] Modify file after it was staged
- [ ] Check the status when part of the file's changes is staged
- [ ] See the diff when a file is partially staged
- [ ] Add the rest of the changes
- [ ] Create my first commit
- [ ] Create my second commit

And check the diff again:

git diff

It now shows what was recently added.

If we check the status:

git status

It lists the same file in both sections: staged and not staged.

Let's mark four more tasks as completed.

- [X] Create my guestbook file
- [X] Check the git status
- [X] Add text to it
- [X] See the diff when only new files are added (it is empty)
- [X] Modify file after it was staged
- [X] Check the status when part of the file's changes is staged
- [X] See the diff when a file is partially staged
- [X] Add the rest of the changes
- [X] Create my first commit
- [ ] Create my second commit

Commit it

The commands for the latter two tasks:

git add <username>.md
git commit -m "Add <username>.md file to guestbook directory"

Congratulations! You just started altering the new alternate reality that you spawned.

Your commit is now part of this new history and this can be corroborated by running:

git log

When running git status, your working tree should show as "clean" again.

By now you should already have realized that git status and git diff are your friends. You can run those commands as many times you need in order to be sure you are adding and committing what you intended to.

Good Commit Messages

You just created a commit with a message that describes what it changed in the repository. The message could have said whatever you wanted to, even nonsense and it could have had any format like all caps or all lowercase, but you need to always keep in mind that commit messages are a mean of written communication between team members and as such, it requires to have good qualities:

  • Informative
  • Succint
  • Professional grammar, punctuation and spelling
Format

Git commit messages are composed of a subject and a body, just like email messages. In fact this is how some teams distribute changes: by exchanging patches through email †.

For setting the message of the commit that you just created, you used the -m option flag which allows you to write the message right there.

Because you only provided one -m then the message only had one paragraph, but it could have had more than one.

git commit -m "First paragraph (subject)" -m "Second paragraph (part of the body)"

Git takes the first paragraph as the subject and the rest of them as the body of the message.

But for when you want to include a body in your commit message, Git offers a more convenient way of doing so by calling commit without the -m option flag.

git commit

It will launch your predefined editor so you can write the full message in there and after you save and close the document, the commit is created.

The predefined editor can be set with:

git config --global core.editor vim

Going back to the format, here's a model commit message courtesy of Tim Pope and also used in Git's book:

Capitalized, short (50 chars or less) summary

More detailed explanatory text, if necessary.  Wrap it to about 72
characters or so.  In some contexts, the first line is treated as the
subject of an email and the rest of the text as the body.  The blank
line separating the summary from the body is critical (unless you omit
the body entirely); tools like rebase can get confused if you run the
two together.

Write your commit message in the imperative: "Fix bug" and not "Fixed bug"
or "Fixes bug."  This convention matches up with commit messages generated
by commands like git merge and git revert.

Further paragraphs come after blank lines.

- Bullet points are okay, too

- Typically a hyphen or asterisk is used for the bullet, followed by a
  single space, with blank lines in between, but conventions vary here

- Use a hanging indent

http://alblue.bandlem.com/2011/12/git-tip-of-week-patches-by-email.html

Subject Line

It is expected to:

  • Inform about what changed.
  • Start with capital letter.
  • Be 50 or less characters long.
  • Start with imperative (to match Git's auto-generated commit messages).
  • Don't end with a period.

It should not contain a ticket number or a "look elsewhere" reference because:

  • Takes valuable space from the subject line.
  • A ticket number does not express what changed by itself, it adds a dependency.
Body

It should mostly express the why of the change because the what is summarized in the subject and can also be seen directly in the code changes.

It can contain the "look elsewhere" reference because space isn't limited and a ticket may help to add information an context about the why. Some teams have the convention of specifying the ticket number in its own paragraph, the very last paragraph. This is specially useful when the tracker tool has the ability of reading this information and automatically creating a hyper-link.

The body isn't always necessary, specially in the case of small changes that don't require much justification or explanation. In such cases it is faster to just use the -m flag.

Continue making changes

Let's open the document again and mark the last task as done.

- [X] Create my guestbook file
- [X] Check the git status
- [X] Add text to it
- [X] See the diff when only new files are added (it is empty)
- [X] Modify file after it was staged
- [X] Check the status when part of the file's changes is staged
- [X] See the diff when a file is partially staged
- [X] Add the rest of the changes
- [X] Create my first commit
- [X] Create my second commit

Then add and commit:

git add <username>.md
git status
git commit -m "Mark last task of <username>.md as completed"

You can check the log once again to corroborate.

Push

At this point, you have signed the guestbook but only in your local copy of your "alternate reality". It is a good idea to frequently back-up your changes and doing so is just one command away:

git push origin sign-guestbook

What push does is take your current local branch sign-guestbook and merges it into the remote branch that you specify to it. You specify which remote branch by telling it the name of the remote and the name of the branch.

If the remote branch doesn't exist yet, it creates it.

There is also a -u flag that you can use to tell it to mark that remote branch as the default upstream for current local one.

git push -u origin sign-guestbook

Then, going forward, you don't need to specify the remote branch when doing a push or pull from this branch.

Pull Request

Now that you have an alternate reality with two "milestones" that aren't part of the history of the original timeline (canonical), but your timeline went so well that you want that new history to be part of the original one.

Then you request the owner or administrator of the canonical one to pull from your branch into theirs.

Curious fact: Git has a command request-pull that produces a written message that you can send to someone else inviting them to use git pull. So the concept of a pull request is as simple as that but nowadays people associate it more with GitHub's sophisticated UI feature.

For such reason, Git is now kind and smart enough to provide you a link for creating a pull request in Github the first time that you push your local branch to a remote (only if your remote has a github.com address).

Then let's take advantage of that and use the link for creating your first one.

Components

Base branch

Into which branch do you want the administrator of canonical to merge your changes.

You need to specify which remote (Github respository: BeenVerifiedInc/) and the branch name (usually master).

Compare branch

Which branch is the one that you want to merge.

You need to specify which remote (Github respository: /) and the branch name.

Subject & Body

Unless you tell Github to rebase upon accepting the pull request, it merges and forces the creation of a merge commit. Such commit follows similar rules and structure of normal commits presented earlier in this training.

Purpose

The idea of the pull request is to act as a control gate and allow other team members to review the changes before incorporating them into the main banch of the project.

Github offers advanced UI tools for helping in the review process, like the ability to write comments about sets of line(s) of code, request modifications or finally approve and merge the changes.

After all is good and the pull request is merged, then the ticket can be considered code complete.

Merge

PENDING

Solving Merge Conflicts

It is important to understand why this happens. Git is smart enough to solve simple conflicts but sometimes it needs human input to make a call. This commonly happens when the exact same lines were edited in a file. Git has no context, therefore it cannot know which change is the correct one. Here is where the developer input is needed.

Types of conflicts:

While checking out a branch:

  • Current changes in your current working directory / staging area could be conflicting with the state of a branch. This commonly happens when you made a change to a file and then you try to checkout a different branch (like staging) where that file also was changed. (Changes should be on the same lines for this to happen, otherwise git auto merges both files and leaves it as unstaged)

While trying to merge with a branch:

  • A failure DURING a merge indicates a conflict between the current local branch and the branch being merged. This indicates a conflict with another developer's code. Git will do its best to merge the files but will leave things for you to resolve manually in the conflicted files.

Fixing a merge conflict

Some things to keep in mind:

  • First and foremost: Keep calm 🧘
  • The process of resolving/merging a conflict is totally normal and can be undone at any moment (don't panic): $ git merge --abort
  • Conflicts are just annotations or marks of where in a file a conflict is present
  • Git denotes conflicts by using the following symbols: <<<<<<< HEAD1, =======, >>>>>>> [branch name]
  • You can use $ git status to know which files are marked as conflicting
  • When a conflict occurs, you open the concerned file with your favorite text editor and resolve said conflict from there

Note: You can also use specialized tools for handling conflicts (links below). Some text editors / IDEs also have build-in tools for solving conflicts but for learning purposes, we will continue to use git commands to achieve this.

Are Merge Conflicts Avoidable?

Sadly, no. But you minimize the impact to some degree if you fetch remote changes frequently from the main branches and then handle the changes upstream. While you may need to resolve merge conflicts more frequently, this means that you'll be resolving smaller conflicts each time albeit a lot easier to resolve. Also you will be more aware of other changes that are ocurring to the codebase.

World Peace: a conflict at a time ⚔️

Now a simple example of how to solve a conflict (merging branches) First, let's create a conflict! Using the current branch where this file is in (sign-guestbook), let's create and checkout a new branch and add some new content on this README.

$ git checkout -b conflict_maker

Then using your text editor of choice, update the title of the README.md file to "Git Conflict" and run the following.

$ git commit -am "Update title of README.md"
[conflict_maker 123456] Update title of README.md
1 file changed, 1 insertion(+), 1 deletion(-)

Current checklist:

+ [X] Create a new 'conflict_maker' branch
+ [X] Update the README.md file with a new title
+ [X] Commit change to title

We will now use this branch to generate a conflict with the base branch (sign-guestbook) but first, let's go back to our base branch and add some new content to the README. The following commands will create a new commit in sign-guestbook and will put the repo in a new state with 2 commits

$ git checkout sign-guestbook
Switched to branch 'sign-guestbook'

Let's update the title of the README.md file to "Git Exercise 2.0" and run the following.

$ git commit -am "Add new title to README.md"
[sign-guestbook 123abc] Add new title to README.md
1 file changed, 1 insertion(+)

So far:

[X] Create a new 'conflict_maker' branch
[X] Update the README.md file with a new title
[X] Commit change to title
+ [X] Checkout to 'sign-guestbook'
+ [X] Add a new title to README.md
+ [X] Commit change to title

Now, if we try to merge the new branch with sign-guestbook we will get...

$ git merge conflict_maker
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

The often times dreaded CONFLICT message, but that's ok. We can solve this conflict very easily. First, let's start by doing a quick status check

$ git status
On branch sign-guestbook
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified:   README.md

Current checklist:

[X] Create a new 'conflict_maker' branch
[X] Update the README.md file with a new title
[X] Commit change to title
[X] Checkout to 'sign-guestbook'
[X] Add a new title to README.md
[X] Commit change to title
+ [X] Run git merge and check the status of the merge (Conflict!)
+ [X] Check status of merge

The current status shows that there are unmerged paths due to a conflict. README.md is clearly marked as modified and needs to be rectified Now using your favorite editor, open the README.md file to see what the conflict is all about. You should see something like this near the title section

<<<<<<< HEAD
Git Exercise 2.0
=======
Git Conflict
>>>>>>> conflict_maker

Like noted before, conflicts are marked by Git by using the <<<<<<<, =======, >>>>>>> dividers. The ===== represents the center of the conflict, the first section denoted by <<<<<<< and ======= will be the content of the current branch you are on (commonly refered as HEAD). Likewise whatever is after the ======= but before the >>>>>>> will be the new merging branch content.

Solving this conflict should be pretty straightforward as you just need to delete the version you don't want to keep (for this case let's drop the "Git Conflict" title)

- <<<<<<< HEAD
+ Git Exercise 2.0
- =======
- Git Conflict
- >>>>>>> conflict_maker

Once the file is correct, stage and commit the changes.

$ git commit -m "Merge and resolve the conflict in README.md"

Wrapping up:

[X] Create a new 'conflict_maker' branch
[X] Update the README.md file with a new title
[X] Commit change to title
[X] Checkout to 'sign-guestbook'
[X] Add a new title to README.md
[X] Commit change to title
[X] Run git merge and check the status of the merge (Conflict!)
[X] Check status of merge
+ [X] Resolve conflict between files
+ [X] Commit the new conflict-free README.md

That's it! One less conflict to worry about. 🕊️

Time Travel: Change the History

Commit Hashes

Amend

PENDING

Reset

Rebase

PENDING

Rebase from Remote

Sometimes you start working on a branch but between you started it and the moment it is ready for merging with master, somebody else updated that master so you need to rebase.

PENDING

Interactive Rebase

PENDING

Considerations

PENDING

Concepts (Glossary)

  • branch
  • canonical
  • commit
  • HEAD
  • remote
  • upstream

Footnotes

  1. HEAD is a reference to the last commit in the currently check-out branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment