~sircmpwn/shit

308ffecc89bce90960bdbbdd2f4cfd4fce8a7367 — Drew DeVault 7 months ago
Initial commit
9 files changed, 480 insertions(+), 0 deletions(-)

A LICENSE
A README.md
A commit-tree
A common.sh
A hash-object
A init
A ls-files
A update-index
A write-tree
A  => LICENSE +13 -0
@@ 1,13 @@
       DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
                   Version 2, December 2004 

Copyright (C) 2004 Sam Hocevar <sam@hocevar.net> 

Everyone is permitted to copy and distribute verbatim or modified 
copies of this license document, and changing it is allowed as long 
as the name is changed. 

           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 
  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 

 0. You just DO WHAT THE FUCK YOU WANT TO.

A  => README.md +17 -0
@@ 1,17 @@
# shit

shit == Shell Git

This is an implementation of Git using (almost) entirely POSIX shell.

Caveats:

- There are a couple of GNU coreutilsisms, which are marked with "XXX: GNUism"
  throughout. They have been tested on BusyBox as well.
- A native zlib implementation is required: [zlib](https://github.com/kevin-cantwell/zlib)
- Why the fuck would you use this

## Status

Enough plumbing commands are written to make this write the initial commit with
itself, which is how the initial commit was written. Huzzah.

A  => commit-tree +52 -0
@@ 1,52 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

tree="$1"
shift

parents=

while getopts p: opt
do
	case $opt in
		p)
			parents="$(printf "$s" "$parents" | tr -s , ' ')"
			;;
		?)
			printf "Usage: %s [-p <parents...>]\n" "$0" >&2
			exit 1
			;;
	esac
done

# TODO: Read from git config
if [ -z "$GIT_AUTHOR_NAME" ]
then
	printf "GIT_AUTHOR_NAME unset\n"
	exit 1
fi
if [ -z "$GIT_AUTHOR_EMAIL" ]
then
	printf "GIT_AUTHOR_EMAIL unset\n"
	exit 1
fi
GIT_COMMITTER_NAME=${GIT_COMMITTER_NAME:-$GIT_AUTHOR_NAME}
GIT_COMMITTER_EMAIL=${GIT_COMMITTER_EMAIL:-$GIT_AUTHOR_EMAIL}
# XXX: GNUism
GIT_AUTHOR_DATE=${GIT_AUTHOR_DATE:-$(date +'%s %z')}
GIT_COMMITTER_DATE=${GIT_COMMITTER_DATE:-$(date +'%s %z')}

printf "tree %s\n" "$tree"
for parent in $parents
do
	printf "parent %s\n" "$parent"
done
printf "author %s <%s> %s\n" \
	"$GIT_AUTHOR_NAME" "$GIT_AUTHOR_EMAIL" "$GIT_AUTHOR_DATE"
printf "committer %s <%s> %s\n" \
	"$GIT_COMMITTER_NAME" "$GIT_COMMITTER_EMAIL" "$GIT_COMMITTER_DATE"
printf "\n"

printf 'Enter your comment message:\n' >&2
cat

A  => common.sh +83 -0
@@ 1,83 @@
# TODO: Find git dir; global options
# TODO: LIBEXECDIR or something
GIT_DIR="${GIT_DIR:-.git}"

INDEX_VERSION=2

gitsort() (
	# This will still often be wrong
	LANG=C sort
)

# Is it hacky? Hell yes. Is it POSIX? HELL YES.
write_hex() {
	hex="$1"
	while [ -n "$hex" ]
	do
		cur=$(printf "%s" "$hex" | cut -c1-2)
		next=$(printf "%s" "$hex" | cut -c3-)
		printf "\\x$(printf "%s" "$cur")"
		hex="$next"
	done
}

# Prints an integer to stdout in binary, big-endian
write_int32() (
	n="$1"
	hex=$(printf "%08X" "$n")
	write_hex "$hex"
)

write_int16() (
	n="$1"
	hex=$(printf "%04X" "$n")
	write_hex "$hex"
)

read_text() (
	path="$1"
	offs="$2"
	len="$3"
	for oct in $(od -An -txC -N"$len" -j"$offs" "$index")
	do
		printf "\x$oct"
	done
)

read_int16() (
	path="$1"
	offs="$2"
	i16=$(od -An -tdS -j"$offs" -N2 "$path" | tr -d ' ')
	i16=$((((i16>>8)&0xff) | ((i16<<8)&0xff00)))
	echo "$i16"
)

