Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Branch: master
Fetching contributors…

Cannot retrieve contributors at this time

executable file 553 lines (529 sloc) 18.641 kB
#!/usr/bin/env perl
# Copyright (c) 2010, Marco Fontani <mfontani@cpan.org>.
# 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:
Jump to Line
Something went wrong with that request. Please try again.