Skip to content

Trunk-Based Development (TBD)

Estimated time to read: 16 minutes

Git trunk-based development is a popular branching strategy that focuses on a single main branch called the trunk. The trunk branch serves as the central repository for all code changes, and it is always in a releasable state. Developers commit their changes directly to the trunk, and regular integration and testing occur to ensure the codebase remains stable.

Here are the steps for implementing the Git trunk-based development lifecycle:

  1. Create a Git repository: Create a new Git repository on your local machine or a remote repository server like GitHub, GitLab, or Bitbucket.

  2. Create the Trunk branch: Create the trunk branch and make it the default branch for the repository. In Git, you can create a new branch using the following command:

Bash
git branch trunk
git checkout trunk
  1. Develop a new feature: Start developing a new feature by creating a new trunk branch. The new branch is where you make your changes to the code. You can create a new branch using the following command:
Bash
git checkout -b feature-branch
  1. Commit Changes: Make your code changes in the feature-branch and commit them to the feature branch using the following command:
Bash
git add .
git commit -m "commit message"
  1. Push changes to the feature branch: Push your changes to the feature-branch in the remote repository using the following command:
Bash
git push -u origin feature-branch
  1. Merge changes to the Trunk branch: Once you've completed your feature development and testing, merge the feature branch back into the trunk branch using the following command: (We are using --no-ff)
Text Only
git checkout trunk
git merge --no-ff feature-branch

The --no-ff option ensures that a merge commit is created, which helps maintain a clear history of the codebase.

  1. Test and release changes: After merging changes to the trunk, run tests and perform any necessary quality checks to ensure the codebase remains stable. Then, deploy the changes to production.

  2. Repeat: Start the cycle again by creating a new feature branch and repeating steps 3-7.

Trunk base development using Tags

Tags are a useful way to label specific points in your Git repository's history, such as releases or milestones. Here's an example of the Git trunk-based development lifecycle that includes creating a tag:

Bash
# Step 1: Create a new Git repository
git init my-project
cd my-project

# Step 2: Create the Trunk branch
git branch trunk
git checkout trunk

# Step 3: Create a new feature branch and start developing a new feature
git checkout -b feature-branch
# ...make changes to the code...

# Step 4: Commit changes to the feature branch
git add .
git commit -m "Implemented feature X"

# Step 5: Push changes to the feature branch
git push -u origin feature-branch

# Step 6: Merge changes to the Trunk branch
git checkout trunk
git merge --no-ff feature-branch

# Step 7: Create a tag for the release
git tag v1.0

# Step 8: Test and release changes
# ...run tests and perform quality checks...
# ...deploy the changes to production...

# Step 9: Push changes to the remote repository
git push origin trunk --tags

# Step 10: Start again with a new feature branch
git checkout -b feature-Y
# ...make changes to the code...

In Step 7, we created a tag called v1.0 for the release. You can choose any name you like for the tag, but it's a good practice to use semantic versioning to keep track of releases.

In Step 9, we pushed the changes to the remote repository along with the tag using the --tags option. This will ensure that the tag is also pushed to the remote repository, so other developers can access it.

Dealing with Merge conflicts

Git is designed to handle merge conflicts when two or more developers make changes to the same file at the same time. Here's how you can resolve a conflict when someone else has merged changes before you:

  1. Fetch changes: Before you start working on your feature branch, fetch the latest changes from the remote repository using the following command:
Bash
git fetch
  1. Create a new feature branch: Create a new feature branch off the latest trunk branch using the following command:
Bash
git checkout -b feature-branch
  1. Make changes and commit: Make your code changes in the feature branch and commit them using the following command:
Bash
git add .
git commit -m "commit message"
  1. Fetch changes again: Before you merge your feature branch back into the trunk branch, fetch the latest changes from the remote repository again using the following command:
Bash
git fetch
  1. Merge changes from the trunk branch: Merge the latest changes from the trunk branch into your feature branch using the following command:
Bash
git merge trunk
  1. Resolve conflicts: If Git detects conflicts between your changes and the changes in the trunk branch, it will prompt you to resolve the conflicts manually. Open the file(s) with conflicts in your code editor and look for the sections labelled <<<<<<<, =======, and >>>>>>>. These sections show the conflicting changes. You'll need to decide which changes to keep, edit the file to resolve the conflicts and save the file.

  2. Commit changes: After you've resolved the conflicts, add the edited file(s) to the staging area and commit them using the following command:

Text Only
git add .
git commit -m "Resolved conflicts"
  1. Push changes: Push your changes to the remote repository using the following command:
Text Only
git push -u origin feature-branch

Dealing with Merge conflicts using rebase

Yes, you can use Git's rebase command instead of merge to incorporate changes from the trunk branch into your feature branch. The difference between merge and rebase is that merge creates a new merge commit that combines the changes from both branches, while rebase moves the entire branch to a new base commit and replays your changes on top of it.

Here's an example of how to use rebase to incorporate changes from the trunk branch into your feature branch and resolve conflicts:

Bash
# Step 1: Fetch changes
git fetch

# Step 2: Create a new feature branch
git checkout -b feature-branch