read_int32() (
	path="$1"
	offs="$2"
	i32=$(od -An -tdI -j"$offs" -N4 "$path" | tr -d ' ')
	i32=$((((i32>>24)&0xff) |
		((i32<<8)&0xff0000) |
		((i32>>8)&0xff00) |
		((i32<<24)&0xff000000)))
	echo "$i32"
)

read_hex() (
	path="$1"
	offs="$2"
	len="$3"
	od -An -txC -N"$len" -j"$offs" "$path" | tr -d ' \n'
)

normalize_path() (
	path="$1"
	path="${path#./}"
	# TODO: Remove the leading / if fully qualified
	if [ "${path#.git}" != "$path" ]
	then
		printf '%s' 'Invalid path %s\n' "$path"
		exit 1
	fi
	printf "%s" "$path"
)

A  => hash-object +77 -0
@@ 1,77 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

header() (
	objtype="$1"
	case "$objtype" in
		blob|tree|commit)
			len="$2"
			printf '%s %d\u0000' "$objtype" "$len"
			;;
		*)
			printf 'Unknown object type %s\n' "$1" >&2
			exit 1
			;;
	esac
)

write_object() (
	object_type="$1"
	path="$2"
	len=$(wc -c "$path" | cut -d' ' -f1)
	header "$object_type" "$len"
	cat "$path"
)

object_type=blob
write=0

while getopts t:w opt
do
	case $opt in
		t)
			object_type="$OPTARG"
			;;
		w)
			write=1
			;;
		?)
			printf "Usage: %s [-t <type>] [-w] <files...>\n" "$0" >&2
			exit 1
			;;
	esac
done

shift $((OPTIND-1))

process() {
	path="$1"
	if [ $write -eq 1 ]
	then
		sha=$(write_object "$object_type" "$path" | sha1sum | cut -d' ' -f1)
		prefix=$(printf "%s" "$sha" | cut -c1-2)
		suffix=$(printf "%s" "$sha" | cut -c3-)
		mkdir -p "$GIT_DIR"/objects/"$prefix"
		if ! [ -e "$GIT_DIR"/objects/"$prefix"/"$suffix" ]
		then
			write_object "$object_type" "$path" | "$SHIT_PATH"/zlib \
				>"$GIT_DIR"/objects/"$prefix"/"$suffix"
		fi
	else
		sha=$(write_object "$object_type" "$path" | sha1sum | cut -d' ' -f1)
	fi
	printf '%s\n' "$sha"
}

for path in "$@"
do
	process "$path"
done

