forked from timcharper/git-helpers
-
Notifications
You must be signed in to change notification settings - Fork 1
/
git-cleanup-branches
executable file
·165 lines (142 loc) · 4.4 KB
/
git-cleanup-branches
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env ruby
require "tempfile.rb"
require 'optparse'
DEFAULT_STABLE_BRANCH="master"
EDITOR=ENV['EDITOR'] || 'vim'
class GitCleanBranchesRunner
class BranchNotFound < RuntimeError; end
module CandidateState
module ConsiderAll
def candidate_instructions
"# The following is a list of all local and remote branches in your repository"
end
def branch_candidates
@branch_candidates ||= branches
end
end
module MergedOnly
def merged_into?(source, dest)
merged = `git rev-list origin/#{dest}..#{source} --`.strip.empty?
raise BranchNotFound, "Error resolving refs. Most likely destination branch 'origin/#{dest}' does not exist." unless $? == 0
merged
end
def branch_candidates
@branch_candidates ||= (
candidates = []
branches.each do |branch|
next if %r(/(#{preserve_always * '|'})$) =~ branch
next unless merged_into?(branch, stable_branch)
candidates << branch
end
candidates
)
end
def candidate_instructions
"# The following branches have been merged in to refs/remotes/origin/#{stable_branch}"
end
end
end
attr_reader :stable_branch
def initialize(args, output = STDOUT, error = STDERR)
@output, @error = output, error
extend CandidateState::MergedOnly
@stable_branch = %x{git config cleanup.stable || echo #{DEFAULT_STABLE_BRANCH}}.chomp
opts = OptionParser.new
opts.banner = "Usage: #{$0} [options]"
opts.separator "Options:"
opts.on("-a", "--all", "Consider all branches for deletion, regardless if they've been merged into stable") do
extend CandidateState::ConsiderAll
end
opts.on("-b", "--branch [branch]", "The stable branch (default: #{@stable_branch}). If a branch is merged into this, and -a parameter not specified, consider it for deletion.") do |b|
@stable_branch = b
end
opts.on("-f", "--fast", "Fast cleanup - Skip fetch/prune remotes") do
@fast = true
end
opts.parse!(args)
end
def preserve_always
[stable_branch] + %w[staging master next HEAD]
end
def fetch_and_prune
puts "Fetching and pruning all remotes"
`git remote`.each do |remote|
system("git fetch #{remote}")
system("git remote prune #{remote}")
end
end
def branches
%x(git for-each-ref).map do |branch_name|
next unless %r{.+(refs/(heads|remotes).+$)}.match(branch_name)
$1
end.compact
end
def candidate_instructions
raise NotImplemented
end
def branch_candidates
raise NotImplemented
end
def edit_branch_list
file = Tempfile.new("cleanup-branch")
file.puts candidate_instructions
file.puts "# To delete the branches, delete them from this list, and then save and quit"
file.puts(branch_candidates * "\n")
file.close
system("#{EDITOR} #{file.path}")
preserve_branches = File.read(file.path).split("\n").grep(/^[^#].+/)
file.delete
unless (erroneous_branches = (preserve_branches - branch_candidates)).empty?
puts <<EOF
Error! unrecognized branches:
#{erroneous_branches.map{|b| " - #{b}"} * "\n"}
EOF
exit 1
end
preserve_branches
end
def sort_branches(branches)
sorted_branches = {}
branches.each do |branch|
case branch
when %r(^refs/remotes/([^/]+)/(.+)$)
remote, branch = $1, $2
sorted_branches[remote] ||= []
sorted_branches[remote] << branch
when %r(^refs/heads/(.+)$)
sorted_branches[nil] ||= []
sorted_branches[nil] << $1
else
puts "I don't know how to delete #{branch_for_deletion}"
end
end
sorted_branches
end
def delete_branches(branches_for_deletion)
sorted_branches = sort_branches(branches_for_deletion)
if sorted_branches.empty?
puts "No branches to delete."
exit 1
end
puts <<EOF
Deleting branches:
#{branches_for_deletion.map{|b| " - #{b}"} * "\n"}
EOF
sorted_branches.each do |remote, branches|
if remote.nil?
system(*%w[git branch -D] + branches)
else
system(*%w[git push] + [remote] + branches.map { |b| ":#{b}" })
end
end
end
def run
fetch_and_prune unless @fast
preserve_branches = edit_branch_list
delete_branches(branch_candidates - preserve_branches)
rescue BranchNotFound => e
puts e
exit 1
end
end
GitCleanBranchesRunner.new(ARGV).run