#!/usr/bin/env perl # Copyright (c) 2010, Marco Fontani . # A Git FUSE filesystem, in Perl. use strict; use warnings; use Data::Dumper; use Fuse qw/fuse_get_context/; use Getopt::Std qw/getopts/; use POSIX qw/ENOENT EISDIR EINVAL/; sub DEBUG () { 1 } sub DIR () { 0040 } sub FILE () { 0100 } sub RONLY () { 0444 } sub RX () { 0555 } sub MODE { ($_[0]<<9)+$_[1] } my $BLKSZ = 10*1024; my $cdup = `git rev-parse --show-cdup 2>/dev/null`; die "Must be launched from a checked-out or bare Git repository" if $?; chomp $cdup; chdir $cdup if $cdup; our $opt_o = ''; getopts('o:') or die "Usage: $0 [-o opt[,opt...]] mountpoint"; my $mountpoint = shift; my $root = { '.' => { type => DIR(), mode => RX(), ctime => time(), }, 'heads' => { type => DIR(), mode => RX(), ctime => time(), }, 'tags' => { type => DIR(), mode => RX(), ctime => time(), }, }; my $pending_writes = {}; # tracks contents for pending writes before flush my $pending_truncate = {}; # tracks contents for pending deletions before flush Fuse::main( mountpoint => $mountpoint, getattr => 'main::e_getattr', getdir => 'main::e_getdir', open => 'main::e_open', statfs => 'main::e_statfs', read => 'main::e_read', 'write' => 'main::e_write', 'mknod' => 'main::e_mknod', 'mkdir' => 'main::e_mkdir', 'unlink' => 'main::e_unlink', 'fsync' => 'main::e_fsync', 'flush' => 'main::e_flush', 'truncate' => 'main::e_truncate', threaded => 0, mountopts => $opt_o, ); sub filename_fixup { $_[0] =~ s!^/!!; $_[0] = '.' unless length $_[0]; $_[0] } sub get_git_info { my ($head,$file) = @_; $file = '' if !defined $file; warn "get_git_info($head,$file)" if DEBUG; my $type = qx{git cat-file -t $head:$file}; chomp($type); if (!length $type) { warn "get_git_info($head,$file): unknown to Git" if DEBUG; return []; } my $size = 0; my $modes = MODE(DIR(),RX()); my $data = undef; if ( $type eq 'blob' ) { $size = qx{git cat-file -s $head:$file}; chomp($size); $data = qx{git cat-file -p $head:$file}; $modes = oct( (split(' ',(split("\n",qx{git ls-tree $head $file}))[0]))[0] ); } warn "get_git_info($head,$file): $type, size $size" if DEBUG; return [$type,$modes,$size,$data]; } sub e_getattr { my $file = filename_fixup( $_[0] ); my ($dev, $ino, $rdev, $blocks, $gid, $uid, $nlink, $blksize) = (0,0,0,1,0,0,1,1024); warn "\n\n" if DEBUG; warn "e_getattr($file)" if DEBUG; my ($atime,$mtime,$ctime) = (time,time,time); if (!exists $root->{$file}) { if ($file =~ /^(heads|tags)\/(.*)$/) { my $head_or_tag = $1; my $head = $2; my $path = ''; if ( $head =~ /^([^\/]*)\/(.*)$/ ) { $head = $1; $path = $2; warn "e_getattr($file): $head_or_tag $head path $path" if DEBUG; } if ( $head_or_tag eq 'tags' and length $head ) { my $data = ''; my $modes = MODE(FILE(),RONLY()); if ( $path eq '__TAG__MESSAGE__' ) { $data = qx{git tag -ln $head}; if (!length $data) { warn "e_getattr($file): $head_or_tag $head path $path ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } elsif ( $path eq '__TAG__SIGNATURE__' ) { $data = qx{git tag -v $head 2>&1}; if (!length $data or $data =~ /^error: tag \`\Q$head\E\` not found\s*$/sm) { warn "e_getattr($file): $head_or_tag $head path $path ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } my $size = length $data; if ( length $data ) { $uid = fuse_get_context()->{"uid"}; $gid = fuse_get_context()->{"gid"}; my $blocks = int((length $data)/$BLKSZ)+1; warn "e_getattr($file) => ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks)" if DEBUG; return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks); } } my $data = get_git_info($head,$path); if (!defined $data->[0]) { warn "e_getattr($file): ENOENT" if DEBUG; return -ENOENT(); } my $modes = $data->[1]; $modes = MODE(FILE(),RONLY()) if ($head_or_tag eq 'tag' and $data->[0] eq 'blob'); my $size = $data->[2]; $uid = fuse_get_context()->{"uid"}; $gid = fuse_get_context()->{"gid"}; my $blocks = int($size/$BLKSZ)+1; warn "e_getattr($file) => ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks)" if DEBUG; return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks); } warn "e_getattr($file): ENOENT" if DEBUG; return -ENOENT(); } # Existing pre-made dir my $modes = MODE( DIR(), RX() ); my $size = 0; $uid = fuse_get_context()->{"uid"}; $gid = fuse_get_context()->{"gid"}; $blocks = 1; warn "e_getattr($file)(cached) => ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks)" if DEBUG; return ($dev,$ino,$modes,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$BLKSZ,$blocks); } sub e_getdir { my $where = shift; warn "\n\n" if DEBUG; warn "e_getdir($where)" if DEBUG; if ($where eq '/') { warn "e_getdir($where) => heads tags 0" if DEBUG; return (qw/heads tags/), 0; }; if ( $where eq '/tags' ) { my @tags = split("\n",qx{git tag}); warn "e_getdir($where) (tags) => @tags, 0" if DEBUG; return @tags, 0; } if ( $where eq '/heads' ) { my @heads = split("\n",qx{git branch}); for (@heads) { $_ =~ s/^\s+//; $_ =~ s/^\*\s+//; } warn "e_getdir($where) (heads)=> @heads, 0" if DEBUG; return @heads, 0; } if ( $where =~ m/^\/?(tags|heads)\/(.*)$/ ) { my $tag_or_head = $1; my $head = $2; my $path = ''; my $thing = $head; if ( $head =~ /^([^\/]*)\/(.*)$/ ) { $head = $1; $path = $2; $thing = "$head:$path"; } my @lines = qx{git ls-tree $thing}; warn "e_getdir($where): executed 'git ls-tree $thing'" if DEBUG; warn "e_getdir($where) ($tag_or_head $head path $path): ls-tree $thing\n@lines\n" if DEBUG; chomp @lines; if (!@lines) { warn "e_getdir($where) ($head|$path) => empty ls-tree => ENOENT" if DEBUG; return -ENOENT(); } # 100644 blob 86cff4ec63304bcf51596839f3582ba293889bbf Makefile my @things; for my $line (@lines) { my ($stuff,$name) = split("\t", $line); my ($perm,$kind,$sha) = split(" ", $stuff); push @things, $name; } # inject special files if ( $tag_or_head eq 'tags' and $path eq '' ) { push @things, ('__TAG__MESSAGE__', '__TAG__SIGNATURE__'); } warn "e_getdir($where) (ls-tree) => @things, 0" if DEBUG; return @things, 0; } warn "e_getdir($where) => ENOENT" if DEBUG; return -ENOENT(); } sub e_open { my ($file) = filename_fixup(shift); warn "e_open($file)" if DEBUG; if ( exists $root->{$file} ) { warn "e_open($file) OK is a root file, 0" if DEBUG; return 0; } if ( $file !~ /^(tags|heads)\/(.*)$/ ) { warn "e_open($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head_or_tag = $1; my $head = $2; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if ( $head_or_tag eq 'tags' and length $head ) { my $data = ''; if ( $thing eq '__TAG__MESSAGE__' ) { $data = qx{git tag -ln $head}; if (!length $data) { warn "e_open($file): $head_or_tag $head path $thing ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } elsif ( $thing eq '__TAG__SIGNATURE__' ) { $data = qx{git tag -v $head 2>&1}; if (!length $data or $data =~ /^error: tag \`\Q$head\E\` not found\s*$/sm) { warn "e_open($file): $head_or_tag $head path $thing ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } my $size = length $data; if ( length $data ) { warn "e_open($file) OK is a file, 0" if DEBUG; return 0; } } my $data = get_git_info($head,$thing); if (!defined $data->[0]) { warn "e_open($file): ENOENT" if DEBUG; return -ENOENT(); } if ($data->[0] eq 'tree') { warn "e_open($file): EISDIR" if DEBUG; return -EISDIR(); } warn "e_open($file) OK is a file, 0" if DEBUG; return 0; } sub e_read { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; my ($buf, $off) = @_; warn "e_read($file, buf $buf, offset $off)" if DEBUG; if ( $file !~ /^(tags|heads)\/(.*)$/ ) { warn "e_open($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head_or_tag = $1; my $head = $2; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if ( $head_or_tag eq 'tags' and length $head ) { my $data = ''; if ( $thing eq '__TAG__MESSAGE__' ) { $data = qx{git tag -ln $head}; if (!length $data) { warn "e_read($file): $head_or_tag $head path $thing ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } elsif ( $thing eq '__TAG__SIGNATURE__' ) { $data = qx{git tag -v $head 2>&1}; if (!length $data or $data =~ /^error: tag \`\Q$head\E\` not found\s*$/sm) { warn "e_read($file): $head_or_tag $head path $thing ENOENT no such tag" if DEBUG; return -ENOENT() if !length $data; } } my $size = length $data; if ( length $data ) { return 0 if $off == length($data); return substr($data,$off,$buf); } } my $data = get_git_info($head,$thing); if (!defined $data->[0]) { warn "e_open($file): ENOENT" if DEBUG; return -ENOENT(); } if (!$data->[2]) { warn "e_read($file) not a file EISDIR" if DEBUG; return -EISDIR(); } my $bytes = $data->[3]; return 0 if $off == length($bytes); return substr($bytes,$off,$buf); } # Arguments: Pathname, scalar buffer, numeric offset, file handle. You can # use length($buffer) to find the buffersize. Returns length($buffer) if # successful (number of bytes written). Called in an attempt to write (or # overwrite) a portion of the file. Be prepared because $buffer could contain # random binary data with NULs and all sorts of other wonderful stuff. sub e_write { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; my ($buf, $off, $fh) = @_; my $buflen = length $buf; warn "e_write($file, buflen $buflen, offset $off)" if DEBUG; if ( $file !~ /^heads\/(.*)$/ ) { warn "e_write($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_write($thing) cannot write to heads/ ENOENT" if DEBUG; return -ENOENT(); } warn "e_write(head $head file $thing) buflen $buflen offset $off"; my $data = get_git_info($head,$thing); if (!defined $data->[0]) { warn "e_write(head $head thing $thing): ENOENT" if DEBUG; return -ENOENT(); } if ($data->[0] eq 'tree') { warn "e_write(head $head thing $thing): EISDIR" if DEBUG; return -EISDIR(); } my $bytes = $off ? ( exists $pending_writes->{$file} ? $pending_writes->{$file} : $data->[3] ) : ''; substr($bytes,$off,length($buf),$buf); $pending_writes->{$file} = $bytes; warn "e_write(head $head thing $thing): returning length of buffer written: ", length $buf; return length $buf; } sub e_fsync { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; my $flags = shift; warn "e_fsync($file, flags $flags) => 0" if DEBUG; return 0; } sub e_flush { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; warn "e_flush($file)" if DEBUG; if ( !exists $pending_writes->{$file} ) { warn "e_flush($file) not in pending writes => 0" if DEBUG; return 0; } if ( $file !~ /^heads\/(.*)$/ ) { warn "e_flush($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_flush($thing) cannot write to heads/ ENOENT" if DEBUG; return -ENOENT(); } warn "e_flush(head $head file $thing)"; my $orig_branch = (map { s/^\* //; $_ } grep { /^\*/ } split("\n",qx{git branch}))[0]; qx{git checkout $head}; open my $w_fh, '>', $thing or do { warn "e_write(head $head thing $thing): cannot open $thing on branch $head for writing: $!"; warn qx{git checkout $orig_branch}; return -ENOENT(); }; local $/=undef; print $w_fh $pending_writes->{$file}; close $w_fh or do { warn "e_write(head $head thing $thing): cannot close $thing on branch $head for writing: $!"; warn qx{git checkout $orig_branch}; return -ENOENT(); }; warn qx{git commit -m "updated $head file $thing" $thing}; warn qx{git checkout $orig_branch}; warn "e_flush($file) => 0" if DEBUG; return 0; } sub e_mknod { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; my ($modes, $device) = @_; warn "e_mknod($file, modes $modes, device $device)" if DEBUG; if ( $file !~ /^heads\/(.*)$/ ) { warn "e_mknod($file) not on heads ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_mknod(head $head thing $thing) cannot mknod heads/ EEXIST" if DEBUG; return -EEXIST(); } warn "e_mknod(head $head file $thing)"; my $orig_branch = (map { s/^\* //; $_ } grep { /^\*/ } split("\n",qx{git branch}))[0]; qx{git checkout $head}; qx{touch $thing}; qx{git add $thing}; qx{git commit $thing -m "Created empty file $thing"}; qx{git checkout $orig_branch}; warn "e_mknod(head $head thing $thing): created $thing" if DEBUG; return 0; } sub e_mkdir { my $newpath = filename_fixup(shift); warn "\n\n" if DEBUG; my $modes = shift; warn "e_mkdir($newpath, modes $modes)" if DEBUG; if ( $newpath !~ /^heads\/(.*)$/ ) { warn "e_mkdir($newpath) not on heads ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_mkdir(head $head thing $thing) cannot mkdir heads/ EEXIST" if DEBUG; return -EEXIST(); } warn "e_mkdir(head $head file $thing)"; my $orig_branch = (map { s/^\* //; $_ } grep { /^\*/ } split("\n",qx{git branch}))[0]; qx{git checkout $head}; qx{mkdir -vp $thing}; qx{touch $thing/.keep}; qx{git add $thing/.keep}; qx{git commit $thing/.keep -m "Created directory $thing"}; qx{git checkout $orig_branch}; warn "e_mkdir(head $head thing $thing): created $thing/.keep" if DEBUG; return 0; } sub e_unlink { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; warn "e_unlink($file)" if DEBUG; if ( $file !~ /^heads\/(.*)$/ ) { warn "e_unlink($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_unlink(head $head thing $thing) cannot unlink heads/ ENOENT" if DEBUG; return -ENOENT(); } warn "e_unlink(head $head file $thing)"; my $data = get_git_info($head,$thing); if (!defined $data->[0]) { warn "e_unlink(head $head thing $thing): ENOENT" if DEBUG; return -ENOENT(); } if (!$data->[2]) { warn "e_unlink(head $head thing $thing): EISDIR" if DEBUG; return -EISDIR(); } my $orig_branch = (map { s/^\* //; $_ } grep { /^\*/ } split("\n",qx{git branch}))[0]; qx{git checkout $head}; qx{git rm $thing}; qx{git commit $thing -m "Removed $thing"}; qx{git checkout $orig_branch}; warn "e_unlink(head $head thing $thing): removed" if DEBUG; return 0; } sub e_truncate { my $file = filename_fixup(shift); warn "\n\n" if DEBUG; my $offset = shift; warn "e_truncate($file) offset $offset" if DEBUG; if ( $file !~ /^heads\/(.*)$/ ) { warn "e_truncate($file) unimplemented ENOENT" if DEBUG; return -ENOENT(); } my $head = $1; my $thing = ''; if ( $head =~ /^([^\/]+)\/(.*)$/ ) { $head = $1; $thing = $2; } if (!length $thing) { warn "e_truncate(head $head thing $thing) cannot truncate to heads/ ENOENT" if DEBUG; return -ENOENT(); } warn "e_truncate(head $head file $thing) offset $offset"; my $data = get_git_info($head,$thing); if (!defined $data->[0]) { warn "e_truncate(head $head thing $thing): ENOENT" if DEBUG; return -ENOENT(); } if (!$data->[2]) { warn "e_truncate(head $head thing $thing): EISDIR" if DEBUG; return -EISDIR(); } ### No need to truncate anything actually #warn qx{git checkout $head}; #warn qx{truncate -s $offset $thing}; return 0; } sub e_statfs { warn "e_statfs(): 255,1,1,1,1,2" if DEBUG; return 255, 1, 1, 1, 1, 2; } # vim: set expandtab ts=4 sw=4 foldmethod=marker: