#!/usr/bin/bash
#   pacdiff : a simple pacnew/pacsave updater
#
#   Copyright (c) 2007 Aaron Griffin <aaronmgriffin@gmail.com>
#   Copyright (c) 2013-2016 Pacman Development Team <pacman-dev@archlinux.org>
#
#   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 2 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 <https://www.gnu.org/licenses/>.
#

shopt -s extglob

declare -r myname='pacdiff'
declare -r myver='1.10.6'

LIBRARY=${LIBRARY:-'/usr/share/makepkg'}

diffprog=${DIFFPROG:-'vim -d'}
diffsearchpath=${DIFFSEARCHPATH:-/etc}
mergeprog=${MERGEPROG:-'diff3 -m'}
cachedirs=()
USE_COLOR='y'
SUDO=''
declare -a oldsaves
declare -i USE_FIND=0 USE_LOCATE=0 USE_PACDB=0 OUTPUTONLY=0 BACKUP=0 THREE_WAY_DIFF=0

# Import libmakepkg
# shellcheck source=/dev/null
source "$LIBRARY"/util/message.sh

die() {
	error "$@"
	exit 1
}

usage() {
	cat <<EOF
${myname} v${myver}

pacorig, pacnew and pacsave maintenance utility.

Usage: $myname [options]

Search Options:   select one (default: --pacmandb)
  -f, --find      scan using find
  -l, --locate    scan using locate
  -p, --pacmandb  scan active config files from pacman database

General Options:
  -b, --backup          when overwriting, save old files with .bak
  -c, --cachedir <dir>  scan "dir" for 3-way merge base candidates
                        (default: read from /etc/pacman.conf)
      --nocolor         do not colorize output
  -o, --output          print files instead of merging them
  -s, --sudo            use sudo and sudoedit to merge/remove files
  -3, --threeway        view diffs in 3-way fashion
  -h, --help            show this help message and exit
  -V, --version         display version information and exit

Environment Variables:
  DIFFPROG        override the merge program (default: 'vim -d')
  DIFFSEARCHPATH  override the search path (only when using find)
                  (default: /etc)
  MERGEPROG       override the 3-way merge program (default: 'diff3 -m')

Example: DIFFPROG=meld DIFFSEARCHPATH="/boot /etc /usr" MERGEPROG="git merge-file -p" $myname
Example: $myname --output --locate
EOF
}

version() {
	printf "%s %s\n" "$myname" "$myver"
	echo 'Copyright (C) 2007 Aaron Griffin <aaronmgriffin@gmail.com>'
	echo 'Copyright (C) 2013-2016 Pacman Development Team <pacman-dev@archlinux.org>'
}

print_existing() {
	[[ -f "$1" ]] && printf '%s\0' "$1"
}

print_existing_pacsave(){
	for f in "${1}"?(.+([0-9])); do
		[[ -f $f ]] && printf '%s\0' "$f"
	done
}

base_cache_tar() {
	package="$1"

	for cachedir in "${cachedirs[@]}"; do
		pushd "$cachedir" &>/dev/null || {
			error "failed to chdir to '%s', skipping" "$cachedir"
			continue
		}

		find "$PWD" -name "$package-[0-9]*.pkg.tar*" ! -name '*.sig' |
			pacsort --files --reverse | sed -ne '2p'

		popd &>/dev/null || exit
	done
}

diffprog_fn() {
	if [[ -n "$SUDO" ]]; then
		SUDO_EDITOR="$diffprog" sudoedit "$@"
	else
		$diffprog "$@"
	fi
}

view_diff() {
	pacfile="$1"
	file="$2"

	package="$(pacman -Qoq "$file")" || return 1
	base_tar="$(base_cache_tar "$package")"

	two_way_diff() {
		diffprog_fn "$pacfile" "$file"
	}

	three_way_diff() {
		diffprog_fn "$pacfile" "$base" "$file"
	}

	unset tempdir

	if (( ! THREE_WAY_DIFF )); then
		two_way_diff
	elif [[ -z $base_tar ]]; then
		msg2 "Unable to find a base package. falling back to 2-way diff."
		two_way_diff
	else
		basename="$(basename "$file")"
		tempdir="$(mktemp -d --tmpdir "pacdiff-diff-$basename.XXX")"
		base="$(mktemp "$tempdir"/"$basename.base.XXX")"
		merged="$(mktemp "$tempdir"/"$basename.merged.XXX")"

		if ! bsdtar -xqOf "$base_tar" "${file#/}" >"$base"; then
			msg2 "Unable to extract the previous version of this file. falling back to 2-way diff."
			two_way_diff
		else
			three_way_diff
		fi
	fi

	ret=1

	if cmp -s "$pacfile" "$file"; then
		msg2 "Files are identical, removing..."
		$SUDO rm -v "$pacfile"
		ret=0
	fi

	$SUDO rm -rf "$tempdir"
	return $ret
}

merge_file() {
	pacfile="$1"
	file="$2"

	package="$(pacman -Qoq "$file")" || return 1
	base_tar="$(base_cache_tar "$package")"

	if [[ -z $base_tar ]]; then
		msg2 "Unable to find a base package."
		return 1
	fi

	basename="$(basename "$file")"
	tempdir="$(mktemp -d --tmpdir "pacdiff-merge-$basename.XXX")"
	base="$(mktemp "$tempdir"/"$basename.base.XXX")"
	merged="$(mktemp "$tempdir"/"$basename.merged.XXX")"

	if ! /usr/bin/bsdtar -xqOf "$base_tar" "${file#/}" >"$base"; then
		msg2 "Unable to extract the previous version of this file."
		return 1
	fi

	if $mergeprog "$file" "$base" "$pacfile" >"$merged"; then
		msg2 "Merged without conflicts."
	fi

	$diffprog "$file" "$merged"

	while :; do
		ask "Would you like to use the results of the merge? [y/n] "

		read -r c || return 1
		case $c in
			y|Y) break ;;
			n|N) return 1 ;;
			*) msg2 "Invalid answer." ;;
		esac
	done

	if ! $SUDO cp -v "$merged" "$file"; then
		warning "Unable to write merged file to %s. Merged file is preserved at %s" "$file" "$merged"
		return 1
	fi
	$SUDO rm -rv "$pacfile" "$tempdir"
	return 0
}

