Repository: pypa/virtualenv

Issue:
move to news files for changelog via towncrier
pip already does this


Discussion:

Resolving patch:
diff --git a/docs/conf.py b/docs/conf.py
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,27 +1,54 @@
-# -*- coding: utf-8 -*-
+from __future__ import absolute_import, unicode_literals
+
 import os
+import re
+import subprocess
+import sys
+from pathlib import Path
+
+from virtualenv import __version__
 
-on_rtd = os.environ.get("READTHEDOCS", None) == "True"
 extensions = ["sphinx.ext.autodoc", "sphinx.ext.extlinks"]
 source_suffix = ".rst"
 master_doc = "index"
 project = "virtualenv"
 copyright = "2007-2018, Ian Bicking, The Open Planning Project, PyPA"
 
+ROOT_SRC_TREE_DIR = Path(__file__).parents[1]
+
 
-try:
-    from virtualenv import __version__
+def generate_draft_news():
+    home = "https://github.com"
+    issue = "{}/issue".format(home)
+    fragments_path = ROOT_SRC_TREE_DIR / "docs" / "changelog"
+    for pattern, replacement in (
+        (r"[^`]@([^,\s]+)", r"`@\1 <{}/\1>`_".format(home)),
+        (r"[^`]#([\d]+)", r"`#pr\1 <{}/\1>`_".format(issue)),
+    ):
+        for path in fragments_path.glob("*.rst"):
+            path.write_text(re.sub(pattern, replacement, path.read_text()))
+    env = os.environ.copy()
+    env["PATH"] += os.pathsep.join([os.path.dirname(sys.executable)] + env["PATH"].split(os.pathsep))
+    changelog = subprocess.check_output(
+        ["towncrier", "--draft", "--version", "DRAFT"], cwd=str(ROOT_SRC_TREE_DIR), env=env
+    ).decode("utf-8")
+    if "No significant changes" in changelog:
+        content = ""
+    else:
+        note = "*Changes in master, but not released yet are under the draft section*."
+        content = "{}\n\n{}".format(note, changelog)
+    (ROOT_SRC_TREE_DIR / "docs" / "_draft.rst").write_text(content)
 
-    # The short X.Y version.
-    version = ".".join(__version__.split(".")[:2])
-    # The full version, including alpha/beta/rc tags.
-    release = __version__
-except ImportError:
-    version = release = "dev"
+
+generate_draft_news()
+
+version = ".".join(__version__.split(".")[:2])
+release = __version__
 
 today_fmt = "%B %d, %Y"
 unused_docs = []
 pygments_style = "sphinx"
+exclude_patterns = ["changelog/*"]
 
 extlinks = {
     "issue": ("https://github.com/pypa/virtualenv/issues/%s", "#"),
@@ -29,6 +56,7 @@
 }
 
 html_theme = "default"
+on_rtd = os.environ.get("READTHEDOCS", None) == "True"
 if not on_rtd:
     try:
         import sphinx_rtd_theme
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -52,25 +52,7 @@ def read_file(*paths):
 # Get long_description from index.rst:
 long_description_full = read_file("docs", "index.rst")
 long_description = long_description_full.strip().split("split here", 1)[0]
-# Add release history
-changes = read_file("docs", "changes.rst")
-# Only report last two releases for brevity
-releases_found = 0
-change_lines = []
-for line in changes.splitlines():
-    change_lines.append(line)
-    if line.startswith("--------------"):
-        releases_found += 1
-    if releases_found > 2:
-        break
-
-changes = "\n".join(change_lines[:-2]) + "\n"
-changes += "`Full Changelog <https://virtualenv.pypa.io/en/latest/changes.html>`_."
-# Replace issue/pull directives
-changes = re.sub(r":pull:`(\d+)`", r"PR #\1", changes)
-changes = re.sub(r":issue:`(\d+)`", r"#\1", changes)
-
-long_description += "\n\n" + changes
+long_description += "\n\n`Read the changelog here <https://virtualenv.pypa.io/en/latest/changes.html>`_."
 
 
 def get_version():
@@ -128,7 +110,7 @@ def get_version():
             'xonsh; python_version>="3.4"',
             "six >= 1.10.0, < 2",
         ],
-        "docs": ["sphinx >= 1.8.0, < 2"],
+        "docs": ["sphinx >= 1.8.0, < 2", "towncrier >= 18.5.0"],
     },
     **setup_params
 )
diff --git a/tasks/release.py b/tasks/release.py
new file mode 100644
--- /dev/null
+++ b/tasks/release.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""Handles creating a release PR"""
+from pathlib import Path
+from subprocess import check_call
+from typing import Tuple
+
+from git import Commit, Head, Remote, Repo, TagReference
+from packaging.version import Version
+
+ROOT_SRC_DIR = Path(__file__).parents[1]
+
+
+def main(version_str: str) -> None:
+    version = Version(version_str)
+    repo = Repo(str(ROOT_SRC_DIR))
+
+    if repo.is_dirty():
+        raise RuntimeError("Current repository is dirty. Please commit any changes and try again.")
+    upstream, release_branch = create_release_branch(repo, version)
+    release_commit = release_changelog(repo, version)
+    tag = tag_release_commit(release_commit, repo, version)
+    print("push release commit")
+    repo.git.push(upstream.name, release_branch)
+    print("push release tag")
+    repo.git.push(upstream.name, tag)
+    print("All done! ✨ 🍰 ✨")
+
+
+def create_release_branch(repo: Repo, version: Version) -> Tuple[Remote, Head]:
+    print("create release branch from upstream master")
+    upstream = get_upstream(repo)
+    upstream.fetch()
+    branch_name = f"release-{version}"
+    release_branch = repo.create_head(branch_name, upstream.refs.master, force=True)
+    upstream.push(refspec=f"{branch_name}:{branch_name}", force=True)
+    release_branch.set_tracking_branch(repo.refs[f"{upstream.name}/{branch_name}"])
+    release_branch.checkout()
+    return upstream, release_branch
+
+
+def get_upstream(repo: Repo) -> Remote:
+    for remote in repo.remotes:
+        for url in remote.urls:
+            if url.endswith("tox-dev/tox.git"):
+                return remote
+    raise RuntimeError("could not find tox-dev/tox.git remote")
+
+
+def release_changelog(repo: Repo, version: Version) -> Commit:
+    print("generate release commit")
+    check_call(["towncrier", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR))
+    release_commit = repo.index.commit(f"release {version}")
+    return release_commit
+
+
+def tag_release_commit(release_commit, repo, version) -> TagReference:
+    print("tag release commit")
+    existing_tags = [x.name for x in repo.tags]
+    if version in existing_tags:
+        print("delete existing tag {}".format(version))
+        repo.delete_tag(version)
+    print("create tag {}".format(version))
+    tag = repo.create_tag(version, ref=release_commit, force=True)
+    return tag
+
+
+if __name__ == "__main__":
+    import argparse
+
+    parser = argparse.ArgumentParser(prog="release")
+    parser.add_argument("--version", required=True)
+    options = parser.parse_args()
+    main(options.version)


Test patch: