This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new ae0d01e110 .gitignore entries appended without a newline. fixes #8002
(#8005)
ae0d01e110 is described below
commit ae0d01e1109b4c0e2385172a1875d3a34c448ea3
Author: Bart Maertens <[email protected]>
AuthorDate: Thu Aug 20 09:44:07 2026 +0200
.gitignore entries appended without a newline. fixes #8002 (#8005)
* .gitignore entries appended without a newline. fixes #8002
* spotless
---------
Co-authored-by: Hans Van Akelyen <[email protected]>
---
.../main/java/org/apache/hop/git/model/UIGit.java | 42 +++++----
.../java/org/apache/hop/git/model/UIGitTest.java | 102 +++++++++++++++++++++
2 files changed, 128 insertions(+), 16 deletions(-)
diff --git a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
index c460cc35ea..08a74c9a11 100644
--- a/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
+++ b/plugins/misc/git/src/main/java/org/apache/hop/git/model/UIGit.java
@@ -1297,34 +1297,44 @@ public class UIGit extends VCS {
}
/**
- * Checks if a given path is already ignored in the specified .gitignore
file.
+ * Checks whether a .gitignore already holds a rule for the given path.
*
- * @param gitIgnore The .gitignore file to be checked.
+ * @param gitIgnoreContent The content of the .gitignore file to be checked.
* @param path The path to verify against the .gitignore file.
* @return true if the path is already ignored; false otherwise.
- * @throws IOException If an I/O error occurs while reading the .gitignore
file.
*/
- private boolean isAlreadyIgnored(File gitIgnore, String path) throws
IOException {
- List<String> lines = Files.readAllLines(gitIgnore.toPath(),
StandardCharsets.UTF_8);
- return lines.stream().map(String::trim).anyMatch(line ->
line.equals(path.trim()));
+ private boolean isAlreadyIgnored(String gitIgnoreContent, String path) {
+ return gitIgnoreContent.lines().map(String::trim).anyMatch(line ->
line.equals(path));
}
public void addPathToIgnore(String path) {
try {
- File gitIgnore = new File(getDirectory(), ".gitignore");
-
- boolean created = gitIgnore.createNewFile();
-
- // Checks if a given path is already ignored
- if (!isAlreadyIgnored(gitIgnore, path)) {
- Files.writeString(gitIgnore.toPath(), path, StandardOpenOption.APPEND);
+ String rule = normalizePathForJGit(path);
+ if (StringUtils.isBlank(rule)) {
+ return;
}
+ rule = rule.trim();
- // If the .gitignore file is created, stage it
- if (created) {
- git.add().addFilepattern(".gitignore").call();
+ File gitIgnore = new File(getDirectory(), ".gitignore");
+ String content =
+ gitIgnore.exists() ? Files.readString(gitIgnore.toPath(),
StandardCharsets.UTF_8) : "";
+ if (isAlreadyIgnored(content, rule)) {
+ return;
}
+ // Every rule is a line of its own. A .gitignore does not have to end
with a newline, and
+ // appending to one that doesn't would glue the new rule onto the last
one, leaving a single
+ // pattern which matches neither file.
+ //
+ String newline = content.contains("\r\n") ? "\r\n" : "\n";
+ String addition =
+ (content.isEmpty() || content.endsWith("\n") ? "" : newline) + rule
+ newline;
+ Files.writeString(
+ gitIgnore.toPath(), addition, StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
+
+ // Stage the .gitignore, so the new rule is part of the next commit
+ git.add().addFilepattern(".gitignore").call();
+
} catch (Exception e) {
showMessageBox(BaseMessages.getString(PKG, CONST_DIALOG_ERROR),
e.getMessage());
}
diff --git
a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
index 38849baf3f..57cb71990e 100644
--- a/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
+++ b/plugins/misc/git/src/test/java/org/apache/hop/git/model/UIGitTest.java
@@ -1010,6 +1010,108 @@ public class UIGitTest extends RepositoryTestCase {
message.getValue().contains("Please commit or revert your changes
before you merge"));
}
+ /**
+ * Every rule has to end up on a line of its own. Appending without a
newline glued the rules
+ * together into a single pattern which ignored neither file.
+ */
+ @Test
+ public void testAddPathToIgnoreWritesEachRuleOnItsOwnLine() throws Exception
{
+ initialCommit();
+ writeTrashFile("first.txt", "first");
+ writeTrashFile("second.txt", "second");
+
+ uiGit.addPathToIgnore("first.txt");
+ uiGit.addPathToIgnore("second.txt");
+
+ assertEquals(
+ List.of("first.txt", "second.txt"), readLines(new
File(db.getWorkTree(), ".gitignore")));
+
+ // Both rules are understood by git, which is what the newline is there for
+ //
+ Set<String> ignored = uiGit.getIgnored(null);
+ assertTrue(ignored.toString(), ignored.contains("first.txt"));
+ assertTrue(ignored.toString(), ignored.contains("second.txt"));
+ }
+
+ /**
+ * A .gitignore does not have to end with a newline. Appending to one that
doesn't used to glue
+ * the new rule onto the last one, breaking a rule which was already there.
+ */
+ @Test
+ public void testAddPathToIgnoreKeepsARuleWrittenWithoutATrailingNewline()
throws Exception {
+ initialCommit();
+ writeTrashFile(".gitignore", "existing.txt");
+ writeTrashFile("existing.txt", "existing");
+ writeTrashFile("added.txt", "added");
+
+ uiGit.addPathToIgnore("added.txt");
+
+ assertEquals(
+ List.of("existing.txt", "added.txt"), readLines(new
File(db.getWorkTree(), ".gitignore")));
+
+ Set<String> ignored = uiGit.getIgnored(null);
+ assertTrue(ignored.toString(), ignored.contains("existing.txt"));
+ assertTrue(ignored.toString(), ignored.contains("added.txt"));
+ }
+
+ /** The same path twice leaves one rule: the duplicate check reads whole
lines. */
+ @Test
+ public void testAddPathToIgnoreDoesNotWriteTheSamePathTwice() throws
Exception {
+ initialCommit();
+ writeTrashFile("once.txt", "once");
+
+ uiGit.addPathToIgnore("once.txt");
+ uiGit.addPathToIgnore("once.txt");
+
+ assertEquals(List.of("once.txt"), readLines(new File(db.getWorkTree(),
".gitignore")));
+ }
+
+ /** A .gitignore written on Windows keeps its line endings instead of ending
up mixed. */
+ @Test
+ public void testAddPathToIgnoreFollowsTheLineEndingsOfTheFile() throws
Exception {
+ initialCommit();
+ writeTrashFile(".gitignore", "existing.txt\r\n");
+
+ uiGit.addPathToIgnore("added.txt");
+
+ String content =
+ new String(
+ java.nio.file.Files.readAllBytes(new File(db.getWorkTree(),
".gitignore").toPath()),
+ StandardCharsets.UTF_8);
+ assertEquals("existing.txt\r\nadded.txt\r\n", content);
+ }
+
+ /** The .gitignore is staged, so the new rule is part of the next commit. */
+ @Test
+ public void testAddPathToIgnoreStagesANewlyCreatedGitIgnore() throws
Exception {
+ initialCommit();
+ writeTrashFile("generated.txt", "generated");
+
+ uiGit.addPathToIgnore("generated.txt");
+
+ assertTrue(git.status().call().getAdded().contains(".gitignore"));
+ }
+
+ /** A .gitignore which was already committed is staged too, not only a newly
created one. */
+ @Test
+ public void testAddPathToIgnoreStagesAnExistingGitIgnore() throws Exception {
+ writeTrashFile(".gitignore", "existing.txt\n");
+ git.add().addFilepattern(".gitignore").call();
+ git.commit().setMessage("initial commit").call();
+ writeTrashFile("generated.txt", "generated");
+
+ uiGit.addPathToIgnore("generated.txt");
+
+ assertTrue(git.status().call().getChanged().contains(".gitignore"));
+ assertTrue(uiGit.getUnstagedFiles().stream().noneMatch(f ->
f.getName().equals(".gitignore")));
+ }
+
+ private List<String> readLines(File file) throws Exception {
+ return java.nio.file.Files.readAllLines(file.toPath(),
StandardCharsets.UTF_8).stream()
+ .filter(line -> !line.isBlank())
+ .toList();
+ }
+
/**
* A merge has to be recorded as a merge. Committing the resolved conflict
used to reset the whole
* index first, which cleared MERGE_HEAD and left an ordinary commit behind:
git no longer