# Step 3: Make changes and commit
# ...make changes to the code...
git add .
git commit -m "Implemented feature X"

# Step 4: Fetch changes again
git fetch

# Step 5: Rebase changes from the trunk branch
git rebase trunk

# Step 6: Resolve conflicts
# ...edit the conflicting file(s) to resolve the conflicts...
git add .
git rebase --continue

# Step 7: Push changes
git push -u origin feature-branch

Extra details

Using any of the below commands is unlikely when you work with trunk-based development. However, it is good to know that exits.

Rebasing

Rebasing is the process of taking a series of commits from one branch and reapplying them onto another branch. This is usually done to maintain a linear history in the project, making it easier to understand and follow. Rebasing can also help resolve conflicts between branches by incorporating the latest changes from the main branch.

Rebasing is useful when you want to:

Keep a linear history: Rebasing allows you to apply your feature branch commits on top of the main branch, creating a linear sequence of commits. This makes it easier to understand the history and relationships between commits.

Resolve conflicts: When multiple developers work on a project simultaneously, conflicts may arise between branches. Rebasing can help resolve these conflicts by incorporating the latest changes from the main branch before merging.

Keep your feature branch up-to-date: Rebasing your feature branch regularly onto the main branch ensures that your branch stays current with the latest changes, reducing the likelihood of conflicts and making it easier to merge.

When to rebase:

Rebasing is most useful when working with short-lived feature branches in a TBD workflow. It helps to keep a clean, linear history and reduces merge conflicts. Before you merge your feature branch into the main branch, you should rebase it onto the main branch to incorporate the latest changes.

However, you should avoid rebasing in the following situations:

Public branches: If a branch has been shared with others and they have based their work on it, rebasing can cause confusion and conflicts. In this case, merging is a better option.

Completed features: If your feature branch has already been merged into the main branch, do not rebase it. Instead, create a new branch for any additional work.

Fast-Forward Merge

A fast-forward merge is a type of merge in which the main branch pointer is moved to the latest commit of the feature branch. This can occur when the main branch is a direct ancestor of the feature branch, meaning that no new commits have been added to the main branch since the feature branch was created. Fast-forward merges maintain a linear history and do not create a new merge commit.

Using --ff-only

The --ff-only flag ensures that a merge only occurs if it can be fast-forwarded. If a fast-forward merge is not possible (e.g., because new commits have been added to the main branch since the feature branch was created), the merge will be aborted. Using --ff-only can help you maintain a linear history and prevent accidental merge commits when fast-forwarding is preferred.

Using --no-ff

The --no-ff flag forces the creation of a merge commit, even when a fast-forward merge is possible. This can help preserve branch history and make it clear when features were merged. Merge commits also provide a clear point at which to revert changes if necessary.

Reverting Changes

Reverting after a commit: If you want to revert changes after a commit, you can use the git revert command, which creates a new commit that undoes the changes from a previous commit:

git revert <commit-id> Alternatively, if you want to undo the last commit and keep the changes in your working directory, you can use the git reset command:

git reset --soft HEAD~1

Reverting after a local merge:

If you've merged a branch locally and want to revert the merge, you can use the git reflog command to find the commit before the merge occurred:

git reflog You'll see a list of entries showing the history of HEAD. Locate the commit before the merge and note its reference (e.g., HEAD@{1}). Then, reset to that reference:

git reset --hard HEAD@{1} This will undo the merge and return your branch to its state before the merge.

Reverting after a remote merge (Git push):

If you've pushed a merge to the remote repository and want to revert it, you can use the git revert command to create a new commit that undoes the changes from the merge commit:

git revert -m 1 <merge-commit-id> The -m 1 option specifies that you want to revert the first parent of the merge commit (usually the main branch). This will create a new commit that undoes the merge, and you can push this commit to the remote repository.

Inspecting the HEAD and Unique IDs of Commits

The HEAD is a reference to the latest commit in the currently checked-out branch. You can check the HEAD and the unique IDs (also known as commit hashes) of commits using the git log command:

git log --oneline This command displays a shortened version of the commit history, showing the commit hashes and commit messages on a single line. The commit hash is a unique identifier for each commit, typically represented as a 40-character hexadecimal string.

For example, you might see something like this:

a1b2c3d (HEAD -> main) Latest commit on main f4g5h6i Previous commit on main In this case, a1b2c3d is the commit hash of the latest commit, and f4g5h6i is the commit hash of the previous commit. The HEAD is currently pointing to the latest commit on the main branch.

In summary, rebasing is an essential technique in Trunk Based Development, as it helps maintain a linear history, resolve conflicts, and keep feature branches up-to-date. Fast-forward merges, --ff-only, and --no-ff provide different ways of merging branches, depending on your preferences for maintaining a linear history or preserving branch information. Change can be reversed at various stages of the Git lifecycle using commands like git reset, git reflog, and git revert. Finally, the git log command allows you to inspect your repository's HEAD and unique commits IDs. Understanding these concepts and commands will help you manage your Git repository effectively and collaborate with your team in a Trunk Based Development workflow.

