git_tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d0e458481a11751c30773053821dc103dcb20066
4
+ data.tar.gz: ad109373d0c651babb912fa2a97ff8f247ad9ad8
5
+ SHA512:
6
+ metadata.gz: b2c2ef68608acf414c38b9acf868c87afa209743c34fc98b87aec3c35a948a398233948132f7957ac2bd272c3362e98f872a85a7b6cab8e2841062c101a89a7b
7
+ data.tar.gz: 0e4c13416a9498544dcd3e10f8711cc00b8953743ed8da487df4c4a05ffba1bb797d0d8730b6882c86d8263a091a8df03b6821cead0bce96dcfdaed99cbc31db
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ *.gem
3
+ pkg/
4
+ *.swp
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ git_tools
2
+ =========
3
+
4
+ Additional git commands and rake tasks for various git related goodness.
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ require 'rake'
2
+
3
+ namespace :gem do
4
+ desc 'Build the gem'
5
+ task :build do
6
+ `mkdir -p pkg`
7
+ `gem build *.gemspec`
8
+ `mv *.gem pkg/`
9
+ end
10
+
11
+ desc 'Publish the gem'
12
+ task :publish do
13
+ gem = `ls pkg | sort | tail -n 1`
14
+ exec("gem push pkg/#{gem}")
15
+ end
16
+
17
+ desc 'Install the gem locally'
18
+ task :install do
19
+ gem = `ls pkg`.split.sort
20
+ `gem install pkg/#{gem.last}`
21
+ end
22
+ end
23
+
24
+ desc 'Remove generated files'
25
+ task :clean do
26
+ `rm -rf pkg`
27
+ end
data/git_tools.gemspec ADDED
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "git_tools"
6
+ s.version = '0.1.0'
7
+ s.authors = ["Birkir A. Barkarson"]
8
+ s.email = ["birkirb@stoicviking.net"]
9
+ s.licenses = ['MIT']
10
+ s.homepage = "https://github.com/birkirb/git_tools"
11
+ s.summary = %q{Collection of various handy git commands and tasks.}
12
+ s.description = %q{Git tools for installing hooks, cleaning up branches and more.}
13
+
14
+ s.rubyforge_project = "git_tools"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+ end
@@ -0,0 +1,237 @@
1
+ require 'git_tools/extensions/time'
2
+
3
+ module GitTools
4
+ module Branches
5
+
6
+ KEEP_LIST = %w(master develop development testing stable release production)
7
+ KEEP_BRANCH_FILENAME = File.join(CUSTOM_DIR, 'keep_branches')
8
+
9
+ def self.git_keep_branches_from_file
10
+ if File.exists?(KEEP_BRANCH_FILENAME)
11
+ File.readlines(KEEP_BRANCH_FILENAME).each { |line| line.strip! }
12
+ else
13
+ []
14
+ end
15
+ end
16
+
17
+ def self.default_keep_list
18
+ KEEP_LIST + git_keep_branches_from_file
19
+ end
20
+
21
+ class Cleaner
22
+ MASTER_BRANCH = 'master'
23
+ DEFAULT_REMOTE = 'origin'
24
+ AGE_THRESHOLD_IN_MONTHS_FOR_DELETING_REMOTE_BRANCHES_IN_MASTER = 1 * Time::SECONDS_IN_MONTH
25
+ AGE_THRESHOLD_IN_MONTHS_FOR_DELETING_ANY_UNMERGED_BRANCHES = 6 * Time::SECONDS_IN_MONTH
26
+
27
+ attr_reader :master_branch, :remote, :protected_branches
28
+
29
+ def self.with_local(master_branch, protected_branches = nil)
30
+ self.new(nil, master_branch, protected_branches)
31
+ end
32
+
33
+ def self.with_origin(protected_branches = nil)
34
+ self.new(DEFAULT_REMOTE, nil, protected_branches)
35
+ end
36
+
37
+ public
38
+
39
+ def initialize(remote = nil, master_branch = nil, protected_branches = nil)
40
+ @remote = remote
41
+ @protected_branches = protected_branches || Branches.default_keep_list
42
+ @master_branch = master_branch || get_remote_head || MASTER_BRANCH
43
+ @branches = branches
44
+
45
+ if @master_branch.nil?
46
+ raise "Master branch was not set or determined."
47
+ else
48
+ puts "Master branch is #{@master_branch}" if $VERBOSE
49
+ end
50
+ end
51
+
52
+ def local?
53
+ @remote.nil?
54
+ end
55
+
56
+ def run!
57
+ (@branches - protected_branches - [master_branch] ).each do |branch|
58
+ branch = Branch.new(branch, remote)
59
+ containing_branches = contained_branches(branch.normalized_name) - [branch.name]
60
+
61
+ if protected_branch?(branch.name)
62
+ puts "#{label_for_remote} branch [#{branch}] is on keep list ( #{kbs.join(" , ")} )" if $VERBOSE
63
+ elsif in_master_branch?(containing_branches)
64
+ if delete_branch_merged_to_master_without_confirmation?(branch.age)
65
+ message = "Removing #{label_for_remote.downcase} branch [#{branch}] since it is in #{master_branch}. [#{branch.age.relative}]"
66
+ branch.remove!(message)
67
+ else
68
+ message = "#{label_for_remote} branch [#{branch}] is in #{master_branch} and could be deleted [#{branch.age.relative}]"
69
+ branch.confirm_remove(message, "Delete #{branch}?")
70
+ end
71
+ elsif delete_unmerged_branch?(branch.age)
72
+ branch_list = containing_branches.empty? ? '' : "Branch has been merged into:\n #{containing_branches.join("\n ")}"
73
+ message = "#{label_for_remote} branch [#{branch}] is not on #{master_branch}, but old [#{branch.age.relative}]. #{branch_list}"
74
+ branch.confirm_remove(message, "Delete old unmerged branch: #{branch} ?")
75
+ else
76
+ puts "Ignoring unmerged #{label_for_remote.downcase} branch [#{branch}]" if $VERBOSE
77
+ end
78
+
79
+ if defined?($signal_handler)
80
+ break if $signal_handler.interrupted?
81
+ end
82
+ end
83
+
84
+ git_remote_prune
85
+ end
86
+
87
+ private
88
+
89
+ def git_remote_prune
90
+ `git remote prune #{remote}` unless local?
91
+ end
92
+
93
+ def label_for_remote
94
+ local? ? "Local" : "Remote"
95
+ end
96
+
97
+ def branches
98
+ clean_branches_result(`git branch #{git_argument_for_remote}`)
99
+ end
100
+
101
+ def git_argument_for_remote
102
+ local? ? '' : ' -r'
103
+ end
104
+
105
+ def get_remote_head
106
+ (`git branch -r | grep #{remote}/HEAD`).sub(/#{remote}\/HEAD -> #{remote}\//, '').strip
107
+ end
108
+
109
+ def clean_branches_result(branches)
110
+ bs = branches.to_s.split(/\n/).map { |b| b.strip.sub(/^\s*\*\s*/, '') }
111
+
112
+ if local?
113
+ bs
114
+ else
115
+ # technically split out remote branch name (may not be origin) and return as list
116
+ bs.delete_if { |b| b =~ /HEAD/ }
117
+ bs.find_all { |b| /^#{remote}\// =~ b }.map { |b| b.sub(/^#{remote}\//, '') }
118
+ end
119
+ end
120
+
121
+ def contained_branches(branch)
122
+ # git's --contains param seems to cause this stderr out:
123
+ # error: branch 'origin/HEAD' does not point at a commit
124
+ # piping that to stderr and cleaning out.
125
+ clean_branches_result(`git branch #{git_argument_for_remote} --contains #{branch} 2>&1`)
126
+ end
127
+
128
+ def in_master_branch?(branch)
129
+ branch.include?(master_branch)
130
+ end
131
+
132
+ def protected_branch?(branch)
133
+ protected_branches.include?(branch)
134
+ end
135
+
136
+ def delete_branch_merged_to_master_without_confirmation?(time)
137
+ if local?
138
+ true
139
+ else
140
+ (Time.now - time) > AGE_THRESHOLD_IN_MONTHS_FOR_DELETING_REMOTE_BRANCHES_IN_MASTER
141
+ end
142
+ end
143
+
144
+ def delete_unmerged_branch?(time)
145
+ (Time.now - time) > AGE_THRESHOLD_IN_MONTHS_FOR_DELETING_ANY_UNMERGED_BRANCHES
146
+ end
147
+
148
+ end
149
+
150
+ class Branch
151
+
152
+ DATE_REGEXP = /^Date:\s+(.*)$/
153
+
154
+ def self.age(branch)
155
+ time = DATE_REGEXP.match(`git log --shortstat --date=iso -n 1 #{branch}`)
156
+ Time.parse(time[1])
157
+ end
158
+
159
+ def self.executor
160
+ ActionExecutor.new
161
+ end
162
+
163
+ public
164
+
165
+ attr_reader :name, :remote, :age
166
+
167
+ def initialize(name, remote)
168
+ @name = name
169
+ @remote = remote
170
+ @age = self.class.age(normalized_name)
171
+ end
172
+
173
+ def normalized_name
174
+ local? ? name : "#{remote}/#{name}"
175
+ end
176
+
177
+ def remove!(message)
178
+ self.class.executor.execute(remove_branch_action, message)
179
+ end
180
+
181
+ def confirm_remove(message, prompt)
182
+ self.class.executor.execute(remove_branch_action, message, prompt)
183
+ end
184
+
185
+ def to_s
186
+ name
187
+ end
188
+
189
+ private
190
+
191
+ def local?
192
+ remote.nil?
193
+ end
194
+
195
+ def remove_branch_action
196
+ local? ? "git branch -d #{name}" : "git push #{remote} :#{name}"
197
+ end
198
+ end
199
+
200
+ class ActionExecutor
201
+
202
+ @@test_mode = true
203
+ @@skip_prompted = false
204
+
205
+ def self.test_mode=(value)
206
+ @@test_mode = (value == true)
207
+ end
208
+
209
+ def self.skip_prompted=(value)
210
+ @@skip_prompted = (value == true)
211
+ end
212
+
213
+ def execute(command, action_message, confirmation_prompt = nil)
214
+ if @@test_mode
215
+ $stderr.puts("#{action_message} -> #{command}")
216
+ else
217
+ if confirmation_prompt
218
+ if @@skip_prompted
219
+ puts "#{action_message} -> skipping prompts" if $VERBOSE
220
+ else
221
+ puts action_message
222
+ puts "#{confirmation_prompt} [y/N]"
223
+ case $stdin.gets.chomp
224
+ when 'y'
225
+ `#{command}`
226
+ end
227
+ end
228
+ else
229
+ puts action_message
230
+ `#{command}`
231
+ end
232
+ end
233
+ end
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,8 @@
1
+ class String
2
+ def multi_gsub!(options = {})
3
+ options.each do |key, value|
4
+ self.gsub!("{#{key}}", value.to_s)
5
+ end
6
+ self
7
+ end
8
+ end
@@ -0,0 +1,50 @@
1
+ require 'time'
2
+ require 'git_tools/extensions/string'
3
+
4
+ class Time
5
+
6
+ DAYS_IN_YEAR = 365.2424
7
+ SECONDS_IN_MINUTE = 60
8
+ SECONDS_IN_HOUR = 3_600
9
+ SECONDS_IN_DAY = 86_400
10
+ SECONDS_IN_WEEK = 604_800
11
+ SECONDS_IN_YEAR = 31_556_736 # AVERAGE
12
+ SECONDS_IN_MONTH = SECONDS_IN_YEAR/12 # AVERAGE
13
+
14
+ HUMAN_TIMES = [
15
+ [SECONDS_IN_MINUTE, 120, "minutes"],
16
+ [SECONDS_IN_HOUR , 72, "hours"],
17
+ [SECONDS_IN_DAY , 21, "days"],
18
+ [SECONDS_IN_WEEK , 12, "weeks"],
19
+ [SECONDS_IN_MONTH , 12, "months"]
20
+ #[SECONDS_IN_YEAR , 10, "years"]
21
+ ]
22
+
23
+ def relative(t0 = Time.now)
24
+ dt = self - t0
25
+
26
+ if dt < 0
27
+ tense = 'ago'
28
+ dt = dt.abs - 1
29
+ else
30
+ tense = 'from now'
31
+ end
32
+
33
+ if dt < SECONDS_IN_MINUTE
34
+ return 'now'.t
35
+ else
36
+ HUMAN_TIMES.each do |time|
37
+ seconds = time[0]
38
+ limit = time[1]
39
+ time_unit = time[2]
40
+
41
+ if dt < seconds * limit
42
+ return "{time} #{time_unit} #{tense}".multi_gsub!(:time => (dt/seconds).ceil.to_i)
43
+ end
44
+ end
45
+ # Above the higest limit
46
+ "over a year #{tense}".t
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,33 @@
1
+ #!/env/ruby
2
+
3
+ FAILURE = false
4
+
5
+ diff_lines = `git diff`.split("\n")
6
+ added_lines = diff_lines.select { |line| line =~ /^\+/ }
7
+
8
+ conflict_markers = ['<<<<<<<', '=======', '>>>>>>>']
9
+ debug_keywords = ['puts', 'debugger']
10
+
11
+ conflict_lines = added_lines.select do |line|
12
+ conflict_markers.any? { |marker| line =~ /#{marker}/ }
13
+ end
14
+ debug_lines = added_lines.select do |line|
15
+ debug_keywords.any? { |keyword| line =~ /\b#{keyword}\b/ }
16
+ end
17
+
18
+ $stdin.readlines.each { |l| puts l }
19
+
20
+ if conflict_lines.any?
21
+ puts "You might be committing changes containing conflict markers, this indicates an unfinished merge"
22
+ puts "The lines are the following:"
23
+ conflict_lines.each { |line| puts line }
24
+ end
25
+ if debug_lines.any?
26
+ puts "You might be committing changes containing debug keywords"
27
+ puts "The lines are the following:"
28
+ debug_lines.each { |line| puts line }
29
+ end
30
+ if conflict_lines.any? || debug_lines.any?
31
+ puts "If you are sure you want to make this commit, commit again with the --no-verify flag"
32
+ exit(FAILURE)
33
+ end
@@ -0,0 +1,51 @@
1
+ #!/env/ruby
2
+ require 'erb'
3
+
4
+ message_file = ARGV[0]
5
+ commit_source = ARGV[1]
6
+ # sha1 = ARGV[2]
7
+
8
+ exit if ['merge', 'commit', 'message'].include? commit_source
9
+
10
+ working_dir = File.expand_path(File.join(File.dirname(message_file), '..'))
11
+ template_file = File.join(working_dir, '.git_tools', 'templates', 'prepare_commit_msg')
12
+ contents = File.read(message_file)
13
+
14
+ has_merge_message = contents.match(/^Merge branch/m) # Commit with resolved merge conflicts don't have a commit source.
15
+ exit if has_merge_message
16
+ ticket_no_from_branch_name = contents.scan(/On branch .+#(\d{1,5})/)
17
+
18
+ if ticket_no_from_branch_name.empty?
19
+ references = nil
20
+ default_line = <<-MESSAGE
21
+ #
22
+ # PLEASE ADD THE ISSUE TRACKER NUMBER TO THE BRANCH NAME.
23
+ # Replace these lines with any applicable issue tracker references.
24
+ MESSAGE
25
+ else
26
+ references = ticket_no_from_branch_name.join(', #')
27
+ default_line = "Issue tracker: ##{references}"
28
+ end
29
+
30
+ if File.exists?(template_file)
31
+ message_template = ERB.new(File.read(template_file), 0, '>').result
32
+ else
33
+ message_template = <<-MESSAGE
34
+
35
+ # [Tell us *why* are you committing on the line above.]
36
+
37
+ # [Details below this line.]
38
+ #{default_line}
39
+ #
40
+ # Check out the following links on how to write proper git messges:
41
+ #
42
+ # http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
43
+ # http://robots.thoughtbot.com/post/48933156625/5-useful-tips-for-a-better-commit-message
44
+ #
45
+ MESSAGE
46
+ end
47
+
48
+ File.open(message_file, 'w') do |file|
49
+ file.write(message_template.chomp)
50
+ file.write(contents)
51
+ end
@@ -0,0 +1,79 @@
1
+ module GitTools
2
+ module Hooks
3
+ GIT_HOOK_INSTALL_LINE_BEGIN = "# BEGIN Ruby git-hooks\n"
4
+ GIT_HOOK_INSTALL_LINE_END = "# END Ruby git-hooks\n"
5
+ GIT_HOOK_DIR = File.join('.git', 'hooks')
6
+ GIT_TOOLS_CUSTOM_HOOKS_DIR = File.join(CUSTOM_DIR, 'hooks')
7
+ GIT_TOOLS_INCLUDED_HOOKS_DIR = File.join(File.dirname(__FILE__), 'hooks')
8
+
9
+ def self.with_git_hook_files
10
+ if File.exist?(GIT_HOOK_DIR)
11
+ default_ruby_hooks.merge(custom_ruby_hooks).each do |dir, files|
12
+ files.each do |file|
13
+ git_hook = File.join(GIT_HOOK_DIR, file)
14
+ yield(dir, file, git_hook)
15
+ end
16
+ end
17
+ else
18
+ puts "Git hook directory not found."
19
+ end
20
+ end
21
+
22
+ def self.default_ruby_hooks
23
+ {GIT_TOOLS_INCLUDED_HOOKS_DIR => (Dir.entries(GIT_TOOLS_INCLUDED_HOOKS_DIR) - ['.', '..'])}
24
+ end
25
+
26
+ def self.custom_ruby_hooks
27
+ if Dir.exists?(GIT_TOOLS_CUSTOM_HOOKS_DIR)
28
+ {GIT_TOOLS_CUSTOM_HOOKS_DIR => (Dir.entries(GIT_TOOLS_CUSTOM_HOOKS_DIR) - ['.', '..'])}
29
+ else
30
+ {}
31
+ end
32
+ end
33
+
34
+ def self.clear_git_hooks
35
+ with_git_hook_files do |dir, ruby_hook, git_hook|
36
+ if File.exists?(git_hook)
37
+ hook_content = File.read(git_hook)
38
+ if hook_content.match(/#{GIT_HOOK_INSTALL_LINE_BEGIN}/)
39
+ puts "Clearing Ruby #{ruby_hook} git-hooks."
40
+ hook_content.gsub!(/#{GIT_HOOK_INSTALL_LINE_BEGIN}.*#{GIT_HOOK_INSTALL_LINE_END}/m, '')
41
+ File.open(git_hook, 'w+') do |file|
42
+ file.write(hook_content)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.install_git_hooks
50
+ with_git_hook_files do |ruby_hook_dir, ruby_hook_file, git_hook|
51
+ if File.exists?(git_hook)
52
+ hook_content = File.read(git_hook)
53
+ else
54
+ hook_content = "#!/bin/sh\n\n"
55
+ end
56
+
57
+ if hook_content.match(/#{GIT_HOOK_INSTALL_LINE_BEGIN}/)
58
+ next
59
+ else
60
+ puts "Installing Ruby #{ruby_hook_file} git-hooks."
61
+ hook_commands = ''
62
+ hook_files = File.join(ruby_hook_dir, ruby_hook_file)
63
+ puts "Hook file: #{hook_files}" if $VERBOSE
64
+ Dir.foreach(hook_files) do |file_path|
65
+ if file_path.match(/\.rb$/)
66
+ hook_commands += "if [ $? -eq 0 ]; then ruby #{File.join(ruby_hook_dir, ruby_hook_file, file_path)} \"$@\"; else exit 1; fi\n"
67
+ end
68
+ end
69
+ hook_content += "#{GIT_HOOK_INSTALL_LINE_BEGIN}\n#{hook_commands}\n#{GIT_HOOK_INSTALL_LINE_END}"
70
+ File.open(git_hook, 'w+') do |file|
71
+ file.write(hook_content)
72
+ end
73
+ FileUtils.chmod(0744, git_hook)
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
data/lib/git_tools.rb ADDED
@@ -0,0 +1,8 @@
1
+ module GitTools
2
+ CUSTOM_DIR = '.git_tools'
3
+ end
4
+
5
+ if defined?(Rake)
6
+ load 'tasks/git_tools/hooks.rake'
7
+ load 'tasks/git_tools/branches.rake'
8
+ end
@@ -0,0 +1,30 @@
1
+ require 'git_tools/branches/cleaner'
2
+
3
+ namespace :git do
4
+ namespace :branches do
5
+ # puts $VERBOSE, $-V, $DEBUG, $-W
6
+ # VERBOSE and DEBUG mode don't seem to be triggered in rake tasks even with RUBYOPTS.
7
+ # Something turning it off?
8
+
9
+ GitTools::Branches::ActionExecutor.test_mode = $DEBUG
10
+ GitTools::Branches::ActionExecutor.skip_prompted = true
11
+
12
+ desc 'Clean up all branches'
13
+ task(:clean => ['clean:local', 'clean:remote']) do
14
+ end
15
+
16
+ namespace :clean do
17
+ desc 'Clean up local branches'
18
+ task :local do
19
+ GitTools::Branches::Cleaner.with_local('stable').run!
20
+ end
21
+
22
+ desc 'Clean up remote branches'
23
+ task :remote do
24
+ # TODO: Handle non origin
25
+ GitTools::Branches::Cleaner.with_origin.run!
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ require 'git_tools/hooks'
2
+
3
+ namespace :git do
4
+ namespace :hooks do
5
+ desc "Clear any installed git-hooks"
6
+ task :clear do
7
+ GitTools::Hooks.clear_git_hooks
8
+ end
9
+
10
+ desc "Install all git-hooks"
11
+ task :install do
12
+ GitTools::Hooks.install_git_hooks
13
+ end
14
+
15
+ desc "Re-install all git-hooks"
16
+ task :reinstall do
17
+ GitTools::Hooks.clear_git_hooks
18
+ GitTools::Hooks.install_git_hooks
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: git_tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Birkir A. Barkarson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-01-14 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Git tools for installing hooks, cleaning up branches and more.
14
+ email:
15
+ - birkirb@stoicviking.net
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - .gitignore
21
+ - README.md
22
+ - Rakefile
23
+ - git_tools.gemspec
24
+ - lib/git_tools.rb
25
+ - lib/git_tools/branches/cleaner.rb
26
+ - lib/git_tools/extensions/string.rb
27
+ - lib/git_tools/extensions/time.rb
28
+ - lib/git_tools/hooks.rb
29
+ - lib/git_tools/hooks/pre-commit/01_check_keywords_and_conflicts.rb
30
+ - lib/git_tools/hooks/prepare-commit-msg/01_capture_issue_tracker_number_from_branch_name.rb
31
+ - lib/tasks/git_tools/branches.rake
32
+ - lib/tasks/git_tools/hooks.rake
33
+ homepage: https://github.com/birkirb/git_tools
34
+ licenses:
35
+ - MIT
36
+ metadata: {}
37
+ post_install_message:
38
+ rdoc_options: []
39
+ require_paths:
40
+ - lib
41
+ required_ruby_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ required_rubygems_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - '>='
49
+ - !ruby/object:Gem::Version
50
+ version: '0'
51
+ requirements: []
52
+ rubyforge_project: git_tools
53
+ rubygems_version: 2.0.6
54
+ signing_key:
55
+ specification_version: 4
56
+ summary: Collection of various handy git commands and tasks.
57
+ test_files: []