git-duet 0.1.1
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/.gitignore +5 -0
- data/.jrubyrc +1 -0
- data/.rbenv-version +1 -0
- data/.rspec +2 -0
- data/.simplecov +5 -0
- data/.travis.yml +7 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +268 -0
- data/Rakefile +10 -0
- data/bin/git-duet +3 -0
- data/bin/git-duet-commit +3 -0
- data/bin/git-duet-install-hook +3 -0
- data/bin/git-duet-pre-commit +3 -0
- data/bin/git-duet-pre-commit-tk +3 -0
- data/bin/git-solo +3 -0
- data/git-duet.gemspec +36 -0
- data/lib/git-duet.rb +1 -0
- data/lib/git/duet.rb +8 -0
- data/lib/git/duet/author_mapper.rb +77 -0
- data/lib/git/duet/cli.rb +119 -0
- data/lib/git/duet/command_methods.rb +109 -0
- data/lib/git/duet/commit_command.rb +76 -0
- data/lib/git/duet/duet_command.rb +49 -0
- data/lib/git/duet/install_hook_command.rb +33 -0
- data/lib/git/duet/key_error.rb +3 -0
- data/lib/git/duet/pre_commit_command.rb +43 -0
- data/lib/git/duet/script_die_error.rb +11 -0
- data/lib/git/duet/solo_command.rb +45 -0
- data/lib/git/duet/version.rb +7 -0
- data/spec/integration/end_to_end_spec.rb +304 -0
- data/spec/lib/git/duet/author_mapper_spec.rb +170 -0
- data/spec/lib/git/duet/cli_spec.rb +46 -0
- data/spec/lib/git/duet/command_methods_spec.rb +51 -0
- data/spec/lib/git/duet/duet_command_spec.rb +88 -0
- data/spec/lib/git/duet/pre_commit_command_spec.rb +36 -0
- data/spec/lib/git/duet/solo_command_spec.rb +115 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/author_mapper_helper.rb +28 -0
- metadata +172 -0
data/lib/git/duet/cli.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'git/duet'
|
3
|
+
require 'git/duet/script_die_error'
|
4
|
+
|
5
|
+
class Git::Duet::Cli
|
6
|
+
class << self
|
7
|
+
def run(prog, argv)
|
8
|
+
case prog
|
9
|
+
when /solo$/
|
10
|
+
solo(parse_solo_options(argv.clone))
|
11
|
+
return 0
|
12
|
+
when /duet$/
|
13
|
+
duet(parse_duet_options(argv.clone))
|
14
|
+
return 0
|
15
|
+
when /pre-commit$/
|
16
|
+
pre_commit(parse_generic_options(argv.clone))
|
17
|
+
return 0
|
18
|
+
when /install-hook$/
|
19
|
+
install_hook(parse_generic_options(argv.clone))
|
20
|
+
return 0
|
21
|
+
when /commit$/
|
22
|
+
commit(parse_commit_options(argv.clone))
|
23
|
+
return 0
|
24
|
+
else
|
25
|
+
raise ScriptError.new('How did you get here???')
|
26
|
+
end
|
27
|
+
rescue Git::Duet::ScriptDieError => e
|
28
|
+
return e.exit_code
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
def with_common_opts(argv, banner)
|
33
|
+
options = {}
|
34
|
+
leftover_argv = OptionParser.new do |opts|
|
35
|
+
opts.banner = banner.gsub(/__PROG__/, opts.program_name)
|
36
|
+
opts.on('-q', 'Silence output') do |q|
|
37
|
+
options[:quiet] = true
|
38
|
+
end
|
39
|
+
if block_given?
|
40
|
+
yield opts, options
|
41
|
+
end
|
42
|
+
end.parse!(argv)
|
43
|
+
return leftover_argv, options
|
44
|
+
end
|
45
|
+
|
46
|
+
def parse_solo_options(argv)
|
47
|
+
leftover_argv, options = with_common_opts(
|
48
|
+
argv, 'Usage: __PROG__ [options] <soloist-initials>'
|
49
|
+
) do |opts,options_hash|
|
50
|
+
opts.on('-g', '--global', 'Change global git config') do |g|
|
51
|
+
options_hash[:global] = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
options[:soloist] = leftover_argv.first
|
55
|
+
options
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_duet_options(argv)
|
59
|
+
leftover_argv, options = with_common_opts(
|
60
|
+
argv, 'Usage: __PROG__ [options] <alpha-initials> <omega-initials>'
|
61
|
+
) do |opts,options_hash|
|
62
|
+
opts.on('-g', '--global', 'Change global git config') do |g|
|
63
|
+
options_hash[:global] = true
|
64
|
+
end
|
65
|
+
end
|
66
|
+
options[:alpha], options[:omega] = leftover_argv[0..1]
|
67
|
+
options
|
68
|
+
end
|
69
|
+
|
70
|
+
def parse_generic_options(argv)
|
71
|
+
with_common_opts(argv, 'Usage: __PROG__').last
|
72
|
+
end
|
73
|
+
|
74
|
+
def parse_commit_options(argv)
|
75
|
+
opts_argv = []
|
76
|
+
opts_argv << '-q' if argv.delete('-q')
|
77
|
+
options = with_common_opts(opts_argv, 'Usage: __PROG__').last
|
78
|
+
options[:passthrough_args] = argv
|
79
|
+
options
|
80
|
+
end
|
81
|
+
|
82
|
+
def solo(options)
|
83
|
+
require 'git/duet/solo_command'
|
84
|
+
Git::Duet::SoloCommand.new(
|
85
|
+
options.fetch(:soloist),
|
86
|
+
options[:quiet],
|
87
|
+
options[:global]
|
88
|
+
).execute!
|
89
|
+
end
|
90
|
+
|
91
|
+
def duet(options)
|
92
|
+
require 'git/duet/duet_command'
|
93
|
+
Git::Duet::DuetCommand.new(
|
94
|
+
options.fetch(:alpha),
|
95
|
+
options.fetch(:omega),
|
96
|
+
options[:quiet],
|
97
|
+
options[:global]
|
98
|
+
).execute!
|
99
|
+
end
|
100
|
+
|
101
|
+
def pre_commit(options)
|
102
|
+
require 'git/duet/pre_commit_command'
|
103
|
+
Git::Duet::PreCommitCommand.new(options[:quiet]).execute!
|
104
|
+
end
|
105
|
+
|
106
|
+
def install_hook(options)
|
107
|
+
require 'git/duet/install_hook_command'
|
108
|
+
Git::Duet::InstallHookCommand.new(options[:quiet]).execute!
|
109
|
+
end
|
110
|
+
|
111
|
+
def commit(options)
|
112
|
+
require 'git/duet/commit_command'
|
113
|
+
Git::Duet::CommitCommand.new(
|
114
|
+
options[:passthrough_args],
|
115
|
+
options[:quiet]
|
116
|
+
).execute!
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'git/duet'
|
2
|
+
require 'git/duet/script_die_error'
|
3
|
+
|
4
|
+
module Git::Duet::CommandMethods
|
5
|
+
private
|
6
|
+
def report_env_vars
|
7
|
+
var_map.each do |key,value|
|
8
|
+
info("#{key}='#{value}'")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def write_env_vars
|
13
|
+
in_repo_root do
|
14
|
+
var_map.each do |key,value|
|
15
|
+
exec_check("git config #{@global ? '--global ' : ''}duet.env.#{key.downcase.gsub(/_/, '-')} '#{value}'")
|
16
|
+
end
|
17
|
+
exec_check("git config #{@global ? '--global ' : ''}duet.env.mtime #{Time.now.to_i}")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump_env_vars
|
22
|
+
extract_env_vars_from_git_config.each do |k,v|
|
23
|
+
puts "#{k}='#{v}'"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def extract_env_vars_from_git_config
|
28
|
+
dest = {}
|
29
|
+
env_vars.each do |env_var,config_key|
|
30
|
+
begin
|
31
|
+
value = exec_check("git config duet.env.#{config_key}").chomp
|
32
|
+
dest[env_var] = value if !value.empty?
|
33
|
+
rescue StandardError => e
|
34
|
+
error("#{e.message}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
dest
|
38
|
+
end
|
39
|
+
|
40
|
+
def exec_git_commit
|
41
|
+
exec 'git commit --signoff ' << quoted_passthrough_args
|
42
|
+
end
|
43
|
+
|
44
|
+
def env_vars
|
45
|
+
@env_vars ||= Hash[env_var_pairs]
|
46
|
+
end
|
47
|
+
|
48
|
+
def env_var_pairs
|
49
|
+
%w(
|
50
|
+
GIT_AUTHOR_NAME
|
51
|
+
GIT_AUTHOR_EMAIL
|
52
|
+
GIT_COMMITTER_NAME
|
53
|
+
GIT_COMMITTER_EMAIL
|
54
|
+
).map do |env_var|
|
55
|
+
[env_var, env_var.downcase.gsub(/_/, '-')]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def in_repo_root
|
60
|
+
Dir.chdir(exec_check('git rev-parse --show-toplevel').chomp) do
|
61
|
+
yield
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def exec_check(command, okay_statuses = [0].freeze)
|
66
|
+
output = `#{command}`
|
67
|
+
if !okay_statuses.include?($?.exitstatus)
|
68
|
+
error("Command #{command.inspect} exited with #{$?.to_i}")
|
69
|
+
raise Git::Duet::ScriptDieError.new(1)
|
70
|
+
end
|
71
|
+
output
|
72
|
+
end
|
73
|
+
|
74
|
+
def with_output_unquieted(&block)
|
75
|
+
@old_quiet = @quiet
|
76
|
+
@quiet = false
|
77
|
+
block.call
|
78
|
+
@quiet = @old_quiet
|
79
|
+
rescue StandardError => e
|
80
|
+
@quiet = @old_quiet
|
81
|
+
raise e
|
82
|
+
end
|
83
|
+
|
84
|
+
def with_output_quieted(&block)
|
85
|
+
@old_quiet = @quiet
|
86
|
+
@quiet = true
|
87
|
+
block.call
|
88
|
+
rescue StandardError => e
|
89
|
+
raise e
|
90
|
+
ensure
|
91
|
+
@quiet = @old_quiet
|
92
|
+
end
|
93
|
+
|
94
|
+
def info(msg)
|
95
|
+
STDOUT.puts(msg) unless quiet?
|
96
|
+
end
|
97
|
+
|
98
|
+
def error(msg)
|
99
|
+
STDERR.puts(msg) unless quiet?
|
100
|
+
end
|
101
|
+
|
102
|
+
def prompt
|
103
|
+
STDOUT.print '> '
|
104
|
+
end
|
105
|
+
|
106
|
+
def quiet?
|
107
|
+
ENV['GIT_DUET_QUIET'] == '1' || @quiet
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'git/duet'
|
2
|
+
require 'git/duet/command_methods'
|
3
|
+
|
4
|
+
class Git::Duet::CommitCommand
|
5
|
+
include Git::Duet::CommandMethods
|
6
|
+
|
7
|
+
def initialize(passthrough_argv, quiet = false)
|
8
|
+
@passthrough_argv = passthrough_argv
|
9
|
+
@quiet = quiet
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute!
|
13
|
+
in_repo_root do
|
14
|
+
add_env_vars_to_env
|
15
|
+
exec_git_commit
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def add_env_vars_to_env
|
21
|
+
extract_env_vars_from_git_config.each do |k,v|
|
22
|
+
ENV[k] = v
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def exec_git_commit
|
27
|
+
exec 'git commit ' << signoff_arg << quoted_passthrough_args
|
28
|
+
end
|
29
|
+
|
30
|
+
def env_vars
|
31
|
+
@env_vars ||= Hash[env_var_pairs]
|
32
|
+
end
|
33
|
+
|
34
|
+
def env_var_pairs
|
35
|
+
env_var_names.map do |env_var|
|
36
|
+
[env_var, env_var.downcase.gsub(/_/, '-')]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def quoted_passthrough_args
|
41
|
+
@passthrough_argv.map do |arg|
|
42
|
+
"'#{arg}'"
|
43
|
+
end.join(' ')
|
44
|
+
end
|
45
|
+
|
46
|
+
def signoff_arg
|
47
|
+
soloing? ? '' : '--signoff '
|
48
|
+
end
|
49
|
+
|
50
|
+
def env_var_names
|
51
|
+
if soloing?
|
52
|
+
%w(
|
53
|
+
GIT_AUTHOR_NAME
|
54
|
+
GIT_AUTHOR_EMAIL
|
55
|
+
)
|
56
|
+
else
|
57
|
+
%w(
|
58
|
+
GIT_AUTHOR_NAME
|
59
|
+
GIT_AUTHOR_EMAIL
|
60
|
+
GIT_COMMITTER_NAME
|
61
|
+
GIT_COMMITTER_EMAIL
|
62
|
+
)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def soloing?
|
67
|
+
@soloing ||= begin
|
68
|
+
with_output_quieted do
|
69
|
+
exec_check('git config duet.env.git-committer-name').chomp
|
70
|
+
end
|
71
|
+
false
|
72
|
+
rescue StandardError
|
73
|
+
true
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'git/duet'
|
2
|
+
require 'git/duet/author_mapper'
|
3
|
+
require 'git/duet/command_methods'
|
4
|
+
|
5
|
+
class Git::Duet::DuetCommand
|
6
|
+
include Git::Duet::CommandMethods
|
7
|
+
|
8
|
+
def initialize(alpha, omega, quiet = false, global = false)
|
9
|
+
@alpha, @omega = alpha, omega
|
10
|
+
@quiet = !!quiet
|
11
|
+
@global = !!global
|
12
|
+
@author_mapper = Git::Duet::AuthorMapper.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute!
|
16
|
+
set_alpha_as_git_config_user
|
17
|
+
report_env_vars
|
18
|
+
write_env_vars
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
attr_accessor :alpha, :omega, :author_mapper
|
23
|
+
|
24
|
+
def set_alpha_as_git_config_user
|
25
|
+
exec_check("git config #{@global ? '--global ' : ''}user.name '#{alpha_info[:name]}'")
|
26
|
+
exec_check("git config #{@global ? '--global ' : ''}user.email '#{alpha_info[:email]}'")
|
27
|
+
end
|
28
|
+
|
29
|
+
def var_map
|
30
|
+
{
|
31
|
+
'GIT_AUTHOR_NAME' => alpha_info[:name],
|
32
|
+
'GIT_AUTHOR_EMAIL' => alpha_info[:email],
|
33
|
+
'GIT_COMMITTER_NAME' => omega_info[:name],
|
34
|
+
'GIT_COMMITTER_EMAIL' => omega_info[:email]
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def alpha_info
|
39
|
+
alpha_omega_info[@alpha]
|
40
|
+
end
|
41
|
+
|
42
|
+
def omega_info
|
43
|
+
alpha_omega_info[@omega]
|
44
|
+
end
|
45
|
+
|
46
|
+
def alpha_omega_info
|
47
|
+
@alpha_omega_info ||= author_mapper.map(@alpha, @omega)
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'git/duet'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'git/duet/command_methods'
|
4
|
+
|
5
|
+
class Git::Duet::InstallHookCommand
|
6
|
+
include Git::Duet::CommandMethods
|
7
|
+
|
8
|
+
HOOK = <<-EOF.gsub(/^ /, '')
|
9
|
+
#!/bin/bash
|
10
|
+
exec git duet-pre-commit "$@"
|
11
|
+
EOF
|
12
|
+
|
13
|
+
def initialize(quiet = false)
|
14
|
+
@quiet = quiet
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute!
|
18
|
+
Dir.chdir(`git rev-parse --show-toplevel`.chomp) do
|
19
|
+
dest = File.join(Dir.pwd, '.git', 'hooks', 'pre-commit')
|
20
|
+
if File.exist?(dest)
|
21
|
+
error("git-duet-install-hook: A pre-commit hook already exists at #{dest}!")
|
22
|
+
error("git-duet-install-hook: Move it out of the way first, mkay?")
|
23
|
+
return 1
|
24
|
+
end
|
25
|
+
File.open(dest, 'w') do |f|
|
26
|
+
f.puts HOOK
|
27
|
+
end
|
28
|
+
FileUtils.chmod(0755, dest)
|
29
|
+
info("git-duet-install-hook: Installed hook to #{dest}")
|
30
|
+
end
|
31
|
+
return 0
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'git/duet'
|
2
|
+
require 'git/duet/command_methods'
|
3
|
+
require 'git/duet/script_die_error'
|
4
|
+
|
5
|
+
class Git::Duet::PreCommitCommand
|
6
|
+
include Git::Duet::CommandMethods
|
7
|
+
|
8
|
+
def initialize(quiet = false)
|
9
|
+
@quiet = !!quiet
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute!
|
13
|
+
in_repo_root do
|
14
|
+
if !env_cache_exists? || env_cache_stale?
|
15
|
+
explode!
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
def explode!
|
22
|
+
error("Your git duet settings are stale, human!")
|
23
|
+
error("Update them with `git duet` or `git solo`.")
|
24
|
+
raise Git::Duet::ScriptDieError.new(1)
|
25
|
+
end
|
26
|
+
|
27
|
+
def env_cache_exists?
|
28
|
+
with_output_quieted do
|
29
|
+
exec_check('git config duet.env.mtime')
|
30
|
+
end
|
31
|
+
true
|
32
|
+
rescue
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def env_cache_stale?
|
37
|
+
Integer(exec_check('git config duet.env.mtime')) < stale_cutoff
|
38
|
+
end
|
39
|
+
|
40
|
+
def stale_cutoff
|
41
|
+
Integer(Time.now - Integer(ENV.fetch('GIT_DUET_SECONDS_AGO_STALE', '1200')))
|
42
|
+
end
|
43
|
+
end
|