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

Reply via email to