Simplifying Git Worktrees with PowerShell
Tags: git, worktree, powershell
Published: 2024-04-07 20:21:52

## 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](https://notes.billmill.org/blog/2024/03/How_I_use_git_worktrees.html), 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:
```powershell
worktree bugfix-header-typo
worktree task-123
```
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: 
```powershell
worktree task-123 -Current
```

### 2. Deleting a worktree
To delete a worktree when you're done with the branch:
```powershell
worktree bugfix-header-typo -Delete
worktree task-123 -Delete
```

### 3. Switching to the root directory (master)
To navigate back to the main repo worktree for the `master` branch:
```powershell
worktree master
```

### 4. Listing existing worktrees
To list all your current worktrees:
```powershell
worktree list
```

## 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
```powershell
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
```