We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Update towncrier
We use on old version of towncrier with a custom script to detect release branch.

The latest towncrier should have support to checking a release branch.

We should update to towncrier and remove our custom script.

Any good stuff used in twisted/twisted custom towncrier script should be moved upstream.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/twisted/python/_release.py`
Content:
```
1 # -*- test-case-name: twisted.python.test.test_release -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4 
5 """
6 Twisted's automated release system.
7 
8 This module is only for use within Twisted's release system. If you are anyone
9 else, do not use it. The interface and behaviour will change without notice.
10 
11 Only Linux is supported by this code.  It should not be used by any tools
12 which must run on multiple platforms (eg the setup.py script).
13 """
14 
15 import os
16 import sys
17 from subprocess import STDOUT, CalledProcessError, check_output
18 from typing import Dict
19 
20 from zope.interface import Interface, implementer
21 
22 from twisted.python.compat import execfile
23 
24 # Types of newsfragments.
25 NEWSFRAGMENT_TYPES = ["doc", "bugfix", "misc", "feature", "removal"]
26 
27 
28 def runCommand(args, **kwargs):
29     """Execute a vector of arguments.
30 
31     This is a wrapper around L{subprocess.check_output}, so it takes
32     the same arguments as L{subprocess.Popen} with one difference: all
33     arguments after the vector must be keyword arguments.
34 
35     @param args: arguments passed to L{subprocess.check_output}
36     @param kwargs: keyword arguments passed to L{subprocess.check_output}
37     @return: command output
38     @rtype: L{bytes}
39     """
40     kwargs["stderr"] = STDOUT
41     return check_output(args, **kwargs)
42 
43 
44 class IVCSCommand(Interface):
45     """
46     An interface for VCS commands.
47     """
48 
49     def ensureIsWorkingDirectory(path):
50         """
51         Ensure that C{path} is a working directory of this VCS.
52 
53         @type path: L{twisted.python.filepath.FilePath}
54         @param path: The path to check.
55         """
56 
57     def isStatusClean(path):
58         """
59         Return the Git status of the files in the specified path.
60 
61         @type path: L{twisted.python.filepath.FilePath}
62         @param path: The path to get the status from (can be a directory or a
63             file.)
64         """
65 
66     def remove(path):
67         """
68         Remove the specified path from a the VCS.
69 
70         @type path: L{twisted.python.filepath.FilePath}
71         @param path: The path to remove from the repository.
72         """
73 
74     def exportTo(fromDir, exportDir):
75         """
76         Export the content of the VCSrepository to the specified directory.
77 
78         @type fromDir: L{twisted.python.filepath.FilePath}
79         @param fromDir: The path to the VCS repository to export.
80 
81         @type exportDir: L{twisted.python.filepath.FilePath}
82         @param exportDir: The directory to export the content of the
83             repository to. This directory doesn't have to exist prior to
84             exporting the repository.
85         """
86 
87 
88 @implementer(IVCSCommand)
89 class GitCommand:
90     """
91     Subset of Git commands to release Twisted from a Git repository.
92     """
93 
94     @staticmethod
95     def ensureIsWorkingDirectory(path):
96         """
97         Ensure that C{path} is a Git working directory.
98 
99         @type path: L{twisted.python.filepath.FilePath}
100         @param path: The path to check.
101         """
102         try:
103             runCommand(["git", "rev-parse"], cwd=path.path)
104         except (CalledProcessError, OSError):
105             raise NotWorkingDirectory(
106                 f"{path.path} does not appear to be a Git repository."
107             )
108 
109     @staticmethod
110     def isStatusClean(path):
111         """
112         Return the Git status of the files in the specified path.
113 
114         @type path: L{twisted.python.filepath.FilePath}
115         @param path: The path to get the status from (can be a directory or a
116             file.)
117         """
118         status = runCommand(["git", "-C", path.path, "status", "--short"]).strip()
119         return status == b""
120 
121     @staticmethod
122     def remove(path):
123         """
124         Remove the specified path from a Git repository.
125 
126         @type path: L{twisted.python.filepath.FilePath}
127         @param path: The path to remove from the repository.
128         """
129         runCommand(["git", "-C", path.dirname(), "rm", path.path])
130 
131     @staticmethod
132     def exportTo(fromDir, exportDir):
133         """
134         Export the content of a Git repository to the specified directory.
135 
136         @type fromDir: L{twisted.python.filepath.FilePath}
137         @param fromDir: The path to the Git repository to export.
138 
139         @type exportDir: L{twisted.python.filepath.FilePath}
140         @param exportDir: The directory to export the content of the
141             repository to. This directory doesn't have to exist prior to
142             exporting the repository.
143         """
144         runCommand(
145             [
146                 "git",
147                 "-C",
148                 fromDir.path,
149                 "checkout-index",
150                 "--all",
151                 "--force",
152                 # prefix has to end up with a "/" so that files get copied
153                 # to a directory whose name is the prefix.
154                 "--prefix",
155                 exportDir.path + "/",
156             ]
157         )
158 
159 
160 def getRepositoryCommand(directory):
161     """
162     Detect the VCS used in the specified directory and return a L{GitCommand}
163     if the directory is a Git repository. If the directory is not git, it
164     raises a L{NotWorkingDirectory} exception.
165 
166     @type directory: L{FilePath}
167     @param directory: The directory to detect the VCS used from.
168 
169     @rtype: L{GitCommand}
170 
171     @raise NotWorkingDirectory: if no supported VCS can be found from the
172         specified directory.
173     """
174     try:
175         GitCommand.ensureIsWorkingDirectory(directory)
176         return GitCommand
177     except (NotWorkingDirectory, OSError):
178         # It's not Git, but that's okay, eat the error
179         pass
180 
181     raise NotWorkingDirectory(f"No supported VCS can be found in {directory.path}")
182 
183 
184 class Project:
185     """
186     A representation of a project that has a version.
187 
188     @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base
189         directory of a Twisted-style Python package. The package should contain
190         a C{_version.py} file and a C{newsfragments} directory that contains a
191         C{README} file.
192     """
193 
194     def __init__(self, directory):
195         self.directory = directory
196 
197     def __repr__(self) -> str:
198         return f"{self.__class__.__name__}({self.directory!r})"
199 
200     def getVersion(self):
201         """
202         @return: A L{incremental.Version} specifying the version number of the
203             project based on live python modules.
204         """
205         namespace: Dict[str, object] = {}
206         directory = self.directory
207         while not namespace:
208             if directory.path == "/":
209                 raise Exception("Not inside a Twisted project.")
210             elif not directory.basename() == "twisted":
211                 directory = directory.parent()
212             else:
213                 execfile(directory.child("_version.py").path, namespace)
214         return namespace["__version__"]
215 
216 
217 def findTwistedProjects(baseDirectory):
218     """
219     Find all Twisted-style projects beneath a base directory.
220 
221     @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside.
222     @return: A list of L{Project}.
223     """
224     projects = []
225     for filePath in baseDirectory.walk():
226         if filePath.basename() == "newsfragments":
227             projectDirectory = filePath.parent()
228             projects.append(Project(projectDirectory))
229     return projects
230 
231 
232 def replaceInFile(filename, oldToNew):
233     """
234     I replace the text `oldstr' with `newstr' in `filename' using science.
235     """
236     os.rename(filename, filename + ".bak")
237     with open(filename + ".bak") as f:
238         d = f.read()
239     for k, v in oldToNew.items():
240         d = d.replace(k, v)
241     with open(filename + ".new", "w") as f:
242         f.write(d)
243     os.rename(filename + ".new", filename)
244     os.unlink(filename + ".bak")
245 
246 
247 class NoDocumentsFound(Exception):
248     """
249     Raised when no input documents are found.
250     """
251 
252 
253 def filePathDelta(origin, destination):
254     """
255     Return a list of strings that represent C{destination} as a path relative
256     to C{origin}.
257 
258     It is assumed that both paths represent directories, not files. That is to
259     say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to
260     L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},
261     not C{baz}.
262 
263     @type origin: L{twisted.python.filepath.FilePath}
264     @param origin: The origin of the relative path.
265 
266     @type destination: L{twisted.python.filepath.FilePath}
267     @param destination: The destination of the relative path.
268     """
269     commonItems = 0
270     path1 = origin.path.split(os.sep)
271     path2 = destination.path.split(os.sep)
272     for elem1, elem2 in zip(path1, path2):
273         if elem1 == elem2:
274             commonItems += 1
275         else:
276             break
277     path = [".."] * (len(path1) - commonItems)
278     return path + path2[commonItems:]
279 
280 
281 class NotWorkingDirectory(Exception):
282     """
283     Raised when a directory does not appear to be a repository directory of a
284     supported VCS.
285     """
286 
287 
288 class CheckNewsfragmentScript:
289     """
290     A thing for checking whether a checkout has a newsfragment.
291     """
292 
293     def __init__(self, _print):
294         self._print = _print
295 
296     def main(self, args):
297         """
298         Run the script.
299 
300         @type args: L{list} of L{str}
301         @param args: The command line arguments to process. This must contain
302             one string: the path to the root of the Twisted checkout.
303         """
304         if len(args) != 1:
305             sys.exit("Must specify one argument: the Twisted checkout")
306 
307         encoding = sys.stdout.encoding or "ascii"
308         location = os.path.abspath(args[0])
309 
310         branch = (
311             runCommand([b"git", b"rev-parse", b"--abbrev-ref", "HEAD"], cwd=location)
312             .decode(encoding)
313             .strip()
314         )
315 
316         # diff-filter=d to exclude deleted newsfiles (which will happen on the
317         # release branch)
318         r = (
319             runCommand(
320                 [
321                     b"git",
322                     b"diff",
323                     b"--name-only",
324                     b"origin/trunk...",
325                     b"--diff-filter=d",
326                 ],
327                 cwd=location,
328             )
329             .decode(encoding)
330             .strip()
331         )
332 
333         if not r:
334             self._print("On trunk or no diffs from trunk; no need to look at this.")
335             sys.exit(0)
336 
337         files = r.strip().split(os.linesep)
338 
339         self._print("Looking at these files:")
340         for change in files:
341             self._print(change)
342         self._print("----")
343 
344         if len(files) == 1:
345             if files[0] == os.sep.join(["docs", "fun", "Twisted.Quotes"]):
346                 self._print("Quotes change only; no newsfragment needed.")
347                 sys.exit(0)
348 
349         newsfragments = []
350 
351         for change in files:
352             if os.sep + "newsfragments" + os.sep in change:
353                 if "." in change and change.rsplit(".", 1)[1] in NEWSFRAGMENT_TYPES:
354                     newsfragments.append(change)
355 
356         if branch.startswith("release-"):
357             if newsfragments:
358                 self._print("No newsfragments should be on the release branch.")
359                 sys.exit(1)
360             else:
361                 self._print("Release branch with no newsfragments, all good.")
362                 sys.exit(0)
363 
364         if os.environ.get("GITHUB_HEAD_REF", "") == "pre-commit-ci-update-config":
365             # The run was triggered by pre-commit.ci.
366             if newsfragments:
367                 self._print(
368                     "No newsfragments should be present on an autoupdated branch."
369                 )
370                 sys.exit(1)
371             else:
372                 self._print("Autoupdated branch with no newsfragments, all good.")
373                 sys.exit(0)
374 
375         for change in newsfragments:
376             self._print("Found " + change)
377             sys.exit(0)
378 
379         self._print("No newsfragment found. Have you committed it?")
380         sys.exit(1)
381
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/twisted/python/_release.py b/src/twisted/python/_release.py
--- a/src/twisted/python/_release.py
+++ b/src/twisted/python/_release.py
@@ -13,7 +13,6 @@
 """
 
 import os
-import sys
 from subprocess import STDOUT, CalledProcessError, check_output
 from typing import Dict
 
@@ -21,9 +20,6 @@
 
 from twisted.python.compat import execfile
 
-# Types of newsfragments.
-NEWSFRAGMENT_TYPES = ["doc", "bugfix", "misc", "feature", "removal"]
-
 
 def runCommand(args, **kwargs):
     """Execute a vector of arguments.
