#!/usr/bin/env bash # # Apply a patch from a file, input, or a commit. # Copyright (c) Petr Baudis, 2005 # # This is basically just a smart patch wrapper. It handles stuff like # mode changes, removal of files vs. zero-size files etc. # # OPTIONS # ------- # -c:: Automatically commit the patch # Automatically extract the commit message and authorship information # (if provided) from the patch and commit it after applying it # successfuly. # # -C COMMIT:: Cherry-pick the given commit # Instead of applying a patch from stdin, apply and commit the patch # introduced by the given commit. This is basically an extension of # `cg-commit -c`. # # In combination with '-R', this does the opposite - it will revert # the given commit and then try to commit a revert commit - it will # prefill the headline and open the commit editor for you to write # further details. # # Note that even though this is functionally equivalent to the # cherry-picking concept present in other version control systems, # this does not play very well together with regular merges and if # you both cherry-pick and merge between two branches, the picking # may increase the number of conflicts you will get when merging. # # -d DIRNAME:: Apply all patches in directory # Instead of applying a patch from stdin, apply and separately commit # all patches in the specified directory. This can be used to import # a range of patches made by cg-mkpatch -d. Implies -c. # # -pN:: Strip path to the level N # Strip path of filenames in the diff to the level N. This works # exactly the same as in the `patch` tool except that the default # strip level is not infinite but 1 (or more if you are in a # subdirectory; in short, `cg-diff | cg-patch -R` and such always # works). # # -R:: Apply in reverse # Apply the patch in reverse (therefore effectively unapply it). # # --resolved:: Resume -d after conflicts were resolved # In case '-d' failed in some patch in the middle with conflicts and # you resolved them, running cg-patch with with the '-d' argument # as well as '--resolved' will cause it to pick up where it dropped off # and go on applying. (This includes committing the failed patch; # do not commit it on your own!) # # -u:: Assume unified diff instead of GIT diff # Make `cg-patch` assume the patch on the input is a classic unified # diff instead of a diff produced by GIT or Cogito. This means only # that file adds and removals will be recorded even if the patch file # does not explicitly describe them. Use this if the patch was not # produced by `cg-diff` or similar but by a traditional `diff` tool. # # Takes the diff on stdin (unless specified otherwise). USAGE="cg-patch [-c] [-C COMMIT] [-pN] [-R] [-u] [OTHER_OPTIONS] < PATCH" . "${COGITO_LIB}"cg-Xlib || exit 1 set -m # Force enable job control for our patch+wait pair redzone_reset() { redzone= origmode= newmode= op= } strip_path() { read -r path if [ x"$path" = x"/dev/null" ]; then echo "$path" return fi echo "$path" | sed 's#^\([^/]*/\)\{'$strip'\}##' } redzone_border() { [ "$redzone" ] || return if [ $strip -gt 0 ] && [ x"$file1" != x"/dev/null" ] && [ x"$(echo "$file1" | strip_path)" = x"$file1" ]; then echo "$file1: too shallow a filename for -p$strip, ignoring" >&2 return fi if [ $strip -gt 0 ] && [ x"$file2" != x"/dev/null" ] && [ x"$(echo "$file2" | strip_path)" = x"$file2" ]; then echo "$file2: too shallow a filename for -p$strip, ignoring" >&2 return fi local sfile1="${file1#*/}" sfile2="${file2#*/}" if [ $strip -gt 1 ] && [ x"$file1" != x"/dev/null" ] && [ "$_git_relpath" ] && [ x"${sfile1#$_git_relpath}" = x"$sfile1" ]; then warn "$file1 was not located in your current subdirectory before stripping" fi if [ $strip -gt 1 ] && [ x"$file2" != x"/dev/null" ] && [ "$_git_relpath" ] && [ x"${sfile2#$_git_relpath}" = x"$sfile2" ] && [ x"$file2" != x"$file1" ]; then warn "$file2 was not located in your current subdirectory before stripping" fi if [ "$op" = "delete" ]; then torm="$(echo "$file1" | strip_path)" if ! [ "$reverse" ]; then (git-ls-files | fgrep -qx "$torm") && echo -ne "rm\0$torm\0" redzone_reset return else (git-ls-files | fgrep -qx "$torm") || echo -ne "add\0$torm\0" fi elif [ "$op" = "add" ]; then toadd="$(echo "$file2" | strip_path)" if ! [ "$reverse" ]; then (git-ls-files | fgrep -qx "$toadd") || echo -ne "add\0$toadd\0" else (git-ls-files | fgrep -qx "$toadd") && echo -ne "rm\0$toadd\0" redzone_reset return fi fi if [ "$origmode" != "$newmode" ]; then if ! [ "$reverse" ]; then tocm=$(echo "$file2" | strip_path) mode="$newmode" else tocm=$(echo "$file1" | strip_path) mode="$origmode" fi echo -ne "cm\0 $mode\000$tocm\0" fi redzone_reset } lookover_patch() { local file="$1" where="$2" local author="$(sed -n '/^\(---\|-- \)$/,$p' < "$file" | sed -n '/^author /p')" [ "$author" ] || warn "no author info found$where, assuming your authorship" eval "$(echo "$author" | pick_author)" } commit_patch() { local file="$1" sed '/^\(---\|-- \|diff --git .*\)$/,$d' < "$file" | cg-commit } resume_filter() { sed "0,/\/$(echo "$lastpatch" | sed 's#/#\\/#g')$/d" } commitid= commit= commitdir= strip=$((1+$(echo "$_git_relpath" | tr -cd / | wc -c))) reverse= resolved= unidiff= while optparse; do if optparse -C=; then commitid="$(cg-object-id -c "$OPTARG")" || exit 1 commitparent="$(cg-object-id -p "$commitid")" || exit 1 [ -z "$commitparent" ] && die "cannot pick initial commit" [ "$(echo "$commitparent" | wc -l)" -gt 1 ] && die "refusing to pick merge commits" elif optparse -c; then commit=1 elif optparse -d=; then commitdir="$(echo "$OPTARG" | sed 's,/*$,,')" [ -d "$commitdir" ] || die "$commitdir: not a directory" elif optparse -p=; then strip="$OPTARG" [ -n "$(echo "$strip" | tr -d 0-9)" ] && die "the -p argument must be numeric" elif optparse --resolved; then resolved=1 elif optparse -R; then reverse=1 elif optparse -u; then unidiff=1 else optfail fi done [ "$resolved" ] && [ -z "$commitdir" ] && die "--resolved can be passed only with -d" if [ "$commitid" ] || [ "$commit" ] || [ "$commitdir" ]; then [ "$_git_relpath" ] && die "must be ran from project root" if [ "$commitid" ]; then [ "$unidiff" ] && die "-u does not make sense here" [ $strip -ne 1 ] && die "-p does not make sense here" files="$(mktemp -t gitpatch.XXXXXX)" git-diff-tree -m -r "$commitparent" "$commitid" | cut -f 2- >"$files" if [ -n "$(git-diff-index -m -r HEAD | cut -f 2- | join "$files" -)" ]; then rm "$files" die "cherry-pick blocked by local changes" fi eval "afiles=($(cat "$files" | sed -e 's/"\|\\/\\&/g' -e 's/^.*$/"&"/'))" rm "$files" ciargs=() if ! [ "$reverse" ]; then ciargs=(-c "$commitid") else ciargs=(-m "Revert ${commitid:0:12}" -e) reverse=-R fi cg-diff -p -r "$commitid" | cg-patch $reverse || exit 1 cg-commit "${ciargs[@]}" "${afiles[@]}" fi if [ "$commit" ]; then [ "$reverse" ] && die "cannot do -R here" [ "$(git-diff-index -m -r HEAD)" ] && die "cannot auto-commit patches when the tree has local changes" file="$(mktemp -t gitpatch.XXXXXX)" cat >"$file" lookover_patch "$file" cg-patch <"$file" || exit 1 commit_patch "$file" rm "$file" fi if [ "$commitdir" ]; then [ "$reverse" ] && die "cannot do -R here" resume="$commitdir/.cg-patch-resume" if [ -s "$resume" ]; then if [ ! "$resolved" ]; then echo "cg-patch: previous import in progress" >&2 echo "Use --resolved to resume after conflicts." >&2 echo "Cancel the resume by deleting the file $resume" >&2 exit 1 fi echo "Resuming import of $commitdir:" filter=resume_filter lastpatch="$(cat "$resume")" echo "* $lastpatch" commit_patch "$commitdir/$lastpatch" rm -f "$resume" elif [ "$(git-diff-index -m -r HEAD)" ]; then die "cannot auto-commit patches when the tree has local changes" else echo "Importing $commitdir:" filter=cat fi find "$commitdir" -name '[0-9]*-*' | "$filter" | \ while read -r file; do echo "* ${file#$commitdir/}" lookover_patch "$file" " in $file" if ! cg-patch < "$file"; then echo "${file#$commitdir/}" > "$resume" echo "cg-patch: conflicts during import" >&2 echo "Rerun cg-patch with the --resolved argument to resume after resolving them." >&2 echo "Cancel the resume by deleting the file $resume" >&2 exit 1 fi commit_patch "$file" done fi exit fi # We want to run patch in the subdirectory and at any rate protect other # parts of the tree from inadverent pollution. [ -n "$_git_relpath" ] && cd "$_git_relpath" [ "$unidiff" ] && newsfile="$(mktemp -t gitapply.XXXXXX)" gonefile="$(mktemp -t gitapply.XXXXXX)" todo="$(mktemp -t gitapply.XXXXXX)" patchfifo="$(mktemp -t gitapply.XXXXXX)" rm "$patchfifo" && mkfifo -m 600 "$patchfifo" [ "$unidiff" ] && git-ls-files --others >"$newsfile" git-ls-files --deleted >"$gonefile" # patch file removal behaviour cannot be sensibly controlled, so we # just handle it all ourselves. patch_args="-p$strip -N" [ "$reverse" ] && patch_args="$patch_args -R" patch $patch_args <"$patchfifo" & tee "$patchfifo" | { redzone_reset while read -r line; do if [ "${line:0:10}" = "diff --git" ]; then redzone_border # The diff line is fundamentally unsafe wrt. spaces, # nothing we can do here. cmd="$(echo "$line" | sed 's/^diff --git //')" # TODO: Simplify. file1="$(bash -c 'echo $1' padding $cmd)" file2="$(bash -c 'echo $2' padding $cmd)" redzone=1 continue fi if [ "$redzone" ] && [ "${line#[nod]}" != "$line" ]; then mode="$(echo "$line" | awk ' /^deleted file mode [0-9]+/ {print "D-"$4} /^old mode [0-9]+/ {print "-"$3} /^new file mode [0-9]+/ {print "A+"$4} /^new mode [0-9]+/ {print "+"$3} ')" if [ "${mode:0:1}" = "D" ]; then op=delete mode="${mode:1}" elif [ "${mode:0:1}" = "A" ]; then op=add mode="${mode:1}" fi if [ "${mode:0:1}" = "-" ]; then origmode="${mode:1}" elif [ "${mode:0:1}" = "+" ]; then newmode="${mode:1}" fi continue fi done redzone_border } >$todo wait %1; ret=$? IFS=$'\n' emptyfiles=($(git-ls-files --deleted | join -v 2 "$gonefile" -)) if [ "$unidiff" ]; then [ "${emptyfiles[*]}" ] && cg-rm "${emptyfiles[@]}" IFS=$'\n' freshfiles=($(git-ls-files --others | join -v 2 "$newsfile" -)) [ "${freshfiles[*]}" ] && cg-add "${freshfiles[@]}" else # Now we just recreate all the supposedly deleted files and # kill only those who really are gone. # # This is done on the assumption that we are never going to have # too many files deleted in the first place anyway. [ "${emptyfiles[*]}" ] && touch "${emptyfiles[@]}" fi cat "$todo" | xargs -0 bash -c ' while [ "$1" ]; do op="$1"; shift; case "$op" in "add") cg-add "$1"; shift;; "rm") cg-rm -f "$1"; shift;; "cm") mode="$1"; shift # $mode contains leading space due to echo braindamage if [ "${mode:(-3):1}" = "7" ]; then mask="$(printf %o $((8#777&~8#$(umask))))" else mask="$(printf %o $((8#666&~8#$(umask))))" fi chmod "$mask" "$1"; shift;; esac done ' padding rm "$patchfifo" "$todo" "$gonefile" [ "$unidiff" ] && rm "$newsfile" exit $ret