briefcase 0.4.0

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