diff --git a/doc/lib/command/doc.py b/doc/lib/command/doc.py
index 9e4666a7b..b51b39d9a 100644
--- a/doc/lib/command/doc.py
+++ b/doc/lib/command/doc.py
@@ -181,7 +181,7 @@ def _build(config, var_store):
var_store.add("release-date", date)
var_store.add("release-year", year)
- release_history_update(path_repo, config.doc_path)
+ claim = release_history_update(path_repo, config.doc_path)
document_map = {
"command": reference_command_render(bld_cfg, bld_hlp),
@@ -189,7 +189,7 @@ def _build(config, var_store):
"index": news_index_render(index, news),
"news": news_render(news),
"user-guide": build_pre(user_guide, bld_hlp, var_store),
- "release": release_render(release, config.doc_path, config.var_map.get("dev") == "y"),
+ "release": release_render(release, config.doc_path, config.var_map.get("dev") == "y", claim),
}
return document_map, reference_man_render(index, bld_cfg, bld_hlp)
diff --git a/doc/lib/command/render/release.py b/doc/lib/command/render/release.py
index 4bec882a4..d1f1a67ec 100644
--- a/doc/lib/command/render/release.py
+++ b/doc/lib/command/render/release.py
@@ -15,7 +15,7 @@ from collections import deque
from common.date import date_render
from common.error import ToolError
-from common.exec import exec_result
+from common.exec import exec_result, exec_status
from common.storage import file_read, file_write
from common.xml import (
xml_document_new,
@@ -35,6 +35,9 @@ from config.project import PROJECT_NAME
# Where the git history is cached
_FILE_HISTORY = "resource/git-history.cache"
+# Local branch the git history ends at when it is related to HEAD
+_HISTORY_BRANCH = "refs/heads/integration"
+
# Subject of a commit that closed a release
_RELEASE_SUBJECT_EXP = re.compile(r"^v[0-9]{1,2}\.[0-9]{1,2}(\.[0-9]+)?: ")
@@ -57,6 +60,7 @@ _ITEM_LIST = (
_CONTRIBUTOR_LIST = (
("release-item-contributor", "Fixed", "Contributed"),
("release-item-reviewer", "Reviewed", "Reviewed"),
+ ("release-item-tester", "Tested", "Tested"),
("release-item-ideator", "Reported", "Suggested"),
)
@@ -71,7 +75,7 @@ _COMMIT_URL = "{[github-url-base]}/commit/%s"
_COMMIT_SIZE = 7
# Releases with version >= this have the paragraphs after the first and the links of an item in details
-_DETAIL_VERSION_MIN = "2.60"
+_DETAIL_VERSION_MIN = "2.33"
# A bug fix release is x.y.z with z > 0
_VERSION_BUG_FIX_EXP = re.compile(r"^[0-9]+\.[0-9]+\.[1-9][0-9]*$")
@@ -105,22 +109,57 @@ def release_current_stable(root):
####################################################################################################################################
def release_history_update(path_repo, path_doc):
- """Write the git history the release notes are checked against when HEAD is not the newest commit in it.
+ """Write the git history the release notes are checked against when the newest commit in it has changed.
- The body of a commit that closed a release is left out."""
+ When the local integration branch is related to HEAD the history ends at the newest commit of HEAD that is also in integration
+ and the result is true. The commits of a release branch are left out this way. Otherwise the history ends at HEAD and the result
+ is false. The body of a commit that closed a release is left out."""
path_history = os.path.join(path_doc, _FILE_HISTORY)
- head = exec_result("git -C %s rev-parse HEAD" % path_repo, suppress_stderr=True)[1].strip()
+ command = "git -C %s merge-base HEAD %s" % (path_repo, _HISTORY_BRANCH)
+ related = exec_status(command) == 0
+ head = exec_result(command if related else "git -C %s rev-parse HEAD" % path_repo, suppress_stderr=True)[1].strip()
- if os.path.isfile(path_history) and json.loads(file_read(path_history))[0]["commit"] == head:
- return
+ # The tag of each patch release with version >= _DETAIL_VERSION_MIN
+ tag_map = {}
+
+ for tag in exec_result("git -C %s tag --list 'release/*'" % path_repo, suppress_stderr=True)[1].split():
+ version = tag[len("release/") :]
+
+ if _VERSION_BUG_FIX_EXP.match(version) is not None and version >= _DETAIL_VERSION_MIN:
+ tag_map[version] = exec_result("git -C %s rev-parse '%s^{commit}'" % (path_repo, tag), suppress_stderr=True)[1].strip()
+
+ if os.path.isfile(path_history):
+ history = json.loads(file_read(path_history))
+
+ if (
+ isinstance(history, dict)
+ and history["head"][0]["commit"] == head
+ and {version: tag_list[0]["commit"] for version, tag_list in history["tag"].items()} == tag_map
+ ):
+ return related
+
+ # The history of a patch release is the commits of its tag that are not in the history of HEAD
+ history = {
+ "head": _history_read(path_repo, head),
+ "tag": {version: _history_read(path_repo, "%s --not %s" % (commit, head)) for version, commit in tag_map.items()},
+ }
+
+ file_write(path_history, json.dumps(history, indent=4, ensure_ascii=False) + "\n")
+
+ return related
+
+
+####################################################################################################################################
+def _history_read(path_repo, revision):
+ """Read the commits git log lists for a revision, newest first."""
# Records are separated by a byte that cannot appear in a commit message, and so are the fields within a record
output = exec_result(
- "git -C %s log --pretty=format:%%H%%x00%%ci%%x00%%s%%x00%%b%%x1e %s" % (path_repo, head), suppress_stderr=True
+ "git -C %s log --pretty=format:%%H%%x00%%ci%%x00%%s%%x00%%b%%x1e %s" % (path_repo, revision), suppress_stderr=True
)[1]
- history = []
+ result = []
for record in output.split("\x1e"):
record = record.strip("\n")
@@ -135,9 +174,9 @@ def release_history_update(path_repo, path_doc):
if body != "" and _RELEASE_SUBJECT_EXP.match(subject) is None:
entry["body"] = body
- history.append(entry)
+ result.append(entry)
- file_write(path_history, json.dumps(history, indent=4, ensure_ascii=False) + "\n")
+ return result
####################################################################################################################################
@@ -157,11 +196,15 @@ def _date_render(date, version):
class _Release:
"""Builds the release document."""
- def __init__(self, root, path_doc, dev):
+ def __init__(self, root, path_doc, dev, claim):
self.root = root
self.dev = dev
+ self.claim = claim # Must every commit of a release be claimed by an item?
+ self.version_list = [xml_node_attribute(release, "version", True) for release in release_list(root)]
- self.history = deque(json.loads(file_read(os.path.join(path_doc, _FILE_HISTORY))))
+ history = json.loads(file_read(os.path.join(path_doc, _FILE_HISTORY)))
+ self.history = deque(history["head"])
+ self.history_tag = history["tag"]
# The first contributor is the default for every item, which is what keeps the common case out of the xml
self.contributor_map = {}
@@ -179,6 +222,9 @@ class _Release:
self.render = DocRender("text")
self.render.tag_set("backrest", PROJECT_NAME)
+ # The items of each release that are not development items, as [text, list item, item, commits, development items]
+ self.release_item_map = {}
+
################################################################################################################################
def _contributor_text(self, item, item_type):
"""What the release notes say about who did the work on an item."""
@@ -239,17 +285,19 @@ class _Release:
return result
################################################################################################################################
- def _commit_find(self, commit_list, subject, regexp=True):
- """Find a commit by what its subject begins with, which is how a release item is tied to the work that did it."""
+ def _commit_find(self, commit_list, subject, regexp=True, key="subject"):
+ """Find a commit by what its subject begins with, which is how a release item is tied to the work that did it.
+
+ With key="commit" the commit is found by what its hash begins with."""
result = None
for commit in commit_list:
- found = re.match(subject, commit["subject"]) is not None if regexp else commit["subject"].startswith(subject)
+ found = re.match(subject, commit[key]) is not None if regexp else commit[key].startswith(subject)
if found:
if result is not None:
- raise ToolError("subject prefix '%s' already found in commit %s" % (subject, commit["commit"]))
+ raise ToolError("%s prefix '%s' already found in commit %s" % (key, subject, commit["commit"]))
result = commit
@@ -274,24 +322,35 @@ class _Release:
def _commit_list(self, version, release_idx, release_all):
"""Every commit that belongs to a release, and whether the items of the release are checked against them.
- A bug fix release that is not the most recent is skipped since it was built on a branch of its own, so the commits are not
- in the history between this release and the one before it."""
+ A bug fix release that is not the most recent was built on a branch of its own, so its commits are read from the history of
+ its tag. It is skipped when there is no history for its tag."""
- if version < "2.01" or (_VERSION_BUG_FIX_EXP.match(version) is not None and release_idx != 0):
+ bug_fix = _VERSION_BUG_FIX_EXP.match(version) is not None and release_idx != 0
+
+ if version < "2.01" or (bug_fix and version not in self.history_tag):
return [], [], {}, False
+ history = deque(self.history_tag[version]) if bug_fix else self.history
dev = version.endswith("dev")
# The commit that opened development on this release and the commit that closed the release before it, which are the ends of
# what belongs to this release
- commit_begin = self._commit_find(self.history, "Begin v%s development\\." % version)
+ commit_begin = self._commit_find(history, "Begin v%s development\\." % version)
+
+ # Development on an x.y.0 release may have been opened as x.y
+ if commit_begin is None and version.endswith(".0"):
+ commit_begin = self._commit_find(history, "Begin v%s development\\." % version[:-2])
+
commit_begin = None if commit_begin is None else commit_begin["commit"]
- # Skip back over bug fix releases, which were built on branches of their own
+ # Skip back over bug fix releases, which were built on branches of their own. The tag history of a bug fix release ends
+ # at the release before it.
last_idx = release_idx + 1
- while last_idx < len(release_all) and _VERSION_BUG_FIX_EXP.match(
- xml_node_attribute(release_all[last_idx], "version", True)
+ while (
+ not bug_fix
+ and last_idx < len(release_all)
+ and _VERSION_BUG_FIX_EXP.match(xml_node_attribute(release_all[last_idx], "version", True))
):
last_idx += 1
@@ -300,21 +359,21 @@ class _Release:
version_last = xml_node_attribute(release_all[last_idx], "version", True)
- commit_last_end = self._commit_find(self.history, "v%s\\: .+" % version_last)
+ commit_last_end = self._commit_find(history, "v%s\\: .+" % version_last)
- if commit_last_end is None:
+ if commit_last_end is None and not bug_fix:
raise ToolError("release %s must have an end commit" % version_last)
# A release that is out has a commit that closed it, which the release after it has already checked for
- commit_end = self._commit_find(self.history, "v%s\\: .+" % version)
+ commit_end = self._commit_find(history, "v%s\\: .+" % version)
commit_end = None if commit_end is None else commit_end["commit"]
commit_list = []
remaining_list = []
commit_map = {}
- while len(self.history) > 0 and self.history[0]["commit"] != commit_last_end["commit"]:
- commit = self.history.popleft()
+ while len(history) > 0 and (commit_last_end is None or history[0]["commit"] != commit_last_end["commit"]):
+ commit = history.popleft()
# The commits that open and close a release are the release rather than part of it
if commit["commit"] in (commit_begin, commit_end):
@@ -357,18 +416,32 @@ class _Release:
for commit_xml in commit_xml_list:
subject = xml_node_attribute(commit_xml, "subject", True)
- commit = self._commit_find(commit_list, subject, False)
+ commit_id = xml_node_attribute(commit_xml, "id")
- if commit is None:
- if check:
- self._commit_error(
- "unable to find release %s commit subject '%s' in list" % (version, subject), remaining_list, commit_map
+ # A commit with an id is found by the id and its subject must begin with the subject named
+ if commit_id is not None:
+ name = "id '%s'" % commit_id
+ commit = self._commit_find(commit_list, commit_id, False, "commit")
+
+ if commit is not None and not commit["subject"].startswith(subject):
+ raise ToolError(
+ "release %s commit id '%s' subject '%s' does not begin with '%s'"
+ % (version, commit_id, commit["subject"], subject)
)
- elif commit["commit"] in remaining_list:
- remaining_list.remove(commit["commit"])
+ else:
+ name = "subject '%s'" % subject
+ commit = self._commit_find(commit_list, subject, False)
+
+ if commit is None and check:
+ self._commit_error("unable to find release %s commit %s in list" % (version, name), remaining_list, commit_map)
result.append((commit_xml, commit))
+ # Remove the commits found from the commits no item has claimed
+ for _, commit in result:
+ if commit is not None and commit["commit"] in remaining_list:
+ remaining_list.remove(commit["commit"])
+
return result
################################################################################################################################
@@ -396,26 +469,43 @@ class _Release:
self._ref_add(ref, label, url % id, "#%s" % id)
################################################################################################################################
- def _detail_add(self, list_item, item, item_commit_list):
- """Add the paragraphs after the first and the issues, pull requests, and commits of an item as details.
+ @staticmethod
+ def _version_line_add(detail, version, text, note=None):
+ """Add a line to details that links to a release, after the text of a release note in quotes when note is not None."""
+
+ ref_item = xml_node_add(xml_node_add(detail, "list-detail-ref"), "list-detail-ref-item")
+
+ xml_node_content_add(ref_item, text)
+
+ if note is not None:
+ xml_node_content_add(xml_node_add(ref_item, "quote"), note)
+ xml_node_content_add(ref_item, " in ")
+
+ xml_node_content_add(xml_node_add(ref_item, "link", {"url": "#%s" % version}), "v%s" % version)
+ xml_node_content_add(ref_item, ":")
+
+ ################################################################################################################################
+ def _ref_line_add(self, detail, item, item_commit_list, commit_line):
+ """Add the issues, pull requests, and commits of an item to details.
An item with one commit has the commit on the same line as its issues and pull requests, after them. An item with more than
- one commit has a line for each commit, with the subject of the commit and the issues and pull requests the commit element
- names."""
+ one commit, or any item when commit_line is true, has a line for each commit, with the subject of the commit and the issues
+ and pull requests the commit element names. The issues and pull requests of an item with one commit are on the line of the
+ commit when commit_line is true."""
- detail = xml_node_add(list_item, "list-detail")
-
- for paragraph in xml_node_child_list(item, "p")[1:]:
- xml_node_child_add(xml_node_add(detail, "list-detail-text"), paragraph)
+ item_line = commit_line and len(item_commit_list) == 1
+ commit_line = commit_line or len(item_commit_list) > 1
ref = xml_node_add(detail, "list-detail-ref")
- self._ref_list_add(ref, item)
+
+ if not item_line:
+ self._ref_list_add(ref, item)
for commit_xml, commit in item_commit_list:
url = None if commit is None else _COMMIT_URL % commit["commit"]
hash = None if commit is None else commit["commit"][:_COMMIT_SIZE]
- if len(item_commit_list) > 1:
+ if commit_line:
ref = xml_node_add(detail, "list-detail-ref")
# Escape the subject for html
@@ -425,9 +515,39 @@ class _Release:
if commit_xml is not None:
self._ref_list_add(ref, commit_xml)
- if len(item_commit_list) == 1 and commit is not None:
+ if item_line:
+ self._ref_list_add(ref, item)
+
+ if not commit_line and commit is not None:
self._ref_add(ref, "Commit", url, hash, True)
+ ################################################################################################################################
+ def _detail_add(self, list_item, item, item_commit_list, commit_line=False, release_note=None, dev_list=()):
+ """Add the paragraphs after the first and the issues, pull requests, and commits of an item as details.
+
+ release_note is the version and text of the release note a development item names. dev_list is the development items
+ that name an item as their release note, as (version, item, commits), listed under a line for each release that links to
+ it."""
+
+ detail = xml_node_add(list_item, "list-detail")
+
+ for paragraph in xml_node_child_list(item, "p")[1:]:
+ xml_node_child_add(xml_node_add(detail, "list-detail-text"), paragraph)
+
+ if release_note is not None:
+ self._version_line_add(detail, release_note[0], "Development for ", release_note[1])
+
+ self._ref_line_add(detail, item, item_commit_list, commit_line)
+
+ dev_version_last = None
+
+ for dev_version, dev_item, dev_commit_list in dev_list:
+ if dev_version != dev_version_last:
+ self._version_line_add(detail, dev_version, "Development in ")
+ dev_version_last = dev_version
+
+ self._ref_line_add(detail, dev_item, dev_commit_list, True)
+
# Remove lines with no links and details with nothing in them
for ref in xml_node_child_list(detail, "list-detail-ref"):
if len(ref) == 0:
@@ -436,6 +556,49 @@ class _Release:
if len(detail) == 0:
list_item.remove(detail)
+ ################################################################################################################################
+ def _release_note_add(self, item, version, item_commit_list):
+ """Add a development item to the details of the release note it names. A release note is an item that is not a development
+ item in a later release. Returns the version and the text without a period at the end of the release note, or None when the
+ item names no release note."""
+
+ release_note = xml_node_child(item, "release-note")
+
+ if release_note is None:
+ return None
+
+ note_version = xml_node_attribute(release_note, "version", True)
+ note_subject = xml_node_attribute(release_note, "subject", True)
+
+ # A release in development is named by the version it will have when it is released
+ if note_version not in self.version_list and note_version + "dev" in self.version_list:
+ note_version += "dev"
+
+ if note_version not in self.version_list or self.version_list.index(note_version) >= self.version_list.index(version):
+ raise ToolError("release %s release note version %s is not a later release" % (version, note_version))
+
+ note_list = [note for note in self.release_item_map.get(note_version, []) if note[0].startswith(note_subject)]
+
+ if len(note_list) == 0:
+ raise ToolError("release %s release note '%s' not found in release %s" % (version, note_subject, note_version))
+
+ if len(note_list) > 1:
+ raise ToolError(
+ "release %s release note '%s' found more than once in release %s" % (version, note_subject, note_version)
+ )
+
+ note_list[0][4].append((version, item, item_commit_list))
+
+ return note_version, re.sub(r"\.$", "", note_list[0][0])
+
+ ################################################################################################################################
+ def _release_item_render(self):
+ """Add the details of each item that is not a development item, with the development items that name it."""
+
+ for item_list in self.release_item_map.values():
+ for _, list_item, item, item_commit_list, dev_list in item_list:
+ self._detail_add(list_item, item, item_commit_list, dev_list=dev_list)
+
################################################################################################################################
def render_document(self):
"""Build the release document."""
@@ -520,6 +683,8 @@ class _Release:
self._release_render(release, release_section, version, commit_list, remaining_list, commit_map, check)
+ self._release_item_render()
+
return root
################################################################################################################################
@@ -536,15 +701,24 @@ class _Release:
continue
for item_type, item_title, item_name in _ITEM_LIST:
- # Development items are only listed on a development build, since they are of no interest to a reader
- if not self.dev and item_type == "release-development-list":
- continue
-
item_node = xml_node_child(type_node, item_type)
if item_node is None:
continue
+ # Development items are only listed on a development build, since they are of no interest to a reader. Their
+ # commits are still claimed when detail is true.
+ if not self.dev and item_type == "release-development-list":
+ if detail and check:
+ for item in xml_node_child_list(item_node, "release-item"):
+ item_commit_list = self._item_commit(
+ item, xml_node_child_list(item, "p")[0], version, commit_list, remaining_list, commit_map, True
+ )
+
+ self._release_note_add(item, version, item_commit_list)
+
+ continue
+
# A note about the release as a whole goes before the first list, whichever list that turns out to be
if not note and xml_node_child(type_node, "text") is not None:
xml_node_child_add(
@@ -562,7 +736,11 @@ class _Release:
for item in xml_node_child_list(item_node, "release-item"):
text = xml_node_child_list(item, "p")[0]
- item_check = check and item_type != "release-development-list"
+
+ # Plain text of the item before credits are added
+ text_plain = self.render.process_text(text) if detail else None
+
+ item_check = check and (detail or item_type != "release-development-list")
item_commit_list = None
if item_check or detail:
@@ -570,7 +748,7 @@ class _Release:
item, text, version, commit_list, remaining_list, commit_map, item_check
)
- # An item written as more than one paragraph reads as one line in the release notes when version < 2.60
+ # An item written as more than one paragraph reads as one line in the release notes when detail is false
if not detail:
for extra in xml_node_child_list(item, "p")[1:]:
xml_node_content_add(text, " ")
@@ -587,11 +765,70 @@ class _Release:
xml_node_child_add(list_item, text)
if detail:
- self._detail_add(list_item, item, item_commit_list)
+ if item_type == "release-development-list":
+ release_note = self._release_note_add(item, version, item_commit_list)
+ self._detail_add(list_item, item, item_commit_list, release_note=release_note)
+ else:
+ if xml_node_child(item, "release-note") is not None:
+ raise ToolError(
+ "release %s item '%s' is not a development item and cannot name a release note"
+ % (version, text_plain)
+ )
+
+ # Details are added after every release is read
+ self.release_item_map.setdefault(version, []).append(
+ [text_plain, list_item, item, item_commit_list, []]
+ )
+
+ self._backpatch_render(release, release_section, version, commit_list, remaining_list, commit_map, check)
+
+ # Every commit of the release is claimed by an item when detail and claim are true
+ if detail and check and self.claim and len(remaining_list) > 0:
+ self._commit_error("release %s has commits that no item claims" % version, remaining_list, commit_map)
+
+ ################################################################################################################################
+ def _backpatch_render(self, release, release_section, version, commit_list, remaining_list, commit_map, check):
+ """Render the commits of a release that were back-patched to a patch release, grouped by the version of the patch release.
+
+ The commits are claimed on every build and only listed on a development build."""
+
+ backpatch_list = xml_node_child(release, "release-backpatch-list")
+
+ if backpatch_list is None:
+ return
+
+ list_node = None
+
+ if self.dev:
+ heading = xml_node_add(release_section, "p")
+ xml_node_content_add(xml_node_add(xml_node_text_add(heading), "b"), "Back-Patched:")
+ list_node = xml_node_add(release_section, "list")
+
+ for backpatch in xml_node_child_list(backpatch_list, "release-backpatch"):
+ backpatch_version = xml_node_attribute(backpatch, "version", True)
+
+ if backpatch_version not in self.version_list or _VERSION_BUG_FIX_EXP.match(backpatch_version) is None:
+ raise ToolError("release %s back-patch version %s is not a patch release" % (version, backpatch_version))
+
+ if len(xml_node_child_list(backpatch, "commit")) == 0:
+ raise ToolError("release %s back-patch version %s has no commits" % (version, backpatch_version))
+
+ item_commit_list = self._item_commit(backpatch, None, version, commit_list, remaining_list, commit_map, check)
+
+ if list_node is not None:
+ list_item = xml_node_text_add(xml_node_add(list_node, "list-item"))
+
+ xml_node_content_add(list_item, "Back-patched to ")
+ xml_node_content_add(xml_node_add(list_item, "link", {"url": "#%s" % backpatch_version}), "v%s" % backpatch_version)
+ xml_node_content_add(list_item, ".")
+
+ self._detail_add(list_item, backpatch, item_commit_list, True)
####################################################################################################################################
-def release_render(root, path_doc, dev):
- """Build the release document from the release list."""
+def release_render(root, path_doc, dev, claim=False):
+ """Build the release document from the release list.
- return _Release(root, path_doc, dev).render_document()
+ When claim is true every commit of a release with version >= _DETAIL_VERSION_MIN must be claimed by an item."""
+
+ return _Release(root, path_doc, dev, claim).render_document()
diff --git a/doc/resource/html/default.css b/doc/resource/html/default.css
index 6a1ff2a88..edc4253b2 100644
--- a/doc/resource/html/default.css
+++ b/doc/resource/html/default.css
@@ -130,6 +130,9 @@ html
font-family: var(--font-body);
font-size: 16px;
-webkit-text-size-adjust: 100%;
+
+ /* Opening details does not scroll the page */
+ overflow-anchor: none;
}
body
diff --git a/doc/xml/release/2020s/2020/2.26.xml b/doc/xml/release/2020s/2020/2.26.xml
index ff95d2cba..8e7526dae 100644
--- a/doc/xml/release/2020s/2020/2.26.xml
+++ b/doc/xml/release/2020s/2020/2.26.xml
@@ -15,10 +15,9 @@
Non-blocking TLS implementation. Azure support for repository storage. Automatically retrieve temporary Improve memory usage of unlogged relation detection in manifest build. Add Improve performance of large file lists in Add Add Fix option warnings breaking async An option warning in the async process was logged to a closed stdout, which made the process abort. Since the async role can now have different valid options than the default role, this happened more often. Option warnings are now logged only for the default role. Fix memory leak in backup during archive copy. Copying WAL into the backup with Fix stack overflow in cipher passphrase generation. The buffer for the generated passphrase had no room for the terminating zero. This was found on arm64 with musl libc. Fix Multiple repository support. Up to four repositories may be configured, e.g. a local repository for fast restores and a remote repository for redundancy. GCS support for repository storage. Set Add The WAL header is checked against the Include recreated system databases during selective restore. A system database ( Exclude Some proxies modify the Consolidate less commonly used repository storage options. The Allow custom Packagers can set a different default for Add Move configuration definition to Improve storage drivers. Improve the configure script. Update help.auto.c missed in prior commits. Clean up code and comments. Update config.guess and config.sub to latest versions. Update reference to include links to user guide examples. The command reference now links to the user guide sections with examples, and some descriptions were clarified, e.g. Update selective restore documentation with caveats. Recovery may fail unless Add WAL must be recompressed when Add The defaults are 9 for Add note about required Fix option validity by role and make sections conform to C help. Improve the documentation build and release notes. Improve unit tests and test tools.HttpUrl object.
Fix issues with leftover spool files from a prior
A notification left in the spool path by a prior restore could make
Fix issue when checking links for large numbers of tablespaces.
+ +The link check used a lot of memory with many tablespaces and could crash. The new check is much faster, uses far less memory, and also detects duplicate file links.
Free no longer needed remotes so they do not timeout during
The connections to remotes were held for the whole restore and could time out, which logged a harmless but distracting warning at the end of a long restore. They are now closed after the manifest is loaded.
Fix
Add
Add automatic
Set
Add
Set the number of days of backup history to keep. By default all history is kept, and history for backups still in the repository is never expired. When a full backup manifest expires, the manifests of its differential and incremental backups expire as well.
Add
Restore excludes the specified databases by writing them as sparse, zeroed files.
Remove stanza archive spool path on
Files in the spool path could interfere with the new cluster, e.g. old
Do not write files atomically or sync paths during
Atomic writes are not needed since checksums are validated when a failed backup is resumed, and all paths are synced at the end of the backup.
Add local process shim.
@@ -231,6 +255,7 @@Simplify defaults for
Refactor modules with inline getters/setters.
+Clean up code and comments.
+Update config.sub to latest version.
+Update contributing documentation.
+ +The contributing guide has more examples of how to write code, add configuration options, and test.
Consolidate
The second guide existed mainly to document
Clarify that
Update the user guide and release documentation.
+Improve test harness macros.
+Improve unit tests and test tools.
+CI updates.
+Detect errors in
Completing a multi-part upload can fail even when
Fix detection of circular symlinks.
+ +Links were followed before they were checked, and a circular link sent the manifest build into endless recursion and a crash.
Only pass selected
Options for other repositories could cause conflicts on the remote, which uses exactly one repository.
Binary protocol.
+ +Local and remote processes now communicate with the binary pack format rather than JSON. Packs are more compact, faster to render and parse, and strictly typed. Each message is written in one place, which makes an error less likely to leave the protocol in a bad state, and messages have no size limit.
Automatically create data directory on
A check migrated from Perl prevented restore from creating the data directory.
Allow
Set
Change level of
This makes the
Loop while waiting for checkpoint LSN to reach replay LSN.
+ +On a standby the checkpoint LSN can lag slightly behind the replay LSN until
Log
The backup size is now logged at the end of the backup and includes files added to the manifest after the main copy, e.g.
Clear error when a CATCH() block finishes.
Migrate config and error code generation to C.
+Fix miscalculation in time option command defaults.
+Refactor local/remote startup.
+Fix infinite loop in protocolServerProcess() on error.
Add temp mem context reset blocks in job processing loops.
+Clean up code and comments.
+Update config.guess and config.sub to latest versions.
+Fix incorrect host names in user guide.
+ +Some examples showed
Update contributing documentation and add pull request template.
+ +The contributing guide now has a table of contents and sections on understanding test output and submitting a pull request. GitHub now shows a template for new pull requests.
Rearrange backup documentation in user guide.
+ +The Fast Start Option
and Archive Timeout
sections moved to Quick Start - Perform Backup
. The Automatic Stop Option
section, which applies only to
Clarify
The reference now states that
Fix documentation and comment typos.
Add optional stress test section to the user guide.
+Add linefeeds around content even when not in pretty mode.
+Add check for test path inside repo path.
+ +A test path inside the repository path caused strange failures because the repository is copied into the test path.
Add CodeQL static code analysis.
+ +CodeQL now runs in CI. Issues it found were fixed, e.g. gmtime()/localtime() were replaced with gmtime_r()/localtime_r().
Update tests to use standard patterns.
Improve test harness macros.
+Improve unit tests and test tools.
+CI updates.
+Allow global
as a stanza prefix.
A stanza name that begins with
Fix segfault on invalid
A
Allow
Increase max index allowed for
The maximum index was 8 for
Add
Set
Add
Set
Report backup file validation errors in
Errors found during a backup, e.g. invalid page checksums, were shown only by
Add recovery start time to online backup restore log.
+ +Restore now logs the time that recovery will start from, which shows how much WAL must be replayed.
Report original error and retries on local job failure.
+ +When a local job fails after its retries, the error now reports the first error with its stack trace, followed by the retry errors. Before, only the last error was reported, which was often a cascade failure such as the protocol being out of sync.
Rename page checksum error to error list in info text output.
+ +The label in error list
rather than page checksum error
. A file may have an alignment error rather than a page checksum error.
Add hints to standby replay timeout message.
+ +The timeout waiting for the standby to replay now has hints to check that replication is running and to disable
Add
Store config values as a union instead of a variant.
Migrate command-line help generation to C.
+Improve the Pack type and use it for filters and configuration rules.
+Simplify configuration rules.
+Improve memory allocation and mem context handling.
+Add configurable storage helpers to create repository storage.
+More efficient generation of diff/incr backup label.
+Add -Werror -Wfatal-errors -g flags to configure --enable-test.
+Add required header for Illumos.
+Clean up code and comments.
+Finalize catalog number for PostgreSQL 14 release.
+Update config.guess and config.sub to latest versions.
+Fix incorrect host name in user guide.
+ +The replication setup used the IP address of
Improve option documentation.
+ +The help for
Eliminate linefeed formatting from documentation.
Update the user guide and documentation build.
+Add analytics to the HTML documentation.
+Update the index page, README.md, and contributing documentation.
+Improve unit tests and test tools.
+CI updates.
+Fix
When
Fix socket leak on connection retries.
+ +In optimized builds the socket of a failed connection attempt was not closed, and each retry leaked a socket.
Add
The
Add
Set
Check archive immediately after backup start.
+ +A backup failed because WAL archiving was not working only after all files were copied. The archive is now checked right after the backup starts. On an idle system a restore point is created to switch the WAL segment.
Add timeline and checkpoint checks to backup.
+ +The backup now checks that
Check that clusters are alive and correctly configured during a backup.
+ +A backup now fails as soon as a cluster stops or the standby is promoted. Before, a stopped primary was detected only at the end of the backup and a promoted standby was not detected.
Error when
This was a warning that was easy to miss, and a lot of time could be lost restoring a backup that cannot reach the target.
Parse protocol/port in
Add warning when
With
Make backup size logging exactly match
The logged backup size now includes only the files copied, plus
Add StringId as an option type.
Do not delete manifests individually during stanza delete.
+Add error type and message to CHECK() macro.
+Clean up and refactor code.
+Update LICENSE.txt and PostgreSQL copyright for 2022.
+Remove obsolete statement about future multi-repository support.
+ +The documentation stated that only one repository was allowed, but multiple repositories have been supported since v2.33.
+ +Update the user guide.
+Update the release notes and RELEASE.md.
+Improve unit and performance tests.
+Retry errors in S3 batch file delete.
+ +A failed batch delete was retried, but errors for individual files in the batch were not, and
Allow case-insensitive matching of
The HTTP specification allows
Add support for
Set
Add
By default
Add backup type filter to
Set
Retry on page validation failure during
A page that fails checksum validation is now read again. It is reported as an error only when it has not changed. A changed page is being written by
Handle
Some servers close the socket rather than the TLS connection. When a response has no content length or chunked encoding and its content can be parsed, the closed socket is now treated as EOF. These servers stopped working in v2.35.
Add backup LSNs to
The start and stop LSNs of a backup are shown with
Automatically strip trailing slashes for
A trailing slash on the path made
Do not retry fatal errors.
+ +Retrying fatal errors, especially out of memory errors, may cause lockups. Assertion and memory errors are now reported without retrying.
Remove support for
There is no evidence that these versions are still in use and supporting them costs development and test time. Use an older version of
Remove logic that tried to determine additional file system compression.
+ +After a file was copied,
Add percent complete to lock file.
+Refactor lock code.
Configuration refactoring.
Add XML to invalid XML error message.
+Fix invalid file descriptor being passed to close().
+Add ioCopy().
+Clean up code and comments.
+Update config.guess and config.sub to latest versions.
+Move
The user guide set these options in the stanza section, which works in some cases but is not best practice.
Remove unused
Add aliveness check to
The
Add FAQ explaining WAL archive suffix.
+ +The suffix on a WAL file in the repository is its SHA1 checksum, which is used to verify the file. It cannot be omitted.
Note that replications slots are not restored.
Update the user guide.
+Improve the documentation build.
+Update the release notes and NEWS.md.
+Improve unit tests.
+CI updates.
+Fix error thrown from FINALLY() causing an infinite loop.
An error thrown in a FINALLY() block returned to the same block, where the error was likely thrown again and caused an infinite loop. FINALLY() now runs only once.
Error on all lock failures except another process holding the lock.
+ +Backup file bundling for improved small file support.
+ +Bundling stores small files together in the repository to save time and space during backup, especially on object stores where creating each file is expensive. Enable it with
Verify command to validate the contents of a repository.
+ +The
Show backup percent complete in
Auto-select backup for
With
Suppress existing WAL warning when
With
Add
Allow
Changing
Increase precision of percent complete logging for
Percent complete is now logged with hundredths, which gives more frequent updates for large backups and restores.
Improve path validation for
A path may not contain
Improve
Improve error message for invalid
With
Add hint to check the log on
When the async process cannot write its status files,
Strip extensions from history manifest before showing in error message.
+ +When a new backup label was not later than the latest label from the backup history, the error showed the history manifest name, e.g.
Add user:group to lock permission error.
Add hint when unable to find the primary cluster.
+ +The unable to find primary cluster
error now includes a hint asking whether all available clusters are in recovery.
Refactor PgClient to return results in Pack format.
Fix undefined behavior found by sanitizers.
+Fix memory leaks and improve memory context usage.
+Add and improve macros.
+Add and improve string functions.
+Refactor remote storage protocol to use Packs instead of JSON.
+Fix reported error line number when ini key length is zero.
+Clean up code and comments.
+Update config.guess and config.sub to latest versions.
+Fix example for
The example was
Fix
The example was missing the client CN and is now
Simplify messaging around supported versions in the documentation.
+ +The user guides listed
Add option type descriptions.
+ +The configuration reference now describes each option type, e.g. the units for size options, rather than repeating the description for each option.
Add FAQ about backup types and restore speed.
+ +A new FAQ explains that the backup type affects only the backup and not how fast a restore runs.
Document required base branch for pull requests.
+ +The contributing guide now states that pull requests must be based on the
Fix typos and formatting in the documentation and help.
+Remove integration expect log testing.
+ +Improve unit tests and test tools.
+CI updates.
+Create snapshot when listing contents of a path.
+ +A path listed without sorting was read while its entries were processed. When files were deleted during the listing, some filesystems skipped files that still existed and the path could not be removed. Listings now always use a snapshot of the path.
Force
On
Truncate files during delta
Delta restore copied a file from the repository whenever its size differed from the manifest. A larger file is now truncated to the size in the manifest and copied only when its checksum does not match. Files that were only appended to, e.g. logs, are no longer copied.
Disable incremental manifest save when
During a backup the manifest is saved periodically for use by resume. These saves are now skipped when
Set backup percent complete to zero before copy start.
+ +The progress of a running backup was not reported until the first file was copied. It is now set to zero when copying starts.
Use
Recent versions of
Fix hard-coded WAL size assertion in walSegmentNext().
Add labels in parse.auto.c to make diffs easier to read.
+Fix repository option definitions for the backup and stanza commands.
+Use storagePathP() instead of cfgOptionStr() to get base repo path.
+Add additional detail to warnings when delta checksum is auto-enabled.
+Clean up code and comments.
+Replace AC_PROG_CC_C99 configure macro with AC_PROG_CC.
+Update config.guess to latest version.
+Skip internal options in the configuration reference.
+ +Options that are internal only for some commands were listed in the configuration reference. The command-line help was not affected.
Add link to
Fix RHEL container build for documentation.
+Update release notes.
+Add experimental
Fix compile error when DEBUG_EXEC_TIME is defined without DEBUG.
Without DEBUG the ASSERT() that used the start time expanded to nothing, and the unused variable caused a compile error. The check now uses CHECK().
Add experimental unit test harness written in C.
+Improve unit and integration tests.
+CI updates.
+Fix incorrect time expiration being used for non-default repositories.
+ +When
Fix issue when listing directories recursively with a filter.
+ +Backup key/value annotations.
+ +Add key/value annotations to a backup with the
Support
With
Allow upload chunk size to be configured for object stores.
+ +Set
Update archive.info timestamps after a successful backup.
+ +Move standby timeline check after checkpoint.
+ +The check that the standby is on the expected timeline used standby is on timeline ... but expected ...
. The check now runs after the checkpoint.
Improve warning message on backup resume.
+ +The warning read as if the user needed to take action. It now states that invalid files will be removed and then the backup will resume.
Add FN_INLINE_ALWAYS macro.
Clear ProtocolClient callback after connection error.
+Improve the code builder.
+Update config.guess to latest version.
+Add absolute path for
The
Allow host memory limits in the user guide to be disabled.
+Add missed reviewer to the release notes.
+Build unit and performance tests with the C test harness.
+Improve unit tests and test tools.
+Update Minio test/documentation container version.
+CI updates.
+Fix memory leak in file bundle
Restore freed some I/O objects for bundled files only after every file in the bundle was restored. With many small files in a bundle, memory use grew until restore failed with unable to allocate
. Backup had a similar, smaller leak when resuming.
Fix protocol error on short read of remote file.
+ +A remote file read that stopped early or failed partway left the protocol in a bad state. The next command failed with client state is 'data-get' but expected 'idle'
and the original error was lost. The rest of the file is now read when the file is closed.
Do not store references for zero-length files when bundling.
+ +Bundled diff/incr backups stored a reference to a prior backup for zero-length files, which are not stored in any backup. The reference was ignored on restore but used space in the manifest.
Use more generic descriptions for pg_start_backup()/pg_stop_backup().
pg_backup_start()/pg_backup_stop(). The logs and documentation now use descriptions that do not depend on the
Block incremental backup.
+Block incremental backup groundwork.
Move link creation to storage interface.
Do not log bundle info when a file is delta matched from a prior backup.
+Return binary as result from CryptoHash filter.
+Clean up code and comments.
+Finalize catalog number for PostgreSQL 15 release.
+Update config.guess and config.sub to latest versions.
+Add missed release note for v2.41.
+Update
The help showed
Improve unit tests and test tools.
+CI updates.
+Fix missing reference in diff/incr backup.
+ +In v2.42 a diff or incr backup failed with unable to find '...' in string list
when its prior backup was made by an earlier version. The reference list built for a manifest from an earlier version now includes the backup's own label.
Add hint when an option is specified without an index.
+ +An option that requires an index, e.g.
Update config.guess and config.sub to latest versions.
+Remove support for
Restore errors when no backup matches the current version of
After a
Add
The
Refactor
Audit memory contexts returned from functions and fix leaks.
+Improve libbacktrace implementation.
+Fix error detail output to stderr.
+Improve debug, check, and buffer macros.
+Clean up code and comments.
+Update LICENSE.txt and PostgreSQL copyright for 2023.
+Add warning about enabling hierarchical namespace
on Azure storage.
With hierarchical namespace
enabled, Azure requires directories to be deleted separately and
Add replacement for linefeeds in monitoring example.
+ +The monitoring example failed when COPY converts
Clarify
The effect of
Updates and clarifications to index page.
+ +The section about other backup solutions not supporting parallel processing was no longer accurate and has been reworded. Other sections were updated for current terminology and clarity.
Add dark mode to the website.
+ +The website uses dark colors when the browser prefers a dark color scheme.
Update RHEL user guide to PostgreSQL 11/12.
+Improve unit tests and test tools.
+CI updates.
+Skip writing
On
Block incremental backup (BETA).
+ +Block incremental stores only the parts of a file that changed since the prior backup instead of the entire file. Enable it with
Keep only one all-default group index.
+ +An option that does not apply, e.g.
Add
Add internal
Use uncrustify for code formatting.
+Improve error handling in build-code binary.
+Improve interface handling in storage module.
+Clean up code and comments.
+Update config.guess and config.sub to latest versions.
+Add explicit instructions for upgrading between
A new user guide section explains that upgrading between
Remove references to
The help for
Fix contributors in release notes.
+Improve unit tests.
+Block incremental backup.
+ +Block incremental stores only the parts of a file that changed since the prior backup, which saves space and time in diff and incr backups of large files. Enable it with
Set
Adds the catalog version and WAL magic number for
Allow page header checks to be skipped.
+ +The page header checks made while verifying page checksums can report false errors for encrypted pages. Set
Avoid chown() on recovery files during restore.
Restore skipped chown() for restored files that already had the right owner but not for the recovery files it writes. The chown() when the owner is already correct.
Add error retry detail for HTTP retries.
+ +An error after HTTP retries now reports how many retries were made and for how long.
Improve internal
Improve object and allocation handling.
+Refactor the lock and configuration load modules.
+Sleep using nanosleep() instead of select().
+Autogenerate PostgreSQL versions.
+Update modules to recent coding standards.
+Remove double spaces and extraneous whitespace.
+Add warning about using recovery
Recovery
Add note about running
Stanzas that already exist are skipped, so
Update the user guide.
+Improve unit and integration tests.
+CI updates.
+Preserve block incremental info in manifest during delta backup.
+ +When a file changed so that it would not use block incremental if it were new, e.g. it shrank below the block incremental limit or its timestamp moved far enough into the past, its block incremental info was lost from the manifest of a delta backup even though the file could still be referenced to the prior backup.
Fix block incremental file names in
The
Fix spurious automatic delta backup on backup from standby.
+ +File sizes on the standby can differ from the primary, e.g. for free space maps, so the next incremental backup could see a size change on the primary and switch to a checksum delta, which can take much longer. The original size is now kept in the manifest and used for the comparison.
Skip
With
Fix unique label generation for diff/incr backup.
+ +With at least two full backups and the latest expired, a diff or incr backup failed as if the clock had gone backward, because its label was compared with the label of the expired full backup.
Fix time-based archive expiration when no backups are expired.
+ +When time-based retention was met but no backups were expired, archive was not expired and time-based archive retention not met
was logged. One more backup was then needed to expire the earlier WAL.
Improve performance of
The
Add timezone offset to
Timestamps in the
Centralize error handling for unsupported features.
+ +Errors for features that were not compiled in, e.g.
Use new LibXML error handler.
+Fix missed memory auditing in FUNCTION_LOG_RETURN_VOID().
+Increase protocolServerError() logging to debug level.
+Log when file size changes during backup.
+Improve comments.
+Clarify preference to install from packages in the user guide.
Update the user guide, feature list, and NEWS.md.
+Build u20 image to speed contributing document generation.
+Improve unit tests.
+CI updates.
+Fix issue restoring block incremental without a block list.
+ +A block map with no blocks, which is written when a file timestamp changes but the file does not, was not decrypted when it followed a file stored without block incremental in the same bundle read. With enough affected files the retries were exhausted and the restore failed.
@@ -30,6 +31,8 @@Add
Set
Add known hosts checking for
Support for dual stack connections.
+ +Each address a host name resolves to is now tried in turn, e.g. an IPv6 address with no route to the host and then an IPv4 address, until one connects or the timeout expires. Repeated retry errors are also summarized in error messages.
Add backup size completed/total to
The JSON output of the
Multi-stanza check command.
+ +When
Retry reads of
On some filesystems, e.g. ext4, a read of
Optimize WAL segment check after successful backup.
+ +The check for WAL segments after a backup listed the storage once for each segment. Each WAL directory is now listed once and the list is reused.
Improve
Chunks of a multi-part upload are now uploaded as soon as they are available.
Allow
The
Accept leading tilde in paths for SFTP public/private keys.
+ +Paths for
Reload GCS credentials before renewing authentication token.
+ +The service key file is now read again before the authentication token is renewed, so the key can be replaced while a command is running.
Add report option to
Update config.guess and config.sub to latest versions.
+Update xxHash to v0.8.2.
+Clean up code.
+Finalize catalog number for PostgreSQL 16 release.
+Add prefix to meson build target to avoid conflicts.
+Fix manifest command filter for files in a tablespace.
+Add block incremental info to restore detail logging.
+Fix configuration reference example for the
The example
Fix command reference example for the
Split release notes into individual files.
+Update help title for the --annotation option.
+Allow
Improve unit tests.
+CI updates.
+Fix regression in retries.
+ +v2.48 dropped the retry that was made when an operation used up its whole timeout, so a timeout became an error, e.g. waiting for a socket to be writable on an object store. Two retries are now made even when the timeout has expired.
Fix recursive path remove in
Some
Remove support for
Fix storageReadRemote() to return actual read bytes.
Update config.guess and config.sub to latest versions.
+Clean up code.
+Document maintainer options.
+ +Maintainer options are now documented in their own section with the caveats for using them, and
Update point-in-time recovery documentation for
Since
Build command and configuration reference in C.
+ +Update the site introduction and the user guide.
+Update Minio test/documentation container version.
+Remove unused fields from backupJobResult() test in command/backup unit.
Improve unit tests.
+Add test to show behavior of bundled files truncated during backup.
+Fix short read in block incremental restore.
+ +A compressed super block can end with bytes that are not needed to decompress it but mark the end of the data. Depending on how a buffer lined up with the data, these bytes were not read and the restore failed.
Fix overflow suppressing backup progress in
The sizes in the lock file were read as 32-bit values, so the
Preserve partial files during block incremental delta restore.
+ +A file smaller than expected was not kept for a block incremental delta restore, although its blocks can be reused, e.g. when retrying a restore that failed or restoring over a smaller file.
Add support for alternate compile-time page sizes.
+ +Skip files truncated during backup when bundling.
+ +A file truncated to zero after the files were assigned to bundles was still stored in its bundle, and a debug build failed an assertion. These files are now skipped.
Refactor backup incremental manifest generation.
Refactor backup and retry code.
+Improve logging of file size changes and resume warnings during backup.
+Fix comments.
+Update LICENSE.txt and PostgreSQL copyright for 2024.
+Improve unit tests.
+CI updates.
+Skip zero-length files for block incremental delta restore.
+ +An existing zero-length file caused a block incremental delta restore to fail in the block checksum filter. These files are now skipped, since there is nothing in them to reuse.
Fix performance regression in storage list.
+ +Listing a path requested detailed information for each file, which could be slow, e.g. on object stores. Only the information the list needs is now requested.
Improved support for dual stack connections.
+ +When a host name resolves to more than one address, e.g. IPv4 and IPv6, the next address is tried once the prior address has had reasonable time to connect, following the happy eyeballs
approach of RFC 8305. A connection attempt that neither fails nor connects no longer uses up all the available time.
Make meson the primary build system.
+ +Detect files that have not changed during non-delta incremental backup.
+ +During a non-delta diff or incr backup, a file that fits in a single buffer, or any block incremental file, is compared to the file in the prior backup and referenced instead of stored when it has not changed.
Prevent invalid recovery when
When
Improve
The size of every WAL segment in the queue was read to measure the queue. The size of the first segment is now used for all of them.
Limit resume functionality to full backups.
+ +Resume did not work correctly for delta diff and incr backups, and a file that changed since the prior backup is likely to change again before a resume. Only full backups are resumed now.
Update resume functionality for block incremental.
+ +Resume removed block incremental files, so it worked but copied all of them again. Block incremental files are now kept on resume.
Allow
Add detailed backtrace to
Error stack traces in the
Improve sort comparators.
+ +Fix help not displaying help for the help command.
+Update config.guess and config.sub to latest versions.
+Improve asserts.
+Validate more fields in
Redact secure options in check --report.
+Update references to
References to
Migrate man page generation to C.
+Add release instructions to update PostgreSQL ecosystem wiki.
+Migrate integration tests to C.
+Improve unit tests and test tools.
+CI updates.
+Fix issue with files larger on the replica than on the primary.
+ +With block incremental, when a file was a different size on the primary than on the standby during a backup from standby, the next diff or incr backup stored the size from the primary. The backup after that failed with repo size must be > 0
.
Add
Files are removed with batch requests instead of one request per file, which speeds up
Set
Adds the catalog version and WAL magic number for
Allow explicit disabling of optional dependencies in meson builds.
+ +An optional feature can be disabled in the
Dynamically find
The
Update Fedora test image to Fedora 38.
Update modules to recent coding standards.
+Silence Coverity complaints.
+Clean up code and comments.
+Update
Update the contributors, release notes, and metrics report.
+Improve unit tests.
+CI updates.
+Fix permissions when
When
Fix segfault on delayed connection errors.
+ +A connection error that was delayed long enough to be reported on a later wait could cause a segfault.
Skip local repository duplicate check for
The check for local repositories that are configured more than once was also applied to
Do not allow help for command roles other than main.
+Fix typo in user guide.
+Fix
Some
Allow backups to run concurrently on different repositories.
+ +Backups of the same stanza can now run at the same time on different repositories, since locks are now per stanza and repository instead of per stanza. This is not possible for
Support IP-based SANs for
Certificates with an IP address as the subject alternative name, e.g.
Default
Warnings and errors written to stderr were often lost when only stdout was redirected while collecting logs. An explicitly set
Allow alternative WAL segment sizes for PostgreSQL ≤ 10.
+ +Add hint to check
When
Display time option defaults and allowed values with appropriate units.
+ +Time option defaults and allowed values were shown in seconds, which was hard to read for large values. Fractional seconds are no longer allowed, so a value such as
Require
Require the lz4 library and compiler builtins in the meson build.
+Improve option units, defaults, and error messages.
+Improve option and command parsing.
+Improve reading lock files in the
Refactor the backup and manifest code.
+Fix formatting.
+Clarify
When one repository is failing,
Update the contributing documentation and the documentation and test tools.
+Migrate coverage testing to C and remove dependency on lcov.
+CI updates.
+Fix
The query timeout logic delayed reading query results, which could make a query that returned a large result take up to
Allow repositories on versioned storage to be read at a target time.
+ +The
Allow requested standby backup to proceed with no standby.
+ +Set
Summarize backup reference list for
The backup reference list can be very long, so the text output of the
Refresh web-id token for each
The token file named by
Correctly display current values for indexed options in help.
+ +The current value shown for an indexed option was always the value for the first index, e.g.
Save
Remove limitation on reading files in parallel during restore.
+ +The protocol could only read or write one file at a time on a remote, so a restore read one file at a time from the repository. Protocol commands can now be multiplexed, so a restore can read more than one file at a time, e.g. a bundle and the block incremental data for a file in it.
Improve
Protocol command multiplexing.
+Refactor config parsing and the help and version commands.
+Refactor the storage and protocol modules.
+Finalize catalog number for PostgreSQL 17 release.
+Retry socket bind.
Add performance tuning section to user guide.
+ +The options that commonly improve performance were spread through the documentation. The quickstart now has a section that lists them.
@@ -161,6 +218,8 @@Clarify source for
Better logic for deciding when a summary should be lower-cased.
+ +Summaries that start with a proper name or an acronym, e.g.
Update documentation.
+Improve documentation build.
+Improve test harness and unit tests.
+CI updates.
+Fix issue with
In v2.54.0 the
Exclude logging macros from coverage reporting and fix volatile qualifiers.
+Update documentation and release notes.
+Stabilize async archiving in integration tests.
+ +An async
Update CI and integration tests.
+Fix issue after disabling bundling with block incremental enabled.
+ +When bundling was disabled after backups were made with bundling and block incremental enabled, the prior backups could not be read. The setting needed to read them is now preserved.
+ +Update LICENSE.txt and PostgreSQL copyright for 2025.
+Clarify behavior of multiple configuration files.
+ +Each configuration file must be valid on its own, so a section must be specified in each file that sets an option in it. The documentation also notes that quoting is not supported, whitespace is trimmed from keys and values, and repeated sections are merged.
+ +Clarify that unhandled errors may occur in edge cases.
+Update CI and integration tests.
+Fix block incremental restore issue on non-default repository.
+ +When the backup being restored used block incremental and was not in the lowest numbered repository, the restore read part of the block incremental data from the lowest numbered repository. Setting
Do not set
Before
Fix expire archive range logging.
+ +Only the last archive path removed was logged as the start of the range, so the log suggested that far fewer files were expired than were.
Fix error reporting for queries with no results.
+ +An error from a query that expected no results was reported as an unexpected result instead of as the error.
Verify recovery target timeline.
+ +A backup that cannot follow the target timeline, e.g. one made after a standby was promoted and the new timeline forked off, failed only when recovery started, with a
Allow verification of a specified backup.
+ +The
Add support for S3/GCS requester pays.
+ +Set
This release was made before
Allow connections to
Add numeric output to
Set
Allow backup command to operate on remote repositories.
+ +The
Use
Calculate
The
Warn when a value for a multi-key option is overwritten.
+ +Options with key/value pairs, e.g.
Add detail logging for expired archive path.
+ +Removing a large number of archive files could look like a hang. A message at detail level now shows progress.
Remove support for
Remove
v2.54.0 was the last release to support the
Remove extraneous const from cvtIntToZ() prototype.
Enable additional compiler warnings.
+Improve logging macros and their coverage.
+Allow negative values for integer options.
+Refactor the compression and protocol modules.
+Update LICENSE.txt and PostgreSQL copyright for 2025.
+Separate version into component parts.
+Add StringId linter.
+Refactor code to satisfy Coverity.
+Fix typos.
+Clean up code.
+Fix documentation for specifying multiple stanzas with
Multiple stanzas are specified for
Clarify incremental backup expiration.
+ +The documentation implied that incremental backups are expired by count. It now explains how incremental backups are expired and that full backups count as differential backups for differential retention.
Add FAQ about exporting self-contained cluster.
+ +The FAQ explains how to create a self-contained export of a cluster with a backup made with
Caveat
A tablespace created after the backup starts is not in the manifest, so
Change
On object stores
Update
Recommend not placing
A spool path inside
Fix instances of of of
.
Update and correct the documentation.
+Fix tests, logging, and comments in unit tests.
+ +CI updates.
+Improve the integration and unit tests.
+Revert calculate content-md5 on S3 only when required
.
Fix lower bounds checking for option keys.
+ +An indexed option with a key of 0, e.g.
Add details about notifying packagers to release instructions.
+Fix issue with adhoc expiration when no backups in a repository.
+ +Adhoc expiration failed with cannot get index 0 from list with 0 value(s)
when a repository had no backups.
Add restore progress to
The
Add progress-only detail level for
Set
Retry failed reads on object stores.
+ +A read from an object store that failed after it started was fatal, which could restart an expensive job, e.g. during restore. A failed read is now retried from where it left off.
Fix defaults in command-line help.
+ +When an option has more than one default, e.g.
Improve the predictability of floating point numbers formatted as strings.
+ +Improve option defaults and dependencies.
+Clean up code.
+Describe discrete option values in a list where appropriate.
+ +The values of options such as
Fix less than
in help output for
Update the contributing, release, and support documentation.
+CI updates.
+Improve unit tests.
+Unnest HTTP/TLS/socket timeouts.
+ +A failed socket connection was also retried by the TLS and HTTP layers, so with the default
Fix possible segfault in page checksum error message.
+ +The error message for invalid page checksums used a string that was out of scope, which could log garbage or crash. This was most likely with
Fix possible segfault in backup resume messages.
+ +The message that reports why a backup cannot be resumed used a string that was out of scope, which could log garbage or crash.
Add
Some filesystems that are otherwise
Add HTTP retries for 408 and 429 errors.
+ +HTTP 408 (Request Timeout) and 429 (Too Many Requests) errors are now retried. 429 errors are common on
Finalize catalog number for PostgreSQL 18 release.
+Remove unused code and fix comments.
+Update user guide and tests to Ubuntu 22.04.
+CI updates.
+Clean up unit tests.
+Fix deadlock due to logging in signal handler.
+ +A signal that arrived while a lock was held by gmtime_r(), e.g. the SIGTERM sent by
HTTP support for
An object store endpoint can now begin with
Allow expiration of oldest full backup regardless of current retention.
+ +The
Support for
Set
Experimental support for
Set
Allow configuration of TLS cipher suites.
+ +Set
Allow process priority to be set.
+ +Allow dots in S3 bucket names when using path-style URIs.
+ +Dots in a bucket name only cause a certificate host name mismatch with host-style URIs, so they are now allowed with
Require TLS >= 1.2 unless verification is disabled.
+ +TLS 1.0 and 1.1 have been deprecated since 2021. Verification is always enabled for connections between
Dynamically size
The number of chunks in an upload is limited, so with the default
Optimize
The whole chunk buffer was allocated when a file was opened, although many files are smaller than a chunk. The buffer now grows as data arrives, which saves memory when small files are uploaded.
Remove support for
Improve logging of default for options with an unresolved dependency.
+ +An option set in the configuration file was logged at the start of a command when its dependency was not resolved, although its default value was used. The option is no longer logged in this case.
Simplify the versioned
Use get_option() for tasks supported by the new meson version.
+Simplify the option definitions in config.yaml.
+Use JsonRead for all info JSON parsing.
+Refactor StringId and option parsing.
+Fix logging, formatting, and comments.
+Remove explicit
Clarify that bundling is useful for filesystems with large block sizes.
+ +Bundling saves space on filesystems with large block sizes, where each small file takes a whole block.
Improve the documentation build.
+Improve wording in the user guide.
+Fix WAL segment numbering in command/expire unit test.
+CI updates.
+Fix
A backup is not added to
Fix
In asynchronous mode the queue was measured against the WAL yet to be pushed. When a WAL file errored repeatedly the WAL files after it were pushed and no longer counted, although they stayed in
Fix async
Two async processes could race for the archive lock, and the one that lost exited before fetching the segment it was started for. The
Fix potential buffer overrun in error module.
+ +A stack trace too long for its buffer was not terminated and could be read past the end of the buffer, and an error message too long for its buffer could write past the end of it when a system error was appended. Both require an unusually long message.
Fix use-after-free of parallel protocol client.
+ +A client that ran out of work was freed but left in the client list and freed again on a later call. With debug or trace logging this read freed memory and could crash, e.g. a parallel backup with
Fix heap overflow when a pg option is set without pg1.
+ +When
Fix crash when a signal arrives during exit on musl 1.2.6.
+ +musl 1.2.6, e.g. on exit() is called while an exit is already in progress. This happened when a signal arrived during exit, e.g. when the
Adds support for
Add
The
Add batch delete for
Files are removed with the
Add support for
Set
Add
Set
Provide a distribution tarball with generated code and documentation.
+ +The tarball includes the generated code and the rendered man page and HTML documentation, so it builds and installs without the code generation and documentation tooling, including
Reconnect SFTP storage after the server drops an idle connection.
+ +Add user/group caching for faster manifest build.
+ +When user and group names are looked up through a remote name service, e.g.
Add per-repo backup progress to
When backups run on more than one repository at once, the
Add
The
Allow the
Set
Add
In asynchronous mode
Exit async
When the repository was unavailable the async process kept trying every ready WAL segment, so a run could take long enough for
Harden HTTP chunked response parsing.
+ +Chunk extensions and trailers are now accepted,
Error when running as root unless
Running a command other than
Fewer binary searches when building manifest.
+ +The manifest build for a diff or incr backup searched the prior manifest three times for each file. It now searches once, which saves millions of searches for clusters with millions of files.
Improve seek performance during block incremental delta restore.
+ +A seek and a flush of the write buffer were done for every block written during a block incremental delta restore, and blocks are often 8KiB. The file position is now tracked and the seek and flush are only done when needed.
Bundle backup files in order rather than scanning for a better fit.
+ +A file that would push a bundle over
Report the underlying error when a query fails to complete.
+ +When the connection was lost after the first result of a query was delivered, e.g. under load, the error was the generic NULL result required to complete request
. The error from
Report the actual error when an
An error from
Report
When the async spool path could not be created, e.g. permission was denied, the error could not be written to the spool and
Hint that pgBackRest may be out of date when a version is unsupported.
+ +When
Add
When built with
Suppress
The main() parameters they did not use, which failed the probes when unused parameter warnings were errors.
Refactor restore module into included modules.
Update LICENSE.txt and PostgreSQL copyright for 2026.
+Enhance StringId with optional sequence information.
+Refactor the storage read and write interfaces.
+Improve error handling in the
Refactor the manifest module into included modules.
+Reverse sort order in backup and restore comparators.
+Free memory sooner in restoreFile().
Move archiveAsyncExec() to command/command module and rename.
+Ban unsafe string functions and limit an oversized log message.
+Improve code generation for the configuration and help.
+Improve compiler compatibility and build checks.
+Clean up code and comments.
+Make
The
Document that info reads encryption settings from global section.
+ +When run without
Document that
Automatic expiration after a backup uses the configuration of the
Document adjusting the
Packages may install a
Document that end-of-line comments are not supported in config files.
+ +End-of-line comments have never been supported, although they may appear to work for some options. Only comments on a line of their own are supported.
Add the news page and sponsors to the website.
+Update and correct the documentation.
+Improve the documentation build.
+Replace Minio with Ceph for S3 documentation and testing.
+Fix Alpine group conflicts in CI containers.
+ +Group entries with members are now handled when the test group is renamed, and the
CI updates.
+Add a smoke test to verify a build against installed PostgreSQL.
+Improve the test harness.
+Improve unit tests.
+Fix hang when the chunk buffer is smaller than the input buffer.
+ +A multi-part upload to
Compare a new backup label only against its own full backup set.
+ +Some object stores keep listing a prefix after everything under it has been deleted, e.g. new backup label ... is not later than latest backup label ...
. A new label is now compared only against the backups in its own full backup set.
Fix source archives generated by GitHub missing files required to build.
+ +The source archives
Document exception for dots in
With
Remove explicit
Fix broken release link in news.xml.
+Fix empty block map when file is truncated to zero during backup.
+ +A block incremental file larger than unexpected eof
until the next full backup.
Disable read retry when filters are passed to the driver.
+ +A retried read from a repository on an object store, accessed through a repository host, crashed. These reads are no longer retried by the client. The repository host still retries its read from the object store.
Fix pagination of
Reading a repository at a target time with unable to find child 'NextContinuationToken'
when more than a thousand versions were listed under a prefix.
Set the
Batch deletes require the version header, so with
Fix async
A zero-size backup history file, e.g. in a cluster copied from a storage snapshot, failed every asynchronous
Fix
When the repository was encrypted and the cipher was configured only on the repository host,
Fix possible checksum error loading encrypted info and manifest files.
+ +A large encrypted info or manifest file could lose its last lines when loaded, which failed the checksum. The lost lines had to fit in the final cipher block, so this is unlikely in practice.
Fix possible weak encryption subkeys/salts.
+ +When
Add
This option applies to the
Support for
Prefetch multi-file/range reads from object stores.
+Set gcloud iam workload-identity-pools create-cred-config. The web identity token is exchanged for an access token without a service account key. The token file is reloaded on each exchange to pick up rotated tokens, e.g.
Combine prior block incremental map reads during backup.
+Prefetch multi-file/range reads from object stores.
+ +Requests are queued ahead of the current read so requests proceed while reading, and small gaps between files are read over instead of starting a new request.
Reduce keep-alive requests to remote processes.
+ +A keep-alive was sent to every remote each time a job completed in
Retry
When
Reduce the manifest checks made when reconstructing
The
Use original size to determine if a file should be bundled.
+ +The size of a block incremental file in the prior backup was used to decide whether to bundle it, so a file that had grown past
Add context to block map read errors during backup.
+ +An error reading the block map of a prior backup now reports the file, repository path, offset, and map size. A read that returns fewer bytes than expected is reported as such instead of as a parse error.
+Cast
A user-defined implicit cast from operator is not unique: unknown || timestamp with time zone
. The label text is unchanged.
Support building on
An
Cast
Support
Building against X509_get_subject_name() now returns a const name.
Fix clang builds.
+ +On
Migrate code generation from
Code is generated during the build from the option, error, help, and
Derive the cipher key with
Derive the cipher key with
Allow test user names that adduser on Debian rejects.
+Fix out-of-bounds reads found by
Fix bugs in the
Refactor the backup and restore modules into included modules.
+Remove unused parameters and struct members.
+Check that block macros are closed by the macro that matches them.
+Improve code generation for the help and
Fix minor issues.
+Store the file owner as an index rather than a pointer in the pack.
+Increase the minimum meson version to 0.55.3.
+Check the unity build object for unexpected exported symbols.
+Add a line length linter and fix formatting issues.
Modernize the website. Special thanks to Jan for the insightful (and hilarious) logo.
+ +The website has a new design with light and dark themes and a layout that works on phones. Pages with a table of contents show it beside the text and mark the section being read. Commands and config files can be copied with a button, config files mark the lines each section changes, and release notes can be expanded to show more detail.
+Document the release schedule.
+ +Feature releases are scheduled for the third Monday of January, April, July, and October. Patch releases are scheduled for the third Monday of any other month as required. The release notes introduction also describes the major, minor, and patch parts of a release number.
+Migrate the documentation tools from
The documentation is built by
Generate
Check documentation links and option allow lists.
+Improve the documentation build and deploy.
+Improve the documentation build containers.
+Merge coding standards into the contributing guide.
+Migrate the test harness from
Tests are run with
Add a
Add fuzz testing for the JSON, Ini, and Pack readers.
The readers are fuzzed with libFuzzer, AddressSanitizer, and UndefinedBehaviorSanitizer in CI. Fuzzing found several bugs in how invalid input is read, e.g. deeply nested
Allow test user names that adduser on Debian rejects.
+ +The test container is built with the same user name as the host user. Debian's
Fix test builds with clang and without optional libraries.
+Improve the
CI updates.
+Check that every code and test module is defined and covered.
+Improve unit and integration tests.
+Fix a bug.
@@ -138,12 +139,15 @@ RELEASE_LIST = """ # Releases with details and the history they are checked against DETAIL_HISTORY = [ + ("dB", "Merge v2.10.1 release."), + ("dA", "Fix a bug on a branch."), + ("d8", "Rework the internals."), ("d0", "Improve speed."), ("d1", "Add a feature."), ("d2", "Fix a bug."), ("d3", 'Fix a "quoted"Speed things up.
Rework the internals.
+Fix a bug.", 'id="nobody"/>\n \n\n' "
Fix a bug.",
@@ -530,7 +557,7 @@ def test_release_render_commit_ambiguous():
####################################################################################################################################
def test_release_render_detail():
- """Releases with version >= 2.60 have the paragraphs after the first and the links of an item in details."""
+ """Releases with version >= _DETAIL_VERSION_MIN have the paragraphs after the first and the links of an item in details."""
root = _render(_doc(DETAIL_RELEASE_LIST), history=DETAIL_HISTORY)
@@ -573,7 +600,7 @@ def test_release_render_detail():
],
)
- # Releases with version < 2.60 have no details
+ # Releases with version < _DETAIL_VERSION_MIN have no details
supported = xml_node_child_list(root, "section")[2]
assert_equal(_item_html(xml_node_child_list(supported, "section")[1]), ["Improve a thing. Say more."])
@@ -640,26 +667,530 @@ def test_release_render_detail_dev():
)
+####################################################################################################################################
+def test_release_render_commit_id():
+ """A commit can be named by id when more than one commit has the same subject."""
+
+ history = DETAIL_HISTORY[:3] + [("e1", "Fix lines."), ("e2", "Fix lines.")] + DETAIL_HISTORY[3:]
+ release_list = DETAIL_RELEASE_LIST.replace(
+ "",
+ """ Fix lines.
Finish the thing.
+Add the thing.
+Add the thing to the docs.
+Polish the thing.
+Improve a thing.
+Prepare for the thing.
+Start the thing.
+Tidy the thing.
+Add the thingin v2.61.0:
Add the thingin v2.61.0:
Finish the thingin ' + + 'v2.62.0dev:
Finish the thing.
", 'Finish the thing.
' + ) + ), + history=NOTE_HISTORY, + ) + + assert_equal( + str(raised.exception), "release 2.62.0 item 'Finish the thing.' is not a development item and cannot name a release note" + ) + + +# A patch release built on a branch of its own +PATCH_RELEASE_LIST = """ +Improve more.
+Fix a bug on a branch.
+ +Say more.
+Fix the build on a branch.
+Improve a thing.
+Fix the build on a branch.
", "Fix the build elsewhere.
")), + history=PATCH_HISTORY, + history_tag=PATCH_HISTORY_TAG, + ) + + assert_in("unable to find commit or no subject match for release 2.60.1 item 'Fix the build elsewhere.'", str(raised.exception)) + + with assert_raises(ToolError) as raised: + _render( + _doc(PATCH_RELEASE_LIST), + history=PATCH_HISTORY, + history_tag={"2.60.1": [("t4", "Do something unclaimed.")] + PATCH_HISTORY_TAG["2.60.1"]}, + ) + + assert_in("release 2.60.1 has commits that no item claims:\n2026-07-30 t4: Do something unclaimed.", str(raised.exception)) + + # A patch release after another patch release ends at the commit that closed the one before it + release_before = """Fix an earlier bug.
+Fix the build elsewhere.
")), + history=PATCH_HISTORY, + ) + + #################################################################################################################################### def test_release_render_bug_fix(): """A bug fix release x.y.z with z > 0 is skipped when it is not the most recent and a release x.y.0 is checked.""" - # The commits of 2.60.0 end at the commit that closed 2.59.0 + # The commits of 2.60.0 end at the commit that closed 2.10.0 with assert_raises(ToolError) as raised: _render(_doc(DETAIL_RELEASE_LIST.replace("Add a feature.
", "Improve a thing.
")), history=DETAIL_HISTORY) assert_in("unable to find commit or no subject match for release 2.60.0 item 'Improve a thing.'", str(raised.exception)) - # 2.59.0 is checked + # Development on 2.60.0 may have been opened as 2.60 + _render( + _doc(DETAIL_RELEASE_LIST), + history=[("d4", "Begin v2.60 development.") if commit == "d4" else (commit, subject) for commit, subject in DETAIL_HISTORY], + ) + + # Every commit of 2.60.0 is claimed by an item + with assert_raises(ToolError) as raised: + _render(_doc(DETAIL_RELEASE_LIST), history=[("d9", "Do something unclaimed.")] + DETAIL_HISTORY) + + assert_in("release 2.60.0 has commits that no item claims:\n2026-07-30 d9: Do something unclaimed.", str(raised.exception)) + + # Commits that no item claims are allowed when claim is false + _render(_doc(DETAIL_RELEASE_LIST), history=[("d9", "Do something unclaimed.")] + DETAIL_HISTORY, claim=False) + + # 2.10.0 is checked with assert_raises(ToolError) as raised: _render(_doc(DETAIL_RELEASE_LIST.replace("Improve a thing.
", "Improve two things.
")), history=DETAIL_HISTORY) - assert_in("unable to find commit or no subject match for release 2.59.0 item 'Improve two things.'", str(raised.exception)) + assert_in("unable to find commit or no subject match for release 2.10.0 item 'Improve two things.'", str(raised.exception)) #################################################################################################################################### def test_release_history_update(): - """The history is written from git when HEAD is not the newest commit in it.""" + """The history is written from git when the newest commit in it or in the tag of a patch release has changed.""" with tempfile.TemporaryDirectory() as path: path_doc = os.path.join(path, "doc") @@ -669,25 +1200,34 @@ def test_release_history_update(): _commit(path, "Add a feature.", "What the feature does.") _commit(path, "v2.01: First.", "The release notes.") - release_history_update(path, path_doc) + # With no integration branch the history ends at HEAD + assert_false(release_history_update(path, path_doc)) history = json.loads(file_read(path_history)) - assert_equal(history[0]["commit"], exec_one("git -C %s rev-parse HEAD" % path).strip()) - assert_equal(sorted(history[1]), ["body", "commit", "date", "subject"]) + assert_equal(history["head"][0]["commit"], exec_one("git -C %s rev-parse HEAD" % path).strip()) + assert_equal(sorted(history["head"][1]), ["body", "commit", "date", "subject"]) + assert_equal(history["tag"], {}) # The body of a commit that closed a release is left out assert_equal( - [(commit["subject"], commit.get("body")) for commit in history], + [(commit["subject"], commit.get("body")) for commit in history["head"]], [("v2.01: First.", None), ("Add a feature.", "What the feature does.")], ) # The history is not written while HEAD is the newest commit in it - file_write(path_history, json.dumps(history[:1])) + file_write(path_history, json.dumps({"head": history["head"][:1], "tag": {}})) release_history_update(path, path_doc) - assert_equal(len(json.loads(file_read(path_history))), 1) + assert_equal(len(json.loads(file_read(path_history))["head"]), 1) + + # A history written in the format before patch releases were read from their tags is written again + file_write(path_history, json.dumps(history["head"][:1])) + + release_history_update(path, path_doc) + + assert_equal(len(json.loads(file_read(path_history))["head"]), 2) # A new commit is first in the history _commit(path, "Fix a bug.") @@ -695,5 +1235,54 @@ def test_release_history_update(): release_history_update(path, path_doc) assert_equal( - [commit["subject"] for commit in json.loads(file_read(path_history))], ["Fix a bug.", "v2.01: First.", "Add a feature."] + [commit["subject"] for commit in json.loads(file_read(path_history))["head"]], + ["Fix a bug.", "v2.01: First.", "Add a feature."], ) + + # The history of a patch release with version >= _DETAIL_VERSION_MIN is the commits of its tag that are not in the history + # of HEAD. Other release tags are left out. + branch = exec_one("git -C %s branch --show-current" % path).strip() + + exec_one("git -C %s tag release/2.60.0" % path) + exec_one("git -C %s tag release/2.10.1" % path) + exec_one("git -C %s checkout -q -b patch" % path) + _commit(path, "Fix a bug on a branch.") + exec_one("git -C %s tag release/2.60.1" % path) + exec_one("git -C %s checkout -q %s" % (path, branch)) + + release_history_update(path, path_doc) + + history = json.loads(file_read(path_history)) + + assert_equal(list(history["tag"]), ["2.60.1"]) + assert_equal([commit["subject"] for commit in history["tag"]["2.60.1"]], ["Fix a bug on a branch."]) + + # The history is written again when the tag of a patch release moves + exec_one("git -C %s checkout -q patch" % path) + _commit(path, "v2.60.1: Patch.") + exec_one("git -C %s tag -f release/2.60.1" % path) + exec_one("git -C %s checkout -q %s" % (path, branch)) + + release_history_update(path, path_doc) + + assert_equal( + [commit["subject"] for commit in json.loads(file_read(path_history))["tag"]["2.60.1"]], + ["v2.60.1: Patch.", "Fix a bug on a branch."], + ) + + # With an integration branch related to HEAD the history ends at the newest commit of HEAD that is also in integration + exec_one("git -C %s branch integration" % path) + _commit(path, "RELEASE: Add release notes.") + + assert_true(release_history_update(path, path_doc)) + assert_equal(json.loads(file_read(path_history))["head"][0]["subject"], "Fix a bug.") + + # With an integration branch that is not related to HEAD the history ends at HEAD + tree = exec_one("git -C %s hash-object -t tree /dev/null" % path).strip() + commit = exec_one( + "git -C %s -c user.name=test -c user.email=test@pgbackrest.org commit-tree %s -m 'Unrelated.'" % (path, tree) + ).strip() + exec_one("git -C %s update-ref refs/heads/integration %s" % (path, commit)) + + assert_false(release_history_update(path, path_doc)) + assert_equal(json.loads(file_read(path_history))["head"][0]["subject"], "RELEASE: Add release notes.")