Skip to content

Commit 17f53d7

Browse files
committed
Run git commands in a background task
1 parent 7cc38c2 commit 17f53d7

File tree

4 files changed

+274
-185
lines changed

4 files changed

+274
-185
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.github.monosoul.git.updateindex.extended
2+
3+
import com.github.monosoul.git.updateindex.extended.logging.Slf4j
4+
import com.intellij.openapi.progress.ProgressIndicator
5+
import com.intellij.openapi.progress.Task
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
8+
import com.intellij.openapi.vfs.VirtualFile
9+
import com.intellij.vcsUtil.VcsUtil
10+
import git4idea.commands.Git
11+
import git4idea.commands.GitCommandResult
12+
import git4idea.commands.GitLineHandler
13+
14+
class ExtendedUpdateIndexTask(
15+
project: Project,
16+
val selectedFiles: Iterable<VirtualFile>,
17+
val command: ExtendedUpdateIndexCommand
18+
) : Task.Backgroundable(project, "Running git update-index") {
19+
20+
private val logger by Slf4j
21+
22+
override fun run(indicator: ProgressIndicator) {
23+
project.run {
24+
selectedFiles
25+
.mapNotNull { fileToVcsRoot(it) }
26+
.groupBy({ (_, vcsRoot) -> vcsRoot }, { (file, _) -> file })
27+
.onEach { (vcsRoot, files) ->
28+
gitLineHandlerFactory(command, vcsRoot, files).runAndLog()
29+
files.forEach(vcsDirtyScopeManager::fileDirty)
30+
}
31+
}
32+
}
33+
34+
private fun GitLineHandler.runAndLog() = run(Git.getInstance()::runCommand)
35+
.takeUnless(GitCommandResult::success)
36+
?.let(GitCommandResult::getErrorOutput)
37+
?.forEach(logger::error)
38+
39+
private val Project.gitLineHandlerFactory: GitLineHandlerFactory
40+
get() = getService(GitLineHandlerFactory::class.java)
41+
42+
private val Project.vcsDirtyScopeManager: VcsDirtyScopeManager
43+
get() = VcsDirtyScopeManager.getInstance(this)
44+
45+
private fun Project.fileToVcsRoot(file: VirtualFile) = VcsUtil.getVcsRootFor(this, file)?.let { file to it }
46+
}
Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
package com.github.monosoul.git.updateindex.extended.support
22

33
import com.github.monosoul.git.updateindex.extended.ExtendedUpdateIndexCommand
4-
import com.github.monosoul.git.updateindex.extended.GitLineHandlerFactory
4+
import com.github.monosoul.git.updateindex.extended.ExtendedUpdateIndexTask
55
import com.github.monosoul.git.updateindex.extended.logging.Slf4j
66
import com.intellij.openapi.components.Service
7+
import com.intellij.openapi.progress.ProgressManager
78
import com.intellij.openapi.project.Project
8-
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
99
import com.intellij.openapi.vfs.VirtualFile
10-
import com.intellij.vcsUtil.VcsUtil.getVcsRootFor
11-
import git4idea.commands.Git
12-
import git4idea.commands.GitCommandResult
13-
import git4idea.commands.GitLineHandler
1410

