#!/bin/sh
#
# git-remote-gcrypt
#
# 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) version 2 or 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 .
#
# See README.rst for usage instructions
set -e # errexit
set -u # nounset
set -f # noglob
set -C # noclobber
export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked
Gref="refs/gcrypt/gitception$GITCEPTION"
Gref_rbranch="refs/heads/master"
Packkey_bytes=63 # nbr random bytes for packfile keys, any >= 256 bit is ok
Hashtype=SHA256 # SHA512 SHA384 SHA256 SHA224 supported.
Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a
Hex40="[a-f0-9]"
Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40
Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40 # Match SHA-1 hexdigest
Did_find_repo= # yes for connected, no for no repo
Localdir="${GIT_DIR:=.git}/remote-gcrypt"
Tempdir=
Repoid=
Refslist=
Packlist=
Keeplist=
Extnlist=
Repack_limit=25
Recipients=
# compat/utility functions
# xfeed: The most basic output function puts $1 into the stdin of $2..$#
xfeed()
{
local input_=
input_=$1; shift
"$@" <&2; }
echo_die() { echo_info "$@" ; exit 1; }
isnull() { case "$1" in "") return 0;; *) return 1;; esac; }
isnonnull() { ! isnull "$1"; }
iseq() { case "$1" in "$2") return 0;; *) return 1;; esac; }
isnoteq() { ! iseq "$1" "$2"; }
negate() { ! "$@"; }
# Execute $@ or die
pipefail()
{
"$@" || { echo_info "'$1' failed!"; kill $$; exit 1; }
}
isurl() { isnull "${2%%$1://*}"; }
islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; }
xgrep() { command grep "$@" || : ; }
# setvar is used for named return variables
# $1 *must* be a valid variable name, $2 is any value
#
# Conventions
# return variable names are passed with a @ prefix
# return variable functions use f_ prefix local vars
# return var consumers use r_ prefix vars (or Titlecase globals)
setvar()
{
isnull "${1##@*}" || echo_die "Missing @ for return variable: $1"
eval ${1#@}=\$2
}
Newline="
"
# $1 is return var, $2 is value appended with newline separator
append_to()
{
local f_append_tmp_=
eval f_append_tmp_=\$${1#@}
isnull "$f_append_tmp_" || f_append_tmp_=$f_append_tmp_$Newline
setvar "$1" "$f_append_tmp_$2"
}
# Pick words from each line
# $1 return variable name
# $2 input value
pick_fields_1_2()
{
local f_ret= f_one= f_two=
while read f_one f_two _ # from << here-document
do
f_ret="$f_ret$f_one $f_two$Newline"
done </dev/null &&
obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" &&
isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: ||
{ ret_=false && : ; }
[ -e "$fet_head.$$~" ] && command mv -f "$fet_head.$$~" "$fet_head" || :
$ret_
}
anon_commit()
{
GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \
GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \
GIT_COMMITTER_EMAIL="root@localhost" \
GIT_COMMITTER_DATE="1356994801 -0400" \
git commit-tree "$@" </dev/null >&2 || :
git rev-parse -q --verify "$Gref" >/dev/null && return 0 ||
commit_id=$(anon_commit "$empty_tree") &&
git update-ref "$Gref" "$commit_id"
}
## end gitception
# Fetch repo $1, file $2, tmpfile in $3
GET()
{
if isurl sftp "$1"
then
(exec 0>&-; curl -s -S -k "$1/$2") > "$3"
elif isurl rsync "$1"
then
(exec 0>&-; rsync -I -W "${1#rsync://}"/"$2" "$3" >&2)
elif islocalrepo "$1"
then
cat "$1/$2" > "$3"
else
gitception_get "${1#gitception://}" "$2" > "$3"
fi
}
# Put repo $1, file $2 or fail, tmpfile in $3
PUT()
{
if isurl sftp "$1"
then
curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2"
elif isurl rsync "$1"
then
rsync -I -W "$3" "${1#rsync://}"/"$2" >&2
elif islocalrepo "$1"
then
cat >| "$1/$2" < "$3"
else
gitception_put "${1#gitception://}" "$2" < "$3"
fi
}
# Put all PUT changes for repo $1 at once
PUT_FINAL()
{
if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch"
fi
}
# Put directory for repo $1
PUTREPO()
{
if isurl sftp "$1"
then
:
elif isurl rsync "$1"
then
rsync -q -r --exclude='*' "$Localdir/" "${1#rsync://}" >&2
elif islocalrepo "$1"
then
mkdir -p "$1"
else
gitception_new_repo "${1#gitception://}"
fi
}
# For repo $1, delete all newline-separated files in $2
REMOVE()
{
local fn_=
if isurl sftp "$1"
then
# FIXME
echo_info "sftp: Ignore remove request $1/$2"
elif isurl rsync "$1"
then
xfeed "$2" rsync -I -W -v -r --delete --include-from=- \
--exclude='*' "$Localdir"/ "${1#rsync://}/" >&2
elif islocalrepo "$1"
then
for fn_ in $2; do
rm -f "$1"/"$fn_"
done
else
for fn_ in $2; do
gitception_remove "${1#gitception://}" "$fn_"
done
fi
}
CLEAN_FINAL()
{
if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1"
then
:
else
git update-ref -d "$Gref" || :
fi
}
ENCRYPT()
{
gpg --batch --force-mdc --compress-algo none --trust-model=always --passphrase-fd 3 -c 3<&1 &&
status_=$(gpg --status-fd 3 -q -d 3>&1 1>&4) &&
xfeed "$status_" grep "^\[GNUPG:\] ENC_TO " >/dev/null &&
(xfeed "$status_" grep -e "$1" >/dev/null || {
echo_info "Failed to verify manifest signature!" &&
echo_info "Only accepting signatories: ${2:-(none)}" &&
return 1
})
}
# Generate $1 random bytes
genkey()
{
gpg --armor --gen-rand 1 "$1"
}
gpg_hash()
{
local hash_=
hash_=$(gpg --with-colons --print-md "$1" | tr A-F a-f)
hash_=${hash_#:*:}
xecho "${hash_%:}"
}
# Pass the branch/ref by pipe to git
safe_git_rev_parse()
{
git cat-file --batch-check 2>/dev/null |
xgrep -v "missing" | cut -f 1 -d ' '
}
make_new_repo()
{
echo_info "Setting up new repository"
PUTREPO "$URL"
# Needed assumption: the same user should have no duplicate Repoid
Repoid=":id:$(genkey 15)"
iseq "${NAME#gcrypt::}" "$URL" ||
git config "remote.$NAME.gcrypt-id" "$Repoid"
echo_info "Remote ID is $Repoid"
Extnlist="extn comment"
}
# $1 return var for goodsig match, $2 return var for signers text
read_config()
{
local recp_= r_keyinfo= r_keyfpr= gpg_list= cap_= conf_part= good_sig= signers_=
Conf_signkey=$(git config --get "remote.$NAME.gcrypt-signingkey" '.+' ||
git config --path user.signingkey || :)
conf_part=$(git config --get "remote.$NAME.gcrypt-participants" '.+' ||
git config --get gcrypt.participants '.+' || :)
# Figure out which keys we should encrypt to or accept signatures from
if isnull "$conf_part" || iseq "$conf_part" simple
then
signers_="(default keyring)"
Recipients="--throw-keyids --default-recipient-self"
good_sig="^\[GNUPG:\] GOODSIG "
setvar "$1" "$good_sig"
setvar "$2" "$signers_"
return 0
fi
for recp_ in $conf_part
do
gpg_list=$(gpg --with-colons --fingerprint -k "$recp_")
filter_to @r_keyinfo "pub*" "$gpg_list"
filter_to @r_keyfpr "fpr*" "$gpg_list"
isnull "$r_keyinfo" || isnonnull "${r_keyinfo##*"$Newline"*}" ||
echo_info "WARNING: '$recp_' matches multiple keys, using one"
isnull "$r_keyfpr" || isnonnull "${r_keyfpr##*"$Newline"*}" ||
echo_info "WARNING: '$recp_' matches multiple fingerprints, using one"
r_keyinfo=${r_keyinfo%%"$Newline"*}
r_keyfpr=${r_keyfpr%%"$Newline"*}
keyid_=$(xfeed "$r_keyinfo" cut -f 5 -d :)
fprid_=$(xfeed "$r_keyfpr" cut -f 10 -d :)
isnonnull "$fprid_" &&
signers_="$signers_ $keyid_" &&
append_to @good_sig "^\[GNUPG:\] VALIDSIG .*$fprid_$" || {
echo_info "WARNING: Skipping missing key $recp_"
continue
}
# Check 'E'ncrypt capability
cap_=$(xfeed "$r_keyinfo" cut -f 12 -d :)
iseq "${cap_#*E}" "$cap_" || Recipients="$Recipients -R $keyid_"
done
if isnull "$Recipients"
then
echo_info "You have not configured any keys you can encrypt to" \
"for this repository"
echo_info "Use ::"
echo_info " git config gcrypt.participants YOURKEYID"
exit 1
fi
setvar "$1" "$good_sig"
setvar "$2" "$signers_"
}
ensure_connected()
{
local manifest_= r_repoid= r_name= url_frag= r_sigmatch= r_signers= \
tmp_manifest=
if isnonnull "$Did_find_repo"
then
return
fi
Did_find_repo=no
read_config @r_sigmatch @r_signers
iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME
if isurl gitception "$URL" && isnonnull "$r_name"; then
git config "remote.$r_name.url" "gcrypt::${URL#gitception://}"
echo_info "Updated URL for $r_name, gitception:// -> ()"
fi
# Find the URL fragment
url_frag=${URL##*"#"}
isnoteq "$url_frag" "$URL" || url_frag=
URL=${URL%"#$url_frag"}
# manifestfile -- sha224 hash if we can, else the default location
if isurl sftp "$URL" || islocalrepo "$URL" || isurl rsync "$URL"
then
# not for gitception
isnull "$url_frag" ||
Manifestfile=$(xecho_n "$url_frag" | gpg_hash SHA224)
else
isnull "$url_frag" || Gref_rbranch="refs/heads/$url_frag"
fi
Repoid=
isnull "$r_name" ||
Repoid=$(git config "remote.$r_name.gcrypt-id" || :)
tmp_manifest="$Tempdir/maniF"
GET "$URL" "$Manifestfile" "$tmp_manifest" 2>/dev/null || {
echo_info "Repository not found: $URL"
return 0
}
Did_find_repo=yes
echo_info "Decrypting manifest"
manifest_=$(PRIVDECRYPT "$r_sigmatch" "$r_signers" < "$tmp_manifest") &&
isnonnull "$manifest_" ||
echo_die "Failed to decrypt manifest!"
rm -f "$tmp_manifest"
filter_to @Refslist "$Hex40 *" "$manifest_"
filter_to @Packlist "pack :*:* *" "$manifest_"
filter_to @Keeplist "keep :*:*" "$manifest_"
filter_to @Extnlist "extn *" "$manifest_"
filter_to @r_repoid "repo *" "$manifest_"
r_repoid=${r_repoid#repo }
r_repoid=${r_repoid% *}
if isnull "$Repoid"
then
echo_info "Remote ID is $r_repoid"
Repoid=$r_repoid
elif isnoteq "$r_repoid" "$Repoid"
then
echo_info "WARNING:"
echo_info "WARNING: Remote ID has changed!"
echo_info "WARNING: from $Repoid"
echo_info "WARNING: to $r_repoid"
echo_info "WARNING:"
Repoid=$r_repoid
else
return 0
fi
isnull "$r_name" || git config "remote.$r_name.gcrypt-id" "$r_repoid"
}
# $1 is the hash type (SHA256 etc)
# $2 the pack id
# $3 the key
get_verify_decrypt_pack()
{
local rcv_id= tmp_encrypted=
tmp_encrypted="$Tempdir/packF"
GET "$URL" "$2" "$tmp_encrypted" &&
rcv_id=$(gpg_hash "$1" < "$tmp_encrypted") &&
iseq "$rcv_id" "$2" || echo_die "Packfile $2 does not match digest!"
DECRYPT "$3" < "$tmp_encrypted"
rm -f "$tmp_encrypted"
}
# download all packlines (pack :SHA256:a32abc1231) from stdin (or die)
# $1 destdir (when repack, else "")
get_pack_files()
{
local pack_id= r_pack_key_line= htype_= pack_= key_=
while IFS=': ' read -r _ htype_ pack_ # </dev/null
xecho "pack $pack_id" >> "$Localdir/have_packs$GITCEPTION"
else
git index-pack -v --stdin "$1/${pack_}.pack" >/dev/null
fi
done
}
# Download and unpack remote packfiles
# $1 return var for list of packfiles to delete
repack_if_needed()
{
local n_= m_= kline_= r_line= r_keep_packlist= r_del_list=
isnonnull "$Packlist" || return 0
if isnonnull "${GCRYPT_FULL_REPACK:-}"
then
Keeplist=
Repack_limit=0
fi
pick_fields_1_2 @r_del_list "$Packlist"
n_=$(line_count "$Packlist")
m_=$(line_count "$Keeplist")
if iseq 0 "$(( $Repack_limit < ($n_ - $m_) ))"; then
return
fi
echo_info "Repacking remote $NAME, ..."
mkdir "$Tempdir/pack"
# Split packages to keep and to repack
if isnonnull "$Keeplist"; then
while read -r _ kline_ _ # < (if sha-1 exists locally)
r_revlist=$(xfeed "$Refslist" cut -f 1 -d ' ' |
safe_git_rev_parse | sed -e 's/^\(.\)/^&/')
fi
while IFS=: read -r src_ dst_ # << +src:dst
do
src_=${src_#+}
filter_to ! @Refslist "$Hex40 $dst_" "$Refslist"
if isnonnull "$src_"
then
append_to @r_revlist "$src_"
obj_=$(xfeed "$src_" safe_git_rev_parse)
append_to @Refslist "$obj_ $dst_"
fi
done < "$tmp_objlist"
# Only send pack if we have any objects to send
if [ -s "$tmp_objlist" ]
then
key_=$(genkey "$Packkey_bytes")
pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES=$Tempdir;
pipefail git pack-objects --stdout < "$tmp_objlist" |
pipefail ENCRYPT "$key_" |
tee "$tmp_encrypted" | gpg_hash "$Hashtype")
append_to @Packlist "pack :${Hashtype}:$pack_id $key_"
if isnonnull "$r_pack_delete"
then
append_to @Keeplist "keep :${Hashtype}:$pack_id 1"
fi
fi
# Generate manifest
echo_info "Encrypting to: $Recipients"
echo_info "Requesting manifest signature"
tmp_manifest="$Tempdir/maniP"
PRIVENCRYPT "$Recipients" > "$tmp_manifest" <&2
}
setup()
{
mkdir -p "$Localdir"
# Set up a subdirectory in /tmp
temp_key=$(genkey 9 | tr '/' _)
Tempdir="${TMPDIR:-/tmp}/git-remote-gcrypt-${temp_key}.$$"
mkdir -m 700 "${Tempdir}"
trap cleanup_tmpfiles EXIT
trap 'exit 1' 1 2 3 15
echo_info "Development version -- Repository format MAY CHANGE"
}
# handle git-remote-helpers protocol
gcrypt_main_loop()
{
local input_= input_inner= r_args= temp_key=
NAME=$1 # Remote name
URL=$2 # Remote URL
setup
while read input_
do
case "$input_" in
capabilities)
do_capabilities
;;
list|list\ for-push)
do_list
;;
fetch\ *)
r_args=${input_##fetch }
while read input_inner
do
case "$input_inner" in
fetch*)
r_args= #ignored
;;
*)
break
;;
esac
done
do_fetch "$r_args"
;;
push\ *)
r_args=${input_##push }
while read input_inner
do
case "$input_inner" in
push\ *)
append_to @r_args "${input_inner#push }"
;;
*)
break
;;
esac
done
do_push "$r_args"
;;
?*)
echo_die "Unknown input!"
;;
*)
CLEAN_FINAL "$URL"
exit 0
;;
esac
done
}
if [ "x$1" = x--check ]
then
NAME=dummy-gcrypt-check
URL=$2
setup
ensure_connected
if iseq "$Did_find_repo" "no"
then
exit 100
fi
else
gcrypt_main_loop "$@"
fi