Skip to content

Commit dbc2651

Browse files
author
Hamed Soleimani
committed
fix: Preserve file permissions in the zip archive, fix bugs in gitignore mattern matching and keep mvm and gradle files
1 parent 5abfb19 commit dbc2651

File tree

3 files changed

+66
-13
lines changed

3 files changed

+66
-13
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "bugfix",
3+
"description" : "Preserve file permissions in the zip archive, fix bugs in gitignore matching and keep mvm and gradle files"
4+
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,25 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() {
4040
fun testWithValidFile() {
4141
val ktFile = mock<VirtualFile>()
4242
whenever(ktFile.extension).thenReturn("kt")
43+
whenever(ktFile.path).thenReturn("code.kt")
4344
assertTrue(featureDevSessionContext.isFileExtensionAllowed(ktFile))
4445
}
4546

4647
@Test
4748
fun testWithInvalidFile() {
4849
val txtFile = mock<VirtualFile>()
4950
whenever(txtFile.extension).thenReturn("txt")
51+
whenever(txtFile.path).thenReturn("file.txt")
5052
assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile))
5153
}
54+
55+
@Test
56+
fun testAllowedFilePath() {
57+
val allowedPaths = listOf("Dockerfile", "Dockerfile.build", "gradlew", "build.gradle", "gradle.properties", ".mvn/wrapper/maven-wrapper.properties")
58+
allowedPaths.forEach({
59+
val txtFile = mock<VirtualFile>()
60+
whenever(txtFile.path).thenReturn(it)
61+
assertTrue(featureDevSessionContext.isFileExtensionAllowed(txtFile))
62+
})
63+
}
5264
}

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,34 @@ import com.intellij.openapi.vfs.VirtualFile
1010
import com.intellij.openapi.vfs.VirtualFileVisitor
1111
import com.intellij.openapi.vfs.isFile
1212
import com.intellij.platform.ide.progress.withBackgroundProgress
13+
import kotlinx.coroutines.Dispatchers
1314
import kotlinx.coroutines.async
1415
import kotlinx.coroutines.flow.channelFlow
1516
import kotlinx.coroutines.launch
1617
import kotlinx.coroutines.runBlocking
1718
import kotlinx.coroutines.withContext
1819
import org.apache.commons.codec.digest.DigestUtils
19-
import software.aws.toolkits.core.utils.outputStream
20-
import software.aws.toolkits.core.utils.putNextEntry
20+
import org.apache.commons.io.FileUtils
2121
import software.aws.toolkits.jetbrains.core.coroutines.EDT
2222
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
2323
import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS
2424
import software.aws.toolkits.resources.AwsCoreBundle
2525
import software.aws.toolkits.telemetry.AmazonqTelemetry
2626
import java.io.File
2727
import java.io.FileInputStream
28+
import java.net.URI
29+
import java.nio.file.FileSystem
30+
import java.nio.file.FileSystems
2831
import java.nio.file.Files
2932
import java.nio.file.Path
33+
import java.nio.file.Paths
34+
import java.nio.file.StandardCopyOption
3035
import java.util.Base64
31-
import java.util.zip.ZipOutputStream
36+
import java.util.UUID
3237
import kotlin.coroutines.coroutineContext
3338
import kotlin.io.path.Path
39+
import kotlin.io.path.createParentDirectories
40+
import kotlin.io.path.getPosixFilePermissions
3441
import kotlin.io.path.relativeTo
3542

3643
interface RepoSizeError {
@@ -74,6 +81,12 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
7481
"Dockerfile.build"
7582
)
7683

84+
// patterns to explicitly allow unless matched with gitignore rules
85+
private val allowedPatterns = setOf(
86+
".*mvn.*",
87+
".*gradle.*",
88+
).map { Regex(it) }
89+
7790
// projectRoot: is the directory where the project is located when selected to open a project.
7891
val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
7992

@@ -108,7 +121,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
108121
fun isFileExtensionAllowed(file: VirtualFile): Boolean {
109122
// if it is a directory, it is allowed
110123
if (file.isDirectory) return true
111-
124+
val explicitAllowed = allowedPatterns.map { pattern -> pattern.matches(file.path) }.any { it }
125+
if (explicitAllowed) {
126+
return true
127+
}
112128
val extension = file.extension ?: return false
113129
return ALLOWED_CODE_EXTENSIONS.contains(extension)
114130
}
@@ -122,7 +138,12 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
122138
// this method reads like something a JS dev would write and doesn't do what the author thinks
123139
val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
124140
withContext(coroutineContext) {
125-
async { pattern.containsMatchIn(path) }
141+
// avoid partial match (pattern.containsMatchIn) since it causes us matching files
142+
// against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
143+
// we convert the glob rules to regex, add a trailing /* to all rules and then match
144+
// entries against them by adding a trailing /.
145+
// TODO: Add unit tests for gitignore matching
146+
async { pattern.matches("$path/") }
126147
}
127148
}
128149

@@ -187,18 +208,34 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
187208
}
188209
}
189210

190-
createTemporaryZipFileAsync { zipOutput ->
211+
val zipFilePath = createTemporaryZipFileAsync { zipfs ->
191212
filesToIncludeFlow.collect { file ->
192-
val relativePath = Path(file.path).relativeTo(projectRoot.toNioPath())
193-
zipOutput.putNextEntry(relativePath.toString(), Path(file.path))
213+
if (!file.isDirectory) {
214+
val externalFilePath = Path(file.path)
215+
val externalFilePermissions = externalFilePath.getPosixFilePermissions()
216+
val relativePath = Path(file.path).relativeTo(projectRoot.toNioPath())
217+
val zipfsPath = zipfs.getPath("/${relativePath}")
218+
withContext(Dispatchers.IO) {
219+
zipfsPath.createParentDirectories()
220+
Files.copy(externalFilePath, zipfsPath, StandardCopyOption.REPLACE_EXISTING)
221+
Files.setAttribute(zipfsPath, "zip:permissions", externalFilePermissions);
222+
}
223+
}
194224
}
195225
}
226+
zipFilePath
196227
}.toFile()
197228

198-
private suspend fun createTemporaryZipFileAsync(block: suspend (ZipOutputStream) -> Unit): Path = withContext(EDT) {
199-
val file = Files.createTempFile(null, ".zip")
200-
ZipOutputStream(file.outputStream()).use { zipOutput -> block(zipOutput) }
201-
file
229+
private suspend fun createTemporaryZipFileAsync(block: suspend (FileSystem) -> Unit): Path = withContext(EDT) {
230+
// Don't use Files.createTempFile since the file must not be created for ZipFS to work
231+
val tempFilePath: Path = Paths.get(FileUtils.getTempDirectory().getAbsolutePath(), "${UUID.randomUUID()}.zip")
232+
val uri = URI.create("jar:file:${tempFilePath}")
233+
val env = hashMapOf("create" to "true")
234+
val zipfs = FileSystems.newFileSystem(uri, env)
235+
zipfs.use {
236+
block(zipfs)
237+
}
238+
tempFilePath
202239
}
203240

204241
private fun parseGitIgnore(): Set<String> {
@@ -216,7 +253,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
216253
private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern
217254
.replace(".", "\\.")
218255
.replace("*", ".*")
219-
.let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash
256+
.let { if (it.endsWith("/")) "$it.*" else "$it/.*" } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
220257

221258
var selectedSourceFolder: VirtualFile
222259
set(newRoot) {

0 commit comments

Comments
 (0)