Think about `gitfs`, which will serve a git repo:
gitfs, gitdb/read, gitdb/pack, gitdb/...  - mount a git repo, and manipulate

term% gitfs [-s service] [-c cachedir] [-m mntpnt] [-d dialstring] path
# for example:
# gitfs tcp!github.com!git oridb/git9
# gitfs tcp!github.com oridb/git9
# gitfs /n/somewhere/perl.git
# in the second form above, ssh, git, https, http, ftp, then 'dumb' service will
be tried, in that order.  When no service given print 'using https service', or
the appropriate service, on stderr.
term% lstree /mnt/git
/mnt/git/
	/ctl # echo dialstring ....  > ctl # updates dial string to use,
	re-doing all of the synthetic tree
		    # echo path ....  > ctl # updates path to use, re-doing all
		    of the synthetic tree
			# echo cachedir ....  > ctl # update cachedir to use.
			cachedir stores
			# echo fetch > ctl # updates cachedir for dialstring &
			path
			# echo fetch branch > ctl # updates cachedir for
			dialstring & path for path
			# echo fetch /tcp > ctl # updates cachedir for all
			connections under /cachedir matching path
			# echo fetch /tcp master > ctl # updates cachedir for
			all connections under /cachedir matching /tcp for their
			master branches
	    # echo dumps 300 >> ctl # make a backup every 5m, creating a
	    hash
	/dialstring
	/path
    /dumps # 'dump' 's'econds
	/branch # <-- present if -b branch is passed in; remove to fetch/sync
	more branches
	/cachedir/ <---- cached content
		/tcp/github.com/git+ssh/oridb/git9.git
		/tcp/github.com/https/oridb/git9.git
		/9p/n/somewhere/perl.git
		/dump/2020/05/10/path/to/work/basesha1/dumpsha1
	/packs
	/exclude <-- system-wide exclude paths under content/ dirs, (that is,
	.git/exclude).
	/log <-- read-only git reflog
	/pack/
		978e03944f5c581011e6998cd0e9e30000905586/idx
												/pack
	/tree/
		/00/6457..../
					/clone # echo /path/to/work >> clone
					# 'git branch -b work'; make working dir
					off of this tree.  Sets '/message' to
					empty
							# echo /path/to/work
							other_tree > clone #
							'git merge other_tree';
							merge other_tree into this
							one, setting staged area
							at /path/to/work.  If more
							than one commit between
							other_tree and the current
							tree, does a squash
							commit.  If you want to do
							interactive merges, use
							git/merge.  Sets
							'/message' to empty.
							print 'see
							/path/to/work/conflicts'
							on stderr if conflicts
							present.  That file
							contains the paths
							relative to /path/to/work
							that require resolution.
							If any conflicts, you must
							resolve under
							/path/to/work.
							/path/to/work will resolve
							conflicted paths when
							doing 'cat
							/path/to/work/conflicts |
							sed 'add $_' > ctl'.
							If no conflicts, then
							ready to 'echo sync >
							ctl'.  Uses contents of
							'/sym/' in tree which
							'clone' opened from.
					/date
					/sha1
					/sym/ # present if HEAD of branch(es)
					/tag/ # present if tag(s)
					/author
					/message
					/content/
						/.git/
							....
						/.gitmodules <- submodules!
						*after* you gitfs to another mount
						point using the values from its
						files, simply 'bind -bc
						./path/to/submodule.  When
						syncing, gitfs will be aware.
						Conjecture: have gitfs -s
						/.gitsubmodules
						/file.txt
						/repo.rb
						/test.txt
						/dir1/
							....
		/sym/
			/master/
						/clone # echo /path/to/work >
						clone # 'git branch -b ...' (see
						above); working path, setting
						'/message' to empty.  if 'echo
						/path/to/more/work > clone'
						done without /path/to/work being
						sync'd, then clones off of
						committed tree (e.g., 'master'),
						adding the contents of
						/path/to/work to
						/path/to/more/work
						/ctl # 'echo sync' to save
						changes.  if only author/message
						changed, then it's 'amend',
						otherwise it's a new commit based
						off of existing master's sha1
								# cat ctl #
								returns git's raw
								tree data
								# echo man >
								ctl [2]| cat #
								return man page
								# echo revert >
								ctl to revert all
								content changes
								# echo date >
								ctl [2]| cat #
								returns date
								# echo author >
								ctl [2]| cat #
								returns author
								# echo message
								> ctl [2]| cat
								returns author
								# echo committer
								> ctl [2]| cat
								returns committer
								# echo log >
								ctl [2]| cat
								returns commit log
						/sha1 # read-only
						/sym/ # present if HEAD of
						branch(es)
						/tag/ # present if tag(s)
							/v1.4/
							/blah/
						/author
						/message
						/committer
						/content/...
						# do some changes in content/,
						then echo sync > ctl
						# error if 'message' is empty or
						not changed.
						# child and parent commits are
						used.
				/mybranch/
		/tag/.....  # <--- tags of trees
	/blob/ref/00/6a07..../
						/ctl
						/content
		/tag/....  # <--- tags of blobs
		/clone # cat foo > clone # create a blob, printing its ref on
		stderr
	/commit/ref/00/58483..../ <--- read-only, except for ctl file, and
	/parent/, /child/ hashes, which are just binds to their respective trees
						/ctl # echo sym ....  > ctl #
						creates lightweight tag
						/author
						/committer
						/message
						/tag/ # present if tag(s)
						/log # returns commit log
						/parent/
							hash1...
							hash2...
						/child/
							hash1...
							hash2...
	/tag/ref/00/a4fd44....
		/man
		/tree/
		/blob/
		/commit/
			/v1.0/
				/clone # echo /path/to/myblob > clone # staging
				area for annotated tag
					/myblob/ctl # echo sync > ctl # save
					tag.  lightweight tag if
					{tagger,message,{commit,blob,tree}} empty.
					If one not empty and others are, an error.
					tagger, message, and {commit,blob,tree}
					create annotated tag
							/tagger
							/message
							# dircp
							../../../{commit,blob,tree}/00/....
							./
							# then echo sync > ctl
							# store, moving this dir
							itself to be under
							tree/tag & tag/tree,
							blob/tag & tag/blob, or
							commit/tag & tag/commit
				/ctl
				/ref
				/tagger
				/message
				/commit/


