feat(8-git-uformat-patch): init

This commit is contained in:
Tiara Rodney 2025-06-04 16:17:39 +02:00
parent 2a4851b6a4
commit 5c60edb932
No known key found for this signature in database
GPG key ID: 5F43FAB4FBE5B5EB
2 changed files with 255 additions and 0 deletions

View file

@ -0,0 +1,55 @@
# Managing patches for package sources with Git
You're working on a package release for a software distribution, but you must
apply some patches to the distribution package sources for the packaging to
succeed.
You have checked out two repositories:
* a repository containing the source code of software you're packaging,
* a repository containing the distribution package sources (package spec,
etc.), referencing the software source code
You start patching files inside the build directory of the package.
Once the patch works, you commit it to the software sources.
Then use `git-uformat-patch` to generate patch files of the commits you've
made to the software sources and store them in the distribution package's
sources.
Clean the build directory and rerun the build after applying the patches through
the patch files you've just created. Rebasing, etc. works through the same
mechanism.
The patches are a shadow of the source code commits.
This way, you can
* manage patches atomically through Git,
* and prepare patches for upstream merge requests to the sources early, while
maintaining focus on packaging
Whether learning along the way and looking for tutoring by the software author,
or quickly making patches redundant through approved merge requests, atomicity
in patch management through Git allows to give concise historic context.
If you're packaging version 6 of some software, you would create patch files for
all commits between now and the release tag in the software sources of the
version you are packaging:
```
/source-repo $> sh git-uformat-patch.sh -o /distro-repo HEAD...v6
```
```
/distro-repo $> patch -i ./*.patch
```
But you can also create patch files for only the top 3 commits:
```
/source-repo $> GITLOGOPTS='-3' sh git-uformat-patch.sh -o /distro-repo HEAD...v6
```
Run `sh git-uformat-patch.sh -h`, for more information.

View file

@ -0,0 +1,200 @@
#!/usr/bin/env sh
GITDIFFOPTS="$GITDIFFOPTS -p"
usage() {
script=$(basename "$0")
cat << EOF
usage: $script [OPTIONS] GITREVRANGE
Prepare Git patches for distro packaging
Basically \`git format-patch\` as a POSIX-compliant shell script, but using
unified patch format instead of the UNIX mailbox format for output. The script
does patch enumeration and automatic filename generation with commit title and
patch number for commits of a given revision range selection.
Use a software's Git-controlled sources to manage and generate patches for
packaging it for a software distribution like ArchLinux, MSYS2, Ubuntu, etc.
While it is probably okay to make quick (monkey-)patches that fix the software
for a specific software distribution (and possibly break it for every other), it
is better to make proper surgical-precision patches and attempt to have them
accepted by the software developer ("upstream").
That's what this script helps with: Use Git on the sources, track patches as
commits for creating a merge request, while generating packagaging patch files
in unified-format for each commit, until the changes were accepted upstream.
Options
-h - show this message
-i INITIAL_PATCH_NUMBER - initial patch number [optional]
-o OUTPUT_DIRECTORY - output directory [optional]
Environment Variables:
AS_LIB - don't actually run the script. Can be used to
import functions from this shell script.
GITDIFFOPTS - optional arguments to supply to \`git diff\`
GITLOGOPTS - optional arguments to supply to \`git log\`
GITREVRANGE - revision range selector, e.g. HEAD...HEAD^
INITIAL_PATCH_NUMBER - number to start counting patches (upward) from
OUTPUT_DIRECTORY - directory to store generated patch files under
Examples:
Output to stdut
$script HEAD...v6
Output to directory
$script -o ../../msys2/MINGW-packages/mingw-w64-mypackage HEAD...v6
Start patch numbering at 5
$script -i 5 HEAD...v6
Pass output directory as environment variable
OUTPUT_DIRECTORY=some/dir $script -i 30 HEAD...v6
dot import a function and call it directly
AS_LIB=yes . $script
patch_from_commit -o some/dir 1a34e 1
Disable renames in diffs by passing \`git diff\` option
GITDIFFOPTS='--no-renames' $script HEAD...v6
Trim revision range selection to 3 by passing \`git log\` option
GITLOGOPTS="-3" sh $script 'HEAD'
See Also:
- https://www.msys2.org/wiki/Creating-Packages/#patch-software
- https://git-scm.com/docs/git-format-patch
- https://git-scm.com/docs/git-diff
- https://www.gnu.org/software/diffutils/manual/html_node/patch-Options.html
EOF
}
patch_from_commit() {
stdout=yes
while getopts "i:o:" opt; do
case $opt in
o) output_directory="$OPTARG";;
i) patch_number="$OPTARG";;
\?)
echo "Invalid option: -$OPTARG" >&2
return 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
return 2
;;
esac
done
shift $((OPTIND -1))
commit_id=$1
test -z "$patch_number" && patch_number=1
! test -z "$output_directory" && stdout=no
name="$(
git log --pretty=format:%s $commit_id -1 \
| sed -e 's|[^A-Za-z0-9_]|-|g' \
| sed -E 's|-*-|-|g' \
| sed -E 's|-$||'
)"
padded="0000$patch_number"
ppatchnumber="$(
echo $padded | sed "s|$(echo "$padded" | sed -e 's|....$||')||"
)"
path="$output_directory"/"$ppatchnumber-$name".patch
cmd="git diff $GITDIFFOPTS $commit_id^..$commit_id"
if ! test "$stdout" '=' 'no'; then
$cmd --color | tee /dev/null
else
echo "$(basename "$0"): $path"
$cmd --no-color | tee "$path"
fi
}
format_patch() {
local OPTIND
local OPTARG
while getopts "o:i:" opt; do
case $opt in
o) optargs_patch_from_commit="$optargs_patch_from_commit -o $OPTARG";;
i) patch_number=$OPTARG;;
\?)
echo "Invalid option: -$OPTARG" >&2
return 4
;;
:)
echo "Option -$OPTARG requires an argument." >&2
return 5
;;
esac
done
shift $((OPTIND -1))
unset OPTIND
unset OPTARG
test -z "$patch_number" && patch_number=1
for commit_id in $(git log $GITLOGOPTS --oneline $@ | cut -d ' ' -f1 | tac); do
git log $commit_id -1 >&2 | tee /dev/null
patch_from_commit $optargs_patch_from_commit -i $patch_number $commit_id \
| sed "s|^|$commit_id: |"
patch_number=$(expr $patch_number '+' 1)
done
}
while getopts "i:o:h" opt; do
case $opt in
o) OUTPUT_DIRECTORY="$OPTARG";;
i) INITIAL_PATCH_NUMBER=$OPTARG;;
h) usage; exit 0;;
:)
echo "Option -$OPTARG requires an argument." >&2
usage
exit 6
;;
esac
done
shift $((OPTIND -1))
unset OPTIND
unset OPTARG
! test -z "$1" && GITREVRANGE="$1"
test -z "$GITREVRANGE" && {
echo "error: missing first argument: Git revision range" >&2
exit 3
}
! test -z "$OUTPUT_DIRECTORY" && optargs="$optargs -o "$OUTPUT_DIRECTORY""
! test -z "$INITIAL_PATCH_NUMBER" && optargs="$optargs -i $INITIAL_PATCH_NUMBER"
! test "$AS_LIB" '=' 'yes' && format_patch $optargs "$GITREVRANGE"