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.
@@ -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["versioning"]["version_mask"])
9
+ @mask = Parser::VersionMask.new(CONFIG.versioning.version_mask)
8
10
  end
9
11
 
10
- def self.last_version(commit_base = nil)
11
- @driver ||= new
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 self.unversioned_notes(commit_base = nil, clean = false)
16
- options = {:mode => :with_objects}
17
- options.delete :mode if clean
18
- new.all_objects_with_notes(commit_base).unversioned_only.to_changelog(options)
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 commit_history(commit_base, top = nil)
22
- top = "-n#{ top }" unless top.nil?
23
- `git log --pretty=oneline --no-color --no-notes #{ top } #{ commit_base }`.gsub(/^(\w+)\s.*$/, '\1').split("\n")
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 increase_version_tag(part, commit_base = nil)
62
- commands = []
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
- tag = mask.increase_version(tag, part)
66
- message = all_objects_with_notes(commit_base)
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 }\" #{ tag }"
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(message, tag)
70
+ commands + steps_for_archiving_notes(notes, new_tag)
71
71
  end
72
72
 
73
- def last_version_tag(commit_base = nil)
74
- objects = commit_history(commit_base)
75
- all_version_tags.each do |tag|
76
- index = objects.index(commit_history(tag, 1).first)
77
- return "#{ tag }#{ '+' unless index.zero? }" unless index.nil?
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
- nil
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 = notes_after_versioned["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
- private
16
-
17
- def notes_sections
18
- CONFIG["notes"]["sections"]
19
- end
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 kept_notes_section
55
- driver.send(:notes_after_versioned)["section"]
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 unversioned_only
59
- notes = {}.extend NotesTransformation
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
- objects_with_notes.sections.each do |section|
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
- commands << "git notes --ref=#{ section } remove #{ object }"
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 = driver.notes_after_versioned["changelog_message"]
113
- objects_with_notes.sections.each do |section|
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
- unless objects.include?(object)
116
- objects << object
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=#{ driver.notes_after_versioned["section"] } add -m \"#{ kept_message }\" #{ object }"
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 origin refs/notes/#{ driver.notes_after_versioned["section"] }" unless objects.empty?
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
- raise ArgumentError unless version[index].is_a?(Fixnum)
46
- unless version[index].zero? && mask[index] =~ /9$/
47
- v << mask[index].sub(/[09]$/, version[index].to_s)
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, part)
62
+ def increase_version(version, level)
54
63
  v = parse version
55
- part = version_parts.index(part)
56
- (v.size-part).times do |index|
57
- v[part+index] = (index.zero? ? v[part+index]+1 : 0)
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 version_parts
69
- CONFIG["versioning"]["version_parts"]
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