octaccord 0.0.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +62 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +9 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.org +95 -0
  8. data/Rakefile +7 -0
  9. data/bin/octaccord +311 -0
  10. data/examples/Itr0000-template.md.erb +93 -0
  11. data/examples/octaccord-completion.zsh +11 -0
  12. data/lib/extensions.rb +2 -0
  13. data/lib/extensions/octokit.rb +16 -0
  14. data/lib/octaccord.rb +10 -0
  15. data/lib/octaccord/command.rb +16 -0
  16. data/lib/octaccord/command/add_collaborator.rb +34 -0
  17. data/lib/octaccord/command/comments.rb +50 -0
  18. data/lib/octaccord/command/completions.rb +56 -0
  19. data/lib/octaccord/command/create_iteration.rb +21 -0
  20. data/lib/octaccord/command/get_team_members.rb +18 -0
  21. data/lib/octaccord/command/info.rb +18 -0
  22. data/lib/octaccord/command/label.rb +16 -0
  23. data/lib/octaccord/command/scan.rb +122 -0
  24. data/lib/octaccord/command/show.rb +28 -0
  25. data/lib/octaccord/command/update_issues.rb +28 -0
  26. data/lib/octaccord/errors.rb +2 -0
  27. data/lib/octaccord/formatter.rb +105 -0
  28. data/lib/octaccord/formatter/comment.rb +58 -0
  29. data/lib/octaccord/formatter/debug.rb +17 -0
  30. data/lib/octaccord/formatter/issue.rb +134 -0
  31. data/lib/octaccord/formatter/list.rb +36 -0
  32. data/lib/octaccord/formatter/number.rb +14 -0
  33. data/lib/octaccord/formatter/pbl.rb +23 -0
  34. data/lib/octaccord/formatter/table.rb +17 -0
  35. data/lib/octaccord/formatter/text.rb +14 -0
  36. data/lib/octaccord/formatter/user.rb +42 -0
  37. data/lib/octaccord/iteration.rb +61 -0
  38. data/lib/octaccord/version.rb +3 -0
  39. data/octaccord.gemspec +24 -0
  40. data/spec/octaccord_spec.rb +11 -0
  41. data/spec/spec_helper.rb +2 -0
  42. metadata +130 -0
