git-story-workflow 1.4.1 → 1.6.1
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 +118 -49
- 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: b2d8278e31abf927d6a9621efe9be58361e75b6edc02ca47015d97bbf61f2674
|
4
|
+
data.tar.gz: b6a82b12b583966eaf6fb10ea2c6e1f24aa3e0fd79db83aca76c2ee37494e7e0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cc21f4fa064f87add0afbef50b7c53d807107f040f89e3a0309bf34b3ba9da289abfd1b1901dedb422aa058eb78feb685643fc1331542902885cc3de4bdbe240
|
7
|
+
data.tar.gz: 0b6860587a49c17bc15143415bb12e5d29622b24cf3bf8095f7c606675090aa5b44951e9a62d05267ae68520330673d67237934127b443c0a334ba944a551e3c
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.6.1
|
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.1 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.1"
|
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-08
|
11
|
+
s.date = "2021-12-08"
|
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
|
@@ -211,7 +205,8 @@ class Git::Story::App
|
|
211
205
|
fetch_tags
|
212
206
|
opts = ([
|
213
207
|
'--color=never',
|
214
|
-
'--pretty=%B'
|
208
|
+
'--pretty=%B',
|
209
|
+
'--reverse',
|
215
210
|
] | rest) * ' '
|
216
211
|
output = capture("git log #{opts} #{ref}")
|
217
212
|
pivotal_ids = SortedSet[]
|
@@ -219,18 +214,44 @@ class Git::Story::App
|
|
219
214
|
fetch_statuses(pivotal_ids) * (?┄ * Tins::Terminal.cols << ?\n)
|
220
215
|
end
|
221
216
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
217
|
+
command doc: '[REF] Create some parts of deploy document'
|
218
|
+
def deploy_document(ref = default_ref, rest: [])
|
219
|
+
ref = build_ref_range(ref)
|
220
|
+
fetch_commits
|
221
|
+
fetch_tags
|
222
|
+
opts = ([
|
223
|
+
'--color=never',
|
224
|
+
'--pretty=%B'
|
225
|
+
] | rest) * ' '
|
226
|
+
output = capture("git log #{opts} #{ref}")
|
227
|
+
pivotal_ids = SortedSet[]
|
228
|
+
output.scan(/\[\s*#\s*(\d+)\s*\]/) { pivotal_ids << $1.to_i }
|
229
|
+
stories = ''
|
230
|
+
attendees = Set[]
|
231
|
+
fetch_stories(pivotal_ids) do |pid|
|
232
|
+
story = fetch_story(pid, with_owner: true)
|
233
|
+
attendees.merge story.owners
|
234
|
+
stories << <<~end
|
235
|
+
• [##{story.id}] #{story.name}
|
236
|
+
○ Pivotal: https://www.pivotaltracker.com/story/show/#{pid}
|
237
|
+
○ Type: #{story.story_type}
|
238
|
+
○ Status: #{story.current_state}
|
239
|
+
○ Owners: #{story.owners.map { |o| "%s <%s>" % [ o.name, o.email ] }.join(', ')}
|
240
|
+
○ Tasks to be done before deployment:
|
241
|
+
☐ …
|
242
|
+
○ Tasks to be done after deployment:
|
243
|
+
☐ …
|
244
|
+
end
|
226
245
|
end
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
246
|
+
attendees.map! { |a| "• @#{a.email}" }
|
247
|
+
<<~end
|
248
|
+
#{attendees.join(?\n)}
|
249
|
+
|
250
|
+
#{stories}
|
231
251
|
end
|
232
252
|
end
|
233
253
|
|
254
|
+
|
234
255
|
command doc: '[REF] output diff since last production deploy tag'
|
235
256
|
def deploy_diff(ref = default_ref, rest: [])
|
236
257
|
ref = build_ref_range(ref)
|
@@ -261,8 +282,8 @@ class Git::Story::App
|
|
261
282
|
"Story #{name} created.".green
|
262
283
|
end
|
263
284
|
|
264
|
-
command doc: '
|
265
|
-
def switch
|
285
|
+
command doc: 'switch to selected story branch'
|
286
|
+
def switch
|
266
287
|
fetch_commits
|
267
288
|
if branch = pick_branch(prompt: 'Switch to story? %s')
|
268
289
|
sh "git checkout #{branch}"
|
@@ -270,7 +291,7 @@ class Git::Story::App
|
|
270
291
|
end
|
271
292
|
end
|
272
293
|
|
273
|
-
command doc: '
|
294
|
+
command doc: 'delete selected story branch'
|
274
295
|
def delete(pattern = nil)
|
275
296
|
fetch_commits
|
276
297
|
if branch = pick_branch(prompt: 'Delete story branch? %s', symbol: ?⌦)
|
@@ -322,6 +343,28 @@ class Git::Story::App
|
|
322
343
|
|
323
344
|
private
|
324
345
|
|
346
|
+
def provide_name(story_id = nil)
|
347
|
+
until story_id.present?
|
348
|
+
story_id = ask(prompt: 'Story id? ').strip
|
349
|
+
end
|
350
|
+
story_id = story_id.gsub(/[^0-9]+/, '')
|
351
|
+
@story_id = Integer(story_id)
|
352
|
+
if stories.any? { |s| s.story_id == @story_id }
|
353
|
+
@reason = "story for ##@story_id already created"
|
354
|
+
return
|
355
|
+
end
|
356
|
+
if name = fetch_story_name(@story_id)
|
357
|
+
name = normalize_name(
|
358
|
+
name,
|
359
|
+
max_size: 128 - 'story'.size - @story_id.to_s.size - 2 * ?_.size
|
360
|
+
).full? || name
|
361
|
+
[ 'story', name, @story_id ] * ?_
|
362
|
+
else
|
363
|
+
@reason = "name for ##@story_id could not be fetched from tracker"
|
364
|
+
return
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
325
368
|
def determine_command
|
326
369
|
c, command = [], nil
|
327
370
|
possible_commands = []
|
@@ -433,11 +476,6 @@ class Git::Story::App
|
|
433
476
|
name
|
434
477
|
end
|
435
478
|
|
436
|
-
def apply_pattern(pattern, stories)
|
437
|
-
pattern = pattern.gsub(?#, '')
|
438
|
-
stories.grep(/#{Regexp.quote(pattern)}/)
|
439
|
-
end
|
440
|
-
|
441
479
|
def error(msg)
|
442
480
|
puts msg.red
|
443
481
|
exit 1
|
@@ -455,8 +493,11 @@ class Git::Story::App
|
|
455
493
|
fetch_story(story_id)&.name
|
456
494
|
end
|
457
495
|
|
458
|
-
def fetch_story(story_id)
|
459
|
-
pivotal_get("projects/#{pivotal_project}/stories/#{story_id}").full?
|
496
|
+
def fetch_story(story_id, with_owner: false)
|
497
|
+
if story = pivotal_get("projects/#{pivotal_project}/stories/#{story_id}").full?
|
498
|
+
story.owners = Array((fetch_story_owners(story_id) if with_owner))
|
499
|
+
end
|
500
|
+
story
|
460
501
|
end
|
461
502
|
|
462
503
|
def fetch_story_owners(story_id)
|
@@ -466,7 +507,12 @@ class Git::Story::App
|
|
466
507
|
def pivotal_get(path)
|
467
508
|
path = path.sub(/\A\/*/, '')
|
468
509
|
url = "https://www.pivotaltracker.com/services/v5/#{path}"
|
469
|
-
|
510
|
+
if pivotal_token
|
511
|
+
@debug and STDERR.puts "Fetching #{url.inspect}"
|
512
|
+
else
|
513
|
+
STDERR.puts "Cannot fetch #{url.inspect} without PIVOTAL_TOKEN set as env var"
|
514
|
+
return
|
515
|
+
end
|
470
516
|
URI.open(url,
|
471
517
|
'X-TrackerToken' => pivotal_token,
|
472
518
|
'Content-Type' => 'application/xml',
|
@@ -566,4 +612,27 @@ class Git::Story::App
|
|
566
612
|
url = url.sub('git@github.com:', 'https://github.com/')
|
567
613
|
url = url.sub(/(\.git)\z/, "/tree/#{branch}")
|
568
614
|
end
|
615
|
+
|
616
|
+
def fetch_stories(pivotal_ids, &block)
|
617
|
+
block or raise ArgumentError, '&block parameter is required'
|
618
|
+
tg = ThreadGroup.new
|
619
|
+
pivotal_ids.each do |pid|
|
620
|
+
order = 0
|
621
|
+
tg.add Thread.new {
|
622
|
+
Thread.current[:order] = order
|
623
|
+
Thread.current[:result] = block.(pid)
|
624
|
+
}
|
625
|
+
end
|
626
|
+
tg.list.with_infobar(label: 'Story').map do |t|
|
627
|
+
t.join
|
628
|
+
+infobar
|
629
|
+
[ t[:order], t[:result] ]
|
630
|
+
end.sort_by(&:first).transpose[1]
|
631
|
+
end
|
632
|
+
|
633
|
+
def fetch_statuses(pivotal_ids)
|
634
|
+
fetch_stories(pivotal_ids) do |pid|
|
635
|
+
status(pid)
|
636
|
+
end
|
637
|
+
end
|
569
638
|
end
|
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.1
|
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-08
|
11
|
+
date: 2021-12-08 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
|