# EXAMPLE
term% echo $home/src/work > /mnt/git/tree/sym/master/ctl
term% cd $home/src/work
term% lc
clone ctl sym/ /tag/ sha1 author message committer content/
term% cat sha1
00495852568493...
term% lc sym/
master branch1 branch2
term% cd $home/src/work/content # then do some changes
term% diff -u /mnt/git/tree/sym/master/content . # shows diff
term% cd ..  # cd $home/src/work
term% echo '
	this is my commit message
	' > $home/src/work/message
term% rm sym/branch1
term% echo sync > ctl
cat sha1
3b15643d.....
term% lc sym
master branch2
term% cd /mnt/git
term% lc
ctl dialstring path cachedir/ packs exclude log pack/ tree/ blob/ commit/ tag/
term% echo fetch > ctl # fetch new commits for dialstring & path
term% echo fetch tree/sym/master > ctl # fetch only master branch
term% echo sync > ctl # push all changes to remote
term% rm -r tree/sym/mybranch # set to remove remote branch
term% echo fetch > ctl # nevermind!  adds back tree/sym/mybranch
term% rm -r tree/sym/secondbranch # set to remove remote branch
term% echo sync > ctl # removed remote branch

If path is a local directory and ends in .git but does not have the structure
expected, fail.
If path is a local directory and does not end in .git, and has a .git subdirectory
but does not have the expected structure, fail.
If path is a local directory and does not end in .git, and does not have .git,
'git init' that directory.


https://stackoverflow.com/a/18732276 <-- why storing in two directories, not
one:
	Git switches from "loose objects" (in files named like
	01/23456789abcdef0123456789abcdef01234567) to "packs" when the number of
	loose objects exceeds a magic constant (6700 by default but configurable,
	gc.auto).  Since SHA-1 values tend to be well-distributed it can
	approximate total loose objects by looking in a single directory.  If
	there are more than (6700 + 255) / 256 = 27 files in one of the object
	directories, it's time for a pack-file.

Thus, there's no need for additional fan-out (01/23/4567...): it's unlikely that
you will get that many objects in one directory.  And in fact, greater fan-out
would tend to make it harder to detect that it is time for an automatic packing,
unless you set the threshold value higher (than 6700), because (27 + 255) / 256 is
1—so you'd want to count everything in 01/*/ instead of just 01/.

One could use 0/1234567...  and allow up to ~419 objects per directory to get the
same behavior, but linear directory scans (on any system that still uses those)
are O(n2), and 272 is a mere 729, while 4192 is 175561.  [Edit: that only applies
to file creation, where you have a two stage search, once to find that it's OK to
create and a second to find a slot or append.  Lookups are still O(n).]


https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Porcelain

https://git-scm.com/docs/gitrepository-layout#gitrepository-layout-objectsinfo

TODO
- how to deal with submodules?