diff --git a/8-git-uformat-patch/README.md b/8-git-uformat-patch/README.md new file mode 100644 index 0000000..50ce0cf --- /dev/null +++ b/8-git-uformat-patch/README.md @@ -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. diff --git a/8-git-uformat-patch/git-uformat-patch.sh b/8-git-uformat-patch/git-uformat-patch.sh new file mode 100644 index 0000000..b8674b0 --- /dev/null +++ b/8-git-uformat-patch/git-uformat-patch.sh @@ -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"