git-story-workflow 1.4.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
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