--- /dev/null
+package File::Tree::Snapshot;
+use Moo;
+use File::Path;
+use File::Basename;
+
+our $VERSION = '0.000001';
+$VERSION = eval $VERSION;
+
+has storage_path => (is => 'ro', required => 1);
+
+has allow_empty => (is => 'ro');
+
+sub file { join '/', (shift)->storage_path, @_}
+
+sub open {
+ my ($self, $mode, $file, %opt) = @_;
+ $file = $self->file($file)
+ unless $opt{is_absolute};
+ $self->_mkpath(dirname $file)
+ if $opt{mkpath};
+ open my $fh, $mode, $file
+ or die "Unable to write '$file': $!\n";
+ return $fh;
+}
+
+sub _mkpath {
+ my ($self, $dir) = @_;
+ mkpath($dir, { error => \(my $err) });
+ if (@$err) {
+ warn "Error while attempting to create '$dir': $_\n"
+ for map { (values %$_) } @$err;
+ }
+ return 1;
+}
+
+sub _exec {
+ my ($self, $cmd) = @_;
+ system($cmd) and die "Error during ($cmd)\n";
+ return 1;
+}
+
+sub _git_exec {
+ my ($self, @cmd) = @_;
+ my $path = $self->storage_path;
+ #local $ENV{GIT_DIR} = "$path/.git";
+ return $self->_exec(
+ sprintf q{cd %s && git %s},
+ $path,
+ join ' ', @cmd,
+ );
+}
+
+sub create {
+ my ($self) = @_;
+ my $path = $self->storage_path;
+ $self->_mkpath($path);
+ $self->_git_exec('init');
+ return 1;
+}
+
+sub _has_changes {
+ my ($self) = @_;
+ my $path = $self->storage_path;
+ my @changes = `cd $path && git diff --name-only --cached`;
+ return scalar @changes;
+}
+
+sub commit {
+ my ($self) = @_;
+ $self->_git_exec('add .');
+ unless ($self->_has_changes) {
+ print "No changes\n";
+ return 1;
+ }
+ $self->_git_exec('commit',
+ '--all',
+ ($self->allow_empty ? '--allow-empty' : ()),
+ '-m' => sprintf('"Updated on %s"', scalar localtime),
+ );
+ return 1;
+}
+
+sub reset {
+ my ($self) = @_;
+ $self->_git_exec('add .');
+ $self->_git_exec('checkout -f');
+ return 1;
+}
+
+sub exists {
+ my ($self) = @_;
+ return -e join '/', $self->storage_path, '.git';
+}
+
+sub find_files {
+ my ($self, $ext, @path) = @_;
+ my $root = $self->file(@path);
+ my @files = `find $root -name '*.$ext' -type f`;
+ chomp @files;
+ return @files;
+}
+
+1;