Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions gitfourchette/assets/icons/git-worktree.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
142 changes: 142 additions & 0 deletions gitfourchette/forms/newworktreedialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# -----------------------------------------------------------------------------
# Copyright (C) 2026 Iliyas Jorio.
# This file is part of GitFourchette, distributed under the GNU GPL v3.
# For full terms, see the included LICENSE file.
# -----------------------------------------------------------------------------

import os

from gitfourchette.forms.brandeddialog import convertToBrandedDialog
from gitfourchette.forms.ui_newworktreedialog import Ui_NewWorktreeDialog
from gitfourchette.localization import *
from gitfourchette.qt import *
from gitfourchette.toolbox import *


class NewWorktreeDialog(QDialog):
"""
Ask where to put a new worktree, and what to check out in it.
Mirrors the three forms of `git worktree add`: a new branch, an existing
branch that isn't checked out anywhere else, or a detached HEAD.
"""

def __init__(
self,
parentDir: str,
availableBranches: list[str],
reservedNames: list[str],
parent=None):

super().__init__(parent)

self.ui = Ui_NewWorktreeDialog()
self.ui.setupUi(self)
self.parentDir = parentDir

self.acceptButton.setText(_("&Create"))

self.ui.existingBranchComboBox.addItems(availableBranches)
if not availableBranches:
# Every local branch is already checked out somewhere, so git would
# refuse this mode anyway.
self.ui.existingBranchRadio.setEnabled(False)
self.ui.existingBranchComboBox.setEnabled(False)

# Radio buttons are auto-exclusive, so we can't fake an initial 'toggled'
# signal the way we would with a checkbox: apply the initial state directly.
self.ui.newBranchEdit.setEnabled(self.ui.newBranchRadio.isChecked())
self.ui.existingBranchComboBox.setEnabled(self.ui.existingBranchRadio.isChecked())

self.ui.browseButton.clicked.connect(self.browse)

nameTaken = _("This name is already taken by another local branch.")
validator = ValidatorMultiplexer(self)
validator.setGatedWidgets(self.acceptButton)
validator.connectInput(self.ui.pathEdit, self.validatePath)
validator.connectInput(
self.ui.newBranchEdit,
lambda name: nameValidationMessage(name, reservedNames, nameTaken)
if self.ui.newBranchRadio.isChecked() else "")

# Re-validate when switching modes: the branch name only matters in one of them
for radio in (self.ui.newBranchRadio, self.ui.existingBranchRadio, self.ui.detachedRadio):
radio.toggled.connect(lambda _dummy: validator.run())

# Keep the default branch name in sync with the folder name until the
# user types their own, which is what `git worktree add <path>` does.
self.ui.pathEdit.textChanged.connect(self.syncDefaultBranchName)
self._branchNameEdited = False
self.ui.newBranchEdit.textEdited.connect(self.onBranchNameEdited)

validator.run()

convertToBrandedDialog(
self, _("New worktree"),
_("A worktree is a second working copy of this repo, with its own checked-out branch."))

self.ui.pathEdit.setFocus()
packDialog(self, lockHeight=True)

@property
def acceptButton(self):
return self.ui.buttonBox.button(QDialogButtonBox.StandardButton.Ok)

def browse(self):
qfd = PersistentFileDialog.saveFile(
self, "NewWorktree", _("New worktree location"), self.path or self.parentDir)
qfd.fileSelected.connect(self.ui.pathEdit.setText)
qfd.show()

def onBranchNameEdited(self):
self._branchNameEdited = True

def syncDefaultBranchName(self, path: str):
if self._branchNameEdited:
return
# Don't block signals here: the validator listens to textChanged, and it
# has already run once (against the empty field) by the time we get here.
# setText doesn't emit textEdited, so this won't look like the user typing.
self.ui.newBranchEdit.setText(os.path.basename(path.rstrip("/\\")))

def validatePath(self, path: str) -> str:
if not path.strip():
return _("Please enter a location for the new worktree.")
path = self.absolutePath(path)
if os.path.isdir(path):
# git accepts an existing directory as long as it's empty
if os.listdir(path):
return _("This folder already exists and isn’t empty.")
elif os.path.lexists(path):
return _("A file already exists at this location.")
elif not os.path.isdir(os.path.dirname(path)):
return _("The parent folder doesn’t exist.")
return ""

def absolutePath(self, path: str = "") -> str:
path = path or self.ui.pathEdit.text()
path = os.path.expanduser(path.strip())
if not os.path.isabs(path):
path = os.path.join(self.parentDir, path)
return os.path.normpath(path)

@property
def path(self) -> str:
return self.ui.pathEdit.text().strip()

@property
def newBranchName(self) -> str:
"""Name of the branch to create, or empty if not creating one."""
if not self.ui.newBranchRadio.isChecked():
return ""
return self.ui.newBranchEdit.text().strip()

@property
def existingBranchName(self) -> str:
"""Name of the branch to check out, or empty if not checking one out."""
if not self.ui.existingBranchRadio.isChecked():
return ""
return self.ui.existingBranchComboBox.currentText()

@property
def detached(self) -> bool:
return self.ui.detachedRadio.isChecked()
198 changes: 198 additions & 0 deletions gitfourchette/forms/newworktreedialog.ui
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>NewWorktreeDialog</class>
<widget class="QDialog" name="NewWorktreeDialog">
<property name="windowModality">
<enum>Qt::WindowModality::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>543</width>
<height>202</height>
</rect>
</property>
<property name="windowTitle">
<string>New worktree</string>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="pathLabel">
<property name="text">
<string>&amp;Location:</string>
</property>
<property name="buddy">
<cstring>pathEdit</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="pathLayout">
<item>
<widget class="QLineEdit" name="pathEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="browseButton">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="checkOutLabel">
<property name="text">
<string>Check out:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="newBranchLayout">
<item>
<widget class="QRadioButton" name="newBranchRadio">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;New branch:</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="newBranchEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="existingBranchLayout">
<item>
<widget class="QRadioButton" name="existingBranchRadio">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Existing branch:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="existingBranchComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertPolicy::NoInsert</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="1">
<widget class="QRadioButton" name="detachedRadio">
<property name="text">
<string>&amp;Detached HEAD at current commit</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<tabstops>
<tabstop>pathEdit</tabstop>
<tabstop>browseButton</tabstop>
<tabstop>newBranchRadio</tabstop>
<tabstop>newBranchEdit</tabstop>
<tabstop>existingBranchRadio</tabstop>
<tabstop>existingBranchComboBox</tabstop>
<tabstop>detachedRadio</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>NewWorktreeDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel"><x>299</x><y>197</y></hint>
<hint type="destinationlabel"><x>286</x><y>156</y></hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>NewWorktreeDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel"><x>463</x><y>197</y></hint>
<hint type="destinationlabel"><x>364</x><y>71</y></hint>
</hints>
</connection>
<connection>
<sender>newBranchRadio</sender>
<signal>toggled(bool)</signal>
<receiver>newBranchEdit</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel"><x>205</x><y>62</y></hint>
<hint type="destinationlabel"><x>244</x><y>62</y></hint>
</hints>
</connection>
<connection>
<sender>existingBranchRadio</sender>
<signal>toggled(bool)</signal>
<receiver>existingBranchComboBox</receiver>
<slot>setEnabled(bool)</slot>
<hints>
<hint type="sourcelabel"><x>205</x><y>92</y></hint>
<hint type="destinationlabel"><x>244</x><y>92</y></hint>
</hints>
</connection>
</connections>
</ui>
Loading