#!/usr/bin/perl use strict; use warnings; use English qw( -no_match_vars ); # Avoids regex performance penalty use Data::Dumper; use FindBin qw($RealBin $RealScript); use Getopt::Long; use Cwd qw(realpath getcwd); use File::Spec; use File::Copy; use File::Basename; use Pod::Usage; our $VERSION = 'v0.7.4'; my %opts; my $shellrc_filename; my $shellrc_load_filename; my $repo_dir; my $home; my $command_aliases = { 'mi' => 'mergeandinstall', 'umi' => 'updatemergeandinstall', 'un' => 'uninstall', 'im' => 'import', 'in' => 'install' }; my $commands = { 'install' => sub { DEBUG("Running in [$RealBin] and installing in [$home]"); # install files install( $home, $repo_dir ); }, 'updates' => sub { my $argv = shift; GetOptionsFromArray( $argv, \%opts, 'no-fetch' ); fetch_updates( \%opts ); }, 'mergeandinstall' => sub { my $argv = shift; GetOptionsFromArray( $argv, \%opts, 'merge', 'rebase' ); merge_and_install( \%opts ); }, 'updatemergeandinstall' => sub { my $argv = shift; GetOptionsFromArray( $argv, \%opts, 'merge', 'no-fetch' ); fetch_updates( \%opts ); merge_and_install( \%opts ); }, 'uninstall' => sub { my $argv = shift; # uninstall files uninstall($home, $repo_dir); }, 'import' => sub { my $argv = shift; GetOptionsFromArray( $argv, \%opts, 'message=s', 'no-commit|n' ); # import files import_files( _abs_repo_path( $home, $repo_dir ), $home, $argv ); }, 'help' => sub { my $argv = shift; my $command = shift @$argv; if ($command) { $command = $command_aliases->{$command} || $command; my %options = ( -verbose => 99, -exitstatus => 0, -sections => uc($command), ); # if run as part of test, add option to point # to real script source if ( $RealScript eq '04.misc.t' ) { $options{'-input'} = '../dfm'; } pod2usage(%options); } else { pod2usage(2); } }, }; run_dfm( $RealBin, @ARGV ) unless defined caller; sub run_dfm { my ( $realbin, @argv ) = @_; # set options to nothing so that running multiple times in tests # does not reuse options %opts = (); $shellrc_filename = undef; $shellrc_load_filename = undef; $repo_dir = undef; $home = undef; my $command; if ( scalar(@argv) == 0 || $argv[0] =~ /^-/ ) { # check to make sure there's not a dfm subcommand later in the arg list if ( grep { exists $commands->{$_} } @argv ) { ERROR("The command should be first."); exit(-2); } $command = 'help'; } else { $command = $argv[0]; } $command = $command_aliases->{$command} || $command; if ( exists $commands->{$command} ) { # parse global options first Getopt::Long::Configure('pass_through'); GetOptionsFromArray( \@argv, \%opts, 'verbose', 'quiet', 'dry-run', 'help', 'version' ); Getopt::Long::Configure('no_pass_through'); } $home = realpath( $ENV{HOME} ); if ( !$home ) { ERROR("unable to determine 'realpath' for $ENV{HOME}"); exit(-2); } if ( $ENV{'DFM_REPO'} ) { $repo_dir = $ENV{'DFM_REPO'}; $repo_dir =~ s/$home\///; } elsif ( -e "$realbin/t/02.updates_mergeandinstall.t" ) { # dfm is being invoked from its own repo, not a dotfiles repo; try and # figure out what repo in the users's homedir is the dotfiles repo # # TODO: alternate strategy: see if there are files in $home that are # already symlinked and use those as a guide foreach my $potential_dotfiles_repo (qw(.dotfiles dotfiles)) { if ( -d "$home/$potential_dotfiles_repo" && -d "$home/$potential_dotfiles_repo/.git" ) { $repo_dir = "$home/$potential_dotfiles_repo"; $repo_dir =~ s/$home\///; } } if ( !$repo_dir ) { ERROR("unable to discover dotfiles repo and dfm is running from its own repo"); exit(-2); } } else { $repo_dir = $realbin; $repo_dir =~ s/$home\///; $repo_dir =~ s/\/bin//; } DEBUG("Repo dir: $repo_dir"); # extract the shell name from env my $shell = basename( $ENV{SHELL} ); $shellrc_filename = '.' . $shell . 'rc'; DEBUG("Shell: $shell, Shell RC filename: $shellrc_filename"); # shellrc in MacOS is ~/.profile if ( lc($OSNAME) eq 'darwin' and $shell eq 'bash' ) { $shellrc_filename = '.profile'; } if ( exists $commands->{$command} ) { if ( $opts{'help'} ) { $commands->{'help'}->( [$command] ); } elsif ( $opts{'version'} ) { show_version(); } else { shift(@argv); # remove the command from the array $commands->{$command}->( \@argv ); } } else { # assume it's a git command and call accordingly _run_git(@argv); } } sub my_symlink { my $target = shift; my $link = shift; if ($^O eq "cygwin") { my $flags = ""; if (-d $target) { $flags = "/D" }; $target = `cygpath -w $target`; $link = `cygpath -w $link`; chomp $target; chomp $link; my $command = "cmd /c mklink $flags \"$link\" \"$target\""; system($command); } else { symlink($target,$link); } } sub get_changes { my $what = shift; return `git log --pretty='format:%h: %s' $what`; } sub get_current_branch { my $current_branch = `git symbolic-ref HEAD`; chomp $current_branch; # convert 'refs/heads/personal' to 'personal' $current_branch =~ s/^.+\///g; DEBUG("current branch: $current_branch"); return $current_branch; } sub check_remote_branch { my $branch = shift; my $branch_remote = `git config branch.$branch.remote`; chomp $branch_remote; DEBUG("remote for branch $branch: $branch_remote"); if ( $branch_remote eq "" ) { WARN("no remote found for branch $branch"); exit(-1); } } # a few log4perl-alikes sub ERROR { print "ERROR: @_\n"; } sub WARN { print "WARN: @_\n"; } sub INFO { print "INFO: @_\n" if !$opts{quiet}; } sub DEBUG { print "DEBUG: @_\n" if $opts{verbose}; } sub fetch_updates { my $opts = shift; chdir( _abs_repo_path( $home, $repo_dir ) ); if ( !$opts->{'no-fetch'} ) { DEBUG('fetching changes'); system("git fetch") if !$opts->{'dry-run'}; } my $current_branch = get_current_branch(); check_remote_branch($current_branch); print get_changes("$current_branch..$current_branch\@{u}"), "\n"; } sub merge_and_install { my $opts = shift; chdir( _abs_repo_path( $home, $repo_dir ) ); my $current_branch = get_current_branch(); check_remote_branch($current_branch); my $sync_command = $opts->{'rebase'} ? 'rebase' : 'merge'; if ( get_changes("$current_branch..$current_branch\@{u}") ) { # check for local commits if ( my $local_changes = get_changes("$current_branch\@{u}..$current_branch") ) { # if a decision wasn't made about how to deal with local commits if ( !$opts->{'merge'} && !$opts->{'rebase'} ) { WARN("local changes detected, run with either --merge or --rebase"); print $local_changes, "\n"; exit; } } INFO("using $sync_command to bring in changes"); system("git $sync_command $current_branch\@{u}") if !$opts->{'dry-run'}; INFO("re-installing dotfiles"); install( $home, $repo_dir ) if !$opts->{'dry-run'}; } else { INFO("no changes to merge"); } } sub install { my ( $home, $repo_dir ) = @_; INFO( "Installing dotfiles..." . ( $opts{'dry-run'} ? ' (dry run)' : '' ) ); DEBUG("Running in [$RealBin] and installing in [$home]"); install_files( _abs_repo_path( $home, $repo_dir ), $home ); $shellrc_load_filename = ''; # link in the shell loader if ( -e _abs_repo_path( $home, $repo_dir ) . "/.shellrc.load" ) { $shellrc_load_filename = '.shellrc.load'; } elsif ( -e _abs_repo_path( $home, $repo_dir ) . "/.bashrc.load" ) { $shellrc_load_filename = '.bashrc.load'; } if ($shellrc_load_filename) { configure_shell_loader(); } } sub uninstall { my ( $home, $repo_dir ) = @_; INFO( "Uninstalling dotfiles..." . ( $opts{'dry-run'} ? ' (dry run)' : '' ) ); DEBUG("Running in [$RealBin] and installing in [$home]"); # uninstall files uninstall_files( _abs_repo_path( $home, $repo_dir ), $home ); # link in the shell loader if ( -e _abs_repo_path( $home, $repo_dir ) . "/.shellrc.load" ) { $shellrc_load_filename = '.shellrc.load'; } elsif ( -e _abs_repo_path( $home, $repo_dir ) . "/.bashrc.load" ) { $shellrc_load_filename = '.bashrc.load'; } if ($shellrc_load_filename) { unconfigure_shell_loader(); } } # function to install files # possible options: # install_only: list of files to install, as opposed to all of them sub install_files { my ( $source_dir, $target_dir, $options ) = @_; my $install_only; if ( $options->{install_only} && scalar @{ $options->{install_only} } > 0 ) { $install_only = $options->{install_only}; } DEBUG("Installing from $source_dir into $target_dir"); my $symlink_base = _calculate_symlink_base( $source_dir, $target_dir ); my $backup_dir = $target_dir . '/.backup'; DEBUG("Backup dir: $backup_dir"); my $cwd_before_install = getcwd(); chdir($target_dir); my $dfm_install = _load_dfminstall("$source_dir/.dfminstall"); if ( !-e $backup_dir ) { DEBUG("Creating $backup_dir"); mkdir($backup_dir) if !$opts{'dry-run'}; } my $dirh; opendir $dirh, $source_dir; foreach my $direntry ( readdir($dirh) ) { if ($install_only) { next unless grep { $_ eq $direntry } @$install_only; } # skip vim swap files next if $direntry =~ /^\..*\.sw.$/; # skip emacs temporary and backup files next if $direntry =~ /^\.#.*$/; next if $direntry =~ /^.*~$/; # skip any other files next if $dfm_install->{skip_files}->{$direntry}; DEBUG(" Working on $direntry"); if ( !-l $direntry ) { if ( -e $direntry ) { INFO(" Backing up $direntry."); system("mv '$direntry' '$backup_dir/$direntry'") if !$opts{'dry-run'}; } INFO(" Symlinking $direntry ($symlink_base/$direntry)."); my_symlink( "$symlink_base/$direntry", "$direntry" ) if !$opts{'dry-run'}; } } cleanup_dangling_symlinks( $source_dir, $target_dir, $dfm_install->{skip_files} ); foreach my $recurse ( @{ $dfm_install->{recurse_files} } ) { if ( -d "$source_dir/$recurse" ) { DEBUG("recursing into $source_dir/$recurse"); if ( -l "$target_dir/$recurse" ) { DEBUG("removing symlink $target_dir/$recurse"); unlink("$target_dir/$recurse"); } if ( !-d "$target_dir/$recurse" ) { DEBUG("making directory $target_dir/$recurse"); mkdir("$target_dir/$recurse"); } my $recurse_options; if ($install_only) { $recurse_options = { install_only => [ map { s/^$recurse\///; $_ } grep {/^$recurse/} @$install_only ] }; } install_files( "$source_dir/$recurse", "$target_dir/$recurse", $recurse_options ); } else { WARN("couldn't recurse into $source_dir/$recurse, not a directory"); } } foreach my $execute ( @{ $dfm_install->{execute_files} } ) { my $cwd = getcwd(); if ( -x "$source_dir/$execute" ) { DEBUG("Executing $source_dir/$execute in $cwd"); system("'$source_dir/$execute'"); } elsif ( -o "$source_dir/$execute" ) { system("chmod +x '$source_dir/$execute'"); DEBUG("Executing $source_dir/$execute in $cwd"); system("'$source_dir/$execute'"); } } foreach my $chmod_file ( keys %{ $dfm_install->{chmod_files} } ) { my $new_perms = $dfm_install->{chmod_files}->{$chmod_file}; # TODO maybe skip if perms are already ok DEBUG("Setting permissions on $chmod_file to $new_perms"); chmod oct($new_perms), $chmod_file; } # restore previous working directory chdir($cwd_before_install); } sub configure_shell_loader { chdir($home); my $shellrc_contents = _read_shellrc_contents(); # check if the loader is in if ( $shellrc_contents !~ /$shellrc_load_filename/ ) { INFO("Appending loader to $shellrc_filename"); $shellrc_contents .= "\n. \$HOME/$shellrc_load_filename\n"; } # if the new loader filename (.shellrc.load) is used, but the old loader # filename (.bashrc.load) is in the shell rc, remove it if ( $shellrc_load_filename =~ m/shellrc/ && $shellrc_contents =~ /\.bashrc\.load/ ) { $shellrc_contents =~ s{\n. \$HOME/\.bashrc\.load\n}{}gs; } _write_shellrc_contents($shellrc_contents); } sub uninstall_files { my ( $source_dir, $target_dir ) = @_; DEBUG("Uninstalling from $target_dir"); my $backup_dir = $target_dir . '/.backup'; DEBUG("Backup dir: $backup_dir"); chdir($target_dir); my $dfm_install = _load_dfminstall("$source_dir/.dfminstall"); my $dirh; opendir $dirh, $target_dir; foreach my $direntry ( readdir($dirh) ) { DEBUG(" Working on $direntry"); if ( -l $direntry ) { my $link_target = readlink($direntry); DEBUG("$direntry points a $link_target"); my ( $volume, @elements ) = File::Spec->splitpath($link_target); my $element = pop @elements; my $target_base = realpath( File::Spec->rel2abs( File::Spec->catpath( '', @elements ) ) ); DEBUG( "target_base '", defined $target_base ? $target_base : '', "' $source_dir" ); if ( defined $target_base and $target_base eq $source_dir ) { INFO(" Removing $direntry ($link_target)."); unlink($direntry) if !$opts{'dry-run'}; } my $backup_path = File::Spec->catpath( '', '.backup', $element ); if ( -e $backup_path ) { INFO(" Restoring $direntry from backup."); rename( $backup_path, $element ) if !$opts{'dry-run'}; } } } foreach my $execute ( @{ $dfm_install->{execute_uninstall_files} } ) { my $cwd = getcwd(); if ( -x "$source_dir/$execute" ) { DEBUG("Executing $source_dir/$execute in $cwd"); system("'$source_dir/$execute'"); } elsif ( -o "$source_dir/$execute" ) { system("chmod +x '$source_dir/$execute'"); DEBUG("Executing $source_dir/$execute in $cwd"); system("'$source_dir/$execute'"); } } foreach my $recurse ( @{ $dfm_install->{recurse_files} } ) { if ( -d "$target_dir/$recurse" ) { DEBUG("recursing into $target_dir/$recurse"); uninstall_files( "$source_dir/$recurse", "$target_dir/$recurse" ); } else { WARN("couldn't recurse into $target_dir/$recurse, not a directory"); } } } sub relative_to_target { my ( $tryfile, $target_dir ) = @_; if ( -l $tryfile ) { my ( $volume, $dirs, $lfile ) = File::Spec->splitpath($tryfile); return File::Spec->abs2rel( File::Spec->catfile( realpath($dirs), $lfile ), $target_dir ); } else { return File::Spec->abs2rel( realpath($tryfile), $target_dir ); } } sub import_files { my ( $source_dir, $target_dir, $files ) = @_; my $symlink_base = _calculate_symlink_base( $source_dir, $target_dir ); foreach my $file (@$files) { if ( $file =~ m{^/} ) { $file = relative_to_target( $file, $target_dir ); } else { my $tryfile = File::Spec->rel2abs($file); if ( -e $tryfile ) { #print "FOUND in cwd\n"; $file = relative_to_target( $tryfile, $target_dir ); } else { my $tryfile = File::Spec->rel2abs( $file, $target_dir ); if ( -e $tryfile ) { #print "FOUND in home\n"; $file = relative_to_target( $tryfile, $target_dir ); } } } if ( $file =~ /^\.\./ ) { ERROR("file $file is not in your home directory"); return; } # if dfm import $HOME is called if ( $file eq '.' ) { ERROR("unable to import your home directory itself"); return; } if ( !-e "$target_dir/$file" ) { ERROR("file $file not found, unable to import"); return; } DEBUG("file path, relative to homedir: $file"); my ( $in_a_subdir, $subdir ) = _file_in_tracked_or_untracked( $source_dir, $source_dir, $file ); if ( $in_a_subdir eq 'untracked' ) { ERROR( "file $file is in a subdirectory that is not tracked, consider using 'dfm import $subdir'." ); return; } elsif ( $in_a_subdir eq 'tracked' ) { ERROR( "file $file is in a subdirectory that is already tracked, consider using 'dfm add $subdir'." ); return; } elsif ( $in_a_subdir eq 'skip' ) { ERROR("file $file is skipped."); return; } # detect file that's already tracked, either by being a symlink that # points into the repo or in the repo itself if (( -l "$target_dir/$file" && ( readlink("$target_dir/$file") =~ /(\.\.\/)*$symlink_base/ ) ) || $file =~ /^$symlink_base/ ) { ERROR("file $file is already tracked."); return; } } my $message = $opts{message} || "importing " . join( ', ', @$files ); foreach my $file (@$files) { INFO( "Importing $file from $target_dir into $source_dir" . ( $opts{'dry-run'} ? ' (dry run)' : '' ) ); DEBUG("moving $file into $source_dir"); if ( !$opts{'dry-run'} ) { move( "$target_dir/$file", "$source_dir/$file" ); } if ( !$opts{'dry-run'} ) { _run_git( 'add', $file ); } } install_files( _abs_repo_path( $home, $repo_dir ), $home, { install_only => [@$files] } ); INFO( "Committing with message '$message'" . ( $opts{'dry-run'} ? ' (dry run)' : '' ) ); if ( !$opts{'dry-run'} ) { if ( !$opts{'no-commit'} ) { _run_git( 'commit', @$files, '-m', $message ); } } } sub cleanup_dangling_symlinks { my ( $source_dir, $target_dir, $skip_files ) = @_; $skip_files ||= {}; DEBUG(" Cleaning up dangling symlinks in $target_dir"); my $dirh; opendir $dirh, $target_dir; foreach my $direntry ( readdir($dirh) ) { DEBUG(" Working on $direntry"); # if symlink is dangling or is now skipped if ( -l $direntry && ( !-e $direntry || $skip_files->{$direntry} ) ) { my $link_target = readlink($direntry); DEBUG("$direntry points at $link_target"); my ( $volume, @elements ) = File::Spec->splitpath($link_target); my $element = pop @elements; my $target_base = realpath( File::Spec->rel2abs( File::Spec->catpath( '', @elements ) ) ); DEBUG( "target_base '", defined $target_base ? $target_base : '', "' $source_dir" ); if ( defined $target_base and $target_base eq $source_dir ) { INFO(" Cleaning up dangling symlink $direntry ($link_target)."); unlink($direntry) if !$opts{'dry-run'}; } } } } sub unconfigure_shell_loader { chdir($home); my $shellrc_contents = _read_shellrc_contents(); # remove shell loader if found $shellrc_contents =~ s{\n. \$HOME/$shellrc_load_filename\n}{}gs; _write_shellrc_contents($shellrc_contents); } sub _write_shellrc_contents { my $shellrc_contents = shift; if ( !$opts{'dry-run'} ) { open( my $shellrc_out, '>', $shellrc_filename ); print $shellrc_out $shellrc_contents; close $shellrc_out; } } sub _read_shellrc_contents { my $shellrc_contents; { local $INPUT_RECORD_SEPARATOR = undef; if ( open( my $shellrc_in, '<', $shellrc_filename ) ) { $shellrc_contents = <$shellrc_in>; close $shellrc_in; } else { $shellrc_contents = ''; } } return $shellrc_contents; } sub _run_git { my @args = @_; my $cwd_before_git = getcwd(); DEBUG( 'running git ' . join( ' ', @args ) . " in $home/$repo_dir" ); chdir( _abs_repo_path( $home, $repo_dir ) ); system( 'git', @args ); chdir($cwd_before_git); } sub _abs_repo_path { my ( $home, $repo ) = @_; if ( File::Spec->file_name_is_absolute($repo) ) { return $repo; } else { return $home . '/' . $repo; } } # when symlinking from source_dir into target_dir, figure out if there's a # relative path between the two sub _calculate_symlink_base { my ( $source_dir, $target_dir ) = @_; my $symlink_base; # if the paths have no first element in common if ( ( File::Spec->splitdir($source_dir) )[1] ne ( File::Spec->splitdir($target_dir) )[1] ) { $symlink_base = $source_dir; # use absolute path } else { # otherwise, calculate the relative path between the two directories $symlink_base = File::Spec->abs2rel( $source_dir, $target_dir ); } return $symlink_base; } sub _file_in_tracked_or_untracked { my ( $orig_source_dir, $source_dir, $file ) = @_; # strip the repo dir off the front, in case the file is already tracked $file =~ s/$repo_dir\///; my $cwd_before_inspection = getcwd(); chdir($source_dir); my $dfm_install = _load_dfminstall("$source_dir/.dfminstall"); # skip vim swap files return ('skip') if $file =~ /.*\.sw.$/; # skip any other files return ('skip') if $dfm_install->{skip_files}->{$file}; my @dirs = File::Spec->splitdir($file); if ( scalar(@dirs) > 1 ) { my $recurse_dir = shift(@dirs); if ( grep { $recurse_dir eq $_ } @{ $dfm_install->{recurse_files} } ) { chdir($cwd_before_inspection); return _file_in_tracked_or_untracked( $orig_source_dir, File::Spec->catfile( $source_dir, $recurse_dir ), File::Spec->catfile(@dirs) ); } else { my $relative_path = File::Spec->abs2rel( $source_dir, $orig_source_dir ); my $dir_type = -e $recurse_dir ? 'tracked' : 'untracked'; chdir($cwd_before_inspection); return ( $dir_type, ( $relative_path eq '.' ) ? $recurse_dir : File::Spec->catfile( $relative_path, $recurse_dir ) ); } } chdir($cwd_before_inspection); return ('install'); } sub _load_dfminstall { my ($dfminstall_path) = @_; my $dfminstall_info = { skip_files => { '.' => 1, '..' => 1, '.dfminstall' => 1, '.gitignore' => 1, '.git' => 1, }, recurse_files => [], execute_files => [], execute_uninstall_files => [], chmod_files => {}, }; if ( -e $dfminstall_path ) { open( my $skip_fh, '<', $dfminstall_path ); foreach my $line (<$skip_fh>) { chomp($line); if ( length($line) ) { my ( $filename, @options ) = split( q{ }, $line ); DEBUG(".dfminstall file $filename has @options"); if ( !defined $options[0] ) { WARN( "using implied recursion in .dfminstall is deprecated, change '$filename' to '$filename recurse' in $dfminstall_path." ); push( @{ $dfminstall_info->{recurse_files} }, $filename ); $dfminstall_info->{skip_files}->{$filename} = 1; } elsif ( $options[0] eq 'skip' ) { $dfminstall_info->{skip_files}->{$filename} = 1; } elsif ( $options[0] eq 'recurse' ) { push( @{ $dfminstall_info->{recurse_files} }, $filename ); $dfminstall_info->{skip_files}->{$filename} = 1; } elsif ( $options[0] eq 'exec' ) { push( @{ $dfminstall_info->{execute_files} }, $filename ); } elsif ( $options[0] eq 'exec-uninstall' ) { push( @{ $dfminstall_info->{execute_uninstall_files} }, $filename ); } elsif ( $options[0] eq 'chmod' ) { if ( !$options[1] ) { ERROR("chmod option requires a mode (e.g. 0600) in $dfminstall_path"); exit 1; } if ( $options[1] !~ /^[0-7]{4}$/ ) { ERROR( "bad mode '$options[1]' (should be 4 digit octal, like 0600) in $dfminstall_path" ); exit 1; } $dfminstall_info->{chmod_files}->{$filename} = $options[1]; } } } close($skip_fh); $dfminstall_info->{skip_files}->{skip} = 1; DEBUG("Skipped file: $_") for keys %{ $dfminstall_info->{skip_files} }; } return $dfminstall_info; } sub show_version { print "dfm version $VERSION\n"; } # work-alike for function from perl 5.8.9 and later # added for compatibility with CentOS 5, which is stuck on 5.8.8 sub GetOptionsFromArray { my ( $argv, $opts, @options ) = @_; local @ARGV = @$argv; GetOptions( $opts, @options ); # update the passed argv array @$argv = @ARGV; } 1; __END__ =head1 NAME dfm - A script to manage a dotfiles repository =head1 SYNOPSIS usage: dfm [--version] [--dry-run] [--verbose] [--quiet] [] The commands are: install Install dotfiles import Add a new dotfile to the repo uninstall Uninstall dotfiles updates Fetch updates but don't merge them in mi Merge in updates and install dotfiles again umi Fetch updates, merge in and install See 'dfm help ' for more information on a specific command. Any git command can be run on the dotfiles repository by using the following syntax: dfm [git subcommand] [git options] =head1 DESCRIPTION Manages installing files from and operating on a repository that contains dotfiles. =head1 COMMON OPTIONS All the subcommands implemented by dfm have the following options: --verbose Show extra information about what dfm is doing --quiet Show as little info as possible. --dry-run Don't do anything. --version Print version information. =head1 HELP All Options: dfm help dfm --help Examples: dfm install --help dfm help install Description: This shows the help for a particular subcommand. =head1 INSTALL All Options: dfm install [--verbose|--quiet] [--dry-run] Examples: dfm install dfm install --dry-run Description: This installs everything in the repository into the current user's home directory by making symlinks. To skip any files, add their names to a file named '.dfminstall'. For instance, to skip 'README.md', put this in .dfminstall: README.md skip To recurse into a directory and install files inside rather than symlinking the directory itself, just add its name to .dfminstall. For instance, to make 'dfm install' symlink files inside of ~/.ssh instead of making ~/.ssh a symlink, put this in .dfminstall: .ssh =head1 UNINSTALL All Options: dfm uninstall [--verbose|--quiet] [--dry-run] - or - dfm un [--verbose|--quiet] [--dry-run] Examples: dfm uninstall dfm uninstall --dry-run Description: This removes all traces of dfm and the dotfiles. It basically is the reverse of 'dfm install'. =head1 IMPORT All Options: dfm import [--verbose|--quiet] [--dry-run] [--no-commit] [--message ] file1 [file2 ..] - or - dfm im [--verbose|--quiet] [--dry-run] [--no-commit] [--message ] file1 [file2 ..] Examples dfm import ~/.vimrc dfm import .tmux.conf --message 'adding my tmux config' Description: This command moves each file specified into the dotfiles repository and symlinks it into $HOME. Then a commit is made. Use '--message' to specify a different commit message. Use '--no-commit' to add the files, but not commit. =head1 UPDATES All Options: dfm updates [--verbose|--quiet] [--dry-run] [--no-fetch] Examples: dfm updates dfm updates --no-fetch Description: This fetches any changes from the upstream remote and then shows a shortlog of what updates would come in if merged into the current branch. Use '--no-fetch' to skip the fetch and just show what's new. =head1 MERGEANDINSTALL All Options: dfm mergeandinstall [--verbose|--quiet] [--dry-run] [--merge|--rebase] - or - dfm mi [--verbose|--quiet] [--dry-run] [--merge|--rebase] Examples: dfm mergeandinstall dfm mi dfm mergeandinstall --rebase Description: This merges or rebases the upstream changes in and re-installs dotfiiles. =head1 UPDATEMERGEANDINSTALL All Options: dfm updatemergeandinstall [--verbose|--quiet] [--dry-run] [--merge|--rebase] [--no-fetch] - or - dfm umi [--verbose|--quiet] [--dry-run] [--merge|--rebase] [--no-fetch] Examples: dfm updatemergeandinstall dfm umi dfm updatemergeandinstall --no-fetch Description: This combines 'updates' and 'mergeandinstall'. =head1 dfm [git subcommand] [git options] This runs any git command as if it was inside the dotfiles repository. For instance, this makes it easy to commit changes that are made by running 'dfm commit'. =head1 AUTHOR Nate Jones =head1 COPYRIGHT Copyright (c) 2010 L as listed above. =head1 LICENSE This program is free software distributed under the Artistic License 2.0. =cut