Skip to content

Commit bb39b87

Browse files
committed
Add forward merge issue creation hook
Closes gh-1486
1 parent 6e9ebb2 commit bb39b87

File tree

2 files changed

+207
-0
lines changed

2 files changed

+207
-0
lines changed

git/hooks/forward-merge

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/ruby
2+
require 'json'
3+
require 'net/http'
4+
require 'yaml'
5+
require 'logger'
6+
7+
$log = Logger.new(STDOUT)
8+
$log.level = Logger::WARN
9+
10+
class ForwardMerge
11+
attr_reader :issue, :milestone, :message, :line
12+
def initialize(issue, milestone, message, line)
13+
@issue = issue
14+
@milestone = milestone
15+
@message = message
16+
@line = line
17+
end
18+
end
19+
20+
def find_forward_merges(message_file)
21+
$log.debug "Searching for forward merge"
22+
rev=`git rev-parse -q --verify MERGE_HEAD`.strip
23+
$log.debug "Found #{rev} from git rev-parse"
24+
return nil unless rev
25+
message = File.read(message_file)
26+
forward_merges = []
27+
message.each_line do |line|
28+
$log.debug "Checking #{line} for message"
29+
match = /^(?:Fixes|Closes) gh-(\d+) in (\d\.\d\.[\dx](?:[\.\-](?:M|RC)\d)?)$/.match(line)
30+
if match then
31+
issue = match[1]
32+
milestone = match[2]
33+
$log.debug "Matched reference to issue #{issue} in milestone #{milestone}"
34+
forward_merges << ForwardMerge.new(issue, milestone, message, line)
35+
end
36+
end
37+
$log.debug "No match in merge message" unless forward_merges
38+
return forward_merges
39+
end
40+
41+
def get_issue(username, password, repository, number)
42+
$log.debug "Getting issue #{number} from GitHub repository #{repository}"
43+
uri = URI("https://api.github.com/repos/#{repository}/issues/#{number}")
44+
http = Net::HTTP.new(uri.host, uri.port)
45+
http.use_ssl=true
46+
request = Net::HTTP::Get.new(uri.path)
47+
request.basic_auth(username, password)
48+
response = http.request(request)
49+
$log.debug "Get HTTP response #{response.code}"
50+
return JSON.parse(response.body) unless response.code != '200'
51+
puts "Failed to retrieve issue #{number}: #{response.message}"
52+
exit 1
53+
end
54+
55+
def find_milestone(username, password, repository, title)
56+
$log.debug "Finding milestone #{title} from GitHub repository #{repository}"
57+
uri = URI("https://api.github.com/repos/#{repository}/milestones")
58+
http = Net::HTTP.new(uri.host, uri.port)
59+
http.use_ssl=true
60+
request = Net::HTTP::Get.new(uri.path)
61+
request.basic_auth(username, password)
62+
response = http.request(request)
63+
milestones = JSON.parse(response.body)
64+
if title.end_with?(".x")
65+
prefix = title.delete_suffix('.x')
66+
$log.debug "Finding nearest milestone from candidates starting with #{prefix}"
67+
titles = milestones.map { |milestone| milestone['title'] }
68+
titles = titles.select{ |title| title.start_with?(prefix) unless title.end_with?('.x') || (title.count('.') > 2)}
69+
titles = titles.sort_by { |v| Gem::Version.new(v) }
70+
$log.debug "Considering candidates #{titles}"
71+
if(titles.empty?)
72+
puts "Cannot find nearest milestone for prefix #{title}"
73+
exit 1
74+
end
75+
title = titles.first
76+
$log.debug "Found nearest milestone #{title}"
77+
end
78+
milestones.each do |milestone|
79+
$log.debug "Considering #{milestone['title']}"
80+
return milestone['number'] if milestone['title'] == title
81+
end
82+
puts "Milestone #{title} not found"
83+
exit 1
84+
end
85+
86+
def create_issue(username, password, repository, original, title, labels, milestone, milestone_name, dry_run)
87+
$log.debug "Finding forward-merge issue in GitHub repository #{repository} for '#{title}'"
88+
uri = URI("https://api.github.com/repos/#{repository}/issues")
89+
http = Net::HTTP.new(uri.host, uri.port)
90+
http.use_ssl=true
91+
request = Net::HTTP::Post.new(uri.path, 'Content-Type' => 'application/json')
92+
request.basic_auth(username, password)
93+
request.body = {
94+
title: title,
95+
labels: labels,
96+
milestone: milestone.to_i,
97+
body: "Forward port of issue ##{original} to #{milestone_name}."
98+
}.to_json
99+
if dry_run then
100+
puts "Dry run"
101+
puts "POSTing to #{uri} with body #{request.body}"
102+
return "dry-run"
103+
end
104+
response = JSON.parse(http.request(request).body)
105+
$log.debug "Created new issue #{response['number']}"
106+
return response['number']
107+
end
108+
109+
$log.debug "Running forward-merge hook script"
110+
message_file=ARGV[0]
111+
112+
forward_merges = find_forward_merges(message_file)
113+
exit 0 unless forward_merges
114+
115+
$log.debug "Loading config from ~/.spring/forward-merge.yml"
116+
config = YAML.load_file(File.join(Dir.home, '.spring', 'forward-merge.yml'))
117+
username = config['github']['credentials']['username']
118+
password = config['github']['credentials']['password']
119+
dry_run = config['dry_run']
120+
121+
repository = 'spring-projects/spring-ws'
122+
123+
forward_merges.each do |forward_merge|
124+
existing_issue = get_issue(username, password, repository, forward_merge.issue)
125+
title = existing_issue['title']
126+
labels = existing_issue['labels'].map { |label| label['name'] }
127+
labels << "status: forward-port"
128+
$log.debug "Processing issue '#{title}'"
129+
130+
milestone = find_milestone(username, password, repository, forward_merge.milestone)
131+
new_issue_number = create_issue(username, password, repository, forward_merge.issue, title, labels, milestone, forward_merge.milestone, dry_run)
132+
133+
puts "Created gh-#{new_issue_number} for forward port of gh-#{forward_merge.issue} into #{forward_merge.milestone}"
134+
rewritten_message = forward_merge.message.sub(forward_merge.line, "Closes gh-#{new_issue_number}\n")
135+
File.write(message_file, rewritten_message)
136+
end

