git-story-workflow 1.4.1 → 1.6.1

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: 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