briefcase 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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