Skip to content

Instantly share code, notes, and snippets.

@briandfoy
Last active January 21, 2020 05:46
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save briandfoy/65cd1648bcdca36436e9b77f7f64603d to your computer and use it in GitHub Desktop.
Save briandfoy/65cd1648bcdca36436e9b77f7f64603d to your computer and use it in GitHub Desktop.
Make a git repo out of the BBEdit Backups file
#!perl
use v5.26;
use strict;
use warnings;
use feature qw(signatures);
no warnings qw(experimental::signatures);
=encoding utf8
=head1 NAME
bbedit_backups_git.pl
=head1 SYNOPSIS
$ bbedit_backups_git.pl
=head1 DESCRIPTION
BBEdit has a feature that saves the state of your file (see
Preferences - Text File). The backup directory can either be in
Dropbox or iCloud, but it must be called F<BBEdit Backups>. Turning on
"Historical Backups" will automatically use this folder. Each calendar
day gets its own folder. If you have saved the file, the filename has
the time added to it, like F<Unix Script Output (2019-12-04
03-24-42-454).log>. If the file hasn't ever been saved, there's no
time but hash marks surround the name, like F<#some_file#.txt>
This program crawls through F<BBEdit Backups>, looking in F<iCloud
Drive> first then F<Dropbox>, and imports all of the files into a git
repo named F<bbedit_backups_git>. Any previous F<bbedit_backups_git>
is deleted. The commit times are the file modification time and the
commit message is just the file name.
Realize that BBEdit backs up based on the basename of the file, so too
different files with the same name are treated as the same file. For
instance, F<project_a/.travis.yml> and F<project_b/.travis.yml> both
backup F<.travis.yml> and there's not a good way to tell them apart. I
don't even try to distinguish these. Additionally, this specifically
ignores any F<.gitignore> file because they belong to some other
project.
=head1 LICENSE
You can use and redistribute this under the terms of the Artistic
License 2.0. L<https://opensource.org/licenses/Artistic-2.0>
=head1 COPYRIGHT
Copyright 2020, brian d foy, C<< <brian.d.foy@gmail.com> >>
=cut
use Data::Dumper;
use File::Basename qw(basename);
use File::Copy qw(copy);
use File::Path qw(make_path remove_tree);
use File::Spec::Functions qw(catfile);
use IO::Interactive qw(interactive);
use Time::Local qw(timelocal_modern);
####################################################################
my $bbedit_backup_dir = get_backup_dir();
say {interactive} "Processing BBEdit Backups in <$bbedit_backup_dir>";
gitify(
get_files(
get_daily_dirs( $bbedit_backup_dir )
)
);
####################################################################
sub get_backup_dir () {
my( $dir ) =
grep { -d }
map { catfile( $ENV{HOME}, $_, 'BBEdit Backups' ) }
( 'Library/Mobile Documents/com~apple~CloudDocs', 'Dropbox' );
return $dir;
}
sub get_daily_dirs ( $dir ) {
opendir my $dh, $dir or die "Could not open <$dir>: $!\n";
my @dirs =
sort
grep { -d }
map { catfile( $dir, $_ ) }
grep { /\A\d{4}-\d{2}-\d{2}\z/ }
readdir($dh);
return \@dirs;
}
sub get_files ( $dirs ) {
# Make an array of hashes like this:
#
# {
# 'real_name' => 'Unix Script Output.log',
# 'date' => 1575446611,
# 'backup_name' => '2019-12-04/Unix Script Output (2019-12-04 03-24-42-454).log'
# },
state $Excludes = map { $_, 1 } qw(.gitignore);
say {interactive} "Found " . $dirs->@* . " directories";
my @files;
foreach my $dir ( $dirs->@* ) {
opendir my($dh), $dir;
push @files,
map {
# most files have a name with the date added:
# Unix Script Output (2019-12-04 03-24-42-454).log
#
# However, if you never saved the file, it doesn't have
# the date and adds hashes around the name:
# #new-host#.pl
my $real_name = s/
\x{20}
\(
(?<year>\d{4}) - (?<month>\d{2}) - (?<day>\d{2})
\x{20}
(?<hour>\d{2}) - (?<minute>\d{2}) - (?<second>\d{2}) - \d{3}
\)
//xr;
my $epoch = do {
if( defined $+{year} ) {
# watch out for Y2K with Time::Local! Use _modern
# so the year is the year I specify
my @keys = qw(second minute hour day);
timelocal_modern( @+{@keys}, $+{month}-1, $+{year} );
}
else { (stat $_)[9] }
};
{
backup_name => $_,
real_name => basename($real_name),
date => $epoch,
}
}
sort
grep { -f }
map { catfile( $dir, $_ ) }
readdir $dh;
}
return \@files;
}
sub gitify ( $files ) {
my $git_dir = 'bbedit_backups_git';
remove_tree $git_dir;
make_path $git_dir;
chdir $git_dir;
system 'git', 'init';
say {interactive} "Found " . $files->@* . " files";
foreach my $file ( $files->@* ) {
next if $file->{real_name} eq '.gitignore';
copy( $file->{backup_name}, $file->{real_name} );
system 'git', 'add', $file->{real_name};
$ENV{GIT_AUTHOR_DATE} = $ENV{GIT_COMMITTER_DATE} = $file->{date};
my $message = $file->{real_name};
say {interactive} "Committing <$file->{real_name}> for <" . localtime($file->{date}) . ">";
system 'git', 'commit', '--quiet', '-m', $message, $file->{real_name};
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment