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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ef8d1e191f13d4e77de20b5fc538031c49bbc464c3ee345636f440552d944ac
4
- data.tar.gz: 748a64331b6287988c2361052148ff60d0399dc74db0f09de01f8d180d38f99a
3
+ metadata.gz: b2d8278e31abf927d6a9621efe9be58361e75b6edc02ca47015d97bbf61f2674
4
+ data.tar.gz: b6a82b12b583966eaf6fb10ea2c6e1f24aa3e0fd79db83aca76c2ee37494e7e0
5
5
  SHA512:
6
- metadata.gz: 88608ce799a9ac2ac6ea5b2f13353bb159319838f3bf6f8201efc4ffdbf151904c3afee2040824f326104173895837bb791834eea4459db0c05e950b393228cc
7
- data.tar.gz: 557b78253cd6e7fcb48886ab607d914dfa5199ea884a066774a5ddfb270fe70e6a5ddaca31741d2839bb048a4845976d0b74db78201a6b7a9d792645adee2a7c
6
+ metadata.gz: cc21f4fa064f87add0afbef50b7c53d807107f040f89e3a0309bf34b3ba9da289abfd1b1901dedb422aa058eb78feb685643fc1331542902885cc3de4bdbe240
7
+ data.tar.gz: 0b6860587a49c17bc15143415bb12e5d29622b24cf3bf8095f7c606675090aa5b44951e9a62d05267ae68520330673d67237934127b443c0a334ba944a551e3c
data/Rakefile CHANGED
@@ -27,6 +27,7 @@ GemHadar do
27
27
  development_dependency 'rake'
28
28
  development_dependency 'simplecov'
29
29
  development_dependency 'rspec'
30
+ development_dependency 'byebug'
30
31
  licenses << 'Apache-2.0'
31
32
  end
32
33
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.1
1
+ 1.6.1
data/config/story.yml CHANGED
@@ -1,4 +1,8 @@
1
1
  ---
2
- pivotal_token: <%= ENV['PIVOTAL_TOKEN'] %>
3
- pivotal_project: 123456789
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 %>
@@ -1,14 +1,14 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: git-story-workflow 1.4.1 ruby lib
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.4.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-17"
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
- Git::Story::Setup.perform
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 << "Github: #{url.color(33)}\n"
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
- def fetch_statuses(pivotal_ids)
223
- tg = ThreadGroup.new
224
- pivotal_ids.each do |pid|
225
- tg.add Thread.new { Thread.current[:status] = status(pid) }
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
- tg.list.with_infobar(label: 'Story').map do |t|
228
- +infobar
229
- t.join
230
- t[:status]
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: '[PATTERN] switch to story matching PATTERN'
265
- def switch(pattern = nil)
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: '[PATTERN] delete story branch matching PATTERN'
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
- @debug and STDERR.puts "Fetching #{url.inspect}"
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  module Git::Story
2
2
  # Git::Story version
3
- VERSION = '1.4.1'
3
+ VERSION = '1.6.1'
4
4
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
5
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
6
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
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.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-17 00:00:00.000000000 Z
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