diff --git a/.gitignore b/.gitignore index 7a73081..b90bfc6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /*.rpm # Expanded source trees. /glibc-*/ +# Support files for patch-git. +/patch-git-generated-*.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3702777 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,189 @@ +# Ticket references + +All commits to this repository must reference a RHEL Jira ticket. To obtain +a ticket reference, login to [issues.redhat.com](https://issues.redhat.com/) +and create an issue of the appropriate type: + +* *Bug* for defect fixes (bugs). +* *Story* for enhancements (new featurees). + +For the component, choose `glibc`. After filing the ticket, leave it in +status _New_, so that it can be reviewed in due course by the glibc team +at Red Hat. + +Please use publicly viewable ticket (keeping *Security Level* unset) +for public contributions. + +# Reviews in Gitlab + +Once the pipeline run is authorized and completed, automation will +approve your merge request. **This automated approval does not +count.** You must wait for human approval from a Red Hat Platform +Tools team member. Once you have obtained review, you may merge. + +Note that for the `c10s` branch, the pipeline is currently not green. +On the `c8s` and `c9s` branches, it is green and should remain so. + +# Trailers in Git commit messages + +The patch management tooling (in `patch-git.lua`) assumes that all Git +commit messages in this repository have a trailer. The trailer must +be separated from the message body by a blank line. See +[git-interpret-trailers](https://git-scm.com/docs/git-interpret-trailers) +for the format details. + +A `Resolves:` or `Related:` tag is required to be present in the +trailer. Incorrectly formatted Git commit messages will lead to build +failures. Running `centpkg srpm` locally is sufficient for performing +the format checks. + +For tags that accept boolean values, `yes`/`no`, `true`/`false`, `1`/`0` +are recognized. + +The following `Key: value` pairs are recognized. + +* `Resolves`, `Related`. Contains a list of ticket references. Each + reference must end in a digit (for example, `RHEL-111490`, + `swbz#567`). Multiple references may be separated by spaces or + commas. Do not repeat the same reference twice. Example: + ``` + Resolves: RHEL-110535, RHEL-110949 + ``` + One of these tags must be present in every commit. The patch + management tooling treats both tags as equivalent. + +* `Parent`. The 40-character hash of the parent commit. Required when + using `RPM-Release` or `RPM-Changelog-Stop`. Building fails if the + commit hash in `Parent` does not match the actual parent commit. + Example: + ``` + Parent: 46a31fdf250a30ae96c082376a8eab95252762c0 + ``` + The `Parent` tag serves as a _rebase protection mechanism_. This + protection is desirable when other tags in the same trailer trigger + changes that could potentially hide information that was merged + since the commit was created originally, against a different project + history. For example, `RPM-Changelog-Stop: yes` stops processing + further changelog entries. If a commit with this trailer gets + rebased, previous RPM changelog entries might get lost because they + previously were only reflected in the auto-generated `%changelog` + part. Similarly, when setting the RPM release with `RPM-Release`, + after an adjusted rebase, the release number may go backwards. The + parent commit check serves as a reminder that before merging the + rebased commit, manual checks are needed that history is not + incorrectly overwritten. + +* `RPM-Branch-Type`. Must be `zstream` if present. Switches to + z-stream release numbering if that mode is not already active, by + appending `.1` to the release. (Future versions may add support for + additional branch types like `hotfix`.) + +* `RPM-Skip-Release`. Boolean. When `yes`, the release number is not + incremented and no new changelog header is created for that commit. + +* `RPM-Changelog`. Optional explicit changelog content. Use `-` on its + own to indicate no entries. For multiple entries, indent continuation + lines and prefix items with `- `. Example: + ``` + RPM-Changelog: + - Fix memory leak in fdopen (bug 31840) + - libio: Test for fdopen memory leak without SEEK_END + ``` + +* `RPM-Changelog-Stop`. Boolean. When `yes`, generation + of changelog entries stops at this commit. Requires `Parent`. + +* `RPM-Version`. Explict RPM version string. Must be a single word. + RPM macros are not permitted (no `%`). Requires `Parent`. If the + RPM version is not specified in a commit, it remains the same as in + its parent commit. + +* `RPM-Release`. Explicit RPM release string. Must be a single word + that contains `%{?dist}`, includes at least one digit, and does not + contain `-` or additional RPM macros. Requires `Parent`. + + If `RPM-Branch-Type: zstream` is present along with `RPM-Release`, + the specified release is assumed to be a z-stream release. As a + result, a subsequent `RPM-Branch-Type: zstream` tag will not add + `.1` at the end of the release, but increment the release as usual. + + If `RPM-Release` is omitted, the RPM release is generated from the + parent commit, by incrementing the left-most number in its release + string. (Special case: branch switching, as described above under + `RPM-Branch-Type`.) + +* `Patch-Git-Version`. Number with the patch-git format revision. + This is only required on the oldest commit, which also has to + specify `Parent`, `RPM-Version`, `RPM-Release`, + `RPM-Changelog-Stop`. Later commits inherit the patch-git version + from their parent commits. Example: + ``` + Parent: 92dfd986b2f2c697144be2ebe10a27d72c660ba4 + Patch-Git-Version: 1 + RPM-Version: 2.39 + RPM-Release: 60%{?dist} + RPM-Changelog-Stop: yes + ``` + Currently, only version 1 is supported. + +The patch-git tool can also be run directly, via `patch-git.lua`. +These subcommands are particilarly useful: + +* `patches --history-only`: Shows the patch files in application order, + as computed from the Git history. + +* `verrel`: Displays the version-release, as computed from the Git + history. + +* `changelog`: Outputs the auto-generated part of the RPM changelog. + Use `changelog HEAD^` to show the changelog entry for the most + recent commit only. + +# Patch file contents + +The `*.patch` files should use `git show` output if they are based on +an identifiable upstream commit (which is very much preferred). If +the patch is not equivalent to upstream, a comment should explain this +_before_ the `commit ` line, otherwise the patch file should start +with this line. + +Further comments about the backport can be added after the upstream +commit message (which is indented by four spaces, ` `). In this +section, the line `Conflicts:` starts a special section of identified +(semantic or textual) conflicts that could not be resolved by Git's +merge tooling. Each line starts with a tab character. Each conflict +resolution statement starts with one or more file lines (tab-indented, +no further indent), followed by tab-and-space indented comments on +the conflict resolution applied. + +# Licensing + +Contributions should be upstream backports, following the upstream +project's licensing conventions. Downstream-specific contributions (such +as RPM spec file updates) follow the existing licenses of the files being +changed. Otherwise, please specify the licensing conditions explicitly. + +If not otherwise indicated in the source file, Red Hat's original +contributions to this repository are licensed under the terms of the GNU +Lesser General Public License as published by the Free Software Foundation; +either version 2.1 of the License, or (at your option) any later version. + + diff --git a/glibc.spec b/glibc.spec index f0f3485..24a4975 100644 --- a/glibc.spec +++ b/glibc.spec @@ -139,16 +139,9 @@ end} ############################################################################## Summary: The GNU libc libraries Name: glibc -Version: %{glibcversion} - -# We'll use baserelease here for two reasons: -# - It is known to rpmdev-bumpspec, so it will be properly handled for mass- -# rebuilds -# - It allows using the Release number without the %%dist tag in the dependency -# generator to make the generated requires interchangeable between Rawhide -# and ELN (.elnYY < .fcXX). -%global baserelease 59 -Release: %{baserelease}%{?dist} +%{lua:dofile(rpm.expand([[%_sourcedir/patch-git.lua]]))} +Version: %{lua:patchgit.version()} +Release: %{lua:patchgit.release()} # Licenses: # @@ -219,6 +212,7 @@ Source15: ld-so-abi-ppc64le.baseline Source16: ld-so-abi-riscv64.baseline Source17: ld-so-abi-s390x.baseline Source18: ld-so-abi-x86_64.baseline +%{lua:patchgit.patches()} # glibc_ldso: ABI-specific program interpreter name. Used for debuginfo # extraction (wrap-find-debuginfo.sh) and smoke testing ($run_ldso below). @@ -333,420 +327,6 @@ rpm.define("__debug_install_post bash " .. wrapper %global glibc_ship_tracelibs_in_utils 1 %endif -############################################################################## -# Patches: -# - See each individual patch file for origin and upstream status. -# - For new patches follow template.patch format. -############################################################################## -Patch4: glibc-fedora-linux-tcsetattr.patch -Patch8: glibc-fedora-manual-dircategory.patch -Patch9: glibc-rh827510.patch -Patch13: glibc-fedora-localedata-rh61908.patch -Patch17: glibc-cs-path.patch -Patch23: glibc-python3.patch -Patch24: glibc-upstream-2.39-1.patch -Patch25: glibc-upstream-2.39-2.patch -Patch26: glibc-upstream-2.39-3.patch -Patch27: glibc-upstream-2.39-4.patch -Patch28: glibc-upstream-2.39-5.patch -Patch29: glibc-upstream-2.39-6.patch -Patch30: glibc-upstream-2.39-7.patch -Patch31: glibc-upstream-2.39-8.patch -Patch32: glibc-upstream-2.39-9.patch -Patch33: glibc-upstream-2.39-10.patch -Patch34: glibc-upstream-2.39-11.patch -Patch35: glibc-upstream-2.39-12.patch -Patch36: glibc-upstream-2.39-13.patch -Patch37: glibc-upstream-2.39-14.patch -Patch38: glibc-upstream-2.39-15.patch -Patch39: glibc-upstream-2.39-16.patch -Patch40: glibc-upstream-2.39-17.patch -Patch41: glibc-upstream-2.39-18.patch -Patch42: glibc-upstream-2.39-19.patch -Patch43: glibc-upstream-2.39-20.patch -Patch44: glibc-upstream-2.39-21.patch -Patch45: glibc-upstream-2.39-22.patch -Patch46: glibc-upstream-2.39-23.patch -Patch47: glibc-upstream-2.39-24.patch -Patch48: glibc-upstream-2.39-25.patch -Patch49: glibc-upstream-2.39-26.patch -Patch50: glibc-upstream-2.39-27.patch -Patch51: glibc-upstream-2.39-28.patch -Patch52: glibc-upstream-2.39-29.patch -Patch53: glibc-upstream-2.39-30.patch -Patch54: glibc-upstream-2.39-31.patch -Patch55: glibc-upstream-2.39-32.patch -Patch56: glibc-upstream-2.39-33.patch -Patch57: glibc-upstream-2.39-34.patch -Patch58: glibc-upstream-2.39-35.patch -Patch59: glibc-upstream-2.39-36.patch -Patch60: glibc-upstream-2.39-37.patch -Patch61: glibc-upstream-2.39-38.patch -Patch62: glibc-upstream-2.39-39.patch -Patch63: glibc-upstream-2.39-40.patch -Patch64: glibc-upstream-2.39-41.patch -Patch65: glibc-upstream-2.39-42.patch -Patch66: glibc-upstream-2.39-43.patch -Patch67: glibc-upstream-2.39-44.patch -Patch68: glibc-upstream-2.39-45.patch -Patch69: glibc-upstream-2.39-46.patch -Patch70: glibc-upstream-2.39-47.patch -Patch71: glibc-upstream-2.39-48.patch -Patch72: glibc-upstream-2.39-49.patch -Patch73: glibc-upstream-2.39-50.patch -Patch74: glibc-upstream-2.39-51.patch -Patch75: glibc-upstream-2.39-52.patch -Patch76: glibc-upstream-2.39-53.patch -Patch77: glibc-upstream-2.39-54.patch -Patch78: glibc-RHEL-22226.patch -Patch79: glibc-upstream-2.39-55.patch -Patch80: glibc-upstream-2.39-56.patch -Patch81: glibc-upstream-2.39-57.patch -Patch82: glibc-upstream-2.39-58.patch -Patch83: glibc-upstream-2.39-59.patch -Patch84: glibc-upstream-2.39-60.patch -Patch85: glibc-upstream-2.39-61.patch -Patch86: glibc-upstream-2.39-62.patch -Patch87: glibc-upstream-2.39-63.patch -Patch88: glibc-upstream-2.39-64.patch -Patch89: glibc-upstream-2.39-65.patch -Patch90: glibc-upstream-2.39-66.patch -Patch91: glibc-upstream-2.39-67.patch -Patch92: glibc-upstream-2.39-68.patch -Patch93: glibc-upstream-2.39-69.patch -Patch94: glibc-upstream-2.39-70.patch -Patch95: glibc-upstream-2.39-71.patch -Patch96: glibc-upstream-2.39-72.patch -# NEWS update: glibc-upstream-2.39-73.patch -# NEWS update: glibc-upstream-2.39-74.patch -Patch97: glibc-upstream-2.39-75.patch -Patch98: glibc-rh2292195-1.patch -Patch99: glibc-rh2292195-2.patch -Patch100: glibc-rh2292195-3.patch -Patch101: glibc-upstream-2.39-76.patch -Patch102: glibc-upstream-2.39-77.patch -Patch103: glibc-upstream-2.39-78.patch -Patch104: glibc-upstream-2.39-79.patch -Patch105: glibc-upstream-2.39-80.patch -Patch106: glibc-upstream-2.39-81.patch -Patch107: glibc-upstream-2.39-82.patch -Patch108: glibc-upstream-2.39-83.patch -Patch109: glibc-upstream-2.39-84.patch -Patch110: glibc-upstream-2.39-85.patch -Patch111: glibc-upstream-2.39-86.patch -Patch112: glibc-upstream-2.39-87.patch -Patch113: glibc-upstream-2.39-88.patch -Patch114: glibc-upstream-2.39-89.patch -Patch115: glibc-upstream-2.39-90.patch -Patch116: glibc-upstream-2.39-91.patch -Patch117: glibc-upstream-2.39-92.patch -Patch118: glibc-upstream-2.39-93.patch -Patch119: glibc-upstream-2.39-94.patch -Patch120: RHEL-18039-1.patch -Patch121: RHEL-18039-2.patch -Patch122: glibc-build-xtests-1.patch -Patch123: glibc-build-xtests-2.patch -Patch124: glibc-RHEL-12867.patch -Patch125: glibc-upstream-2.39-95.patch -Patch126: glibc-upstream-2.39-96.patch -Patch127: glibc-upstream-2.39-97.patch -Patch128: glibc-upstream-2.39-98.patch -Patch129: glibc-upstream-2.39-99.patch -Patch130: glibc-upstream-2.39-100.patch -Patch131: glibc-upstream-2.39-101.patch -Patch132: glibc-upstream-2.39-102.patch -Patch133: glibc-upstream-2.39-103.patch -Patch134: glibc-upstream-2.39-104.patch -Patch135: glibc-upstream-2.39-105.patch -Patch136: glibc-upstream-2.39-106.patch -Patch137: glibc-upstream-2.39-107.patch -Patch138: glibc-upstream-2.39-108.patch -Patch139: glibc-upstream-2.39-109.patch -Patch140: glibc-upstream-2.39-110.patch -Patch141: glibc-upstream-2.39-111.patch -Patch142: glibc-upstream-2.39-112.patch -Patch143: glibc-upstream-2.39-113.patch -Patch144: glibc-upstream-2.39-114.patch -Patch145: glibc-upstream-2.39-115.patch -Patch146: glibc-upstream-2.39-116.patch -Patch147: glibc-upstream-2.39-117.patch -Patch148: glibc-upstream-2.39-118.patch -Patch149: glibc-upstream-2.39-119.patch -Patch150: glibc-upstream-2.39-120.patch -Patch151: glibc-upstream-2.39-121.patch -Patch152: glibc-upstream-2.39-122.patch -Patch153: glibc-upstream-2.39-123.patch -Patch154: glibc-upstream-2.39-124.patch -Patch155: glibc-upstream-2.39-125.patch -Patch156: glibc-upstream-2.39-126.patch -Patch157: glibc-upstream-2.39-127.patch -Patch158: glibc-upstream-2.39-128.patch -Patch159: glibc-upstream-2.39-129.patch -Patch160: glibc-upstream-2.39-130.patch -Patch161: glibc-upstream-2.39-131.patch -Patch162: glibc-upstream-2.39-132.patch -Patch163: glibc-upstream-2.39-133.patch -Patch164: glibc-upstream-2.39-134.patch -Patch165: glibc-upstream-2.39-135.patch -Patch166: glibc-upstream-2.39-136.patch -Patch167: glibc-upstream-2.39-137.patch -Patch168: glibc-RHEL-12867-2.patch -Patch169: glibc-RHEL-12867-3.patch -Patch170: glibc-RHEL-42410.patch -Patch171: glibc-RHEL-71530-1.patch -Patch172: glibc-RHEL-71530-2.patch -Patch173: glibc-RHEL-71530-3.patch -Patch174: glibc-RHEL-71530-4.patch -Patch175: glibc-RHEL-71530-5.patch -Patch176: glibc-RHEL-71530-6.patch -Patch177: glibc-RHEL-71530-7.patch -Patch178: glibc-RHEL-71530-8.patch -Patch179: glibc-RHEL-71530-9.patch -Patch180: glibc-RHEL-71530-10.patch -Patch181: glibc-upstream-2.39-138.patch -Patch182: glibc-upstream-2.39-139.patch -Patch183: glibc-upstream-2.39-140.patch -Patch184: glibc-upstream-2.39-141.patch -Patch185: glibc-upstream-2.39-142.patch -Patch186: glibc-upstream-2.39-143.patch -Patch187: glibc-upstream-2.39-144.patch -Patch188: glibc-upstream-2.39-145.patch -Patch189: glibc-upstream-2.39-146.patch -Patch190: glibc-RHEL-75809.patch -Patch191: glibc-RHEL-75555.patch -Patch192: glibc-RHEL-75809-2.patch -Patch193: glibc-RHEL-75809-3.patch -Patch194: glibc-upstream-2.39-147.patch -Patch195: glibc-upstream-2.39-148.patch -Patch196: glibc-upstream-2.39-149.patch -Patch197: glibc-upstream-2.39-150.patch -Patch198: glibc-upstream-2.39-151.patch -Patch199: glibc-upstream-2.39-152.patch -Patch200: glibc-upstream-2.39-153.patch -Patch201: glibc-upstream-2.39-154.patch -Patch202: glibc-upstream-2.39-155.patch -Patch203: glibc-upstream-2.39-156.patch -Patch204: glibc-upstream-2.39-157.patch -Patch205: glibc-upstream-2.39-158.patch -Patch206: glibc-upstream-2.39-159.patch -Patch207: glibc-upstream-2.39-160.patch -Patch208: glibc-upstream-2.39-161.patch -Patch209: glibc-upstream-2.39-162.patch -Patch210: glibc-upstream-2.39-163.patch -Patch211: glibc-upstream-2.39-164.patch -Patch212: glibc-upstream-2.39-165.patch -Patch213: glibc-upstream-2.39-166.patch -Patch214: glibc-upstream-2.39-167.patch -Patch215: glibc-upstream-2.39-168.patch -Patch216: glibc-upstream-2.39-169.patch -Patch217: glibc-upstream-2.39-170.patch -Patch218: glibc-upstream-2.39-171.patch -Patch219: glibc-upstream-2.39-172.patch -Patch220: glibc-upstream-2.39-173.patch -Patch221: glibc-upstream-2.39-174.patch -Patch222: glibc-upstream-2.39-175.patch -Patch223: glibc-upstream-2.39-176.patch -Patch224: glibc-upstream-2.39-177.patch -Patch225: glibc-upstream-2.39-178.patch -Patch226: glibc-upstream-2.39-179.patch -Patch227: glibc-upstream-2.39-180.patch -Patch228: glibc-upstream-2.39-181.patch -Patch229: glibc-upstream-2.39-182.patch -Patch230: glibc-upstream-2.39-183.patch -Patch231: glibc-upstream-2.39-184.patch -Patch232: glibc-upstream-2.39-185.patch -Patch233: glibc-upstream-2.39-186.patch -Patch234: glibc-upstream-2.39-187.patch -Patch235: glibc-upstream-2.39-188.patch -Patch236: glibc-upstream-2.39-189.patch -Patch237: glibc-upstream-2.39-190.patch -Patch238: glibc-upstream-2.39-191.patch -Patch239: glibc-upstream-2.39-192.patch -Patch240: glibc-upstream-2.39-193.patch -Patch241: glibc-upstream-2.39-194.patch -Patch242: glibc-upstream-2.39-195.patch -Patch243: glibc-upstream-2.39-196.patch -Patch244: glibc-upstream-2.39-197.patch -Patch245: glibc-upstream-2.39-198.patch -Patch246: glibc-upstream-2.39-199.patch -Patch247: glibc-upstream-2.39-200.patch -Patch248: glibc-upstream-2.39-201.patch -Patch249: glibc-upstream-2.39-202.patch -Patch250: glibc-upstream-2.39-203.patch -Patch251: glibc-upstream-2.39-204.patch -Patch252: glibc-upstream-2.39-205.patch -Patch253: glibc-upstream-2.39-206.patch -Patch254: glibc-upstream-2.39-207.patch -Patch255: glibc-upstream-2.39-208.patch -Patch256: glibc-upstream-2.39-209.patch -Patch257: glibc-upstream-2.39-210.patch -Patch258: glibc-upstream-2.39-211.patch -Patch259: glibc-RHEL-82285.patch -Patch260: glibc-RHEL-101754-1.patch -Patch261: glibc-RHEL-101754-2.patch -Patch262: glibc-RHEL-104151.patch -Patch263: glibc-RHEL-95246-1.patch -Patch264: glibc-RHEL-95246-2.patch -Patch265: glibc-RHEL-105324.patch -Patch266: glibc-RHEL-72564-1.patch -Patch267: glibc-RHEL-72564-2.patch -Patch268: glibc-RHEL-107540-1.patch -Patch269: glibc-RHEL-107540-2.patch -Patch270: glibc-RHEL-107540-3.patch -Patch271: glibc-RHEL-106562-1.patch -Patch272: glibc-RHEL-106562-2.patch -Patch273: glibc-RHEL-106562-3.patch -Patch274: glibc-RHEL-106562-4.patch -Patch275: glibc-RHEL-106562-5.patch -Patch276: glibc-RHEL-106562-6.patch -Patch277: glibc-RHEL-106562-7.patch -Patch278: glibc-RHEL-106562-8.patch -Patch279: glibc-RHEL-106562-9.patch -Patch280: glibc-RHEL-106562-10.patch -Patch281: glibc-RHEL-106562-11.patch -Patch282: glibc-RHEL-106562-12.patch -Patch283: glibc-RHEL-106562-13.patch -Patch284: glibc-RHEL-106562-14.patch -Patch285: glibc-RHEL-106562-15.patch -Patch286: glibc-RHEL-106562-16.patch -Patch287: glibc-RHEL-106562-17.patch -Patch288: glibc-RHEL-106562-18.patch -Patch289: glibc-RHEL-106562-19.patch -Patch290: glibc-RHEL-106562-20.patch -Patch291: glibc-RHEL-106562-21.patch -Patch292: glibc-RHEL-106562-22.patch -Patch293: glibc-RHEL-106562-23.patch -Patch294: glibc-RHEL-106562-24.patch -Patch295: glibc-RHEL-107861-1.patch -Patch296: glibc-RHEL-107861-2.patch -Patch297: glibc-RHEL-58357-1.patch -Patch298: glibc-RHEL-58357-2.patch -Patch299: glibc-RHEL-58357-3.patch -Patch300: glibc-RHEL-58357-4.patch -Patch301: glibc-RHEL-58357-5.patch -Patch302: glibc-RHEL-58357-6.patch -Patch303: glibc-RHEL-58357-7.patch -Patch304: glibc-RHEL-58357-8.patch -Patch305: glibc-RHEL-58357-9.patch -Patch306: glibc-RHEL-58357-10.patch -Patch307: glibc-RHEL-58357-11.patch -Patch308: glibc-RHEL-107695-1.patch -Patch309: glibc-RHEL-107695-2.patch -Patch310: glibc-RHEL-107695-3.patch -Patch311: glibc-RHEL-107695-4.patch -Patch312: glibc-RHEL-107695-5.patch -Patch313: glibc-RHEL-107695-6.patch -Patch314: glibc-RHEL-107695-7.patch -Patch315: glibc-RHEL-107695-8.patch -Patch316: glibc-RHEL-107695-9.patch -Patch317: glibc-RHEL-107695-10.patch -Patch318: glibc-RHEL-107695-11.patch -Patch319: glibc-RHEL-107695-12.patch -Patch320: glibc-RHEL-107695-13.patch -Patch321: glibc-RHEL-107695-14.patch -Patch322: glibc-RHEL-107695-15.patch -Patch323: glibc-RHEL-107695-16.patch -Patch324: glibc-RHEL-107695-17.patch -Patch325: glibc-RHEL-107695-18.patch -Patch326: glibc-RHEL-107695-19.patch -Patch327: glibc-RHEL-108475-1.patch -Patch328: glibc-RHEL-108475-2.patch -Patch329: glibc-RHEL-108974-1.patch -Patch330: glibc-RHEL-108974-2.patch -Patch331: glibc-RHEL-108974-3.patch -Patch332: glibc-RHEL-108974-4.patch -Patch333: glibc-RHEL-108974-5.patch -Patch334: glibc-RHEL-108974-6.patch -Patch335: glibc-RHEL-108974-7.patch -Patch336: glibc-RHEL-108974-8.patch -Patch337: glibc-RHEL-108974-9.patch -Patch338: glibc-RHEL-108974-10.patch -Patch339: glibc-RHEL-108974-11.patch -Patch340: glibc-RHEL-108974-12.patch -Patch341: glibc-RHEL-108974-13.patch -Patch342: glibc-RHEL-108974-14.patch -Patch343: glibc-RHEL-108974-15.patch -Patch344: glibc-RHEL-108974-16.patch -Patch345: glibc-RHEL-108974-17.patch -Patch346: glibc-RHEL-108974-18.patch -Patch347: glibc-RHEL-108974-19.patch -Patch348: glibc-RHEL-108974-20.patch -Patch349: glibc-RHEL-108974-21.patch -Patch350: glibc-RHEL-108974-22.patch -Patch351: glibc-RHEL-108974-23.patch -Patch352: glibc-RHEL-108974-24.patch -Patch353: glibc-RHEL-108974-25.patch -Patch354: glibc-RHEL-108974-26.patch -Patch355: glibc-RHEL-108974-27.patch -Patch356: glibc-RHEL-108974-28.patch -Patch357: glibc-RHEL-108974-29.patch -Patch358: glibc-RHEL-108974-30.patch -Patch359: glibc-RHEL-108974-31.patch -Patch360: glibc-RHEL-108974-32.patch -Patch361: glibc-RHEL-108974-33.patch -Patch362: glibc-RHEL-108974-34.patch -Patch363: glibc-RHEL-108823-1.patch -Patch364: glibc-RHEL-108823-2.patch -Patch365: glibc-RHEL-108823-3.patch -Patch366: glibc-RHEL-108823-4.patch -Patch367: glibc-RHEL-108823-5.patch -Patch368: glibc-RHEL-108823-6.patch -Patch369: glibc-RHEL-108823-7.patch -Patch370: glibc-RHEL-108823-8.patch -Patch371: glibc-RHEL-108823-9.patch -Patch372: glibc-RHEL-108823-10.patch -Patch373: glibc-RHEL-108823-11.patch -Patch374: glibc-RHEL-108823-12.patch -Patch375: glibc-RHEL-108823-13.patch -Patch376: glibc-RHEL-108823-14.patch -# glibc-2.39-212-gb027d5b145 is glibc-RHEL-105324.patch. -Patch377: glibc-upstream-2.39-213.patch -Patch378: glibc-upstream-2.39-214.patch -Patch379: glibc-upstream-2.39-215.patch -Patch380: glibc-upstream-2.39-216.patch -Patch381: glibc-upstream-2.39-217.patch -Patch382: glibc-upstream-2.39-218.patch -Patch383: glibc-upstream-2.39-219.patch -Patch384: glibc-upstream-2.39-220.patch -Patch385: glibc-upstream-2.39-221.patch -Patch386: glibc-upstream-2.39-222.patch -Patch387: glibc-upstream-2.39-223.patch -Patch388: glibc-upstream-2.39-224.patch -Patch389: glibc-upstream-2.39-225.patch -# glibc-2.39-226-g42a8cb7560 is glibc-RHEL-108475-1.patch. -# glibc-2.39-227-gf0e8d04eef is glibc-RHEL-108475-2.patch. -Patch390: glibc-upstream-2.39-228.patch -Patch391: glibc-upstream-2.39-229.patch -Patch392: glibc-upstream-2.39-230.patch -Patch393: glibc-upstream-2.39-231.patch -Patch394: glibc-upstream-2.39-232.patch -Patch395: glibc-upstream-2.39-233.patch -Patch396: glibc-upstream-2.39-234.patch -Patch397: glibc-upstream-2.39-235.patch -Patch398: glibc-upstream-2.39-236.patch -Patch399: glibc-upstream-2.39-237.patch -Patch400: glibc-upstream-2.39-238.patch -Patch401: glibc-upstream-2.39-239.patch -Patch402: glibc-upstream-2.39-240.patch -Patch403: glibc-upstream-2.39-241.patch -Patch404: glibc-upstream-2.39-242.patch -Patch405: glibc-upstream-2.39-243.patch -Patch406: glibc-upstream-2.39-244.patch -Patch407: glibc-upstream-2.39-245.patch -Patch408: glibc-upstream-2.39-246.patch -Patch409: glibc-upstream-2.39-247.patch -Patch410: glibc-upstream-2.39-248.patch -Patch411: glibc-upstream-2.39-249.patch -Patch412: glibc-upstream-2.39-250.patch -Patch413: glibc-upstream-2.39-251.patch -Patch414: glibc-upstream-2.39-252.patch -Patch415: glibc-upstream-2.39-253.patch -# glibc-2.39-254-g3b6c8ea878 is glibc-RHEL-106562-16.patch. -# glibc-2.39-255-g1f17635507 is glibc-RHEL-106562-17.patch. -Patch416: glibc-upstream-2.39-256.patch -Patch417: glibc-upstream-2.39-257.patch -Patch418: glibc-upstream-2.39-258.patch - ############################################################################## # Continued list of core "glibc" package information: ############################################################################## @@ -2765,6 +2345,7 @@ update_gconv_modules_cache () %endif %changelog +%{lua:patchgit.changelog()} * Tue Aug 26 2025 Arjun Shankar - 2.39-59 - glibc-locale-source: Require gzip to handle compressed charmaps (RHEL-102553) diff --git a/patch-git.lua b/patch-git.lua new file mode 100644 index 0000000..6f3d230 --- /dev/null +++ b/patch-git.lua @@ -0,0 +1,1791 @@ +-- patch-git, a patch management tooling for dist-git. +-- Copyright Red Hat, Inc. +-- +-- This program is free software: you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation, either version 3 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program. If not, see . + +--[==[ +The canonical source for this file is here: + +* https://gitlab.com/redhat/centos-stream/rpms/glibc/-/blob/c10s/patch-git.lua + +To activate patch-git, add this file into your dist-git repository, and +add this line to the spec file: + +%{lua:dofile(rpm.expand([[%_sourcedir/patch-git.lua]]))} + +Do not indent this line, and use this line verbatim. Some tools +may use it to recognize that patch-git is in use. + +If patch-git can infer the patch list correctly, you can remove all +Patch…: lines from the spec file, and replace them with a line +like this: + +%{lua:patchgit.patches()} + +If the patch-git heuristics do not result in a correct patch +application order (which can happen if sorting patches +lexicographically within one commit does not yield the correct patch +application order), you can keep historic Patch…: lines before the +patchgit.patches() line. + +To auto-generate the changelog, start the %changelog section like this: + +%changelog +%{lua:patchgit.changelog()} + +This will add auto-generated changelog entries from the Git history. +]==] + +-- The patchgit global variable caches data extracted from Git-generated files, +-- and provides functions that can be called from the spec file. +-- +-- patchgit.commits: a list which contains the commit history. The +-- oldest commit is at index 1, its successor is at index 2, +-- and so on. The table entries are themselves tables with the +-- following fields: +-- commit: Git commit hash (40 hexadecimal digits) +-- author: name and email address of the commit author +-- author_date: author date according to Git +-- committer: like author, but for the Git committer +-- commit_date: like author_date, but for the commit +-- message: the commit message (unparsed) +-- changes: the file changes (from git log --raw output). A list +-- of tables with the following fields: +-- src_mode: number with file mode, 0 for creation +-- dst_mode: number with file mode, 0 for deletion +-- src_blob: abbreviated blob hash for original +-- dst_blob: abbreviated blob hash for result +-- status: flags, should only be 'A' (add), 'M' (modify), +-- 'D' (delete) (due to --no-renames) +-- path: path of the blob being changed +-- +-- patchgit.patches(): Emit a patch list constructed from Git history +-- into the spec file. +-- +-- patchgit.changelog(): Emit auto-generated changelog entries into the +-- spec file. +-- +patchgit = {} + +-- Used to trigger file regeneration in case of changes. This is only +-- necessary to change if the set of files or the data format in the +-- files is changed. If data extraction from the files is modified, +-- this change will take place immediately even if those files are not +-- regenerated, so no VERSION update is needed. +local VERSION = 1 + +-- Cached value of %_sourcedir from the RPM environment. When not +-- running under rpm, default to the current directory. +local sourcedir +if rpm then + sourcedir = rpm.expand('%_sourcedir') +else + -- If not running under rpm, default to the current directory. + sourcedir = '.' +end + +-- This file contains the commit hash for HEAD (corresponding to the +-- rest of the files) and the VERSION marker. +local git_commit_file = 'patch-git-generated-commit.txt' + +-- This file contains git log --raw output. +local git_log_file = 'patch-git-generated-log.txt' + +-- Read the file with the specified name in the RPM source directory. +-- Returns nil and an error message if the source file cannot be opened. +local function read_source_file(name) + local path = sourcedir .. '/' .. name + local fp, err = io.open(path, 'r') + if not fp then + return fp, err + end + local s = fp:read('a') + assert(fp:close()) + return s +end + +-- Write the specified contents to a file with the specified name in +-- the RPM source directory. Asserts if the file cannot be written. +local function write_source_file(name, contents) + local fp = assert(io.open(sourcedir .. '/' .. name, 'w+')) + assert(fp:write(contents)) + assert(fp:close()) +end + +-- Run 'git ' .. cmd in the RPM source directory and return the output +-- string. Assert that the command completed successfully. +local function run_git(cmd) + cmd = 'cd ' .. sourcedir .. ' && TZ=UTC LC_ALL=C.utf8 git ' .. cmd + local fp = assert(io.popen(cmd, 'r')) + local s = fp:read('a') + local ok, term, status = fp:close() + if not ok or term ~= 'exit' or status ~= 0 then + assert(false, cmd .. ': ' .. term .. ' ' .. status) + end + return s +end + +-- Check the result of os.execute and the close method on streams +-- created by io.popen. The first argument is the command to use in +-- an error message. +local function check_cmd_result(cmd, ok, term, status) + if not ok or term ~= 'exit' or status ~= 0 then + assert(false, cmd .. ': ' .. term .. ' ' .. status) + end +end + +-- Run the command with the shell and return the output. +local function run_shell(cmd) + local fp = assert(io.popen(cmd, 'r')) + local s = fp:read('a') + check_cmd_result(cmd, fp:close()) + return s +end + +-- Calls f with a temporary file name as an argument, which is created +-- automatically. Delete the file once f returns (normally or through +-- an error). +local function with_temporary_file(f, ...) + local tmpfile = assert(string.match(run_shell('mktemp'), '^(/.*)\n$')) + local fp = assert(io.open(tmpfile, 'w+')) + return (function(ok, ...) + os.remove(tmpfile) + if ok then + return ... + else + error(...) + end + end)(pcall(f, tmpfile, ...)) +end + +-- Run 'git ' .. cmd in the RPM source directory and assert that the +-- command completed successfully. +local function check_git(cmd) + cmd = 'cd ' .. sourcedir .. ' && TZ=UTC LC_ALL=C.utf8 git ' .. cmd + check_cmd_result(cmd, os.execute(cmd)) +end + +-- Lists the contents of the directory. The special entries '.' and +-- '..' are included. +local function list_directory(path) + if posix then + return posix.dir(path) + else + -- Not running under rpm. Avoid additional dependencies + -- by falling back to ls. Not ideal, but gets the job done + -- (unless there are files with newlines in their names). + local result = {} + local cmd = 'ls -a -- ' .. path + local fp = assert(io.popen(cmd, 'r')) + while true do + local line = fp:read('l') + if not line then + break + end + result[#result + 1] = line + end + check_cmd_result(cmd, fp:close()) + return result + end +end + +-- Return the single RPM spec file name from the source directory. +local function get_single_spec_file() + local spec + for _, file in ipairs(list_directory(sourcedir)) do + if string.match(file, '%.spec$') then + if spec then + error('multiple RPM spec files: ' + .. spec .. ' and ' .. file) + end + spec = file + break + end + end + if not spec then + error('RPM spec file not found') + end + return spec +end + +-- Quote a string so that shell meta-characters are not interpreted by +-- the shell anymore. +local function shell_quote(s) + if s == '' then + return "''" + end + if not string.match(s, '[^A-Za-z0-9_./+-]') then + -- The string does not contain shell meta-characters. + return s + end + -- Replace "'" with a sequence that ends the string, emits an escaped + -- "'", and then starts a new string. + return "'" .. string.gsub(s, "'", [['\'']]) .. "'" +end +-- Tests for shell_quote. +do + assert(shell_quote('') == "''") + assert(shell_quote('foo') == 'foo') + assert(shell_quote('foo bar') == "'foo bar'") + assert(shell_quote("foo'bar") == "'foo'\\''bar'") + assert(shell_quote("'foo'bar") == "''\\''foo'\\''bar'") +end + +local function shell_args(args) + local result = {} + for _, arg in ipairs(args) do + result[#result + 1] = shell_quote(arg) + end + return table.concat(result, ' ') +end +-- Tests for shell_args. +do + assert(shell_args({}) == '') + assert(shell_args({'foo', 'bar'}) == "foo bar") + assert(shell_args({'foo', "bar'baz"}) == [[foo 'bar'\''baz']]) +end + +-- Evaluate the string using rpmspec against the spec file +-- (previously obtained with get_single_spec_file). Return the result +-- of the evaluation. The mode of operation needs to be selected with +-- rpmspec options such as --eval, --qf, -P. +local run_rpmspec +-- Same as check_rpmspec, but print the result to stdout. +local check_rpmspec +do + local function cmd(spec, ...) + return shell_args({'rpmspec', '-D', '_sourcedir ' .. sourcedir, + sourcedir .. '/' .. spec, ...}) + end + + function run_rpmspec(spec, ...) + local script = cmd(spec, ...) + local fp = assert(io.popen(script, 'r')) + local result = assert(fp:read('a')) + check_cmd_result(script, fp:close()) + return result + end + + function check_rpmspec(spec, ...) + local script = cmd(spec, ...) + check_cmd_result(script, os.execute(script)) + end +end + +-- Called to generate the files (if HEAD or the script version has changed). +local function generate_files() + -- True if %_sourcedir refers to a Git repository and git is installed. + local have_git = (posix == nil -- Not running under rpm. + or (posix.access('/usr/bin/git', 'x') + and posix.access(sourcedir .. '/.git/.', 'x'))) + local commit_marker_contents -- For git_commit_file. + if have_git then + commit_marker_contents = + run_git('rev-parse HEAD') .. 'v' .. VERSION .. '\n' + if commit_marker_contents == read_source_file(git_commit_file) then + -- HEAD and version did not change. No file generation + return + end + elseif not read_source_file(git_commit_file) then + assert(false, 'no Git repository and no captured Git history') + else + -- No Git, so no regeneration possible, but the required files + -- are present. No work to do. + return + end + + -- At this point, we have a Git repository, and we need to regenerate the + -- patch-git-generated*.txt files. Make sure that the repository is not + -- shallow. + do + local shallow = run_git('rev-parse --is-shallow-repository') + if shallow == 'true\n' then + check_git('fetch --unshallow --filter=blob:none') + else + assert(shallow == 'false\n', shallow) + end + end + + -- Delete the marker file if it exists, to force regeneration after + -- partial generation below. + os.remove(sourcedir .. '/' .. git_commit_file) + + -- Produce the Git log. The --first-parent option ignores side + -- branches (so that it is possible to merge in arbitrary history + -- without growing the log too much, and to control the + -- script-processed commit messages). With --no-renames, no blobs + -- are needed (see --filter=blob:none above). With --raw, + -- information about the changed files is preserved (required for + -- patch depth sorting below). To include committer dates, use + -- --pretty=fuller. + check_git( + 'log --first-parent --no-renames --raw --pretty=fuller --date=default > ' + .. git_log_file) + + -- Atomically replace the contents of git_commit_file, confirming that + -- the new Git log has been written. + write_source_file(git_commit_file .. '.tmp', commit_marker_contents) + assert(os.rename(sourcedir .. '/' .. git_commit_file .. '.tmp', + sourcedir .. '/' .. git_commit_file)) +end + +-- Older RPM versions use doubles for patch and source numbers, not +-- integers. This causes problems because current Lua formats such +-- numbers with a decimal point. Therefore, use tointeger when +-- obtaining numbers from source_nums, patch_nums. If the Lua +-- interpreter has the double/integer distinction, it defines +-- math.tointeger. Otherwise, the conversion is not necessary, and +-- doubles that are formatted without a decimal point if there are +-- integers. +local tointeger = math.tointeger +if not tointeger then + function tointeger(n) + return n + end +end + +-- Inject Sources: lines for the auto-generated files and this script +-- into the spec file. +local function emit_sources() + -- If not running under rpm, the source list is not meaningful. Do + -- not print it. + if not rpm then + return + end + + local max = 0 + for i=1,#source_nums do + local k = source_nums[i] + if k > max then + max = tointeger(k) + end + end + local function emit(name) + max = max + 1 + print('Source' .. max .. ': ' .. name .. '\n') + end + emit(git_commit_file) + emit(git_log_file) + emit('patch-git.lua') -- This file. +end + +local function parse_commits() + local fp = assert(io.open(sourcedir .. '/' .. git_log_file), 'r') + local line -- The current line. Updated by readline1(), readline(). + local lineno = 0 -- Its number. + local function readline1() -- No error checking, may return nil. + lineno = lineno + 1 + line = fp:read('L') -- Include '\n'. + return line + end + local function readline() -- Does not return nil. + if not readline1() then + assert(false, 'unexpected end of file at line ' .. lineno) + end + return line + end + local function check(cond) -- Report an error if not cond. + if not cond then + io.stderr:write(git_log_file .. ':' .. lineno .. ': error: ' + .. line) + io.stderr:write(debug.traceback(nil, 2)) + error('git log parse error') + end + end + + local commits = {} + readline() + + while line do + local commit = string.match(line, '^commit ([0-9-a-f]+)\n') + check(commit and #commit == 40) + if string.match(readline(), '^Merge: ') then + readline() + end + local author = string.match(line, '^Author: (.+)\n') + check(author) + local author_date = string.match(readline(), '^AuthorDate: (.+)\n') + check(author_date) + local committer = string.match(readline(), '^Commit: (.+)\n') + check(committer) + local commit_date = string.match(readline(), '^CommitDate: (.+)\n') + check(commit_date) + check(readline() == '\n') -- Separator between header and commit message. + + -- Read the commit message. Remove the indentation. + local remove_indent = '^ (.*\n)' + local message = {string.match(readline(), remove_indent)} + check(message[1]) + while true do + if not readline1() then + -- EOF in initial commit message. + break + end + local l = string.match(line, remove_indent) + if not l then break + -- No longer the commit message. + break + end + message[#message + 1] = l + end + message = table.concat(message) + + if line == '\n' then + readline1() + end + + -- Read the file changes (lines starting with ':'). + local changes = {} + while line and string.match(line, '^:') do + local src_mode, dst_mode, src_blob, dst_blob, status, path = + string.match( + line, + '^:([0-7]+) ([0-7]+) ([0-9a-f]+) ([0-9a-f]+) ([^\t]+)\t([^\n]+)\n') + check(string.match(status, '^[ADM]$')) -- See patchgit.patches below. + check(path) + changes[#changes + 1] = { + src_mode=tonumber(src_mode, 8), + dst_mode=tonumber(dst_mode, 8), + src_blob=src_blob, + dst_blob=dst_blob, + status=status, + path=path, + }; + readline1() + end + + -- Store the commit. + commits[#commits + 1] = { + commit=commit, + author=author, + author_date=author_date, + committer=committer, + commit_date=commit_date, + message=message, + changes=changes, + } + + if line == '\n' then + readline1() + end + end + assert(fp:close()) + + -- Reverse the order of the commits list, so that the oldest commit + -- comes first. + do + local i = 1 + local j = #commits + while i < j do + local tmp = commits[i] + commits[i] = commits[j] + commits[j] = tmp + i = i + 1 + j = j - 1 + end + end + + patchgit.commits = commits +end + +-- Sort the list lexicographically, in place, treating sequences of +-- digits as a single positive decimal number. +local function version_sort(list) + -- Sorting is only needed for two or more elements. + if #list <= 1 then + return + end + + -- Maximum length of a sequence of consecutive digits in patch names. + local max_width = 1 + for i=1,#list do + local s = list[i] + for number in string.gmatch(s, '%d+') do + if #number > max_width then + max_width = #number + end + end + end + + -- Pad the number argument with leading '0' to max_width. + local function pad(s) + return string.rep('0', max_width - #s) .. s + end + + local padded = {} + for i=1,#list do + local s = list[i] + padded[s] = string.gsub(s, '%d+', pad) + end + + table.sort(list, function (a, b) + return padded[a] < padded[b] + end) +end +-- Tests for version_sort. +do + local test = {'b2-30b', 'b1', 'b10', 'b2', 'a', 'b2-30', 'b2-4'} + version_sort(test) + assert(#test == 7) + assert(test[1] == 'a') + assert(test[2] == 'b1') + assert(test[3] == 'b2') + assert(test[4] == 'b2-4') + assert(test[5] == 'b2-30') + assert(test[6] == 'b2-30b') + assert(test[7] == 'b10') +end + +-- Inject the Patch: lines into the spec file, in the appropriate +-- (commit) order. Unapplied patches found in the source directory +-- and that are not present in the Git history are applied last. +function patchgit.patches(options) + local history_only = options and options.history_only + emit_sources() + + -- Maximum patch number emitted so far. + local patchno = 0 + + -- True entries for patch files that have already been applied in + -- the spec file. Only populated when running under rpm. + local preordered = {} + + -- Table with the patches in the source directory. As patches are + -- scheduled for application, they are removed from this table. + local patches_in_sourcedir = {} + + if not history_only then + for i=1,#patches do + -- Remove the %_sourcedir prefix. + local pname = assert(string.match(patches[i], '.*/([^/]+)$')) + preordered[pname] = true + local n = patch_nums[i] + if n > patchno then + patchno = tointeger(n) + end + + end + for _, fname in ipairs(list_directory(sourcedir)) do + if string.find(fname, '%.patch$') and not preordered[fname] then + patches_in_sourcedir[fname] = true + end + end + end + + -- Table indexed by patch name, mapping it to the age (number) of + -- the patch. Lower age numbers are applied earlier. + local patch_age = {} + for age, commit in ipairs(patchgit.commits) do + for _, change in ipairs(commit.changes) do + -- Only look at patch files in the top-level directory. + if string.match(change.path, '^[^/]+%.patch$') then + if change.status == 'A' then -- Add. + assert(not patch_age[change.path], change.path) + patch_age[change.path] = age + elseif change.status == 'D' then -- Delete. + assert(patch_age[change.path], change.path) + patch_age[change.path] = nil + elseif change.status == 'M' then -- Modify. + assert(patch_age[change.path], change.path) + else + assert(false) -- See [ADM] match above. + end + end + end + end + + -- If not running under rpm, the default Lua print function behaves + -- differently (it adds a '\n'). Directly write to stdout to avoid + -- adding the extra '\n'. + local emit + if rpm then + emit = print + else + function emit(s) + io.stdout:write(s) + end + end + + + -- Group the patches by age. The keys of by_age are the age numbers. + -- The values are lists of patch names (initially unsorted). + local by_age = {} + local max_age = #patchgit.commits + for patch, age in pairs(patch_age) do + assert(age <= max_age) + local patchlist = by_age[age] + if not patchlist then + patchlist = {} + by_age[age] = patchlist + end + patchlist[#patchlist + 1] = patch + end + + -- Perform version sort on the table and emit the patch names in + -- that order. + function emit_patchlist(patchlist) + -- Within one commit, patches are sorted lexicographically. + -- Remove the '.patch' suffix, so that it does not interfere + -- with sorting ('patch2b.patch' sorting before 'patch2.patch'). + local list = {} + for i, name in ipairs(patchlist) do + list[i] = assert(string.match(name, '^(.+)%.patch$')) + end + version_sort(list) + for i=1,#list do + local pname = list[i] .. '.patch' + if not preordered[pname] then + patchno = patchno + 1 + emit('Patch' .. patchno .. ': ' .. pname .. '\n') + patches_in_sourcedir[pname] = nil + end + end + end + + -- Do not use ipairs here because the by_age table may have holes. + -- Iterate over the maximum possible age range instead. + for age=1,max_age do + local patchlist = by_age[age] + if patchlist then + emit_patchlist(patchlist) + end + end + + -- Emit the unapplied patches in the source directory. + if not history_only then + local remaining_patches = {} + for patch, _ in pairs(patches_in_sourcedir) do + remaining_patches[#remaining_patches + 1] = patch + end + emit_patchlist(remaining_patches) + end +end + +---------------------------------------------------------------------- +-- Processing of commit messages, for release numbers and changelogs +---------------------------------------------------------------------- + +-- Return a table with the issue numbers in the string. +-- Return nil if the argument is not valid or empty. +-- If previous is not nil, the tickets are appended to this list. +local function parse_ticket_string(tag, s, previous) + local result + if previous ~= nil then + result = previous + else + result = {} + end + local old = #result + for ref1 in string.gmatch(s, '[ \t\n]*([^ \t\n]+)[ \t\n]*') do + local ref = string.match(ref1, '([a-zA-Z0-9#-]+),?') + if not ref then + return nil, 'invalid ticket reference: ' .. ref1 + end + if not string.match(ref, '.*%d$') then + return nil, 'ticket reference without trailing number: ' .. ref + end + for i=1,#result do + if result[i] == ref1 then + return nil, 'duplicate ticket reference: ' .. ref + end + end + result[#result + 1] = ref + end + if #result == old then + return nil, 'no ticket references found in ' .. tag + end + return result +end +-- Tests for parse_ticket_string. +do + local t, err + t = assert(parse_ticket_string('Resolves', ' RHEL-1234 swbz#567\n')) + assert(#t == 2) + assert(t[1] == 'RHEL-1234') + assert(t[2] == 'swbz#567') + t = assert(parse_ticket_string('Resolves', ' RHEL-1234, swbz#567\n')) + assert(#t == 2) + assert(t[1] == 'RHEL-1234') + assert(t[2] == 'swbz#567') + t, err = parse_ticket_string('Related', ' ') + assert(not t) + assert(err == 'no ticket references found in Related') + t, err = parse_ticket_string('Related', ' bug 567') + assert(not t) + assert(err == 'ticket reference without trailing number: bug') +end + +-- Extract the trailers from the passed commit message. Returns a +-- single table where keys are derived from the trailer tags. +-- On error, returns nil and an error message. +-- +-- The result table may contain the following fields: +-- +-- tickets: list of ticket reference strings (Resolves:, Related:) +-- parent: Git commit hash, 40 characters (Parent:) +-- patchgit_version: numeric revision number for patch-git directives. +-- rpm_branch_type: 'zstream' if present (RPM-Branch-Type:) +-- rpm_skip_release: boolean; true to skip release increment (RPM-Skip-Release:) +-- rpm_changelog: table of changelog entry strings +-- rpm_changelog_stop: boolean; true to stop including older commits +-- (RPM-Changelog-Stop:) +-- rpm_release: RPM release string (RPM-Release:) +-- rpm_version: RPM version string (RPM-Version:) +-- +-- The rpm_changelog table contains zero or more strings, each of +-- which is a changelog entry. The leading '-' is not included. +local parse_trailer +do + -- Parser for Patch-Git-Version. + local function parse_patchgit_version(tag, s) + s = string.match(s, '%s*(%d+)%s*$') + local n = s and tonumber(s) + if not s or s ~= tostring(n) then + return nil, tag .. ' does not contain a number' + end + return n + end + -- Tests for parse_patchgit_version. + do + local n, err + assert(parse_patchgit_version('Patch-Git-Version', ' 0\n') == 0) + assert(parse_patchgit_version('Patch-Git-Version', ' 1\n') == 1) + assert(parse_patchgit_version('Patch-Git-Version', ' 2\n') == 2) + n, err = parse_patchgit_version('Patch-Git-Version', '') + assert(not n and err == 'Patch-Git-Version does not contain a number') + n, err = parse_patchgit_version('Patch-Git-Version', ' \n') + assert(not n and err == 'Patch-Git-Version does not contain a number') + n, err = parse_patchgit_version('Patch-Git-Version', + ' 9999999999999999999\n') + assert(not n and err == 'Patch-Git-Version does not contain a number') + end + + -- Validator for RPM-Changelog. + local function parse_rpm_changelog(tag, s) + assert(string.sub(s, #s) == '\n') + + -- '-' is used to denote no changelog entry. + if string.match(s, '%s*%-%s*$') then + return {} + end + + local lines = {} + for line in string.gmatch(s, '([^\n]+\n)') do + lines[#lines + 1] = line + end + -- Remove leading whitespace from the first line. + lines[1] = assert(string.match(lines[1], '^[ \t]*(.*\n)$')) + if #lines == 1 then + -- Nothing to do + else + -- Strip shared whitespace prefix from second and further lines. + -- First compute the shared prefix. + local prefix = assert(string.match(lines[2], '^([ \t]+)')) + for i=3,#lines do + -- Reduce the shared prefix length until equality is reached. + while prefix ~= '' and string.sub(lines[i], 1, #prefix) ~= prefix do + prefix = string.sub(prefix, 1, #prefix - 1) + end + end + local skip = #prefix + 1 + -- Then strip the shared prefix. + for i=2,#lines do + lines[i] = string.sub(lines[i], skip) + end + end + + local result = {} + if not string.match(lines[1], '^- ') then + -- This is not an itemized changelog entry. The loop below + -- appends to this entry. + result[1] = '' + end + for _, line in ipairs(lines) do + local tagged = string.match(line, '^-[ \t]+(.*)%s*\n') + if tagged then + -- New entry. + result[#result + 1] = tagged + else + local stripped = assert(string.match(line, '%s*(.*)%s*\n')) + local old = result[#result] + if old ~= '' then + old = old .. ' ' + end + result[#result] = old .. stripped + end + end + return result + end + -- Tests for parse_rpm_changelog. + do + local function prc(s) + return parse_rpm_changelog('RPM-Changelog', s) + end + local t, err + + -- Single-line changelog entry, not itemized. + t = assert(prc('Switch to patch-git\n')) + assert(#t == 1) + assert(t[1] == 'Switch to patch-git') + + -- Single-line changelog entry, itemized. + t = assert(prc(' - Switch to patch-git\n')) + assert(#t == 1) + assert(t[1] == 'Switch to patch-git') + + -- Multi-line changelog entry, not itemized. + t = assert(prc(' Switch to\n patch-git\n')) + assert(#t == 1) + assert(t[1] == 'Switch to patch-git') + + -- Multi-line changelog entry, one item. + t = assert(prc('- Switch to\n patch-git\n')) + assert(#t == 1) + assert(t[1] == 'Switch to patch-git') + + -- Multi-line changelog entry, two items. + t = assert(prc( + '- Switch to\n patch-git\n' + .. ' - Additional patch-git\n fixes\n')) + assert(#t == 2, t[1]) + assert(t[1] == 'Switch to patch-git') + assert(t[2] == 'Additional patch-git fixes') + end + + local function parse_rpm_release(tag, s) + s = string.match(s, '^%s(%g+)%s*$') + if not s then + return nil, tag .. ' must contain a single word' + end + local dist = string.find(s, '%{?dist}', 1, true) + if not dist then + return nil, tag .. ' must contain %{?dist}' + end + if string.find(s, '%%.*%%{%?dist}') + or string.find(s, '%%{%?dist}.*%%') then + return nil, tag .. ' contains unexpected RPM macros' + end + if string.find(s, '-', 1, true) then + return nil, tag .. ' contains "-"' + end + if not string.find(s, '%d') then + return nil, tag .. ' must contain a digit' + end + return s + end + -- Tests for parse_rpm_release. + do + local r, err + assert(parse_rpm_release('RPM-Release', ' 59%{?dist}\n') == '59%{?dist}') + r, err = parse_rpm_release('RPM-Release', ' 59.el10\n') + assert(not r) + assert(err == 'RPM-Release must contain %{?dist}') + r, err = parse_rpm_release('RPM-Release', ' 59.el10_0\n') + assert(not r) + assert(err == 'RPM-Release must contain %{?dist}') + r, err = parse_rpm_release('RPM-Release', ' %{?dist}\n') + assert(not r) + assert(err == 'RPM-Release must contain a digit') + r, err = parse_rpm_release('RPM-Release', '59 %{?dist}') + assert(not r) + assert(err == 'RPM-Release must contain a single word') + end + + local function parse_parent(tag, s) + local commit = string.match(s, '^%s*([0-9a-f]+)%s*\n') + if not commit then + return nil, 'commit hash expected in ' .. tag + elseif #commit ~= 40 then + return nil, 'full 40-character commit hash needed in ' .. tag + end + return commit + end + do + local r, err + assert(assert(parse_parent('Parent', + ' 92dfd986b2f2c697144be2ebe10a27d72c660ba4\n')) + == '92dfd986b2f2c697144be2ebe10a27d72c660ba4') + r, err = parse_parent('Parent', + ' 92dfd986b2f2c697144be2ebe10a27d72c660ba4.\n') + assert(not r) + assert(err == 'commit hash expected in Parent') + r, err = parse_parent('Parent', + ' 92dfd986b2f2c697144be2ebe10a27d72c660ba\n') + assert(not r) + assert(err == 'full 40-character commit hash needed in Parent') + end + + local function parse_rpm_version(tag, s) + s = string.match(s, '^%s(%g+)%s*$') + if not s then + return nil, tag .. ' must contain a single word' + end + if string.find(s, '%', 1, true) then + return nil, tag .. ' contains unexpected RPM macros' + end + if string.find(s, '-', 1, true) then + return nil, tag .. ' contains "-"' + end + if not string.find(s, '%d') then + return nil, tag .. ' must contain a digit' + end + return s + end + -- Tests for parse_rpm_version. + do + local v, err + assert(parse_rpm_version('RPM-Version', ' 2.39.1\n') == '2.39.1') + v, err = parse_rpm_version('RPM-Version', ' 1%{?dist}\n') + assert(not v) + assert(err == 'RPM-Version contains unexpected RPM macros') + v, err = parse_rpm_version('RPM-Version', ' %{version}\n') + assert(not v) + assert(err == 'RPM-Version contains unexpected RPM macros') + v, err = parse_rpm_version('RPM-Version', ' 2-39\n') + assert(not v) + assert(err == 'RPM-Version contains "-"') + v, err = parse_rpm_version('RPM-Version', ' version\n') + assert(not v) + assert(err == 'RPM-Version must contain a digit') + v, err = parse_rpm_version('RPM-Version', ' 2 39\n') + assert(not v) + assert(err == 'RPM-Version must contain a single word') + end + + local string_to_bool = { + yes=true, + no=false, + ['true']=true, + ['false']=false, + ['0']=false, + ['1']=true, + } + local function parse_bool(tag, s) + s = string.match(s, '^%s*([^%s]+)%s*$') + local flag + if s ~= nil then + flag = string_to_bool[s] + end + if flag == nil then + return nil, tag .. ' must be yes/no' + end + return flag + end + -- Tests for parse_bool. + do + local bad = {'', 'y', 'n', '0 0', '0 1', 'none', 'Yes', 'No', 't', 'f'} + local function ps(pfx, sfx) + assert(parse_bool('Bool', pfx .. 'yes' .. sfx) == true) + assert(parse_bool('Bool', pfx .. 'no' .. sfx) == false) + assert(parse_bool('Bool', pfx .. 'true' .. sfx) == true) + assert(parse_bool('Bool', pfx .. 'false' .. sfx) == false) + assert(parse_bool('Bool', pfx .. '0' .. sfx) == false) + assert(parse_bool('Bool', pfx .. '1' .. sfx) == true) + for _, value in ipairs(bad) do + local r, err = parse_bool('Bool', pfx .. value .. sfx) + assert(not r, value) + assert(err == 'Bool must be yes/no') + end + end + ps('', '') + ps(' ', '\n') + ps(' ', ' \n') + end + + local function parse_rpm_branch_type(tag, s) + s = string.match(s, '^%s*([^%s]+)%s*$') + if s ~= 'zstream' then + return nil, tag .. ' must be zstream' + end + return s + end + -- Tests for parse_rpm_branch_type. + do + assert(parse_rpm_branch_type('Branch', ' zstream\n') == 'zstream') + local b, err = parse_rpm_branch_type('Branch', ' \n') + assert(not b and err == 'Branch must be zstream') + local b, err = parse_rpm_branch_type('Branch', ' hotfix\n') + assert(not b and err == 'Branch must be zstream') + end + + -- These are the recognized trailer tags in Git commit messages. + -- (Git calls them keys, see git-interpret-trailers(1).) + local recognized_trailer_tags = { + ['Resolves']={field='tickets', + parse=parse_ticket_string, + duplicates=true}, + ['Parent']={parse=parse_parent}, + ['Patch-Git-Version']={parse=parse_patchgit_version, + need_parent=true, + field='patchgit_version'}, + ['RPM-Branch-Type']={parse=parse_rpm_branch_type}, + ['RPM-Skip-Release']={parse=parse_bool}, + ['RPM-Changelog']={parse=parse_rpm_changelog}, + ['RPM-Changelog-Stop']={parse=parse_bool, + need_parent=true}, + ['RPM-Release']={parse=parse_rpm_release, + need_parent=true}, + ['RPM-Version']={parse=parse_rpm_version, + need_parent=true}, + } + recognized_trailer_tags.Related = recognized_trailer_tags.Resolves + for k, v in pairs(recognized_trailer_tags) do + if not v.field then + v.field = string.lower(string.gsub(k, '%-', '_')) + end + end + + function parse_trailer(message) + -- This holds due to the format of the input file. + assert(string.sub(message, #message) == '\n') + + -- Skip the non-trailer part in the message. There is no reverse + -- find, so call string.find repeatedly, under the assumption that + -- it's simpler and faster than the alternatives. + local pos = 0 + do + while true do + -- Only skip one '\n', in case there are more than two. + local npos = string.find(message, '\n\n', pos + 1, true) + if not npos then + if pos == 0 then + -- No '\n\n', no trailer. + return nil, 'missing Git trailer' + end + pos = pos + 2 + break + end + pos = npos + end + end + + -- pos is the start of the next line to parse. + local result = {} + local tags_seen = {} -- Keys are tag names, not fields. Values are true. + local last_tag + local need_parent -- Name of tag that requires Parent:. + while true do + local tag, contents, npos = + string.match(message, '^([%w-]+):([^\n]*\n)()', pos) + if not npos then + if pos > #message then + break + elseif not last_tag then + -- Probably just a new paragraph in the commit message. + return nil, 'no Git trailer found' + else + return nil, 'malformed line in Git trailer after ' .. last_tag + .. ' tag' + end + end + pos = npos + last_tag = tag + + -- Find the tag descriptor. + local descr = recognized_trailer_tags[tag] + if not descr then + return nil, 'not a recognized Git trailer tag: ' .. tag + end + if descr.need_parent then + need_parent = tag + end + + if tags_seen[tag] and not descr.duplicates then + return nil, 'duplicate ' .. tag .. ' tag' + end + tags_seen[tag] = true + + -- Append indented continuation lines. + while true do + local cont, npos = string.match(message, '^([ \t][^\n]*\n)()', pos) + if not cont then + break + end + pos = npos + -- This has quadratic behavior. Assume that there are few + -- continuation lines. + contents = contents .. cont + end + + local value, err = descr.parse(tag, contents, result[descr.field]) + if value == nil then + return nil, err + end + result[descr.field] = value + end + if not last_tag then + return nil, 'no Git trailer found' + end + if need_parent and not result.parent then + return nil, need_parent .. ' requires Parent tag' + end + return result + end + -- Tests for parse_trailer. + do + local t, err + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +- Backport: Remove memory leak in fdopen (bug 31840) +- Backport: libio: Test for fdopen memory leak without SEEK_END + support (bug 31840) + +Resolves: RHEL-108475 +]]) + assert(t, err) + assert(t.tickets) + assert(#t.tickets == 1) + assert(t.tickets[1] == 'RHEL-108475') + assert(t.patchgit_version == nil) + assert(t.rpm_version == nil) + assert(t.rpm_release == nil) + assert(t.rpm_changelog_stop == nil) + assert(t.rpm_branch_type == nil) + + -- Initial commit. + t, err = parse_trailer([[Switch to patch-git + +Resolves: RHEL-111490 +Parent: 92dfd986b2f2c697144be2ebe10a27d72c660ba4 +Patch-Git-Version: 1 +RPM-Version: 2.39 +RPM-Release: 60%{?dist} +RPM-Changelog-Stop: yes +]]) + assert(t, err) + assert(t.tickets) + assert(#t.tickets == 1) + assert(t.tickets[1] == 'RHEL-111490') + assert(t.patchgit_version == 1) + assert(t.rpm_version == '2.39') + assert(t.rpm_release == '60%{?dist}') + assert(t.rpm_changelog_stop == true) + assert(t.rpm_branch_type == nil) + + -- Missing blank line before Resolves:. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +- Backport: Remove memory leak in fdopen (bug 31840) +- Backport: libio: Test for fdopen memory leak without SEEK_END + support (bug 31840) +Resolves: RHEL-108475 +]]) + assert(not t) + assert(err == 'no Git trailer found', err) + + -- RPM release is set. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +Resolves: RHEL-108475 +Parent: 46a31fdf250a30ae96c082376a8eab95252762c0 +RPM-Release: 59%{?dist} +]]) + assert(t, err) + assert(t.parent == '46a31fdf250a30ae96c082376a8eab95252762c0') + assert(t.rpm_release == '59%{?dist}') + + -- RPM-Version requires Parent and accepts a simple version. + t, err = parse_trailer([[Set version for next release + +Resolves: RHEL-108475 +RPM-Version: 2.39.1 +]]) + assert(not t) + assert(err == 'RPM-Version requires Parent tag') + + t, err = parse_trailer([[Set version for next release + +Resolves: RHEL-108475 +Parent: 46a31fdf250a30ae96c082376a8eab95252762c0 +RPM-Version: 2.39.1 +]]) + assert(t, err) + assert(t.rpm_version == '2.39.1') + + -- Duplicate RPM-Release:. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +Resolves: RHEL-108475 +RPM-Release: 59%{?dist} +RPM-Release: 59%{?dist}.1 +]]) + assert(not t) + assert(err == 'duplicate RPM-Release tag', err) + + -- Duplicate RPM-Version:. + t, err = parse_trailer([[Set version for next release + +Resolves: RHEL-108475 +Parent: 46a31fdf250a30ae96c082376a8eab95252762c0 +RPM-Version: 2.39.1 +RPM-Version: 2.39.2 +]]) + assert(not t) + assert(err == 'duplicate RPM-Version tag', err) + + -- Multiple trailers. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +- Backport: Remove memory leak in fdopen (bug 31840) +- Backport: libio: Test for fdopen memory leak without SEEK_END + support (bug 31840) + +Resolves: RHEL-108475 +Resolves: swbz#31840 +RPM-Skip-Release: yes +]]) + assert(t, err) + assert(#t.tickets, 2) + assert(t.tickets[1] == 'RHEL-108475') + assert(t.tickets[2] == 'swbz#31840') + assert(not t.rpm_release) + assert(t.rpm_skip_release == true) + + -- Invalid value for Resolves trailer. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +- Backport: Remove memory leak in fdopen (bug 31840) +- Backport: libio: Test for fdopen memory leak without SEEK_END + support (bug 31840) + +Resolves: RHEL-108475 +Related: +RPM-Release: no +]]) + assert(not t) + assert(err == 'no ticket references found in Related') + + -- Broken line in trailer. + t, err = parse_trailer([[Fix memory leak after fdopen seek failure + +- Backport: Remove memory leak in fdopen (bug 31840) +- Backport: libio: Test for fdopen memory leak without SEEK_END + support (bug 31840) + +Resolves: RHEL-108475 +swbz#31840 +RPM-Release: no +]]) + assert(not t) + assert(err == 'malformed line in Git trailer after Resolves tag') + end +end + +-- Produce a list of changelog messages. The list can be empty. Use +-- trailer.rpm_changelog if available. +local function rpm_changelog_default(message, trailer) + if trailer.rpm_changelog then + return trailer.rpm_changelog + end + local subject = assert(string.match(message, '^%s*([^\n]-)%s*\n')) + -- See if there are tickets listed in parentheses. + local parens = string.match(subject, '%s+[(]([^)]+)[)]%s*$') + local tickets = trailer.tickets + if (not (parens and parse_ticket_string('subject', parens)) + and tickets and #tickets > 0) then + subject = subject .. ' (' .. table.concat(tickets, ', ') .. ')' + end + return {subject} +end +-- Tests for rpm_changelog_default. +do + local t + local function rcd(message) + return rpm_changelog_default(message, assert(parse_trailer(message))) + end + t = rcd([[Remove memory leak in fdopen + +Resolves: RHEL-108475 +]]) + assert(#t == 1) + assert(t[1] == 'Remove memory leak in fdopen (RHEL-108475)', t[1]) + + t = rcd([[Remove memory leak in fdopen (RHEL-108475) + +Resolves: RHEL-108475 +]]) + assert(#t == 1) + assert(t[1] == 'Remove memory leak in fdopen (RHEL-108475)', t[1]) + + t = rcd([[Remove memory leak in fdopen (RHEL-108475) + +Resolves: RHEL-108475 +RPM-Changelog: - +]]) + assert(#t == 0) + + t = rcd([[Remove memory leak in fdopen + +Resolves: RHEL-108475 +RPM-Changelog: + - Remove memory leak in fdopen (bug 31840) + - libio: Test for fdopen memory leak without SEEK_END +]]) + assert(#t == 2) + assert(t[1] == 'Remove memory leak in fdopen (bug 31840)') + assert(t[2] == 'libio: Test for fdopen memory leak without SEEK_END') +end + + +-- Turns a commit message into a string for diagnostic purposes. +local function commit_to_string(commit) + local result = { + 'commit ' .. commit.commit .. '\n', + 'Author: ' .. commit.author .. '\n', + 'Date: ' .. commit.author_date .. '\n', + '\n', + } + -- Indent the commit message by four spaces. + for line in string.gmatch(commit.message, '([^\n]*\n)') do + result[#result + 1] = ' ' .. line + end + return table.concat(result) +end +-- Abort execution after logging the commit message to stderr. +local function assert_commit(commit, cond, ...) + if cond then + -- Return all function arguments except the first. + return cond, ... + end + local message = ... -- Extract first variadic argument. + local err = message or 'assertion failure' + io.stderr:write('error in commit message: ' .. err .. '\n\n' + .. commit_to_string(commit) + .. '\n') + error(err) +end +-- Test for assert_commit. +assert(3 == #{assert_commit(false, 1, 2, 3)}) + +-- Go through the relevant commits in patchgit.commits. Set +-- patchgit.start_commit_index_for_changelog and +-- patchgit.start_commit_index for future use by process_commits below. +local function parse_commit_messages() + -- If already invoked, do not parse the commits again. + if patchgit.start_commit_index then + return + end + + local commits = patchgit.commits + + -- Cache the parsed trailers for forward traversal. + local trailers = {} + local start_commit = 1 + + local patchgit_version + + -- These are set to true once enough commits have been found to + -- compute their values. + local version_known = false + local release_known = false + local changelog_known = false + + for i=#patchgit.commits,1,-1 do + local commit = patchgit.commits[i] + + local trailer = assert_commit(commit, parse_trailer(commit.message)) + if trailer.parent then + if i == 1 then + assert_commit(commit, false, 'Parent tag in commit without parent') + elseif commits[i - 1].commit ~= trailer.parent then + assert_commit(commit, false, 'found unexpected parent commit ' + .. commits[i - 1].commit) + end + end + + trailers[i] = trailer + + patchgit_version = trailer.patchgit_version + if patchgit_version then + assert_commit(commit, patchgit_version == 1, + 'unsupport patch-git version ' .. patchgit_version) + end + + -- Stop iterating if all data can be determined from the commits seen. + if trailer.rpm_version then + version_known = true + end + if trailer.rpm_release then + release_known = true + end + if trailer.rpm_changelog_stop then + changelog_known = true + if not patchgit.start_commit_index_for_changelog then + patchgit.start_commit_index_for_changelog = i + end + end + + -- A Patch-Git-Version commit on its own does not tell us how to + -- interpret previous history. The first commit setting version/release + -- must also set the patch-git version. + if patchgit_version + and patchgit_version and release_known and changelog_known + then + start_commit = i + break + end + end + + assert_commit(commits[1], patchgit_version, 'RPM version not determined') + assert_commit(commits[1], version_known, 'RPM version not determined') + assert_commit(commits[1], release_known, 'RPM release not determined') + + assert(patchgit.start_commit_index_for_changelog) + patchgit.start_commit_index = start_commit + patchgit.commit_trailers = trailers +end + +-- +-- Returns a YYYY-MM-DD formatted date as the second result. +local git_date_to_rpm_date +do + local months = { + Jan=1, + Feb=2, + Mar=3, + Apr=4, + May=5, + Jun=6, + Jul=7, + Aug=8, + Sep=9, + Oct=10, + Nov=11, + Dec=12, + } + function git_date_to_rpm_date(s) + local wd, mon, d, y = string.match( + s, '^([A-z][a-z][a-z]) ([A-Z][a-z][a-z]) (%d+) %d%d:%d%d:%d%d (%d+)') + assert(y, s) + local m = assert(months[mon], s) + local rpmdate = string.format('%s %s %02d %04d', wd, mon, d, y) + local ymd = string.format('%04d-%02d-%02d', y, m, d) + return rpmdate, ymd + end +end +-- Tests for git_date_to_rpm_date. +do + local rpmdate, ymd + local rpmdate, ymd = assert(git_date_to_rpm_date( + 'Wed Jul 23 09:14:49 2025 +0200')) + assert(rpmdate == 'Wed Jul 23 2025') + assert(ymd == '2025-07-23') +end + +-- Quote RPM macro invocations in s and return the string. +local function rpm_quote(s) + -- Parentheses are needed to elide the second return value of string.gsub. + return (string.gsub(s, '%%', '%%%%')) +end +-- Tests for rpm_quote. +do + assert(rpm_quote('') == '') + assert(rpm_quote('abc') == 'abc') + assert(rpm_quote('%abc') == '%%abc') + assert(rpm_quote('a%bc') == 'a%%bc') + assert(rpm_quote('ab%c') == 'ab%%c') + assert(rpm_quote('abc%') == 'abc%%') + assert(rpm_quote('%%abc') == '%%%%abc') + assert(rpm_quote('a%%bc') == 'a%%%%bc') + assert(rpm_quote('ab%%c') == 'ab%%%%c') + assert(rpm_quote('abc%%') == 'abc%%%%') +end + +-- If changelog is not nil, it is used as a table of tables for +-- changelog entries. +local function process_commits(changelog, changelog_after_commit) + parse_commit_messages() + + local commits = patchgit.commits + local start_changelog = assert(patchgit.start_commit_index_for_changelog) + local trailers = assert(patchgit.commit_trailers) + + local rpm_version + + -- rpm_release_num is the rightmost number that needs to be + -- incremented for new RPM releases. Call set_release to change + -- and get_release to read. + local rpm_release_pre, rpm_release_num, rpm_release_post + local function set_release(rel) + rpm_release_pre, rpm_release_num, rpm_release_post = + assert_commit(commit, string.match(rel, '^(.-)(%d+)([^%d]*)$')) + end + local function get_release() + return rpm_release_pre .. rpm_release_num .. rpm_release_post + end + + local on_zstream = false + local last_changelog_rpmdate + local last_changelog_ymd + + -- This is set to true once changelog_after_commit is found + -- as a commit hash. + local include_commits_in_changelog = not changelog_after_commit + assert(not changelog_after_commit or #changelog_after_commit == 40) + + for i=assert(patchgit.start_commit_index), #commits do + local commit = commits[i] + local trailer = trailers[i] + local zstream_switch_request = trailer.rpm_branch_type == 'zstream' + + if trailer.rpm_version then + rpm_version = trailer.rpm_version + -- The version gets incremented below. + if on_zstream or zstream_switch_request then + set_release('1%{?dist}.0') + zstream_switch_request = false + else + set_release('0%{?dist}') + end + end + + if trailer.rpm_release then + set_release(trailer.rpm_release) + assert_commit(commit, get_release() == trailer.rpm_release, + 'RPM release parsing') + on_zstream = zstream_switch_request + -- Do not bump the release below. + else + if zstream_switch_request and not on_zstream then + assert_commit(commit, rpm_release_num, + 'RPM release not determined for zstream') + -- ZStream NVR policy: Release counter follows %{?dist}. + set_release(get_release() .. '.0') + on_zstream = true + end + if not trailer.rpm_skip_release then + assert_commit(commit, rpm_release_num, 'RPM release not determined') + rpm_release_num = rpm_release_num + 1 + end + end + + if changelog then + if not include_commits_in_changelog then + if commit.commit == changelog_after_commit then + -- Start including commits after this one. + include_commits_in_changelog = true + end + else + local cl_entries = assert_commit( + commit, rpm_changelog_default(commit.message, trailer)) + if #cl_entries > 0 then + -- The changelog list where the new entries are inserted. + local target_cl = changelog[#changelog] + + if not trailer.rpm_skip_release then + -- Changelog is not skipped. Generate a new header. + -- Use commit date because it gets updated when + -- cherry-picking. Avoid going backwards in time + -- because it generates warnings from RPM. + local rpmdate, ymd = git_date_to_rpm_date(commit.commit_date) + if last_changelog_ymd and ymd < last_changelog_ymd then + rpmdate = last_changelog_rpmdate + ymd = last_changelog_ymd + end + local author = commit.author + -- The %changelog section does not include the %dist macro. + local rpmrel = string.gsub(get_release(), '%%{%?dist}', '') + assert(not string.find(rpmrel, '%', 1, true), rpmrel) + local hdr = '* ' .. rpmdate .. ' ' .. author .. ' - ' + .. rpm_version .. '-' .. rpmrel + + -- Append a new changelog list. + target_cl = {hdr} + changelog[#changelog + 1] = target_cl + end + + -- Insert the changelog entries into the previous + -- changelog, at the end. No direct move because of % + -- quoting. If no new entry was inserted above, but we + -- switched to zstream, this adds to an entry that does not + -- have the expected release, which is a minor inconsistency. + assert_commit(commit, + target_cl and #target_cl > 0, + 'first commit skips changelog and has an entry') + for i=1,#cl_entries do + target_cl[#target_cl + 1] = '- ' .. cl_entries[i] + end + end + end + end + end + + assert_commit(commits[#commits], rpm_version, + 'RPM version not determined for HEAD') + assert_commit(commits[#commits], rpm_release_num, + 'RPM release not determined for HEAD') + patchgit.rpm_version = rpm_version + patchgit.rpm_release = get_release() +end + +---------------------------------------------------------------------- +-- Launching the actual work, and command line handling +---------------------------------------------------------------------- + +if rpm then + -- If running under rpm, generate the missing parts of the spec file. + generate_files() + parse_commits() + + function patchgit.release() + process_commits() + print(rpm.expand(patchgit.rpm_release)) + end + function patchgit.version() + process_commits() + print(rpm.expand(patchgit.rpm_version)) + end + function patchgit.changelog() + -- This can be defined to extract the final set of patches from + -- the spec file in a programmable manner. It is a replacement + -- for rpmspec --eval, which does not work as expected because + -- it is not evaluated in the context of the patch file. Do + -- this from the changelog writing procedure because at this + -- point, all Patch: directives in the spec file definitely have + -- been processed, so the patches global variable has been + -- populated. + local patches_log = rpm.expand('%{?_patchgit_log_patches}') + if patches_log ~= '' then + local fp = assert(io.open(patches_log, 'w+')) + for i=1,#patches do + local pname = assert(string.match(patches[i], '.*/([^/]+)$')) + fp:write('Patch' .. i .. ': ' .. pname .. '\n') + end + assert(fp:close()) + end + + local changelog = {} + process_commits(changelog) + for i=#changelog,1,-1 do + local cl = changelog[i] + for j=1,#cl do + -- RPM does not recursively macro-expand what we emit here, + -- so do not apply %-escaping. + print(cl[j], '\n') + end + print('\n') + end + end +else + local args = {...} + if #args == 0 then + args[1] = 'help' + end + local cmds = {} + function cmds.help() + print([[Available subcommands: + help this output + patches print list of PatchNNN: directives + version print the value of the computed RPM version at HEAD + release print the value of the computed RPM release at HEAD + verrel print the value of RPM version-release + changelog show the auto-generated changelog entries]]) + end + function cmds.patches(flag, extra) + if extra then + error('unrecognized argument: ' .. extra) + end + if flag and flag ~= '--history-only' then + error('unrecognized argument: ' .. flag) + end + generate_files() + parse_commits() + if flag == '--history-only' then + -- Only print the patches that come from the Git history. + patchgit.patches({history_only=true}) + else + -- There does not seem to be a way to do this in a better way. + -- Instruct one of the patch-git macros to write a temporary file. + with_temporary_file( + function(tmpfile) + check_rpmspec(get_single_spec_file(), + '-D _patchgit_log_patches ' .. tmpfile, + '-q', '--srpm', '--qf', '') + local fp = assert(io.open(tmpfile, 'r')) + assert(io.stdout:write(assert(fp:read('a')))) + assert(fp:close()) + end) + end + end + function cmds.version() + generate_files() + parse_commits() + process_commits() + print(patchgit.rpm_version) + end + function cmds.release() + generate_files() + parse_commits() + process_commits() + print(patchgit.rpm_release) + end + function cmds.verrel() + generate_files() + parse_commits() + process_commits() + print(patchgit.rpm_version .. '-' .. patchgit.rpm_release) + end + function cmds.changelog(start_commit) + if start_commit then + start_commit = + assert(string.match(run_git('rev-parse ' + .. shell_quote(start_commit)), + '^([^\n]+)\n')) + end + generate_files() + parse_commits() + local changelog = {} + process_commits(changelog, start_commit) + for i=#changelog,1,-1 do + local cl = changelog[i] + for j=1,#cl do + -- Apply %-escaping here, so that the output can be copy-pasted + -- into %changelog. + print(rpm_quote(cl[j])) + end + print() + end + end + function cmds.selftest() + -- Hidden command to run all subcommands. + local test_commands = {'patches --history-only', + 'changelog HEAD^'} + for k, _ in pairs(cmds) do + if k ~= 'selftest' then + test_commands[#test_commands + 1] = k + end + end + table.sort(test_commands) + local failure + for _, cmd in ipairs(test_commands) do + cmd = 'lua patch-git.lua ' .. cmd + print('* ' .. cmd) + local ok, term, status = os.execute(cmd) + if not ok or term ~= 'exit' or status ~= 0 then + failure = true + print('FAIL: term=' .. term .. ', status=' .. status) + end + end + if fail then + os.exit(1) + end + end + local cmd = cmds[args[1]] + if not cmd then + io.stderr:write('usage: Unrecognized command "' .. args[1] + .. '". Use "help" for a list of commands.\n') + os.exit(1) + end + cmd(table.unpack(args, 2)) +end