Skip to content

Commit 1a0b2ad

Browse files
authored
feat(props): preserve comments in updated gradle.properties files (#6)
1 parent c9346f8 commit 1a0b2ad

File tree

5 files changed

+246
-19
lines changed

5 files changed

+246
-19
lines changed

.editorconfig

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
insert_final_newline = true
7+
trim_trailing_whitespace = true
8+
indent_style = space
9+
indent_size = 4

build.gradle.kts

+13-1
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,21 @@ dependencies {
1515
implementation("io.github.microutils:kotlin-logging:1.7.8")
1616
implementation("org.eclipse.jgit:org.eclipse.jgit:5.7.0.202003110725-r")
1717
implementation("org.kohsuke:github-api:1.109")
18-
implementation("nu.studer:java-ordered-properties:1.0.2")
1918

2019
runtimeOnly("org.slf4j:slf4j-simple:1.7.30")
20+
21+
testImplementation(platform("org.junit:junit-bom:5.6.2"))
22+
testImplementation("org.junit.jupiter:junit-jupiter-api")
23+
testImplementation("org.junit.platform:junit-platform-runner")
24+
25+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
26+
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
27+
}
28+
29+
tasks.withType<Test> {
30+
useJUnitPlatform {
31+
includeEngines("junit-jupiter")
32+
}
2133
}
2234

2335
application {

src/main/kotlin/io/spinnaker/bumpdeps/Main.kt

+8-18
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,17 @@ import com.github.ajalt.clikt.parameters.options.default
77
import com.github.ajalt.clikt.parameters.options.option
88
import com.github.ajalt.clikt.parameters.options.required
99
import com.github.ajalt.clikt.parameters.options.split
10-
import java.io.FileInputStream
11-
import java.io.FileOutputStream
1210
import java.nio.file.Files
1311
import java.nio.file.Path
12+
import java.time.Duration
1413
import kotlin.system.exitProcess
1514
import mu.KotlinLogging
16-
import nu.studer.java.util.OrderedProperties.OrderedPropertiesBuilder
1715
import org.eclipse.jgit.api.Git
1816
import org.eclipse.jgit.transport.RefSpec
1917
import org.eclipse.jgit.transport.URIish
2018
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
2119
import org.kohsuke.github.GHIssueState
2220
import org.kohsuke.github.GitHubBuilder
23-
import java.time.Duration
24-
import java.util.concurrent.TimeUnit
2521

2622
class BumpDeps : CliktCommand() {
2723

@@ -120,23 +116,17 @@ class BumpDeps : CliktCommand() {
120116
}
121117

122118
private fun updatePropertiesFile(gradlePropsFile: Path, repoName: String) {
123-
val gradleProps = FileInputStream(gradlePropsFile.toFile()).use { fis ->
124-
OrderedPropertiesBuilder()
125-
.withOrdering(String.CASE_INSENSITIVE_ORDER)
126-
.withSuppressDateInComment(true)
127-
.build()
128-
.apply { load(fis) }
129-
}
130-
if (!gradleProps.containsProperty(key)) {
119+
val props = NonDestructivePropertiesEditor(gradlePropsFile)
120+
val result = props.updateProperty(key, version)
121+
122+
if (!result.matched) {
131123
throw IllegalArgumentException("Couldn't locate key $key in $repoName's gradle.properties file")
132124
}
133-
if (gradleProps.getProperty(key) == version) {
125+
if (!result.updated) {
134126
throw IllegalArgumentException("$repoName's $key is already set to $version")
135127
}
136-
gradleProps.setProperty(key, version)
137-
FileOutputStream(gradlePropsFile.toFile()).use { fos ->
138-
gradleProps.store(fos, /* comments= */ null)
139-
}
128+
129+
props.saveResult(result.lines)
140130
}
141131

142132
private fun createPullRequest(repoName: String, branchName: String) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package io.spinnaker.bumpdeps
2+
3+
import java.nio.charset.Charset
4+
import java.nio.file.Files
5+
import java.nio.file.Path
6+
import java.util.concurrent.atomic.AtomicBoolean
7+
import java.util.regex.Pattern
8+
9+
class NonDestructivePropertiesEditor(internal val propertiesFile: Path) {
10+
internal val propsCharset: Charset = Charsets.ISO_8859_1
11+
12+
fun updateProperty(key: String, value: String): Result =
13+
updateProperty(Files.readAllLines(propertiesFile, propsCharset), key, value)
14+
15+
internal fun updateProperty(lines: List<String>, key: String, value: String): Result {
16+
val trimmedKey = key.trim()
17+
val regex = Pattern.compile("^\\s*${Pattern.quote(trimmedKey)}\\s*=(.*)$")
18+
19+
val modified = AtomicBoolean(false)
20+
val exists = AtomicBoolean(false)
21+
return lines.map processLine@{ line ->
22+
val matcher = regex.matcher(line)
23+
if (matcher.matches()) {
24+
exists.set(true)
25+
val current = matcher.group(1)?.trim()
26+
modified.set(current != value)
27+
if (modified.get()) {
28+
return@processLine "$key=$value"
29+
}
30+
}
31+
line
32+
}.let {
33+
Result(it, exists.get(), modified.get())
34+
}
35+
}
36+
37+
fun saveResult(lines: List<String>) {
38+
Files.write(propertiesFile, lines, propsCharset)
39+
}
40+
}
41+
42+
data class Result(val lines: List<String>, val matched: Boolean, val updated: Boolean)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package io.spinnaker.bumpdeps
2+
3+
import java.io.FileInputStream
4+
import java.io.FileOutputStream
5+
import java.nio.file.Files
6+
import java.util.Properties
7+
import org.junit.jupiter.api.Assertions.assertEquals
8+
import org.junit.jupiter.api.Test
9+
10+
internal class NonDestructivePropertiesEditorTest {
11+
12+
private fun subject(): NonDestructivePropertiesEditor =
13+
Files.createTempFile("gradle", ".properties").let {
14+
it.toFile().deleteOnExit()
15+
NonDestructivePropertiesEditor(it)
16+
}
17+
18+
@Test
19+
fun testParsesLines() {
20+
val subject = subject()
21+
22+
val lines = listOf(
23+
"#comment",
24+
"foo=bar"
25+
)
26+
27+
val result = subject.updateProperty(lines, "foo", "bar")
28+
assertResult(result, lines, expectedMatched = true, expectedUpdated = false)
29+
}
30+
31+
@Test
32+
fun testIgnoresWhitespace() {
33+
val subject = subject()
34+
35+
val lines = listOf(
36+
" foo = bar "
37+
)
38+
39+
val result = subject.updateProperty(lines, "foo", "bar")
40+
assertResult(result, lines, expectedMatched = true, expectedUpdated = false)
41+
}
42+
43+
@Test
44+
fun testIgnoresCommentedOutValue() {
45+
val subject = subject()
46+
47+
val lines = listOf(
48+
"#foo=bar"
49+
)
50+
51+
val result = subject.updateProperty(lines, "foo", "bar")
52+
assertResult(result, lines, expectedMatched = false, expectedUpdated = false)
53+
}
54+
55+
@Test
56+
fun testUpdatesValue() {
57+
val subject = subject()
58+
59+
val lines = listOf(
60+
"foo=baz"
61+
)
62+
val result = subject.updateProperty(lines, "foo", "bar")
63+
64+
val expected = listOf(
65+
"foo=bar"
66+
)
67+
assertResult(result, expected, expectedMatched = true, expectedUpdated = true)
68+
}
69+
70+
@Test
71+
fun testUpdatedValueTrimsWhitespace() {
72+
val subject = subject()
73+
74+
val lines = listOf(
75+
" foo = baz "
76+
)
77+
val result = subject.updateProperty(lines, "foo", "bar")
78+
79+
val expected = listOf(
80+
"foo=bar"
81+
)
82+
assertResult(result, expected, expectedMatched = true, expectedUpdated = true)
83+
}
84+
85+
@Test
86+
fun testLastValueForPropertyInFileDeterminesUpdated() {
87+
val subject = subject()
88+
89+
val lines = listOf(
90+
" foo = bar ",
91+
"foo=baz"
92+
)
93+
val result = subject.updateProperty(lines, "foo", "bar")
94+
95+
val expected = listOf(
96+
" foo = bar ",
97+
"foo=bar"
98+
)
99+
assertResult(result, expected, expectedMatched = true, expectedUpdated = true)
100+
}
101+
102+
@Test
103+
fun testIfNotUpdatedIfLastValueAlreadyMatchedDespiteMutatingIntermediateLines() {
104+
val subject = subject()
105+
106+
val lines = listOf(
107+
" foo = bar ",
108+
"foo=baz"
109+
)
110+
val result = subject.updateProperty(lines, "foo", "baz")
111+
112+
val expected = listOf(
113+
"foo=baz",
114+
"foo=baz"
115+
)
116+
assertResult(result, expected, expectedMatched = true, expectedUpdated = false)
117+
}
118+
119+
@Test
120+
fun testReadsFromPropertiesFile() {
121+
val subject = subject()
122+
val file = subject.propertiesFile
123+
124+
val props = Properties()
125+
126+
props.setProperty("foo", "bar")
127+
FileOutputStream(file.toFile()).use {
128+
props.store(it, "the comments")
129+
}
130+
131+
val expected = Files.readAllLines(file, subject.propsCharset)
132+
val result = subject.updateProperty("foo", "bar")
133+
134+
assertResult(result, expected, expectedMatched = true, expectedUpdated = false)
135+
}
136+
137+
@Test
138+
fun testWritesToPropertiesFile() {
139+
val subject = subject()
140+
141+
val file = subject.propertiesFile
142+
143+
val props = Properties()
144+
145+
props.setProperty("foo", "baz")
146+
FileOutputStream(file.toFile()).use {
147+
props.store(it, "the comments")
148+
}
149+
150+
// this sucks as a test but work around the indeterminate date header
151+
val expected = Files.readAllLines(file, subject.propsCharset)
152+
153+
val result = subject.updateProperty("foo", "bar")
154+
expected[2] = "foo=bar"
155+
assertResult(result, expected, expectedMatched = true, expectedUpdated = true)
156+
157+
subject.saveResult(result.lines)
158+
159+
FileInputStream(file.toFile()).use {
160+
props.load(it)
161+
}
162+
163+
assertEquals("bar", props.getProperty("foo"))
164+
}
165+
166+
private fun assertResult(result: Result, expectedLines: List<String>, expectedMatched: Boolean, expectedUpdated: Boolean) {
167+
assertEquals(expectedMatched, result.matched, "whether the key was matched in the properties file")
168+
assertEquals(expectedUpdated, result.updated, "whether the effective value in the properties file was updated")
169+
assertEquals(expectedLines.size, result.lines.size, "expected number of lines")
170+
for ((index, line) in expectedLines.withIndex()) {
171+
assertEquals(line, result.lines[index], "expected line ${index + 1} to match")
172+
}
173+
}
174+
}

0 commit comments

Comments
 (0)