/
suse-changelog-merge
executable file
·213 lines (187 loc) · 5.43 KB
/
suse-changelog-merge
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#! /usr/bin/env ruby
# SUSE .changes merge tool for Git
# license: http://en.wikipedia.org/wiki/MIT_License
require "date"
require "optparse"
require "time"
class Change
# what is being changed (usu. String)
attr_accessor :object
# who changed it (string, email)
attr_accessor :author
# when it changed. datetime, or date
attr_accessor :timestamp
# to preserve order of items with the same timestamp
attr_accessor :lineno
# how it changed, the message, single string (including trailing \n)
attr_accessor :description
def to_s
"----\n" + "#{object} @#{timestamp} #{author}\n" + description.to_s
end
end
class SUSEChange < Change
HEADER = "-" * 67
HEADER_NEWLINE = HEADER + "\n"
# Preserve the textual format of the timestamp
# in case we need to exactly reproduce the original changelog.
# Sample differences: "Feb 1" vs "Feb 01", wrong weekday.
attr_accessor :timestamp_text
def timestamp_s(options)
if options[:timestamp] == :original # or :reparsed
timestamp_text
else
# `vc` calls `date` and this is the default timestamp format:
# https://www.gnu.org/software/coreutils/manual/html_node/date-invocation.html
timestamp.strftime '%a %b %e %H:%M:%S %Z %Y'
end
end
def to_s(options = {})
HEADER_NEWLINE +
"#{timestamp_s(options)} - #{author}\n" +
description
end
end
class SUSEChangeLog
# [Array<SUSEChange>]
attr_accessor :items
# [Bool], default true
attr_accessor :timestamps_verbatim
def self.new_from_filename(filename)
cl = self.new
File.open(filename) do |f|
cl.parse_io(f)
end
cl
end
def initialize
@items = []
@timestamps_verbatim = true
end
def parse_io(io)
parse_io_yield(io) do |item|
items << item
end
end
def parse_io_yield(io)
# parses an IO object
# yields each Change
item = nil
expect = :header
io.each_line do |line|
case expect
when :header
if line != SUSEChange::HEADER_NEWLINE
raise "Garbage before 1st header: #{line}"
end
expect = :timestamp_author
when :timestamp_author
if line =~ /^([^-]*) - (.*)/
item = SUSEChange.new
item.lineno = io.lineno
item.description = ""
item.timestamp_text = $1
item.timestamp = Time.parse($1)
item.author = $2
expect = :description
else
raise "Not in 'TIMESTAMP - AUTHOR' format: #{line}"
end
when :description
if line == SUSEChange::HEADER_NEWLINE
yield item
expect = :timestamp_author
else
item.description += line
end
end
end
yield item unless item.nil?
end
# ugh, pattern for sending to IO instead?
def to_s
s = ""
items.each do |i|
s << i.to_s(:timestamp => (timestamps_verbatim ? :original : :reparsed))
end
s
end
def +(other)
result = self.dup # copy timestamps_verbatim
result.items = self.items + other.items
result
end
# Subtract changelogs.
def -(other)
# assume self has some added entries at beginning
result = self.dup
# DIRTY: do it by length, don't compare the actual contents
result.items = self.items.slice(0, self.items.size - other.items.size)
result
end
# Are the timestamps in reverse chronological order?
# (Duplicate timestamps are OK)
def monotonic?
prev_time = nil # argh name
items.each do |i|
if prev_time and prev_time < i.timestamp
return false
end
prev_time = i.timestamp
end
true
end
end
# $verbose = false
$output = nil # to become CURRENT
marker_size = 7
opts = OptionParser.new "Usage: #{$0} [options] CURRENT COMMON OTHER"
# opts.on("-v", "--verbose", "Run verbosely") {|v| $verbose = v }
opts.on("-o", "--output FILE", "Send the result to FILE instead of to CURRENT") {|v| $output = v }
opts.on("-p", "--print", "Send the result to stdout instead of to CURRENT") {|v| $output = "/dev/stdout" }
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
args = opts.parse(ARGV)
if args.size == 1
cl = SUSEChangeLog.new_from_filename args[0]
cl.timestamps_verbatim = false
print cl
elsif args.size == 3
# TODO --merge, like merge(1), mimic its interface
# "The merge driver is expected to leave the result of the merge in the file
# named with %A [current] by overwriting it, and exit with zero status if it
# managed to merge them cleanly, or non-zero if there were conflicts."
current = SUSEChangeLog.new_from_filename args[0]
common = SUSEChangeLog.new_from_filename args[1]
other = SUSEChangeLog.new_from_filename args[2]
status = 0
# puts common.items.last
# p current.items.last
# p common.items.last.to_s == current.items.last.to_s
# TODO one or both(?) branches may have no diff, or same(?) diff
# harcoding the reverse-chronological order is OK, all changelogs have that?
added = other - common
merged = added + current
File.open($output || args[0], "w") do |f|
if merged.monotonic?
f.write(merged)
else
$stderr.puts "#{$0}: not monotonic"
status = 1 # signal merge failure
f.puts "<" * marker_size
f.puts "=" * marker_size
f.write(added)
f.puts ">" * marker_size
f.write(current)
end
end
# TODO unless -q --quiet
if status == 1
$stderr.puts "#{$0}: warning: conflicts during merge"
end
exit status
else
puts opts
exit 2
end