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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04601b201fea90d5e5054d3e61881533d1d5c87b8fb9f4d0c23ab71b74fbda4d
4
- data.tar.gz: 3991c64783f6d63815273b6475597a87731a7737cbfc41e2050919c18ec5c1af
3
+ metadata.gz: 71a678629ab94d345b9362db3a9632978eecccf4a0b42074b8926a8caa8e7793
4
+ data.tar.gz: c709d15f03f8a751ec2c61626210ec240fbd5ab1e89b4cd314d9548499ab3284
5
5
  SHA512:
6
- metadata.gz: 34536de7b1148214bd5fa6a09dfee1ac876ff16a0d4a9543eb34b08ae92aac111d264bc8b0aefb4bdde112b76c9867ebd7622e58f771ffce411e2a9aee3e2a9e
7
- data.tar.gz: 9ecadc6812f41148ad7e79b0184b694a19ec593a041b93626972f9ad2100f6b6b7a6c51962a1e16ceb86222a0800794f2538086b8ad5d7666d406a1fb4e45cd6
6
+ metadata.gz: c1b06bbac8125456cb8095081affae06ee3105f3d157ae2b5f24f970d92072cac0b5aa129d6abde397fdf8d91e7040232673dbb43c55e46aebf30b8cddf66fc4
7
+ data.tar.gz: 03b2db927c695b5c16b1c0503ff26dbd570e4fe1e12317cf4dd69e3332c2329b2c8fe6c75709b8c4738bd6a4eec68b8dc71bf60c15728e9a163837f09457c734
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.0
1
+ 1.6.0
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.0 ruby lib
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.4.0"
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-08-17"
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
- 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
@@ -219,18 +213,44 @@ class Git::Story::App
219
213
  fetch_statuses(pivotal_ids) * (?┄ * Tins::Terminal.cols << ?\n)
220
214
  end
221
215
 
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) }
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
- tg.list.with_infobar(label: 'Story').map do |t|
228
- +infobar
229
- t.join
230
- t[:status]
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: '[PATTERN] switch to story matching PATTERN'
265
- def switch(pattern = nil)
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: '[PATTERN] delete story branch matching PATTERN'
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
- @debug and STDERR.puts "Fetching #{url.inspect}"
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, "", ]
@@ -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.0'
3
+ VERSION = '1.6.0'
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.0
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-08-17 00:00:00.000000000 Z
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