Skip to content

Commit 482cbea

Browse files
committed
Handle deletion of uncommitted news fragments
Before this commit, all the news fragments needed to be committed into git, or the fragments removal after building the news file would crash. In my workflow, I add missing fragments before building the news file because I'm extracting author names from the git log, and towncrier crashes at the end of the build process. Signed-off-by: Aurélien Bompard <[email protected]>
1 parent 5dce0fa commit 482cbea

File tree

4 files changed

+112
-23
lines changed

4 files changed

+112
-23
lines changed

src/towncrier/_git.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
11
# Copyright (c) Amber Brown, 2015
22
# See LICENSE for details.
33

4-
from subprocess import call
5-
64
import os
5+
import sys
6+
from subprocess import call, check_output, CalledProcessError
7+
78
import click
89

910

11+
def run(args, **kwargs):
12+
# Use UTF-8 both when sys.stdout does not have .encoding (Python 2.7) and
13+
# when the attribute is present but set to None (explicitly piped output
14+
# and also some CI such as GitHub Actions).
15+
encoding = getattr(sys.stdout, "encoding", None)
16+
if encoding is None:
17+
encoding = "utf8"
18+
19+
return check_output(args, **kwargs).decode(encoding).strip()
20+
21+
1022
def remove_files(fragment_filenames, answer_yes):
1123
if not fragment_filenames:
1224
return
@@ -16,11 +28,23 @@ def remove_files(fragment_filenames, answer_yes):
1628
else:
1729
click.echo("I want to remove the following files:")
1830

19-
for filename in fragment_filenames:
31+
for filename in sorted(fragment_filenames):
2032
click.echo(filename)
2133

34+
# Filter out files that are unknown to git
35+
try:
36+
known_fragments = run(
37+
["git", "ls-files"] + fragment_filenames
38+
).split("\n")
39+
except CalledProcessError:
40+
known_fragments = []
41+
2242
if answer_yes or click.confirm("Is it okay if I remove those files?", default=True):
23-
call(["git", "rm", "--quiet"] + fragment_filenames)
43+
call(["git", "rm", "--quiet", "--force"] + known_fragments)
44+
known_fragments_full = [os.path.abspath(f) for f in known_fragments]
45+
unknown_fragments = set(fragment_filenames) - set(known_fragments_full)
46+
for unknown_fragment in unknown_fragments:
47+
os.remove(unknown_fragment)
2448

2549

2650
def stage_newsfile(directory, filename):

src/towncrier/check.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,11 @@
88

99
import click
1010

11-
from subprocess import CalledProcessError, check_output, STDOUT
11+
from subprocess import CalledProcessError, STDOUT
1212

1313
from ._settings import load_config_from_options
1414
from ._builder import find_fragments
15-
16-
17-
def _run(args, **kwargs):
18-
kwargs["stderr"] = STDOUT
19-
return check_output(args, **kwargs)
15+
from ._git import run
2016

2117

2218
@click.command(name="check")
@@ -31,20 +27,10 @@ def __main(comparewith, directory, config):
3127

3228
base_directory, config = load_config_from_options(directory, config)
3329

34-
# Use UTF-8 both when sys.stdout does not have .encoding (Python 2.7) and
35-
# when the attribute is present but set to None (explicitly piped output
36-
# and also some CI such as GitHub Actions).
37-
encoding = getattr(sys.stdout, "encoding", None)
38-
if encoding is None:
39-
encoding = "utf8"
40-
4130
try:
42-
files_changed = (
43-
_run(
44-
["git", "diff", "--name-only", comparewith + "..."], cwd=base_directory
45-
)
46-
.decode(encoding)
47-
.strip()
31+
files_changed = run(
32+
["git", "diff", "--name-only", comparewith + "..."],
33+
cwd=base_directory, stderr=STDOUT
4834
)
4935
except CalledProcessError as e:
5036
click.echo("git produced output while failing:")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Handle deletion of uncommitted news fragments

src/towncrier/test/test_build.py

+78
Original file line numberDiff line numberDiff line change
@@ -836,3 +836,81 @@ def test_start_string(self):
836836
""")
837837

838838
self.assertEqual(expected_output, output)
839+
840+
def test_uncommitted_files(self):
841+
runner = CliRunner()
842+
843+
with runner.isolated_filesystem():
844+
setup_simple_project()
845+
with open("foo/newsfragments/123.feature", "w") as f:
846+
f.write("Adds levitation")
847+
with open("foo/newsfragments/124.feature", "w") as f:
848+
f.write("Extends levitation")
849+
with open("foo/newsfragments/125.feature", "w") as f:
850+
f.write("Baz levitation")
851+
with open("foo/newsfragments/126.feature", "w") as f:
852+
f.write("Fix (literal) crash")
853+
854+
call(["git", "init"])
855+
call(["git", "config", "user.name", "user"])
856+
call(["git", "config", "user.email", "[email protected]"])
857+
# 123 is committed, 124 is modified, 125 is just added, 126 is unknown
858+
call([
859+
"git", "add",
860+
"foo/newsfragments/123.feature",
861+
"foo/newsfragments/124.feature"
862+
])
863+
call(["git", "commit", "-m", "Initial Commit"])
864+
with open("foo/newsfragments/124.feature", "a") as f:
865+
f.write(" for an hour")
866+
call(["git", "add", "foo/newsfragments/125.feature"])
867+
868+
result = runner.invoke(_main, ["--date", "01-01-2001", "--yes"])
869+
870+
self.assertEqual(0, result.exit_code)
871+
for fragment in ("123", "124", "125", "126"):
872+
self.assertFalse(
873+
os.path.isfile(
874+
"foo/newsfragments/{}.feature".format(fragment)
875+
)
876+
)
877+
878+
path = "NEWS.rst"
879+
self.assertTrue(os.path.isfile(path))
880+
news_contents = open(path).read()
881+
self.assertEqual(
882+
news_contents,
883+
dedent(
884+
"""\
885+
Foo 1.2.3 (01-01-2001)
886+
======================
887+
888+
Features
889+
--------
890+
891+
- Adds levitation (#123)
892+
- Extends levitation for an hour (#124)
893+
- Baz levitation (#125)
894+
- Fix (literal) crash (#126)
895+
"""
896+
),
897+
)
898+
self.assertEqual(
899+
result.output,
900+
dedent(
901+
"""\
902+
Loading template...
903+
Finding news fragments...
904+
Rendering news fragments...
905+
Writing to newsfile...
906+
Staging newsfile...
907+
Removing news fragments...
908+
Removing the following files:
909+
{cwd}/foo/newsfragments/123.feature
910+
{cwd}/foo/newsfragments/124.feature
911+
{cwd}/foo/newsfragments/125.feature
912+
{cwd}/foo/newsfragments/126.feature
913+
Done!
914+
""".format(cwd=os.getcwd())
915+
),
916+
)

0 commit comments

Comments
 (0)