cmd() {
	if (( USE_LOCATE )); then
		# plocate searches for files that match all patterns whereas mlocate searches for files that match one or more patterns
		if type -p plocate >/dev/null; then
			for p in \*.pacnew \*.pacorig \*.pacsave '*.pacsave.[0-9]*'; do
				locate -0 -e -b "$p"
			done
		else
			locate -0 -e -b \*.pacnew \*.pacorig \*.pacsave '*.pacsave.[0-9]*'
		fi
	elif (( USE_FIND )); then
		find "$diffsearchpath" \( -name \*.pacnew -o -name \*.pacorig -o -name \*.pacsave -o -name '*.pacsave.[0-9]*' \) -print0
	elif (( USE_PACDB )); then
		awk '/^%BACKUP%$/ {
		while (getline) {
			if (/^$/) { nextfile }
			print $1
		}
		}' "${pac_db}"/*/files | while read -r bkup; do
			print_existing "/$bkup.pacnew"
			print_existing "/$bkup.pacorig"
			print_existing_pacsave "/$bkup.pacsave"
		done
	fi
}

while [[ -n "$1" ]]; do
	case "$1" in
		-f|--find)
			USE_FIND=1;;
		-l|--locate)
			USE_LOCATE=1;;
		-p|--pacmandb)
			USE_PACDB=1;;
		-b|--backup)
			BACKUP=1;;
		-c|--cachedir)
			cachedirs+=("$2"); shift;;
		--nocolor)
			USE_COLOR='n';;
		-o|--output)
			OUTPUTONLY=1;;
		-s|--sudo)
			SUDO=sudo;;
		-3|--threeway)
			THREE_WAY_DIFF=1 ;;
		-V|--version)
			version; exit 0;;
		-h|--help)
			usage; exit 0;;
		*)
			usage; exit 1;;
	esac
	shift
done

# check if messages are to be printed using color
if [[ -t 2 && $USE_COLOR != "n" ]]; then
	colorize
else
	unset ALL_OFF BOLD BLUE GREEN RED YELLOW
fi

if ! type -p "${diffprog%% *}" >/dev/null && (( ! OUTPUTONLY )); then
	die "Cannot find the $diffprog binary required for viewing differences."
fi

if ! type -p "${mergeprog%% *}" >/dev/null && (( ! OUTPUTONLY )); then
	die "Cannot find the $mergeprog binary required for merging differences."
fi

case $(( USE_FIND + USE_LOCATE + USE_PACDB )) in
	0) USE_PACDB=1;; # set the default search option
	[^1]) error "Only one search option may be used at a time"
	 	usage; exit 1;;
esac

if (( USE_PACDB )); then
	if ! DBPath="$(pacman-conf DBPath)"; then
		error "unable to read /etc/pacman.conf"
		usage; exit 1
	fi
	pac_db="${DBPath:-/var/lib/pacman}/local"
	if [[ ! -d "${pac_db}" ]]; then
		error "unable to read pacman database %s". "${pac_db}"
		usage; exit 1
	fi
fi

if [[ -z ${cachedirs[*]} ]]; then
	readarray -t cachedirs < <(pacman-conf CacheDir)
fi

# see https://mywiki.wooledge.org/BashFAQ/020
while IFS= read -u 3 -r -d '' pacfile; do
	file="${pacfile%.pac*}"
	file_type="pac${pacfile##*.pac}"

	if (( OUTPUTONLY )); then
		echo "$pacfile"
		continue
	fi

	# add matches for pacsave.N to oldsaves array, do not prompt
	if [[ $file_type = pacsave.+([0-9]) ]]; then
		oldsaves+=("$pacfile")
		continue
	fi

	msg "%s file found for %s" "$file_type" "$file"
	if [ ! -f "$file" ]; then
		warning "$file does not exist"
		$SUDO rm -iv "$pacfile"
		continue
	fi

	if cmp -s "$pacfile" "$file"; then
		msg2 "Files are identical, removing..."
		$SUDO rm -v "$pacfile"
	else
		while :; do
			ask "(V)iew, (M)erge, (S)kip, (R)emove %s, (O)verwrite with %s, (Q)uit: [v/m/s/r/o/q] " "$file_type" "$file_type"
			read -r c || break
			case $c in
				q|Q) exit 0;;
				r|R) $SUDO rm -v "$pacfile"; break ;;
				o|O)
					if (( BACKUP )); then
						$SUDO cp -v "$file" "$file.bak"
					fi
					$SUDO mv -v "$pacfile" "$file"
					break ;;
				v|V)
					if view_diff "$pacfile" "$file"; then
						break
					fi ;;
				m|M)
					if merge_file "$pacfile" "$file"; then
						break
					fi ;;
				s|S) break ;;
				*) msg2 "Invalid answer." ;;
			esac
		done
	fi
done 3< <(cmd)

(( ${#oldsaves[@]} )) && warning "Ignoring %s" "${oldsaves[@]}"

exit 0
