Skip to content

Commit 88c7687

Browse files
osdemahHamed Soleimanimanodnyab
authored
fix(amazonq): file premissions are now preserved and mvn and gradle files are kept in the repo archives (#5235)
* fix(amazonq): archives now preserve file premissions and amazon q can edit nvm/gradle files * fix test failures on windows * change FS operations to use BG context --------- Co-authored-by: Hamed Soleimani <[email protected]> Co-authored-by: manodnyab <[email protected]>
1 parent 0426d76 commit 88c7687

File tree

5 files changed

+156
-53
lines changed

5 files changed

+156
-53
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" : "Amazon Q can update mvn and gradle build files"
4+
}

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

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,17 @@ import org.mockito.kotlin.whenever
1313
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
1414
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevTestBase
1515
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
16+
import software.aws.toolkits.jetbrains.utils.rules.HeavyJavaCodeInsightTestFixtureRule
17+
import software.aws.toolkits.jetbrains.utils.rules.addFileToModule
18+
import java.util.zip.ZipFile
19+
20+
class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTestFixtureRule()) {
21+
22+
private fun addFilesToProjectModule(vararg path: String) {
23+
val module = projectRule.module
24+
path.forEach { projectRule.fixture.addFileToModule(module, it, it) }
25+
}
1626

17-
class FeatureDevSessionContextTest : FeatureDevTestBase() {
1827
@Rule
1928
@JvmField
2029
val ruleChain = RuleChain(projectRule, disposableRule)
@@ -40,13 +49,64 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() {
4049
fun testWithValidFile() {
4150
val ktFile = mock<VirtualFile>()
4251
whenever(ktFile.extension).thenReturn("kt")
52+
whenever(ktFile.path).thenReturn("code.kt")
4353
assertTrue(featureDevSessionContext.isFileExtensionAllowed(ktFile))
4454
}
4555

4656
@Test
4757
fun testWithInvalidFile() {
4858
val txtFile = mock<VirtualFile>()
4959
whenever(txtFile.extension).thenReturn("txt")
60+
whenever(txtFile.path).thenReturn("file.txt")
5061
assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile))
5162
}
63+
64+
@Test
65+
fun testAllowedFilePath() {
66+
val allowedPaths = listOf("build.gradle", "gradle.properties", ".mvn/wrapper/maven-wrapper.properties")
67+
allowedPaths.forEach({
68+
val txtFile = mock<VirtualFile>()
69+
whenever(txtFile.path).thenReturn(it)
70+
whenever(txtFile.extension).thenReturn(it.split(".").last())
71+
assertTrue(featureDevSessionContext.isFileExtensionAllowed(txtFile))
72+
})
73+
}
74+
75+
@Test
76+
fun testZipProject() {
77+
addFilesToProjectModule(
78+
".gradle/cached.jar",
79+
"src/MyClass.java",
80+
"gradlew",
81+
"gradlew.bat",
82+
"README.md",
83+
"settings.gradle",
84+
"build.gradle",
85+
"gradle/wrapper/gradle-wrapper.properties",
86+
)
87+
88+
val zipResult = featureDevSessionContext.getProjectZip()
89+
val zipPath = zipResult.payload.path
90+
91+
val zippedFiles = mutableSetOf<String>()
92+
ZipFile(zipPath).use { zipFile ->
93+
for (entry in zipFile.entries()) {
94+
if (!entry.name.endsWith("/")) {
95+
zippedFiles.add(entry.name)
96+
}
97+
}
98+
}
99+
100+
val expectedFiles = setOf(
101+
"src/MyClass.java",
102+
"gradlew",
103+
"gradlew.bat",
104+
"README.md",
105+
"settings.gradle",
106+
"build.gradle",
107+
"gradle/wrapper/gradle-wrapper.properties",
108+
)
109+
110+
assertTrue(zippedFiles == expectedFiles)
111+
}
52112
}

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,6 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod
372372
// Adding gitignore file and gitignore file member for testing.
373373
// The tests include the markdown file but not these two files.
374374
projectRule.fixture.addFileToProject("/.gitignore", "node_modules\n.idea\n.vscode\n.DS_Store").virtualFile
375-
projectRule.fixture.addFileToProject("test.idea", "ref: refs/heads/main")
375+
projectRule.fixture.addFileToProject("/.idea/ref", "ref: refs/heads/main")
376376
}
377377
}

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

