1#!/bin/sh 2 3# git-remote-gcrypt 4# 5# Copyright (c) 2013 engla 6# Copyright (c) 2013, 2014 Joey Hess <id@joeyh.name> 7# Copyright (c) 2016, 2018 Sean Whitton <spwhitton@spwhitton.name> and contributors 8# 9# This program is free software: you can redistribute it and/or modify 10# it under the terms of the GNU General Public License as published by 11# the Free Software Foundation, either version 3 of the License, or 12# (at your option) version 2 or any later version. 13# 14# This program is distributed in the hope that it will be useful, 15# but WITHOUT ANY WARRANTY; without even the implied warranty of 16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17# GNU General Public License for more details. 18# 19# You should have received a copy of the GNU General Public License 20# along with this program. If not, see <http://www.gnu.org/licenses/>. 21# 22# See README.rst for usage instructions 23 24set -e # errexit 25set -f # noglob 26set -C # noclobber 27 28export GITCEPTION="${GITCEPTION:-}+" # Reuse $Gref except when stacked 29Gref="refs/gcrypt/gitception$GITCEPTION" 30Gref_rbranch="refs/heads/master" 31Packkey_bytes=63 # nbr random bytes for packfile keys, any >= 256 bit is ok 32Hashtype=SHA256 # SHA512 SHA384 SHA256 SHA224 supported. 33Manifestfile=91bd0c092128cf2e60e1a608c31e92caf1f9c1595f83f2890ef17c0e4881aa0a 34Hex40="[a-f0-9]" 35Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40$Hex40 36Hex40=$Hex40$Hex40$Hex40$Hex40$Hex40 # Match SHA-1 hexdigest 37GPG="$(git config --get "gpg.program" '.+' || echo gpg)" 38 39Did_find_repo= # yes for connected, no for no repo 40Localdir="${GIT_DIR:=.git}/remote-gcrypt" 41Tempdir= 42 43Repoid= 44Refslist= 45Packlist= 46Keeplist= 47Extnlist= 48Repack_limit=25 49 50Recipients= 51 52# compat/utility functions 53# xfeed: The most basic output function puts $1 into the stdin of $2..$# 54xfeed() 55{ 56 local input_= 57 input_=$1; shift 58 "$@" <<EOF 59$input_ 60EOF 61} 62xecho() { xfeed "$*" cat; } 63xecho_n() { xecho "$@" | tr -d \\n ; } # kill newlines 64echo_git() { xecho "$@" ; } # Code clarity 65echo_info() { xecho "gcrypt:" "$@" >&2; } 66echo_die() { echo_info "$@" ; exit 1; } 67 68isnull() { case "$1" in "") return 0;; *) return 1;; esac; } 69isnonnull() { ! isnull "$1"; } 70iseq() { case "$1" in "$2") return 0;; *) return 1;; esac; } 71isnoteq() { ! iseq "$1" "$2"; } 72negate() { ! "$@"; } 73 74# Execute $@ or die 75pipefail() 76{ 77 "$@" || { echo_info "'$1' failed!"; kill $$; exit 1; } 78} 79 80isurl() { isnull "${2%%$1://*}"; } 81islocalrepo() { isnull "${1##/*}" && [ ! -e "$1/HEAD" ]; } 82 83xgrep() { command grep "$@" || : ; } 84 85# setvar is used for named return variables 86# $1 *must* be a valid variable name, $2 is any value 87# 88# Conventions 89# return variable names are passed with a @ prefix 90# return variable functions use f_ prefix local vars 91# return var consumers use r_ prefix vars (or Titlecase globals) 92setvar() 93{ 94 isnull "${1##@*}" || echo_die "Missing @ for return variable: $1" 95 eval ${1#@}=\$2 96} 97 98Newline=" 99" 100 101# $1 is return var, $2 is value appended with newline separator 102append_to() 103{ 104 local f_append_tmp_= 105 eval f_append_tmp_=\$${1#@} 106 isnull "$f_append_tmp_" || f_append_tmp_=$f_append_tmp_$Newline 107 setvar "$1" "$f_append_tmp_$2" 108} 109 110# Pick words from each line 111# $1 return variable name 112# $2 input value 113pick_fields_1_2() 114{ 115 local f_ret= f_one= f_two= 116 while read f_one f_two _ # from << here-document 117 do 118 f_ret="$f_ret$f_one $f_two$Newline" 119 done <<EOF 120$2 121EOF 122 setvar "$1" "${f_ret#$Newline}" 123} 124 125# Take all lines matching $2 (full line) 126# $1 return variable name 127# $2 filter word 128# $3 input value 129# if $1 is a literal `!', the match is reversed (and arguments shift) 130# we instead remove all lines matching 131filter_to() 132{ 133 local f_neg= f_line= f_ret= IFS= 134 isnoteq "$1" "!" || { f_neg=negate; shift; } 135 IFS=$Newline 136 for f_line in $3 137 do 138 $f_neg isnonnull "${f_line##$2}" || f_ret=$f_ret$f_line$Newline 139 done 140 setvar "$1" "${f_ret%$Newline}" 141} 142 143# Output the number of lines in $1 144line_count() 145{ 146 local IFS= 147 IFS=$Newline 148 set -- $1 149 xecho "$#" 150} 151 152# Convert URI in standard or nonstandard form to rsync's user@host:path 153rsynclocation () 154{ 155 echo "${1#rsync://}" | sed 's/\(^[^:/]*\)\//\1:\//' 156} 157 158 159## gitception part 160# Fetch giturl $1, file $2 161gitception_get() 162{ 163 # Take care to preserve FETCH_HEAD 164 local ret_=: obj_id= fet_head="$GIT_DIR/FETCH_HEAD" 165 [ -e "$fet_head" ] && command mv -f "$fet_head" "$fet_head.$$~" || : 166 git fetch -q -f "$1" "$Gref_rbranch:$Gref" >/dev/null && 167 obj_id="$(git ls-tree "$Gref" | xgrep -E '\b'"$2"'$' | awk '{print $3}')" && 168 isnonnull "$obj_id" && git cat-file blob "$obj_id" && ret_=: || 169 { ret_=false && : ; } 170 [ -e "$fet_head.$$~" ] && command mv -f "$fet_head.$$~" "$fet_head" || : 171 $ret_ 172} 173 174anon_commit() 175{ 176 GIT_AUTHOR_NAME="root" GIT_AUTHOR_EMAIL="root@localhost" \ 177 GIT_AUTHOR_DATE="1356994801 -0400" GIT_COMMITTER_NAME="root" \ 178 GIT_COMMITTER_EMAIL="root@localhost" \ 179 GIT_COMMITTER_DATE="1356994801 -0400" \ 180 git commit-tree "$@" <<EOF 181Initial commit 182EOF 183} 184 185# Get 'tree' from $1, change file $2 to obj id $3 186update_tree() 187{ 188 local tab_=" " 189 # $2 is a filename from the repo format 190 (set +e; 191 git ls-tree "$1" | xgrep -v -E '\b'"$2"'$'; 192 xecho "100644 blob $3$tab_$2" 193 ) | git mktree 194} 195 196# Put giturl $1, file $2 197# depends on previous GET to set $Gref and depends on PUT_FINAL later 198gitception_put() 199{ 200 local obj_id= tree_id= commit_id= 201 obj_id=$(git hash-object -w --stdin) && 202 tree_id=$(update_tree "$Gref" "$2" "$obj_id") && 203 commit_id=$(anon_commit "$tree_id") && 204 git update-ref "$Gref" "$commit_id" 205} 206 207# Remove giturl $1, file $2 208# depends on previous GET like put 209gitception_remove() 210{ 211 local tree_id= commit_id= tab_=" " 212 # $2 is a filename from the repo format 213 tree_id=$(git ls-tree "$Gref" | xgrep -v -E '\b'"$2"'$' | git mktree) && 214 commit_id=$(anon_commit "$tree_id") && 215 git update-ref "$Gref" "$commit_id" 216} 217 218gitception_new_repo() 219{ 220 local commit_id= empty_tree=4b825dc642cb6eb9a060e54bf8d69288fbee4904 221 # get any file to update Gref, and if it's not updated we create empty 222 git update-ref -d "$Gref" || : 223 gitception_get "$1" "x" 2>/dev/null >&2 || : 224 git rev-parse -q --verify "$Gref" >/dev/null && return 0 || 225 commit_id=$(anon_commit "$empty_tree") && 226 git update-ref "$Gref" "$commit_id" 227} 228## end gitception 229 230# Fetch repo $1, file $2, tmpfile in $3 231GET() 232{ 233 if isurl sftp "$1" 234 then 235 (exec 0>&-; curl -s -S -k "$1/$2") > "$3" 236 elif isurl rsync "$1" 237 then 238 (exec 0>&-; rsync -I -W "$(rsynclocation "$1")"/"$2" "$3" >&2) 239 elif isurl rclone "$1" 240 then 241 (exec 0>&-; rclone copyto "${1#rclone://}"/"$2" "$3" >&2) 242 elif islocalrepo "$1" 243 then 244 cat "$1/$2" > "$3" 245 else 246 gitception_get "${1#gitception://}" "$2" > "$3" 247 fi 248} 249 250# Put repo $1, file $2 or fail, tmpfile in $3 251PUT() 252{ 253 if isurl sftp "$1" 254 then 255 curl -s -S -k --ftp-create-dirs -T "$3" "$1/$2" 256 elif isurl rsync "$1" 257 then 258 rsync $Conf_rsync_put_flags -I -W "$3" "$(rsynclocation "$1")"/"$2" >&2 259 elif isurl rclone "$1" 260 then 261 rclone copyto "$3" "${1#rclone://}"/"$2" >&2 262 elif islocalrepo "$1" 263 then 264 cat >| "$1/$2" < "$3" 265 else 266 gitception_put "${1#gitception://}" "$2" < "$3" 267 fi 268} 269 270# Put all PUT changes for repo $1 at once 271PUT_FINAL() 272{ 273 if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" || isurl rclone "$1" 274 then 275 : 276 else 277 git push --quiet -f "${1#gitception://}" "$Gref:$Gref_rbranch" 278 fi 279} 280 281# Put directory for repo $1 282PUTREPO() 283{ 284 if isurl sftp "$1" 285 then 286 : 287 elif isurl rsync "$1" 288 then 289 rsync $Conf_rsync_put_flags -q -r --exclude='*' \ 290 "$Localdir/" "$(rsynclocation "$1")" >&2 291 elif isurl rclone "$1" 292 then 293 rclone mkdir "${1#rclone://}" >&2 294 elif islocalrepo "$1" 295 then 296 mkdir -p "$1" 297 else 298 gitception_new_repo "${1#gitception://}" 299 fi 300} 301 302# For repo $1, delete all newline-separated files in $2 303REMOVE() 304{ 305 local fn_= 306 if isurl sftp "$1" 307 then 308 # FIXME 309 echo_info "sftp: Ignore remove request $1/$2" 310 elif isurl rsync "$1" 311 then 312 xfeed "$2" rsync -I -W -v -r --delete --include-from=- \ 313 --exclude='*' "$Localdir"/ "$(rsynclocation "$1")/" >&2 314 elif isurl rclone "$1" 315 then 316 xfeed "$2" rclone delete -v --include-from=/dev/stdin "${1#rclone://}/" >&2 317 elif islocalrepo "$1" 318 then 319 for fn_ in $2; do 320 rm -f "$1"/"$fn_" 321 done 322 else 323 for fn_ in $2; do 324 gitception_remove "${1#gitception://}" "$fn_" 325 done 326 fi 327} 328 329CLEAN_FINAL() 330{ 331 if isurl sftp "$1" || islocalrepo "$1" || isurl rsync "$1" || isurl rclone "$1" 332 then 333 : 334 else 335 git update-ref -d "$Gref" || : 336 fi 337} 338 339ENCRYPT() 340{ 341 rungpg --batch --force-mdc --compress-algo none --trust-model=always --passphrase-fd 3 -c 3<<EOF 342$1 343EOF 344} 345 346DECRYPT() 347{ 348 rungpg -q --batch --no-default-keyring --secret-keyring /dev/null \ 349 --keyring /dev/null --passphrase-fd 3 -d 3<<EOF 350$1 351EOF 352} 353 354# Encrypt to recipients $1 355PRIVENCRYPT() 356{ 357 set -- $1 358 if isnonnull "$Conf_signkey"; then 359 set -- "$@" -u "$Conf_signkey" 360 fi 361 rungpg --compress-algo none --trust-model=always -se "$@" 362} 363 364# $1 is the match for good signature, $2 is the textual signers list 365PRIVDECRYPT() 366{ 367 local status_= 368 exec 4>&1 && 369 status_=$(rungpg --status-fd 3 -q -d 3>&1 1>&4) && 370 xfeed "$status_" grep "^\[GNUPG:\] ENC_TO " >/dev/null && 371 (xfeed "$status_" grep -e "$1" >/dev/null || { 372 echo_info "Failed to verify manifest signature!" && 373 echo_info "Only accepting signatories: ${2:-(none)}" && 374 return 1 375 }) 376} 377 378# Generate $1 random bytes 379genkey() 380{ 381 rungpg --armor --gen-rand 1 "$1" 382} 383 384gpg_hash() 385{ 386 local hash_= 387 hash_=$(rungpg --with-colons --print-md "$1" | tr A-F a-f) 388 hash_=${hash_#:*:} 389 xecho "${hash_%:}" 390} 391 392rungpg() 393{ 394 if isnonnull "$Conf_gpg_args"; then 395 set -- "$Conf_gpg_args" "$@" 396 fi 397 # gpg will fail to run when there is no controlling tty, 398 # due to trying to print messages to it, even if a gpg agent is set 399 # up. --no-tty fixes this. 400 if [ "x$GPG_AGENT_INFO" != "x" ]; then 401 ${GPG} --no-tty $@ 402 else 403 ${GPG} $@ 404 fi 405} 406 407# Pass the branch/ref by pipe to git 408safe_git_rev_parse() 409{ 410 git cat-file --batch-check 2>/dev/null | 411 xgrep -v "missing" | cut -f 1 -d ' ' 412} 413 414make_new_repo() 415{ 416 echo_info "Setting up new repository" 417 PUTREPO "$URL" 418 419 # Needed assumption: the same user should have no duplicate Repoid 420 Repoid=":id:$(genkey 15)" 421 iseq "${NAME#gcrypt::}" "$URL" || 422 git config "remote.$NAME.gcrypt-id" "$Repoid" 423 echo_info "Remote ID is $Repoid" 424 Extnlist="extn comment" 425} 426 427 428# $1 return var for goodsig match, $2 return var for signers text 429read_config() 430{ 431 local recp_= r_tail= r_keyinfo= r_keyfpr= gpg_list= cap_= conf_part= good_sig= signers_= 432 Conf_signkey=$(git config --get "remote.$NAME.gcrypt-signingkey" '.+' || 433 git config --path user.signingkey || :) 434 conf_part=$(git config --get "remote.$NAME.gcrypt-participants" '.+' || 435 git config --get gcrypt.participants '.+' || :) 436 Conf_pubish_participants=$(git config --get --bool "remote.$NAME.gcrypt-publish-participants" '.+' || 437 git config --get --bool gcrypt.publish-participants || :) 438 Conf_gpg_args=$(git config --get gcrypt.gpg-args '.+' || :) 439 Conf_rsync_put_flags=$(git config --get "remote.$NAME.gcrypt-rsync-put-flags" '.+' || 440 git config --get "gcrypt.rsync-put-flags" '.+' || :) 441 Conf_force_required=$(git config --get --bool "remote.$NAME.gcrypt-require-explicit-force-push" '.+' || 442 git config --get --bool gcrypt.require-explicit-force-push '.+' || :) 443 444 # Figure out which keys we should encrypt to or accept signatures from 445 if isnull "$conf_part" || iseq "$conf_part" simple 446 then 447 signers_="(default keyring)" 448 Recipients="--throw-keyids --default-recipient-self" 449 good_sig="^\[GNUPG:\] GOODSIG " 450 setvar "$1" "$good_sig" 451 setvar "$2" "$signers_" 452 return 0 453 fi 454 455 for recp_ in $conf_part 456 do 457 gpg_list=$(rungpg --with-colons --fingerprint -k "$recp_") 458 r_tail_=$(echo "$recp_" | sed -e 's/^0x//') 459 filter_to @r_keyinfo "pub*" "$gpg_list" 460 if echo "$recp_" | grep -E -q '^[xA-F0-9]+$'; then # is $recp_ a keyid? 461 filter_to @r_keyfpr "fpr*$r_tail_*" "$gpg_list" 462 else 463 filter_to @r_keyfpr "fpr*" "$gpg_list" 464 fi 465 isnull "$r_keyinfo" || isnonnull "${r_keyinfo##*"$Newline"*}" || 466 echo_info "WARNING: '$recp_' matches multiple keys, using one" 467 isnull "$r_keyfpr" || isnonnull "${r_keyfpr##*"$Newline"*}" || 468 echo_info "WARNING: '$recp_' matches multiple fingerprints, using one" 469 r_keyinfo=${r_keyinfo%%"$Newline"*} 470 r_keyfpr=${r_keyfpr%%"$Newline"*} 471 keyid_=$(xfeed "$r_keyinfo" cut -f 5 -d :) 472 fprid_=$(xfeed "$r_keyfpr" cut -f 10 -d :) 473 474 isnonnull "$fprid_" && 475 signers_="$signers_ $keyid_" && 476 append_to @good_sig "^\[GNUPG:\] VALIDSIG .*$fprid_$" || { 477 echo_info "WARNING: Skipping missing key $recp_" 478 continue 479 } 480 # Check 'E'ncrypt capability 481 cap_=$(xfeed "$r_keyinfo" cut -f 12 -d :) 482 if ! iseq "${cap_#*E}" "$cap_"; then 483 if [ "$Conf_pubish_participants" = true ]; then 484 Recipients="$Recipients -r $keyid_" 485 else 486 Recipients="$Recipients -R $keyid_" 487 fi 488 fi 489 done 490 491 if isnull "$Recipients" 492 then 493 echo_info "You have not configured any keys you can encrypt to" \ 494 "for this repository" 495 echo_info "Use ::" 496 echo_info " git config gcrypt.participants YOURKEYID" 497 exit 1 498 fi 499 setvar "$1" "$good_sig" 500 setvar "$2" "$signers_" 501} 502 503ensure_connected() 504{ 505 local manifest_= r_repoid= r_name= url_frag= r_sigmatch= r_signers= \ 506 tmp_manifest= tmp_stderr= 507 508 if isnonnull "$Did_find_repo" 509 then 510 return 511 fi 512 Did_find_repo=no 513 read_config @r_sigmatch @r_signers 514 515 iseq "${NAME#gcrypt::}" "$URL" || r_name=$NAME 516 517 if isurl gitception "$URL" && isnonnull "$r_name"; then 518 git config "remote.$r_name.url" "gcrypt::${URL#gitception://}" 519 echo_info "Updated URL for $r_name, gitception:// -> ()" 520 fi 521 522 # Find the URL fragment 523 url_frag=${URL##*"#"} 524 isnoteq "$url_frag" "$URL" || url_frag= 525 URL=${URL%"#$url_frag"} 526 527 # manifestfile -- sha224 hash if we can, else the default location 528 if isurl sftp "$URL" || islocalrepo "$URL" || isurl rsync "$URL" || isurl rclone "$URL" 529 then 530 # not for gitception 531 isnull "$url_frag" || 532 Manifestfile=$(xecho_n "$url_frag" | gpg_hash SHA224) 533 else 534 isnull "$url_frag" || Gref_rbranch="refs/heads/$url_frag" 535 fi 536 537 Repoid= 538 isnull "$r_name" || 539 Repoid=$(git config "remote.$r_name.gcrypt-id" || :) 540 541 542 tmp_manifest="$Tempdir/maniF" 543 tmp_stderr="$Tempdir/stderr" 544 GET "$URL" "$Manifestfile" "$tmp_manifest" 2>| "$tmp_stderr" || { 545 if ! isnull "$Repoid"; then 546 cat >&2 "$tmp_stderr" 547 echo_info "Repository not found: $URL" 548 echo_info "..but repository ID is set. Aborting." 549 return 1 550 else 551 echo_info "Repository not found: $URL" 552 return 0 553 fi 554 } 555 556 Did_find_repo=yes 557 echo_info "Decrypting manifest" 558 manifest_=$(PRIVDECRYPT "$r_sigmatch" "$r_signers" < "$tmp_manifest") && 559 isnonnull "$manifest_" || 560 echo_die "Failed to decrypt manifest!" 561 rm -f "$tmp_manifest" 562 563 filter_to @Refslist "$Hex40 *" "$manifest_" 564 filter_to @Packlist "pack :*:* *" "$manifest_" 565 filter_to @Keeplist "keep :*:*" "$manifest_" 566 filter_to @Extnlist "extn *" "$manifest_" 567 filter_to @r_repoid "repo *" "$manifest_" 568 569 r_repoid=${r_repoid#repo } 570 r_repoid=${r_repoid% *} 571 if isnull "$Repoid" 572 then 573 echo_info "Remote ID is $r_repoid" 574 Repoid=$r_repoid 575 elif isnoteq "$r_repoid" "$Repoid" 576 then 577 echo_info "WARNING:" 578 echo_info "WARNING: Remote ID has changed!" 579 echo_info "WARNING: from $Repoid" 580 echo_info "WARNING: to $r_repoid" 581 echo_info "WARNING:" 582 Repoid=$r_repoid 583 else 584 return 0 585 fi 586 587 isnull "$r_name" || git config "remote.$r_name.gcrypt-id" "$r_repoid" 588} 589 590# $1 is the hash type (SHA256 etc) 591# $2 the pack id 592# $3 the key 593get_verify_decrypt_pack() 594{ 595 local rcv_id= tmp_encrypted= 596 tmp_encrypted="$Tempdir/packF" 597 GET "$URL" "$2" "$tmp_encrypted" && 598 rcv_id=$(gpg_hash "$1" < "$tmp_encrypted") && 599 iseq "$rcv_id" "$2" || echo_die "Packfile $2 does not match digest!" 600 DECRYPT "$3" < "$tmp_encrypted" 601 rm -f "$tmp_encrypted" 602} 603 604# download all packlines (pack :SHA256:a32abc1231) from stdin (or die) 605# $1 destdir (when repack, else "") 606get_pack_files() 607{ 608 local pack_id= r_pack_key_line= htype_= pack_= key_= 609 while IFS=': ' read -r _ htype_ pack_ # <<here-document 610 do 611 isnonnull "$pack_" || continue 612 613 # Get the Packlist line with the key 614 pack_id=":${htype_}:$pack_" 615 filter_to @r_pack_key_line "pack $pack_id *" "$Packlist" 616 key_=${r_pack_key_line#pack $pack_id } 617 618 if isnonnull "${pack_##$Hex40*}" || 619 isnoteq "$htype_" SHA256 && isnoteq "$htype_" SHA224 && 620 isnoteq "$htype_" SHA384 && isnoteq "$htype_" SHA512 621 then 622 echo_die "Packline malformed: $pack_id" 623 fi 624 625 get_verify_decrypt_pack "$htype_" "$pack_" "$key_" | \ 626 if isnull "${1:-}" 627 then 628 # add to local pack list 629 git index-pack -v --stdin >/dev/null 630 xecho "pack $pack_id" >> "$Localdir/have_packs$GITCEPTION" 631 else 632 git index-pack -v --stdin "$1/${pack_}.pack" >/dev/null 633 fi 634 done 635} 636 637# Download and unpack remote packfiles 638# $1 return var for list of packfiles to delete 639repack_if_needed() 640{ 641 local n_= m_= kline_= r_line= r_keep_packlist= r_del_list= 642 643 isnonnull "$Packlist" || return 0 644 645 if isnonnull "${GCRYPT_FULL_REPACK:-}" 646 then 647 Keeplist= 648 Repack_limit=0 649 fi 650 651 pick_fields_1_2 @r_del_list "$Packlist" 652 653 n_=$(line_count "$Packlist") 654 m_=$(line_count "$Keeplist") 655 if iseq 0 "$(( $Repack_limit < ($n_ - $m_) ))"; then 656 return 657 fi 658 echo_info "Repacking remote $NAME, ..." 659 660 mkdir "$Tempdir/pack" 661 662 # Split packages to keep and to repack 663 if isnonnull "$Keeplist"; then 664 while read -r _ kline_ _ # <<here-document 665 do 666 isnonnull "$kline_" || continue 667 filter_to @r_line "pack $kline_ *" "$Packlist" 668 append_to @r_keep_packlist "$r_line" 669 filter_to ! @r_del_list "pack $kline_" "$r_del_list" 670 done <<EOF 671$Keeplist 672EOF 673 fi 674 675 xfeed "$r_del_list" get_pack_files "$Tempdir/pack/" 676 677 (set +f; pipefail git verify-pack -v "$Tempdir"/pack/*.idx) | 678 grep -E '^[0-9a-f]{40}' | cut -f 1 -d ' ' 679 680 Packlist=$r_keep_packlist 681 setvar "$1" "$r_del_list" 682} 683 684do_capabilities() 685{ 686 echo_git fetch 687 echo_git push 688 echo_git 689} 690 691do_list() 692{ 693 local obj_id= ref_name= line_= 694 ensure_connected 695 696 xecho "$Refslist" | while read line_ 697 do 698 isnonnull "$line_" || break 699 obj_id=${line_%% *} 700 ref_name=${line_##* } 701 echo_git "$obj_id" "$ref_name" 702 if iseq "$ref_name" "refs/heads/master" 703 then 704 echo_git "@refs/heads/master HEAD" 705 fi 706 done 707 708 # end with blank line 709 echo_git 710} 711 712do_fetch() 713{ 714 # Download packs in the manifest that don't appear in have_packs 715 local pneed_= premote_= 716 717 ensure_connected 718 719 # The `+` for $GITCEPTION is pointless but we will be safe for stacking 720 pick_fields_1_2 @premote_ "$Packlist" 721 if [ -s "$Localdir/have_packs+" ] 722 then 723 pneed_=$(xfeed "$premote_" xgrep -v -x -f "$Localdir/have_packs+") 724 else 725 pneed_=$premote_ 726 fi 727 728 xfeed "$pneed_" get_pack_files 729 730 echo_git # end with blank line 731} 732 733# do_push PUSHARGS (multiple lines like +src:dst, with both + and src opt.) 734do_push() 735{ 736 # Security protocol: 737 # Each git packfile is encrypted and then named for the encrypted 738 # file's hash. The manifest is updated with the pack id. 739 # The manifest is encrypted. 740 local r_revlist= pack_id= key_= obj_= src_= dst_= \ 741 r_pack_delete= tmp_encrypted= tmp_objlist= tmp_manifest= \ 742 force_passed= 743 744 ensure_connected 745 746 if iseq "$Did_find_repo" "no" 747 then 748 make_new_repo 749 fi 750 751 if isnonnull "$Refslist" 752 then 753 # mark all remote refs with ^<sha-1> (if sha-1 exists locally) 754 r_revlist=$(xfeed "$Refslist" cut -f 1 -d ' ' | 755 safe_git_rev_parse | sed -e 's/^\(.\)/^&/') 756 fi 757 758 while IFS=: read -r src_ dst_ # << +src:dst 759 do 760 if [ $(echo "$src_" | cut -c1) != + ] 761 then 762 force_passed=false 763 fi 764 765 src_=${src_#+} 766 filter_to ! @Refslist "$Hex40 $dst_" "$Refslist" 767 768 if isnonnull "$src_" 769 then 770 append_to @r_revlist "$src_" 771 obj_=$(xfeed "$src_" safe_git_rev_parse) 772 append_to @Refslist "$obj_ $dst_" 773 fi 774 done <<EOF 775$1 776EOF 777 778 if [ "$force_passed" = false ] 779 then 780 if [ "$Conf_force_required" = true ] 781 then 782 echo_die "Implicit force push disallowed by gcrypt configuration." 783 else 784 echo_info "Due to a longstanding bug, this push implicitly has --force." 785 echo_info "Consider explicitly passing --force, and setting" 786 echo_info "gcrypt's require-explicit-force-push git config key." 787 fi 788 fi 789 790 tmp_encrypted="$Tempdir/packP" 791 tmp_objlist="$Tempdir/objlP" 792 793 { 794 xfeed "$r_revlist" git rev-list --objects --stdin -- 795 repack_if_needed @r_pack_delete 796 } > "$tmp_objlist" 797 798 # Only send pack if we have any objects to send 799 if [ -s "$tmp_objlist" ] 800 then 801 key_=$(genkey "$Packkey_bytes") 802 pack_id=$(export GIT_ALTERNATE_OBJECT_DIRECTORIES=$Tempdir; 803 pipefail git pack-objects --stdout < "$tmp_objlist" | 804 pipefail ENCRYPT "$key_" | 805 tee "$tmp_encrypted" | gpg_hash "$Hashtype") 806 807 append_to @Packlist "pack :${Hashtype}:$pack_id $key_" 808 if isnonnull "$r_pack_delete" 809 then 810 append_to @Keeplist "keep :${Hashtype}:$pack_id 1" 811 fi 812 fi 813 814 # Generate manifest 815 echo_info "Encrypting to: $Recipients" 816 echo_info "Requesting manifest signature" 817 818 tmp_manifest="$Tempdir/maniP" 819 PRIVENCRYPT "$Recipients" > "$tmp_manifest" <<EOF 820$Refslist 821$Packlist 822$Keeplist 823repo $Repoid 824$Extnlist 825EOF 826 827 # Upload pack 828 if [ -s "$tmp_objlist" ] 829 then 830 PUT "$URL" "$pack_id" "$tmp_encrypted" 831 fi 832 833 # Upload manifest 834 PUT "$URL" "$Manifestfile" "$tmp_manifest" 835 836 rm -f "$tmp_encrypted" 837 rm -f "$tmp_objlist" 838 rm -f "$tmp_manifest" 839 840 # Delete packs 841 if isnonnull "$r_pack_delete"; then 842 REMOVE "$URL" "$(xecho "$r_pack_delete" | \ 843 while IFS=': ' read -r _ _ pack_ 844 do 845 isnonnull "$pack_" || continue 846 xecho "$pack_" 847 done)" 848 fi 849 850 PUT_FINAL "$URL" 851 852 # ok all updates 853 while IFS=: read -r src_ dst_ # << +src:dst 854 do 855 echo_git "ok $dst_" 856 done <<EOF 857$1 858EOF 859 860 echo_git 861} 862 863cleanup_tmpfiles() 864{ 865 if isnonnull "${Tempdir%%*."$$"}"; then 866 echo_die "Unexpected Tempdir value: $Tempdir" 867 fi 868 rm -r -f -- "${Tempdir}" >&2 869} 870 871setup() 872{ 873 mkdir -p "$Localdir" 874 875 # Set up a subdirectory in /tmp 876 temp_key=$(genkey 9 | tr '/' _) 877 Tempdir="${TMPDIR:-/tmp}/git-remote-gcrypt-${temp_key}.$$" 878 case "${MSYSTEM:-unknown}" in 879 MSYS*|MINGW*) 880 mkdir "${Tempdir}" 881 echo_info "Warning: Not securing tempdir ${Tempdir} because we are on mingw/msys" 882 ;; 883 unknown|*) 884 mkdir -m 700 "${Tempdir}" 885 ;; 886 esac 887 888 trap cleanup_tmpfiles EXIT 889 trap 'exit 1' 1 2 3 15 890 891 if isurl rclone "$URL"; then 892 echo_info "WARNING: rclone support is experimental." 893 echo_info "WARNING: Early adoptors only. Keep backups." 894 fi 895} 896 897# handle git-remote-helpers protocol 898gcrypt_main_loop() 899{ 900 local input_= input_inner= r_args= temp_key= 901 902 NAME=$1 # Remote name 903 URL=$2 # Remote URL 904 905 setup 906 907 while read input_ 908 do 909 case "$input_" in 910 capabilities) 911 do_capabilities 912 ;; 913 list|list\ for-push) 914 do_list 915 ;; 916 fetch\ *) 917 r_args=${input_##fetch } 918 while read input_inner 919 do 920 case "$input_inner" in 921 fetch*) 922 r_args= #ignored 923 ;; 924 *) 925 break 926 ;; 927 esac 928 done 929 do_fetch "$r_args" 930 ;; 931 push\ *) 932 r_args=${input_##push } 933 while read input_inner 934 do 935 case "$input_inner" in 936 push\ *) 937 append_to @r_args "${input_inner#push }" 938 ;; 939 *) 940 break 941 ;; 942 esac 943 done 944 do_push "$r_args" 945 ;; 946 ?*) 947 echo_die "Unknown input!" 948 ;; 949 *) 950 CLEAN_FINAL "$URL" 951 exit 0 952 ;; 953 esac 954 done 955} 956 957if [ "x$1" = x--check ] 958then 959 NAME=dummy-gcrypt-check 960 URL=$2 961 setup 962 ensure_connected 963 git remote remove $NAME 2>/dev/null || true 964 if iseq "$Did_find_repo" "no" 965 then 966 exit 100 967 fi 968else 969 gcrypt_main_loop "$@" 970fi 971