Introduction: Building Features, Layer by Layer

Welcome back, fellow developer! In our previous chapters, we laid the groundwork by understanding GitButler’s core concepts like virtual branches and its local-first approach. Now, it’s time to put that knowledge into action and tackle a common development challenge: building a significant feature that naturally breaks down into smaller, dependent steps.

Imagine you’re tasked with adding a new “User Profile” section to an application. This isn’t a single change; it often involves updating the database, modifying API endpoints, and finally, updating the user interface. Traditionally, managing these interdependent changes with Git can become a tangle of git rebase -i commands, temporary branches, and constant fear of breaking something.

This chapter will guide you through developing such a feature using GitButler’s powerful stacked branches. You’ll learn how to break down complex work into logical, manageable layers, making your development process smoother, your code reviews easier, and your Git history cleaner. By the end, you’ll have hands-on experience in building a feature incrementally and observing how GitButler simplifies the entire workflow.

Core Concepts: Understanding Stacked Branches for Feature Development

Before we dive into code, let’s solidify our understanding of what stacked branches are and why they’re a game-changer for feature development.

What are Stacked Branches?

Think of stacked branches like building blocks, or layers of a delicious cake! Each block (or layer) represents a distinct, logical change that builds directly on the one below it. In the context of GitButler, a “stack” is a sequence of virtual branches where each branch is based on the previous one in the sequence.

Instead of having one massive branch for an entire feature, you create smaller, focused virtual branches for each logical step. For example, if you’re adding a new user setting, your stack might look like this:

  1. Base Branch (e.g., main or develop): The stable foundation.
  2. feat/user-settings-db: Adds the necessary database schema changes.
  3. feat/user-settings-api: Implements the API endpoints to interact with the new database fields, based on feat/user-settings-db.
  4. feat/user-settings-ui: Develops the frontend UI to display and modify the settings, based on feat/user-settings-api.

Each branch in the stack is a complete, testable unit of work, but it relies on the changes introduced by the branches beneath it.

Why Use Stacked Branches for Features?

Stacked branches offer several compelling advantages over traditional monolithic feature branches:

  1. Simplified Code Review: Reviewers can examine one small, logical change at a time. This makes reviews faster, more focused, and significantly reduces cognitive load. Imagine reviewing 500 lines of code versus three separate reviews of 50, 150, and 300 lines.
  2. Easier Iteration and Modification: What happens if you need to tweak something in an earlier layer of your feature? In traditional Git, this often means painful interactive rebases. With GitButler, you simply make the change on the relevant virtual branch, commit it, and GitButler automatically re-applies the dependent branches on top, handling the rebase for you. This is a huge time-saver and stress-reducer!
  3. Reduced Merge Conflicts: By working in smaller, isolated steps, you reduce the surface area for conflicts. When conflicts do arise, they are typically smaller and easier to resolve within the context of a single layer.
  4. Clearer History: Your Git history becomes a clean, linear progression of logical changes, making it much easier to understand how a feature was built, debug issues, or revert specific parts.
  5. Local-First Flexibility: All this magic happens locally within GitButler. You can experiment, reorder, and refine your stack as much as you need before ever pushing anything to a remote repository.

Traditional Git vs. GitButler Stacks

Let’s quickly visualize the difference:

flowchart TD subgraph Traditional Git Workflow T1[main] --> T2[feat/user-profile-huge] T2 -->|\1| T3[git rebase -i] T3 -->|\1| T4[Re-apply changes] end subgraph GitButler Stacked Workflow G1[main] --> G2[feat/profile-db] G2 --> G3[feat/profile-api] G3 --> G4[feat/profile-ui] G2 -->|\1| G5[Edit feat/profile-db] G5 -->|\1| G6[GitButler Re-stacks] G6 --> G3_new[feat/profile-api] G3_new --> G4_new[feat/profile-ui] end

In the GitButler workflow, making a change to feat/profile-db (G5) automatically triggers GitButler to rebase feat/profile-api and feat/profile-ui onto the updated feat/profile-db. This is a core strength of GitButler.

GitButler’s Visual Approach

GitButler’s desktop application (latest stable version as of 2026-04-10) provides a highly visual interface for managing these stacks. You’ll see your virtual branches arranged vertically, clearly showing their dependencies. You can even drag and drop branches to reorder them or change their parent, and GitButler will handle the underlying Git operations. This visual feedback is crucial for understanding your workflow.

Step-by-Step Implementation: Building a “User Bio” Feature

Let’s get practical! We’ll simulate adding a “bio” field to a user profile, breaking it down into three distinct, dependent layers.

