briefcase 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|