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.
- data/LICENSE +20 -0
- data/README.rdoc +104 -0
- data/Rakefile +9 -0
- data/bin/briefcase +3 -0
- data/briefcase.gemspec +33 -0
- data/lib/briefcase.rb +42 -0
- data/lib/briefcase/commands.rb +6 -0
- data/lib/briefcase/commands/base.rb +94 -0
- data/lib/briefcase/commands/core/files.rb +81 -0
- data/lib/briefcase/commands/core/output.rb +24 -0
- data/lib/briefcase/commands/core/secrets.rb +50 -0
- data/lib/briefcase/commands/generate.rb +65 -0
- data/lib/briefcase/commands/git.rb +37 -0
- data/lib/briefcase/commands/import.rb +81 -0
- data/lib/briefcase/commands/redact.rb +76 -0
- data/lib/briefcase/commands/sync.rb +66 -0
- data/lib/briefcase/main.rb +44 -0
- data/lib/briefcase/version.rb +3 -0
- data/spec/bin/editor +7 -0
- data/spec/generate_spec.rb +78 -0
- data/spec/git_spec.rb +21 -0
- data/spec/helpers/assertions.rb +84 -0
- data/spec/helpers/commands.rb +42 -0
- data/spec/helpers/files.rb +56 -0
- data/spec/helpers/stubbing.rb +25 -0
- data/spec/import_spec.rb +145 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/sync_spec.rb +52 -0
- metadata +183 -0
@@ -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
|
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
|