4
By the end of this chapter, you will be able to:
This chapter describes the working of branches, workflows and conflict resolution.
In the previous chapter, we explored how local repositories are able to connect to a remotely hosted repository. We covered the git push, git fetch, and git pull commands, which facilitate the retrieval of changes and uploading of changes to the shared repository. Lastly, we explored the git revert and git reset commands, which rescind changes.
In this chapter, we'll look at a common workflow that's utilized in version control, the feature-branch workflow. In doing so, we will also learn about branches and how changes are affected through the merging of branches and shipping the work to the user-facing product on your live environment. The topics covered in this chapter are geared toward demonstrating how version control fits into the picture, from when you start building a feature to when it's shipped.
Workflows refer to the approach a team takes to introduce changes to a code base. A workflow is characterized by a distinct approach in the use of branches, or lack thereof, to introduce changes into a repository.
Gitflow Workflow
This uses two branches: master and develop. The master branch is used to track the release history, while the develop branch is used to track feature integration into the product.
To introduce a feature, first, you must create a feature branch from the develop branch and then make changes in the created branch and commit those changes. Next, you should push the changes to the remote feature branch. Additionally, you must raise a pull request against the develop branch, and then follow up by resolving the feedback provided in the pull request. Afterward, merge the feature branch to the develop branch and create a release branch once an agreed number of features are merged to develop. Next, you should raise a pull request against the master. Lastly, you need to merge the pull request once it's been approved.
Hotfix workflow
To make a hotfix, first you must create a branch off the branch master, make changes, and commit them. Next, you can push the changes to the remote hotfix branch. Follow this up by raising a pull request against the master. Lastly, merge the pull request once it's been approved.
Centralized Workflow
This approach uses the master branch as the default development branch. The changes are committed to the master branch. It's a suitable workflow for small size teams and teams transitioning from Apache Subversion. In Apache Subversion, the trunk is the equivalent of the master branch.
To commission work and conduct development on a project, you need to initialize the central repository, host the central repository on GitHub, and clone the central repository. The next step is to make the desired changes and commit. Finally, push the changes to the central repository and manage the emergent conflicts by using the rebase utility of Git.
In this workflow, feature development is carried out in a dedicated branch. The branch is then merged with the master once the intended changes are approved.
To introduce a feature into an application, you should first initialize a repository with the default master branch. Then, you must clone the repository or pull changes from the remote master branch in the event that you have the repository locally. Next, create a new branch, make changes, and commit the changes. Then, push the feature branch to the remote repository and raise a pull request. Lastly, resolve the feedback provided in the pull request review and merge the pull request.
A forking workflow takes the following approach in the development of features or in the general introduction of a change to the code base. Initially, you must fork an official product repository to your account on GitHub and clone the forked repository to your local environment. Then, create a new branch for the feature, make changes, and commit the changes. Next, you should push the changes to the remote cloned repository and raise a pull request against the official repository. Finally, resolve the feedback provided and merge the PR into the original/official repository. Merging is done by an authorized repository owner.
In this workflow, feature and maintenance-oriented development is carried out in a dedicated branch. The branch is then merged to the master once the intended changes are approved.
To ensure ease in the tracking of changes being introduced to the branch master, a naming convention is required for the branches that are created off of the branch master. The branch name should have an appropriate description, in a manner that indicates the change being introduced by a branch.
The following sample notation demonstrates the form a branch name assumes:
branch_type-task_description
Given that a project is undertaken using an issue tracking tool such as Jira, Trello, or PivotalTracker, the branch name should be suffixed by the issue number corresponding to the change the branch seeks to introduce.
Branch types include feature, bug, fix, and chore. Take a look at their abbreviations in the following table:
Branch Type |
Abbreviation |
feature |
ft |
bug |
bg |
fix |
fx |
chore |
ch |
To introduce a feature into an application, you should initialize a repository with the default master branch. Then, clone the repository or pull changes from the remote master branch in the event that the repository is local. Next, you need to create a new branch, make changes, and commit those changes. Then, follow up by pushing the feature branch to the remote repository and raising a pull request. Finally, round up by resolving the feedback provided in the pull request review and merge the pull request.
To roll out a feature using the feature-branch workflow, follow these steps:
Templates for task organization are available and may be used for scenarios where a bespoke ordering of tasks isn't required. For the purpose of this demonstration, we shall use the Basic kanban template.
git branch ft-support-exponents
git checkout ft-support-exponents
Live Link for file exercise_1_step_14.py: https://bit.ly/2QgHNRa
def exponent(self):
num_exponent = self.operands[0] ** self.operands[1]
print(num_exponent)
git add src/lib/compute.py
git commit -m "Add support for exponents"
git push origin ft-support-exponents
Add an apt title and description. In the description, indicate the issue that the pull request (PR) is meant to resolve. GitHub establishes the issue number when the said digit(s) is prefixed with a #. Conclude the process by clicking the Create pull request button, as shown in the following screenshot:
Outcome
By using the steps outlined in this exercise, you should be able to demonstrate the feature-branch process of change incorporation.
In git reset, we examined how Git stores a commit and the contents of a commit. The commit object stores a snapshot of the directories and files that constitute a repository at a given point in time. In addition, the commit specifies auxiliary information, which includes the parent commit of the created commit, the author, the committer date and time, and the commit message:
A branch is therefore a pointer to a snapshot of the repository. This pointer refers to the commit at the tip of the branch. These tips are the commit hashes stored in .git/refs/heads/. HEAD is the pointer that references the commit at the tip of the current branch. This commit is imperative because it's based on the fact that git is able to navigate the history of a repository with the help of the parent-child association between commits. The creation of a branch, in turn, creates a pointer and the head, which bears a branch's name. Navigating from one branch to another updates the HEAD to refer to the tip of the branch you switch to – or in Git terms, check out to.
git branch [branch_name]
git branch --set-upstream-to [remote_branch_name]
e.g. git branch --set-upstream-to origin/ft-support-exponents
git branch --unset-upstream [branch_name]
git branch [branch_name] [start_point]
Renaming:
git branch -m [old_branch_name] [new_branch_name]
git branch -M [old_branch_name] [new_branch_name]
This is similar to invoking git branch with the --move and --force options.
git branch -c [old_branch_name] [new_branch_name]
Copy:
git branch -C [old_branch_name] [new_branch_name]
This is similar to invoking git branch with the --copy and --force options.
Deleting:
git branch -d [branch_name]
Delete a branch, granted that it's fully merged into its upstream branch or the HEAD, in the event that the upstream branch is not specified.
git branch -D [branch_name]
This forces the deletion of a branch.
It's similar to using --delete --force.
Listing:
git branch --list
git branch --list [pattern]
For example, you can use git branch --list 'ft*'.
git branch --contains [commit]
For example, you can use git branch --contains 8354043.
git branch --no-contains [commit]
git branch --merged [commit]
For example, you can use git git branch --merged 8354043.
This lists branches that have been merged into a given commit, that is, commits whose tip is reachable from the given commit.
git branch --no-merged [commit]
This is used for branches that are not merged into the given commit.
git branch -a
This is used to get all branches:
git branch -r
This is used for the remote tracking of branches.
Switching to New and Existing Branches
The process of moving from one branch to another is done by switching to [branch] and then setting the files in the index and working tree to reflect [branch]'s latest commit. Lastly, you must set the HEAD to branch:
Modifications to the working tree are retained in case you wish to commit them in [branch].
git checkout [branch_name]
git checkout -b [branch_name]
git branch -B [branch_name] [start_point]
Create a new branch and set its tip to [start_point]. If the branch exists, then reset it to [start_point].
Switching to a Detached Head
Consider a rookie-day-one branch with the commits a-->b-->c. Commit a is the initial/first commit.
HEAD refers to a named branch. In this case, branch rookie-day-one refers to commit c.
a-->b-->c-->d
In adding a new commit, d, to the branch, HEAD refers to branch rookie-day-one, which is updated to refer to commit d:
Example:
git checkout b
HEAD now points to commit b:
echo 'Test HEAD' >> a.txt
git add a.txt && git commit -m "Test"
echo 'Test HEAD' >> a.txt
git add a.txt && git commit -m "Test 2"
Adding changes and commits by extension changes HEAD to refer to commit f.
git checkout rookie-day-one
e-->f
a-->b-->c-->d
The preceding command shifts HEAD back to a named branch, rookie-day-one.
As such, changes in commits e and f are discarded, given that no reference is created to point to the commit, for example, via the following:
git checkout -b sample
git branch sample
git tag v1.0
git checkout [commit_hash]
git checkout [tag]
git checkout --detach [branch]
This detaches HEAD from the specified branch and switches to the specified branch.
git checkout --detach [commit]
The preceding code detaches HEAD from the [commit] and updates the working tree and index to match the state at [commit].
Switching to a Specific Version of a File
When switching, the git check out command takes the following syntax:
git checkout [commit] -- [path]
Other uses of git checkout are as follows:
git checkout -b --orphan [new_branch] [start_point]
This creates a branch whereby the first commit has no parent. This is necessary when certain information contained in the repository's history needs to remain unexposed for privacy reasons.
Incorporating Changes with Stashing
In the book of development work, emerging requests are a typical occurrence, including in scenarios where you are attending a planned work stream over a specific period of time. The book of action in this scenario, normally, is to put aside what you're working on and attend to this request, be it an emergency or not.
How does Git enable you to "put away" what you're working on without losing the progress you'd achieved on a certain task? Ask no more.
In comes git stash. The git stash command temporarily moves staged, unstaged, or untracked modifications made to a repository, to and from the index and working directory.
To achieve this with git stash, use the following subcommands:
git stash push -m [message] or git stash push —message [message]
This saves modifications to a stash list and reverts the index and the working tree to the state reflected by HEAD.
The —keep-index option retains changes made to the index. This means that the modifications in the index are not reverted.
The —include-untracked option includes untracked files in the stash entry made to the stash list.
The —all option stashes ignored files in the stash entry made to the stash list.
git stash save was deprecated in favor of this command.
git stash list
This command lists the entries in the stash list. These are all of the created stashes:
Git stash show [stash_id]
This command displays the changes introduced by the stash identified by [stash_id].
git stash apply [stash_id]
This updates the working directory with the changes stored in [stash_id].
git stash pop [stash_id]
This updates the working directory with the stash [stash_id] and removes it from the stash list.
git stash drop [stash_id]
This removes the specified stash from the stash list.
git stash clear
This removes all stashes from the stash list.
git stash branch [branchname] [stash_id]
The stash list is available from every branch.
The order that the introduction of a change to a repository entails is as follows:
The exercise of merging is one of unifying the change into the main branch. This involves reconciling the divergent histories of the change branch and the feature branch, and creating a snapshot referenced by a commit to indicate the emergent result of incorporating the change branch.
A merge takes one of two modes, namely:
Where a merge is required from, branch-a to split, branch-b; in the event that there have been no changes in branch-a, Git adds the commits from branch-a to branch-b and updates the tip of branch-b (HEAD) to be the commit at the tip of branch-a. This is a fast-forward merge.
In merging two branches, that is, branch-a into branch-b, where there have been changes introduced into branch-b in the form of commits since branch-a was created off branch-b, that is, their shared ancestor, Git does the following:
This is a Three-way merge.
Merging is achieved by using the git merge command. This command uses the following syntax:
git merge [options] [branch_name]
--no-commit
This option merges changes into the current branch. However, the command does not create a merge commit in order to leave room for evaluating the result of the merge.
--edit
This option launches the editor to allow for editing of the generated commit message.
--no-edit
This conducts the merge using the message generated by the command.
--no-ff
This option creates a merge commit in all merge scenarios, including when the merge resolves to a fast-forward merge.
--squash
This option instructs git merge to update the index and working directory to reflect the incoming changes without creating a commit. Using this option enables you to create a commit as part of the current branch, thus referencing the incoming changes. With this option, the HEAD is not changed and the MERGE_HEAD ref is not recorded. As a result, the subsequent commit is not a merge commit.
--strategy=[strategy]
This specifies the merge strategy to be used for the merge.
The supported [strategy] includes
--strategy-option=[strategy_option]
This sets the option that's specific to the provided strategy.
The git cherry-pick command takes a commit from one branch and applies the specified commit to another branch.
git cherry-pick is useful when you wish to check the effects of certain changes that have been introduced to a branch you're working on.
The syntax of the git cherry-pick command is a follows:
git cherry-pick [options] [commit].
git cherry-pick supports options that dictate how the introduced commits are handled. This includes the following:
-x: This option adds a standardized message to the commit of the form "cherry picked from commit ..." to specify the commit that introduces the incoming change.
--edit: This allows you to edit the commit message for the incoming changes.
--no-commit: You may wish to integrate changes from a specific commit without creating a corresponding commit. The --no-commit command integrates changes from a commit without creating a commit.
--mainline [parent_number]: Since a parent commit possesses two parents, running a cherry-pick against a merge commit requires that a parent is specified. The given parent is compared to the merge tree of the merge commit and the resulting difference is introduced into the branch where git cherry-pick is invoked from.
Consider two branches, namely branch-1 and branch-2.
Branch-1 implements the add-to-cart feature, while branch-2 implements a system-wide refactor.
From branch-1, you can merge branch-2:
git checkout branch-1
git merge branch-2
This creates a merge commit.
The merge commit corresponds to a merge tree that combines the changes implemented by branch-1 and the changes introduced by branch-2. This merge commit, as is the case with all merge commits, has two parents. Each parent represents the series of changes implemented in each branch.
The --mainline option requires that a parent is specified in order to indicate which of the two sets of changes should be incorporated when a merge commit is specified:
parent-1 is the last commit made in branch-1, that is, the add-to-cart implementation.
parent-2 is the last commit made in branch-2, that is, the refactor.
Consider branch-3.
In this branch, we would like to test the effects of changes introduced by the merge commit in git check out branch-3.
Scenario 1
git cherry-pick -m 1 merge_commit
By specifying -m 1, we choose parent-1.
This is the difference, or diff in Git terms, between the merge tree of merge_commit and commit parent-1. This is the refactor.
As such, this command will introduce the changes that refactor the code base in branch-3.
Scenario 2
git cherry-pick -m 2 merge_commit
By specifying -m 2, we choose parent-2.
This is the difference, or diff in Git terms, between the merge tree of merge_commit and commit parent-2. This is the add-to-cart implementation.
As such, this command will introduce, the changes that add the add-to-cart feature in branch-3.
A pull request (PR) is the culmination of a piece of work undertaken on a branch. A PR is an intent to merge. Pull requests are intended to avail the forum where changes that are to be made to a repository's main branch are accorded scrutiny. Consensus is arrived upon the completion of a satisfactory discussion on the modifications that are borne by the branch that seeks to introduce changes.
To gain a clear picture of the changes that a PR seeks to introduce, we need to check the diff between two branches. Let's take a look at how you can achieve this.
To compare branches to establish impending changes, follow these steps:
Outcome
By following the preceding steps, you should be able to identify the changes that a branch introduces into another branch through a merged pull request.
In Chapter 1-Introduction to Version Control, we looked at the usage of issue templates to aid in the contribution process by setting a guideline for the process of the resolution of bug and feature requests. To ensure the seamless integration of changes being introduced to the primary branch of a repository, GitHub supports standardized pull requests by providing means for stating a format for pull requests. A template seeks to ensure that each pull request raised on the repository is clear in communicating what the potential change seeks to resolve. This lends itself to the appropriate discourse and comprehensive scrutiny, which in turn ensures that the result of the PR review process is a change that has been agreed upon by the relevant stakeholders. We shall now proceed and define a template that is to be used for raising a pull request.
The pull request template must adhere to the following canons:
1. The template should be stored in a repository's default branch.
2. The template should be stored in the repository's root directory, the docs directory, or the hidden.github directory.
To demonstrate the usage of templates in raising pull requests and organizing the application's pipeline, follow these steps:
git branch ch-add-pr-template
git checkout ch-add-pr-template
What issue does this pull request correspond to?
What is the acceptance criteria for the proposed solution?
#### Merging Checklist
- [ ] PR approved
- [ ] All checks pass
- [ ] Manual tests approved
git add PULL_REQUEST_TEMPLATE.md
git commit -m "Add pull request template"
git push origin ch-add-pr-template
git checkout master
git pull origin master
git checkout -b ch-add-usage-instructions
Getting started:
1. git clone [email protected]:[username]/abacus.git.
2. Navigate to the repository location.
3. Run python bin/runner.py.
git add READ.md
git commit -m "Add usage instructions"
git push origin ch-add-usage-instructions
Outcome
By following this exercise, you should be able to outline the steps that need to be adhered to for a pull request to be merged. Additionally, you should be able to lay out the format that a pull request's description follows.
As part of incorporating changes, certain checks need to pass for a pull request to be merged. These checks vary from repository to repository. In this section, we will explore failed status checks and merge conflicts.
Failed Status Checks
A PR may execute unit tests as part of a pre-merge sanity check. To proceed with the merging of PR, you need to ensure that the set checks pass. In this scenario, this means ensuring that unit and integration tests pass in the CI/CD builds.
As demonstrated in the following screenshot, failed checks need to pass for a merge to be conducted:
In Chapter 6: Automated Testing and Release Management, we will look into configuring tests that are to be triggered when we create pull requests and rectify failed builds when they occur.
Merge Conflicts
A merge conflict is a term that depicts an issue whereby modifications made in separate branches can't be amalgamated into one unit of change or modification.
A merge conflict will occur when:
Merge conflict resolution encompasses picking which of the differing sets of changes should be used in a merge process.
To demonstrate merge conflicts that are the result of multiple contributors changing the same line in a file, follow these steps:
git add PULL_REQUEST_TEMPLATE.md
git commit -m "Add PR instructions"
git checkout master
git checkout -b conflict-branch-2
git add PULL_REQUEST_TEMPLATE.md
git commit -m "Modify PR checklist"
git checkout conflict-branch-1
git merge conflict-branch-2
On the terminal, you will see the following message, indicating conflicting sets of changes:
Take a look at the format to be used:
<<<<<<< HEAD
Content in current branch
=======
Content from the incoming branch
>>>>>>>
git add PULL_REQUEST_TEMPLATE.md
git commit -m "Merge conflicting edits"
Outcome
Having followed these steps, you should be able to resolve a merge conflict that encompasses competing changes on the same line of a single file.
To demonstrate merge conflicts that are the result of the removal of a file in one branch and the changing of a file's content in another branch, follow these steps:
git add PULL_REQUEST_TEMPLATE.md
git commit -m "Add PR instructions"
git checkout master
git checkout -b conflict-branch-4
git checkout conflict-branch-3
git merge conflict-branch-4
In the Terminal, you will see the following message, indicating conflicting sets of changes:
git add PULL_REQUEST_TEMPLATE.md
or git rm PULL_REQUEST_TEMPLATE.md
then git commit -m "Merge conflicting edits"
Outcome
Following the preceding steps should enable you to resolve a merge conflict where the competing changes are the removal of a file in one branch and the modification of the same file in another branch.
With the prospective changes approved and all of the checks having been successful, a PR may be merged.
There are three modes available for merging pull requests, as shown in the following screenshot:
Consider a PR merging the feature-1 branch to the master branch:
Merge Commit
This mode adds commits c4 and c5 to the branch master using a unifying commit, c6, referred to as a merge commit. This is a non-fast-forward mode:
Squash and Merge Commits c4 and c5 are combined into a single commit, c6. The c6 command is then merged in the fast-forward mode:
Rebase and Merge
In this mode, each commit from the feature-1 branch is added to the branch master without the use of a merge commit:
To establish the process of reversing a merged pull request, follow these steps:
git checkout conflict-branch-1
git push origin conflict-branch-1
Clicking this button redirects you to a page so that you can create a revert commit.
Outcome
Having followed these steps, you should be able to create a pull request and reverse changes once a pull request has been merged.
You have been tasked with adding capabilities for computing speed and distance to your company's application. The features need to be handled separately and merged as a single work stream. You need to raise a pull request of the release-candidate branch you created in the activity in Topic 1: Utilizing Workflows. Merge the pull request once the changes have been approved. This is required since the tasks in this activity are a continuation of the work done in the activity in Topic 1: Utilizing Workflows.
The aim of this activity is to demonstrate being able to handle branches.
To start this activity, you need to have the Git command-line tool installed on your computer. Then, you need to have an account on https://github.com/. You should be logged into your account on GitHub. Lastly, you should have the abacus application repository on GitHub and your computer:
Outcome
The activity in the culmination of this topic should enable you to navigate branches as well as the snapshots represented by their respective commits.
For detailed steps for this activity, refer to the Appendix section on page 294.
In this chapter, you used the feature-branch workflow to implement units of work through branches. This workflow has also introduced you to the naming convention utilized to identify the nature of work a branch delivers. Using the git branch command, you've created, listed, and deleted branches. We've explored how to navigate different revisions of a repository and utilized the same revisions to selectively integrate changes into branches. We then looked at how to manage unstaged changes in the working directory. Lastly, we shipped the changes we've introduced by comparing branches, raising pull requests to merge desired changes, and reverting the changes where necessary.
In the next chapter, you will develop collaboratively on a remote repository, build application artifacts, and automate testing on GitHub. Additionally, you will develop new software version releases.
18.223.124.244