Lines changed: 85 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,26 @@ import kotlinx.coroutines.launch
1616
import kotlinx.coroutines.runBlocking
1717
import kotlinx.coroutines.withContext
1818
import org.apache.commons.codec.digest.DigestUtils
19-
import software.aws.toolkits.core.utils.outputStream
20-
import software.aws.toolkits.core.utils.putNextEntry
21-
import software.aws.toolkits.jetbrains.core.coroutines.EDT
19+
import org.apache.commons.io.FileUtils
2220
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
2321
import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS
2422
import software.aws.toolkits.resources.AwsCoreBundle
2523
import software.aws.toolkits.telemetry.AmazonqTelemetry
2624
import java.io.File
2725
import java.io.FileInputStream
26+
import java.net.URI
27+
import java.nio.file.FileSystem
28+
import java.nio.file.FileSystems
2829
import java.nio.file.Files
2930
import java.nio.file.Path
31+
import java.nio.file.Paths
32+
import java.nio.file.StandardCopyOption
3033
import java.util.Base64
31-
import java.util.zip.ZipOutputStream
34+
import java.util.UUID
3235
import kotlin.coroutines.coroutineContext
3336
import kotlin.io.path.Path
37+
import kotlin.io.path.createParentDirectories
38+
import kotlin.io.path.getPosixFilePermissions
3439
import kotlin.io.path.relativeTo
3540

3641
interface RepoSizeError {
@@ -41,41 +46,45 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep
4146
class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) {
4247
// TODO: Need to correct this class location in the modules going further to support both amazonq and codescan.
4348

44-
private val ignorePatterns = setOf(
45-
"\\.aws-sam",
46-
"\\.svn",
47-
"\\.hg/?",
48-
"\\.rvm",
49-
"\\.git/?",
50-
"\\.gitignore",
51-
"\\.project",
52-
"\\.gem",
53-
"/\\.idea/?",
54-
"\\.zip$",
55-
"\\.bin$",
56-
"\\.png$",
57-
"\\.jpg$",
58-
"\\.svg$",
59-
"\\.pyc$",
60-
"/license\\.txt$",
61-
"/License\\.txt$",
62-
"/LICENSE\\.txt$",
63-
"/license\\.md$",
64-
"/License\\.md$",
65-
"/LICENSE\\.md$",
66-
"node_modules/?",
67-
"build/?",
68-
"dist/?"
69-
).map { Regex(it) }
49+
private val additionalGitIgnoreRules = setOf(
50+
".aws-sam",
51+
".gem",
52+
".git",
53+
".gitignore",
54+
".gradle",
55+
".hg",
56+
".idea",
57+
".project",
58+
".rvm",
59+
".svn",
60+
"*.zip",
61+
"*.bin",
62+
"*.png",
63+
"*.jpg",
64+
"*.svg",
65+
"*.pyc",
66+
"license.txt",
67+
"License.txt",
68+
"LICENSE.txt",
69+
"license.md",
70+
"License.md",
71+
"LICENSE.md",
72+
"node_modules",
73+
"build",
74+
"dist"
75+
)
7076

7177
// well known source files that do not have extensions
7278
private val wellKnownSourceFiles = setOf(
7379
"Dockerfile",
74-
"Dockerfile.build"
80+
"Dockerfile.build",
81+
"gradlew",
82+
"mvnw"
7583
)
7684

7785
// projectRoot: is the directory where the project is located when selected to open a project.
7886
val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
87+
private val projectRootPath = Paths.get(projectRoot.path) ?: error("Can not find project root path")
7988

8089
// selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
8190
private var _selectedSourceFolder = projectRoot
@@ -85,10 +94,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
8594
init {
8695
ignorePatternsWithGitIgnore = try {
8796
buildList {
88-
addAll(ignorePatterns)
89-
parseGitIgnore().mapNotNull { pattern ->
90-
runCatching { Regex(pattern) }.getOrNull()
91-
}.let { addAll(it) }
97+
addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) })
98+
addAll(parseGitIgnore())
99+
}.mapNotNull { pattern ->
100+
runCatching { Regex(pattern) }.getOrNull()
92101
}
93102
} catch (e: Exception) {
94103
emptyList()
@@ -108,21 +117,26 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
108117
fun isFileExtensionAllowed(file: VirtualFile): Boolean {
109118
// if it is a directory, it is allowed
110119
if (file.isDirectory) return true
111-
112120
val extension = file.extension ?: return false
113121
return ALLOWED_CODE_EXTENSIONS.contains(extension)
114122
}
115123

116124
private fun ignoreFileByExtension(file: VirtualFile) =
117125
!isFileExtensionAllowed(file)
118126

119-
suspend fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(file.path)
127+
suspend fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(file.presentableUrl)
120128

121129
suspend fun ignoreFile(path: String): Boolean {
122130
// this method reads like something a JS dev would write and doesn't do what the author thinks
123131
val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
124132
withContext(coroutineContext) {
125-
async { pattern.containsMatchIn(path) }
133+
// avoid partial match (pattern.containsMatchIn) since it causes us matching files
134+
// against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
135+
// we convert the glob rules to regex, add a trailing /* to all rules and then match
136+
// entries against them by adding a trailing /.
137+
// TODO: Add unit tests for gitignore matching
138+
val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path
139+
async { pattern.matches("$relative/") }
126140
}
127141
}
128142

@@ -132,7 +146,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
132146
return deferredResults.any { it.await() }
133147
}
134148