@@ -283,98 +279,3 @@
     Raised when a directory does not appear to be a repository directory of a
     supported VCS.
     """
-
-
-class CheckNewsfragmentScript:
-    """
-    A thing for checking whether a checkout has a newsfragment.
-    """
-
-    def __init__(self, _print):
-        self._print = _print
-
-    def main(self, args):
-        """
-        Run the script.
-
-        @type args: L{list} of L{str}
-        @param args: The command line arguments to process. This must contain
-            one string: the path to the root of the Twisted checkout.
-        """
-        if len(args) != 1:
-            sys.exit("Must specify one argument: the Twisted checkout")
-
-        encoding = sys.stdout.encoding or "ascii"
-        location = os.path.abspath(args[0])
-
-        branch = (
-            runCommand([b"git", b"rev-parse", b"--abbrev-ref", "HEAD"], cwd=location)
-            .decode(encoding)
-            .strip()
-        )
-
-        # diff-filter=d to exclude deleted newsfiles (which will happen on the
-        # release branch)
-        r = (
-            runCommand(
-                [
-                    b"git",
-                    b"diff",
-                    b"--name-only",
-                    b"origin/trunk...",
-                    b"--diff-filter=d",
-                ],
-                cwd=location,
-            )
-            .decode(encoding)
-            .strip()
-        )
-
-        if not r:
-            self._print("On trunk or no diffs from trunk; no need to look at this.")
-            sys.exit(0)
-
-        files = r.strip().split(os.linesep)
-
-        self._print("Looking at these files:")
-        for change in files:
-            self._print(change)
-        self._print("----")
-
-        if len(files) == 1:
-            if files[0] == os.sep.join(["docs", "fun", "Twisted.Quotes"]):
-                self._print("Quotes change only; no newsfragment needed.")
-                sys.exit(0)
-
-        newsfragments = []
-
-        for change in files:
-            if os.sep + "newsfragments" + os.sep in change:
-                if "." in change and change.rsplit(".", 1)[1] in NEWSFRAGMENT_TYPES:
-                    newsfragments.append(change)
-
-        if branch.startswith("release-"):
-            if newsfragments:
-                self._print("No newsfragments should be on the release branch.")
-                sys.exit(1)
-            else:
-                self._print("Release branch with no newsfragments, all good.")
-                sys.exit(0)
-
-        if os.environ.get("GITHUB_HEAD_REF", "") == "pre-commit-ci-update-config":
-            # The run was triggered by pre-commit.ci.
-            if newsfragments:
-                self._print(
-                    "No newsfragments should be present on an autoupdated branch."
-                )
-                sys.exit(1)
-            else:
-                self._print("Autoupdated branch with no newsfragments, all good.")
-                sys.exit(0)
-
-        for change in newsfragments:
-            self._print("Found " + change)
-            sys.exit(0)
-
-        self._print("No newsfragment found. Have you committed it?")
-        sys.exit(1)

{"golden_diff": "diff --git a/src/twisted/python/_release.py b/src/twisted/python/_release.py\n--- a/src/twisted/python/_release.py\n+++ b/src/twisted/python/_release.py\n@@ -13,7 +13,6 @@\n \"\"\"\n \n import os\n-import sys\n from subprocess import STDOUT, CalledProcessError, check_output\n from typing import Dict\n \n@@ -21,9 +20,6 @@\n \n from twisted.python.compat import execfile\n \n-# Types of newsfragments.\n-NEWSFRAGMENT_TYPES = [\"doc\", \"bugfix\", \"misc\", \"feature\", \"removal\"]\n-\n \n def runCommand(args, **kwargs):\n     \"\"\"Execute a vector of arguments.\n@@ -283,98 +279,3 @@\n     Raised when a directory does not appear to be a repository directory of a\n     supported VCS.\n     \"\"\"\n-\n-\n-class CheckNewsfragmentScript:\n-    \"\"\"\n-    A thing for checking whether a checkout has a newsfragment.\n-    \"\"\"\n-\n-    def __init__(self, _print):\n-        self._print = _print\n-\n-    def main(self, args):\n-        \"\"\"\n-        Run the script.\n-\n-        @type args: L{list} of L{str}\n-        @param args: The command line arguments to process. This must contain\n-            one string: the path to the root of the Twisted checkout.\n-        \"\"\"\n-        if len(args) != 1:\n-            sys.exit(\"Must specify one argument: the Twisted checkout\")\n-\n-        encoding = sys.stdout.encoding or \"ascii\"\n-        location = os.path.abspath(args[0])\n-\n-        branch = (\n-            runCommand([b\"git\", b\"rev-parse\", b\"--abbrev-ref\", \"HEAD\"], cwd=location)\n-            .decode(encoding)\n-            .strip()\n-        )\n-\n-        # diff-filter=d to exclude deleted newsfiles (which will happen on the\n-        # release branch)\n-        r = (\n-            runCommand(\n-                [\n-                    b\"git\",\n-                    b\"diff\",\n-                    b\"--name-only\",\n-                    b\"origin/trunk...\",\n-                    b\"--diff-filter=d\",\n-                ],\n-                cwd=location,\n-            )\n-            .decode(encoding)\n-            .strip()\n-        )\n-\n-        if not r:\n-            self._print(\"On trunk or no diffs from trunk; no need to look at this.\")\n-            sys.exit(0)\n-\n-        files = r.strip().split(os.linesep)\n-\n-        self._print(\"Looking at these files:\")\n-        for change in files:\n-            self._print(change)\n-        self._print(\"----\")\n-\n-        if len(files) == 1:\n-            if files[0] == os.sep.join([\"docs\", \"fun\", \"Twisted.Quotes\"]):\n-                self._print(\"Quotes change only; no newsfragment needed.\")\n-                sys.exit(0)\n-\n-        newsfragments = []\n-\n-        for change in files:\n-            if os.sep + \"newsfragments\" + os.sep in change:\n-                if \".\" in change and change.rsplit(\".\", 1)[1] in NEWSFRAGMENT_TYPES:\n-                    newsfragments.append(change)\n-\n-        if branch.startswith(\"release-\"):\n-            if newsfragments:\n-                self._print(\"No newsfragments should be on the release branch.\")\n-                sys.exit(1)\n-            else:\n-                self._print(\"Release branch with no newsfragments, all good.\")\n-                sys.exit(0)\n-\n-        if os.environ.get(\"GITHUB_HEAD_REF\", \"\") == \"pre-commit-ci-update-config\":\n-            # The run was triggered by pre-commit.ci.\n-            if newsfragments:\n-                self._print(\n-                    \"No newsfragments should be present on an autoupdated branch.\"\n-                )\n-                sys.exit(1)\n-            else:\n-                self._print(\"Autoupdated branch with no newsfragments, all good.\")\n-                sys.exit(0)\n-\n-        for change in newsfragments:\n-            self._print(\"Found \" + change)\n-            sys.exit(0)\n-\n-        self._print(\"No newsfragment found. Have you committed it?\")\n-        sys.exit(1)\n", "issue": "Update towncrier\nWe use on old version of towncrier with a custom script to detect release branch.\r\n\r\nThe latest towncrier should have support to checking a release branch.\r\n\r\nWe should update to towncrier and remove our custom script.\r\n\r\nAny good stuff used in twisted/twisted custom towncrier script should be moved upstream.\n", "before_files": [{"content": "# -*- test-case-name: twisted.python.test.test_release -*-\n# Copyright (c) Twisted Matrix Laboratories.\n# See LICENSE for details.\n\n\"\"\"\nTwisted's automated release system.\n\nThis module is only for use within Twisted's release system. If you are anyone\nelse, do not use it. The interface and behaviour will change without notice.\n\nOnly Linux is supported by this code.  It should not be used by any tools\nwhich must run on multiple platforms (eg the setup.py script).\n\"\"\"\n\nimport os\nimport sys\nfrom subprocess import STDOUT, CalledProcessError, check_output\nfrom typing import Dict\n\nfrom zope.interface import Interface, implementer\n\nfrom twisted.python.compat import execfile\n\n# Types of newsfragments.\nNEWSFRAGMENT_TYPES = [\"doc\", \"bugfix\", \"misc\", \"feature\", \"removal\"]\n\n\ndef runCommand(args, **kwargs):\n    \"\"\"Execute a vector of arguments.\n\n    This is a wrapper around L{subprocess.check_output}, so it takes\n    the same arguments as L{subprocess.Popen} with one difference: all\n    arguments after the vector must be keyword arguments.\n\n    @param args: arguments passed to L{subprocess.check_output}\n    @param kwargs: keyword arguments passed to L{subprocess.check_output}\n    @return: command output\n    @rtype: L{bytes}\n    \"\"\"\n    kwargs[\"stderr\"] = STDOUT\n    return check_output(args, **kwargs)\n\n\nclass IVCSCommand(Interface):\n    \"\"\"\n    An interface for VCS commands.\n    \"\"\"\n\n    def ensureIsWorkingDirectory(path):\n        \"\"\"\n        Ensure that C{path} is a working directory of this VCS.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to check.\n        \"\"\"\n\n    def isStatusClean(path):\n        \"\"\"\n        Return the Git status of the files in the specified path.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to get the status from (can be a directory or a\n            file.)\n        \"\"\"\n\n    def remove(path):\n        \"\"\"\n        Remove the specified path from a the VCS.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to remove from the repository.\n        \"\"\"\n\n    def exportTo(fromDir, exportDir):\n        \"\"\"\n        Export the content of the VCSrepository to the specified directory.\n\n        @type fromDir: L{twisted.python.filepath.FilePath}\n        @param fromDir: The path to the VCS repository to export.\n\n        @type exportDir: L{twisted.python.filepath.FilePath}\n        @param exportDir: The directory to export the content of the\n            repository to. This directory doesn't have to exist prior to\n            exporting the repository.\n        \"\"\"\n\n\n@implementer(IVCSCommand)\nclass GitCommand:\n    \"\"\"\n    Subset of Git commands to release Twisted from a Git repository.\n    \"\"\"\n\n    @staticmethod\n    def ensureIsWorkingDirectory(path):\n        \"\"\"\n        Ensure that C{path} is a Git working directory.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to check.\n        \"\"\"\n        try:\n            runCommand([\"git\", \"rev-parse\"], cwd=path.path)\n        except (CalledProcessError, OSError):\n            raise NotWorkingDirectory(\n                f\"{path.path} does not appear to be a Git repository.\"\n            )\n\n    @staticmethod\n    def isStatusClean(path):\n        \"\"\"\n        Return the Git status of the files in the specified path.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to get the status from (can be a directory or a\n            file.)\n        \"\"\"\n        status = runCommand([\"git\", \"-C\", path.path, \"status\", \"--short\"]).strip()\n        return status == b\"\"\n\n    @staticmethod\n    def remove(path):\n        \"\"\"\n        Remove the specified path from a Git repository.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to remove from the repository.\n        \"\"\"\n        runCommand([\"git\", \"-C\", path.dirname(), \"rm\", path.path])\n\n    @staticmethod\n    def exportTo(fromDir, exportDir):\n        \"\"\"\n        Export the content of a Git repository to the specified directory.\n\n        @type fromDir: L{twisted.python.filepath.FilePath}\n        @param fromDir: The path to the Git repository to export.\n\n        @type exportDir: L{twisted.python.filepath.FilePath}\n        @param exportDir: The directory to export the content of the\n            repository to. This directory doesn't have to exist prior to\n            exporting the repository.\n        \"\"\"\n        runCommand(\n            [\n                \"git\",\n                \"-C\",\n                fromDir.path,\n                \"checkout-index\",\n                \"--all\",\n                \"--force\",\n                # prefix has to end up with a \"/\" so that files get copied\n                # to a directory whose name is the prefix.\n                \"--prefix\",\n                exportDir.path + \"/\",\n            ]\n        )\n\n\ndef getRepositoryCommand(directory):\n    \"\"\"\n    Detect the VCS used in the specified directory and return a L{GitCommand}\n    if the directory is a Git repository. If the directory is not git, it\n    raises a L{NotWorkingDirectory} exception.\n\n    @type directory: L{FilePath}\n    @param directory: The directory to detect the VCS used from.\n\n    @rtype: L{GitCommand}\n\n    @raise NotWorkingDirectory: if no supported VCS can be found from the\n        specified directory.\n    \"\"\"\n    try:\n        GitCommand.ensureIsWorkingDirectory(directory)\n        return GitCommand\n    except (NotWorkingDirectory, OSError):\n        # It's not Git, but that's okay, eat the error\n        pass\n\n    raise NotWorkingDirectory(f\"No supported VCS can be found in {directory.path}\")\n\n\nclass Project:\n    \"\"\"\n    A representation of a project that has a version.\n\n    @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base\n        directory of a Twisted-style Python package. The package should contain\n        a C{_version.py} file and a C{newsfragments} directory that contains a\n        C{README} file.\n    \"\"\"\n\n    def __init__(self, directory):\n        self.directory = directory\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.directory!r})\"\n\n    def getVersion(self):\n        \"\"\"\n        @return: A L{incremental.Version} specifying the version number of the\n            project based on live python modules.\n        \"\"\"\n        namespace: Dict[str, object] = {}\n        directory = self.directory\n        while not namespace:\n            if directory.path == \"/\":\n                raise Exception(\"Not inside a Twisted project.\")\n            elif not directory.basename() == \"twisted\":\n                directory = directory.parent()\n            else:\n                execfile(directory.child(\"_version.py\").path, namespace)\n        return namespace[\"__version__\"]\n\n\ndef findTwistedProjects(baseDirectory):\n    \"\"\"\n    Find all Twisted-style projects beneath a base directory.\n\n    @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside.\n    @return: A list of L{Project}.\n    \"\"\"\n    projects = []\n    for filePath in baseDirectory.walk():\n        if filePath.basename() == \"newsfragments\":\n            projectDirectory = filePath.parent()\n            projects.append(Project(projectDirectory))\n    return projects\n\n\ndef replaceInFile(filename, oldToNew):\n    \"\"\"\n    I replace the text `oldstr' with `newstr' in `filename' using science.\n    \"\"\"\n    os.rename(filename, filename + \".bak\")\n    with open(filename + \".bak\") as f:\n        d = f.read()\n    for k, v in oldToNew.items():\n        d = d.replace(k, v)\n    with open(filename + \".new\", \"w\") as f:\n        f.write(d)\n    os.rename(filename + \".new\", filename)\n    os.unlink(filename + \".bak\")\n\n\nclass NoDocumentsFound(Exception):\n    \"\"\"\n    Raised when no input documents are found.\n    \"\"\"\n\n\ndef filePathDelta(origin, destination):\n    \"\"\"\n    Return a list of strings that represent C{destination} as a path relative\n    to C{origin}.\n\n    It is assumed that both paths represent directories, not files. That is to\n    say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to\n    L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},\n    not C{baz}.\n\n    @type origin: L{twisted.python.filepath.FilePath}\n    @param origin: The origin of the relative path.\n\n    @type destination: L{twisted.python.filepath.FilePath}\n    @param destination: The destination of the relative path.\n    \"\"\"\n    commonItems = 0\n    path1 = origin.path.split(os.sep)\n    path2 = destination.path.split(os.sep)\n    for elem1, elem2 in zip(path1, path2):\n        if elem1 == elem2:\n            commonItems += 1\n        else:\n            break\n    path = [\"..\"] * (len(path1) - commonItems)\n    return path + path2[commonItems:]\n\n\nclass NotWorkingDirectory(Exception):\n    \"\"\"\n    Raised when a directory does not appear to be a repository directory of a\n    supported VCS.\n    \"\"\"\n\n\nclass CheckNewsfragmentScript:\n    \"\"\"\n    A thing for checking whether a checkout has a newsfragment.\n    \"\"\"\n\n    def __init__(self, _print):\n        self._print = _print\n\n    def main(self, args):\n        \"\"\"\n        Run the script.\n\n        @type args: L{list} of L{str}\n        @param args: The command line arguments to process. This must contain\n            one string: the path to the root of the Twisted checkout.\n        \"\"\"\n        if len(args) != 1:\n            sys.exit(\"Must specify one argument: the Twisted checkout\")\n\n        encoding = sys.stdout.encoding or \"ascii\"\n        location = os.path.abspath(args[0])\n\n        branch = (\n            runCommand([b\"git\", b\"rev-parse\", b\"--abbrev-ref\", \"HEAD\"], cwd=location)\n            .decode(encoding)\n            .strip()\n        )\n\n        # diff-filter=d to exclude deleted newsfiles (which will happen on the\n        # release branch)\n        r = (\n            runCommand(\n                [\n                    b\"git\",\n                    b\"diff\",\n                    b\"--name-only\",\n                    b\"origin/trunk...\",\n                    b\"--diff-filter=d\",\n                ],\n                cwd=location,\n            )\n            .decode(encoding)\n            .strip()\n        )\n\n        if not r:\n            self._print(\"On trunk or no diffs from trunk; no need to look at this.\")\n            sys.exit(0)\n\n        files = r.strip().split(os.linesep)\n\n        self._print(\"Looking at these files:\")\n        for change in files:\n            self._print(change)\n        self._print(\"----\")\n\n        if len(files) == 1:\n            if files[0] == os.sep.join([\"docs\", \"fun\", \"Twisted.Quotes\"]):\n                self._print(\"Quotes change only; no newsfragment needed.\")\n                sys.exit(0)\n\n        newsfragments = []\n\n        for change in files:\n            if os.sep + \"newsfragments\" + os.sep in change:\n                if \".\" in change and change.rsplit(\".\", 1)[1] in NEWSFRAGMENT_TYPES:\n                    newsfragments.append(change)\n\n        if branch.startswith(\"release-\"):\n            if newsfragments:\n                self._print(\"No newsfragments should be on the release branch.\")\n                sys.exit(1)\n            else:\n                self._print(\"Release branch with no newsfragments, all good.\")\n                sys.exit(0)\n\n        if os.environ.get(\"GITHUB_HEAD_REF\", \"\") == \"pre-commit-ci-update-config\":\n            # The run was triggered by pre-commit.ci.\n            if newsfragments:\n                self._print(\n                    \"No newsfragments should be present on an autoupdated branch.\"\n                )\n                sys.exit(1)\n            else:\n                self._print(\"Autoupdated branch with no newsfragments, all good.\")\n                sys.exit(0)\n\n        for change in newsfragments:\n            self._print(\"Found \" + change)\n            sys.exit(0)\n\n        self._print(\"No newsfragment found. Have you committed it?\")\n        sys.exit(1)\n", "path": "src/twisted/python/_release.py"}], "after_files": [{"content": "# -*- test-case-name: twisted.python.test.test_release -*-\n# Copyright (c) Twisted Matrix Laboratories.\n# See LICENSE for details.\n\n\"\"\"\nTwisted's automated release system.\n\nThis module is only for use within Twisted's release system. If you are anyone\nelse, do not use it. The interface and behaviour will change without notice.\n\nOnly Linux is supported by this code.  It should not be used by any tools\nwhich must run on multiple platforms (eg the setup.py script).\n\"\"\"\n\nimport os\nfrom subprocess import STDOUT, CalledProcessError, check_output\nfrom typing import Dict\n\nfrom zope.interface import Interface, implementer\n\nfrom twisted.python.compat import execfile\n\n\ndef runCommand(args, **kwargs):\n    \"\"\"Execute a vector of arguments.\n\n    This is a wrapper around L{subprocess.check_output}, so it takes\n    the same arguments as L{subprocess.Popen} with one difference: all\n    arguments after the vector must be keyword arguments.\n\n    @param args: arguments passed to L{subprocess.check_output}\n    @param kwargs: keyword arguments passed to L{subprocess.check_output}\n    @return: command output\n    @rtype: L{bytes}\n    \"\"\"\n    kwargs[\"stderr\"] = STDOUT\n    return check_output(args, **kwargs)\n\n\nclass IVCSCommand(Interface):\n    \"\"\"\n    An interface for VCS commands.\n    \"\"\"\n\n    def ensureIsWorkingDirectory(path):\n        \"\"\"\n        Ensure that C{path} is a working directory of this VCS.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to check.\n        \"\"\"\n\n    def isStatusClean(path):\n        \"\"\"\n        Return the Git status of the files in the specified path.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to get the status from (can be a directory or a\n            file.)\n        \"\"\"\n\n    def remove(path):\n        \"\"\"\n        Remove the specified path from a the VCS.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to remove from the repository.\n        \"\"\"\n\n    def exportTo(fromDir, exportDir):\n        \"\"\"\n        Export the content of the VCSrepository to the specified directory.\n\n        @type fromDir: L{twisted.python.filepath.FilePath}\n        @param fromDir: The path to the VCS repository to export.\n\n        @type exportDir: L{twisted.python.filepath.FilePath}\n        @param exportDir: The directory to export the content of the\n            repository to. This directory doesn't have to exist prior to\n            exporting the repository.\n        \"\"\"\n\n\n@implementer(IVCSCommand)\nclass GitCommand:\n    \"\"\"\n    Subset of Git commands to release Twisted from a Git repository.\n    \"\"\"\n\n    @staticmethod\n    def ensureIsWorkingDirectory(path):\n        \"\"\"\n        Ensure that C{path} is a Git working directory.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to check.\n        \"\"\"\n        try:\n            runCommand([\"git\", \"rev-parse\"], cwd=path.path)\n        except (CalledProcessError, OSError):\n            raise NotWorkingDirectory(\n                f\"{path.path} does not appear to be a Git repository.\"\n            )\n\n    @staticmethod\n    def isStatusClean(path):\n        \"\"\"\n        Return the Git status of the files in the specified path.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to get the status from (can be a directory or a\n            file.)\n        \"\"\"\n        status = runCommand([\"git\", \"-C\", path.path, \"status\", \"--short\"]).strip()\n        return status == b\"\"\n\n    @staticmethod\n    def remove(path):\n        \"\"\"\n        Remove the specified path from a Git repository.\n\n        @type path: L{twisted.python.filepath.FilePath}\n        @param path: The path to remove from the repository.\n        \"\"\"\n        runCommand([\"git\", \"-C\", path.dirname(), \"rm\", path.path])\n\n    @staticmethod\n    def exportTo(fromDir, exportDir):\n        \"\"\"\n        Export the content of a Git repository to the specified directory.\n\n        @type fromDir: L{twisted.python.filepath.FilePath}\n        @param fromDir: The path to the Git repository to export.\n\n        @type exportDir: L{twisted.python.filepath.FilePath}\n        @param exportDir: The directory to export the content of the\n            repository to. This directory doesn't have to exist prior to\n            exporting the repository.\n        \"\"\"\n        runCommand(\n            [\n                \"git\",\n                \"-C\",\n                fromDir.path,\n                \"checkout-index\",\n                \"--all\",\n                \"--force\",\n                # prefix has to end up with a \"/\" so that files get copied\n                # to a directory whose name is the prefix.\n                \"--prefix\",\n                exportDir.path + \"/\",\n            ]\n        )\n\n\ndef getRepositoryCommand(directory):\n    \"\"\"\n    Detect the VCS used in the specified directory and return a L{GitCommand}\n    if the directory is a Git repository. If the directory is not git, it\n    raises a L{NotWorkingDirectory} exception.\n\n    @type directory: L{FilePath}\n    @param directory: The directory to detect the VCS used from.\n\n    @rtype: L{GitCommand}\n\n    @raise NotWorkingDirectory: if no supported VCS can be found from the\n        specified directory.\n    \"\"\"\n    try:\n        GitCommand.ensureIsWorkingDirectory(directory)\n        return GitCommand\n    except (NotWorkingDirectory, OSError):\n        # It's not Git, but that's okay, eat the error\n        pass\n\n    raise NotWorkingDirectory(f\"No supported VCS can be found in {directory.path}\")\n\n\nclass Project:\n    \"\"\"\n    A representation of a project that has a version.\n\n    @ivar directory: A L{twisted.python.filepath.FilePath} pointing to the base\n        directory of a Twisted-style Python package. The package should contain\n        a C{_version.py} file and a C{newsfragments} directory that contains a\n        C{README} file.\n    \"\"\"\n\n    def __init__(self, directory):\n        self.directory = directory\n\n    def __repr__(self) -> str:\n        return f\"{self.__class__.__name__}({self.directory!r})\"\n\n    def getVersion(self):\n        \"\"\"\n        @return: A L{incremental.Version} specifying the version number of the\n            project based on live python modules.\n        \"\"\"\n        namespace: Dict[str, object] = {}\n        directory = self.directory\n        while not namespace:\n            if directory.path == \"/\":\n                raise Exception(\"Not inside a Twisted project.\")\n            elif not directory.basename() == \"twisted\":\n                directory = directory.parent()\n            else:\n                execfile(directory.child(\"_version.py\").path, namespace)\n        return namespace[\"__version__\"]\n\n\ndef findTwistedProjects(baseDirectory):\n    \"\"\"\n    Find all Twisted-style projects beneath a base directory.\n\n    @param baseDirectory: A L{twisted.python.filepath.FilePath} to look inside.\n    @return: A list of L{Project}.\n    \"\"\"\n    projects = []\n    for filePath in baseDirectory.walk():\n        if filePath.basename() == \"newsfragments\":\n            projectDirectory = filePath.parent()\n            projects.append(Project(projectDirectory))\n    return projects\n\n\ndef replaceInFile(filename, oldToNew):\n    \"\"\"\n    I replace the text `oldstr' with `newstr' in `filename' using science.\n    \"\"\"\n    os.rename(filename, filename + \".bak\")\n    with open(filename + \".bak\") as f:\n        d = f.read()\n    for k, v in oldToNew.items():\n        d = d.replace(k, v)\n    with open(filename + \".new\", \"w\") as f:\n        f.write(d)\n    os.rename(filename + \".new\", filename)\n    os.unlink(filename + \".bak\")\n\n\nclass NoDocumentsFound(Exception):\n    \"\"\"\n    Raised when no input documents are found.\n    \"\"\"\n\n\ndef filePathDelta(origin, destination):\n    \"\"\"\n    Return a list of strings that represent C{destination} as a path relative\n    to C{origin}.\n\n    It is assumed that both paths represent directories, not files. That is to\n    say, the delta of L{twisted.python.filepath.FilePath} /foo/bar to\n    L{twisted.python.filepath.FilePath} /foo/baz will be C{../baz},\n    not C{baz}.\n\n    @type origin: L{twisted.python.filepath.FilePath}\n    @param origin: The origin of the relative path.\n\n    @type destination: L{twisted.python.filepath.FilePath}\n    @param destination: The destination of the relative path.\n    \"\"\"\n    commonItems = 0\n    path1 = origin.path.split(os.sep)\n    path2 = destination.path.split(os.sep)\n    for elem1, elem2 in zip(path1, path2):\n        if elem1 == elem2:\n            commonItems += 1\n        else:\n            break\n    path = [\"..\"] * (len(path1) - commonItems)\n    return path + path2[commonItems:]\n\n\nclass NotWorkingDirectory(Exception):\n    \"\"\"\n    Raised when a directory does not appear to be a repository directory of a\n    supported VCS.\n    \"\"\"\n", "path": "src/twisted/python/_release.py"}]}