Find file
Fetching contributors…
Cannot retrieve contributors at this time
executable file 449 lines (274 sloc) 10.8 KB
#! /usr/bin/perl
## no critic qw( ValuesAndExpressions::ProhibitAccessOfPrivateData ValuesAndExpressions::ProhibitLeadingZeros )
## no critic qw( Lax::ProhibitLeadingZeros::ExceptChmod ErrorHandling::RequireCarping Bangs::ProhibitNumberedNames )
## no critic qw( ValuesAndExpressions::RequireQuotedHeredocTerminator ValuesAndExpressions::RestrictLongStrings )
## no critic qw( ValuesAndExpressions::ProhibitInterpolationOfLiterals ValuesAndExpressions::ProhibitMagicNumbers )
use 5.006;
use strict;
use warnings;
use Fcntl qw( :mode S_ISDIR S_ISLNK);
use File::Spec ();
use File::stat; # overrides perl's stat and lstat
use File::Temp 'tempfile';
use File::Touch;
use File::Basename;
use Cwd 'abs_path';
use Getopt::Long;
use IPC::Run3::Simple;
use Linux::Ext2::Attributes 'set_attrs';
use POSIX;
use Term::ReadKey;
use Unix::Mknod;
use YAML::Syck;
our $NOTE_NS = 'gitperms'; # What namespace are we using for notes?
$ENV{GIT_DIR} = abs_path(dirname($0).'/../');
$ENV{GIT_WORK_TREE} = qx/git config --get core.worktree/;
chomp $ENV{GIT_WORK_TREE};
my $topdir = get_toplevel();
chdir $topdir
or die "Unable to change to git top level directory: $!\n";
my $se_enforcing = qx/cat \/selinux\/enforce/;
GetOptions(
'create-on-missing' => \my $create_on_missing,
force => \my $force,
help => \my $help,
quiet => \my $quiet,
save => \my $save,
set => \my $set,
) or die "Problem parsing options, exiting.\n";
my $help_txt = <<EOH;
usage: $0 [options ...]
--save Save a note with current permissions information.
--force If there is already a note, overwrite it instead of dying.
--set Set permissions according to the note.
--create-on-missing If a note does not exist, create it without prompting the user.
--quiet Don't be so noisy.
--help This screen.
EOH
die $help_txt
if defined $help;
die "either --save or --set is required\n"
if ! defined $save && ! defined $set;
die "--save and --set are mutually exclusive\n"
if defined $save && defined $set;
my %metadata;
# Get all files git knows about
run3( { cmd => [qw( git ls-tree -r -t HEAD )], stdout => \my @files } );
# Get unstaged files so we can ignore them
run3( { cmd => [qw( git status --porcelain --untracked-files=no )], stdout => \my @ignore } );
my %ignore; @ignore{ grep { s/^\s\S\s// } @ignore } = undef;
if ( defined $save ) {
get_metadata_from_system();
set_note();
print "Done saving ...\n";
} elsif ( defined $set ) {
if ( get_note() ) {
# note existed
set_metadata_in_system();
print "Done setting ...\n";
}
} else {
die "Unknown and unhandled error! How'd you get here?\n";
}
exit 0;
################################################################################################
sub get_toplevel {
my ( $toplevel, $err, $syserr ) = run3( [qw( git rev-parse --show-toplevel )] );
die "System error: $syserr\n"
if $syserr;
die "Unable to get top level directory: $err\n"
if $err;
$toplevel = File::Spec->canonpath( $toplevel );
return $toplevel;
}
sub get_metadata_from_system {
for my $f ( @files ) {
my ( $sha1, $file ) = $f =~ /^\d+\s\w+\s([[:xdigit:]]+)\s(.*)$/;
next if exists $ignore{ $file };
my $md = $metadata{ $sha1 } ||= {};
if ( my $st = lstat $file ) {
my $inode = $md->{ $st->ino } ||= {};
## no critic qw( ValuesAndExpressions::ProhibitMagicNumbers )
my $ftype = ( $st->mode & 0170000 ) >> 12;
$inode->{ type }
= ( $st->rdev || $ftype == 1 || $ftype == 12 ) ? 'CF'
: ( ! S_ISDIR( $st->mode ) && $st->nlink > 1 && $inode != {} ) ? 'HL'
: S_ISDIR( $st->mode ) ? 'DIR'
: S_ISLNK( $st->mode ) ? 'SL'
: 'RF';
$inode->{ $_ } = $st->$_ for qw( dev ino mode nlink uid gid rdev size atime mtime ctime blksize blocks );
$inode->{ chmod } = sprintf '%04o', $inode->{ mode } & 07777;
push @{ $inode->{ name } }, $file;
if ( my $attributes = Linux::Ext2::Attributes->load( $file ) ) {
$attributes = $attributes->strip;
$inode->{ extnd_attr } = $attributes->flags;
} else {
! defined $quiet && warn "Problem getting extended attributes on $file ($!).\n";
}
} else {
! defined $quiet && warn "Problem getting stat on $file ($!), skipping.\n";
next;
}
} ## end for my $f ( @files)
return;
} ## end sub get_metadata_from_system
sub set_metadata_in_system { ## no critic qw( Subroutines::ProhibitExcessComplexity )
my ( %chown_file, %chmod_file, %touch_file, %extnd_attr, @restor_ctx );
for my $sha1 ( keys %metadata ) {
my $md = $metadata{ $sha1 };
for my $inode ( keys %$md ) { ## no critic qw( References::ProhibitDoubleSigils )
my $attr = $md->{ $inode };
# Don't do anything for symbolic links
next if $attr->{ type } eq 'SL';
! defined $quiet && warn "$sha1, $inode has multiple names, only the first one will be handled"
if ( @{ $attr->{ name } } > 1 ) && ( $attr->{ type } ne 'HL' );
my $name = shift @{ $attr->{ name } };
next if exists $ignore{ $name };
do { warn "$name does not exist, skipping\n"; next }
if ( ! -e $name ) && ( $attr->{ type } ne 'HL' );
if ( $attr->{ type } eq 'HL' ) { ## no critic qw( ControlStructures::ProhibitCascadingIfElse )
# Remove all but the first file
# Hardlink removed files to first file.
if ( @{ $attr->{ name } } == 0 ) {
warn "No linked files (how did you get here?!), skipping\n";
next;
}
my $files = join ', ', @{ $attr->{ name } };
! defined $quiet && warn "unlinking $files\n";
my $removed = unlink @{ $attr->{ name } };
warn "Unable to remove all files.\n"
if $removed != @{ $attr->{ name } };
! defined $quiet && warn "Hardlinking $files to $name\n";
for my $link ( @{ $attr->{ name } } ) {
my $linked = link $name, $link;
warn "Unable to link $name to $link: $!\n"
unless $linked;
}
} elsif ( $attr->{ type } eq 'CF' ) {
## no critic qw( ValuesAndExpressions::ProhibitMagicNumbers )
( Unix::Mknod::mknod( $name, $attr->{ mode }, $attr->{ rdev } ) == -1 )
&& warn "Problem making special file $name: $!\n";
} elsif ( $attr->{ type } eq 'DIR' ) {
if ( ! -d $name ) {
warn "$name exists and is not a directory, skipping\n";
next;
}
} elsif ( $attr->{ type } ne 'RF' ) {
! defined $quiet && warn sprintf "Unknown file type or type not set for %s\n", join ', ', @{ $attr->{ name } };
next;
}
my $chown_attr = sprintf '%s_%s', $attr->{ uid }, $attr->{ gid };
push @{ $chown_file{ $chown_attr } }, $name;
#push @{ $chmod_file{ $attr->{ mode } } }, $name;
push @{ $chmod_file{ $attr->{ chmod } } }, $name;
my $time_attr = sprintf '%s_%s', $attr->{ atime }, $attr->{ mtime };
push @{ $touch_file{ $time_attr } }, $name;
$extnd_attr{ $name } = $attr->{ extnd_attr };
push @restor_ctx, $name;
} ## end for my $inode ( keys...)
} ## end for my $sha1 ( keys...)
chown_file( \%chown_file );
chmod_file( \%chmod_file );
touch_file( \%touch_file );
extnd_attr( \%extnd_attr );
restor_ctx( \@restor_ctx );
return;
} ## end sub set_metadata_in_system
sub set_note {
my ( $fh, $filename ) = tempfile( unlink => 1 );
print $fh Dump( \%metadata );
my @cmd = ( qw( git notes --ref ), $NOTE_NS, qw( add HEAD -F ), $filename );
push @cmd, '--force' if defined $force;
my ( $out, $err ) = run3( \@cmd );
! defined $quiet && warn "$out\n$err" if $err;
return;
}
sub get_note {
my @cmd = ( qw( git notes --ref ), $NOTE_NS, qw( show HEAD ) );
my ( $out, $err ) = run3( \@cmd );
if ($err) {
if ( $err =~ /No note found for object/ ) {
if ( defined $create_on_missing ) {
get_metadata_from_system();
set_note();
print "Missing note created.\n";
return;
} else {
print "
!!! This commit does not have a note associated with it. !!!
Your git action has successfully completed, but no permissions have
been set.
To create a note for this commit, run the following command from the
top level of your git repository:
.git/hooks/handle_metadata --save
If you wish this to happen automatically, edit the
.git/hooks/post-commit file and add the '--create-on-missing' option.
";
return;
}
} else {
warn "$err\n\nNot setting metadata ...\n";
return;
}
} else {
%metadata = %{ Load( "$out\n" ) };
return 1;
}
} ## end sub get_note
sub chown_file {
my ( $chown_file ) = @_;
for my $attr ( keys %$chown_file ) { ## no critic qw( References::ProhibitDoubleSigils )
my ( $uid, $gid ) = split /_/, $attr, 2;
my @files = @{ $chown_file->{ $attr } };
! defined $quiet && warn "chown $uid, $gid, @files\n";
my $chowned = chown $uid, $gid, @files;
warn "Unable to change owner and/or group for one or more files: $!\n"
unless $chowned == @files;
}
return;
} ## end sub chown_file
sub chmod_file {
my ( $chmod_file ) = @_;
for my $chmod ( keys %$chmod_file ) { ## no critic qw( References::ProhibitDoubleSigils )
my @files = @{ $chmod_file->{ $chmod } };
! defined $quiet && warn sprintf "chmod %04o, @files\n", oct $chmod;
my $chmodded = chmod oct $chmod, @files;
warn "Unable to change mode for one or more files: $!\n"
unless $chmodded == @files;
}
return;
} ## end sub chmod_file
sub touch_file {
my ( $touch_file ) = @_;
for my $time ( keys %$touch_file ) { ## no critic qw( References::ProhibitDoubleSigils )
my ( $atime, $mtime ) = split /_/, $time, 2;
my @files = @{ $touch_file->{ $time } };
! defined $quiet && warn "touch -a $atime -m $mtime @files\n";
my $t = File::Touch->new( atime => $atime, mtime => $mtime, no_create => 1 );
my $touched = $t->touch( @files );
warn "Unable to touch one or more files: $!\n"
unless $touched == @files;
}
return;
} ## end sub touch_file
sub extnd_attr {
my ( $extnd_attr ) = @_;
while ( my ( $f, $x ) = each %$extnd_attr ) { ## no critic qw( References::ProhibitDoubleSigils )
! defined $quiet && warn "chattr $x $f\n";
set_attrs( $f, $x )
or warn "Unable to set extended attributes on $f\n";
}
return;
}
sub restor_ctx {
my ( $restor_ctx ) = @_;
return if not $se_enforcing;
my $top = $topdir =~ m/\/$/ ? $topdir : $topdir."/";
open RESTORCON, "|restorecon -f- -v" || warn "unable to execute restorecon";
foreach (@$restor_ctx) {
! defined $quiet && warn "restorecon ". $top . $_ . "\n";
print RESTORCON "". $top . $_ . "\n";
}
close RESTORCON;
return;
}