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