Scenario: We need to add a “bio” field to our user profile. This involves:

  1. Adding a bio column to the users table in the database.
  2. Updating the API to allow setting and retrieving the bio.
  3. Adding a UI element to display and edit the bio.

Prerequisites:

  • GitButler Desktop Application (latest stable release from gitbutler.com) installed and running.
  • An existing Git repository opened in GitButler. If you don’t have one, create a simple dummy project:
    mkdir gitbutler-bio-feature
    cd gitbutler-bio-feature
    git init
    echo "# User Bio Feature Project" > README.md
    echo "console.log('Hello, GitButler!');" > app.js
    git add .
    git commit -m "Initial project setup"
    
    Then, open this gitbutler-bio-feature folder in GitButler.

Step 1: Start the Base Feature Branch

First, we’ll create a main virtual branch for our entire feature. This will be the root of our stack.

  1. Ensure you are on your main or develop branch in GitButler. You can select it from the “Branches” panel on the left.
  2. Click the “New Virtual Branch” button (usually a + icon or similar) in the Branches panel.
  3. Name the branch feature/user-bio.
  4. Ensure its parent is set to your main branch (or develop, depending on your project).
  5. Click “Create Branch”.

You’ll now be on the feature/user-bio virtual branch. GitButler automatically switches your working directory to reflect this branch.

Step 2: Database Migration Layer (feat/bio-db-migration)

Let’s create the first layer: the database change.

  1. With feature/user-bio currently selected, click the “New Virtual Branch” button again.
  2. Name this new branch feat/bio-db-migration.
  3. Crucially, ensure its parent is feature/user-bio. This is what makes it a stack!
  4. Click “Create Branch”.

Now you’re on feat/bio-db-migration. Let’s simulate a database migration:

  1. Create a new folder and file in your project, e.g., db/migrations/20260410_add_user_bio.sql.
    mkdir -p db/migrations
    touch db/migrations/20260410_add_user_bio.sql
    
  2. Open db/migrations/20260410_add_user_bio.sql in your code editor and add some SQL:
    -- Add 'bio' column to the 'users' table
    ALTER TABLE users
    ADD COLUMN bio TEXT;
    
    -- Optionally, add a rollback for development
    -- ALTER TABLE users
    -- DROP COLUMN bio;
    
    (Note: For a real project, you’d use a proper migration tool like Flyway, Liquibase, or ORM migrations.)
  3. Save the file.
  4. Observe GitButler’s “Changes” view. You should see db/migrations/20260410_add_user_bio.sql listed.
  5. Stage the change by clicking the + icon next to the file or using “Stage All”.
  6. Enter a commit message: feat: add bio column to users table
  7. Click “Commit”.

Great! You’ve just created the first layer of your stack. In the GitButler UI, you should now see feat/bio-db-migration stacked on top of feature/user-bio.

Step 3: API Endpoint Layer (feat/bio-api-endpoint)

Next, we’ll build the API changes on top of our database migration.

  1. With feat/bio-db-migration currently selected, click the “New Virtual Branch” button.
  2. Name this new branch feat/bio-api-endpoint.
  3. Ensure its parent is feat/bio-db-migration. This is key for stacking!
  4. Click “Create Branch”.

You’re now on feat/bio-api-endpoint. Let’s simulate API changes:

  1. Create a new file api/user.js or modify an existing app.js if you’re using a simple setup.
    mkdir -p api
    touch api/user.js
    
  2. Open api/user.js and add some mock API logic:
    // api/user.js - Mock API for user profile
    const users = {
        'user123': { id: 'user123', name: 'Alice', email: 'alice@example.com', bio: 'Software Engineer' }
    };
    
    function getUser(id) {
        return users[id];
    }
    
    function updateUserBio(id, newBio) {
        if (users[id]) {
            users[id].bio = newBio;
            return true;
        }
        return false;
    }
    
    // In a real app, this would be an Express/Fastify route:
    // app.put('/users/:id/bio', (req, res) => {
    //     const { id } = req.params;
    //     const { bio } = req.body;
    //     if (updateUserBio(id, bio)) {
    //         res.status(200).send({ message: 'Bio updated' });
    //     } else {
    //         res.status(404).send({ message: 'User not found' });
    //     }
    // });
    
    console.log("User API loaded with bio support.");
    
  3. Save the file.
  4. Observe GitButler’s “Changes” view. Stage and commit the new file.
  5. Commit message: feat: add API endpoints for user bio
  6. Click “Commit”.

Now your GitButler UI should show three branches stacked: feat/bio-api-endpoint on top of feat/bio-db-migration, which is on top of feature/user-bio.

