step-up 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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