git_tools 0.1.0

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.
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: []