Repository: commitizen-tools/commitizen

Issue:
Ensure At Least One Blank Line Above When Updating Changelog
### Description

If you initialize the changelog with some additional documentation (e.g., "This changelog is auto-generated by the Python-based
[Commitizen](https://commitizen-tools.github.io/commitizen)."), then the first time you release, the auto-generated text appears immediately below the existing text, which looks fairly ugly. Subsequent releases have one blank line of whitespace between them. It's simple enough to manually add the extra blank line at the top in oneself (ideally before first running `cz bump`), but it would add a bit of polish to automatically insert the extra blank line on the first release just like all of the rest.

### Possible Solution

If the changelog already exists, the first release is no handled no differently than the rest. If there is no changelog, the current behavior should remain unchanged, meaning don't start adding an extra blank line at the top of the file.

### Additional context

I am happy to open a pull request if there are no concerns regarding this minor change. This makes it possible to include an initial changelog with some documentation in a template repository without violating a Prettier pre-commit hook.

### Additional context

_No response_

Discussion:

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index d0d5eba1..14bee6b4 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -10,7 +10,7 @@ Please fill in the following content to let us know better about this change.
 ## Checklist
 
 - [ ] Add test cases to all the changes you introduce
-- [ ] Run `./script/format` and `./script/test` locally to ensure this change passes linter check and test
+- [ ] Run `./scripts/format` and `./scripts/test` locally to ensure this change passes linter check and test
 - [ ] Test the changes on the local machine manually
 - [ ] Update the documentation for the changes
 
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index c7b0c4a3..71e15d28 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,14 +5,14 @@ repos:
     hooks:
       - id: check-vcs-permalinks
       - id: end-of-file-fixer
-        exclude: "tests/[test_*|data|commands/tests_*]/*"
+        exclude: "tests/((commands|data)/|test_).+"
       - id: trailing-whitespace
         args: [--markdown-linebreak-ext=md]
       - id: debug-statements
       - id: no-commit-to-branch
 
-  - repo: https://github.com/Woile/commitizen
-    rev: v1.23.0
+  - repo: https://github.com/commitizen-tools/commitizen
+    rev: v2.24.0 # automatically updated by Commitizen
     hooks:
       - id: commitizen
         stages: [commit-msg]
diff --git a/commitizen/changelog.py b/commitizen/changelog.py
index 0738c2e2..5e43e2fe 100644
--- a/commitizen/changelog.py
+++ b/commitizen/changelog.py
@@ -235,11 +235,11 @@ def get_metadata(filepath: str) -> Dict:
     }
 
 
-def incremental_build(new_content: str, lines: List, metadata: Dict) -> List:
+def incremental_build(new_content: str, lines: List[str], metadata: Dict) -> List[str]:
     """Takes the original lines and updates with new_content.
 
-    The metadata holds information enough to remove the old unreleased and
-    where to place the new content
+    The metadata governs how to remove the old unreleased section and where to place the
+    new content.
 
     Args:
         lines: The lines from the changelog
@@ -253,7 +253,7 @@ def incremental_build(new_content: str, lines: List, metadata: Dict) -> List:
     unreleased_end = metadata.get("unreleased_end")
     latest_version_position = metadata.get("latest_version_position")
     skip = False
-    output_lines: List = []
+    output_lines: List[str] = []
     for index, line in enumerate(lines):
         if index == unreleased_start:
             skip = True
@@ -270,16 +270,14 @@ def incremental_build(new_content: str, lines: List, metadata: Dict) -> List:
         if skip:
             continue
 
-        if (
-            isinstance(latest_version_position, int)
-            and index == latest_version_position
-        ):
-
-            output_lines.append(new_content)
-            output_lines.append("\n")
+        if index == latest_version_position:
+            output_lines.extend([new_content, "\n"])
 
         output_lines.append(line)
     if not isinstance(latest_version_position, int):
+        if output_lines and output_lines[-1].strip():
+            # Ensure at least one blank line between existing and new content.
+            output_lines.append("\n")
         output_lines.append(new_content)
     return output_lines
 
diff --git a/pyproject.toml b/pyproject.toml
index 070b950c..0bb7d8eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,8 @@ version = "2.24.0"
 tag_format = "v$version"
 version_files = [
   "pyproject.toml:version",
-  "commitizen/__version__.py"
+  "commitizen/__version__.py",
+  ".pre-commit-config.yaml:rev.\\s+(?=[^\\n]+Commitizen)"
 ]
 
 [tool.black]


Test patch:
diff --git a/tests/commands/test_changelog_command.py b/tests/commands/test_changelog_command.py
index 222bf8dd..3f90800e 100644
--- a/tests/commands/test_changelog_command.py
+++ b/tests/commands/test_changelog_command.py
@@ -320,6 +320,30 @@ def test_changelog_multiple_incremental_do_not_add_new_lines(
     assert out.startswith("#")
 
 
+@pytest.mark.usefixtures("tmp_commitizen_project")
+def test_changelog_incremental_newline_separates_new_content_from_old(
+    mocker, changelog_path
+):
+    """Test for https://github.com/commitizen-tools/commitizen/issues/509"""
+    with open(changelog_path, "w") as f:
+        f.write("Pre-existing content that should be kept\n")
+
+    create_file_and_commit("feat: add more cat videos")
+
+    testargs = ["cz", "changelog", "--incremental"]
+
+    mocker.patch.object(sys, "argv", testargs)
+    cli.main()
+
+    with open(changelog_path, "r") as f:
+        out = f.read()
+
+    assert (
+        out
+        == "Pre-existing content that should be kept\n\n## Unreleased\n\n### Feat\n\n- add more cat videos\n"
+    )
+
+
 def test_changelog_without_revision(mocker, tmp_commitizen_project):
     changelog_file = tmp_commitizen_project.join("CHANGELOG.md")
     changelog_file.write(