step-up 0.1.0 → 0.2.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.
- data/.stepuprc +31 -0
- data/README.md +31 -2
- data/Rakefile +1 -1
- data/lib/step-up.rb +1 -0
- data/lib/step-up/cli.rb +253 -8
- data/lib/step-up/config.rb +67 -2
- data/lib/step-up/config/step-up.yml +17 -5
- data/lib/step-up/driver/git.rb +65 -38
- data/lib/step-up/git_extensions.rb +32 -82
- data/lib/step-up/parser/version_mask.rb +18 -9
- data/lib/step-up/ranged_notes.rb +166 -0
- data/lib/step-up/version.rb +4 -26
- data/spec/lib/config_spec.rb +46 -0
- data/spec/lib/step-up/driver/git_spec.rb +82 -74
- data/spec/lib/step-up/parser/version_mask_spec.rb +27 -3
- data/spec/lib/step-up/ranged_notes_spec.rb +140 -0
- metadata +10 -4
data/lib/step-up/driver/git.rb
CHANGED
@@ -1,26 +1,33 @@
|
|
1
1
|
module StepUp
|
2
2
|
module Driver
|
3
3
|
class Git
|
4
|
+
VERSION_MESSAGE_FILE_PATH = ".git/TAG_EDITMSG"
|
5
|
+
|
4
6
|
include GitExtensions::Notes
|
5
7
|
attr_reader :mask
|
6
8
|
def initialize
|
7
|
-
@mask = Parser::VersionMask.new(CONFIG
|
9
|
+
@mask = Parser::VersionMask.new(CONFIG.versioning.version_mask)
|
8
10
|
end
|
9
11
|
|
10
|
-
def self.last_version
|
11
|
-
|
12
|
-
@driver.last_version_tag(commit_base) || "%s%s" % [@driver.mask.blank, '+']
|
12
|
+
def self.last_version
|
13
|
+
new.last_version_tag
|
13
14
|
end
|
14
15
|
|
15
|
-
def
|
16
|
-
options =
|
17
|
-
|
18
|
-
|
16
|
+
def commit_history(commit_base, *args)
|
17
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
18
|
+
top = args.shift
|
19
|
+
top = "-n#{ top }" unless top.nil?
|
20
|
+
commits = `git log --pretty=oneline --no-color --no-notes #{ top } #{ commit_base }`
|
21
|
+
if options[:with_messages]
|
22
|
+
commits.split(/\n/).map{ |commit| commit =~ /^(\w+)\s+(.*)$/ ? [$1, $2] : nil }
|
23
|
+
else
|
24
|
+
commits.gsub(/^(\w+)\s.*$/, '\1').split(/\n/)
|
25
|
+
end
|
19
26
|
end
|
20
27
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
28
|
+
def commits_between(first_commit, last_commit = "HEAD", *args)
|
29
|
+
commit_base = first_commit.nil? ? last_commit : "#{ first_commit }..#{ last_commit }"
|
30
|
+
commit_history(commit_base, *args)
|
24
31
|
end
|
25
32
|
|
26
33
|
def all_tags
|
@@ -31,21 +38,6 @@ module StepUp
|
|
31
38
|
`git notes --ref=#{ ref } list`.gsub(/^\w+\s(\w+)$/, '\1').split(/\n/)
|
32
39
|
end
|
33
40
|
|
34
|
-
def all_objects_with_notes(commit_base = nil)
|
35
|
-
objects = commit_history(commit_base)
|
36
|
-
objects_with_notes = {}.extend GitExtensions::NotesTransformation
|
37
|
-
objects_with_notes.driver = self
|
38
|
-
notes_sections.each do |section|
|
39
|
-
obj = objects_with_notes_of(section)
|
40
|
-
obj = obj.collect { |object|
|
41
|
-
pos = objects.index(object)
|
42
|
-
pos.nil? ? nil : [pos, object]
|
43
|
-
}.compact.sort.reverse
|
44
|
-
objects_with_notes[section] = obj.collect{ |o| o.last }
|
45
|
-
end
|
46
|
-
objects_with_notes
|
47
|
-
end
|
48
|
-
|
49
41
|
def note_message(ref, commit)
|
50
42
|
`git notes --ref=#{ ref } show #{ commit }`
|
51
43
|
end
|
@@ -58,25 +50,60 @@ module StepUp
|
|
58
50
|
@version_tags ||= all_tags.map{ |tag| mask.parse(tag) }.compact.sort.map{ |tag| mask.format(tag) }.reverse
|
59
51
|
end
|
60
52
|
|
61
|
-
def
|
62
|
-
|
53
|
+
def version_tag_info(tag)
|
54
|
+
full_message = `git show #{ tag }`
|
55
|
+
tag_message = full_message[/\A.*?\n\n(.*)\n\ncommit\s\w{40}\n/m, 1]
|
56
|
+
tagger = full_message[/\A.*?\nTagger:\s(.*?)\s</m, 1]
|
57
|
+
date = Time.parse(full_message[/\A.*?\nDate:\s+(.*?)\n/m, 1])
|
58
|
+
{:message => tag_message, :tagger => tagger, :date => date}
|
59
|
+
end
|
60
|
+
|
61
|
+
def steps_to_increase_version(level, commit_base = "HEAD", message = nil)
|
63
62
|
tag = last_version_tag(commit_base)
|
64
63
|
tag = tag.sub(/\+$/, '')
|
65
|
-
|
66
|
-
|
64
|
+
new_tag = mask.increase_version(tag, level)
|
65
|
+
notes = RangedNotes.new(self, tag, commit_base).notes.as_hash
|
66
|
+
commands = []
|
67
67
|
commands << "git fetch"
|
68
|
-
commands << "git tag -a -m \"#{ message.to_changelog }\" #{
|
68
|
+
commands << "git tag -a -m \"#{ (message || notes.to_changelog).gsub(/([\$\\"])/, '\\\\\1') }\" #{ new_tag }"
|
69
69
|
commands << "git push --tags"
|
70
|
-
commands + steps_for_archiving_notes(
|
70
|
+
commands + steps_for_archiving_notes(notes, new_tag)
|
71
71
|
end
|
72
72
|
|
73
|
-
def last_version_tag(commit_base =
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
73
|
+
def last_version_tag(commit_base = "HEAD", count_commits = false)
|
74
|
+
all_versions = all_version_tags
|
75
|
+
unless all_versions.empty?
|
76
|
+
commits = commit_history(commit_base)
|
77
|
+
all_versions.each do |tag|
|
78
|
+
commit_under_the_tag = commit_history(tag, 1).first
|
79
|
+
index = commits.index(commit_under_the_tag)
|
80
|
+
unless index.nil?
|
81
|
+
unless index.zero?
|
82
|
+
count = count_commits == true ? commits_between(tag, commit_base).size : 0
|
83
|
+
tag = "#{ tag }+#{ count unless count.zero? }"
|
84
|
+
end
|
85
|
+
return tag
|
86
|
+
end
|
87
|
+
end
|
88
|
+
no_tag_version_in_commit_history = nil
|
89
|
+
else
|
90
|
+
zero_version(commit_base, count_commits)
|
78
91
|
end
|
79
|
-
|
92
|
+
end
|
93
|
+
|
94
|
+
def fetched_remotes(refs_type = 'heads')
|
95
|
+
config = `git config --get-regexp 'remote'`.split(/\n/)
|
96
|
+
config.collect{ |line|
|
97
|
+
$1 if line =~ /^remote\.(\w+)\.fetch\s\+refs\/#{ refs_type }/
|
98
|
+
}.compact.uniq.sort
|
99
|
+
end
|
100
|
+
|
101
|
+
def editor_name
|
102
|
+
ENV["EDITOR"] || `git config core.editor`.chomp
|
103
|
+
end
|
104
|
+
|
105
|
+
def zero_version(commit_base = "HEAD", count_commits = false)
|
106
|
+
"%s+%s" % [mask.blank, "#{ commit_history(commit_base).size if count_commits }"]
|
80
107
|
end
|
81
108
|
end
|
82
109
|
end
|
@@ -7,87 +7,29 @@ module StepUp
|
|
7
7
|
|
8
8
|
module Notes
|
9
9
|
def steps_for_archiving_notes(objects_with_notes, tag)
|
10
|
-
strategy =
|
10
|
+
strategy = CONFIG.notes.after_versioned.strategy
|
11
11
|
raise ArgumentError, "unknown strategy: #{ strategy }" unless NOTES_STRATEGIES.include?(strategy)
|
12
12
|
NOTES_STRATEGIES[strategy].steps_for_archiving_notes(objects_with_notes, tag, self)
|
13
13
|
end
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def notes_after_versioned
|
22
|
-
CONFIG["notes"]["after_versioned"]
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
module NotesTransformation
|
27
|
-
def self.extend_object(base)
|
28
|
-
super
|
29
|
-
class << base
|
30
|
-
attr_writer :driver, :parent, :kept_notes
|
31
|
-
def []=(p1, p2)
|
32
|
-
super
|
33
|
-
sections << p1 unless sections.include?(p1)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
def driver
|
39
|
-
@driver ||= parent.driver
|
40
|
-
end
|
41
|
-
|
42
|
-
def sections
|
43
|
-
@sections ||= parent != self && parent.sections && parent.sections.dup || []
|
44
|
-
end
|
45
|
-
|
46
|
-
def parent
|
47
|
-
@parent ||= self
|
48
|
-
end
|
49
|
-
|
50
|
-
def kept_notes
|
51
|
-
@kept_notes ||= driver.objects_with_notes_of(kept_notes_section)
|
15
|
+
def steps_for_add_notes(section, message, commit_base = nil)
|
16
|
+
commands = []
|
17
|
+
commands << "git fetch"
|
18
|
+
commands << "git notes --ref=#{ section } add -m \"#{ message.gsub(/([\$"])/, '\\\\\1') }\" #{ commit_base }"
|
19
|
+
commands << "git push #{ notes_remote } refs/notes/#{ section }"
|
20
|
+
commands
|
52
21
|
end
|
53
22
|
|
54
|
-
def
|
55
|
-
|
23
|
+
def steps_to_remove_notes(section, commit_base)
|
24
|
+
commands = []
|
25
|
+
commands << "git fetch"
|
26
|
+
commands << "git notes --ref=#{ section } remove #{ commit_base }"
|
27
|
+
commands << "git push #{ notes_remote } refs/notes/#{ section }"
|
28
|
+
commands
|
56
29
|
end
|
57
30
|
|
58
|
-
def
|
59
|
-
notes
|
60
|
-
notes.driver = driver
|
61
|
-
notes.kept_notes = kept_notes
|
62
|
-
sections.each do |section|
|
63
|
-
notes[section] = (parent[section] || []).select{ |commit| not kept_notes.include?(commit) }
|
64
|
-
end
|
65
|
-
notes
|
66
|
-
end
|
67
|
-
|
68
|
-
def messages
|
69
|
-
unless defined? @messages
|
70
|
-
notes = {}.extend NotesTransformation
|
71
|
-
notes.parent = self
|
72
|
-
sections.each do |section|
|
73
|
-
notes[section] = (parent[section] || []).map{ |commit| driver.note_message(section, commit) }
|
74
|
-
end
|
75
|
-
@messages = notes
|
76
|
-
end
|
77
|
-
@messages
|
78
|
-
end
|
79
|
-
|
80
|
-
def to_changelog(options = {})
|
81
|
-
changelog = []
|
82
|
-
sections.each_with_index do |section, index|
|
83
|
-
changelog << "#{ section.capitalize.gsub(/_/, ' ') }:\n" unless index.zero? || messages[section].empty?
|
84
|
-
messages[section].each_with_index do |note, index|
|
85
|
-
note = note.sub(/$/, " (#{ parent[section][index] })") if options[:mode] == :with_objects
|
86
|
-
changelog += note.split(/\n+/).collect{ |line| line.sub(/^(\s*)/, '\1 - ') }
|
87
|
-
end
|
88
|
-
changelog << "" unless messages[section].empty?
|
89
|
-
end
|
90
|
-
changelog.join("\n")
|
31
|
+
def notes_remote
|
32
|
+
fetched_remotes('notes').first
|
91
33
|
end
|
92
34
|
end
|
93
35
|
|
@@ -95,11 +37,17 @@ module StepUp
|
|
95
37
|
class RemoveNotes
|
96
38
|
def steps_for_archiving_notes(objects_with_notes, tag, driver)
|
97
39
|
commands = []
|
98
|
-
|
40
|
+
CONFIG.notes_sections.names.each do |section|
|
41
|
+
next unless objects_with_notes.has_key?(section)
|
42
|
+
removed_notes = []
|
99
43
|
objects_with_notes[section].each do |object|
|
100
|
-
|
44
|
+
next if object[2] == RangedNotes::COMMIT_NOTE
|
45
|
+
removed_notes << "git notes --ref=#{ section } remove #{ object[0] }"
|
46
|
+
end
|
47
|
+
unless removed_notes.empty?
|
48
|
+
commands += removed_notes
|
49
|
+
commands << "git push #{ driver.notes_remote } refs/notes/#{ section }"
|
101
50
|
end
|
102
|
-
commands << "git push origin refs/notes/#{ section }" unless objects_with_notes[section].empty?
|
103
51
|
end
|
104
52
|
commands
|
105
53
|
end
|
@@ -109,17 +57,19 @@ module StepUp
|
|
109
57
|
def steps_for_archiving_notes(objects_with_notes, tag, driver)
|
110
58
|
commands = []
|
111
59
|
objects = []
|
112
|
-
changelog_message =
|
113
|
-
|
60
|
+
changelog_message = CONFIG.notes.after_versioned.changelog_message
|
61
|
+
CONFIG.notes_sections.names.each do |section|
|
62
|
+
next unless objects_with_notes.has_key?(section)
|
114
63
|
objects_with_notes[section].each do |object|
|
115
|
-
|
116
|
-
|
64
|
+
next if object[2] == RangedNotes::COMMIT_NOTE
|
65
|
+
unless objects.include?(object[0])
|
66
|
+
objects << object[0]
|
117
67
|
kept_message = changelog_message.gsub(/\{version\}/, tag)
|
118
|
-
commands << "git notes --ref=#{
|
68
|
+
commands << "git notes --ref=#{ CONFIG.notes.after_versioned.section } add -m \"#{ kept_message.gsub(/([\$\\"])/, '\\\\\1') }\" #{ object[0] }"
|
119
69
|
end
|
120
70
|
end
|
121
71
|
end
|
122
|
-
commands << "git push
|
72
|
+
commands << "git push #{ driver.notes_remote } refs/notes/#{ CONFIG.notes.after_versioned.section }" unless objects.empty?
|
123
73
|
commands
|
124
74
|
end
|
125
75
|
end
|
@@ -11,6 +11,14 @@ module StepUp
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
+
def to_regex
|
15
|
+
re = []
|
16
|
+
mask.each_with_index do |level, index|
|
17
|
+
re << "(?:#{ iterator[index].source })#{ '?' if level.end_with?('9') }"
|
18
|
+
end
|
19
|
+
re.join
|
20
|
+
end
|
21
|
+
|
14
22
|
def parse(version)
|
15
23
|
return unless version.is_a?(String)
|
16
24
|
i = 0
|
@@ -42,19 +50,20 @@ module StepUp
|
|
42
50
|
raise ArgumentError unless version.is_a?(Array) && version.size == mask.size
|
43
51
|
v = []
|
44
52
|
iterator.each_with_index do |pattern, index|
|
45
|
-
|
46
|
-
unless
|
47
|
-
|
53
|
+
level = version[index] || 0
|
54
|
+
raise ArgumentError unless level.is_a?(Fixnum)
|
55
|
+
unless level.zero? && mask[index] =~ /9$/
|
56
|
+
v << mask[index].sub(/[09]$/, level.to_s)
|
48
57
|
end
|
49
58
|
end
|
50
59
|
v.join
|
51
60
|
end
|
52
61
|
|
53
|
-
def increase_version(version,
|
62
|
+
def increase_version(version, level)
|
54
63
|
v = parse version
|
55
|
-
|
56
|
-
(v.size-
|
57
|
-
v[
|
64
|
+
level = version_levels.index(level)
|
65
|
+
(v.size-level).times do |index|
|
66
|
+
v[level+index] = (index.zero? ? (v[level+index] || 0) + 1 : nil)
|
58
67
|
end
|
59
68
|
format v
|
60
69
|
end
|
@@ -65,8 +74,8 @@ module StepUp
|
|
65
74
|
|
66
75
|
private
|
67
76
|
|
68
|
-
def
|
69
|
-
CONFIG["versioning"]["
|
77
|
+
def version_levels
|
78
|
+
CONFIG["versioning"]["version_levels"]
|
70
79
|
end
|
71
80
|
end
|
72
81
|
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module StepUp
|
2
|
+
class RangedNotes
|
3
|
+
COMMIT_NOTE = 0
|
4
|
+
NOTE = 1
|
5
|
+
|
6
|
+
attr_reader :driver, :first_commit, :last_commit
|
7
|
+
|
8
|
+
def initialize(driver, first_commit = nil, last_commit = "HEAD")
|
9
|
+
@driver = driver
|
10
|
+
@last_commit = driver.commit_history(last_commit, 1).first
|
11
|
+
first_commit = driver.commit_history(first_commit, 1).first unless first_commit.nil?
|
12
|
+
raise ArgumentError, "The object #{ first_commit.inspect } was not found" unless first_commit.nil? || all_commits.include?(first_commit)
|
13
|
+
@first_commit = first_commit
|
14
|
+
end
|
15
|
+
|
16
|
+
def notes
|
17
|
+
(visible_detached_notes + scoped_commit_notes).sort.reverse.extend NotesArray
|
18
|
+
end
|
19
|
+
|
20
|
+
def all_notes
|
21
|
+
(visible_detached_notes + scoped_attached_notes + scoped_commit_notes).sort.reverse.extend NotesArray
|
22
|
+
end
|
23
|
+
|
24
|
+
def notes_of(commit)
|
25
|
+
all_visible_notes.select{ |note| note[3] == commit }.extend NotesArray
|
26
|
+
end
|
27
|
+
|
28
|
+
def all_commits
|
29
|
+
@all_commits ||= driver.commit_history(last_commit)
|
30
|
+
end
|
31
|
+
|
32
|
+
def scoped_commits
|
33
|
+
@scoped_commits ||= driver.commits_between(first_commit, last_commit, :with_messages => true).compact
|
34
|
+
end
|
35
|
+
|
36
|
+
def scoped_tags
|
37
|
+
unless defined? @scoped_tags
|
38
|
+
tags = []
|
39
|
+
commits = scoped_commits.collect(&:first)
|
40
|
+
driver.all_version_tags.each do |version_tag|
|
41
|
+
object = driver.commit_history(version_tag, 1).first
|
42
|
+
tags << version_tag if commits.include?(object)
|
43
|
+
end
|
44
|
+
@scoped_tags = tags
|
45
|
+
end
|
46
|
+
@scoped_tags
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def versioned_objects
|
52
|
+
unless defined? @versioned_objects
|
53
|
+
notes = []
|
54
|
+
all_notes_of_section(CONFIG.notes.after_versioned.section, all_commits).each{ |note| notes << note }
|
55
|
+
@versioned_objects = notes
|
56
|
+
end
|
57
|
+
@versioned_objects
|
58
|
+
end
|
59
|
+
|
60
|
+
def all_visible_notes
|
61
|
+
unless defined? @all_visible_notes
|
62
|
+
notes = []
|
63
|
+
CONFIG.notes_sections.names.each do |section|
|
64
|
+
all_notes_of_section(section, all_commits).each{ |note| notes << note }
|
65
|
+
end
|
66
|
+
@all_visible_notes = notes
|
67
|
+
end
|
68
|
+
@all_visible_notes
|
69
|
+
end
|
70
|
+
|
71
|
+
def visible_detached_notes
|
72
|
+
all_visible_notes.select do |note|
|
73
|
+
not versioned_objects.any?{ |object| object[3] == note[3] }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def scoped_attached_notes
|
78
|
+
return [] if scoped_tags.empty?
|
79
|
+
version_tags = scoped_tags.map{ |tag| tag.gsub(/([\.\*\?\{\}])/, '\\\\\1') }.join('|')
|
80
|
+
matcher = /^#{ CONFIG.notes.after_versioned.changelog_message.gsub(/([\.\*\?\{\}])/, '\\\\\1').sub(/\\\{version\\\}/, "(?:#{ version_tags })") }$/
|
81
|
+
|
82
|
+
all_visible_notes.select do |note|
|
83
|
+
versioned_objects.any?{ |object| object[3] == note[3] && object[4] =~ matcher }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def scoped_commit_notes
|
88
|
+
prefixes = CONFIG.notes_sections.prefixes
|
89
|
+
sections = CONFIG.notes_sections.names
|
90
|
+
tags = CONFIG.notes_sections.tags
|
91
|
+
notes = []
|
92
|
+
scoped_commits.each do |commit|
|
93
|
+
message = commit.last
|
94
|
+
prefixes.each_with_index do |prefix, index|
|
95
|
+
message = message[prefix.size..-1] if message.start_with?(prefix)
|
96
|
+
message = message.sub(/\s*##{ tags[index] }[\s\n]*\z/, '')
|
97
|
+
if message != commit.last
|
98
|
+
notes << [all_commits.index(commit.first), sections[index], COMMIT_NOTE, commit.first, message]
|
99
|
+
break
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
notes
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def all_notes_of_section(section, commits = nil)
|
109
|
+
notes = []
|
110
|
+
commits = scoped_commits.collect(&:first) if commits.nil?
|
111
|
+
driver.objects_with_notes_of(section).each do |commit|
|
112
|
+
if commits.include?(commit)
|
113
|
+
message = driver.note_message(section, commit)
|
114
|
+
notes << [all_commits.index(commit), section, NOTE, commit, message]
|
115
|
+
end
|
116
|
+
end
|
117
|
+
notes
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
module NotesHash
|
123
|
+
def to_changelog(options = {})
|
124
|
+
changelog = []
|
125
|
+
|
126
|
+
if options[:custom_message]
|
127
|
+
changelog << "Custom message:\n"
|
128
|
+
changelog << parse_message(options[:custom_message])
|
129
|
+
changelog << ""
|
130
|
+
end
|
131
|
+
|
132
|
+
CONFIG.notes_sections.names.each do |section|
|
133
|
+
next unless has_key?(section)
|
134
|
+
changelog << "#{ CONFIG.notes_sections.label(section) }\n"
|
135
|
+
self[section].each do |note|
|
136
|
+
message = note[1]
|
137
|
+
message = message.sub(/$/, " (#{ note[0] })") if options[:mode] == :with_objects
|
138
|
+
changelog << parse_message(message)
|
139
|
+
end
|
140
|
+
changelog << ""
|
141
|
+
end
|
142
|
+
changelog.join("\n").rstrip
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def parse_message(message)
|
148
|
+
message = message.rstrip.gsub(/^(\s\s)?([^\s\-])/, '\1 - \2')
|
149
|
+
begin
|
150
|
+
changed = message.sub!(/^(\s*-\s.*?\n)\n+(\s*-\s)/, '\1\2')
|
151
|
+
end until changed.nil?
|
152
|
+
message
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
module NotesArray
|
157
|
+
def as_hash
|
158
|
+
notes = {}.extend NotesHash
|
159
|
+
each do |note|
|
160
|
+
notes[note[1]] ||= []
|
161
|
+
notes[note[1]] << [note[3], note[4], note[2]] unless notes[note[1]].any?{ |n| n[0] == note[3] }
|
162
|
+
end
|
163
|
+
notes
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|