git/hooks/prepare-forward-merge

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/ruby
2+
require 'json'
3+
require 'net/http'
4+
require 'yaml'
5+
require 'logger'
6+
7+
$main_branch = "4.1.x"
8+
9+
$log = Logger.new(STDOUT)
10+
$log.level = Logger::WARN
11+
12+
def get_fixed_issues()
13+
$log.debug "Searching for forward merge"
14+
rev=`git rev-parse -q --verify MERGE_HEAD`.strip
15+
$log.debug "Found #{rev} from git rev-parse"
16+
return nil unless rev
17+
fixed = []
18+
message = `git log -1 --pretty=%B #{rev}`
19+
message.each_line do |line|
20+
$log.debug "Checking #{line} for message"
21+
fixed << line.strip if /^(?:Fixes|Closes) gh-(\d+)/.match(line)
22+
end
23+
$log.debug "Found fixed issues #{fixed}"
24+
return fixed;
25+
end
26+
27+
def rewrite_message(message_file, fixed)
28+
current_branch = `git rev-parse --abbrev-ref HEAD`.strip
29+
if current_branch == "main"
30+
current_branch = $main_branch
31+
end
32+
rewritten_message = ""
33+
message = File.read(message_file)
34+
message.each_line do |line|
35+
match = /^Merge.*branch\ '(.*)'(?:\ into\ (.*))?$/.match(line)
36+
if match
37+
from_branch = match[1]
38+
if from_branch.include? "/"
39+
from_branch = from_branch.partition("/").last
40+
end
41+
to_branch = match[2]
42+
$log.debug "Rewriting merge message"
43+
line = "Merge branch '#{from_branch}'" + (to_branch ? " into #{to_branch}\n" : "\n")
44+
end
45+
if fixed and line.start_with?("#")
46+
$log.debug "Adding fixed"
47+
rewritten_message << "\n"
48+
fixed.each do |fixes|
49+
rewritten_message << "#{fixes} in #{current_branch}\n"
50+
end
51+
fixed = nil
52+
end
53+
rewritten_message << line
54+
end
55+
return rewritten_message
56+
end
57+
58+
$log.debug "Running prepare-forward-merge hook script"
59+
60+
message_file=ARGV[0]
61+
message_type=ARGV[1]
62+
63+
if message_type != "merge"
64+
$log.debug "Not a merge commit"
65+
exit 0;
66+
end
67+
68+
$log.debug "Searching for forward merge"
69+
fixed = get_fixed_issues()
70+
rewritten_message = rewrite_message(message_file, fixed)
71+
File.write(message_file, rewritten_message)

0 commit comments

Comments
 (0)