git-duet 0.1.1

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