git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[RFC PATCH 1/1] recover: restoration of deleted worktree files

From
Edward Thomson <ethomson@edwardthomson.com>
Date
Aug 4, 2018, 14:24 UTC
Message-ID
<20180804142416.GA6@5f28dc333bbd>
In-Reply-To
<20180804142247.GA7@e3c0ce5ceb57>
Introduce git-recover, a simple script to aide in restoration of deleted
worktree files.  This will look for unreachable blobs in the object
database and prompt users to restore them to disk, either interactively
or on the command-line.
---
 git-recover.sh | 311 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 311 insertions(+)
 create mode 100755 git-recover.sh
diff --git a/git-recover.sh b/git-recover.sh
new file mode 100755
index 000000000..651d4116f
--- /dev/null
+++ b/git-recover.sh
@@ -0,0 +1,311 @@
+#!/usr/bin/env bash
+#
+# This program helps recover files in your repository that were deleted
+# from the working tree.
+#
+# Copyright (c) 2017-2018 Edward Thomson.
+
+set -e
+
+IFS=$'\n'
+
+PROGNAME=$(echo "$0" | sed -e 's/.*\///')
+GIT_DIR=$(git rev-parse --git-dir)
+
+DO_RECOVER=0
+DO_FULL=0
+DO_INTERACTIVE=0
+BLOBS=()
+FILENAMES=()
+
+function die_usage {
+	echo "usage: $PROGNAME [-a] [-i] [--full] [<id> [-f <filename>] ...]" >&2
+	exit 1
+}
+
+while [[ $# -gt 0 ]]; do
+	case "$1" in
+	-a|--all)
+		DO_RECOVER=1
+		;;
+	-i|--interactive)
+		DO_INTERACTIVE=1
+		;;
+	--full)
+		DO_FULL=1
+		;;
+	*)
+		if [ "${1:0:1}" == "-" ]; then
+			echo "$PROGNAME: unknown argument: $1" >&2
+			die_usage
+		fi
+		BLOBS+=("$1")
+
+		shift
+		if [ "$1" == "-f" ] || [ "$1" == "--filename" ]; then
+			shift
+			if [ $# == 0 ]; then
+				die_usage
+			fi
+			FILENAMES+=("$1")
+			shift
+		else
+			FILENAMES+=("")
+		fi
+		continue
+	;;
+	esac
+	shift
+done
+
+if [ ${#BLOBS[@]} != 0 ] && [ $DO_RECOVER == 1 ]; then
+	die_usage
+elif [ ${#BLOBS[@]} != 0 ]; then
+	DO_RECOVER=1
+fi
+
+case "$OSTYPE" in
+	darwin*|freebsd*) IS_BSD=1 ;;
+	*) IS_BSD=0 ;;
+esac
+
+function expand_given_blobs() {
+	for i in "${!BLOBS[@]}"; do
+		ID=$(git rev-parse --verify "${BLOBS[$i]}" 2>/dev/null || true)
+
+		if [ -z "$ID" ]; then
+			echo "$PROGNAME: ${BLOBS[$i]} is not a valid object." 1>&2
+			exit 1
+		fi
+
+		TYPE=$(git cat-file -t "${ID}" 2>/dev/null || true)
+
+		if [ "$TYPE" != "blob" ]; then
+			echo "$PROGNAME: ${BLOBS[$i]} is not a blob." 1>&2
+			exit
+		fi
+
+		BLOBS[$i]=$ID
+	done
+}
+
+# find all the unreachable blobs
+function find_unreachable() {
+	FULLNESS="--no-full"
+
+	if [ $DO_FULL == 1 ]; then FULLNESS="--full"; fi
+
+	BLOBS=($(git fsck --unreachable --no-reflogs \
+		"${FULLNESS}" --no-progress | sed -ne 's/^unreachable blob //p'))
+}
+
+function read_one_file {
+	BLOB=$1
+	FILTER_NAME=$2
+	ARGS=()
+
+	if [ -z "$FILTER_NAME" ]; then
+		ARGS+=("blob")
+	else
+		ARGS+=("--filters" "--path=$FILTER_NAME")
+	fi
+
+	git cat-file "${ARGS[@]}" "$BLOB"
+}
+
+function write_one_file {
+	BLOB=$1
+	FILTER_NAME=$2
+	OUTPUT_NAME=$3
+
+	ABBREV=$(git rev-parse --short "${BLOB}")
+
+	echo -n "Writing $ABBREV: "
+	read_one_file "$BLOB" "$FILTER_NAME" > "$OUTPUT_NAME"
+	echo "$OUTPUT_NAME."
+}
+
+function unique_filename {
+	if [ ! -f "${BLOB}" ]; then
+		echo "$BLOB"
+	else
+		cnt=1
+		while true
+		do
+			fn="${BLOB}~${cnt}"
+			if [ ! -f "${fn}" ]; then
+				echo "${fn}"
+				break
+			fi
+			cnt=$((cnt+1))
+		done
+	fi
+}
+
+function write_recoverable {
+	for i in "${!BLOBS[@]}"; do
+		BLOB=${BLOBS[$i]}
+		FILTER_NAME=${FILENAMES[$i]}
+		OUTPUT_NAME=${FILENAMES[$i]:-$(unique_filename)}
+
+		write_one_file "$BLOB" "$FILTER_NAME" "$OUTPUT_NAME"
+	done
+}
+
+function file_time {
+	if [ $IS_BSD == 1 ]; then
+		stat -f %c "$1"
+	else
+		stat -c %Y "$1"
+	fi
+}
+
+function timestamp_to_s {
+	if [ $IS_BSD == 1 ]; then
+		date -r "$1"
+	else
+		date -d @"$1"
+	fi
+}
+
+function sort_by_timestamp {
+	# sort blobs in loose objects by their timestamp (packed blobs last)
+	BLOB_AND_TIMESTAMPS=($(for BLOB in "${BLOBS[@]}"; do
+		LOOSE="${BLOB::2}/${BLOB:2}"
+		TIME=$(file_time "$GIT_DIR/objects/$LOOSE" 2>/dev/null || true)
+		echo "$BLOB $TIME"
+	done | sort -k2 -r))
+}
+
+function print_recoverable {
+	echo "Recoverable orphaned git blobs:"
+	echo ""
+
+	sort_by_timestamp
+	for BLOB_AND_TIMESTAMP in "${BLOB_AND_TIMESTAMPS[@]}"; do
+		BLOB=${BLOB_AND_TIMESTAMP::40}
+		TIME=${BLOB_AND_TIMESTAMP:41}
+		DATE=$([ ! -z "$TIME" ] && timestamp_to_s "$TIME" || echo "(Unknown)") 
+
+		echo "$BLOB  $DATE"
+	done
+}
+
+function prompt_for_filename {
+	while true
+	do
+		echo -n "Filename (return to skip): "
+		read -r FILENAME
+
+		if [ -f "$FILENAME" ]; then
+			echo -n "File exists, overwrite? [y,N]: "
+			read -r overwrite
+
+			case "$overwrite" in
+			[yY]*)
+				return 0
+				;;
+			esac
+
+			echo
+		else
+			return 0
+		fi
+	done
+}
+
+function view_file {
+	read_one_file "${BLOB}" | ${PAGER:-less}
+}
+
+function show_summary {
+	FILETYPE=$(read_one_file "${BLOB}" | file -b -)
+	IS_TEXT=$(echo "${FILETYPE}" | grep -c ' text$' 2>/dev/null || true)
+
+	if [ "$IS_TEXT" == "1" ]; then
+		read_one_file "${BLOB}"
+	else
+		read_one_file "${BLOB}" | hexdump -C
+	fi
+}
+
+function interactive {
+	echo "Recoverable orphaned git blobs:"
+
+	sort_by_timestamp
+	for BLOB_AND_TIMESTAMP in "${BLOB_AND_TIMESTAMPS[@]}"; do
+		echo
+
+		BLOB=${BLOB_AND_TIMESTAMP::40}
+		TIME=${BLOB_AND_TIMESTAMP:41}
+		DATE=$([ ! -z "$TIME" ] && timestamp_to_s "$TIME" || echo "(Unknown)") 
+
+		echo "$BLOB  ($DATE)"
+		show_summary "${BLOB}" | head -4 | sed -e 's/^/> /'
+		echo
+
+		while true
+		do
+			echo -n "Recover this file? [y,n,v,f,q,?]: "
+			read -r ans || return 1
+
+			case "$ans" in
+			[yY]*)
+				write_one_file "${BLOB}" "" "$(unique_filename)"
+				break
+				;;
+			[nN]*)
+				break
+				;;
+			[vV]*)
+				view_file "${BLOB}"
+				echo
+				;;
+			[fF]*)
+				prompt_for_filename
+
+				if [ "$FILENAME" == "" ]; then
+					break
+				fi
+
+				write_one_file "${BLOB}" "${FILENAME}" "${FILENAME}"
+				break
+				;;
+			\?*)
+				echo
+				echo "Do you want to recover this file?"
+				echo " y: yes, write the file to ${BLOB}"
+				echo " n: no, skip this file and see the next orphaned file"
+				echo " v: view the file"
+				echo " f: prompt for a filename to use for recovery"
+				echo " q: quit"
+				echo
+				;;
+			[qQ]*)
+				return 0
+				;;
+			esac
+		done
+	done
+}
+
+
+if [ ${#BLOBS[@]} != 0 ]; then
+	expand_given_blobs
+else
+	find_unreachable
+fi
+
+if [ ${#BLOBS[@]} == 0 ]; then
+	echo "$PROGNAME: no recoverable orphaned blobs."
+	exit
+fi
+
+if [ $DO_INTERACTIVE == 1 ]; then
+	interactive
+elif [ $DO_RECOVER == 1 ]; then
+	write_recoverable
+else
+	print_recoverable
+fi
+
-- 
2.0.0 (libgit2)
Previous: Edward ThomsonNext: Junio C Hamano
Message 2 of 8 in “Introduce git-recover”
  1. 0/1 Introduce git-recoverEdward Thomson, Aug 4, 2018
  2. 1/1 recover: restoration of deleted worktree filesEdward Thomson, Aug 4, 2018
  3. Junio C HamanoAug 4, 2018
  4. Edward ThomsonAug 4, 2018
  5. Junio C HamanoAug 4, 2018
  6. Robert P. J. DayAug 4, 2018
  7. Todd ZullingerAug 4, 2018
  8. Jonathan NiederAug 5, 2018

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.