about summary refs log tree commit diff
path: root/third_party/git/ci
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/git/ci')
-rwxr-xr-xthird_party/git/ci/install-dependencies.sh74
-rwxr-xr-xthird_party/git/ci/lib.sh193
-rwxr-xr-xthird_party/git/ci/make-test-artifacts.sh12
-rwxr-xr-xthird_party/git/ci/mount-fileshare.sh25
-rwxr-xr-xthird_party/git/ci/print-test-failures.sh91
-rwxr-xr-xthird_party/git/ci/run-build-and-tests.sh37
-rwxr-xr-xthird_party/git/ci/run-linux32-build.sh60
-rwxr-xr-xthird_party/git/ci/run-linux32-docker.sh31
-rwxr-xr-xthird_party/git/ci/run-static-analysis.sh29
-rwxr-xr-xthird_party/git/ci/run-test-slice.sh17
-rwxr-xr-xthird_party/git/ci/test-documentation.sh42
-rwxr-xr-xthird_party/git/ci/util/extract-trash-dirs.sh50
12 files changed, 661 insertions, 0 deletions
diff --git a/third_party/git/ci/install-dependencies.sh b/third_party/git/ci/install-dependencies.sh
new file mode 100755
index 000000000000..8cc72503cb76
--- /dev/null
+++ b/third_party/git/ci/install-dependencies.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+#
+# Install dependencies required to build and test Git on Linux and macOS
+#
+
+. ${0%/*}/lib.sh
+
+P4WHENCE=http://filehost.perforce.com/perforce/r$LINUX_P4_VERSION
+LFSWHENCE=https://github.com/github/git-lfs/releases/download/v$LINUX_GIT_LFS_VERSION
+
+case "$jobname" in
+linux-clang|linux-gcc)
+	sudo apt-add-repository -y "ppa:ubuntu-toolchain-r/test"
+	sudo apt-get -q update
+	sudo apt-get -q -y install language-pack-is libsvn-perl apache2
+	case "$jobname" in
+	linux-gcc)
+		sudo apt-get -q -y install gcc-8
+		;;
+	esac
+
+	mkdir --parents "$P4_PATH"
+	pushd "$P4_PATH"
+		wget --quiet "$P4WHENCE/bin.linux26x86_64/p4d"
+		wget --quiet "$P4WHENCE/bin.linux26x86_64/p4"
+		chmod u+x p4d
+		chmod u+x p4
+	popd
+	mkdir --parents "$GIT_LFS_PATH"
+	pushd "$GIT_LFS_PATH"
+		wget --quiet "$LFSWHENCE/git-lfs-linux-amd64-$LINUX_GIT_LFS_VERSION.tar.gz"
+		tar --extract --gunzip --file "git-lfs-linux-amd64-$LINUX_GIT_LFS_VERSION.tar.gz"
+		cp git-lfs-$LINUX_GIT_LFS_VERSION/git-lfs .
+	popd
+	;;
+osx-clang|osx-gcc)
+	export HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_NO_INSTALL_CLEANUP=1
+	# Uncomment this if you want to run perf tests:
+	# brew install gnu-time
+	test -z "$BREW_INSTALL_PACKAGES" ||
+	brew install $BREW_INSTALL_PACKAGES
+	brew link --force gettext
+	brew install caskroom/cask/perforce
+	case "$jobname" in
+	osx-gcc)
+		brew link gcc@8
+		;;
+	esac
+	;;
+StaticAnalysis)
+	sudo apt-get -q update
+	sudo apt-get -q -y install coccinelle
+	;;
+Documentation)
+	sudo apt-get -q update
+	sudo apt-get -q -y install asciidoc xmlto
+
+	test -n "$ALREADY_HAVE_ASCIIDOCTOR" ||
+	gem install --version 1.5.8 asciidoctor
+	;;
+esac
+
+if type p4d >/dev/null && type p4 >/dev/null
+then
+	echo "$(tput setaf 6)Perforce Server Version$(tput sgr0)"
+	p4d -V | grep Rev.
+	echo "$(tput setaf 6)Perforce Client Version$(tput sgr0)"
+	p4 -V | grep Rev.
+fi
+if type git-lfs >/dev/null
+then
+	echo "$(tput setaf 6)Git-LFS Version$(tput sgr0)"
+	git-lfs version
+fi
diff --git a/third_party/git/ci/lib.sh b/third_party/git/ci/lib.sh
new file mode 100755
index 000000000000..44db2d5cbb22
--- /dev/null
+++ b/third_party/git/ci/lib.sh
@@ -0,0 +1,193 @@
+# Library of functions shared by all CI scripts
+
+skip_branch_tip_with_tag () {
+	# Sometimes, a branch is pushed at the same time the tag that points
+	# at the same commit as the tip of the branch is pushed, and building
+	# both at the same time is a waste.
+	#
+	# When the build is triggered by a push to a tag, $CI_BRANCH will
+	# have that tagname, e.g. v2.14.0.  Let's see if $CI_BRANCH is
+	# exactly at a tag, and if so, if it is different from $CI_BRANCH.
+	# That way, we can tell if we are building the tip of a branch that
+	# is tagged and we can skip the build because we won't be skipping a
+	# build of a tag.
+
+	if TAG=$(git describe --exact-match "$CI_BRANCH" 2>/dev/null) &&
+		test "$TAG" != "$CI_BRANCH"
+	then
+		echo "$(tput setaf 2)Tip of $CI_BRANCH is exactly at $TAG$(tput sgr0)"
+		exit 0
+	fi
+}
+
+# Save some info about the current commit's tree, so we can skip the build
+# job if we encounter the same tree again and can provide a useful info
+# message.
+save_good_tree () {
+	echo "$(git rev-parse $CI_COMMIT^{tree}) $CI_COMMIT $CI_JOB_NUMBER $CI_JOB_ID" >>"$good_trees_file"
+	# limit the file size
+	tail -1000 "$good_trees_file" >"$good_trees_file".tmp
+	mv "$good_trees_file".tmp "$good_trees_file"
+}
+
+# Skip the build job if the same tree has already been built and tested
+# successfully before (e.g. because the branch got rebased, changing only
+# the commit messages).
+skip_good_tree () {
+	if ! good_tree_info="$(grep "^$(git rev-parse $CI_COMMIT^{tree}) " "$good_trees_file")"
+	then
+		# Haven't seen this tree yet, or no cached good trees file yet.
+		# Continue the build job.
+		return
+	fi
+
+	echo "$good_tree_info" | {
+		read tree prev_good_commit prev_good_job_number prev_good_job_id
+
+		if test "$CI_JOB_ID" = "$prev_good_job_id"
+		then
+			cat <<-EOF
+			$(tput setaf 2)Skipping build job for commit $CI_COMMIT.$(tput sgr0)
+			This commit has already been built and tested successfully by this build job.
+			To force a re-build delete the branch's cache and then hit 'Restart job'.
+			EOF
+		else
+			cat <<-EOF
+			$(tput setaf 2)Skipping build job for commit $CI_COMMIT.$(tput sgr0)
+			This commit's tree has already been built and tested successfully in build job $prev_good_job_number for commit $prev_good_commit.
+			The log of that build job is available at $(url_for_job_id $prev_good_job_id)
+			To force a re-build delete the branch's cache and then hit 'Restart job'.
+			EOF
+		fi
+	}
+
+	exit 0
+}
+
+check_unignored_build_artifacts ()
+{
+	! git ls-files --other --exclude-standard --error-unmatch \
+		-- ':/*' 2>/dev/null ||
+	{
+		echo "$(tput setaf 1)error: found unignored build artifacts$(tput sgr0)"
+		false
+	}
+}
+
+# Clear MAKEFLAGS that may come from the outside world.
+export MAKEFLAGS=
+
+# Set 'exit on error' for all CI scripts to let the caller know that
+# something went wrong.
+# Set tracing executed commands, primarily setting environment variables
+# and installing dependencies.
+set -ex
+
+if test true = "$TRAVIS"
+then
+	CI_TYPE=travis
+	# When building a PR, TRAVIS_BRANCH refers to the *target* branch. Not
+	# what we want here. We want the source branch instead.
+	CI_BRANCH="${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH}"
+	CI_COMMIT="$TRAVIS_COMMIT"
+	CI_JOB_ID="$TRAVIS_JOB_ID"
+	CI_JOB_NUMBER="$TRAVIS_JOB_NUMBER"
+	CI_OS_NAME="$TRAVIS_OS_NAME"
+	CI_REPO_SLUG="$TRAVIS_REPO_SLUG"
+
+	cache_dir="$HOME/travis-cache"
+
+	url_for_job_id () {
+		echo "https://travis-ci.org/$CI_REPO_SLUG/jobs/$1"
+	}
+
+	BREW_INSTALL_PACKAGES="git-lfs gettext"
+	export GIT_PROVE_OPTS="--timer --jobs 3 --state=failed,slow,save"
+	export GIT_TEST_OPTS="--verbose-log -x --immediate"
+	MAKEFLAGS="$MAKEFLAGS --jobs=2"
+elif test -n "$SYSTEM_COLLECTIONURI" || test -n "$SYSTEM_TASKDEFINITIONSURI"
+then
+	CI_TYPE=azure-pipelines
+	# We are running in Azure Pipelines
+	CI_BRANCH="$BUILD_SOURCEBRANCH"
+	CI_COMMIT="$BUILD_SOURCEVERSION"
+	CI_JOB_ID="$BUILD_BUILDID"
+	CI_JOB_NUMBER="$BUILD_BUILDNUMBER"
+	CI_OS_NAME="$(echo "$AGENT_OS" | tr A-Z a-z)"
+	test darwin != "$CI_OS_NAME" || CI_OS_NAME=osx
+	CI_REPO_SLUG="$(expr "$BUILD_REPOSITORY_URI" : '.*/\([^/]*/[^/]*\)$')"
+	CC="${CC:-gcc}"
+
+	# use a subdirectory of the cache dir (because the file share is shared
+	# among *all* phases)
+	cache_dir="$HOME/test-cache/$SYSTEM_PHASENAME"
+
+	url_for_job_id () {
+		echo "$SYSTEM_TASKDEFINITIONSURI$SYSTEM_TEAMPROJECT/_build/results?buildId=$1"
+	}
+
+	BREW_INSTALL_PACKAGES=gcc@8
+	export GIT_PROVE_OPTS="--timer --jobs 10 --state=failed,slow,save"
+	export GIT_TEST_OPTS="--verbose-log -x --write-junit-xml"
+	MAKEFLAGS="$MAKEFLAGS --jobs=10"
+	test windows_nt != "$CI_OS_NAME" ||
+	GIT_TEST_OPTS="--no-chain-lint --no-bin-wrappers $GIT_TEST_OPTS"
+else
+	echo "Could not identify CI type" >&2
+	exit 1
+fi
+
+good_trees_file="$cache_dir/good-trees"
+
+mkdir -p "$cache_dir"
+
+skip_branch_tip_with_tag
+skip_good_tree
+
+if test -z "$jobname"
+then
+	jobname="$CI_OS_NAME-$CC"
+fi
+
+export DEVELOPER=1
+export DEFAULT_TEST_TARGET=prove
+export GIT_TEST_CLONE_2GB=YesPlease
+
+case "$jobname" in
+linux-clang|linux-gcc)
+	if [ "$jobname" = linux-gcc ]
+	then
+		export CC=gcc-8
+	fi
+
+	export GIT_TEST_HTTPD=YesPlease
+
+	# The Linux build installs the defined dependency versions below.
+	# The OS X build installs much more recent versions, whichever
+	# were recorded in the Homebrew database upon creating the OS X
+	# image.
+	# Keep that in mind when you encounter a broken OS X build!
+	export LINUX_P4_VERSION="16.2"
+	export LINUX_GIT_LFS_VERSION="1.5.2"
+
+	P4_PATH="$HOME/custom/p4"
+	GIT_LFS_PATH="$HOME/custom/git-lfs"
+	export PATH="$GIT_LFS_PATH:$P4_PATH:$PATH"
+	;;
+osx-clang|osx-gcc)
+	if [ "$jobname" = osx-gcc ]
+	then
+		export CC=gcc-8
+	fi
+
+	# t9810 occasionally fails on Travis CI OS X
+	# t9816 occasionally fails with "TAP out of sequence errors" on
+	# Travis CI OS X
+	export GIT_SKIP_TESTS="t9810 t9816"
+	;;
+GIT_TEST_GETTEXT_POISON)
+	export GIT_TEST_GETTEXT_POISON=true
+	;;
+esac
+
+MAKEFLAGS="$MAKEFLAGS CC=${CC:-cc}"
diff --git a/third_party/git/ci/make-test-artifacts.sh b/third_party/git/ci/make-test-artifacts.sh
new file mode 100755
index 000000000000..646967481f6d
--- /dev/null
+++ b/third_party/git/ci/make-test-artifacts.sh
@@ -0,0 +1,12 @@
+#!/bin/sh
+#
+# Build Git and store artifacts for testing
+#
+
+mkdir -p "$1" # in case ci/lib.sh decides to quit early
+
+. ${0%/*}/lib.sh
+
+make artifacts-tar ARTIFACTS_DIRECTORY="$1"
+
+check_unignored_build_artifacts
diff --git a/third_party/git/ci/mount-fileshare.sh b/third_party/git/ci/mount-fileshare.sh
new file mode 100755
index 000000000000..26b58a80960f
--- /dev/null
+++ b/third_party/git/ci/mount-fileshare.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+
+die () {
+	echo "$*" >&2
+	exit 1
+}
+
+test $# = 4 ||
+die "Usage: $0 <share> <username> <password> <mountpoint>"
+
+mkdir -p "$4" || die "Could not create $4"
+
+case "$(uname -s)" in
+Linux)
+	sudo mount -t cifs -o vers=3.0,username="$2",password="$3",dir_mode=0777,file_mode=0777,serverino "$1" "$4"
+	;;
+Darwin)
+	pass="$(echo "$3" | sed -e 's/\//%2F/g' -e 's/+/%2B/g')" &&
+	mount -t smbfs,soft "smb://$2:$pass@${1#//}" "$4"
+	;;
+*)
+	die "No support for $(uname -s)"
+	;;
+esac ||
+die "Could not mount $4"
diff --git a/third_party/git/ci/print-test-failures.sh b/third_party/git/ci/print-test-failures.sh
new file mode 100755
index 000000000000..e688a26f0d61
--- /dev/null
+++ b/third_party/git/ci/print-test-failures.sh
@@ -0,0 +1,91 @@
+#!/bin/sh
+#
+# Print output of failing tests
+#
+
+. ${0%/*}/lib.sh
+
+# Tracing executed commands would produce too much noise in the loop below.
+set +x
+
+cd t/
+
+if ! ls test-results/*.exit >/dev/null 2>/dev/null
+then
+	echo "Build job failed before the tests could have been run"
+	exit
+fi
+
+case "$jobname" in
+osx-clang|osx-gcc)
+	# base64 in OSX doesn't wrap its output at 76 columns by
+	# default, but prints a single, very long line.
+	base64_opts="-b 76"
+	;;
+esac
+
+combined_trash_size=0
+for TEST_EXIT in test-results/*.exit
+do
+	if [ "$(cat "$TEST_EXIT")" != "0" ]
+	then
+		TEST_OUT="${TEST_EXIT%exit}out"
+		echo "------------------------------------------------------------------------"
+		echo "$(tput setaf 1)${TEST_OUT}...$(tput sgr0)"
+		echo "------------------------------------------------------------------------"
+		cat "${TEST_OUT}"
+
+		test_name="${TEST_EXIT%.exit}"
+		test_name="${test_name##*/}"
+		trash_dir="trash directory.$test_name"
+		case "$CI_TYPE" in
+		travis)
+			;;
+		azure-pipelines)
+			mkdir -p failed-test-artifacts
+			mv "$trash_dir" failed-test-artifacts
+			continue
+			;;
+		*)
+			echo "Unhandled CI type: $CI_TYPE" >&2
+			exit 1
+			;;
+		esac
+		trash_tgz_b64="trash.$test_name.base64"
+		if [ -d "$trash_dir" ]
+		then
+			tar czp "$trash_dir" |base64 $base64_opts >"$trash_tgz_b64"
+
+			trash_size=$(wc -c <"$trash_tgz_b64")
+			if [ $trash_size -gt 1048576 ]
+			then
+				# larger than 1MB
+				echo "$(tput setaf 1)Didn't include the trash directory of '$test_name' in the trace log, it's too big$(tput sgr0)"
+				continue
+			fi
+
+			new_combined_trash_size=$(($combined_trash_size + $trash_size))
+			if [ $new_combined_trash_size -gt 1048576 ]
+			then
+				echo "$(tput setaf 1)Didn't include the trash directory of '$test_name' in the trace log, there is plenty of trash in there already.$(tput sgr0)"
+				continue
+			fi
+			combined_trash_size=$new_combined_trash_size
+
+			# DO NOT modify these two 'echo'-ed strings below
+			# without updating 'ci/util/extract-trash-dirs.sh'
+			# as well.
+			echo "$(tput setaf 1)Start of trash directory of '$test_name':$(tput sgr0)"
+			cat "$trash_tgz_b64"
+			echo "$(tput setaf 1)End of trash directory of '$test_name'$(tput sgr0)"
+		fi
+	fi
+done
+
+if [ $combined_trash_size -gt 0 ]
+then
+	echo "------------------------------------------------------------------------"
+	echo "Trash directories embedded in this log can be extracted by running:"
+	echo
+	echo "  curl https://api.travis-ci.org/v3/job/$TRAVIS_JOB_ID/log.txt |./ci/util/extract-trash-dirs.sh"
+fi
diff --git a/third_party/git/ci/run-build-and-tests.sh b/third_party/git/ci/run-build-and-tests.sh
new file mode 100755
index 000000000000..ff0ef7f08e75
--- /dev/null
+++ b/third_party/git/ci/run-build-and-tests.sh
@@ -0,0 +1,37 @@
+#!/bin/sh
+#
+# Build and test Git
+#
+
+. ${0%/*}/lib.sh
+
+case "$CI_OS_NAME" in
+windows*) cmd //c mklink //j t\\.prove "$(cygpath -aw "$cache_dir/.prove")";;
+*) ln -s "$cache_dir/.prove" t/.prove;;
+esac
+
+make
+case "$jobname" in
+linux-gcc)
+	make test
+	export GIT_TEST_SPLIT_INDEX=yes
+	export GIT_TEST_FULL_IN_PACK_ARRAY=true
+	export GIT_TEST_OE_SIZE=10
+	export GIT_TEST_OE_DELTA_SIZE=5
+	export GIT_TEST_COMMIT_GRAPH=1
+	export GIT_TEST_MULTI_PACK_INDEX=1
+	make test
+	;;
+linux-gcc-4.8)
+	# Don't run the tests; we only care about whether Git can be
+	# built with GCC 4.8, as it errors out on some undesired (C99)
+	# constructs that newer compilers seem to quietly accept.
+	;;
+*)
+	make test
+	;;
+esac
+
+check_unignored_build_artifacts
+
+save_good_tree
diff --git a/third_party/git/ci/run-linux32-build.sh b/third_party/git/ci/run-linux32-build.sh
new file mode 100755
index 000000000000..e3a193adbce3
--- /dev/null
+++ b/third_party/git/ci/run-linux32-build.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+#
+# Build and test Git in a 32-bit environment
+#
+# Usage:
+#   run-linux32-build.sh <host-user-id>
+#
+
+set -ex
+
+if test $# -ne 1 || test -z "$1"
+then
+	echo >&2 "usage: run-linux32-build.sh <host-user-id>"
+	exit 1
+fi
+
+# Update packages to the latest available versions
+linux32 --32bit i386 sh -c '
+    apt update >/dev/null &&
+    apt install -y build-essential libcurl4-openssl-dev libssl-dev \
+	libexpat-dev gettext python >/dev/null
+'
+
+# If this script runs inside a docker container, then all commands are
+# usually executed as root. Consequently, the host user might not be
+# able to access the test output files.
+# If a non 0 host user id is given, then create a user "ci" with that
+# user id to make everything accessible to the host user.
+HOST_UID=$1
+if test $HOST_UID -eq 0
+then
+	# Just in case someone does want to run the test suite as root.
+	CI_USER=root
+else
+	CI_USER=ci
+	if test "$(id -u $CI_USER 2>/dev/null)" = $HOST_UID
+	then
+		echo "user '$CI_USER' already exists with the requested ID $HOST_UID"
+	else
+		useradd -u $HOST_UID $CI_USER
+	fi
+
+	# Due to a bug the test suite was run as root in the past, so
+	# a prove state file created back then is only accessible by
+	# root.  Now that bug is fixed, the test suite is run as a
+	# regular user, but the prove state file coming from Travis
+	# CI's cache might still be owned by root.
+	# Make sure that this user has rights to any cached files,
+	# including an existing prove state file.
+	test -n "$cache_dir" && chown -R $HOST_UID:$HOST_UID "$cache_dir"
+fi
+
+# Build and test
+linux32 --32bit i386 su -m -l $CI_USER -c '
+	set -ex
+	cd /usr/src/git
+	test -n "$cache_dir" && ln -s "$cache_dir/.prove" t/.prove
+	make
+	make test
+'
diff --git a/third_party/git/ci/run-linux32-docker.sh b/third_party/git/ci/run-linux32-docker.sh
new file mode 100755
index 000000000000..751acfcf8a8c
--- /dev/null
+++ b/third_party/git/ci/run-linux32-docker.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+#
+# Download and run Docker image to build and test 32-bit Git
+#
+
+. ${0%/*}/lib.sh
+
+docker pull daald/ubuntu32:xenial
+
+# Use the following command to debug the docker build locally:
+# $ docker run -itv "${PWD}:/usr/src/git" --entrypoint /bin/bash daald/ubuntu32:xenial
+# root@container:/# /usr/src/git/ci/run-linux32-build.sh <host-user-id>
+
+container_cache_dir=/tmp/travis-cache
+
+docker run \
+	--interactive \
+	--env DEVELOPER \
+	--env DEFAULT_TEST_TARGET \
+	--env GIT_PROVE_OPTS \
+	--env GIT_TEST_OPTS \
+	--env GIT_TEST_CLONE_2GB \
+	--env cache_dir="$container_cache_dir" \
+	--volume "${PWD}:/usr/src/git" \
+	--volume "$cache_dir:$container_cache_dir" \
+	daald/ubuntu32:xenial \
+	/usr/src/git/ci/run-linux32-build.sh $(id -u $USER)
+
+check_unignored_build_artifacts
+
+save_good_tree
diff --git a/third_party/git/ci/run-static-analysis.sh b/third_party/git/ci/run-static-analysis.sh
new file mode 100755
index 000000000000..a19aa7ebbc09
--- /dev/null
+++ b/third_party/git/ci/run-static-analysis.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# Perform various static code analysis checks
+#
+
+. ${0%/*}/lib.sh
+
+make coccicheck
+
+set +x
+
+fail=
+for cocci_patch in contrib/coccinelle/*.patch
+do
+	if test -s "$cocci_patch"
+	then
+		echo "$(tput setaf 1)Coccinelle suggests the following changes in '$cocci_patch':$(tput sgr0)"
+		cat "$cocci_patch"
+		fail=UnfortunatelyYes
+	fi
+done
+
+if test -n "$fail"
+then
+	echo "$(tput setaf 1)error: Coccinelle suggested some changes$(tput sgr0)"
+	exit 1
+fi
+
+save_good_tree
diff --git a/third_party/git/ci/run-test-slice.sh b/third_party/git/ci/run-test-slice.sh
new file mode 100755
index 000000000000..f8c2c3106a2e
--- /dev/null
+++ b/third_party/git/ci/run-test-slice.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+#
+# Test Git in parallel
+#
+
+. ${0%/*}/lib.sh
+
+case "$CI_OS_NAME" in
+windows*) cmd //c mklink //j t\\.prove "$(cygpath -aw "$cache_dir/.prove")";;
+*) ln -s "$cache_dir/.prove" t/.prove;;
+esac
+
+make --quiet -C t T="$(cd t &&
+	./helper/test-tool path-utils slice-tests "$1" "$2" t[0-9]*.sh |
+	tr '\n' ' ')"
+
+check_unignored_build_artifacts
diff --git a/third_party/git/ci/test-documentation.sh b/third_party/git/ci/test-documentation.sh
new file mode 100755
index 000000000000..d49089832de0
--- /dev/null
+++ b/third_party/git/ci/test-documentation.sh
@@ -0,0 +1,42 @@
+#!/usr/bin/env bash
+#
+# Perform sanity checks on documentation and build it.
+#
+
+. ${0%/*}/lib.sh
+
+filter_log () {
+	sed -e '/^GIT_VERSION = /d' \
+	    -e '/^    \* new asciidoc flags$/d' \
+	    "$1"
+}
+
+make check-builtins
+make check-docs
+
+# Build docs with AsciiDoc
+make doc > >(tee stdout.log) 2> >(tee stderr.raw >&2)
+cat stderr.raw
+filter_log stderr.raw >stderr.log
+test ! -s stderr.log
+test -s Documentation/git.html
+test -s Documentation/git.xml
+test -s Documentation/git.1
+grep '<meta name="generator" content="AsciiDoc ' Documentation/git.html
+
+rm -f stdout.log stderr.log stderr.raw
+check_unignored_build_artifacts
+
+# Build docs with AsciiDoctor
+make clean
+make USE_ASCIIDOCTOR=1 doc > >(tee stdout.log) 2> >(tee stderr.raw >&2)
+cat stderr.raw
+filter_log stderr.raw >stderr.log
+test ! -s stderr.log
+test -s Documentation/git.html
+grep '<meta name="generator" content="Asciidoctor ' Documentation/git.html
+
+rm -f stdout.log stderr.log stderr.raw
+check_unignored_build_artifacts
+
+save_good_tree
diff --git a/third_party/git/ci/util/extract-trash-dirs.sh b/third_party/git/ci/util/extract-trash-dirs.sh
new file mode 100755
index 000000000000..8e67bec21a27
--- /dev/null
+++ b/third_party/git/ci/util/extract-trash-dirs.sh
@@ -0,0 +1,50 @@
+#!/bin/sh
+
+error () {
+	echo >&2 "error: $@"
+	exit 1
+}
+
+find_embedded_trash () {
+	while read -r line
+	do
+		case "$line" in
+		*Start\ of\ trash\ directory\ of\ \'t[0-9][0-9][0-9][0-9]-*\':*)
+			test_name="${line#*\'}"
+			test_name="${test_name%\'*}"
+
+			return 0
+		esac
+	done
+
+	return 1
+}
+
+extract_embedded_trash () {
+	while read -r line
+	do
+		case "$line" in
+		*End\ of\ trash\ directory\ of\ \'$test_name\'*)
+			return
+			;;
+		*)
+			printf '%s\n' "$line"
+			;;
+		esac
+	done
+
+	error "unexpected end of input"
+}
+
+# Raw logs from Linux build jobs have CRLF line endings, while OSX
+# build jobs mostly have CRCRLF, except an odd line every now and
+# then that has CRCRCRLF.  'base64 -d' from 'coreutils' doesn't like
+# CRs and complains about "invalid input", so remove all CRs at the
+# end of lines.
+sed -e 's/\r*$//' | \
+while find_embedded_trash
+do
+	echo "Extracting trash directory of '$test_name'"
+
+	extract_embedded_trash |base64 -d |tar xzp
+done