DEV Community

Cover image for Simplifying Git Worktrees with PowerShell
Krzysztof Koziarski
Krzysztof Koziarski

Posted on

Simplifying Git Worktrees with PowerShell

Git Worktree

One useful but often overlooked feature of Git is git worktree.

This feature allows you to maintain separate working directories for different branches of your Git repository at the same time.
This can help keep your development environment organized and efficient, particularly when working on multiple features or projects simultaneously.
With git worktree, you no longer need to stash or commit your current changes before switching branches. However, managing multiple worktrees can be cumbersome. This is where my PowerShell script, inspired by Bill Mill's approach to using git worktrees, comes into play.

What the script does

The PowerShell script, named Git-Worktree simplifies working with git worktree by automating the creation, deletion, and navigation of worktrees directly from your PowerShell terminal. The tool simplifies the process of setting up worktrees by providing easy-to-use commands.

For your convenience, aliases worktree and gw are also set up to call the Git-Worktree function

Here's a breakdown of its capabilities:

  • Listing worktrees: Running worktree list provides a list of all existing worktrees within your Git repository.
  • Creating worktrees: Running worktree mybranch, the script allows you to create new worktrees for specific branches. The first parameter should specify the branch name. If the branch does not exist it will be created. If the branch already exists, a worktree will be created for it. If both the branch and worktree already exist, you will be switched to that worktree. All worktrees will be created inside .worktrees repository for keeping your project directory clean.
  • Deleting worktrees: The script can delete existing worktrees. The -Delete parameter is used to initiate the deletion process.
  • Switching to the master branch: Using worktree master switches you to the root directory of the repository for working with the master branch.

Using the script

Here are some examples of how you can leverage the script in your workflow:

1. Creating/switching a worktree for a branch

To create a new worktree for a branch or switch to existing, run:

worktree bugfix-header-typo
worktree task-123
Enter fullscreen mode Exit fullscreen mode

By default every new branch is checked-out from master branch, not current. If you want to checkout from current branch, you need to add -Current parameter:

worktree task-123 -Current
Enter fullscreen mode Exit fullscreen mode

2. Deleting a worktree

To delete a worktree when you're done with the branch:

worktree bugfix-header-typo -Delete
worktree task-123 -Delete
Enter fullscreen mode Exit fullscreen mode

3. Switching to the root directory (master)

To navigate back to the main repo worktree for the master branch:

worktree master
Enter fullscreen mode Exit fullscreen mode

4. Listing existing worktrees

To list all your current worktrees:

worktree list
Enter fullscreen mode Exit fullscreen mode

Summary

By using this script, you simplify the context-switching process that typically hampers productivity when you need to work on multiple branches. The script also helps you maintain a clean work environment by organizing worktrees systematically.

The Git-Worktree script handles the boilerplate of worktree management, so you can retain your focus on what really matters: coding and contributing valuable features to your projects.

To get started, integrate the script into your PowerShell $profile file (run notepad $profile) and use the aliased commands worktree or gw to take your branch management to the next level. Enjoy seamless multitasking across your Git repositories and say goodbye to the old hassles of branch switching.

The script

function Git-Worktree {
   [CmdletBinding()] 
    Param(
        [Parameter(Mandatory=$true)]
        [string]$BranchName,        
        [Parameter(Mandatory=$false)]
        [switch]$Delete,
        [Parameter(Mandatory=$false)]
        [switch]$Current
    )

    function Resolve-GitRoot {
        $dotGitpath = git rev-parse --path-format=absolute --git-common-dir #includes '.git' at the end
        $gitTopLevel = Split-Path -Path $dotGitpath -Parent
        if ($LASTEXITCODE -eq 0 -and $gitTopLevel) {
            return $gitTopLevel
        } else {
            Write-Error "Not inside a git repository or unable to find the git repository root."
            throw "Not a git repository (or any of the parent directories)"
        }
    }

    try {
        $gitRoot = Resolve-GitRoot
    }
    catch {
        Write-Error "Error: $($_.Exception.Message)"
    }

    if ($BranchName -eq 'list') {
        git worktree list
        return;
    }

    if($BranchName -eq 'master') {
        Write-Output "Switching to the root of the repository for main branch 'master' at '$gitRoot'"
        Set-Location -Path $gitRoot
        return
    }

    function Get-GitWorktrees {
        $worktrees = git worktree list --porcelain | Where-Object { $_ -match '^worktree (.+)' } | ForEach-Object {
            $Matches[1].Replace('\', '/')
        }
        return $worktrees
    }

    $existingWorktreePaths = Get-GitWorktrees
    $worktreePath = Join-Path $gitRoot '.worktrees' $BranchName
    if ($Delete) {
        if (Test-Path -Path $worktreePath) {
            try {
                Write-Output "Switching to root"
                Set-Location -Path $gitRoot
                git worktree remove $worktreePath
                if ($LASTEXITCODE -ne 0) {
                    throw "Could not remove worktree"
                }
                if (Test-Path -Path $worktreePath){
                    Remove-Item $worktreePath -Recurse
                    Write-Host "'$worktreePath' folder removed successfully."
                }
                Write-Output "Worktree for branch '$BranchName' has been removed."
            } catch {
                Write-Error "Error: Failed to remove worktree for branch '$BranchName' at '$worktreePath'."
            }
        } else {
            Write-Output "No worktree for branch '$BranchName' at '$worktreePath' exists to remove."
        }
        return
    } 

    $normalizedWorktreePath = $worktreePath.Replace('\', '/')
    if ($existingWorktreePaths -like "*$normalizedWorktreePath*") {
        Write-Output "Worktree for branch '$BranchName' already exists at '$worktreePath'. Switching to that directory."
        Set-Location -Path $worktreePath
        return
    }

    function Create-GitWorktree {
        param (
            [string]$BranchName,
            [string]$WorktreePath,
            [switch]$Current
        )

        git show-ref --quiet -- "refs/heads/$BranchName"
        $branchExists = $LASTEXITCODE -eq 0

        if (-not $branchExists) {
            git ls-remote --quiet --exit-code origin "refs/heads/$BranchName" > $null
            $branchExists = $LASTEXITCODE -eq 0
        }

        if ($branchExists) {
            git worktree add $worktreePath $branchName
        } else {
            $checkoutBranch = $Current ? $null : ($branchExists ? $BranchName : 'master')
            git worktree add -b $branchName $worktreePath $checkoutBranch
        }

        if ($LASTEXITCODE -ne 0) {
            throw "Could not add new worktree for branch '$BranchName' at '$worktreePath'."
        }
    }

    try {
        Create-GitWorktree -BranchName $BranchName -WorktreePath $worktreePath -Current $Current

        Write-Output "Worktree for branch '$BranchName' successfully created at '$worktreePath'."
        Set-Location -Path $worktreePath
    }
    catch {
        Write-Error "Error: Failed to create worktree for branch '$BranchName' and path '$worktreePath'."
    }
}
Set-Alias worktree Git-Worktree
Set-Alias gw Git-Worktree
Enter fullscreen mode Exit fullscreen mode

Top comments (0)