1511
@Service
1612
class CommandInvoker(private val project: Project) {
@@ -20,27 +16,8 @@ class CommandInvoker(private val project: Project) {
2016
operator fun invoke(selectedFiles: Iterable<VirtualFile>, command: ExtendedUpdateIndexCommand) {
2117
logger.debug("Calling {} against {}", command, selectedFiles)
2218

23-
project.run {
24-
selectedFiles
25-
.mapNotNull { fileToVcsRoot(it) }
26-
.groupBy({ (_, vcsRoot) -> vcsRoot }, { (file, _) -> file })
27-
.onEach { (vcsRoot, files) ->
28-
gitLineHandlerFactory(command, vcsRoot, files).runAndLog()
29-
}
30-
.values.flatten().forEach(vcsDirtyScopeManager::fileDirty)
31-
}
19+
ProgressManager.getInstance().run(
20+
ExtendedUpdateIndexTask(project, selectedFiles, command)
21+
)
3222
}
33-
34-
private fun GitLineHandler.runAndLog() = run(Git.getInstance()::runCommand)
35-
.takeUnless(GitCommandResult::success)
36-
?.let(GitCommandResult::getErrorOutput)
37-
?.forEach(logger::error)
38-
39-
private val Project.gitLineHandlerFactory: GitLineHandlerFactory
40-
get() = getService(GitLineHandlerFactory::class.java)
41-
42-
private val Project.vcsDirtyScopeManager: VcsDirtyScopeManager
43-
get() = VcsDirtyScopeManager.getInstance(this)
44-
45-
private fun Project.fileToVcsRoot(file: VirtualFile) = getVcsRootFor(this, file)?.let { file to it }
4623
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package com.github.monosoul.git.updateindex.extended
2+
3+
import com.github.monosoul.git.updateindex.extended.ExtendedUpdateIndexTaskTest.FilesAndCommandArgumentsSource.NoVcsRoot
4+
import com.github.monosoul.git.updateindex.extended.ExtendedUpdateIndexTaskTest.FilesAndCommandArgumentsSource.WithVcsRoot
5+
import com.intellij.mock.MockApplication
6+
import com.intellij.mock.MockProject
7+
import com.intellij.openapi.application.ApplicationManager.setApplication
8+
import com.intellij.openapi.progress.ProgressIndicator
9+
import com.intellij.openapi.util.Disposer.dispose
10+
import com.intellij.openapi.vcs.ProjectLevelVcsManager
11+
import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager
12+
import com.intellij.openapi.vfs.VirtualFile
13+
import git4idea.commands.Git
14+
import git4idea.commands.GitCommandResult
15+
import git4idea.commands.GitLineHandler
16+
import io.mockk.Called
17+
import io.mockk.every
18+
import io.mockk.impl.annotations.MockK
19+
import io.mockk.junit5.MockKExtension
20+
import io.mockk.mockk
21+
import io.mockk.verify
22+
import io.mockk.verifyAll
23+
import io.mockk.verifyOrder
24+
import org.apache.commons.lang3.RandomStringUtils
25+
import org.apache.commons.lang3.RandomUtils.nextInt
26+
import org.apache.log4j.Appender
27+
import org.apache.log4j.Level.ERROR
28+
import org.junit.jupiter.api.AfterEach
29+
import org.junit.jupiter.api.BeforeEach
30+
import org.junit.jupiter.api.extension.ExtendWith
31+
import org.junit.jupiter.params.ParameterizedTest
32+
import org.junit.jupiter.params.provider.ArgumentsSource
33+
import strikt.api.expectThat
34+
import strikt.assertions.containsExactlyInAnyOrder
35+
import strikt.assertions.isEqualTo
36+
import java.util.stream.Stream.generate
37+
import kotlin.streams.toList
38+
39+
@ExtendWith(MockKExtension::class)
40+
internal class ExtendedUpdateIndexTaskTest {
41+
42+
private lateinit var parent: TestDisposable
43+
private lateinit var application: MockApplication
44+
private lateinit var project: MockProject
45+
46+
private lateinit var appender: Appender
47+
48+
@MockK
49+
private lateinit var git: Git
50+
51+
@MockK
52+
private lateinit var vcsManager: ProjectLevelVcsManager
53+
54+
@MockK(relaxUnitFun = true)
55+
private lateinit var dirtyScopeManager: VcsDirtyScopeManager
56+
57+
@MockK
58+
private lateinit var gitLineHandlerFactory: GitLineHandlerFactory
59+
60+
@MockK
61+
private lateinit var gitLineHandler: GitLineHandler
62+
63+
@MockK
64+
private lateinit var gitCommandResult: GitCommandResult
65+
66+
@BeforeEach
67+
fun setUp() {
68+
parent = TestDisposable()
69+
70+
application = MockApplication(parent)
71+
setApplication(application, parent)
72+
application.registerService(git, parent)
73+
74+
project = MockProject(null, parent)
75+
project.registerService(vcsManager, parent)
76+
project.registerService(dirtyScopeManager, parent)
77+
project.registerService(gitLineHandlerFactory, parent)
78+
79+
appender = mockedAppender<ExtendedUpdateIndexTask>()
80+
81+
every { gitLineHandlerFactory.invoke(any(), any(), any()) } returns gitLineHandler
82+
every { git.runCommand(any<GitLineHandler>()) } returns gitCommandResult
83+
}
84+
85+
@AfterEach
86+
fun tearDown() {
87+
dispose(parent)
88+
}
89+
90+
@ParameterizedTest
91+
@ArgumentsSource(NoVcsRoot::class)
92+
fun `should do nothing if a file has no VCS root`(
93+
files: List<VirtualFile>,
94+
command: ExtendedUpdateIndexCommand,
95+
@MockK indicator: ProgressIndicator
96+
) {
97+
every { vcsManager.getVcsRootFor(any<VirtualFile>()) } returns null
98+
99+
ExtendedUpdateIndexTask(project, files, command).run(indicator)
100+
101+
verifyOrder {
102+
vcsManager.getVcsRootFor(files[0])
103+
}
104+
verifyAll {
105+
git wasNot Called
106+
gitLineHandlerFactory wasNot Called
107+
gitLineHandler wasNot Called
108+
dirtyScopeManager wasNot Called
109+
}
110+
}
111+
112+
@ParameterizedTest
113+
@ArgumentsSource(WithVcsRoot::class)
114+
internal fun `should execute a command and log nothing if it was successful`(
115+
files: List<VirtualFile>,
116+
command: ExtendedUpdateIndexCommand,
117+
@MockK indicator: ProgressIndicator
118+
) {
119+
val root = mockk<VirtualFile>()
120+
every { vcsManager.getVcsRootFor(any<VirtualFile>()) } returns root
121+
122+
every { gitCommandResult.success() } returns true
123+
124+
125+
ExtendedUpdateIndexTask(project, files, command).run(indicator)
126+
127+
verify(exactly = files.size) {
128+
vcsManager.getVcsRootFor(any<VirtualFile>())
129+
}
130+
verifyOrder {
131+
gitLineHandlerFactory.invoke(command, root, withArg {
132+
expectThat(it) containsExactlyInAnyOrder files
133+
})
134+
git.runCommand(gitLineHandler)
135+
gitCommandResult.success()
136+
}
137+
verify(exactly = files.size) {
138+
dirtyScopeManager.fileDirty(any<VirtualFile>())
139+
}
140+
}
141+
142+
@ParameterizedTest
143+
@ArgumentsSource(WithVcsRoot::class)
144+
internal fun `should execute a command and log errors if it was unsuccessful`(
145+
files: List<VirtualFile>,
146+
command: ExtendedUpdateIndexCommand,
147+
@MockK indicator: ProgressIndicator
148+
) {
149+
val root = mockk<VirtualFile>()
150+
every { vcsManager.getVcsRootFor(any<VirtualFile>()) } returns root
151+
152+
every { gitCommandResult.success() } returns false
153+
val error = RandomStringUtils.randomAlphabetic(LIMIT)
154+
every { gitCommandResult.errorOutput } returns listOf(error)
155+
156+
ExtendedUpdateIndexTask(project, files, command).run(indicator)
157+
158+
verify(exactly = files.size) {
159+
vcsManager.getVcsRootFor(any<VirtualFile>())
160+
}
161+
verifyOrder {
162+
gitLineHandlerFactory.invoke(command, root, withArg {
163+
expectThat(it) containsExactlyInAnyOrder files
164+
})
165+
git.runCommand(gitLineHandler)
166+
gitCommandResult.success()
167+
gitCommandResult.errorOutput
168+
appender.doAppend(withArg {
169+
expectThat(it) {
170+
get { message } isEqualTo error
171+
get { getLevel() } isEqualTo ERROR
172+
}
173+
})
174+
}
175+
verify(exactly = files.size) {
176+
dirtyScopeManager.fileDirty(any<VirtualFile>())
177+
}
178+
}
179+
180+
private sealed class FilesAndCommandArgumentsSource(
181+
vararg generators: () -> Any?
182+
) : AbstractMultiArgumentsSource(*generators) {
183+
184+
class NoVcsRoot : FilesAndCommandArgumentsSource(
185+
{ listOf(mockk<VirtualFile>()) },
186+
{ randomEnum<ExtendedUpdateIndexCommand>() }
187+
)
188+
189+
class WithVcsRoot : FilesAndCommandArgumentsSource(
190+
{ virtualFileList() },
191+
{ randomEnum<ExtendedUpdateIndexCommand>() }
192+
)
193+
194+
companion object {
195+
private fun virtualFileList() = generate { mockk<VirtualFile>() }
196+
.limit(nextInt(1, LIMIT))
197+
.toList()
198+
}
199+
}
200+
}

0 commit comments

Comments
 (0)