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
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Jim Benton
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
= briefcase
|
2
|
+
|
3
|
+
briefcase is a tool to facilitate keeping dotfiles in git, including those with private information (such as .gitconfig).
|
4
|
+
|
5
|
+
|
6
|
+
=== This is alpha software! I haven't published a gem yet for a good reason. Use at your own risk.
|
7
|
+
|
8
|
+
|
9
|
+
== Getting started
|
10
|
+
|
11
|
+
gem install briefcase
|
12
|
+
briefcase import ~/.bashrc
|
13
|
+
|
14
|
+
At this point, a git repository will have been created at ~/.dotfiles. At some point more of the git workflow will be automated (such as creating a repository at Github), but for now most git tasks are manual:
|
15
|
+
|
16
|
+
cd ~/.dotfiles
|
17
|
+
git commit -m "Added bashrc"
|
18
|
+
|
19
|
+
== Filesystem layout
|
20
|
+
|
21
|
+
With briefcase, dotfiles are stored in a centralized location (by default ~/.dotfiles), and symlinks are created for these files in the user's home directory. Here is a basic setup for a .gitconfig file:
|
22
|
+
|
23
|
+
+-~/ Home Directory
|
24
|
+
| +-.briefcase_secrets Secrets file
|
25
|
+
| +-.gitconfig Symlink to ~/.dotfiles/gitconfig
|
26
|
+
| +-.dotfiles/ Dotfiles directory
|
27
|
+
| | +-gitconfig Standard Dotfile
|
28
|
+
| | +-gitconfig.dynamic Redacted dotfile
|
29
|
+
|
30
|
+
=== Home directory (~)
|
31
|
+
|
32
|
+
Where the action happens. Dotfiles that normally exist here are replaced by symlinks to files in the dotfiles directory
|
33
|
+
|
34
|
+
=== Dotfiles directory (~/)
|
35
|
+
|
36
|
+
Where dotfiles are stored. There are two types of dotfiles.
|
37
|
+
|
38
|
+
==== Standard dotfiles
|
39
|
+
|
40
|
+
A basic config file that would normally live in a user's home directory.
|
41
|
+
|
42
|
+
==== Redacted dotfiles
|
43
|
+
A config file that contains some secret information. The file is stored, sans secret info, with a '.redacted' extension in the repo. The dotfile that is symlinked to the user's home directory is then generated from this file.
|
44
|
+
|
45
|
+
=== Secrets file
|
46
|
+
|
47
|
+
This file, be default located at ~/.briefcase_secrets, contains the information removed from dotfiles imported using the redact command.
|
48
|
+
|
49
|
+
== Commands
|
50
|
+
|
51
|
+
=== import PATH_TO_FILE
|
52
|
+
|
53
|
+
Imports a dotfile into thedotfiles directory, moving it to the dotfiles directory and replacing it with a symlink to its new location.
|
54
|
+
|
55
|
+
|
56
|
+
=== redact PATH_TO_FILE
|
57
|
+
|
58
|
+
Imports a dotfile that contains sensitive information. The user is presented with an editor, where the sensitive information can be removed by replacing secret information with a commented out call to a briefcase function
|
59
|
+
|
60
|
+
# before
|
61
|
+
password: superSecretPassword
|
62
|
+
|
63
|
+
# after
|
64
|
+
password: # briefcase(password)
|
65
|
+
|
66
|
+
This file is then saved with a '.redacted' extension, and the original file is added to ~/.dotfiles/.gitignore so it will not be added to the repository.
|
67
|
+
|
68
|
+
'superSecretPassword' will be stored using the key "password" in the secrets file.
|
69
|
+
|
70
|
+
|
71
|
+
=== sync
|
72
|
+
Creates a symlink in the user's home directory for each dotfile in the dotfiles directory.
|
73
|
+
|
74
|
+
|
75
|
+
=== generate
|
76
|
+
Creates local version of a redacted dotfile, using information found in the secrets file to fill in any that was removed.
|
77
|
+
|
78
|
+
|
79
|
+
== Configuration
|
80
|
+
|
81
|
+
The following environment variables can by used to customized the paths used by briefcase:
|
82
|
+
|
83
|
+
SHHH_DOTFILES_PATH: dotfiles path, defaults to SHHH_HOME_PATH/.dotfiles
|
84
|
+
SHHH_HOME_PATH: home path, defaults to ~
|
85
|
+
SHHH_SECRETS_PATH: secrets path, defaults to SHHH_HOME_PATH/.briefcase_secrets
|
86
|
+
|
87
|
+
== Note on Patches/Pull Requests
|
88
|
+
|
89
|
+
* Fork the project.
|
90
|
+
* Make your feature addition or bug fix on a topic branch.
|
91
|
+
* Add tests for it. This is important so I don't break it in a future version unintentionally.
|
92
|
+
* Commit, do not mess with rakefile, version, or history.
|
93
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
94
|
+
* Send me a pull request.
|
95
|
+
|
96
|
+
== Changelog
|
97
|
+
* 0.3.0 Added code documentation, internal renaming, general cleanup. First public release.
|
98
|
+
* 0.2.0 Added redact command, use .redacted for dynamic dotfiles
|
99
|
+
* 0.1.3 The sync command no longer creates symlinks for dynamic files
|
100
|
+
* 0.1.2 Added dynamic file generation
|
101
|
+
|
102
|
+
== Copyright
|
103
|
+
|
104
|
+
Copyright (c) 2010 Jim Benton. See LICENSE for details.
|
data/Rakefile
ADDED
data/bin/briefcase
ADDED
data/briefcase.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.expand_path('lib/briefcase/version', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{briefcase}
|
5
|
+
s.version = Briefcase::VERSION
|
6
|
+
s.summary = %q{Briefcase manages dotfiles and handles keeping their secrets safe}
|
7
|
+
s.description = %q{Command line program to migrate dotfiles to a git repo at ~/.dotfiles and generate static dotfiles with secret values.}
|
8
|
+
s.authors = ["Jim Benton"]
|
9
|
+
s.date = %q{2011-12-22}
|
10
|
+
s.default_executable = %q{briefcase}
|
11
|
+
s.email = %q{jim@autonomousmachine.com}
|
12
|
+
s.executables = ["briefcase"]
|
13
|
+
s.extra_rdoc_files = %w{README.rdoc LICENSE}
|
14
|
+
s.files = Dir['lib/**/*.rb'] + # library
|
15
|
+
Dir['bin/*'] + # executable
|
16
|
+
Dir['spec/**/*.rb'] + # spec files
|
17
|
+
Dir['spec/bin/editor'] + # spec editor
|
18
|
+
%w{README.rdoc LICENSE briefcase.gemspec Rakefile} # misc
|
19
|
+
|
20
|
+
s.homepage = %q{http://github.com/jim/briefcase}
|
21
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
s.rubygems_version = %q{1.3.7}
|
24
|
+
s.test_files = Dir['spec/*.rb']
|
25
|
+
|
26
|
+
s.add_runtime_dependency('commander')
|
27
|
+
s.add_runtime_dependency('activesupport')
|
28
|
+
s.add_development_dependency('minitest')
|
29
|
+
s.add_development_dependency('open4')
|
30
|
+
s.add_development_dependency('rake', '0.9.2.2')
|
31
|
+
s.add_development_dependency('turn')
|
32
|
+
end
|
33
|
+
|
data/lib/briefcase.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
require 'active_support/core_ext/hash/deep_merge'
|
4
|
+
|
5
|
+
require File.expand_path('briefcase/commands', File.dirname(__FILE__))
|
6
|
+
require File.expand_path('briefcase/version', File.dirname(__FILE__))
|
7
|
+
|
8
|
+
module Briefcase
|
9
|
+
|
10
|
+
# The user's home path
|
11
|
+
DEFAULT_HOME_PATH = '~'
|
12
|
+
|
13
|
+
# The default path wher dotfiles are stored
|
14
|
+
DEFAULT_DOTFILES_PATH = File.join(DEFAULT_HOME_PATH, '.dotfiles')
|
15
|
+
|
16
|
+
# The default path to where secret information is stored
|
17
|
+
DEFAULT_SECRETS_PATH = File.join(DEFAULT_HOME_PATH, '.briefcase_secrets')
|
18
|
+
|
19
|
+
class << self
|
20
|
+
attr_accessor :dotfiles_path, :home_path, :secrets_path, :testing
|
21
|
+
|
22
|
+
def dotfiles_path
|
23
|
+
@dotfiles_path ||= File.expand_path(ENV['BRIEFCASE_DOTFILES_PATH'] || DEFAULT_DOTFILES_PATH)
|
24
|
+
end
|
25
|
+
|
26
|
+
def home_path
|
27
|
+
@home_path ||= File.expand_path(ENV['BRIEFCASE_HOME_PATH'] || DEFAULT_HOME_PATH)
|
28
|
+
end
|
29
|
+
|
30
|
+
def secrets_path
|
31
|
+
@secrets_path ||= File.expand_path(ENV['BRIEFCASE_SECRETS_PATH'] || DEFAULT_SECRETS_PATH)
|
32
|
+
end
|
33
|
+
|
34
|
+
def testing?
|
35
|
+
@testing ||= ENV['BRIEFCASE_TESTING'] == 'true'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class UnrecoverableError < StandardError; end
|
40
|
+
class CommandAborted < StandardError; end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require File.expand_path('commands/base', File.dirname(__FILE__))
|
2
|
+
require File.expand_path('commands/import', File.dirname(__FILE__))
|
3
|
+
require File.expand_path('commands/redact', File.dirname(__FILE__))
|
4
|
+
require File.expand_path('commands/sync', File.dirname(__FILE__))
|
5
|
+
require File.expand_path('commands/generate', File.dirname(__FILE__))
|
6
|
+
require File.expand_path('commands/git', File.dirname(__FILE__))
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require File.expand_path('core/secrets', File.dirname(__FILE__))
|
3
|
+
require File.expand_path('core/files', File.dirname(__FILE__))
|
4
|
+
require File.expand_path('core/output', File.dirname(__FILE__))
|
5
|
+
|
6
|
+
module Briefcase
|
7
|
+
module Commands
|
8
|
+
|
9
|
+
# Briefcase::Commands::Base is the base class for all commands in the system.
|
10
|
+
#
|
11
|
+
# Most behavior is actually defined by Core modules.
|
12
|
+
#
|
13
|
+
# Actual commands, which are created by creating a subclass of Base, must
|
14
|
+
# implement an instance method `execute`.
|
15
|
+
#
|
16
|
+
# Running a Commands::Base subclass is done by instantiating it with
|
17
|
+
# arguments and options.
|
18
|
+
class Base
|
19
|
+
|
20
|
+
# The extension to append to files when redacting information
|
21
|
+
REDACTED_EXTENSION = 'redacted'
|
22
|
+
|
23
|
+
include FileUtils
|
24
|
+
include Core::Files
|
25
|
+
include Core::Secrets
|
26
|
+
include Core::Output
|
27
|
+
|
28
|
+
def initialize(args, options)
|
29
|
+
@args = args
|
30
|
+
@options = options
|
31
|
+
run
|
32
|
+
end
|
33
|
+
|
34
|
+
# Begin execution of this command. Subclasses should not override this
|
35
|
+
# method, instead, they should define an `execute` method that performs
|
36
|
+
# their actual work.
|
37
|
+
def run
|
38
|
+
begin
|
39
|
+
execute
|
40
|
+
say('')
|
41
|
+
success "Done."
|
42
|
+
rescue CommandAborted, UnrecoverableError => e
|
43
|
+
error(e.message)
|
44
|
+
exit(255)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Perform this command's work.
|
49
|
+
#
|
50
|
+
# This method should be overridden in subclasses.
|
51
|
+
def execute
|
52
|
+
raise "Not Implemented"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Add a file to the .gitignore file inside the dotfiles_path
|
56
|
+
#
|
57
|
+
# filename - The String filename to be appended to the list of ignored paths
|
58
|
+
#
|
59
|
+
# Returns the Integer number of bytes written.
|
60
|
+
def add_to_git_ignore(filename)
|
61
|
+
File.open(File.join(dotfiles_path, '.gitignore'), "a+") do |file|
|
62
|
+
contents = file.read
|
63
|
+
unless contents =~ %r{^#{filename}$}
|
64
|
+
info("Adding #{filename} to #{File.join(dotfiles_path, '.gitignore')}")
|
65
|
+
file.write(filename + "\n")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Check to see if the dotfiles directory exists. If it doesn't, present
|
71
|
+
# the user with the option to create it. If the user accepts, the
|
72
|
+
# directory is created.
|
73
|
+
#
|
74
|
+
# If the user declines creating the directory, a CommandAborted exception
|
75
|
+
# is raised.
|
76
|
+
def verify_dotfiles_directory_exists
|
77
|
+
if !File.directory?(dotfiles_path)
|
78
|
+
choice = choose("You don't appear to have a git repository at #{dotfiles_path}. Do you want to create one now?", 'create', 'abort') do |menu|
|
79
|
+
menu.index = :letter
|
80
|
+
menu.layout = :one_line
|
81
|
+
end
|
82
|
+
if choice == 'create'
|
83
|
+
info "Creating a directory at #{dotfiles_path}"
|
84
|
+
mkdir_p(dotfiles_path)
|
85
|
+
info `git init #{dotfiles_path}`
|
86
|
+
else
|
87
|
+
raise CommandAborted.new('Can not continue without a dotfiles repository!')
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Briefcase
|
2
|
+
module Commands
|
3
|
+
module Core
|
4
|
+
module Files
|
5
|
+
|
6
|
+
# Create a symlink at a path with a given target.
|
7
|
+
#
|
8
|
+
# target - the String target for the symlink
|
9
|
+
# destination - The String location for the symlink
|
10
|
+
def symlink(target, destination)
|
11
|
+
ln_s(target, destination)
|
12
|
+
info "Symlinking %s -> %s", destination, target
|
13
|
+
end
|
14
|
+
|
15
|
+
# Move a file from one location to another
|
16
|
+
#
|
17
|
+
# path - The String path to be moved
|
18
|
+
# destination - The String path to move the file to
|
19
|
+
def move(path, destination)
|
20
|
+
mv(path, destination)
|
21
|
+
info "Moving %s to %s", path, destination
|
22
|
+
end
|
23
|
+
|
24
|
+
# Write some content to a file path.
|
25
|
+
#
|
26
|
+
# path - The String path to the file that should be written to
|
27
|
+
# content - The String content to write to the file
|
28
|
+
# io_mode - The String IO mode to use when writing the file
|
29
|
+
def write_file(path, content, io_mode='w')
|
30
|
+
File.open(path, io_mode) do |file|
|
31
|
+
file.write(content)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the globally configured home path for the user.
|
36
|
+
def home_path
|
37
|
+
Briefcase.home_path
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns the globally configured dotfiles path.
|
41
|
+
def dotfiles_path
|
42
|
+
Briefcase.dotfiles_path
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the globally configured secrets path.
|
46
|
+
def secrets_path
|
47
|
+
Briefcase.secrets_path
|
48
|
+
end
|
49
|
+
|
50
|
+
# Build a full dotfile path from a given file path, using the gobal
|
51
|
+
# dotfiles path setting.
|
52
|
+
#
|
53
|
+
# file_path - The String path to build a dotfile path from
|
54
|
+
def generate_dotfile_path(file_path)
|
55
|
+
File.join(dotfiles_path, visible_name(file_path))
|
56
|
+
end
|
57
|
+
|
58
|
+
# Check to see if there is a stored dotfile in the dotfiles directory
|
59
|
+
# that corresponds to the specified file.
|
60
|
+
#
|
61
|
+
# file_path - The String file name to check
|
62
|
+
#
|
63
|
+
# Returns whether the file exists or not as a Boolean
|
64
|
+
def dotfile_exists?(file_path)
|
65
|
+
File.exist?(generate_dotfile_path(file_path))
|
66
|
+
end
|
67
|
+
|
68
|
+
# Convert a file path into a file name and remove a leading period if
|
69
|
+
# it exists.
|
70
|
+
#
|
71
|
+
# file_path - The String file path to create a visible name for
|
72
|
+
#
|
73
|
+
# Returns the manipulated file name
|
74
|
+
def visible_name(file_path)
|
75
|
+
File.basename(file_path).gsub(/^\./, '')
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Briefcase
|
2
|
+
module Commands
|
3
|
+
module Core
|
4
|
+
module Output
|
5
|
+
|
6
|
+
# Print some bold green text to the console.
|
7
|
+
def success(*args); say $terminal.color(format(*args), :green, :bold); end
|
8
|
+
|
9
|
+
# Print some yellow text to the console.
|
10
|
+
def info(*args); say $terminal.color(format(*args), :yellow); end
|
11
|
+
|
12
|
+
# Print some red text to the console.
|
13
|
+
def error(*args); say $terminal.color(format(*args), :red); end
|
14
|
+
|
15
|
+
# Print some magenta text to the console.
|
16
|
+
def warn(*args); say $terminal.color(format(*args), :magenta); end
|
17
|
+
|
18
|
+
# Print some bold text to the console.
|
19
|
+
def intro(*args); say $terminal.color(format(*args), :bold); say(''); end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Briefcase
|
2
|
+
module Commands
|
3
|
+
module Core
|
4
|
+
module Secrets
|
5
|
+
|
6
|
+
COMMENT_REPLACEMENT_REGEX = /^([^#]*)#\s*briefcase\(([a-zA-Z_]+)\)\s*$/
|
7
|
+
|
8
|
+
# Add a key and value to the secrets file for the given file.
|
9
|
+
#
|
10
|
+
# path - The String path to the file containing the secret.
|
11
|
+
# key - The String key to store the value as
|
12
|
+
# value - The String value to store
|
13
|
+
def add_secret(path, key, value)
|
14
|
+
path_key = File.basename(path)
|
15
|
+
secrets[path_key] ||= {}
|
16
|
+
secrets[path_key][key] = value
|
17
|
+
end
|
18
|
+
|
19
|
+
# Get a secret value for the given file and key.
|
20
|
+
#
|
21
|
+
# path - The String path to the file that contains the secret
|
22
|
+
# key - The String key to retrieve the value for
|
23
|
+
#
|
24
|
+
# Returns the string value from the secrets file for the given key.
|
25
|
+
def get_secret(path, key)
|
26
|
+
path_key = File.basename(path)
|
27
|
+
secrets[path_key][key] if secrets[path_key] && secrets[path_key][key]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Write the internal secrets hash to the secrets file as YAML.
|
31
|
+
def write_secrets
|
32
|
+
write_file(secrets_path, secrets.to_yaml)
|
33
|
+
end
|
34
|
+
|
35
|
+
# The secrets hash.
|
36
|
+
#
|
37
|
+
# Returns the secrets hash if a a secrets file exists, or an empty hash
|
38
|
+
# if it does not.
|
39
|
+
def secrets
|
40
|
+
@secrets ||= if File.exist?(secrets_path)
|
41
|
+
info "Loading existing secrets from #{secrets_path}"
|
42
|
+
YAML.load_file(secrets_path)
|
43
|
+
else
|
44
|
+
{}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|