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