git-story-workflow 1.4.0 → 1.6.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.
- checksums.yaml +4 -4
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/config/story.yml +6 -2
- data/git-story-workflow.gemspec +5 -3
- data/lib/git/story/app.rb +111 -48
- data/lib/git/story/prepare-commit-msg +1 -1
- data/lib/git/story/setup.rb +46 -4
- data/lib/git/story/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 71a678629ab94d345b9362db3a9632978eecccf4a0b42074b8926a8caa8e7793
|
4
|
+
data.tar.gz: c709d15f03f8a751ec2c61626210ec240fbd5ab1e89b4cd314d9548499ab3284
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1b06bbac8125456cb8095081affae06ee3105f3d157ae2b5f24f970d92072cac0b5aa129d6abde397fdf8d91e7040232673dbb43c55e46aebf30b8cddf66fc4
|
7
|
+
data.tar.gz: 03b2db927c695b5c16b1c0503ff26dbd570e4fe1e12317cf4dd69e3332c2329b2c8fe6c75709b8c4738bd6a4eec68b8dc71bf60c15728e9a163837f09457c734
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.6.0
|
data/config/story.yml
CHANGED
@@ -1,4 +1,8 @@
|
|
1
1
|
---
|
2
|
-
pivotal_token:
|
3
|
-
pivotal_project:
|
2
|
+
pivotal_token: <%= ENV['PIVOTAL_TOKEN'] %>
|
3
|
+
pivotal_project: 1254912
|
4
|
+
pivotal_reference_prefix: pivotal
|
4
5
|
deploy_tag_prefix: production_deploy_
|
6
|
+
semaphore_auth_token: <%= ENV['SEMAPHORE_AUTH_TOKEN'] %>
|
7
|
+
semaphore_project_url: https://betterplace.semaphoreci.com/projects/betterplace
|
8
|
+
todo_nudging: <%= ENV['TODO_NUDGING'].to_i == 1 %>
|
data/git-story-workflow.gemspec
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
|
-
# stub: git-story-workflow 1.
|
2
|
+
# stub: git-story-workflow 1.6.0 ruby lib
|
3
3
|
|
4
4
|
Gem::Specification.new do |s|
|
5
5
|
s.name = "git-story-workflow".freeze
|
6
|
-
s.version = "1.
|
6
|
+
s.version = "1.6.0"
|
7
7
|
|
8
8
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
9
9
|
s.require_paths = ["lib".freeze]
|
10
10
|
s.authors = ["Florian Frank".freeze]
|
11
|
-
s.date = "2021-
|
11
|
+
s.date = "2021-11-15"
|
12
12
|
s.description = "Gem abstracting a git workflow\u2026".freeze
|
13
13
|
s.email = "flori@ping.de".freeze
|
14
14
|
s.executables = ["git-story".freeze]
|
@@ -30,6 +30,7 @@ Gem::Specification.new do |s|
|
|
30
30
|
s.add_development_dependency(%q<rake>.freeze, [">= 0"])
|
31
31
|
s.add_development_dependency(%q<simplecov>.freeze, [">= 0"])
|
32
32
|
s.add_development_dependency(%q<rspec>.freeze, [">= 0"])
|
33
|
+
s.add_development_dependency(%q<byebug>.freeze, [">= 0"])
|
33
34
|
s.add_runtime_dependency(%q<infobar>.freeze, [">= 0"])
|
34
35
|
s.add_runtime_dependency(%q<tins>.freeze, [">= 0"])
|
35
36
|
s.add_runtime_dependency(%q<mize>.freeze, [">= 0"])
|
@@ -41,6 +42,7 @@ Gem::Specification.new do |s|
|
|
41
42
|
s.add_dependency(%q<rake>.freeze, [">= 0"])
|
42
43
|
s.add_dependency(%q<simplecov>.freeze, [">= 0"])
|
43
44
|
s.add_dependency(%q<rspec>.freeze, [">= 0"])
|
45
|
+
s.add_dependency(%q<byebug>.freeze, [">= 0"])
|
44
46
|
s.add_dependency(%q<infobar>.freeze, [">= 0"])
|
45
47
|
s.add_dependency(%q<tins>.freeze, [">= 0"])
|
46
48
|
s.add_dependency(%q<mize>.freeze, [">= 0"])
|
data/lib/git/story/app.rb
CHANGED
@@ -39,7 +39,13 @@ class Git::Story::App
|
|
39
39
|
@opts = go 'n:', @argv
|
40
40
|
@debug = debug
|
41
41
|
determine_command
|
42
|
-
|
42
|
+
if !cc.story? && @command != :setup
|
43
|
+
warn "git story configuration file " +
|
44
|
+
"config/story.yml".bold + " is missing.\n\nCall " +
|
45
|
+
"git story setup".bold + " to create an initial one, inspect and " +
|
46
|
+
"edit it yourself, and also set the appropriate env vars.\n\n"
|
47
|
+
@command, @argv = :help, []
|
48
|
+
end
|
43
49
|
end
|
44
50
|
|
45
51
|
def run
|
@@ -72,6 +78,15 @@ class Git::Story::App
|
|
72
78
|
)
|
73
79
|
end
|
74
80
|
|
81
|
+
command doc: 'initialize git story config file if missing'
|
82
|
+
def setup
|
83
|
+
if cc.config?
|
84
|
+
red("config/story.yml configration already exists!")
|
85
|
+
else
|
86
|
+
Git::Story::Setup.perform
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
75
90
|
command doc: 'output the current story branch if it is checked out'
|
76
91
|
def current(check: true)
|
77
92
|
if check
|
@@ -85,31 +100,9 @@ class Git::Story::App
|
|
85
100
|
end
|
86
101
|
end
|
87
102
|
|
88
|
-
def provide_name(story_id = nil)
|
89
|
-
until story_id.present?
|
90
|
-
story_id = ask(prompt: 'Story id? ').strip
|
91
|
-
end
|
92
|
-
story_id = story_id.gsub(/[^0-9]+/, '')
|
93
|
-
@story_id = Integer(story_id)
|
94
|
-
if stories.any? { |s| s.story_id == @story_id }
|
95
|
-
@reason = "story for ##@story_id already created"
|
96
|
-
return
|
97
|
-
end
|
98
|
-
if name = fetch_story_name(@story_id)
|
99
|
-
name = normalize_name(
|
100
|
-
name,
|
101
|
-
max_size: 128 - 'story'.size - @story_id.to_s.size - 2 * ?_.size
|
102
|
-
).full? || name
|
103
|
-
[ 'story', name, @story_id ] * ?_
|
104
|
-
else
|
105
|
-
@reason = "name for ##@story_id could not be fetched from tracker"
|
106
|
-
return
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
103
|
command doc: '[STORY_ID] fetch status of current story, -n SECONDS refreshes'
|
111
104
|
def status(story_id = current(check: true)&.[](/_(\d+)\z/, 1)&.to_i)
|
112
|
-
if story = fetch_story(story_id)
|
105
|
+
if story = fetch_story(story_id, with_owner: true)
|
113
106
|
color_state =
|
114
107
|
case cs = story.current_state
|
115
108
|
when 'unscheduled', 'planned', 'unstarted'
|
@@ -132,20 +125,21 @@ class Git::Story::App
|
|
132
125
|
else
|
133
126
|
t
|
134
127
|
end
|
135
|
-
owners = Array(fetch_story_owners(story_id)).map { |o| "#{o.name} <#{o.email}>" }
|
136
128
|
result = <<~end
|
137
129
|
Id: #{(?# + story.id.to_s).green}
|
138
130
|
Name: #{story.name.inspect.bold}
|
139
131
|
Type: #{color_type}
|
140
132
|
Estimate: #{story.estimate.to_s.full? { |e| e.yellow.bold } || 'n/a'}
|
141
133
|
State: #{color_state}
|
142
|
-
Branch: #{current_branch_checked?&.color('#ff5f00')}
|
143
134
|
Labels: #{story.labels.map { |l| l.name.on_color(91) }.join(' ')}
|
144
|
-
Owners: #{owners.join(', ').yellow}
|
135
|
+
Owners: #{story.owners.map { |o| "%s <%s>" % [ o.name, o.email ] }.join(', ').yellow}
|
145
136
|
Pivotal: #{story.url.color(33)}
|
146
137
|
end
|
147
|
-
if url = github_url(current_branch_checked?)
|
148
|
-
result <<
|
138
|
+
if branch = current_branch_checked? and url = github_url(current_branch_checked?)
|
139
|
+
result << <<~end
|
140
|
+
Github: #{url.color(33)}
|
141
|
+
Branch: #{branch.color('#ff5f00')}
|
142
|
+
end
|
149
143
|
end
|
150
144
|
result
|
151
145
|
end
|
@@ -219,18 +213,44 @@ class Git::Story::App
|
|
219
213
|
fetch_statuses(pivotal_ids) * (?┄ * Tins::Terminal.cols << ?\n)
|
220
214
|
end
|
221
215
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
216
|
+
command doc: '[REF] Create some parts of deploy document'
|
217
|
+
def deploy_document(ref = default_ref, rest: [])
|
218
|
+
ref = build_ref_range(ref)
|
219
|
+
fetch_commits
|
220
|
+
fetch_tags
|
221
|
+
opts = ([
|
222
|
+
'--color=never',
|
223
|
+
'--pretty=%B'
|
224
|
+
] | rest) * ' '
|
225
|
+
output = capture("git log #{opts} #{ref}")
|
226
|
+
pivotal_ids = SortedSet[]
|
227
|
+
output.scan(/\[\s*#\s*(\d+)\s*\]/) { pivotal_ids << $1.to_i }
|
228
|
+
stories = ''
|
229
|
+
attendees = Set[]
|
230
|
+
fetch_stories(pivotal_ids) do |pid|
|
231
|
+
story = fetch_story(pid, with_owner: true)
|
232
|
+
attendees.merge story.owners
|
233
|
+
stories << <<~end
|
234
|
+
• [##{story.id}] #{story.name}
|
235
|
+
○ Pivotal: https://www.pivotaltracker.com/story/show/#{pid}
|
236
|
+
○ Type: #{story.story_type}
|
237
|
+
○ Status: #{story.current_state}
|
238
|
+
○ Owners: #{story.owners.map { |o| "%s <%s>" % [ o.name, o.email ] }.join(', ')}
|
239
|
+
○ Tasks to be done before deployment:
|
240
|
+
☐ …
|
241
|
+
○ Tasks to be done after deployment:
|
242
|
+
☐ …
|
243
|
+
end
|
226
244
|
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
245
|
+
attendees.map! { |a| "• @#{a.email}" }
|
246
|
+
<<~end
|
247
|
+
#{attendees.join(?\n)}
|
248
|
+
|
249
|
+
#{stories}
|
231
250
|
end
|
232
251
|
end
|
233
252
|
|
253
|
+
|
234
254
|
command doc: '[REF] output diff since last production deploy tag'
|
235
255
|
def deploy_diff(ref = default_ref, rest: [])
|
236
256
|
ref = build_ref_range(ref)
|
@@ -261,8 +281,8 @@ class Git::Story::App
|
|
261
281
|
"Story #{name} created.".green
|
262
282
|
end
|
263
283
|
|
264
|
-
command doc: '
|
265
|
-
def switch
|
284
|
+
command doc: 'switch to selected story branch'
|
285
|
+
def switch
|
266
286
|
fetch_commits
|
267
287
|
if branch = pick_branch(prompt: 'Switch to story? %s')
|
268
288
|
sh "git checkout #{branch}"
|
@@ -270,7 +290,7 @@ class Git::Story::App
|
|
270
290
|
end
|
271
291
|
end
|
272
292
|
|
273
|
-
command doc: '
|
293
|
+
command doc: 'delete selected story branch'
|
274
294
|
def delete(pattern = nil)
|
275
295
|
fetch_commits
|
276
296
|
if branch = pick_branch(prompt: 'Delete story branch? %s', symbol: ?⌦)
|
@@ -322,6 +342,28 @@ class Git::Story::App
|
|
322
342
|
|
323
343
|
private
|
324
344
|
|
345
|
+
def provide_name(story_id = nil)
|
346
|
+
until story_id.present?
|
347
|
+
story_id = ask(prompt: 'Story id? ').strip
|
348
|
+
end
|
349
|
+
story_id = story_id.gsub(/[^0-9]+/, '')
|
350
|
+
@story_id = Integer(story_id)
|
351
|
+
if stories.any? { |s| s.story_id == @story_id }
|
352
|
+
@reason = "story for ##@story_id already created"
|
353
|
+
return
|
354
|
+
end
|
355
|
+
if name = fetch_story_name(@story_id)
|
356
|
+
name = normalize_name(
|
357
|
+
name,
|
358
|
+
max_size: 128 - 'story'.size - @story_id.to_s.size - 2 * ?_.size
|
359
|
+
).full? || name
|
360
|
+
[ 'story', name, @story_id ] * ?_
|
361
|
+
else
|
362
|
+
@reason = "name for ##@story_id could not be fetched from tracker"
|
363
|
+
return
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
325
367
|
def determine_command
|
326
368
|
c, command = [], nil
|
327
369
|
possible_commands = []
|
@@ -433,11 +475,6 @@ class Git::Story::App
|
|
433
475
|
name
|
434
476
|
end
|
435
477
|
|
436
|
-
def apply_pattern(pattern, stories)
|
437
|
-
pattern = pattern.gsub(?#, '')
|
438
|
-
stories.grep(/#{Regexp.quote(pattern)}/)
|
439
|
-
end
|
440
|
-
|
441
478
|
def error(msg)
|
442
479
|
puts msg.red
|
443
480
|
exit 1
|
@@ -455,8 +492,10 @@ class Git::Story::App
|
|
455
492
|
fetch_story(story_id)&.name
|
456
493
|
end
|
457
494
|
|
458
|
-
def fetch_story(story_id)
|
459
|
-
pivotal_get("projects/#{pivotal_project}/stories/#{story_id}").full?
|
495
|
+
def fetch_story(story_id, with_owner: false)
|
496
|
+
story = pivotal_get("projects/#{pivotal_project}/stories/#{story_id}").full?
|
497
|
+
story.owners = Array((fetch_story_owners(story_id) if with_owner))
|
498
|
+
story
|
460
499
|
end
|
461
500
|
|
462
501
|
def fetch_story_owners(story_id)
|
@@ -466,7 +505,12 @@ class Git::Story::App
|
|
466
505
|
def pivotal_get(path)
|
467
506
|
path = path.sub(/\A\/*/, '')
|
468
507
|
url = "https://www.pivotaltracker.com/services/v5/#{path}"
|
469
|
-
|
508
|
+
if pivotal_token
|
509
|
+
@debug and STDERR.puts "Fetching #{url.inspect}"
|
510
|
+
else
|
511
|
+
STDERR.puts "Cannot fetch #{url.inspect} without PIVOTAL_TOKEN set as env var"
|
512
|
+
return
|
513
|
+
end
|
470
514
|
URI.open(url,
|
471
515
|
'X-TrackerToken' => pivotal_token,
|
472
516
|
'Content-Type' => 'application/xml',
|
@@ -566,4 +610,23 @@ class Git::Story::App
|
|
566
610
|
url = url.sub('git@github.com:', 'https://github.com/')
|
567
611
|
url = url.sub(/(\.git)\z/, "/tree/#{branch}")
|
568
612
|
end
|
613
|
+
|
614
|
+
def fetch_stories(pivotal_ids, &block)
|
615
|
+
block or raise ArgumentError, '&block parameter is required'
|
616
|
+
tg = ThreadGroup.new
|
617
|
+
pivotal_ids.each do |pid|
|
618
|
+
tg.add Thread.new { Thread.current[:result] = block.(pid) }
|
619
|
+
end
|
620
|
+
tg.list.with_infobar(label: 'Story').map do |t|
|
621
|
+
t.join
|
622
|
+
+infobar
|
623
|
+
t[:result]
|
624
|
+
end
|
625
|
+
end
|
626
|
+
|
627
|
+
def fetch_statuses(pivotal_ids)
|
628
|
+
fetch_stories(pivotal_ids) do |pid|
|
629
|
+
status(pid)
|
630
|
+
end
|
631
|
+
end
|
569
632
|
end
|
@@ -67,7 +67,7 @@ Tempfile.open('commit') do |output|
|
|
67
67
|
message_parsed = CommitMesssageParser.new.parse(template)
|
68
68
|
if message_parsed.story_number_found?
|
69
69
|
output.puts message_parsed.total
|
70
|
-
elsif complex_config.todo_nudging? && story_numbers.empty? && !message_parsed.story_number_done?
|
70
|
+
elsif complex_config.story.todo_nudging? && story_numbers.empty? && !message_parsed.story_number_done?
|
71
71
|
output.puts message_parsed.data, "", "[TODO]", "", message_parsed.footer
|
72
72
|
else
|
73
73
|
full_message = [ message_parsed.data, "", ]
|
data/lib/git/story/setup.rb
CHANGED
@@ -8,15 +8,35 @@ module Git::Story::Setup
|
|
8
8
|
PREPARE_COMMIT_MESSAGE_SRC = File.join(__dir__, 'prepare-commit-msg')
|
9
9
|
PREPARE_COMMIT_MESSAGE_DST = File.join(HOOKS_DIR, 'prepare-commit-msg')
|
10
10
|
|
11
|
+
CONFIG_TEMPLATE = <<~end
|
12
|
+
---
|
13
|
+
pivotal_token: <%= ENV['PIVOTAL_TOKEN'] %>
|
14
|
+
pivotal_project: 123456789
|
15
|
+
pivotal_reference_prefix: pivotal
|
16
|
+
deploy_tag_prefix: production_deploy_
|
17
|
+
semaphore_auth_token: <%= ENV['SEMAPHORE_AUTH_TOKEN'] %>
|
18
|
+
semaphore_project_url: https://betterplace.semaphoreci.com/projects/betterplace
|
19
|
+
todo_nudging: <%= ENV['TODO_NUDGING'].to_i == 1 %>
|
20
|
+
end
|
21
|
+
|
22
|
+
|
11
23
|
module_function
|
12
24
|
|
13
25
|
def perform(force: false)
|
26
|
+
unless File.directory?('.git')
|
27
|
+
puts "No directory .git found, you need an initialized git repo for this to work"
|
28
|
+
return
|
29
|
+
end
|
30
|
+
install_config('config/story.yml', force: force)
|
31
|
+
install_hooks(force: force)
|
32
|
+
"Setup was performed."
|
33
|
+
end
|
34
|
+
|
35
|
+
def install_hooks(force: false)
|
14
36
|
for filename in %w[ prepare-commit-msg pre-push ]
|
15
37
|
if path = file_installed?(filename)
|
16
|
-
if force
|
38
|
+
if force || File.read(path).match?(MARKER)
|
17
39
|
install_file filename
|
18
|
-
elsif File.read(path).match?(MARKER)
|
19
|
-
;
|
20
40
|
else
|
21
41
|
ask(
|
22
42
|
prompt: "File #{path.inspect} not created by git-story."\
|
@@ -43,6 +63,28 @@ module Git::Story::Setup
|
|
43
63
|
|
44
64
|
def install_file(filename)
|
45
65
|
File.exist?(HOOKS_DIR) or mkdir_p(HOOKS_DIR)
|
46
|
-
cp File.join(__dir__, filename), File.join(HOOKS_DIR, filename)
|
66
|
+
cp File.join(__dir__, filename), dest = File.join(HOOKS_DIR, filename)
|
67
|
+
puts "#{filename.to_s.inspect} was installed to #{dest.to_s.inspect}."
|
68
|
+
end
|
69
|
+
|
70
|
+
def install_config(filename, force: false)
|
71
|
+
filename = File.expand_path(filename)
|
72
|
+
if !force && File.exist?(filename)
|
73
|
+
ask(
|
74
|
+
prompt: "File #{filename.to_s.inspect} exists."\
|
75
|
+
" Overwrite? (y/n, default is %s) ",
|
76
|
+
default: ?n,
|
77
|
+
) do |response|
|
78
|
+
if response != ?y
|
79
|
+
puts "Skipping creation of #{filename.to_s.inspect}."
|
80
|
+
return
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
mkdir_p File.dirname(filename)
|
85
|
+
File.secure_write(filename) do |io|
|
86
|
+
io.puts CONFIG_TEMPLATE
|
87
|
+
end
|
88
|
+
puts "#{filename.to_s.inspect} was created."
|
47
89
|
end
|
48
90
|
end
|
data/lib/git/story/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: git-story-workflow
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Florian Frank
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: gem_hadar
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: byebug
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: infobar
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|