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 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
@@ -0,0 +1,9 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new('spec') do |t|
5
+ t.libs << 'spec'
6
+ t.pattern = "spec/*_spec.rb"
7
+ end
8
+
9
+ task :default => :spec
data/bin/briefcase ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+ require 'briefcase/main'
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