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.
- 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/.stepuprc
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
notes:
|
2
|
+
after_versioned:
|
3
|
+
#strategy: "remove"
|
4
|
+
strategy: "keep"
|
5
|
+
section: "versioning"
|
6
|
+
changelog_message: "available on {version}"
|
7
|
+
sections:
|
8
|
+
- name: "changes"
|
9
|
+
prefix: "change: "
|
10
|
+
label: "Changes:"
|
11
|
+
tag: "change"
|
12
|
+
- name: "bugfixes"
|
13
|
+
prefix: "bugfix: "
|
14
|
+
label: "Bugfixes:"
|
15
|
+
tag: "bugfix"
|
16
|
+
- name: "features"
|
17
|
+
prefix: "feature: "
|
18
|
+
label: "Features:"
|
19
|
+
tag: "feature"
|
20
|
+
- name: "deploy_steps"
|
21
|
+
prefix: "deploy_step: "
|
22
|
+
label: "Deploy steps:"
|
23
|
+
tag: "deploy_step"
|
24
|
+
versioning:
|
25
|
+
version_mask: "v0.0.0.9.rc9"
|
26
|
+
version_levels:
|
27
|
+
- major
|
28
|
+
- minor
|
29
|
+
- tiny
|
30
|
+
- patch
|
31
|
+
- rc
|
data/README.md
CHANGED
@@ -1,3 +1,32 @@
|
|
1
|
-
#
|
1
|
+
# StepUp: a project to 'step up' projects
|
2
2
|
|
3
|
-
|
3
|
+
StepUp is an utility to manage versioning, based on source control management features (i.e. tags and notes of Git). That is, you don't need to keep track of your application version in a file anymore.
|
4
|
+
Initially it just supports Git as SCM. But more SCM's are planned in the future.
|
5
|
+
|
6
|
+
For now the only feature will help you to check the application version based on the default version pattern from StepUp.
|
7
|
+
|
8
|
+
The next (real soon) features to come are:
|
9
|
+
- create notes
|
10
|
+
- increment versions based on notes
|
11
|
+
- pattern of version defined by user
|
12
|
+
- and lots more
|
13
|
+
|
14
|
+
### Installation
|
15
|
+
|
16
|
+
gem install step-up
|
17
|
+
|
18
|
+
### Usage
|
19
|
+
|
20
|
+
1) Enter your directory project
|
21
|
+
|
22
|
+
2) Run StepUp and see the magic
|
23
|
+
|
24
|
+
### Commands
|
25
|
+
|
26
|
+
stepup
|
27
|
+
|
28
|
+
Shows your application version
|
29
|
+
|
30
|
+
stepup -h
|
31
|
+
|
32
|
+
Shows the current features
|
data/Rakefile
CHANGED
data/lib/step-up.rb
CHANGED
data/lib/step-up/cli.rb
CHANGED
@@ -3,24 +3,269 @@ require 'step-up'
|
|
3
3
|
|
4
4
|
module StepUp
|
5
5
|
class CLI < Thor
|
6
|
-
|
6
|
+
include Thor::Actions
|
7
|
+
map %w(--version -v) => :gem_version # $ stepup [--version|-v]
|
7
8
|
|
8
9
|
default_task :version
|
9
10
|
|
10
|
-
desc "", "
|
11
|
-
|
12
|
-
|
11
|
+
desc "version ACTION [OPTIONS]", "manage versions of your project"
|
12
|
+
method_options %w(levels -L) => :boolean # $ stepup version [--levels|-L]
|
13
|
+
method_options %w(level -l) => :string, %w(steps -s) => :boolean, %w(message -m) => :string, :'no-editor' => :boolean # $ stepup version create [--level|-l <level-name>] [--steps|-s] [--message|-m <comment-string>] [--no-editor]
|
14
|
+
VERSION_ACTIONS = %w[show create help]
|
15
|
+
def version(action = nil)
|
16
|
+
action = "show" unless VERSION_ACTIONS.include?(action)
|
17
|
+
if self.respond_to?("version_#{action}")
|
18
|
+
send("version_#{action}")
|
19
|
+
else
|
20
|
+
puts "invalid action: #{action}"
|
21
|
+
end
|
13
22
|
end
|
14
23
|
|
15
|
-
desc "
|
16
|
-
method_options :
|
17
|
-
def
|
18
|
-
|
24
|
+
desc "init", "adds .stepuprc to your project and prepare your local repository to use notes"
|
25
|
+
method_options :update => :boolean
|
26
|
+
def init
|
27
|
+
content = File.read(File.expand_path("../config/step-up.yml", __FILE__))
|
28
|
+
if options[:update] || ! File.exists?(".stepuprc")
|
29
|
+
puts "#{File.exists?(".stepuprc") ? 'updating' : 'creating' } .stepuprc"
|
30
|
+
File.open(".stepuprc", "w") do |f|
|
31
|
+
f.write content
|
32
|
+
end
|
33
|
+
end
|
34
|
+
remotes_with_notes = driver.fetched_remotes('notes')
|
35
|
+
unfetched_remotes = driver.fetched_remotes - remotes_with_notes
|
36
|
+
unless remotes_with_notes.any? || unfetched_remotes.empty?
|
37
|
+
if unfetched_remotes.size > 1
|
38
|
+
remote = choose(unfetched_remotes, "Which remote would you like to fetch notes?")
|
39
|
+
return unless unfetched_remotes.include?(remote)
|
40
|
+
else
|
41
|
+
remote = unfetched_remotes.first
|
42
|
+
end
|
43
|
+
puts "Adding attribute below to .git/config (remote.#{ remote })"
|
44
|
+
puts " fetch = +refs/notes/*:refs/notes/*"
|
45
|
+
`git config --add remote.#{ remote }.fetch +refs/notes/*:refs/notes/*`
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
desc "changelog --top=<num> --format={default|wiki|html}", "show changelog from each version tag"
|
50
|
+
method_options %w[top -n] => :numeric
|
51
|
+
method_options %w[format -f] => :string
|
52
|
+
def changelog
|
53
|
+
log = []
|
54
|
+
method_name = "changelog_format_#{ options[:format] }"
|
55
|
+
method_name = "changelog_format_default" unless respond_to?(method_name)
|
56
|
+
driver.all_version_tags.each_with_index do |tag, index|
|
57
|
+
break if options[:top] && index >= options[:top]
|
58
|
+
log << send(method_name, tag)
|
59
|
+
end
|
60
|
+
puts log.join("\n\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "notes ACTION [object] [OPTIONS]", "show notes for the next version"
|
64
|
+
method_options :clean => :boolean, :steps => :boolean, :"-m" => :string, :since => :string
|
65
|
+
def notes(action = "show", commit_base = nil)
|
66
|
+
unless %w[show add remove help].include?(action)
|
67
|
+
commit_base ||= action
|
68
|
+
action = "show"
|
69
|
+
end
|
70
|
+
if self.respond_to?("notes_#{action}")
|
71
|
+
send("notes_#{action}", commit_base)
|
72
|
+
else
|
73
|
+
puts "invalid action: #{action}"
|
74
|
+
end
|
19
75
|
end
|
20
76
|
|
21
77
|
desc "-v, --version", "show the last version of the gem"
|
22
78
|
def gem_version
|
23
79
|
puts StepUp::VERSION
|
24
80
|
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def changelog_format_default(tag)
|
85
|
+
tag_info = driver.version_tag_info(tag)
|
86
|
+
created_at = tag_info[:date].strftime("%b/%d %Y %H:%M %z")
|
87
|
+
"\033[0;33m#{tag} (#{created_at} by #{tag_info[:tagger]})\033[0m\n\n#{ tag_info[:message] }"
|
88
|
+
end
|
89
|
+
|
90
|
+
def changelog_format_html(tag)
|
91
|
+
tag_info = driver.version_tag_info(tag)
|
92
|
+
created_at = tag_info[:date].strftime("%b/%d %Y %H:%M %z")
|
93
|
+
log = []
|
94
|
+
log << "<h3 class=\"changelog_header\">#{tag} (#{created_at} by #{tag_info[:tagger]})</h3>"
|
95
|
+
log << " <pre class=\"changelog_description\">"
|
96
|
+
log << tag_info[:message].gsub(/^/, ' ')
|
97
|
+
log << " </pre>"
|
98
|
+
log << "<br/>"
|
99
|
+
log.join("\n")
|
100
|
+
end
|
101
|
+
|
102
|
+
def changelog_format_wiki(tag)
|
103
|
+
tag_info = driver.version_tag_info(tag)
|
104
|
+
created_at = tag_info[:date].strftime("%b/%d %Y %H:%M %z")
|
105
|
+
log = []
|
106
|
+
log << "== #{tag} (#{created_at} by #{tag_info[:tagger]}) ==\n"
|
107
|
+
log << tag_info[:message].gsub(/^(\s+)-/, '\1*')
|
108
|
+
log.join("\n")
|
109
|
+
end
|
110
|
+
|
111
|
+
def notes_show(commit_base = nil)
|
112
|
+
message = []
|
113
|
+
message << "Showing notes since #{ options[:since] }#{ " (including notes of tags: #{ ranged_notes.scoped_tags.join(", ")})" if ranged_notes.scoped_tags.any? }" unless options[:since].nil?
|
114
|
+
message << "---"
|
115
|
+
message << get_notes
|
116
|
+
puts message.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
def notes_remove(commit_base)
|
120
|
+
commit_base = "HEAD" if commit_base.nil?
|
121
|
+
ranged_notes = StepUp::RangedNotes.new(driver, nil, commit_base)
|
122
|
+
notes = ranged_notes.notes_of(ranged_notes.last_commit).as_hash
|
123
|
+
sections = notes.keys
|
124
|
+
if sections.empty?
|
125
|
+
puts "No notes found"
|
126
|
+
else
|
127
|
+
if sections.size > 1
|
128
|
+
section = choose(sections, "Which section you want to remove notes?")
|
129
|
+
return unless sections.include?(section)
|
130
|
+
else
|
131
|
+
section = sections.first
|
132
|
+
end
|
133
|
+
steps = driver.steps_to_remove_notes(section, ranged_notes.last_commit)
|
134
|
+
print_or_run(steps, options[:steps])
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def notes_add(commit_base = nil)
|
139
|
+
message = options[:m]
|
140
|
+
message = nil if options[:m] =~ /^(|m)$/
|
141
|
+
message ||= get_message("Note message:\n>>", " >")
|
142
|
+
unless message.empty?
|
143
|
+
section = choose(CONFIG.notes_sections.names, "Choose a section to add the note:")
|
144
|
+
return if section.nil? || ! CONFIG.notes_sections.names.include?(section)
|
145
|
+
steps = driver.steps_for_add_notes(section, message, commit_base)
|
146
|
+
print_or_run(steps, options[:steps])
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def version_show
|
151
|
+
if options[:levels]
|
152
|
+
puts "Current version levels:"
|
153
|
+
version_levels.each do |level|
|
154
|
+
puts " - #{level}"
|
155
|
+
end
|
156
|
+
else
|
157
|
+
puts driver.last_version_tag("HEAD", true)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def version_create
|
162
|
+
level = options[:level] || version_levels.last
|
163
|
+
message = get_notes(true, get_custom_message)
|
164
|
+
message = edit_message(driver.class::VERSION_MESSAGE_FILE_PATH, message) unless options[:'no-editor']
|
165
|
+
|
166
|
+
if message.strip.empty?
|
167
|
+
puts "\ninvalid version message: too short"
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
if version_levels.include? level
|
172
|
+
steps = driver.steps_to_increase_version(level, "HEAD", message)
|
173
|
+
print_or_run(steps, options[:steps])
|
174
|
+
else
|
175
|
+
puts "invalid version create option: #{level}"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def edit_message(temp_file, initial_content)
|
182
|
+
File.open(temp_file, "w"){ |f| f.write initial_content }
|
183
|
+
editor = driver.editor_name
|
184
|
+
if editor =~ /\w/
|
185
|
+
if editor =~ /^vim?\b/
|
186
|
+
system "#{ editor } #{ temp_file }"
|
187
|
+
else
|
188
|
+
`#{ editor } #{ temp_file } && wait $!`
|
189
|
+
end
|
190
|
+
File.read(temp_file).rstrip
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def driver
|
195
|
+
@driver ||= StepUp::Driver::Git.new
|
196
|
+
end
|
197
|
+
|
198
|
+
def ranged_notes
|
199
|
+
unless defined? @ranged_notes
|
200
|
+
tag = options[:since] || driver.last_version_tag
|
201
|
+
if tag =~ /[1-9]/
|
202
|
+
tag = tag.gsub(/\+\d*$/, '')
|
203
|
+
else
|
204
|
+
tag = nil
|
205
|
+
end
|
206
|
+
@ranged_notes = StepUp::RangedNotes.new(driver, tag, "HEAD")
|
207
|
+
end
|
208
|
+
@ranged_notes
|
209
|
+
end
|
210
|
+
|
211
|
+
def get_notes(clean = options[:clean], custom_message = nil)
|
212
|
+
changelog_options = {}
|
213
|
+
changelog_options[:mode] = :with_objects unless clean
|
214
|
+
changelog_options[:custom_message] = custom_message
|
215
|
+
notes = (options[:since].nil? ? ranged_notes.notes : ranged_notes.all_notes)
|
216
|
+
notes.as_hash.to_changelog(changelog_options)
|
217
|
+
end
|
218
|
+
|
219
|
+
def choose(list, statement)
|
220
|
+
puts statement
|
221
|
+
list.each_with_index do |item, index|
|
222
|
+
puts " #{ index + 1 }. #{ item }"
|
223
|
+
end
|
224
|
+
value = ask(">>")
|
225
|
+
if value =~ /^\d+$/
|
226
|
+
value = value.to_i
|
227
|
+
value > 0 && list.size > value - 1 ? list[value - 1] : nil
|
228
|
+
else
|
229
|
+
value.empty? ? nil : value
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def get_message(prompt1, prompt2, total_breaks = 1)
|
234
|
+
lines = []
|
235
|
+
prompt = prompt1
|
236
|
+
empty = total_breaks - 1
|
237
|
+
line = nil
|
238
|
+
begin
|
239
|
+
lines << line unless line.nil?
|
240
|
+
line = raw_ask(prompt)
|
241
|
+
prompt = prompt2
|
242
|
+
empty = line.empty? ? empty + 1 : 0
|
243
|
+
end while empty < total_breaks
|
244
|
+
lines.join("\n").gsub(/\n+\z/, '').gsub(/\A\n+/, '')
|
245
|
+
end
|
246
|
+
|
247
|
+
def raw_ask(statement, color = nil)
|
248
|
+
say("#{statement} ", color)
|
249
|
+
$stdin.gets.chomp
|
250
|
+
end
|
251
|
+
|
252
|
+
def version_levels
|
253
|
+
CONFIG["versioning"]["version_levels"]
|
254
|
+
end
|
255
|
+
|
256
|
+
def print_or_run(steps, print)
|
257
|
+
if print
|
258
|
+
puts steps.join("\n")
|
259
|
+
else
|
260
|
+
steps.each do |step|
|
261
|
+
run step
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def get_custom_message
|
267
|
+
message = options[:message]
|
268
|
+
(message && !message.strip.empty?) ? message : nil
|
269
|
+
end
|
25
270
|
end
|
26
271
|
end
|
data/lib/step-up/config.rb
CHANGED
@@ -2,7 +2,72 @@ require 'yaml'
|
|
2
2
|
module StepUp
|
3
3
|
CONFIG = {}
|
4
4
|
|
5
|
-
|
5
|
+
module ConfigExt
|
6
|
+
def method_missing(m, *args, &block)
|
7
|
+
super unless self.key?(m.to_s)
|
8
|
+
value = self[m.to_s]
|
9
|
+
if value.is_a?(Hash) && ! value.kind_of?(ConfigExt)
|
10
|
+
class << value
|
11
|
+
include ConfigExt
|
12
|
+
end
|
13
|
+
end
|
14
|
+
value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
module ConfigSectionsExt
|
18
|
+
def names
|
19
|
+
map{ |section| section.is_a?(String) ? section : section["name"] }
|
20
|
+
end
|
21
|
+
|
22
|
+
def prefixes
|
23
|
+
map{ |section| section.is_a?(String) ? to_prefix(section) : (section["prefix"] || to_prefix(section["name"])) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def labels
|
27
|
+
map{ |section| section.is_a?(String) ? to_label(section) : (section["label"] || to_label(section["name"])) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def tags
|
31
|
+
map{ |section| section.is_a?(String) ? to_tag(section) : (section["tag"] || to_tag(section["name"])) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def label(section)
|
35
|
+
labels[names.index(section)]
|
36
|
+
end
|
37
|
+
|
38
|
+
def tag(section)
|
39
|
+
tags[names.index(section)]
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def to_prefix(name)
|
45
|
+
"#{ (name.respond_to?(:singularize) ? name.singularize : name).gsub(/_/, ' ') }: "
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_label(name)
|
49
|
+
"#{ name.capitalize.gsub(/_/, ' ') }:"
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_tag(name)
|
53
|
+
name.respond_to?(:singularize) ? name.singularize : name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
class << CONFIG
|
57
|
+
include ConfigExt
|
58
|
+
|
59
|
+
def notes_sections
|
60
|
+
sections = notes.sections
|
61
|
+
unless sections.kind_of?(ConfigSectionsExt)
|
62
|
+
class << sections
|
63
|
+
include ConfigSectionsExt
|
64
|
+
end
|
65
|
+
end
|
66
|
+
sections
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.load_config(path)
|
6
71
|
return CONFIG unless File.exists? path
|
7
72
|
CONFIG.merge! YAML.load_file(path)
|
8
73
|
rescue TypeError => e
|
@@ -10,5 +75,5 @@ module StepUp
|
|
10
75
|
end
|
11
76
|
|
12
77
|
load_config File.expand_path('../config/step-up.yml', __FILE__)
|
13
|
-
load_config '.
|
78
|
+
load_config '.stepuprc' # from working folder
|
14
79
|
end
|
@@ -5,13 +5,25 @@ notes:
|
|
5
5
|
section: "versioning"
|
6
6
|
changelog_message: "available on {version}"
|
7
7
|
sections:
|
8
|
-
- changes
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
- name: "changes"
|
9
|
+
prefix: "change: "
|
10
|
+
label: "Changes:"
|
11
|
+
tag: "change"
|
12
|
+
- name: "bugfixes"
|
13
|
+
prefix: "bugfix: "
|
14
|
+
label: "Bugfixes:"
|
15
|
+
tag: "bugfix"
|
16
|
+
- name: "features"
|
17
|
+
prefix: "feature: "
|
18
|
+
label: "Features:"
|
19
|
+
tag: "feature"
|
20
|
+
- name: "deploy_steps"
|
21
|
+
prefix: "deploy_step: "
|
22
|
+
label: "Deploy steps:"
|
23
|
+
tag: "deploy_step"
|
12
24
|
versioning:
|
13
25
|
version_mask: "v0.0.0.9.rc9"
|
14
|
-
|
26
|
+
version_levels:
|
15
27
|
- major
|
16
28
|
- minor
|
17
29
|
- tiny
|