Make a git repo out of the BBEdit Backups file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!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