135-
private fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name)
149+
fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name)
136150

137151
suspend fun zipFiles(projectRoot: VirtualFile): File = withContext(getCoroutineBgContext()) {
138152
val files = mutableListOf<VirtualFile>()
@@ -187,22 +201,43 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
187201
}
188202
}
189203

190-
createTemporaryZipFileAsync { zipOutput ->
204+
val zipFilePath = createTemporaryZipFileAsync { zipfs ->
205+
val posixFileAttributeSubstr = "posix"
206+
val isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains(posixFileAttributeSubstr)
191207
filesToIncludeFlow.collect { file ->
192-
try {
193-
val relativePath = Path(file.path).relativeTo(projectRoot.toNioPath())
194-
zipOutput.putNextEntry(relativePath.toString(), Path(file.path))
195-
} catch (e: NoSuchFileException) {
196-
// Noop: Skip if file was deleted
208+
209+
if (!file.isDirectory) {
210+
val externalFilePath = Path(file.path)
211+
val relativePath = Path(file.path).relativeTo(projectRootPath)
212+
val zipfsPath = zipfs.getPath("/$relativePath")
213+
withContext(getCoroutineBgContext()) {
214+
zipfsPath.createParentDirectories()
215+
try {
216+
Files.copy(externalFilePath, zipfsPath, StandardCopyOption.REPLACE_EXISTING)
217+
if (isPosix) {
218+
val zipPermissionAttributeName = "zip:permissions"
219+
Files.setAttribute(zipfsPath, zipPermissionAttributeName, externalFilePath.getPosixFilePermissions())
220+
}
221+
} catch (e: NoSuchFileException) {
222+
// Noop: Skip if file was deleted
223+
}
224+
}
197225
}
198226
}
199227
}
228+
zipFilePath
200229
}.toFile()
201230

202-
private suspend fun createTemporaryZipFileAsync(block: suspend (ZipOutputStream) -> Unit): Path = withContext(EDT) {
203-
val file = Files.createTempFile(null, ".zip")
204-
ZipOutputStream(file.outputStream()).use { zipOutput -> block(zipOutput) }
205-
file
231+
private suspend fun createTemporaryZipFileAsync(block: suspend (FileSystem) -> Unit): Path = withContext(getCoroutineBgContext()) {
232+
// Don't use Files.createTempFile since the file must not be created for ZipFS to work
233+
val tempFilePath: Path = Paths.get(FileUtils.getTempDirectory().absolutePath, "${UUID.randomUUID()}.zip")
234+
val uri = URI.create("jar:${tempFilePath.toUri()}")
235+
val env = hashMapOf("create" to "true")
236+
val zipfs = FileSystems.newFileSystem(uri, env)
237+
zipfs.use {
238+
block(zipfs)
239+
}
240+
tempFilePath
206241
}
207242

208243
private fun parseGitIgnore(): Set<String> {
@@ -220,7 +255,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
220255
private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern
221256
.replace(".", "\\.")
222257
.replace("*", ".*")
223-
.let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash
258+
.let { if (it.endsWith("/")) "$it.*" else "$it/.*" } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
224259

225260
var selectedSourceFolder: VirtualFile
226261
set(newRoot) {

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
1616
"bash",
1717
"bat",
1818
"boo",
19+
"bms",
1920
"c",
2021
"cbl",
2122
"cc",
@@ -27,6 +28,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
2728
"cljs",
2829
"cls",
2930
"cmake",
31+
"cmd",
3032
"cob",
3133
"cobra",
3234
"coffee",
@@ -122,11 +124,13 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
122124
"pike",
123125
"pir",
124126
"pl",
127+
"pli",
125128
"pm",
126129
"pmod",
127130
"pp",
128131
"pro",
129132
"prolog",
133+
"properties",
130134
"ps1",
131135
"psd1",
132136
"psm1",
@@ -200,7 +204,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
200204
"xml",
201205
"yaml",
202206
"yml",
203-
"zig"
207+
"zig",
204208
)
205209

206210
fun scrubNames(messageToBeScrubbed: String, username: String? = getSystemUserName()): String {

0 commit comments

Comments
 (0)