@@ -0,0 +1,93 @@
1
+ :bangbang: This Iteration Record is created by octaccord from examples/Itr0000-template.md.erb
2
+
3
+ Example:
4
+ ```sh
5
+ $ octaccord create_iteration nomlab/LastNote Itr0095 \
6
+ --team="nomlab/GN" \
7
+ --start="2014-07-03T16:30:00+09:00" \
8
+ --due="2014-07-16T13:00:00+09:00" \
9
+ --manager="okada-takuya" \
10
+ --template=../octaccord/examples/Itr0000-template.md.erb \
11
+ | octaccord filter > Itr0095-sample.md
12
+
13
+ $ octaccord filter < Product-Backlog.md > tmp.md
14
+ $ mv tmp.md Product-Backlog.md
15
+ ```
16
+ <%
17
+ now = Time.now.iso8601
18
+ repos = itr.repository
19
+ members = itr.members.sort.map{|m| "@" + m}.join(", ")
20
+ -%>
21
+ [[(<%= itr.prev.name %>)|<%= itr.prev.name %>]] | [[(<%= itr.next.name %>)|<%= itr.next.name %>]] | [[(PBL)|Product-Backlog]] | [PBL Issues](../issues?labels=PBL&state=open&sort=updated)
22
+
23
+ # <%= itr.name %> Iteration Record
24
+
25
+ * Manager: @<%= itr.manager %>
26
+ * Term: <%= itr.start.strftime("%Y-%m-%d %H:%M") %>..<%= itr.due.strftime("%Y-%m-%d %H:%M") %>
27
+
28
+ ## <%= itr.start.strftime("%Y-%m-%d") %> Iteration Plan
29
+
30
+ * Attendees: <%= members %>
31
+ * Pairs: <%= members %>
32
+
33
+ ### 1. High-Level Objectives
34
+
35
+ [[PBL|Product-Backlog]] と [PBL Issues](../issues?labels=PBL&state=open&sort=updated) を見ながら以下について議論した.
36
+
37
+ #### 完成イメージと本イテレーションの方針について
38
+
39
+ * blah blah ........
40
+ * blah blah ........
41
+ * blah blah ........
42
+
43
+ #### [[Product Backlog|Product-Backlog]] の調整について
44
+
45
+ * #17 は,Cost を 5 から 8 に上げるべき.なぜなら blah blah ........
46
+ * #17 よりも #38 を優先したほうがよい.なぜなら blah blah ........
47
+ * blah blah ........
48
+
49
+ #### 本イテレーションで取り組む PBL Issue について
50
+
51
+ * 議論の結果,[[Product Backlog|Product-Backlog]] を調整して優先順位の並べ替えを行った.
52
+ * 取り組むべき PBL Issue に PBL と <% itr.name %> ラベルを付与し,それらの description 欄に Story, Demo, Cost の項目を記述した.
53
+ * 個々の PBL Issue に関する技術的議論は,その Issue のコメント欄に記述した.
54
+ * 以下,[PBL Issues](../issues?labels=PBL&state=open&sort=updated) から octaccord によって自動抽出した結果を示す.
55
+
56
+ <!-- begin:octaccord scan <%= repos %> --search="label:PBL label:<%= itr.name %>" --format=pbl --topology=Product-Backlog.md -->
57
+ <!-- end:octaccord -->
58
+
59
+ #### 本議論中で更新された Issue コメント一覧
60
+
61
+ <!-- begin:octaccord comments <%= repos %> --since="<%= itr.start.iso8601 %>" --before="<%= (itr.start + 3600 * 2).iso8601 %>"-->
62
+ <!-- end:octaccord -->
63
+
64
+ ### 2. Work Item Assignments
65
+
66
+ <!-- begin:octaccord scan <%= repos %> --search="label:<%= itr.name %> -label:PBL" --format=list -->
67
+ <!-- end:octaccord -->
68
+
69
+ ### 3. Evaluation Criteria
70
+
71
+ * 全テストの完成と通過
72
+ * コードレビューの完了
73
+ * 全ての PBL Issue について,デモを実施
74
+
75
+ ## <%= itr.due.strftime("%Y-%m-%d") %> Iteration Review
76
+
77
+ * Attendees: <%= members %>
78
+
79
+ できあがったプロダクトを見ながら,リリース可能かどうかを判断する会議.
80
+ * 完了できなかった,プロダクトバックログについて担当者が説明する
81
+ * プロダクトバックログに追加すべき項目の有無について議論する
82
+ * プロジェクトを進めるうえで問題となる項目について議論する
83
+ * 現在の進捗をふまえて,リリース日や完了日を予測する
84
+
85
+ ### 1. Issue and Pull Request Review
86
+
87
+ Issues を見ながら,Issues にコメントを付ける.議事は,Issues のコメントとして付ける.
88
+ 本 Wiki には, octaccord がコメントを追加する.
89
+
90
+ #### 本議論中で更新された Issue コメント一覧
91
+
92
+ <!-- begin:octaccord comments <%= repos %> --since="<%= itr.due.iso8601 %>" --before="<%= (itr.due + 3600 * 2).iso8601 %>"-->
93
+ <!-- end:octaccord -->
@@ -0,0 +1,11 @@
1
+ compdef _octaccord octaccord
2
+
3
+ _octaccord () {
4
+ local curcontext="$curcontext"
5
+ local line state _opts
6
+
7
+ _opts=("${(@f)$(octaccord completions -- ${(Q)words[2,-1]})}")
8
+ $_opts && return 0
9
+
10
+ return 1
11
+ }
@@ -0,0 +1,2 @@
1
+ dir = File.dirname(__FILE__) + "/extensions"
2
+ require "#{dir}/octokit.rb"
@@ -0,0 +1,16 @@
1
+ require "octokit"
2
+
3
+ module Octokit
4
+ class Client
5
+ module Issues
6
+ # There are these two PRs in the octokit upstream issues.
7
+ # However, both of them are not merged yet.
8
+ # So, I introduced PR 232 by hand.
9
+ # https://github.com/octokit/octokit.rb/pull/229
10
+ # https://github.com/octokit/octokit.rb/pull/232
11
+ def ext_update_issue(repo, number, options = {})
12
+ patch "#{Repository.path repo}/issues/#{number}", options
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ require "octaccord/version"
2
+ require "octaccord/errors"
3
+ require "octaccord/command"
4
+ require "octaccord/formatter"
5
+ require "octaccord/iteration"
6
+ require "extensions"
7
+
8
+ module Octaccord
9
+ # Your code goes here...
10
+ end
@@ -0,0 +1,16 @@
1
+ module Octaccord
2
+ module Command
3
+ dir = File.dirname(__FILE__) + "/command"
4
+ autoload :AddCollaborator, "#{dir}/add_collaborator.rb"
5
+ autoload :Comments, "#{dir}/comments.rb"
6
+ autoload :Completions, "#{dir}/completions.rb"
7
+ autoload :GetTeamMembers, "#{dir}/get_team_members.rb"
8
+ autoload :Info, "#{dir}/info.rb"
9
+ autoload :Label, "#{dir}/label.rb"
10
+ autoload :Link, "#{dir}/link.rb"
11
+ autoload :Scan, "#{dir}/scan.rb"
12
+ autoload :Show, "#{dir}/show.rb"
13
+ autoload :UpdateIssues, "#{dir}/update_issues.rb"
14
+ autoload :CreateIteration, "#{dir}/create_iteration.rb"
15
+ end # module Command
16
+ end # module Octaccord
@@ -0,0 +1,34 @@
1
+ module Octaccord
2
+ module Command
3
+ class AddCollaborator
4
+ Encoding.default_external = "UTF-8"
5
+
6
+ def initialize(client, repos, **options)
7
+ if options[:users]
8
+ add_users(client, repos, options[:users].split(','))
9
+ elsif options[:teams]
10
+ add_team_members(client, repos, options[:teams].split(','))
11
+ end
12
+ end
13
+
14
+ def add_users(client, repos, users)
15
+ users.each do |user|
16
+ response = client.add_collaborator(repos, user)
17
+ end
18
+ end
19
+
20
+ def add_team_members(client, repos, teams)
21
+ teams.each do |team|
22
+ org, team_name = team.split('/')
23
+ response = client.organization_teams(org)
24
+ team_id = response.select{|t| t.name == team_name}.first.id
25
+ response = client.team_members(team_id)
26
+ members = response.map(&:login)
27
+ members.each do |member|
28
+ response = client.add_collaborator(repos, member)
29
+ end
30
+ end
31
+ end
32
+ end # class AddCollaborator
33
+ end # module Command
34
+ end # module Octaccord
@@ -0,0 +1,50 @@
1
+ module Octaccord
2
+ module Command
3
+ class Comments
4
+
5
+ def initialize(client, repos, since, before)
6
+ begin
7
+ comments = client.issues_comments(repos, :since => since)
8
+ issues = gather_issues(comments, before)
9
+
10
+ issues.each do |uri, comments|
11
+ next if comments.empty?
12
+ issue = comments.first.rels[:issue].get.data
13
+ print format_issue(issue)
14
+
15
+ comments.each do |comment|
16
+ print format_comment(comment)
17
+ end
18
+ end
19
+ print "\n"
20
+ rescue Octokit::ClientError => e
21
+ STDERR.puts "Error: ##{issue} -- #{e.message.split(' // ').first}"
22
+ end
23
+ end
24
+ private
25
+
26
+ def gather_issues(comments, before)
27
+ issues = {}
28
+
29
+ comments.each do |comment|
30
+ next if comment.updated_at > before
31
+ uri = comment.rels[:issue].href
32
+ issues[uri] ||= []
33
+ issues[uri] << comment
34
+ end
35
+ return issues
36
+ end
37
+
38
+ def format_comment(comment)
39
+ comment = Formatter::Comment.new(comment)
40
+ " * #{comment.summary} #{comment.link(text: "...")}\n"
41
+ end
42
+
43
+ def format_issue(issue)
44
+ issue = Formatter::Issue.new(issue)
45
+ return "* #{issue.link} #{issue.status} #{issue.title}\n"
46
+ end
47
+
48
+ end # class Comments
49
+ end # module Command
50
+ end # module Octaccord
@@ -0,0 +1,56 @@
1
+ module Octaccord
2
+ module Command
3
+ class Completions
4
+
5
+ def initialize(help, global_options, arguments)
6
+ command_name = arguments.first
7
+
8
+ if command_name and help[command_name]
9
+ options(help, global_options, command_name)
10
+ else
11
+ commands(help)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def commands(help)
18
+ puts "_values"
19
+ puts "Sub-commands:"
20
+ puts "help[Available commands or one specific COMMAND]"
21
+
22
+ help.each do |name, option|
23
+ next if name == "completions"
24
+ puts "#{name}[#{option.description}]"
25
+ end
26
+ end
27
+
28
+ def options(help, global_options, command_name)
29
+ print "_arguments\n-A\n*\n"
30
+ options = help[command_name].options.merge(global_options)
31
+
32
+ options.each do |name, opt|
33
+ if opt.type == :boolean
34
+ print "(--#{name})--#{name}[#{opt.description}]\n"
35
+ else
36
+ print "(--#{name})--#{name}=-[#{opt.description}]:#{opt.banner}:#{possible_values(opt)}\n"
37
+ end
38
+ end
39
+ end
40
+
41
+ def possible_values(option)
42
+ return "(" + option.enum.join(" ") + ")" if option.enum
43
+
44
+ case option.banner
45
+ when "FILE"
46
+ "_files"
47
+ when "DIRECTORY"
48
+ "_files -/"
49
+ else
50
+ ""
51
+ end
52
+ end
53
+
54
+ end # class Completions
55
+ end # module Command
56
+ end # module Octaccord
@@ -0,0 +1,21 @@
1
+ require "erb"
2
+
3
+ module Octaccord
4
+ module Command
5
+ class CreateIteration
6
+ def initialize(client, repos, name, **options)
7
+ if file = options[:template]
8
+ content = File.open(file).read
9
+ else
10
+ content = gets(nil)
11
+ end
12
+ template = ERB.new(content, nil, "-")
13
+ options.delete(:template)
14
+
15
+ # binding itr, repos
16
+ itr = Octaccord::Iteration.new(client: client, name: name, repository: repos, **options)
17
+ puts template.result(binding)
18
+ end
19
+ end # class CreateIteration
20
+ end # module Command
21
+ end # module Octaccord
@@ -0,0 +1,18 @@
1
+ module Octaccord
2
+ module Command
3
+ class GetTeamMembers
4
+ Encoding.default_external = "UTF-8"
5
+
6
+ def initialize(client, teams, **options)
7
+ teams.split(',').each do |team|
8
+ org, team_name = team.split('/')
9
+ response = client.organization_teams(org)
10
+ team_id = response.select{|t| t.name == team_name}.first.id
11
+ response = client.team_members(team_id)
12
+ members = response.map(&:login)
13
+ puts members.join(', ')
14
+ end
15
+ end
16
+ end # class GetTeamMembers
17
+ end # module Command
18
+ end # module Octaccord
@@ -0,0 +1,18 @@
1
+ require "rugged"
2
+
3
+ module Octaccord
4
+ module Command
5
+ class Info
6
+
7
+ def initialize(client, **options)
8
+ begin
9
+ repo = Rugged::Repository.discover(".")
10
+ url = repo.remotes.find {|remote| remote.url =~ /github\.com/}.url
11
+ puts url
12
+ rescue Rugged::RepositoryError => e
13
+ STDERR.print "Error: you are not in github repository.\n"
14
+ end
15
+ end
16
+ end # class Info
17
+ end # module Command
18
+ end # module Octaccord
@@ -0,0 +1,16 @@
1
+ module Octaccord
2
+ module Command
3
+ class Label
4
+
5
+ def initialize(client, repos, **options)
6
+ begin
7
+ client.labels(repos).each do |label|
8
+ puts label.name
9
+ end
10
+ rescue Octokit::ClientError => e
11
+ STDERR.puts "Error: ##{issue} -- #{e.message.split(' // ').first}"
12
+ end
13
+ end
14
+ end # class Label
15
+ end # module Command
16
+ end # module Octaccord
@@ -0,0 +1,122 @@
1
+ module Octaccord
2
+ module Command
3
+ class Scan
4
+ Encoding.default_external = "UTF-8"
5
+
6
+ def initialize(client, repos, replace = nil, **options)
7
+ if options[:format] == "json"
8
+ formatter = nil
9
+ else
10
+ formatter = Octaccord::Formatter.build(formatter: options[:format] || :text)
11
+ end
12
+ if query = options[:search]
13
+ items = search(client, repos, query, formatter)
14
+ else
15
+ items = scan(client, repos, options[:type], formatter)
16
+ end
17
+ format(items, formatter, replace)
18
+ end
19
+
20
+ def search(client, repos, query, formatter)
21
+ # https://help.github.com/articles/searching-issues
22
+ # or issues = client.list_issues(repos)
23
+ # type:issue means ignore pull request
24
+ query = "repo:#{repos} #{query}"
25
+ query << " state:open" unless query =~ /state:/
26
+ query << " type:issue" unless query =~ /type:/
27
+ STDERR.puts "Query: #{query}" if $OCTACCORD_DEBUG
28
+
29
+ if query =~ /(-)?(refers|refered):\(([^)]*)\)/
30
+ negop, reftype, subquery = $1, $2, $3
31
+ query = query.sub(/-?(refers|refered):\([^)]*\)/, "")
32
+
33
+ subquery << " repo:#{repos}"
34
+ subquery << " state:open" unless subquery =~ /state:/
35
+ subquery << " type:issue" unless subquery =~ /type:/
36
+
37
+ if $OCTACCORD_DEBUG
38
+ STDERR.puts "SUBQUERY: #{subquery}"
39
+ STDERR.puts "QUERY: #{query}"
40
+ end
41
+
42
+ subitems = client.search_issues(subquery).items
43
+ issues = client.search_issues(query).items
44
+
45
+ if reftype == "refers"
46
+ return refers(subitems, issues, (negop == "-"))
47
+ else
48
+ return refered(issues, subitems, (negop == "-"))
49
+ end
50
+ end
51
+
52
+ issues = client.search_issues(query).items
53
+ end
54
+
55
+ def scan(client, repos, type, formatter)
56
+ if type == "pr"
57
+ issues = client.issues(repos).select{|issue| issue.pull_request}
58
+ else
59
+ issues = client.issues(repos).select{|issue| not issue.pull_request}
60
+ end
61
+ end
62
+
63
+ def refers(parents, issues, negop)
64
+ parents, children = corss_reference(parents, issues, negop)
65
+ return children
66
+ end
67
+
68
+ def refered(parents, issues, negop)
69
+ parents, children = corss_reference(parents, issues, negop)
70
+ return parents
71
+ end
72
+
73
+ def corss_reference(parents, children, negop)
74
+ if $OCTACCORD_DEBUG
75
+ STDERR.puts "* PARENTS: #{parents.map{|i| i.number}.join(', ')}"
76
+ STDERR.puts "* CHILDREN: #{children.map{|i| i.number}.join(', ')}"
77
+ STDERR.puts "* NEGOP: #{negop}"
78
+ end
79
+
80
+ refered_parents = []
81
+ refering_children = []
82
+
83
+ parents.each do |parent|
84
+ STDERR.puts "ABOUT PARENT: #{parent.number}:" if $OCTACCORD_DEBUG
85
+ children.each do |child|
86
+ Octaccord::Formatter::Issue.new(child).references.each do |refer_num|
87
+ STDERR.puts " child #{child.number} refers #{refer_num} #{parent.number.to_i == refer_num ? " FOUND" : "" }" if $OCTACCORD_DEBUG
88
+ if parent.number.to_i == refer_num
89
+ STDERR.puts "FOUND #{refer_num}" if $OCTACCORD_DEBUG
90
+ refered_parents << parent
91
+ refering_children << child
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ refered_parents.uniq!
98
+ refering_children.uniq!
99
+
100
+ if negop
101
+ return [parents-refered_parents, children-refering_children]
102
+ else
103
+ return [refered_parents, refering_children]
104
+ end
105
+ end
106
+
107
+ def format(issues, formatter, replace = nil)
108
+ if formatter == nil # raw format
109
+ print issues.to_s
110
+ return
111
+ end
112
+
113
+ issues.each do |issue|
114
+ formatter << issue
115
+ end
116
+ formatter.order(replace) if replace
117
+ print formatter.to_s
118
+ end
119
+
120
+ end # class Scan
121
+ end # module Command
122
+ end # module Octaccord