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.
@@ -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,3 @@
1
+ unless defined?(KeyError)
2
+ KeyError = Class.new(IndexError)
3
+ 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
@@ -0,0 +1,11 @@
1
+ module Git
2
+ module Duet
3
+ class ScriptDieError < StandardError
4
+ attr_reader :exit_code
5
+
6
+ def initialize(exit_code)
7
+ @exit_code = exit_code
8
+ end
9
+ end
10
+ end
11
+ end