Skip to content

Commit 11d633b

Browse files
osdemahHamed Soleimanimanodnyab
authored andcommitted
fix(amazonq): file premissions are now preserved and mvn and gradle files are kept in the repo archives (aws#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 06f984c commit 11d633b

File tree

5 files changed

+154
-51
lines changed

5 files changed

+154
-51
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: 60 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,6 +49,7 @@ 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

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

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: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,27 @@ 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.jetbrains.utils.isDevFile
2523
import software.aws.toolkits.resources.AwsCoreBundle
2624
import software.aws.toolkits.telemetry.AmazonqTelemetry
2725
import java.io.File
2826
import java.io.FileInputStream
27+
import java.net.URI
28+
import java.nio.file.FileSystem
29+
import java.nio.file.FileSystems
2930
import java.nio.file.Files
3031
import java.nio.file.Path
32+
import java.nio.file.Paths
33+
import java.nio.file.StandardCopyOption
3134
import java.util.Base64
32-
import java.util.zip.ZipOutputStream
35+
import java.util.UUID
3336
import kotlin.coroutines.coroutineContext
3437
import kotlin.io.path.Path
38+
import kotlin.io.path.createParentDirectories
39+
import kotlin.io.path.getPosixFilePermissions
3540
import kotlin.io.path.relativeTo
3641

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

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

7178
// well known source files that do not have extensions
7279
private val wellKnownSourceFiles = setOf(
7380
"Dockerfile",
74-
"Dockerfile.build"
81+
"Dockerfile.build",
82+
"gradlew",
83+
"mvnw"
7584
)
7685

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

8090
// selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
8191
private var _selectedSourceFolder = projectRoot
@@ -85,10 +95,10 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
8595
init {
8696
ignorePatternsWithGitIgnore = try {
8797
buildList {
88-
addAll(ignorePatterns)
89-
parseGitIgnore().mapNotNull { pattern ->
90-
runCatching { Regex(pattern) }.getOrNull()
91-
}.let { addAll(it) }
98+
addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) })
99+
addAll(parseGitIgnore())
100+
}.mapNotNull { pattern ->
101+
runCatching { Regex(pattern) }.getOrNull()
92102
}
93103
} catch (e: Exception) {
94104
emptyList()
@@ -116,21 +126,26 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
116126
fun isFileExtensionAllowed(file: VirtualFile): Boolean {
117127
// if it is a directory, it is allowed
118128
if (file.isDirectory) return true
119-
120129
val extension = file.extension ?: return false
121130
return ALLOWED_CODE_EXTENSIONS.contains(extension)
122131
}
123132

124133
private fun ignoreFileByExtension(file: VirtualFile) =
125134
!isFileExtensionAllowed(file)
126135

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

129138
suspend fun ignoreFile(path: String): Boolean {
130139
// this method reads like something a JS dev would write and doesn't do what the author thinks
131140
val deferredResults = ignorePatternsWithGitIgnore.map { pattern ->
132141
withContext(coroutineContext) {
133-
async { pattern.containsMatchIn(path) }
142+
// avoid partial match (pattern.containsMatchIn) since it causes us matching files
143+
// against folder patterns. (e.g. settings.gradle ignored by .gradle rule!)
144+
// we convert the glob rules to regex, add a trailing /* to all rules and then match
145+
// entries against them by adding a trailing /.
146+
// TODO: Add unit tests for gitignore matching
147+
val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path
148+
async { pattern.matches("$relative/") }
134149
}
135150
}
136151

@@ -201,22 +216,43 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
201216
}
202217
}
203218

204-
createTemporaryZipFileAsync { zipOutput ->
219+
val zipFilePath = createTemporaryZipFileAsync { zipfs ->
220+
val posixFileAttributeSubstr = "posix"
221+
val isPosix = FileSystems.getDefault().supportedFileAttributeViews().contains(posixFileAttributeSubstr)
205222
filesToIncludeFlow.collect { file ->
206-
try {
207-
val relativePath = Path(file.path).relativeTo(projectRoot.toNioPath())
208-
zipOutput.putNextEntry(relativePath.toString(), Path(file.path))
209-
} catch (e: NoSuchFileException) {
210-
// Noop: Skip if file was deleted
223+
224+
if (!file.isDirectory) {
225+
val externalFilePath = Path(file.path)
226+
val relativePath = Path(file.path).relativeTo(projectRootPath)
227+
val zipfsPath = zipfs.getPath("/$relativePath")
228+
withContext(getCoroutineBgContext()) {
229+
zipfsPath.createParentDirectories()
230+
try {
231+
Files.copy(externalFilePath, zipfsPath, StandardCopyOption.REPLACE_EXISTING)
232+
if (isPosix) {
233+
val zipPermissionAttributeName = "zip:permissions"
234+
Files.setAttribute(zipfsPath, zipPermissionAttributeName, externalFilePath.getPosixFilePermissions())
235+
}
236+
} catch (e: NoSuchFileException) {
237+
// Noop: Skip if file was deleted
238+
}
239+
}
211240
}
212241
}
213242
}
243+
zipFilePath
214244
}.toFile()
215245

216-
private suspend fun createTemporaryZipFileAsync(block: suspend (ZipOutputStream) -> Unit): Path = withContext(EDT) {
217-
val file = Files.createTempFile(null, ".zip")
218-
ZipOutputStream(file.outputStream()).use { zipOutput -> block(zipOutput) }
219-
file
246+
private suspend fun createTemporaryZipFileAsync(block: suspend (FileSystem) -> Unit): Path = withContext(getCoroutineBgContext()) {
247+
// Don't use Files.createTempFile since the file must not be created for ZipFS to work
248+
val tempFilePath: Path = Paths.get(FileUtils.getTempDirectory().absolutePath, "${UUID.randomUUID()}.zip")
249+
val uri = URI.create("jar:${tempFilePath.toUri()}")
250+
val env = hashMapOf("create" to "true")
251+
val zipfs = FileSystems.newFileSystem(uri, env)
252+
zipfs.use {
253+
block(zipfs)
254+
}
255+
tempFilePath
220256
}
221257

222258
private fun parseGitIgnore(): Set<String> {
@@ -234,7 +270,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
234270
private fun convertGitIgnorePatternToRegex(pattern: String): String = pattern
235271
.replace(".", "\\.")
236272
.replace("*", ".*")
237-
.let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash
273+
.let { if (it.endsWith("/")) "$it.*" else "$it/.*" } // Add a trailing /* to all patterns. (we add a trailing / to all files when matching)
238274

239275
var selectedSourceFolder: VirtualFile
240276
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",
@@ -28,6 +29,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
2829
"cljs",
2930
"cls",
3031
"cmake",
32+
"cmd",
3133
"cob",
3234
"cobra",
3335
"coffee",
@@ -128,11 +130,13 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
128130
"pike",
129131
"pir",
130132
"pl",
133+
"pli",
131134
"pm",
132135
"pmod",
133136
"pp",
134137
"pro",
135138
"prolog",
139+
"properties",
136140
"ps1",
137141
"psd1",
138142
"psm1",
@@ -209,7 +213,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf(
209213
"xml",
210214
"yaml",
211215
"yml",
212-
"zig"
216+
"zig",
213217
)
214218

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

0 commit comments

Comments
 (0)