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