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.
- checksums.yaml +7 -0
- data/.gitignore +62 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.org +95 -0
- data/Rakefile +7 -0
- data/bin/octaccord +311 -0
- data/examples/Itr0000-template.md.erb +93 -0
- data/examples/octaccord-completion.zsh +11 -0
- data/lib/extensions.rb +2 -0
- data/lib/extensions/octokit.rb +16 -0
- data/lib/octaccord.rb +10 -0
- data/lib/octaccord/command.rb +16 -0
- data/lib/octaccord/command/add_collaborator.rb +34 -0
- data/lib/octaccord/command/comments.rb +50 -0
- data/lib/octaccord/command/completions.rb +56 -0
- data/lib/octaccord/command/create_iteration.rb +21 -0
- data/lib/octaccord/command/get_team_members.rb +18 -0
- data/lib/octaccord/command/info.rb +18 -0
- data/lib/octaccord/command/label.rb +16 -0
- data/lib/octaccord/command/scan.rb +122 -0
- data/lib/octaccord/command/show.rb +28 -0
- data/lib/octaccord/command/update_issues.rb +28 -0
- data/lib/octaccord/errors.rb +2 -0
- data/lib/octaccord/formatter.rb +105 -0
- data/lib/octaccord/formatter/comment.rb +58 -0
- data/lib/octaccord/formatter/debug.rb +17 -0
- data/lib/octaccord/formatter/issue.rb +134 -0
- data/lib/octaccord/formatter/list.rb +36 -0
- data/lib/octaccord/formatter/number.rb +14 -0
- data/lib/octaccord/formatter/pbl.rb +23 -0
- data/lib/octaccord/formatter/table.rb +17 -0
- data/lib/octaccord/formatter/text.rb +14 -0
- data/lib/octaccord/formatter/user.rb +42 -0
- data/lib/octaccord/iteration.rb +61 -0
- data/lib/octaccord/version.rb +3 -0
- data/octaccord.gemspec +24 -0
- data/spec/octaccord_spec.rb +11 -0
- data/spec/spec_helper.rb +2 -0
- 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 -->
|
data/lib/extensions.rb
ADDED
@@ -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
|
data/lib/octaccord.rb
ADDED
@@ -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
|