Step 4: Frontend UI Layer (feat/bio-ui)

Finally, let’s add the UI changes, building on the API.

  1. With feat/bio-api-endpoint currently selected, click the “New Virtual Branch” button.
  2. Name this new branch feat/bio-ui.
  3. Ensure its parent is feat/bio-api-endpoint.
  4. Click “Create Branch”.

You’re now on feat/bio-ui. Let’s simulate UI changes:

  1. Create a new file public/index.html or modify an existing one.
    mkdir -p public
    touch public/index.html
    
  2. Open public/index.html and add some mock UI for the bio:
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>User Profile</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            .profile-card { border: 1px solid #ccc; padding: 20px; border-radius: 8px; max-width: 400px; }
            textarea { width: 100%; height: 80px; margin-top: 10px; }
            button { margin-top: 10px; padding: 8px 15px; background-color: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        </style>
    </head>
    <body>
        <h1>User Profile</h1>
        <div class="profile-card">
            <h2>Alice's Profile</h2>
            <p><strong>Email:</strong> alice@example.com</p>
            <p><strong>Bio:</strong></p>
            <textarea id="userBio" placeholder="Tell us about yourself...">Software Engineer passionate about open source and learning new things.</textarea>
            <button onclick="saveBio()">Save Bio</button>
        </div>
    
        <script>
            // Simulate fetching and updating bio
            document.addEventListener('DOMContentLoaded', () => {
                const bioTextArea = document.getElementById('userBio');
                // In a real app, you'd fetch this from the API
                bioTextArea.value = 'Software Engineer passionate about open source and learning new things.';
            });
    
            function saveBio() {
                const bioTextArea = document.getElementById('userBio');
                const newBio = bioTextArea.value;
                console.log('Saving new bio:', newBio);
                alert('Bio saved! (Simulated)');
                // In a real app, you'd send this to the API
                // updateUserBio('user123', newBio);
            }
        </script>
    </body>
    </html>
    
  3. Save the file.
  4. Observe GitButler’s “Changes” view. Stage and commit the new file.
  5. Commit message: feat: add UI for user bio field
  6. Click “Commit”.

Congratulations! You’ve successfully built a feature using a stack of three virtual branches.

Step 5: Observing and Modifying the Stack

Now, look at your GitButler UI. You should see a clear stack of branches:

main
└── feature/user-bio
    └── feat/bio-db-migration
        └── feat/bio-api-endpoint
            └── feat/bio-ui (Current Branch)

This visual representation makes the dependencies crystal clear. What if we need to make a change to a lower layer? This is where GitButler truly shines!

Let’s say we realize the bio column in the database should be TEXT with a NOT NULL constraint and a default empty string.

  1. Switch to the feat/bio-db-migration branch. You can do this by clicking on the branch name in the GitButler UI.
  2. Open db/migrations/20260410_add_user_bio.sql in your editor.
  3. Modify the SQL to add the NOT NULL constraint and a default value:
    -- Add 'bio' column to the 'users' table
    ALTER TABLE users
    ADD COLUMN bio TEXT NOT NULL DEFAULT '';
    
  4. Save the file.
  5. Stage and commit this change on feat/bio-db-migration.
    • Commit message: refactor: make bio column not null with default empty string
    • Click “Commit”.

Observe What Happens Next: GitButler will detect that a base branch (feat/bio-db-migration) has changed. It will then automatically rebase the dependent branches (feat/bio-api-endpoint and feat/bio-ui) on top of the updated feat/bio-db-migration. You’ll see a brief spinner or notification as GitButler performs these operations.

This is the power of GitButler’s local-first, automatic rebase capabilities. You didn’t have to manually git rebase -i or git stash and re-apply. GitButler handled the complex Git operations for you, keeping your stack consistent.

Step 6: Consolidating and Pushing the Feature

Once your stacked feature is complete and reviewed (if using a review process):

  1. Switch back to your feature/user-bio branch.
  2. Integrate the stack: You have a few options, depending on your team’s workflow:
    • Squash and Merge: Often, the entire stack is squashed into a single, clean commit on feature/user-bio. This is a common practice for pull requests. GitButler allows you to easily squash branches together.
    • Merge individual branches: Less common for stacked features, as it retains all intermediate commits.
    • Submit as Stacked Pull Requests: Some platforms (like GitHub with specific integrations or tools) support stacked PRs, allowing reviewers to go through each layer. GitButler integrates with GitHub to enable this, which is a powerful feature for larger teams.

For simplicity in this guide, let’s imagine we’re ready to squash feat/bio-db-migration, feat/bio-api-endpoint, and feat/bio-ui into feature/user-bio.

  1. Right-click on feature/user-bio in the GitButler Branches panel.
  2. Look for options like “Squash Children” or “Merge Children Into This Branch”. The exact wording might vary with GitButler updates.
  3. If you choose “Squash Children”, GitButler will combine all changes from feat/bio-db-migration, feat/bio-api-endpoint, and feat/bio-ui into a single commit on feature/user-bio, then delete the child branches. You’ll then have a single, clean feature/user-bio branch ready to be merged into main or develop.

This process effectively brings all the changes into one branch, preparing it for a final pull request.

Mini-Challenge: Enhancing the Stack

You’ve built a solid stack! Now, let’s test your understanding and flexibility.

Challenge: Add a new, small feature to our user profile: a “last updated” timestamp. This timestamp should be updated whenever the bio is saved.

Here’s how you should implement it using GitButler’s stacked branches:

  1. Create a new virtual branch for this feature.
  2. Decide where it fits in the existing stack:
    • Does it belong before the bio API? (e.g., if the DB schema needs to change for the timestamp).
    • Does it belong between the bio API and UI? (e.g., if only the API needs modification).
    • Does it belong after the bio UI? (e.g., if it’s a completely independent UI component).
  3. Implement the necessary changes (e.g., add last_updated_at to the SQL, update the API to set this timestamp, perhaps add a small display to the UI).
  4. Commit your changes to this new branch.

Hint: Think about the dependencies. If the “last updated” timestamp requires a new database column, it should likely be based on feat/bio-db-migration or even be a separate branch at that level. If it only affects the API and UI, it might be based on feat/bio-api-endpoint. GitButler allows you to easily re-parent branches, so don’t be afraid to experiment!

What to Observe/Learn:

  • How easily you can insert a new logical step into an existing stack.
  • How GitButler automatically re-arranges and rebases subsequent branches if you insert a branch into the middle of a stack.
  • The flexibility of defining parent branches.

Common Pitfalls & Troubleshooting

Even with GitButler’s assistance, understanding potential issues helps you navigate them smoothly.

  1. Forgetting to Select the Correct Parent Branch:

    • Pitfall: You create a new virtual branch, but accidentally base it on main instead of the previous branch in your intended stack. This results in parallel branches instead of a stack.
    • Troubleshooting: GitButler allows you to easily re-parent branches. In the Branches panel, right-click the mis-parented branch, select “Re-parent Branch,” and choose the correct parent (e.g., feat/bio-db-migration instead of main). GitButler will handle the rebase.
  2. Merge Conflicts During Automatic Rebase:

    • Pitfall: While GitButler automates rebasing, complex or overlapping changes between stacked branches can still lead to conflicts. This usually happens if a change in a lower layer directly conflicts with a change in an upper layer that Git cannot automatically resolve.
    • Troubleshooting: GitButler will notify you of conflicts and provide a UI to resolve them, similar to a traditional Git merge tool. Go through the conflicting files, choose the correct changes, mark them as resolved, and commit. GitButler will then complete the rebase.
  3. Pushing Individual Stacked Branches Prematurely:

    • Pitfall: Each virtual branch in your stack is a separate Git branch. If you push them individually to your remote and create separate pull requests, it can complicate the review process, especially if your team isn’t set up for stacked PRs.
    • Best Practice: Typically, you’ll consolidate your stack into a single feature branch (e.g., feature/user-bio) before creating a pull request. Use GitButler’s “Squash Children” or similar features to achieve this. If your team does use stacked PRs, ensure you understand the specific workflow and tooling (e.g., GitHub’s CLI or GitButler’s native integration if available) required to manage them effectively.

Summary: The Power of Layered Development

You’ve just experienced the power of GitButler’s stacked branches for practical feature development! Let’s recap the key takeaways:

  • Stacked branches allow you to break down complex features into smaller, logical, and dependent units of work.
  • GitButler simplifies dependency management by visually representing your stack and automatically handling rebases when lower layers are modified.
  • This approach leads to cleaner Git history, easier code reviews, and reduced merge conflict headaches.
  • The local-first nature of GitButler means you can experiment and refine your stack before ever pushing changes to a remote repository.
  • By using GitButler, you’re embracing a modern Git workflow that aims to eliminate common frustrations associated with managing complex feature development.

You’re now equipped to tackle larger features with confidence, knowing you can manage them in a structured, iterative, and flexible way. In the next chapter, we’ll explore even more advanced GitButler features, including collaboration and how to manage multiple ongoing projects simultaneously.

References


This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.