mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
[git] IJPL-211607 IJPL-204771 Working tree creation actions IJ-MR-185641
GitOrigin-RevId: 52a5062f5e732d15103da68bf3684ee45cbbb54d
This commit is contained in:
committed by
intellij-monorepo-bot
parent
258afe3f84
commit
61ff67fcf5
@@ -41,6 +41,7 @@
|
||||
<action id="Git.Show.WorkingTrees" class="git4idea.actions.workingTree.ShowWorkingTreesAction">
|
||||
<add-to-group group-id="Vcs.Log.ToolWindow.TabActions.DropDown" relative-to-action="Vcs.ShowConsoleTab" anchor="after"/>
|
||||
</action>
|
||||
<action id="Git.CreateNewWorkingTree" class="git4idea.actions.workingTree.GitCreateWorkingTreeAction"/>
|
||||
|
||||
<action id="Git.Drop.Selected.Changes" class="git4idea.rebase.log.changes.GitDropSelectedChangesAction">
|
||||
<add-to-group group-id="Vcs.RepositoryChangesBrowserMenu" relative-to-action="Vcs.ApplySelectedChanges" anchor="after"/>
|
||||
@@ -161,6 +162,7 @@
|
||||
<action id="Git.Tag" class="git4idea.actions.GitTag"/>
|
||||
<action id="Git.Reset" class="git4idea.actions.GitResetHead"/>
|
||||
<separator/>
|
||||
<reference ref="Git.CreateNewWorkingTree"/>
|
||||
<reference ref="Git.Show.WorkingTrees"/>
|
||||
<separator/>
|
||||
<reference ref="Vcs.Show.Log"/>
|
||||
@@ -250,6 +252,8 @@
|
||||
<action class="git4idea.actions.branch.GitRebaseBranchAction"/>
|
||||
<action class="git4idea.actions.ref.GitMergeRefAction"/>
|
||||
<separator/>
|
||||
<reference ref="Git.CreateNewWorkingTree"/>
|
||||
<separator/>
|
||||
<action class="git4idea.actions.branch.GitUpdateSelectedBranchAction"/>
|
||||
<action class="git4idea.actions.branch.GitPushBranchAction"/>
|
||||
<group id="Git.Branch.Tracked" class="git4idea.actions.branch.GitTrackedBranchActionGroup" popup="true"/>
|
||||
@@ -338,6 +342,8 @@
|
||||
<separator/>
|
||||
<reference ref="Git.Show.WorkingTrees"/>
|
||||
<separator/>
|
||||
<reference ref="Git.CreateNewWorkingTree"/>
|
||||
<separator/>
|
||||
<add-to-group group-id="Vcs.Log.ContextMenu" relative-to-action="Vcs.Log.GoToChild" anchor="before"/>
|
||||
</group>
|
||||
|
||||
@@ -548,6 +554,7 @@
|
||||
</action>
|
||||
|
||||
<group id="Git.WorkingTrees.ToolwindowGroup.Toolbar">
|
||||
<reference ref="Git.CreateNewWorkingTree"/>
|
||||
<reference ref="Git.WorkingTrees.Remove"/>
|
||||
</group>
|
||||
|
||||
@@ -886,6 +893,7 @@
|
||||
predicateClassName="git4idea.workingTrees.ui.GitWorkingTreesContentVisibilityPredicate"
|
||||
preloaderClassName="git4idea.workingTrees.ui.GitWorkingTreesContentPreloader"
|
||||
isInCommitToolWindow="false"/>
|
||||
<statistics.actionCustomPlaceAllowlist places="GitWorkingTreeToolWindowTabToolbar"/>
|
||||
<registryKey key="git.enable.working.trees.feature" defaultValue="false" restartRequired="true"
|
||||
description="Enables the 'Working Trees' feature, see https://youtrack.jetbrains.com/issue/IJPL-204771"/>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ jvm_library(
|
||||
"//platform/platform-impl/ui",
|
||||
"//platform/diagnostic/telemetry",
|
||||
"@lib//:opentelemetry",
|
||||
"//platform/vcs-api/shared",
|
||||
]
|
||||
)
|
||||
### auto-generated section `build intellij.vcs.git.shared` end
|
||||
@@ -54,5 +54,6 @@
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.diagnostic.telemetry" />
|
||||
<orderEntry type="library" name="opentelemetry" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.vcs.shared" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -1841,3 +1841,28 @@ action.Git.WorkingTrees.Open.text=Open
|
||||
action.Git.WorkingTrees.Open.description=Open the worktree as a project
|
||||
Git.WorkingTrees.delete.worktree.success.message=Successfully deleted worktree `{0}`
|
||||
Git.WorkingTrees.delete.worktrees.failure.notification.title=Worktree deletion failed
|
||||
|
||||
action.Git.CreateNewWorkingTree.text = New Worktree\u2026
|
||||
action.Git.CreateNewWorkingTree.description = Create a new Git working tree from the current repository
|
||||
working.tree.dialog.title=New Worktree
|
||||
working.tree.dialog.button.ok=Create Worktree
|
||||
working.tree.dialog.label.existing.branch=From branch:
|
||||
working.tree.dialog.label.name=Project name:
|
||||
working.tree.dialog.label.location=Location:
|
||||
working.tree.dialog.label.location.file.chooser.title=Worktree Location
|
||||
working.tree.dialog.checkbox.new.branch=New branch:
|
||||
working.tree.dialog.label.new.branch.comment=Based on ''{0}'' branch
|
||||
working.tree.dialog.label.new.branch.detached.comment=Based on current repository state
|
||||
working.tree.dialog.label.location.comment=The worktree will be created in:<br/>{0}
|
||||
working.tree.dialog.existing.branch.combo.box.empty.text=Select branch
|
||||
progress.title.creating.worktree=Creating worktree
|
||||
working.tree.dialog.location.validation.empty=Provide a location for a worktree
|
||||
working.tree.dialog.location.validation.is.a.file=''{0}'' is a file
|
||||
working.tree.dialog.location.validation.is.not.empty=''{0}'' is not empty
|
||||
working.tree.dialog.location.validation.select.branch=Select a branch to create a worktree
|
||||
working.tree.dialog.location.validation.provide.name=Provide a name for a worktree
|
||||
working.tree.dialog.location.validation.provide.new.branch.name=Provide a name for the new branch
|
||||
working.tree.dialog.branch.validation.already.checked.out.in.working.tree=Branch {0} is already checked out in working tree `{1}`
|
||||
notification.title.worktree.creation.failed=Worktree creation failed
|
||||
action.Git.CreateNewWorkingTree.from.branch.text=New Worktree from ''{0}''\u2026
|
||||
action.Git.CreateNewWorkingTree.from.branch.description=Create a new Git working tree from the ''{0}'' branch of the current repository
|
||||
|
||||
@@ -4,6 +4,7 @@ package git4idea
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.openapi.vcs.FilePath
|
||||
import com.intellij.openapi.vcs.LocalFilePath
|
||||
import com.intellij.openapi.vcs.actions.VcsContextFactory
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -29,7 +30,7 @@ data class GitWorkingTree(
|
||||
|
||||
constructor(path: @NlsSafe String, fullBranchName: @NlsSafe String?, isMain: Boolean, isCurrent: Boolean) :
|
||||
this(
|
||||
LocalFilePath(path, true),
|
||||
VcsContextFactory.getInstance().createFilePath(path, true),
|
||||
if (fullBranchName == null) null else GitStandardLocalBranch(fullBranchName),
|
||||
isMain,
|
||||
isCurrent
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea
|
||||
|
||||
import com.intellij.notification.impl.NotificationIdsHolder
|
||||
@@ -129,6 +129,8 @@ class GitNotificationIdsHolder : NotificationIdsHolder {
|
||||
GPG_AGENT_CONFIGURATION_PROPOSE,
|
||||
GPG_AGENT_CONFIGURATION_PROPOSE_SUGGESTION,
|
||||
MODAL_COMMIT_DEPRECATION,
|
||||
WORKTREE_ADD_FAILED,
|
||||
WORKTREE_COULD_NOT_CREATE_TARGET_DIR,
|
||||
WORKING_TREE_DELETED,
|
||||
WORKING_TREE_COULD_NOT_DELETE,
|
||||
)
|
||||
@@ -259,6 +261,8 @@ class GitNotificationIdsHolder : NotificationIdsHolder {
|
||||
const val GPG_AGENT_CONFIGURATION_PROPOSE: String = "git.gpg.agent.configuration.propose"
|
||||
const val GPG_AGENT_CONFIGURATION_PROPOSE_SUGGESTION: String = "git.gpg.agent.configuration.propose.suggestion"
|
||||
const val MODAL_COMMIT_DEPRECATION: String = "git.commit.modal.deprecation"
|
||||
const val WORKTREE_ADD_FAILED: String = "git.worktree.add.failed"
|
||||
const val WORKTREE_COULD_NOT_CREATE_TARGET_DIR: String = "git.worktree.could.not.create.target.dir"
|
||||
const val WORKING_TREE_DELETED: String = "git.working.tree.deleted"
|
||||
const val WORKING_TREE_COULD_NOT_DELETE: String = "git.working.tree.not.deleted"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea
|
||||
|
||||
import com.intellij.internal.statistic.StructuredIdeActivity
|
||||
@@ -6,7 +6,9 @@ import com.intellij.internal.statistic.eventLog.EventLogGroup
|
||||
import com.intellij.internal.statistic.eventLog.events.EventFields
|
||||
import com.intellij.internal.statistic.eventLog.events.RoundedIntEventField
|
||||
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.project.Project
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialogData
|
||||
import git4idea.commands.GitCommandResult
|
||||
import git4idea.inMemory.rebase.InMemoryRebaseResult
|
||||
import git4idea.push.GitPushRepoResult
|
||||
@@ -17,7 +19,7 @@ import git4idea.rebase.interactive.CantRebaseUsingLogException
|
||||
internal object GitOperationsCollector : CounterUsagesCollector() {
|
||||
override fun getGroup(): EventLogGroup = GROUP
|
||||
|
||||
private val GROUP: EventLogGroup = EventLogGroup("git.operations", 6)
|
||||
private val GROUP: EventLogGroup = EventLogGroup("git.operations", 7)
|
||||
|
||||
internal val UPDATE_FORCE_PUSHED_BRANCH_ACTIVITY = GROUP.registerIdeActivity("update.force.pushed")
|
||||
|
||||
@@ -45,10 +47,12 @@ internal object GitOperationsCollector : CounterUsagesCollector() {
|
||||
)
|
||||
private val INTERACTIVE_REBASE_WAS_SUCCESSFUL = EventFields.Boolean("was_successful")
|
||||
private val INTERACTIVE_REBASE_ACTIVITY = GROUP.registerIdeActivity("interactive.rebase",
|
||||
finishEventAdditionalFields = arrayOf(INTERACTIVE_REBASE_WAS_SUCCESSFUL))
|
||||
finishEventAdditionalFields = arrayOf(
|
||||
INTERACTIVE_REBASE_WAS_SUCCESSFUL))
|
||||
private val IN_MEMORY_REBASE_RESULT = EventFields.Enum<InMemoryRebaseResult>("in_memory_rebase_result")
|
||||
private val IN_MEMORY_INTERACTIVE_REBASE_ACTIVITY = GROUP.registerIdeActivity("in.memory.interactive.rebase",
|
||||
finishEventAdditionalFields = arrayOf(IN_MEMORY_REBASE_RESULT))
|
||||
finishEventAdditionalFields = arrayOf(
|
||||
IN_MEMORY_REBASE_RESULT))
|
||||
|
||||
private val CANT_REBASE_USING_LOG_REASON = EventFields.Enum<CantRebaseUsingLogException.Reason>("cant_rebase_using_log_reason")
|
||||
private val CANT_REBASE_USING_LOG_EVENT = GROUP.registerEvent("cant.rebase.using.log", CANT_REBASE_USING_LOG_REASON)
|
||||
@@ -59,6 +63,20 @@ internal object GitOperationsCollector : CounterUsagesCollector() {
|
||||
private val REBASE_START_USING_LOG_EVENT = GROUP.registerVarargEvent("rebase.start.using.log",
|
||||
*REBASE_ENTRY_TYPE_FIELDS.values.toTypedArray())
|
||||
|
||||
private val WITH_PROVIDED_BRANCH = EventFields.Boolean("with_provided_branch")
|
||||
private val WORKING_TREE_CREATION_ACTIVITY =
|
||||
GROUP.registerIdeActivity("create.worktree",
|
||||
startEventAdditionalFields = arrayOf(EventFields.ActionPlace, WITH_PROVIDED_BRANCH),
|
||||
subStepWithStepId = true)
|
||||
|
||||
private val WITH_EXISTING_BRANCH = EventFields.Boolean("with_existing_branch")
|
||||
private val WORKTREE_CREATION_DIALOG_EXIT_OK_STAGE =
|
||||
WORKING_TREE_CREATION_ACTIVITY.registerStage("worktree.creation.dialog.exited.with.ok",
|
||||
arrayOf(WITH_EXISTING_BRANCH))
|
||||
|
||||
private val WORKTREE_PROJECT_WAS_OPENED_AFTER_CREATION_STAGE =
|
||||
WORKING_TREE_CREATION_ACTIVITY.registerStage("worktree.project.was.opened.after.creation")
|
||||
|
||||
@JvmStatic
|
||||
fun startLogPush(project: Project): StructuredIdeActivity {
|
||||
return PUSH_ACTIVITY.started(project)
|
||||
@@ -122,4 +140,20 @@ internal object GitOperationsCollector : CounterUsagesCollector() {
|
||||
|
||||
REBASE_START_USING_LOG_EVENT.log(project, *eventPairs)
|
||||
}
|
||||
|
||||
fun logCreateWorktreeActionInvoked(e: AnActionEvent, branch: GitStandardLocalBranch?): StructuredIdeActivity {
|
||||
return WORKING_TREE_CREATION_ACTIVITY.started(e.project) {
|
||||
listOf(EventFields.ActionPlace.with(e.place), WITH_PROVIDED_BRANCH.with(branch != null))
|
||||
}
|
||||
}
|
||||
|
||||
fun logWorktreeCreationDialogExitedWithOk(activity: StructuredIdeActivity, workingTreeData: GitWorkingTreeDialogData) {
|
||||
activity.stageStarted(WORKTREE_CREATION_DIALOG_EXIT_OK_STAGE) {
|
||||
listOf(WITH_EXISTING_BRANCH.with(workingTreeData.newBranchName == null))
|
||||
}
|
||||
}
|
||||
|
||||
fun logWorktreeProjectOpenedAfterCreation(activity: StructuredIdeActivity) {
|
||||
activity.stageStarted(WORKTREE_PROJECT_WAS_OPENED_AFTER_CREATION_STAGE)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.actions;
|
||||
|
||||
import com.intellij.dvcs.actions.DvcsQuickListContentProvider;
|
||||
import com.intellij.openapi.actionSystem.ActionManager;
|
||||
import com.intellij.openapi.actionSystem.AnAction;
|
||||
import com.intellij.openapi.actionSystem.IdeActions;
|
||||
import com.intellij.openapi.actionSystem.Separator;
|
||||
import git4idea.GitVcs;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -25,6 +26,11 @@ public class GitQuickListContentProvider extends DvcsQuickListContentProvider {
|
||||
add("Git.Stash", manager, actions);
|
||||
add("Git.Unstash", manager, actions);
|
||||
|
||||
actions.add(Separator.getInstance());
|
||||
add("Git.CreateNewWorkingTree", manager, actions);
|
||||
add("Show.WorkingTrees", manager, actions);
|
||||
actions.add(Separator.getInstance());
|
||||
|
||||
add("ChangesView.AddUnversioned", manager, actions);
|
||||
add("Vcs.CopyCurrentBranchName", manager, actions);
|
||||
add("Git.ResolveConflicts", manager, actions);
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.actions.workingTree
|
||||
|
||||
import com.intellij.dvcs.ui.CloneDvcsValidationUtils
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.impl.ProjectUtil
|
||||
import com.intellij.internal.statistic.StructuredIdeActivity
|
||||
import com.intellij.openapi.actionSystem.ActionUpdateThread
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.application.UiWithModelAccess
|
||||
import com.intellij.openapi.application.readAction
|
||||
import com.intellij.openapi.project.DumbAwareAction
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.guessProjectDir
|
||||
import com.intellij.openapi.roots.ProjectFileIndex
|
||||
import com.intellij.openapi.vcs.VcsNotifier
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||
import com.intellij.vcs.git.actions.GitSingleRefActions
|
||||
import fleet.util.safeAs
|
||||
import git4idea.GitNotificationIdsHolder
|
||||
import git4idea.GitOperationsCollector
|
||||
import git4idea.GitStandardLocalBranch
|
||||
import git4idea.actions.branch.GitBranchActionsDataKeys
|
||||
import git4idea.i18n.GitBundle
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.workingTrees.GitWorkingTreesService
|
||||
import git4idea.workingTrees.ui.GitWorkingTreesContentProvider
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.swing.Icon
|
||||
|
||||
internal class GitCreateWorkingTreeAction : DumbAwareAction() {
|
||||
|
||||
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
|
||||
|
||||
override fun update(e: AnActionEvent) {
|
||||
val project = e.project
|
||||
val singleRepository = GitWorkingTreesService.getRepoForWorkingTreesSupport(project)
|
||||
if (singleRepository == null) {
|
||||
e.presentation.isEnabledAndVisible = false
|
||||
return
|
||||
}
|
||||
|
||||
e.presentation.isEnabledAndVisible = true
|
||||
e.presentation.icon = computeIcon(e)
|
||||
val localBranchFromContext = getLocalBranchFromContext(e, singleRepository)
|
||||
if (localBranchFromContext == null) {
|
||||
e.presentation.text = GitBundle.message("action.Git.CreateNewWorkingTree.text")
|
||||
e.presentation.description = GitBundle.message("action.Git.CreateNewWorkingTree.description")
|
||||
}
|
||||
else {
|
||||
val branchName = localBranchFromContext.name
|
||||
e.presentation.text = GitBundle.message("action.Git.CreateNewWorkingTree.from.branch.text", branchName)
|
||||
e.presentation.description = GitBundle.message("action.Git.CreateNewWorkingTree.from.branch.description", branchName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeIcon(e: AnActionEvent): Icon? {
|
||||
return if (e.place == GitWorkingTreesContentProvider.GIT_WORKING_TREE_TOOLWINDOW_TAB_TOOLBAR) {
|
||||
AllIcons.General.Add
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
val project = e.project ?: return
|
||||
val repository = GitWorkingTreesService.getRepoForWorkingTreesSupport(project) ?: return
|
||||
val localBranchFromContext = getLocalBranchFromContext(e, repository)
|
||||
val ideActivity = GitOperationsCollector.logCreateWorktreeActionInvoked(e, localBranchFromContext)
|
||||
e.coroutineScope.launch(Dispatchers.Default) {
|
||||
val preDialogData = readAction {
|
||||
val initialParentPath = computeInitialParentPath(project, repository)
|
||||
GitWorkingTreePreDialogData(project, repository, ideActivity, localBranchFromContext, initialParentPath)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.UiWithModelAccess) {
|
||||
val dialog = GitWorkingTreeDialog(preDialogData)
|
||||
if (dialog.showAndGet()) {
|
||||
val workingTreeData = dialog.getWorkTreeData()
|
||||
withContext(Dispatchers.Default) {
|
||||
doCreateWorkingTree(preDialogData.project, preDialogData.repository, preDialogData.ideActivity, workingTreeData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLocalBranchFromContext(e: AnActionEvent, repository: GitRepository?): GitStandardLocalBranch? {
|
||||
val explicitRefFromCtx = e.getData(GitSingleRefActions.SELECTED_REF_DATA_KEY)
|
||||
val ref = when {
|
||||
explicitRefFromCtx != null -> explicitRefFromCtx
|
||||
e.getData(GitBranchActionsDataKeys.USE_CURRENT_BRANCH) == true -> repository?.currentBranch
|
||||
else -> null
|
||||
}
|
||||
return ref.safeAs<GitStandardLocalBranch>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches for a directory that doesn't lie under any of roots of the [project].
|
||||
*/
|
||||
@RequiresReadLock
|
||||
private fun computeInitialParentPath(project: Project, repository: GitRepository): VirtualFile? {
|
||||
val fromProject = project.guessProjectDir()?.parent
|
||||
var root: VirtualFile? = fromProject ?: repository.root.parent
|
||||
val index = ProjectFileIndex.getInstance(project)
|
||||
while (root != null && index.isInProjectOrExcluded(root)) {
|
||||
root = root.parent
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
private suspend fun doCreateWorkingTree(
|
||||
project: Project,
|
||||
repository: GitRepository,
|
||||
ideActivity: StructuredIdeActivity,
|
||||
workingTreeData: GitWorkingTreeDialogData,
|
||||
) {
|
||||
GitOperationsCollector.logWorktreeCreationDialogExitedWithOk(ideActivity, workingTreeData)
|
||||
|
||||
val path = workingTreeData.workingTreePath.path
|
||||
val destinationValidation = CloneDvcsValidationUtils.createDestination(path)
|
||||
if (destinationValidation != null) {
|
||||
VcsNotifier.getInstance(project).notifyError(GitNotificationIdsHolder.WORKTREE_COULD_NOT_CREATE_TARGET_DIR,
|
||||
GitBundle.message("notification.title.worktree.creation.failed"),
|
||||
destinationValidation.message,
|
||||
true)
|
||||
return
|
||||
}
|
||||
|
||||
val result = GitWorkingTreesService.getInstance(project).createWorkingTree(repository, workingTreeData)
|
||||
|
||||
if (!result.success) {
|
||||
VcsNotifier.getInstance(project).notifyError(GitNotificationIdsHolder.WORKTREE_ADD_FAILED,
|
||||
GitBundle.message("notification.title.worktree.creation.failed"),
|
||||
result.errorOutputAsHtmlString,
|
||||
true)
|
||||
return
|
||||
}
|
||||
|
||||
val worktreeProject = ProjectUtil.openOrImport(workingTreeData.workingTreePath.path, null, false)
|
||||
if (worktreeProject != null) {
|
||||
GitOperationsCollector.logWorktreeProjectOpenedAfterCreation(ideActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.actions.workingTree
|
||||
|
||||
import com.intellij.openapi.application.ModalityState
|
||||
import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
|
||||
import com.intellij.openapi.observable.properties.AtomicLazyProperty
|
||||
import com.intellij.openapi.observable.properties.ObservableMutableProperty
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.ui.*
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.platform.util.coroutines.childScope
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBTextField
|
||||
import com.intellij.ui.dsl.builder.*
|
||||
import com.intellij.ui.layout.ValidationInfoBuilder
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.vcs.git.ui.GitBranchesTreeIconProvider
|
||||
import com.intellij.vcsUtil.VcsUtil
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitStandardLocalBranch
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.i18n.GitBundle
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.workingTrees.GitWorkingTreesService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.ClosedSendChannelException
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.awt.Dimension
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JList
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.isDirectory
|
||||
import kotlin.io.path.listDirectoryEntries
|
||||
import kotlin.io.path.name
|
||||
|
||||
internal class GitWorkingTreeDialog(
|
||||
private val data: GitWorkingTreePreDialogData,
|
||||
) : DialogWrapper(data.project, false) {
|
||||
private val uiScope: CoroutineScope = GitWorkingTreesService.getInstance(data.project).coroutineScope.childScope("GitWorkingTreeDialog")
|
||||
private val branchToWorkingTreeMap = data.repository.workingTreeHolder.getWorkingTrees()
|
||||
.filter { it.currentBranch != null }
|
||||
.associateBy { it.currentBranch!! }
|
||||
|
||||
private lateinit var parentPathCell: Cell<TextFieldWithBrowseButton>
|
||||
private lateinit var projectNameCell: Cell<JBTextField>
|
||||
|
||||
private val existingBranchWithWorkingTree: ObservableMutableProperty<BranchWithWorkingTree?> = AtomicLazyProperty {
|
||||
data.initialExistingBranch?.toBranchWithWorkingTree()
|
||||
}
|
||||
private val projectName: ObservableMutableProperty<String> = AtomicLazyProperty { "" }
|
||||
private val parentPath: ObservableMutableProperty<String> = AtomicLazyProperty { data.initialParentPath?.path ?: "" }
|
||||
private val createNewBranch: ObservableMutableProperty<Boolean> = AtomicLazyProperty { false }
|
||||
private val newBranchName: ObservableMutableProperty<String> = AtomicLazyProperty { "" }
|
||||
|
||||
private var lastSuggestedProjectName: String = ""
|
||||
private var projectNameEdited: Boolean = false
|
||||
private val lastPathValidationChannel = Channel<PathValidationMessage>(Channel.CONFLATED)
|
||||
|
||||
init {
|
||||
init()
|
||||
title = GitBundle.message("working.tree.dialog.title")
|
||||
setOKButtonText(GitBundle.message("working.tree.dialog.button.ok"))
|
||||
|
||||
Disposer.register(disposable) {
|
||||
uiScope.cancel()
|
||||
lastPathValidationChannel.close()
|
||||
}
|
||||
}
|
||||
|
||||
private data class BranchWithWorkingTree(val branch: GitBranch, val workingTree: GitWorkingTree?)
|
||||
|
||||
private fun GitStandardLocalBranch.toBranchWithWorkingTree(): BranchWithWorkingTree =
|
||||
BranchWithWorkingTree(this, branchToWorkingTreeMap[this])
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
return panel {
|
||||
row(GitBundle.message("working.tree.dialog.label.existing.branch")) {
|
||||
val localBranchesWithTrees: List<BranchWithWorkingTree?> = computeBranchesWithWorkingTrees()
|
||||
comboBox(localBranchesWithTrees, BranchWithTreeCellRenderer(data.project, data.repository))
|
||||
.bindItem(existingBranchWithWorkingTree).align(Align.FILL).validationOnApply { validateBranchOnApply(it) }
|
||||
existingBranchWithWorkingTree.afterChange { updateSuggestedProjectName() }
|
||||
}
|
||||
|
||||
row {
|
||||
checkBox(GitBundle.message("working.tree.dialog.checkbox.new.branch")).bindSelected(createNewBranch).gap(RightGap.SMALL)
|
||||
createNewBranch.afterChange { updateSuggestedProjectName() }
|
||||
|
||||
textField().bindText(newBranchName).align(Align.FILL).validationOnApply { validateBranchNameOnApply(it) }
|
||||
.comment(getNewBranchComment())
|
||||
.enabledIf(createNewBranch)
|
||||
newBranchName.afterChange { updateSuggestedProjectName() }
|
||||
}
|
||||
.bottomGap(BottomGap.MEDIUM)
|
||||
.layout(RowLayout.LABEL_ALIGNED)
|
||||
|
||||
row(GitBundle.message("working.tree.dialog.label.name")) {
|
||||
projectNameCell = textField().bindText(projectName).align(Align.FILL).validationOnApply { validateProjectNameOnApply(it) }
|
||||
lastSuggestedProjectName = projectNameCell.component.text
|
||||
updateSuggestedProjectName()
|
||||
}
|
||||
row(GitBundle.message("working.tree.dialog.label.location")) {
|
||||
val descriptor = FileChooserDescriptorFactory.singleDir()
|
||||
.withTitle(GitBundle.message("working.tree.dialog.label.location.file.chooser.title"))
|
||||
parentPathCell = textFieldWithBrowseButton(descriptor, data.project)
|
||||
.bindText(parentPath).align(Align.FILL).validationOnApply { validateLocationOnApply() }
|
||||
.comment("")
|
||||
|
||||
supportFieldCommentsAndPathValidation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidationInfoBuilder.validateBranchOnApply(box: ComboBox<BranchWithWorkingTree?>): ValidationInfo? {
|
||||
val value = existingBranchWithWorkingTree.get()
|
||||
return when {
|
||||
value == null -> error(GitBundle.message("working.tree.dialog.location.validation.select.branch"))
|
||||
value.workingTree != null -> {
|
||||
val item = (box.selectedItem as? BranchWithWorkingTree)!!
|
||||
error(GitBundle.message("working.tree.dialog.branch.validation.already.checked.out.in.working.tree",
|
||||
item.branch.name, item.workingTree!!.path.name))
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidationInfoBuilder.validateProjectNameOnApply(field: JBTextField): ValidationInfo? {
|
||||
return if (field.text.isBlank()) {
|
||||
error(GitBundle.message("working.tree.dialog.location.validation.provide.name"))
|
||||
}
|
||||
else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun ValidationInfoBuilder.validateBranchNameOnApply(field: JBTextField): ValidationInfo? {
|
||||
return if (field.isEnabled && field.text.isBlank())
|
||||
error(GitBundle.message("working.tree.dialog.location.validation.provide.new.branch.name"))
|
||||
else
|
||||
null
|
||||
}
|
||||
|
||||
private fun ValidationInfoBuilder.validateLocationOnApply(): ValidationInfo? {
|
||||
if (parentPath.get().isBlank()) return error(GitBundle.message("working.tree.dialog.location.validation.empty"))
|
||||
|
||||
val validation = lastPathValidationChannel.tryReceive().getOrNull() ?: return null
|
||||
if (parentPath.get() == validation.parentPath && projectName.get() == validation.dirName && validation.message != null) {
|
||||
return error(validation.message)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun supportFieldCommentsAndPathValidation() {
|
||||
updateParentPathCellComment()
|
||||
parentPath.afterChange {
|
||||
updateParentPathCellComment()
|
||||
precomputePathValidation()
|
||||
}
|
||||
projectName.afterChange {
|
||||
precomputePathValidation()
|
||||
if (hasErrors(parentPathCell.component.textField)) {
|
||||
initValidation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeBranchesWithWorkingTrees(): List<BranchWithWorkingTree> {
|
||||
val branches = data.repository.branches
|
||||
val result = branches.localBranches.sortedBy { it.name }
|
||||
.map { it.toBranchWithWorkingTree() }.toMutableList()
|
||||
val remotes = branches.remoteBranches.sortedBy { it.name }
|
||||
.map { BranchWithWorkingTree(it, null) }
|
||||
result.addAll(remotes)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getNewBranchComment(): @NlsContexts.DetailedDescription String {
|
||||
val name = existingBranchWithWorkingTree.get()?.branch?.name
|
||||
return if (name == null) {
|
||||
GitBundle.message("working.tree.dialog.label.new.branch.detached.comment")
|
||||
}
|
||||
else {
|
||||
GitBundle.message("working.tree.dialog.label.new.branch.comment", name)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSuggestedProjectName() {
|
||||
if (projectNameEdited) return
|
||||
if (lastSuggestedProjectName != projectNameCell.component.text) {
|
||||
projectNameEdited = true
|
||||
return
|
||||
}
|
||||
val branchToUse = if (createNewBranch.get()) newBranchName.get() else existingBranchWithWorkingTree.get()?.branch?.name
|
||||
val newName = createInitialWorkingTreeName(data.projectNameBase, branchToUse)
|
||||
projectName.set(newName)
|
||||
lastSuggestedProjectName = newName
|
||||
}
|
||||
|
||||
private fun createInitialWorkingTreeName(root: Path, branchName: String?): String {
|
||||
return if (branchName.isNullOrEmpty()) {
|
||||
""
|
||||
}
|
||||
else {
|
||||
root.name + "-" + branchName.substringAfterLast("/")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateParentPathCellComment() {
|
||||
parentPathCell.comment?.text = GitBundle.message("working.tree.dialog.label.location.comment", getPresentablePath(parentPath.get()))
|
||||
}
|
||||
|
||||
private class BranchWithTreeCellRenderer(project: Project, repository: GitRepository) :
|
||||
ColoredListCellRenderer<BranchWithWorkingTree?>() {
|
||||
|
||||
private val repositoryModel = GitWorkingTreesService.getInstance(project).repositoryToModel(repository)
|
||||
|
||||
override fun customizeCellRenderer(
|
||||
list: JList<out BranchWithWorkingTree?>,
|
||||
value: BranchWithWorkingTree?,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
hasFocus: Boolean,
|
||||
) {
|
||||
if (value == null) {
|
||||
append(GitBundle.message("working.tree.dialog.existing.branch.combo.box.empty.text"))
|
||||
return
|
||||
}
|
||||
val branch = value.branch
|
||||
append(branch.name)
|
||||
|
||||
val isCurrent = repositoryModel?.state?.isCurrentRef(branch) ?: false
|
||||
val isFavorite = repositoryModel?.favoriteRefs?.contains(branch) ?: false
|
||||
|
||||
icon = GitBranchesTreeIconProvider.forRef(branch, current = isCurrent, favorite = isFavorite,
|
||||
favoriteToggleOnClick = false, selected = selected)
|
||||
|
||||
val workingTreeName = value.workingTree?.path?.name ?: return
|
||||
append(" ")
|
||||
append(workingTreeName, SimpleTextAttributes.GRAYED_ATTRIBUTES)
|
||||
}
|
||||
}
|
||||
|
||||
fun getWorkTreeData(): GitWorkingTreeDialogData {
|
||||
val path = VcsUtil.getFilePath(Paths.get(parentPath.get()).resolve(projectName.get()), true)
|
||||
return if (createNewBranch.get()) {
|
||||
GitWorkingTreeDialogData.createForNewBranch(path, existingBranchWithWorkingTree.get()!!.branch, newBranchName.get())
|
||||
}
|
||||
else {
|
||||
GitWorkingTreeDialogData.createForExistingBranch(path, existingBranchWithWorkingTree.get()!!.branch)
|
||||
}
|
||||
}
|
||||
|
||||
private fun precomputePathValidation() {
|
||||
val parentPath = parentPath.get()
|
||||
val dirName = projectName.get()
|
||||
uiScope.launch(Dispatchers.IO + ModalityState.current().asContextElement()) {
|
||||
val message = getPathValidationMessage(parentPath, dirName)
|
||||
try {
|
||||
lastPathValidationChannel.send(PathValidationMessage(parentPath, dirName, message))
|
||||
}
|
||||
catch (e: ClosedSendChannelException) {
|
||||
LOG.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class PathValidationMessage(val parentPath: String, val dirName: String, val message: @NlsContexts.DialogMessage String?)
|
||||
|
||||
override fun getDimensionServiceKey(): String = "Git.CreateWorkingTreeDialog"
|
||||
|
||||
override fun getInitialSize(): Dimension {
|
||||
return Dimension(JBUI.DialogSizes.medium().width, -1)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG: Logger = logger<GitWorkingTreeDialog>()
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun getPathValidationMessage(parentPath: String, dirName: String): @NlsContexts.DialogMessage String? {
|
||||
if (dirName.isBlank()) return null
|
||||
if (parentPath.isBlank()) return GitBundle.message("working.tree.dialog.location.validation.empty")
|
||||
|
||||
val fullPath = Paths.get(parentPath).resolve(dirName)
|
||||
return when {
|
||||
!fullPath.exists() -> null
|
||||
!fullPath.isDirectory() -> GitBundle.message("working.tree.dialog.location.validation.is.a.file",
|
||||
getPresentablePath(fullPath.toString()))
|
||||
fullPath.listDirectoryEntries().isNotEmpty() -> GitBundle.message("working.tree.dialog.location.validation.is.not.empty",
|
||||
getPresentablePath(fullPath.toString()))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.actions.workingTree
|
||||
|
||||
import com.intellij.openapi.vcs.FilePath
|
||||
import git4idea.GitBranch
|
||||
|
||||
internal class GitWorkingTreeDialogData private constructor(
|
||||
val workingTreePath: FilePath,
|
||||
val sourceBranch: GitBranch,
|
||||
val newBranchName: String?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun createForNewBranch(workingTreePath: FilePath, sourceBranch: GitBranch, newBranchName: String) =
|
||||
GitWorkingTreeDialogData(workingTreePath, sourceBranch, newBranchName)
|
||||
|
||||
fun createForExistingBranch(workingTreePath: FilePath, sourceBranch: GitBranch) =
|
||||
GitWorkingTreeDialogData(workingTreePath, sourceBranch, null)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.actions.workingTree
|
||||
|
||||
import com.intellij.internal.statistic.StructuredIdeActivity
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import git4idea.GitStandardLocalBranch
|
||||
import git4idea.repo.GitRepository
|
||||
import java.nio.file.Path
|
||||
|
||||
internal data class GitWorkingTreePreDialogData(
|
||||
val project: Project,
|
||||
val repository: GitRepository,
|
||||
val ideActivity: StructuredIdeActivity,
|
||||
val initialExistingBranch: GitStandardLocalBranch?,
|
||||
val initialParentPath: VirtualFile?,
|
||||
//should be the main repo root in case of working in a worktree
|
||||
val projectNameBase: Path = repository.repositoryFiles.configFile.toPath().parent.parent,
|
||||
)
|
||||
@@ -22,7 +22,7 @@ internal class ShowWorkingTreesAction : DumbAwareAction() {
|
||||
private fun shouldShow(e: AnActionEvent): Boolean {
|
||||
val project = e.project ?: return false
|
||||
if (ToolWindowManager.getInstance(project).getToolWindow(ChangesViewContentManager.TOOLWINDOW_ID) == null) return false
|
||||
return GitWorkingTreesService.getSingleRepositoryOrNullIfEnabled(project) != null
|
||||
return GitWorkingTreesService.getRepoForWorkingTreesSupport(project) != null
|
||||
}
|
||||
|
||||
override fun actionPerformed(e: AnActionEvent) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.commands;
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
@@ -8,6 +8,8 @@ import com.intellij.openapi.vcs.FilePath;
|
||||
import com.intellij.openapi.vcs.VcsException;
|
||||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import com.intellij.vcs.log.Hash;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitWorkingTree;
|
||||
import git4idea.branch.GitRebaseParams;
|
||||
import git4idea.push.GitPushParams;
|
||||
import git4idea.repo.GitRemote;
|
||||
@@ -340,4 +342,16 @@ public interface Git {
|
||||
|
||||
@Nullable
|
||||
GitObjectType getObjectTypeEnum(@NotNull GitRepository repository, @NotNull String object);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult deleteWorkingTree(@NotNull Project project, @NotNull GitWorkingTree tree);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult listWorktrees(@NotNull GitRepository repository, GitLineHandlerListener @NotNull ... listeners);
|
||||
|
||||
@NotNull
|
||||
GitCommandResult createWorkingTree(@NotNull GitRepository repository,
|
||||
@NotNull FilePath workingTreePath,
|
||||
@NotNull GitBranch sourceBranch,
|
||||
@Nullable String newBranchName);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.commands;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@@ -26,8 +26,10 @@ import com.intellij.vcs.log.Hash;
|
||||
import com.intellij.vcs.log.impl.HashImpl;
|
||||
import com.intellij.vcsUtil.VcsFileUtil;
|
||||
import com.intellij.vcsUtil.VcsUtil;
|
||||
import git4idea.GitBranch;
|
||||
import git4idea.GitContentRevision;
|
||||
import git4idea.GitUtil;
|
||||
import git4idea.GitWorkingTree;
|
||||
import git4idea.branch.GitRebaseParams;
|
||||
import git4idea.config.GitConfigUtil;
|
||||
import git4idea.config.GitExecutable;
|
||||
@@ -47,6 +49,7 @@ import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
import static git4idea.GitUtil.COMMENT_CHAR;
|
||||
@@ -842,6 +845,43 @@ public class GitImpl extends GitImplBase {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull GitCommandResult deleteWorkingTree(@NotNull Project project, @NotNull GitWorkingTree tree) {
|
||||
GitLineHandler handler = new GitLineHandler(project, Paths.get(tree.getPath().getPath()), GitCommand.WORKTREE);
|
||||
handler.setSilent(false);
|
||||
handler.setStdoutSuppressed(false);
|
||||
handler.setStderrSuppressed(false);
|
||||
handler.addParameters(asList("remove", tree.getPath().getPath(), "--force"));
|
||||
return Git.getInstance().runCommand(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull GitCommandResult listWorktrees(@NotNull GitRepository repository, GitLineHandlerListener @NotNull ... listeners) {
|
||||
GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.WORKTREE);
|
||||
handler.addParameters("list");
|
||||
handler.addParameters("--porcelain");
|
||||
|
||||
addListeners(handler, listeners);
|
||||
return Git.getInstance().runCommand(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull GitCommandResult createWorkingTree(@NotNull GitRepository repository,
|
||||
@NotNull FilePath workingTreePath,
|
||||
@NotNull GitBranch sourceBranch,
|
||||
@Nullable String newBranchName) {
|
||||
GitLineHandler handler = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.WORKTREE);
|
||||
handler.setSilent(false);
|
||||
handler.setStdoutSuppressed(false);
|
||||
handler.setStderrSuppressed(false);
|
||||
handler.addParameters("add");
|
||||
if (newBranchName != null) {
|
||||
handler.addParameters("-b", newBranchName);
|
||||
}
|
||||
handler.addParameters(workingTreePath.getPath(), sourceBranch.getName());
|
||||
return Git.getInstance().runCommand(handler);
|
||||
}
|
||||
|
||||
private static void addListeners(@NotNull GitLineHandler handler, GitLineHandlerListener @NotNull ... listeners) {
|
||||
addListeners(handler, asList(listeners));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.repo
|
||||
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.progress.util.BackgroundTaskUtil
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.commands.Git
|
||||
import git4idea.remoteApi.GitRepositoryFrontendSynchronizer
|
||||
import git4idea.workingTrees.GitListWorktreeLineListener
|
||||
import git4idea.workingTrees.GitWorkingTreesCommandService
|
||||
|
||||
class GitWorkingTreeHolder(repository: GitRepository) : GitRepositoryDataHolder(repository, "GitWorkingTreeHolder") {
|
||||
private var workingTrees: Collection<GitWorkingTree> = emptyList()
|
||||
@@ -25,7 +25,7 @@ class GitWorkingTreeHolder(repository: GitRepository) : GitRepositoryDataHolder(
|
||||
LOG.debug { "Reloading working trees for ${repository.root}" }
|
||||
|
||||
val listener = GitListWorktreeLineListener(repository)
|
||||
val commandResult = GitWorkingTreesCommandService.getInstance().listWorktrees(repository, listener)
|
||||
val commandResult = Git.getInstance().listWorktrees(repository, listener)
|
||||
if (!commandResult.success()) {
|
||||
LOG.info("Failed to list worktrees: $commandResult.errorOutputAsJoinedString")
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.commands.*
|
||||
import git4idea.repo.GitRepository
|
||||
import kotlin.io.path.Path
|
||||
|
||||
@Service
|
||||
internal class GitWorkingTreesCommandService {
|
||||
|
||||
companion object {
|
||||
fun getInstance(): GitWorkingTreesCommandService = service<GitWorkingTreesCommandService>()
|
||||
}
|
||||
|
||||
fun deleteWorkingTree(project: Project, tree: GitWorkingTree): GitCommandResult {
|
||||
val handler = GitLineHandler(project, Path(tree.path.path), GitCommand.WORKTREE)
|
||||
handler.addParameters(listOf("remove", tree.path.path, "--force"))
|
||||
return Git.getInstance().runCommand(handler)
|
||||
}
|
||||
|
||||
fun listWorktrees(repository: GitRepository, vararg listeners: GitLineHandlerListener): GitCommandResult {
|
||||
val handler = GitLineHandler(repository.project, repository.root, GitCommand.WORKTREE)
|
||||
handler.addParameters("list")
|
||||
handler.addParameters("--porcelain")
|
||||
|
||||
for (listener in listeners) {
|
||||
handler.addLineListener(listener)
|
||||
}
|
||||
return Git.getInstance().runCommand(handler)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,19 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.dvcs.repo.repositoryId
|
||||
import com.intellij.ide.util.PropertiesComponent
|
||||
import com.intellij.openapi.components.Service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.util.NlsContexts
|
||||
import com.intellij.openapi.util.NlsSafe
|
||||
import com.intellij.platform.ide.progress.withBackgroundProgress
|
||||
import com.intellij.vcs.git.repo.GitRepositoriesHolder
|
||||
import com.intellij.vcs.git.repo.GitRepositoryModel
|
||||
import git4idea.GitRemoteBranch
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialogData
|
||||
import git4idea.commands.Git
|
||||
import git4idea.i18n.GitBundle
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.repo.GitRepositoryManager
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -19,7 +29,7 @@ internal class GitWorkingTreesService(private val project: Project, val coroutin
|
||||
/**
|
||||
* So far only the `single repository` case is supported for working trees
|
||||
*/
|
||||
fun getSingleRepositoryOrNullIfEnabled(project: Project?): GitRepository? {
|
||||
fun getRepoForWorkingTreesSupport(project: Project?): GitRepository? {
|
||||
if (project == null) return null
|
||||
if (!GitWorkingTreesUtil.isWorkingTreesFeatureEnabled()) return null
|
||||
val repositories = GitRepositoryManager.getInstance(project).repositories
|
||||
@@ -27,8 +37,12 @@ internal class GitWorkingTreesService(private val project: Project, val coroutin
|
||||
}
|
||||
}
|
||||
|
||||
fun repositoryToModel(repository: GitRepository): GitRepositoryModel? {
|
||||
return GitRepositoriesHolder.getInstance(project).get(repository.repositoryId())
|
||||
}
|
||||
|
||||
fun shouldWorkingTreesTabBeShown(): Boolean {
|
||||
if (getSingleRepositoryOrNullIfEnabled(project) == null) {
|
||||
if (getRepoForWorkingTreesSupport(project) == null) {
|
||||
return false
|
||||
}
|
||||
return !PropertiesComponent.getInstance(project).getBoolean(WORKING_TREE_TAB_CLOSED_BY_USER_PROPERTY, false)
|
||||
@@ -41,4 +55,34 @@ internal class GitWorkingTreesService(private val project: Project, val coroutin
|
||||
fun workingTreesTabClosedByUser() {
|
||||
PropertiesComponent.getInstance(project).setValue(WORKING_TREE_TAB_CLOSED_BY_USER_PROPERTY, true)
|
||||
}
|
||||
|
||||
class Result private constructor(
|
||||
val success: Boolean,
|
||||
val errorOutputAsHtmlString: @NlsSafe @NlsContexts.NotificationContent String,
|
||||
) {
|
||||
companion object {
|
||||
val SUCCESS = Result(true, "")
|
||||
|
||||
fun createFailure(@NlsContexts.NotificationContent errorOutputAsHtmlString: @NlsSafe String): Result {
|
||||
return Result(false, errorOutputAsHtmlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createWorkingTree(repository: GitRepository, data: GitWorkingTreeDialogData): Result {
|
||||
return withBackgroundProgress(project, GitBundle.message("progress.title.creating.worktree"), cancellable = true) {
|
||||
val newBranchName = when {
|
||||
data.newBranchName != null -> data.newBranchName
|
||||
data.sourceBranch is GitRemoteBranch -> data.sourceBranch.nameForRemoteOperations
|
||||
else -> null
|
||||
}
|
||||
val commandResult = Git.getInstance().createWorkingTree(repository, data.workingTreePath, data.sourceBranch, newBranchName)
|
||||
if (commandResult.success()) {
|
||||
Result.SUCCESS
|
||||
}
|
||||
else {
|
||||
Result.createFailure(commandResult.errorOutputAsHtmlString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
|
||||
@@ -3,7 +3,6 @@ package git4idea.workingTrees.ui
|
||||
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.openapi.actionSystem.*
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces.CHANGES_VIEW_TOOLBAR
|
||||
import com.intellij.openapi.actionSystem.toolbarLayout.ToolbarLayoutStrategy
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.extensions.ExtensionNotApplicableException
|
||||
@@ -36,6 +35,12 @@ import javax.swing.JList
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
internal class GitWorkingTreesContentProvider(private val project: Project) : ChangesViewContentProvider {
|
||||
|
||||
companion object {
|
||||
//registered with com.intellij.statistics.actionCustomPlaceAllowlist ExtensionPoint
|
||||
internal const val GIT_WORKING_TREE_TOOLWINDOW_TAB_TOOLBAR: String = "GitWorkingTreeToolWindowTabToolbar"
|
||||
}
|
||||
|
||||
override fun initTabContent(content: Content) {
|
||||
content.component = GitWorkingTreesUi()
|
||||
}
|
||||
@@ -46,8 +51,10 @@ internal class GitWorkingTreesContentProvider(private val project: Project) : Ch
|
||||
val scrollPane = ScrollPaneFactory.createScrollPane(list, true)
|
||||
addToCenter(scrollPane)
|
||||
|
||||
val toolbarActionGroup = ActionManager.getInstance().getAction("Git.WorkingTrees.ToolwindowGroup.Toolbar") as ActionGroup
|
||||
val toolbar = ActionManager.getInstance().createActionToolbar(CHANGES_VIEW_TOOLBAR, toolbarActionGroup, false)
|
||||
val actionManager = ActionManager.getInstance()
|
||||
val toolbarActionGroup = actionManager.getAction("Git.WorkingTrees.ToolwindowGroup.Toolbar") as ActionGroup
|
||||
val toolbar = actionManager.createActionToolbar(GIT_WORKING_TREE_TOOLWINDOW_TAB_TOOLBAR,
|
||||
toolbarActionGroup, false)
|
||||
toolbar.setTargetComponent(list)
|
||||
toolbar.layoutStrategy = ToolbarLayoutStrategy.AUTOLAYOUT_STRATEGY
|
||||
toolbar.setOrientation(SwingConstants.VERTICAL)
|
||||
@@ -103,7 +110,7 @@ internal class GitWorkingTreesContentProvider(private val project: Project) : Ch
|
||||
|
||||
fun reload(project: Project) {
|
||||
clear()
|
||||
val currentRepository = GitWorkingTreesService.getSingleRepositoryOrNullIfEnabled(project)
|
||||
val currentRepository = GitWorkingTreesService.getRepoForWorkingTreesSupport(project)
|
||||
repository = currentRepository
|
||||
val workingTrees = currentRepository?.workingTreeHolder?.getWorkingTrees()
|
||||
if (workingTrees != null && workingTrees.size > 1) {
|
||||
|
||||
@@ -15,9 +15,9 @@ import git4idea.GitNotificationIdsHolder
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.actions.workingTree.GitWorkingTreeTabActionsDataKeys
|
||||
import git4idea.actions.workingTree.GitWorkingTreeTabActionsDataKeys.SELECTED_WORKING_TREES
|
||||
import git4idea.commands.Git
|
||||
import git4idea.i18n.GitBundle
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.workingTrees.GitWorkingTreesCommandService
|
||||
import git4idea.workingTrees.GitWorkingTreesService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -71,7 +71,7 @@ internal class RemoveWorkingTreeAction : DumbAwareAction() {
|
||||
|
||||
private suspend fun delete(project: Project, tree: GitWorkingTree, repository: GitRepository) {
|
||||
val commandResult = withBackgroundProgress(project, GitBundle.message("progress.title.deleting.worktree"), cancellable = true) {
|
||||
service<GitWorkingTreesCommandService>().deleteWorkingTree(project, tree)
|
||||
service<Git>().deleteWorkingTree(project, tree)
|
||||
}
|
||||
if (commandResult.success()) {
|
||||
repository.workingTreeHolder.reload()
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.repo
|
||||
|
||||
import com.intellij.ide.impl.OpenProjectTask
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ex.ProjectManagerEx
|
||||
import com.intellij.openapi.vcs.Executor.cd
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.test.git
|
||||
import git4idea.test.initRepo
|
||||
import git4idea.test.registerRepo
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
class GitLinkedWorktreeTest : GitRepositoriesFrontendHolderTestBase() {
|
||||
private val branchName = "feature"
|
||||
private val mainRepoFileName = "mainRepo"
|
||||
val mainRepoPath: Path
|
||||
get() = testNioRoot.resolve(mainRepoFileName)
|
||||
|
||||
override fun doCreateAndOpenProject(): Project {
|
||||
val projectRootPath = getProjectDirOrFile(true)
|
||||
return setUpProjectAndWorkingTree(testNioRoot, projectRootPath, mainRepoFileName, branchName)
|
||||
}
|
||||
|
||||
override fun createRepository(): GitRepository {
|
||||
return registerRepo(project, projectNioRoot)
|
||||
}
|
||||
|
||||
fun `test creating a worktree on a main repo`() {
|
||||
doTestWorkingTreeCreation(
|
||||
mainRepoPath,
|
||||
GitWorkingTree(mainRepoPath.toString(), "refs/heads/master", true, false),
|
||||
GitWorkingTree(repo.root.path, GitBranch.REFS_HEADS_PREFIX + branchName, false, true),
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setUpProjectAndWorkingTree(
|
||||
testNioRoot: Path,
|
||||
projectRootPath: Path,
|
||||
mainRepoRelativePath: String,
|
||||
branchName: String,
|
||||
): Project {
|
||||
val mainRepoPath = testNioRoot.resolve(mainRepoRelativePath)
|
||||
initRepo(null, mainRepoPath, true)
|
||||
|
||||
cd(mainRepoPath)
|
||||
git(null, "worktree add -B $branchName ../${projectRootPath.fileName}")
|
||||
Files.createDirectories(projectRootPath.resolve(Project.DIRECTORY_STORE_FOLDER))
|
||||
return runBlocking {
|
||||
ProjectManagerEx.getInstanceEx().openProjectAsync(projectIdentityFile = projectRootPath, options = OpenProjectTask {})!!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,80 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.repo
|
||||
|
||||
import com.intellij.dvcs.repo.repositoryId
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.platform.project.projectId
|
||||
import com.intellij.testFramework.assertErrorLogged
|
||||
import com.intellij.vcs.git.repo.GitRepositoriesHolder
|
||||
import com.intellij.vcs.git.repo.GitRepositoryModel
|
||||
import com.intellij.vcs.git.rpc.GitRepositoryApi
|
||||
import com.intellij.vcs.git.rpc.GitUiSettingsApi
|
||||
import com.intellij.vcsUtil.VcsUtil.getFilePath
|
||||
import git4idea.GitStandardLocalBranch
|
||||
import git4idea.GitTag
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.branch.GitBranchType
|
||||
import git4idea.branch.GitBranchesCollection
|
||||
import git4idea.test.GitSingleRepoTest
|
||||
import git4idea.test.checkoutNew
|
||||
import git4idea.test.createSubRepository
|
||||
import git4idea.test.git
|
||||
import git4idea.ui.branch.GitBranchManager
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase() {
|
||||
class GitRepositoriesFrontendHolderTest : GitSingleRepoTest() {
|
||||
class GitRepositoriesHolderEventHandler {
|
||||
private val updatesChanel = Channel<GitRepositoriesHolder.UpdateType>(capacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
fun subscribe(project: Project) {
|
||||
project.messageBus.connect().subscribe(GitRepositoriesHolder.UPDATES,
|
||||
GitRepositoriesHolder.UpdatesListener { updateType -> updatesChanel.trySend(updateType) })
|
||||
}
|
||||
|
||||
fun executeAndExpectEvent(
|
||||
operation: suspend () -> Unit,
|
||||
condition: (currentEvent: GitRepositoriesHolder.UpdateType, previousEvents: List<GitRepositoriesHolder.UpdateType>) -> Boolean,
|
||||
) {
|
||||
runBlocking {
|
||||
skipEvents()
|
||||
operation()
|
||||
val collected = mutableListOf<GitRepositoriesHolder.UpdateType>()
|
||||
withTimeout(5.seconds) {
|
||||
updatesChanel.consumeAsFlow().first {
|
||||
LOG.info("Received update: $it")
|
||||
collected.add(it)
|
||||
condition(it, collected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun skipEvents() {
|
||||
withTimeout(1.seconds) {
|
||||
while (!updatesChanel.isEmpty) {
|
||||
updatesChanel.tryReceive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val eventHandler = GitRepositoriesHolderEventHandler()
|
||||
|
||||
override fun getDebugLogCategories() = super.getDebugLogCategories().plus(GitRepositoriesHolder::class.java.name)
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
eventHandler.subscribe(project)
|
||||
}
|
||||
|
||||
fun `test single repository data is available`() {
|
||||
val holder = GitRepositoriesHolder.getInstance(project)
|
||||
@@ -50,7 +103,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
holder.init()
|
||||
}
|
||||
assertSize(1, holder.getAll())
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{ vcsManager.unregisterVcs(vcs) },
|
||||
{ event, _ -> event == GitRepositoriesHolder.UpdateType.REPOSITORY_DELETED })
|
||||
|
||||
@@ -64,7 +117,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
}
|
||||
assertSize(1, holder.getAll())
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{ repo.createSubRepository("nested") },
|
||||
{ event, _ -> event == GitRepositoriesHolder.UpdateType.REPOSITORY_CREATED })
|
||||
|
||||
@@ -80,7 +133,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
assertTrue("master should be favorite",
|
||||
holder.getTestRepo().favoriteRefs.contains(GitStandardLocalBranch("master")))
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{
|
||||
project.service<GitBranchManager>().setFavorite(
|
||||
GitBranchType.LOCAL,
|
||||
@@ -108,7 +161,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
branchesToCheckout.forEach { branch -> repo.checkoutNew(branch.name) }
|
||||
|
||||
val newCurrentBranch = GitStandardLocalBranch("new-branch")
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{ repo.checkoutNew(newCurrentBranch.name) },
|
||||
{ event, _ -> event == GitRepositoriesHolder.UpdateType.REPOSITORY_STATE_UPDATED })
|
||||
|
||||
@@ -132,7 +185,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
|
||||
assertEquals(setOf(GitTag(tagName)), holder.getTestRepo().state.tags)
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{ GitUiSettingsApi.getInstance().setShowTags(project.projectId(), false) },
|
||||
{ event, _ -> event == GitRepositoriesHolder.UpdateType.TAGS_HIDDEN }
|
||||
)
|
||||
@@ -150,7 +203,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
holder.init()
|
||||
}
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{
|
||||
GitUiSettingsApi.getInstance().setShowTags(project.projectId(), false)
|
||||
GitUiSettingsApi.getInstance().setShowTags(project.projectId(), true)
|
||||
@@ -169,7 +222,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
holder.init()
|
||||
}
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{ GitRepositoryApi.getInstance().forceSync(project.projectId()) },
|
||||
{ event, _ -> event == GitRepositoriesHolder.UpdateType.RELOAD_STATE }
|
||||
)
|
||||
@@ -181,7 +234,7 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
holder.init()
|
||||
}
|
||||
|
||||
executeAndExpectEvent(
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{
|
||||
holder.clearRepositories()
|
||||
assertTrue(holder.getAll().isEmpty())
|
||||
@@ -193,10 +246,9 @@ class GitRepositoriesFrontendHolderTest : GitRepositoriesFrontendHolderTestBase(
|
||||
holder.getTestRepo()
|
||||
}
|
||||
|
||||
fun `test creating a worktree on a main repo`() {
|
||||
doTestWorkingTreeCreation(
|
||||
projectNioRoot,
|
||||
GitWorkingTree(repo.root.path, repo.currentBranch!!.fullName, true, true)
|
||||
)
|
||||
private fun GitRepositoriesHolder.getTestRepo(): GitRepositoryModel {
|
||||
val holderRepo = checkNotNull(get(repo.repositoryId()))
|
||||
assertEquals(holderRepo.root, getFilePath(repo.root))
|
||||
return holderRepo
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.repo
|
||||
|
||||
import com.intellij.dvcs.repo.repositoryId
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.vcs.git.repo.GitRepositoriesHolder
|
||||
import com.intellij.vcs.git.repo.GitRepositoryModel
|
||||
import com.intellij.vcsUtil.VcsUtil.getFilePath
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.test.GitSingleRepoTest
|
||||
import git4idea.test.git
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
abstract class GitRepositoriesFrontendHolderTestBase : GitSingleRepoTest() {
|
||||
private val updatesChanel = Channel<GitRepositoriesHolder.UpdateType>(capacity = 1000, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
override fun getDebugLogCategories() = super.getDebugLogCategories().plus(GitRepositoriesHolder::class.java.name)
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
|
||||
project.messageBus.connect().subscribe(GitRepositoriesHolder.UPDATES,
|
||||
GitRepositoriesHolder.UpdatesListener { updateType -> updatesChanel.trySend(updateType) })
|
||||
}
|
||||
|
||||
protected fun GitRepositoriesHolder.getTestRepo(): GitRepositoryModel {
|
||||
val holderRepo = checkNotNull(get(repo.repositoryId()))
|
||||
assertEquals(holderRepo.root, getFilePath(repo.root))
|
||||
return holderRepo
|
||||
}
|
||||
|
||||
protected fun executeAndExpectEvent(
|
||||
operation: suspend () -> Unit,
|
||||
condition: (currentEvent: GitRepositoriesHolder.UpdateType, previousEvents: List<GitRepositoriesHolder.UpdateType>) -> Boolean,
|
||||
) {
|
||||
runBlocking {
|
||||
skipEvents()
|
||||
operation()
|
||||
val collected = mutableListOf<GitRepositoriesHolder.UpdateType>()
|
||||
withTimeout(5.seconds) {
|
||||
updatesChanel.consumeAsFlow().first {
|
||||
LOG.info("Received update: $it")
|
||||
collected.add(it)
|
||||
condition(it, collected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun skipEvents() {
|
||||
withTimeout(1.seconds) {
|
||||
while (!updatesChanel.isEmpty) {
|
||||
updatesChanel.tryReceive()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun doTestWorkingTreeCreation(mainDirectoryRepoPath: Path, vararg initialExpectedWorkingTrees: GitWorkingTree) {
|
||||
val holder = GitRepositoriesHolder.getInstance(project)
|
||||
runBlocking {
|
||||
holder.init()
|
||||
}
|
||||
|
||||
assertSameElements(holder.getTestRepo().state.workingTrees, initialExpectedWorkingTrees.toList())
|
||||
|
||||
val branch = "tree"
|
||||
val treeRoot = "treeRoot"
|
||||
|
||||
executeAndExpectEvent(
|
||||
{
|
||||
repo.git("worktree add -B $branch ../$treeRoot")
|
||||
val worktreesDir = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(mainDirectoryRepoPath.resolve(".git/worktrees"))
|
||||
refresh(worktreesDir!!)
|
||||
},
|
||||
{ event, _ ->
|
||||
return@executeAndExpectEvent event == GitRepositoriesHolder.UpdateType.WORKING_TREES_LOADED
|
||||
}
|
||||
)
|
||||
|
||||
val workingTrees = holder.getTestRepo().state.workingTrees
|
||||
val expected = initialExpectedWorkingTrees.toMutableList()
|
||||
expected.add(
|
||||
GitWorkingTree("${testNioRoot.pathString}/$treeRoot", GitRefUtil.addRefsHeadsPrefixIfNeeded(branch)!!, false, false)
|
||||
)
|
||||
|
||||
assertSameElements(workingTrees, expected)
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,44 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.openapi.ui.getPresentablePath
|
||||
import com.intellij.openapi.vcs.LocalFilePath
|
||||
import com.intellij.vcs.git.repo.GitRepositoriesHolder
|
||||
import git4idea.GitLocalBranch
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialog
|
||||
import git4idea.commands.Git
|
||||
import git4idea.commands.GitCommand
|
||||
import git4idea.commands.GitLineHandler
|
||||
import git4idea.test.GitSingleRepoTest
|
||||
import git4idea.test.branch
|
||||
import git4idea.test.registerRepo
|
||||
import git4idea.workingTrees.GitWorkingTreeTestBase.Companion.createBranch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
class GitCreateWorkingTreeTest : GitSingleRepoTest() {
|
||||
|
||||
fun `test create worktree validation in non-empty directory`() {
|
||||
val workingTreeDir = "worktreeDir"
|
||||
val parent = File(repo.root.path, workingTreeDir)
|
||||
assertTrue(parent.mkdir())
|
||||
val childFile = parent.resolve("a.txt")
|
||||
assertTrue(childFile.createNewFile())
|
||||
|
||||
val validationMessage = GitWorkingTreeDialog.getPathValidationMessage(repo.root.path, workingTreeDir)
|
||||
assertEquals("'${getPresentablePath(parent.path)}' is not empty", validationMessage)
|
||||
}
|
||||
|
||||
fun `test create worktree validation in invalid directory`() {
|
||||
val workingTreeDir = "worktreeDir"
|
||||
val parent = File(repo.root.path, workingTreeDir)
|
||||
assertTrue(parent.createNewFile())
|
||||
|
||||
val validationMessage = GitWorkingTreeDialog.getPathValidationMessage(repo.root.path, workingTreeDir)
|
||||
assertEquals("'${getPresentablePath(parent.path)}' is a file", validationMessage)
|
||||
}
|
||||
|
||||
fun `test listing detached working tree`() {
|
||||
val treeRoot = "rootOfTree"
|
||||
val branch = "branch"
|
||||
@@ -27,8 +51,19 @@ class GitCreateWorkingTreeTest : GitSingleRepoTest() {
|
||||
}
|
||||
|
||||
val workingTreePath = LocalFilePath(testNioRoot.resolve(treeRoot).toString(), true)
|
||||
val output = git("worktree add ../$treeRoot $branchParameterForWorkingTree")
|
||||
assertTrue(output.contains("Preparing worktree (detached HEAD"))
|
||||
val handler = GitLineHandler(repo.getProject(), repo.getRoot(), GitCommand.WORKTREE)
|
||||
handler.addParameters("add", workingTreePath.path, branchParameterForWorkingTree)
|
||||
handler.setSilent(false)
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
handler.apply {
|
||||
setStdoutSuppressed(false)
|
||||
setStderrSuppressed(false)
|
||||
}
|
||||
val result = Git.getInstance().runCommand(handler)
|
||||
|
||||
val errorOutput = result.errorOutputAsJoinedString
|
||||
assertTrue(errorOutput, result.success())
|
||||
assertTrue(errorOutput, errorOutput.contains("Preparing worktree (detached HEAD"))
|
||||
|
||||
val workingTreeRepo = registerRepo(project, Paths.get(workingTreePath.path))
|
||||
assertNull("Current branch is should be null, got ${workingTreeRepo.currentBranchName} instead",
|
||||
@@ -47,14 +82,4 @@ class GitCreateWorkingTreeTest : GitSingleRepoTest() {
|
||||
|
||||
assertSameElements(workingTrees, expected)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createBranch(repo: GitRepository, branchName: String): GitLocalBranch {
|
||||
repo.branch(branchName)
|
||||
repo.update()
|
||||
val newBranch = repo.branches.findLocalBranch(branchName)
|
||||
assertNotNull(newBranch)
|
||||
return newBranch!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.ide.impl.OpenProjectTask
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ex.ProjectManagerEx
|
||||
import com.intellij.openapi.vcs.Executor.touch
|
||||
import com.intellij.openapi.vcs.LocalFilePath
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialogData
|
||||
import git4idea.repo.GitRefUtil
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.test.cloneRepo
|
||||
import git4idea.test.git
|
||||
import git4idea.test.initRepo
|
||||
import git4idea.test.registerRepo
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
internal class GitWorkingTreeFromRemoteBranchesTest : GitWorkingTreeTestBase() {
|
||||
|
||||
val remoteBranchName = "remoteBranch"
|
||||
val remoteRepoRelativePath = "remoteRepo"
|
||||
lateinit var lastCommitInRemoteBranch: String
|
||||
|
||||
override val mainRepoPath: Path
|
||||
get() = projectNioRoot
|
||||
|
||||
override fun doCreateAndOpenProject(): Project {
|
||||
val remoteRepoPath = testNioRoot.resolve(remoteRepoRelativePath)
|
||||
initRepo(null, remoteRepoPath, true)
|
||||
val file = "a.txt"
|
||||
touch(file, "content" + Math.random())
|
||||
git(null, "add $file")
|
||||
git(null, "commit -m initial")
|
||||
lastCommitInRemoteBranch = git(null, "log -1 --pretty=%H")
|
||||
git(null, "branch $remoteBranchName")
|
||||
|
||||
val projectRootPath = getProjectDirOrFile(true)
|
||||
cloneRepo(remoteRepoPath.toString(), projectRootPath.toString(), false)
|
||||
|
||||
Files.createDirectories(projectRootPath.resolve(Project.DIRECTORY_STORE_FOLDER))
|
||||
return runBlocking {
|
||||
ProjectManagerEx.getInstanceEx().openProjectAsync(projectIdentityFile = projectRootPath, options = OpenProjectTask {})!!
|
||||
}
|
||||
}
|
||||
|
||||
override fun createRepository(): GitRepository {
|
||||
return registerRepo(project, projectNioRoot)
|
||||
}
|
||||
|
||||
override fun getExpectedDefaultWorkingTrees(): List<GitWorkingTree> {
|
||||
return listOf(
|
||||
GitWorkingTree(repo.toString(), "refs/heads/master", true, true)
|
||||
)
|
||||
}
|
||||
|
||||
fun `test creating a worktree from remote branch`() {
|
||||
doTestWorkingTreeFromRemoteBranchCreation(false)
|
||||
}
|
||||
|
||||
fun `test creating a worktree from remote branch with custom name`() {
|
||||
doTestWorkingTreeFromRemoteBranchCreation(true)
|
||||
}
|
||||
|
||||
private fun doTestWorkingTreeFromRemoteBranchCreation(withNewBranch: Boolean) {
|
||||
val remoteBranch = repo.branches.findRemoteBranch("origin/$remoteBranchName")!!
|
||||
val workingTreeDataPath = LocalFilePath(testNioRoot.resolve("treeRoot"), true)
|
||||
val data = if (withNewBranch) {
|
||||
GitWorkingTreeDialogData.createForNewBranch(workingTreeDataPath, remoteBranch, remoteBranchName)
|
||||
}
|
||||
else {
|
||||
GitWorkingTreeDialogData.createForExistingBranch(workingTreeDataPath, remoteBranch)
|
||||
}
|
||||
|
||||
doTestWorkingTreeCreation(data,
|
||||
GitWorkingTree(data.workingTreePath.path,
|
||||
GitRefUtil.addRefsHeadsPrefixIfNeeded(remoteBranchName)!!,
|
||||
false, false),
|
||||
remoteBranchName,
|
||||
lastCommitInRemoteBranch)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,33 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.ide.impl.OpenProjectTask
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.project.ex.ProjectManagerEx
|
||||
import com.intellij.openapi.vcs.Executor.cd
|
||||
import com.intellij.openapi.vcs.LocalFilePath
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import git4idea.GitBranch
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.repo.GitLinkedWorktreeTest
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialogData
|
||||
import git4idea.commands.Git
|
||||
import git4idea.repo.GitRefUtil
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.test.GitSingleRepoTest
|
||||
import git4idea.test.git
|
||||
import git4idea.test.initRepo
|
||||
import git4idea.test.registerRepo
|
||||
import git4idea.test.tac
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.pathString
|
||||
|
||||
abstract class GitWorkingTreeTest : GitSingleRepoTest() {
|
||||
internal abstract class GitWorkingTreeTest : GitWorkingTreeTestBase() {
|
||||
|
||||
class GitWorkingTreeOnMainRepoTest : GitWorkingTreeTest() {
|
||||
override val mainRepoPath: Path
|
||||
get() = repo.root.toNioPath()
|
||||
|
||||
override fun getExpectedDefaultWorkingTrees(): List<GitWorkingTree> {
|
||||
return listOf(GitWorkingTree(repo.root.path, repo.currentBranch!!.fullName, true, true))
|
||||
}
|
||||
@@ -24,12 +38,19 @@ abstract class GitWorkingTreeTest : GitSingleRepoTest() {
|
||||
val branchName = "feature"
|
||||
val mainRepoRelativePath = "mainRepo"
|
||||
|
||||
val mainRepoPath: Path
|
||||
override val mainRepoPath: Path
|
||||
get() = testNioRoot.resolve(mainRepoRelativePath)
|
||||
|
||||
override fun doCreateAndOpenProject(): Project {
|
||||
val projectRootPath = getProjectDirOrFile(true)
|
||||
return GitLinkedWorktreeTest.setUpProjectAndWorkingTree(testNioRoot, projectRootPath, mainRepoRelativePath, branchName)
|
||||
initRepo(null, mainRepoPath, true)
|
||||
|
||||
cd(mainRepoPath)
|
||||
git(null, "worktree add -B $branchName ../${projectRootPath.fileName}")
|
||||
Files.createDirectories(projectRootPath.resolve(Project.DIRECTORY_STORE_FOLDER))
|
||||
return runBlocking {
|
||||
ProjectManagerEx.getInstanceEx().openProjectAsync(projectIdentityFile = projectRootPath, options = OpenProjectTask {})!!
|
||||
}
|
||||
}
|
||||
|
||||
override fun createRepository(): GitRepository {
|
||||
@@ -44,9 +65,6 @@ abstract class GitWorkingTreeTest : GitSingleRepoTest() {
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getExpectedDefaultWorkingTrees(): List<GitWorkingTree>
|
||||
|
||||
|
||||
fun `test listing working trees`() {
|
||||
val trees = listTrees()
|
||||
val expected = getExpectedDefaultWorkingTrees()
|
||||
@@ -68,7 +86,7 @@ abstract class GitWorkingTreeTest : GitSingleRepoTest() {
|
||||
val workingTree = createdWorkingTrees.firstOrNull { it.path.path.endsWith(treeRoot) }
|
||||
assertNotNull(workingTree)
|
||||
|
||||
GitWorkingTreesCommandService.getInstance().deleteWorkingTree(project, workingTree!!)
|
||||
Git.getInstance().deleteWorkingTree(project, workingTree!!)
|
||||
val removedWorkingTree = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(newWorkingTreeRootPath)
|
||||
assertNull(removedWorkingTree)
|
||||
|
||||
@@ -78,9 +96,39 @@ abstract class GitWorkingTreeTest : GitSingleRepoTest() {
|
||||
|
||||
fun listTrees(): List<GitWorkingTree> {
|
||||
val listener = GitListWorktreeLineListener(repo)
|
||||
val commandResult = GitWorkingTreesCommandService.getInstance().listWorktrees(repo, listener)
|
||||
val commandResult = Git.getInstance().listWorktrees(repo, listener)
|
||||
commandResult.throwOnError()
|
||||
return listener.trees
|
||||
}
|
||||
|
||||
fun `test creating a worktree with new branch`() {
|
||||
doTestWorkingTreeCreation(workingTreeWithNewBranch = true)
|
||||
}
|
||||
|
||||
fun `test creating a worktree with existing branch`() {
|
||||
doTestWorkingTreeCreation(workingTreeWithNewBranch = false)
|
||||
}
|
||||
|
||||
protected fun doTestWorkingTreeCreation(
|
||||
workingTreeWithNewBranch: Boolean,
|
||||
treeRoot: String = "treeRoot",
|
||||
branchName: String = "tree",
|
||||
expectedWorkingTree: GitWorkingTree = GitWorkingTree("${testNioRoot.pathString}/$treeRoot",
|
||||
GitRefUtil.addRefsHeadsPrefixIfNeeded("tree")!!,
|
||||
false, false),
|
||||
) {
|
||||
val commit = tac("a.txt")
|
||||
|
||||
val workingTreeDataPath = LocalFilePath(testNioRoot.resolve(treeRoot), true)
|
||||
val data = if (workingTreeWithNewBranch) {
|
||||
val localBranch = createBranch(repo, "initial-$branchName")
|
||||
GitWorkingTreeDialogData.createForNewBranch(workingTreeDataPath, localBranch, branchName)
|
||||
}
|
||||
else {
|
||||
val localBranch = createBranch(repo, branchName)
|
||||
GitWorkingTreeDialogData.createForExistingBranch(workingTreeDataPath, localBranch)
|
||||
}
|
||||
|
||||
doTestWorkingTreeCreation(data, expectedWorkingTree, branchName, commit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package git4idea.workingTrees
|
||||
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.vcs.git.repo.GitRepositoriesHolder
|
||||
import git4idea.GitLocalBranch
|
||||
import git4idea.GitWorkingTree
|
||||
import git4idea.actions.workingTree.GitWorkingTreeDialogData
|
||||
import git4idea.repo.GitRepositoriesFrontendHolderTest
|
||||
import git4idea.repo.GitRepository
|
||||
import git4idea.test.GitSingleRepoTest
|
||||
import git4idea.test.branch
|
||||
import git4idea.test.registerRepo
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
internal abstract class GitWorkingTreeTestBase : GitSingleRepoTest() {
|
||||
|
||||
abstract fun getExpectedDefaultWorkingTrees(): List<GitWorkingTree>
|
||||
abstract val mainRepoPath: Path
|
||||
|
||||
protected val eventHandler = GitRepositoriesFrontendHolderTest.GitRepositoriesHolderEventHandler()
|
||||
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
eventHandler.subscribe(project)
|
||||
}
|
||||
|
||||
protected fun doTestWorkingTreeCreation(
|
||||
data: GitWorkingTreeDialogData,
|
||||
expectedWorkingTree: GitWorkingTree,
|
||||
expectedWorkingTreeBranchName: String,
|
||||
expectedWorkingTreeLastCommit: String,
|
||||
) {
|
||||
val holder = GitRepositoriesHolder.getInstance(project)
|
||||
runBlocking {
|
||||
holder.init()
|
||||
}
|
||||
|
||||
assertSameElements(repo.workingTreeHolder.getWorkingTrees(), getExpectedDefaultWorkingTrees())
|
||||
|
||||
eventHandler.executeAndExpectEvent(
|
||||
{
|
||||
val result = GitWorkingTreesService.getInstance(project).createWorkingTree(repo, data)
|
||||
assertTrue(result.errorOutputAsHtmlString, result.success)
|
||||
val worktreesDir = LocalFileSystem.getInstance().refreshAndFindFileByNioFile(mainRepoPath.resolve(".git/worktrees"))
|
||||
refresh(worktreesDir!!)
|
||||
},
|
||||
{ event, _ ->
|
||||
return@executeAndExpectEvent event == GitRepositoriesHolder.UpdateType.WORKING_TREES_LOADED
|
||||
}
|
||||
)
|
||||
|
||||
val workingTrees = repo.workingTreeHolder.getWorkingTrees()
|
||||
val expected = getExpectedDefaultWorkingTrees().toMutableList()
|
||||
expected.add(expectedWorkingTree)
|
||||
|
||||
assertSameElements(workingTrees, expected)
|
||||
|
||||
val workingTreeRepo = registerRepo(project, Paths.get(data.workingTreePath.path))
|
||||
assertEquals("Current branch of the created working tree is incorrect",
|
||||
expectedWorkingTreeBranchName,
|
||||
workingTreeRepo.currentBranchName)
|
||||
assertEquals("Last commit of the created working tree is incorrect", expectedWorkingTreeLastCommit, workingTreeRepo.currentRevision)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createBranch(repo: GitRepository, branchName: String): GitLocalBranch {
|
||||
repo.branch(branchName)
|
||||
repo.update()
|
||||
val newBranch = repo.branches.findLocalBranch(branchName)
|
||||
assertNotNull(newBranch)
|
||||
return newBranch!!
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user