logo

shit

Unnamed repository; edit this file 'description' to name the repository.
commit: 308ffecc89bce90960bdbbdd2f4cfd4fce8a7367
Author: Drew DeVault <sir@cmpwn.com>
Date:   Tue, 11 Feb 2020 17:46:49 -0500

Initial commit

Diffstat:

ALICENSE13+++++++++++++
AREADME.md17+++++++++++++++++
Acommit-tree52++++++++++++++++++++++++++++++++++++++++++++++++++++
Acommon.sh83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahash-object77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainit21+++++++++++++++++++++
Als-files71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aupdate-index123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awrite-tree23+++++++++++++++++++++++
9 files changed, 480 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,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. diff --git a/README.md b/README.md @@ -0,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. diff --git a/commit-tree b/commit-tree @@ -0,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 diff --git a/common.sh b/common.sh @@ -0,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" +) diff --git a/hash-object b/hash-object @@ -0,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 diff --git a/init b/init @@ -0,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 diff --git a/ls-files b/ls-files @@ -0,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 diff --git a/update-index b/update-index @@ -0,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 diff --git a/write-tree b/write-tree @@ -0,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