Now that we've covered rebasing, merging, and reverting changes, let's discuss some additional Git concepts and commands that can help you manage your repository more effectively in a Trunk Based Development workflow.

Stashing Changes

Sometimes, you might be in the middle of working on a feature or bugfix when you need to switch to another branch to work on something else. In this situation, you can use the git stash command to save your uncommitted changes temporarily, allowing you to switch branches and come back to your work later.

To stash your changes, use the following command:

git stash save "A brief description of your changes" You can then switch to another branch and work on other tasks. When you're ready to return to your original branch and resume your work, switch back to the branch and apply the stash:

git stash apply Or, if you want to apply the stash and remove it from the list of stashes:

git stash pop Working with Remote Branches

In a Trunk Based Development workflow, you'll often need to work with remote branches. You can list all remote branches using the following command:

git branch -r To fetch the latest changes from the remote repository, use:

git fetch This command downloads the latest changes from the remote repository but does not merge them with your local branches. If you want to fetch and merge the latest changes in one command, use git pull:

git pull If you want to push your local branch to the remote repository, use git push:

git push origin <branch-name> Tagging

Tagging is a way to mark specific points in your repository's history as important, such as a release or a milestone. You can create a lightweight tag or an annotated tag. Annotated tags are recommended, as they include additional information like the tagger's name, email, and a tag message.

To create an annotated tag, use the following command:

git tag -a -m "Tag message" You can then push the tag to the remote repository:

git push origin To list all tags in your repository, use:

git tag Understanding Git Internals

Git uses several objects and references to manage your repository:

Blob: A blob object represents a file's content. It stores the file data but not the file name or directory structure.

Tree: A tree object represents a directory structure, mapping file names to blobs and other trees (subdirectories).

Commit: A commit object represents a snapshot of the repository, referencing a tree object and providing metadata like the author, committer, and commit message.

Branch: A branch is a reference to a commit, allowing you to create different lines of development in your repository.

HEAD: The HEAD is a reference to the latest commit in the currently checked-out branch.

These objects and references form the basis of Git's version control system, allowing you to track changes, manage branches, and collaborate with your team.

In conclusion, mastering Git concepts and commands is crucial for successfully implementing Trunk Based Development. Stashing changes, working with remote branches, tagging, and understanding Git internals are all important aspects of managing your repository effectively. By combining this knowledge with the techniques discussed earlier (rebasing, merging, and reverting changes), you'll be well-equipped to manage your Git repository and collaborate with your team in a Trunk Based Development workflow.

Let's explore some advanced Git commands and best practices that can further improve your workflow in Trunk Based Development.

Interactive Rebase

Interactive rebasing allows you to modify a series of commits before they are applied to another branch. This can be useful for cleaning up your commit history by squashing commits, editing commit messages, or reordering commits.

To start an interactive rebase, use the following command:

git rebase -i <base-commit> This command will open an editor with a list of commits between the base commit and the current branch's HEAD. You can then modify the list to perform actions like pick, reword, edit, squash, or fixup on the listed commits. Save and close the editor to start the interactive rebase process.

Bisecting

When you encounter a bug or regression in your code, Git bisect can help you find the commit that introduced the issue. Git bisect performs a binary search through your commit history, allowing you to quickly identify the problematic commit.

To start bisecting, use the following commands:

Text Only
git bisect start
git bisect bad        # Mark the current commit as bad
git bisect good <commit-id>  # Mark a known-good commit
Git will then check out a commit halfway between the good and bad commits. You can test the code and mark the commit as good or bad accordingly:

git bisect good  # If the commit is good
git bisect bad   # If the commit is bad
Repeat this process until Git identifies the commit that introduced the issue. When you're finished, use git bisect reset to return to your original branch.

Cherry-picking

Cherry-picking is the process of applying specific commits from one branch onto another branch. This can be useful for including a bug fix or feature from another branch without merging the entire branch.

To cherry-pick a commit, use the following command:

git cherry-pick <commit-id>

Best Practices

Write clear and concise commit messages: Commit messages should briefly describe the changes made in the commit. This makes it easier for your teammates to understand the commit history and identify the purpose of each commit.

Keep your commits focused and small:

Each one should represent a cohesive change. Avoid making large, sweeping changes in a single commit, as this can make reviewing, understanding, and reverting changes difficult.

Regularly fetch and merge/rebase:

To reduce the likelihood of conflicts and ensure you're working with the latest changes, regularly fetch updates from the remote repository and merge or rebase your branch.

Communicate with your team:

Keep your team informed about your work, any issues you encounter, and any changes you make to the repository. Good communication can help prevent conflicts and ensure everyone is on the same page.

Use meaningful branch names:

Branch names should be descriptive and reflect the purpose of the branch. This makes it easier for your team to understand the context of each branch and its relationship to the project.

In summary, advanced Git commands like interactive rebase, bisect, and cherry-pick can help you manage your repository more effectively and troubleshoot issues. Following best practices like writing clear commit messages, keeping commits focused, and regularly fetching and merging updates will improve your Trunk Based Development workflow and collaboration with your team. By mastering these techniques, you'll be better equipped to handle complex projects and navigate software development challenges.