briefcase 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ module Briefcase
2
+ module Commands
3
+
4
+ # Generate looks through through the dotfiles directory for any redacted
5
+ # dotfiles. It attempts to generate a normal version of each file it finds,
6
+ # using the values stored in the .briefcase_secrets file.
7
+ class Generate < Base
8
+
9
+ def execute
10
+ intro "Generating redacted dotfiles in #{dotfiles_path}"
11
+
12
+ Dir.glob(File.join(dotfiles_path, "*.#{REDACTED_EXTENSION}")) do |path|
13
+ generate_file_for_path(path)
14
+ end
15
+
16
+ write_secrets
17
+ end
18
+
19
+ private
20
+
21
+ # Generates a standard dotfile from a redacted dotfile
22
+ #
23
+ # This method will attempt to find secret values in the secrets file. If
24
+ # any aren't found, it will print a message about the offending key and
25
+ # add that key to the secrets file without a value (to make it easier for
26
+ # users to fill in the missing value).
27
+ #
28
+ # path - the path to the redacted dotfile
29
+ def generate_file_for_path(path)
30
+ static_path = path.gsub(/.#{REDACTED_EXTENSION}$/, '')
31
+ basename = File.basename(static_path)
32
+ dotfile_path = generate_dotfile_path(basename)
33
+
34
+ if !File.exist?(dotfile_path) || overwrite_file?(dotfile_path)
35
+ info "Generating %s", dotfile_path
36
+ content = File.read(path)
37
+ edited_content = content.gsub(COMMENT_REPLACEMENT_REGEX) do |match|
38
+ key = $2
39
+ if (replacement = get_secret(static_path, key))
40
+ info "Restoring secret value for key: #{key}"
41
+ $1 + replacement
42
+ else
43
+ info "Secret missing for key: #{key}"
44
+ add_secret(static_path, key, '')
45
+ match
46
+ end
47
+ end
48
+ write_file(dotfile_path, edited_content)
49
+ else
50
+ info "Skipping %s as there is already a file at %s", path, dotfile_path
51
+ end
52
+ end
53
+
54
+ # TODO consolidate this method and Import#overwrite_file?
55
+ def overwrite_file?(path)
56
+ decision = choose("#{path} already exists as a dotfile. Do you want to replace it?", 'replace', 'skip') do |menu|
57
+ menu.index = :letter
58
+ menu.layout = :one_line
59
+ end
60
+ decision == 'replace'
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ module Briefcase
2
+ module Commands
3
+
4
+ # Run git commands in the dotfiles directory.
5
+ #
6
+ # This command simply passes everything passed to it on to the git
7
+ # executable on the user's PATH.
8
+ #
9
+ # A basic equivalent of this command:
10
+ #
11
+ # briefcase git status
12
+ #
13
+ # Would be:
14
+ #
15
+ # cd ~/.dotfiles && git status
16
+ class Git < Base
17
+
18
+ # Execute a git command in the dotfiles directory. Will prompt the user
19
+ # to create a dotfiles directory if it does not exist, which will also
20
+ # create an empty git repository.
21
+ def execute
22
+ verify_dotfiles_directory_exists
23
+ command = @args.join(' ')
24
+ intro("Running git %s in %s", command, dotfiles_path)
25
+ run_git_command(command)
26
+ end
27
+
28
+ private
29
+
30
+ def run_git_command(command)
31
+ puts `cd #{dotfiles_path} && git #{command}`
32
+ end
33
+
34
+ end
35
+ end
36
+ end
37
+
@@ -0,0 +1,81 @@
1
+ require 'tempfile'
2
+
3
+ module Briefcase
4
+ module Commands
5
+
6
+ # Import copies a dotfile into the dotfiles directory and created a
7
+ # symlink in the file's previous location, pointing to its new location.
8
+ #
9
+ # The file's name in the repository will be the dotfile's name with the
10
+ # leading period removed. So, importing '~/.bashrc' will move that file to
11
+ # ~/.dotfiles/bashrc and then create a symlink at ~/.bashrc pointing to
12
+ # ~/.dotfiles/bashrc.
13
+ #
14
+ # If there was already a file located at ~/.dotfiles/bashrc, the user will
15
+ # be asked if it is OK to mvoe the existing file aside. If the user accepts
16
+ # the move, the existing file is renamed to bashrc.old.1 before the new
17
+ # file is imported, and a message would be shown indicating this has
18
+ # occurred.
19
+ class Import < Base
20
+
21
+ # Execute verifies that the dotfiles directory exists before attempting
22
+ # the import.
23
+ #
24
+ # Raises an error if the specified path does not exist.
25
+ def execute
26
+ verify_dotfiles_directory_exists
27
+
28
+ @path = File.expand_path(@args.first)
29
+ raise UnrecoverableError.new("#{@path} does not exist") unless File.exist?(@path)
30
+
31
+ intro("Importing %s into %s", @path, dotfiles_path)
32
+ import_file
33
+ end
34
+
35
+ private
36
+
37
+ # Import a file. Creates the dotfiles directory if it doesn't exist
38
+ # before attempting the import. Prompts the user when there is a naming
39
+ # collision between an existing dotfile and the file to be imported.
40
+ #
41
+ # Raises CommandAborted if there is a colision and the user declines to
42
+ # move the existing file.
43
+ def import_file
44
+ collision = dotfile_exists?(@path)
45
+ if !collision || overwrite_file?
46
+
47
+ mkdir_p(dotfiles_path)
48
+ destination = generate_dotfile_path(@path)
49
+
50
+ if collision
51
+ existing = Dir.glob("#{destination}.old.*").size
52
+ sideline = "#{destination}.old.#{existing+1}"
53
+ info "Moving %s to %s", destination, sideline
54
+ mv(destination, sideline)
55
+ end
56
+
57
+ move(@path, destination)
58
+ symlink(destination, @path)
59
+ else
60
+ raise CommandAborted.new('Cancelled.')
61
+ end
62
+ end
63
+
64
+ # Ask the user if it is OK to move an existing file so a new file can be
65
+ # imported.
66
+ #
67
+ # Returns whether the user accepts the move or not as a Boolean.
68
+ #
69
+ # TODO: Rename this method as it doesn't overwrite anything.
70
+ def overwrite_file?
71
+ decision = choose("#{@path} already exists as a dotfile. Do you want to replace it? Your original file will be renamed.", 'replace', 'abort') do |menu|
72
+ menu.index = :letter
73
+ menu.layout = :one_line
74
+ end
75
+
76
+ decision == 'replace'
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,76 @@
1
+ module Briefcase
2
+ module Commands
3
+
4
+ # Redact is similar to Import, but it will also prompt the user to replace
5
+ # any secure information in the file with a special replacement syntax. Any
6
+ # values replaces in this way will be stored in the secrets file, and the
7
+ # dotfile will be imported sans secrets.
8
+ class Redact < Import
9
+
10
+ EDITING_HELP_TEXT = <<-TEXT
11
+ # Edit the file below, replacing and sensitive information to turn this:
12
+ #
13
+ # password: superSecretPassword
14
+ #
15
+ # Into:
16
+ #
17
+ # password: # briefcase(password)
18
+ #
19
+ ########################################################################
20
+ TEXT
21
+
22
+ private
23
+
24
+ def import_file
25
+ super
26
+ create_redacted_version
27
+ end
28
+
29
+ # Copy the file to be imported into the dotfiles directory and append the
30
+ # redacted extension to its name. This file is then opened in an
31
+ # editor, where the user has a chance to replace sensitive information.
32
+ # After saving and closing the file, the differences are examined and the
33
+ # replaces values are detected. These values and their replacement keys
34
+ # are stored in the secrets file.
35
+ def create_redacted_version
36
+ destination = generate_dotfile_path(@path)
37
+ redacted_path = destination + ".#{REDACTED_EXTENSION}"
38
+ info "Creating redacted version at #{redacted_path}"
39
+
40
+ content_to_edit = original_content = File.read(destination)
41
+
42
+ unless Briefcase.testing?
43
+ content_to_edit = EDITING_HELP_TEXT + content_to_edit
44
+ end
45
+
46
+ write_file(redacted_path, content_to_edit)
47
+ edited_content = edit_file_with_editor(redacted_path).gsub!(EDITING_HELP_TEXT, '')
48
+ write_file(redacted_path, edited_content)
49
+
50
+ edited_content.lines.each_with_index do |line, line_index|
51
+ if line =~ COMMENT_REPLACEMENT_REGEX
52
+ key = $2
53
+ mask = %r{^#{$1}(.*)$}
54
+ value = original_content.lines.to_a[line_index].match(mask)[1]
55
+ info "Storing secret value for key: #{key}"
56
+ add_secret(destination, key, value)
57
+ end
58
+ end
59
+
60
+ write_secrets
61
+ add_to_git_ignore(visible_name(destination))
62
+ end
63
+
64
+ # Open a file with an editor. The editor can be specified using the
65
+ # EDITOR environment variable, with vim being the current default.
66
+ #
67
+ # Returns the content of the file after the editor is closed.
68
+ def edit_file_with_editor(path)
69
+ editor_command = ENV['BRIEFCASE_EDITOR'] || 'vim'
70
+ system(editor_command, path)
71
+ File.read(path)
72
+ end
73
+
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,66 @@
1
+ module Briefcase
2
+ module Commands
3
+
4
+ # Sync scans the dotfiles directory for dotfiles, and creates any missing
5
+ # symlinks in the user's home directory.
6
+ #
7
+ # A message is printed out for each file, indicating if a matching symlink
8
+ # was found in the home directory or if one was created.
9
+ #
10
+ # If there is a symlink in the user's home directory with a dotfile's name
11
+ # but it is pointing to the wrong location, it is removed and a new symlink
12
+ # is created in its place.
13
+ class Sync < Base
14
+
15
+ # Scan the dotfiles directory for files, and process each one. Files
16
+ # with names containing '.old' are ignored and a warning is show to alert
17
+ # the user of their presence.
18
+ def execute
19
+ intro "Synchronizing dotfiles between #{dotfiles_path} and #{home_path}"
20
+
21
+ Dir.glob(File.join(dotfiles_path, '*')) do |path|
22
+ basename = File.basename(path)
23
+ next if %w{. ..}.include?(basename)
24
+ next if basename =~ /.#{REDACTED_EXTENSION}$/
25
+
26
+ if basename.include?('.old')
27
+ warn "Skipping %s, you may want to remove it.", path
28
+ else
29
+ create_or_verify_symlink(basename)
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ # Verifies if a symlink following briefcase's conventions exists for the
37
+ # supplied filename. Creates a symlink if it doesn't exist, or if there
38
+ # is a corectly named symlink with the wrong target.
39
+ #
40
+ # basename - The String filename to use when building paths
41
+ def create_or_verify_symlink(basename)
42
+ dotfile_name = ".#{basename}"
43
+ symlink_path = File.join(home_path, dotfile_name)
44
+ dotfile_path = generate_dotfile_path(basename)
45
+
46
+ if File.exist?(symlink_path)
47
+ if File.symlink?(symlink_path)
48
+ if File.readlink(symlink_path) == dotfile_path
49
+ info "Symlink verified: %s -> %s", symlink_path, dotfile_path
50
+ return
51
+ else
52
+ info "Removing outdated symlink %s", symlink_path
53
+ rm(symlink_path)
54
+ end
55
+ else
56
+ info "Found normal file at %s, skipping...", symlink_path
57
+ return
58
+ end
59
+ end
60
+
61
+ symlink(dotfile_path, symlink_path)
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ require 'commander/import'
2
+ require File.expand_path('../briefcase', File.dirname(__FILE__))
3
+
4
+ program :name, 'Briefcase'
5
+ program :version, Briefcase::VERSION
6
+ program :description, 'Makes it easier to keep dotfiles in git'
7
+
8
+ command :import do |c|
9
+ c.syntax = 'briefcase import PATH'
10
+ c.description = 'Move PATH to the version controlled directory and symlink its previous location to its new one.'
11
+ c.when_called Briefcase::Commands::Import
12
+ end
13
+
14
+ command :redact do |c|
15
+ c.syntax = 'briefcase redact PATH'
16
+ c.description = 'Edit PATH to remove sensitive information, save the edited version to the version controlled directory, and symlink its previous location to its new one, and add to .gitignore.'
17
+ c.when_called Briefcase::Commands::Redact
18
+ end
19
+
20
+ command :sync do |c|
21
+ c.syntax = 'briefcase sync'
22
+ c.description = 'Updates all symlinks for files included in ~/.dotfiles'
23
+ c.when_called Briefcase::Commands::Sync
24
+ end
25
+
26
+ command :generate do |c|
27
+ c.syntax = 'briefcase generate'
28
+ c.description = 'Generates static versions of all redacted dotfiles in ~/.dotfiles'
29
+ c.when_called Briefcase::Commands::Generate
30
+ end
31
+
32
+ command :git do |c|
33
+ c.syntax = 'briefcase git [options]'
34
+ c.description = 'Run a git command in the dotfiles directory'
35
+ c.when_called Briefcase::Commands::Git
36
+ end
37
+
38
+ default_command :help
39
+
40
+ # command :suggest do |c|
41
+ # c.syntax = 'briefcase suggest'
42
+ # c.description 'List dotfiles that are in your home directory and not in ~/.dotfiles'
43
+ # c.when_called Briefcase::Commands::Suggest
44
+ # end
@@ -0,0 +1,3 @@
1
+ module Briefcase
2
+ VERSION = '0.4.0'
3
+ end
data/spec/bin/editor ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ responses_path = File.expand_path('/tmp/briefcase_spec_work/briefcase_editor_responses', File.dirname(__FILE__))
4
+ response_file = ARGV[0].gsub(/\//, '_')
5
+ File.open(ARGV[0], 'w') do |file|
6
+ file.write(File.read(File.join(responses_path, response_file)))
7
+ end
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe Briefcase::Commands::Generate do
4
+
5
+ describe "with an existing dotfiles directory" do
6
+
7
+ before do
8
+ create_dotfiles_directory
9
+ create_home_directory
10
+ end
11
+
12
+ after do
13
+ cleanup_dotfiles_directory
14
+ cleanup_home_directory
15
+ end
16
+
17
+ it "generates a static version of a redacted dotfile" do
18
+ static_path = File.join(dotfiles_path, 'test')
19
+ redacted_path = File.join(dotfiles_path, 'test.redacted')
20
+
21
+ create_secrets('test' => {'email' => 'google@internet.com'})
22
+ create_file redacted_path, <<-TEXT
23
+ username: # briefcase(email)
24
+ favorite_color: blue
25
+ TEXT
26
+
27
+ run_command("generate")
28
+
29
+ output_must_contain(/Generating/, /Loading existing secrets/, /Restoring secret value/)
30
+
31
+ file_must_contain static_path, <<-TEXT
32
+ username: google@internet.com
33
+ favorite_color: blue
34
+ TEXT
35
+ end
36
+
37
+ it "create a secrets file and adds discovered secrets to it" do
38
+ static_path = File.join(dotfiles_path, 'test')
39
+ redacted_path = File.join(dotfiles_path, 'test.redacted')
40
+
41
+ create_file redacted_path, <<-TEXT
42
+ username: # briefcase(email)
43
+ TEXT
44
+
45
+ run_command("generate")
46
+
47
+ output_must_contain(/Generating/, /Secret missing for key: email/)
48
+
49
+ file_must_contain static_path, <<-TEXT
50
+ username: # briefcase(email)
51
+ TEXT
52
+
53
+ secret_must_be_stored('test', 'email', '')
54
+ end
55
+
56
+ it "adds discovered secrets to the secrets file without values" do
57
+ static_path = File.join(dotfiles_path, 'test')
58
+ redacted_path = File.join(dotfiles_path, 'test.redacted')
59
+
60
+ create_file redacted_path, <<-TEXT
61
+ username: # briefcase(email)
62
+ TEXT
63
+ create_secrets
64
+
65
+ run_command("generate")
66
+
67
+ output_must_contain(/Generating/, /Secret missing for key: email/)
68
+
69
+ file_must_contain static_path, <<-TEXT
70
+ username: # briefcase(email)
71
+ TEXT
72
+
73
+ secret_must_be_stored('test', 'email', '')
74
+ end
75
+
76
+ end
77
+
78
+ end