if [ $# -eq 0 ]
then
	tee > "$GIT_DIR"/objects/NEW_OBJECT
	trap "rm '$GIT_DIR/objects/NEW_OBJECT'" EXIT
	process "$GIT_DIR"/objects/NEW_OBJECT
fi

A  => init +21 -0
@@ 1,21 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

for dir in branches info objects/info objects/pack refs/heads refs/tags
do
	mkdir -p "$GIT_DIR"/$dir
done

cat <<EOF >"$GIT_DIR"/config
[core]
	repositoryformatversion = 0
	filemode = true
	bare = false
	logallrefupdates = true
EOF

echo "Unnamed repository; edit this file 'description' to name the repository." \
	>"$GIT_DIR"/description

echo "ref: refs/heads/master" >"$GIT_DIR"/HEAD

A  => ls-files +71 -0
@@ 1,71 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

index="$GIT_DIR"/index

magic=$(od -An -tc -N4 "$index" | tr -d ' ')
version=$(read_int32 "$index" 4)
nentries=$(read_int32 "$index" 8)

if [ "$magic" != "DIRC" ]
then
	printf "Invalid git index format\n" >&2
	exit
fi

if [ $version -ne 2 ]
then
	printf "Only git index version %d is supported, %d was found\n" \
		"$INDEX_VERSION" "$version" >&2
	exit 1
fi

offs=12
while [ $nentries -gt 0 ]
do
	# mode  @ 24 bytes
	# sha   @ 40 bytes
	# flags @ 60 bytes
	# name  @ 62 bytes
	mode=$(read_int32 "$index" $((offs+24)))
	sha=$(read_hex "$index" $((offs+40)) 20)
	flags=$(read_int16 "$index" $((offs+60)))

	# 0bNNNNxxxMMMMMMMMM
	# NNNN -> object type
	# MMMMMMMMM -> unix file mode (644 or 755)
	case $(printf "%X" $((mode & 0xF000))) in
		8000)
			objtype=file
			;;
		A000)
			objtype=link
			;;
		E000)
			objtype=gitlink
			;;
		*)
			printf "Invalid object type %x\n" $(((mode & 0xF000) >> 12)) >&2
			exit 1
			;;
	esac
	mode=$((mode & 0x1FF))
	if [ $mode -ne $((0644)) ] && [ $mode -ne $((0755)) ] && [ $mode -ne 0 ]
	then
		printf "Invalid file mode %o\n" $mode >&2
		exit 1
	fi

	pathlen=$((flags & 0xFFF))
	path="$(read_text "$index" $((offs+62)) $pathlen)"

	printf "%s %o %s %s\n" "$objtype" "$mode" "$sha" "$path"

	padding=$((${#path} + 1 + 62))
	padding=$((padding % 8))
	padding=$(((8 - padding) % 8))
	offs=$((offs+62+${#path}+1+padding))

	nentries=$((nentries-1))
done

A  => update-index +123 -0
@@ 1,123 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

write_index_header() (
	nentries="$1"
	printf 'DIRC'
	write_int32 $INDEX_VERSION
	write_int32 $nentries
	# TODO: Extensions would go here if we cared
)

write_index_file() (
	path="$(normalize_path "$1")"
	stat=$(stat -c "%W %Y %d %i %u %g %s" "$path") # XXX: GNUism
	ctime=$(printf "%s" "$stat" | cut -d' ' -f1)
	mtime=$(printf "%s" "$stat" | cut -d' ' -f2)
	dev=$(printf "%s" "$stat" | cut -d' ' -f3)
	inode=$(printf "%s" "$stat" | cut -d' ' -f4)
	uid=$(printf "%s" "$stat" | cut -d' ' -f5)
	gid=$(printf "%s" "$stat" | cut -d' ' -f6)
	size=$(printf "%s" "$stat" | cut -d' ' -f7)

	write_int32 "$ctime"
	write_int32 0 # nanoseconds
	write_int32 "$mtime"
	write_int32 0 # nanoseconds
	write_int32 "$dev"
	write_int32 "$inode"
	# object type & mode
	if [ -x "$path" ]
	then
		write_int32 $((0x8000 | 0755))
	else
		write_int32 $((0x8000 | 0644))
	fi
	write_int32 "$uid"
	write_int32 "$gid"
	write_int32 "$size"
	sha=$("$SHIT_PATH"/hash-object -w "$path")
	write_hex "$sha"
	# XXX: If file name length is >0xFFF this is wrong
	write_int16 "${#path}"
	printf '%s\0' "$path"
	padding=$((${#path} + 1 + 62))
	padding=$((padding % 8))
	padding=$(((8 - padding) % 8))
	while [ $padding -gt 0 ]
	do
		printf '\0'
		padding=$((padding-1))
	done
)

write_index_link() (
	printf '%s' "Symlinks are not implemented\n" >&2
	exit 1
)

do_add=0
do_remove=0
force_remove=0

while [ $# -ne 0 ]
do
	case "$1" in
		--add)
			do_add=1
			;;
		--remove)
			do_remove=1
			;;
		--force-remove)
			do_remove=1
			force_remove=1
			;;
		*)
			break
			;;
	esac
	shift
done

# TODO: Update existing index

cleanup_old_index() {
	if [ $? -eq 0 ]
	then
		rm "$GIT_DIR"/index.old
	else
		# Restore old index on error
		mv "$GIT_DIR"/index.old "$GIT_DIR"/index
	fi
}

if [ -f "$GIT_DIR"/index ]
then
	mv "$GIT_DIR"/index "$GIT_DIR"/index.old
	trap cleanup_old_index EXIT
fi

# TODO: Write out to >>"$GIT_DIR"/index
write_index_header "$#" >>"$GIT_DIR"/index

for path in "$@"
do
	printf "%s\n" "$path"
done | gitsort | while read -r path
do
	if [ -f "$path" ]
	then
		write_index_file "$path" >>"$GIT_DIR"/index
	elif [ -L "$path" ]
	then
		write_index_link "$path" >>"$GIT_DIR"/index
	else
		printf "Invalid path for indexing: %s\n" "$path" >&2
		exit 1
	fi
done

sha=$(sha1sum "$GIT_DIR"/index | cut -d' ' -f1)
write_hex "$sha" >>"$GIT_DIR"/index

A  => write-tree +23 -0
@@ 1,23 @@
#!/bin/sh -eu
SHIT_PATH=$(dirname "$0")
. $SHIT_PATH/common.sh

"$SHIT_PATH"/ls-files | while read -r type mode sha path
do
	case $type in
		file)
			mode=100$mode
			;;
		link)
			mode=012$mode
			;;
		gitlink)
			printf "submodules are unimplemented\n" >&2
			exit 1
			;;
	esac
	# TODO: subtrees
	objtype=blob
	printf "%s %s\0" $mode "$path"
	write_hex "$sha"
done