git-reviewer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.solargraph.yml +33 -0
- data/.vscode/launch.json +14 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +109 -0
- data/README.md +37 -0
- data/Rakefile +4 -0
- data/exe/git-reviewer +5 -0
- data/lib/gitreviewer/algorithm/myers.rb +169 -0
- data/lib/gitreviewer/analyze/analyzer.rb +172 -0
- data/lib/gitreviewer/analyze/blame_tree.rb +68 -0
- data/lib/gitreviewer/analyze/builder.rb +141 -0
- data/lib/gitreviewer/analyze/diff_tree.rb +154 -0
- data/lib/gitreviewer/analyze/result_item.rb +31 -0
- data/lib/gitreviewer/command.rb +112 -0
- data/lib/gitreviewer/config/configuration.rb +93 -0
- data/lib/gitreviewer/option/analyze_option.rb +85 -0
- data/lib/gitreviewer/option/init_option.rb +57 -0
- data/lib/gitreviewer/utils/checker.rb +63 -0
- data/lib/gitreviewer/utils/printer.rb +63 -0
- data/lib/gitreviewer/version.rb +3 -0
- data/lib/gitreviewer.rb +9 -0
- data/sig/git/reviewer.rbs +6 -0
- metadata +96 -0
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require_relative 'blame_tree'
|
3
|
+
require_relative '../utils/printer'
|
4
|
+
require_relative '../utils/checker'
|
5
|
+
require_relative '../algorithm/myers'
|
6
|
+
|
7
|
+
module GitReviewer
|
8
|
+
class Builder
|
9
|
+
attr_accessor :source_branch # source branch
|
10
|
+
attr_accessor :target_branch
|
11
|
+
attr_accessor :source_blame # BlameBranch
|
12
|
+
attr_accessor :target_blame # BlameBranch
|
13
|
+
|
14
|
+
attr_accessor :diff_files # Array<DiffFiles>
|
15
|
+
|
16
|
+
def initialize(source_branch, target_branch)
|
17
|
+
@source_branch = source_branch
|
18
|
+
@target_branch = target_branch
|
19
|
+
end
|
20
|
+
|
21
|
+
def build
|
22
|
+
# 遍历分支改动的每个文件,得到 BlameFile 数组
|
23
|
+
files = Checker.diff_files(@source_branch, @target_branch)
|
24
|
+
header_length = 0
|
25
|
+
|
26
|
+
if files == nil || files.count == 0
|
27
|
+
Printer.warning "Warning: there are no analyzable differences between the target branch and source branch."
|
28
|
+
exit 0
|
29
|
+
else
|
30
|
+
header = "============ Diff files between source<#{@source_branch}> and target<#{@target_branch}> ============"
|
31
|
+
header_length = header.length
|
32
|
+
footer = "=" * header_length
|
33
|
+
|
34
|
+
Printer.verbose_put header
|
35
|
+
Printer.verbose_put files
|
36
|
+
Printer.verbose_put footer
|
37
|
+
Printer.verbose_put "\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
# 构建 source branch 的 BlameBranch
|
41
|
+
source_header_title = " Source Blame Files "
|
42
|
+
target_header_title = " Target Blame Files "
|
43
|
+
source_prefix_length = (header_length - source_header_title.length) / 2
|
44
|
+
target_prefix_length = (header_length - target_header_title.length) / 2
|
45
|
+
source_header = "=" * source_prefix_length + source_header_title + "=" * (header_length - source_prefix_length - source_header_title.length)
|
46
|
+
target_header = "=" * target_prefix_length + target_header_title + "=" * (header_length - target_prefix_length - target_header_title.length)
|
47
|
+
footer = "=" * header_length
|
48
|
+
|
49
|
+
# 打印 source branch Log
|
50
|
+
Printer.verbose_put source_header
|
51
|
+
files.each do |file_name|
|
52
|
+
Printer.verbose_put "#{file_name}"
|
53
|
+
end
|
54
|
+
Printer.verbose_put footer
|
55
|
+
Printer.verbose_put "\n"
|
56
|
+
|
57
|
+
# 打印 target branch Log
|
58
|
+
Printer.verbose_put target_header
|
59
|
+
files.each do |file_name|
|
60
|
+
Printer.verbose_put "#{file_name}"
|
61
|
+
end
|
62
|
+
Printer.verbose_put footer
|
63
|
+
Printer.verbose_put "\n"
|
64
|
+
|
65
|
+
# 构建 source branch & target branch
|
66
|
+
@source_blame = blame_branch(@source_branch, files)
|
67
|
+
@target_blame = blame_branch(@target_branch, files)
|
68
|
+
|
69
|
+
# 构建 diff_files
|
70
|
+
@diff_files = []
|
71
|
+
@source_blame.blame_files.each_with_index do |sfile, index|
|
72
|
+
tfile = @target_blame.blame_files[index]
|
73
|
+
# Diff 时需要交换 tfile 和 sfile
|
74
|
+
myers = Myers.new(tfile, sfile)
|
75
|
+
@diff_files.append(myers.resolve)
|
76
|
+
end
|
77
|
+
|
78
|
+
# 打印 Code Diff
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
def blame_branch(branch, files)
|
83
|
+
blame_files = []
|
84
|
+
files.each do |file_name|
|
85
|
+
bf = BlameFile.new("", [], false, false)
|
86
|
+
|
87
|
+
if Checker.is_file_exist?(branch, file_name) then
|
88
|
+
if Checker.is_file_binary?(branch, file_name)
|
89
|
+
bf = BlameFile.new(file_name, [], true, true)
|
90
|
+
else
|
91
|
+
bf = blame_file(branch, file_name)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
bf = BlameFile.new(file_name, [], false, false)
|
95
|
+
end
|
96
|
+
blame_files.append(bf)
|
97
|
+
end
|
98
|
+
result = BlameBranch.new(branch, blame_files)
|
99
|
+
return result
|
100
|
+
end
|
101
|
+
|
102
|
+
def blame_file(branch, file_name)
|
103
|
+
blame_lines = []
|
104
|
+
content = Checker.snapshot_of_blame_file(branch, file_name)
|
105
|
+
# 遍历文件的每一行,得到 BlameLine 数组
|
106
|
+
content.lines do |line|
|
107
|
+
blame_lines.append(blame_line(line))
|
108
|
+
end
|
109
|
+
|
110
|
+
result = BlameFile.new(file_name, blame_lines, true, false)
|
111
|
+
return result
|
112
|
+
end
|
113
|
+
|
114
|
+
def blame_line(text)
|
115
|
+
# 获取哈希值
|
116
|
+
hash = text.slice!(0, 40)
|
117
|
+
rest = text.strip
|
118
|
+
# 移除 (
|
119
|
+
rest = rest[1..-1]
|
120
|
+
# 根据时间格式进行拆分
|
121
|
+
pattern = /\b\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4}\b/
|
122
|
+
pattern_index = rest.index(pattern)
|
123
|
+
pattern_index = pattern_index + 25 # 2022-04-08 06:19:37 -0200 长度为 25
|
124
|
+
# 根据特定位置进行拆分
|
125
|
+
user_date = rest.slice(0, pattern_index)
|
126
|
+
rest = rest.slice(pattern_index , rest.length)
|
127
|
+
# user_date, rest = rest.split("+", 2)
|
128
|
+
user_date = user_date.strip
|
129
|
+
# 提取作者,日期。日期:提取倒数 19 个字符
|
130
|
+
date = user_date.slice!(-25, 25)
|
131
|
+
date = date.strip
|
132
|
+
user = user_date.strip
|
133
|
+
# # 提取行号,代码
|
134
|
+
line, code = rest.split(")", 2)
|
135
|
+
line = line.strip
|
136
|
+
# 结果
|
137
|
+
result = BlameLine.new(hash, user, date, line, code)
|
138
|
+
return result
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require_relative 'blame_tree'
|
2
|
+
require_relative '../utils/printer'
|
3
|
+
require 'terminal-table'
|
4
|
+
|
5
|
+
module GitReviewer
|
6
|
+
|
7
|
+
class DiffFile
|
8
|
+
UNKNOWN = 0 # 未知情况,理论上不存在
|
9
|
+
DELETE = 1 # 删除文件
|
10
|
+
ADD = 2 # 新增文件
|
11
|
+
MODIFY = 3 # 修改文件
|
12
|
+
|
13
|
+
attr_accessor :operation
|
14
|
+
attr_accessor :file_name
|
15
|
+
attr_accessor :diff_lines
|
16
|
+
|
17
|
+
attr_writer :binary
|
18
|
+
|
19
|
+
def binary?
|
20
|
+
@binary
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(file_name, diff_lines, operation, binary)
|
24
|
+
@file_name = file_name
|
25
|
+
@diff_lines = diff_lines
|
26
|
+
@operation = operation
|
27
|
+
@binary = binary
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_meta_info
|
31
|
+
rows = [
|
32
|
+
["filename", file_name],
|
33
|
+
["operation", format_operation],
|
34
|
+
["binary", binary?]
|
35
|
+
]
|
36
|
+
table = Terminal::Table.new do |t|
|
37
|
+
t.rows = rows
|
38
|
+
end
|
39
|
+
Printer.verbose_put table
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_property
|
43
|
+
result = ""
|
44
|
+
case @operation
|
45
|
+
when DiffFile::UNKNOWN
|
46
|
+
result += "operation: UNKNOWN \n"
|
47
|
+
when DiffFile::DELETE
|
48
|
+
result += "operation: DELETE \n"
|
49
|
+
when DiffFile::ADD
|
50
|
+
result += "operation: ADD \n"
|
51
|
+
when DiffFile::MODIFY
|
52
|
+
result += "operation: MODIFY \n"
|
53
|
+
end
|
54
|
+
|
55
|
+
if binary?
|
56
|
+
result += "binary: true \n"
|
57
|
+
else
|
58
|
+
result += "binary: false \n"
|
59
|
+
end
|
60
|
+
return result
|
61
|
+
end
|
62
|
+
|
63
|
+
def format_operation
|
64
|
+
case @operation
|
65
|
+
when DiffFile::UNKNOWN
|
66
|
+
return "UNKNOWN"
|
67
|
+
when DiffFile::DELETE
|
68
|
+
return "DELETE"
|
69
|
+
when DiffFile::ADD
|
70
|
+
return "ADD"
|
71
|
+
when DiffFile::MODIFY
|
72
|
+
return "MODIFY"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def format_file_name
|
77
|
+
return "filename: #{file_name}\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def format_line_diff
|
81
|
+
name_max_length = 0
|
82
|
+
result = []
|
83
|
+
|
84
|
+
diff_lines.each_with_index do |line, index|
|
85
|
+
name_max_length = [name_max_length, line.format_user.length].max
|
86
|
+
|
87
|
+
if line.is_unchange
|
88
|
+
if result.size == 0 || !result.last.is_unchange
|
89
|
+
result.append(line)
|
90
|
+
end
|
91
|
+
else
|
92
|
+
result.append(line)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
name_max_length += 2
|
97
|
+
|
98
|
+
format_content = ""
|
99
|
+
result.each do |line|
|
100
|
+
if line.operation == DiffLine::DELETE
|
101
|
+
format_content += "\033[0;31m#{line.source_line.user.rjust(name_max_length)} #{line.source_line.format_line} - #{line.source_line.description}\033[0m\n"
|
102
|
+
elsif line.operation == DiffLine::ADD
|
103
|
+
format_content += "\033[0;32m#{line.target_line.user.rjust(name_max_length)} #{line.target_line.format_line} + #{line.target_line.description}\033[0m\n"
|
104
|
+
else
|
105
|
+
format_content += "...\n"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
return format_content
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class DiffLine
|
114
|
+
UNCHANGE = 0 # 未变化
|
115
|
+
DELETE = 1 # 删除行
|
116
|
+
ADD = 2 # 新增行
|
117
|
+
|
118
|
+
# 对于 UNCHANGE 操作,sLine 有值,tLine 有值
|
119
|
+
# 对于 DELETE 操作,sLine 有值,tLine 无值
|
120
|
+
# 对于 ADD 操作,sLine 无值,tLine 有值
|
121
|
+
|
122
|
+
attr_accessor :source_line # 原始行, BlameLine
|
123
|
+
attr_accessor :target_line # 目标行, BlameLine
|
124
|
+
attr_accessor :operation
|
125
|
+
|
126
|
+
def initialize(source_line, target_line, operation)
|
127
|
+
@source_line = source_line
|
128
|
+
@target_line = target_line
|
129
|
+
@operation = operation
|
130
|
+
end
|
131
|
+
|
132
|
+
def is_unchange
|
133
|
+
operation == UNCHANGE
|
134
|
+
end
|
135
|
+
|
136
|
+
def s_line_number
|
137
|
+
source_line.line
|
138
|
+
end
|
139
|
+
|
140
|
+
def t_line_number
|
141
|
+
target_line.line
|
142
|
+
end
|
143
|
+
|
144
|
+
def format_user
|
145
|
+
if operation == DiffLine::DELETE
|
146
|
+
return source_line.user
|
147
|
+
elsif operation == DiffLine::ADD
|
148
|
+
return target_line.user
|
149
|
+
else
|
150
|
+
return source_line.user
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module GitReviewer
|
4
|
+
class ResultItem
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :file_count
|
7
|
+
attr_accessor :line_count
|
8
|
+
|
9
|
+
attr_accessor :file_names
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name
|
13
|
+
@file_count = 0
|
14
|
+
@line_count = 0
|
15
|
+
@file_names = Set.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_file_name(name)
|
19
|
+
@file_names.add(name)
|
20
|
+
@file_count = file_names.count
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_file_count(count)
|
24
|
+
@ile_count += count
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_line_count(count)
|
28
|
+
@line_count += count
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'claide'
|
2
|
+
require_relative './analyze/blame_tree'
|
3
|
+
require_relative './analyze/builder'
|
4
|
+
require_relative './analyze/analyzer'
|
5
|
+
require_relative './algorithm/myers'
|
6
|
+
require_relative './option/init_option'
|
7
|
+
require_relative './utils/checker'
|
8
|
+
require_relative './option/analyze_option'
|
9
|
+
|
10
|
+
module GitReviewer
|
11
|
+
|
12
|
+
class Command < CLAide::Command
|
13
|
+
|
14
|
+
self.abstract_command = false
|
15
|
+
|
16
|
+
self.description = <<-DESC
|
17
|
+
git-reviewer is a git plugin used to analyze who should review a Merge Request or Pull Request, and more details related to code modifications.
|
18
|
+
DESC
|
19
|
+
|
20
|
+
self.command = 'git-reviewer'
|
21
|
+
|
22
|
+
def self.options
|
23
|
+
[
|
24
|
+
['--init', 'Initialize the code review configuration file of the Git repository. It will generate a `gitreviewer.yml` file if needed.'],
|
25
|
+
['--target', 'The target branch to be analyzed, which is the same as the target branch selected when creating a Merge Request or Pull Request.'],
|
26
|
+
['--source', 'Optional, if not specified, the default is the current branch pointed to by Git HEAD. The source branch to be analyzed, which is the same as the source branch selected when creating a Merge Request or Pull Request. '],
|
27
|
+
['--author', 'Only analyze relevant authors involved in code changes.'],
|
28
|
+
['--reviewer', 'Only analyze suggested reviewers for code changes.'],
|
29
|
+
].concat(super)
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(argv)
|
33
|
+
@init = argv.flag?('init', false)
|
34
|
+
@target = argv.option('target')
|
35
|
+
@source = argv.option('source')
|
36
|
+
@analyze_reviewer = argv.flag?('reviewer', false)
|
37
|
+
@analyze_author = argv.flag?('author', false)
|
38
|
+
@version = argv.flag?('version', false)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def run
|
43
|
+
# 处理 help 选项
|
44
|
+
if @help_arg
|
45
|
+
help!
|
46
|
+
return
|
47
|
+
end
|
48
|
+
|
49
|
+
# 处理 version 选项
|
50
|
+
if @version
|
51
|
+
Printer.put "git-reviewer #{GitReviewer::VERSION}"
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
# 处理 init 选项
|
56
|
+
if @init
|
57
|
+
initOption = InitOption.new
|
58
|
+
initOption.execute
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
# 分析
|
63
|
+
analyze
|
64
|
+
end
|
65
|
+
|
66
|
+
def analyze
|
67
|
+
# 检查环境
|
68
|
+
if !Checker.is_git_repository_exist?
|
69
|
+
Printer.red "Error: git repository not exist. Please execute the command in the root director of a git repository."
|
70
|
+
exit 1
|
71
|
+
end
|
72
|
+
# 检查参数
|
73
|
+
if !@analyze_author && !@analyze_reviewer
|
74
|
+
# 如果两个选项均没有,则默认分析作者和审查者
|
75
|
+
@analyze_author = true
|
76
|
+
@analyze_reviewer = true
|
77
|
+
end
|
78
|
+
# 设置默认分支
|
79
|
+
if @source == nil
|
80
|
+
# 默认 source 为当前分支
|
81
|
+
@source = Checker.current_git_branch
|
82
|
+
end
|
83
|
+
if @target == nil
|
84
|
+
Printer.red "Error: target branch cannot be nil or empty. Please use `--target=<branch>` to specify the target branch."
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
|
88
|
+
# 检查分支
|
89
|
+
if @source != nil && @target != nil
|
90
|
+
# source 分支
|
91
|
+
if !Checker.is_git_branch_exist?(@source)
|
92
|
+
Printer.red "Error: source branch `#{@source}` not exist."
|
93
|
+
exit 1
|
94
|
+
end
|
95
|
+
# target 分支
|
96
|
+
if !Checker.is_git_branch_exist?(@target)
|
97
|
+
Printer.red "Error: target branch `#{@target}` not exist."
|
98
|
+
exit 1
|
99
|
+
end
|
100
|
+
# source、target 判重
|
101
|
+
if @source == @target
|
102
|
+
Printer.red "Error: source branch and target branch should not be the same."
|
103
|
+
exit 1
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# 执行分析
|
108
|
+
analyzeOption = AnalyzeOption.new(@source, @target, @analyze_author, @analyze_reviewer, @verbose)
|
109
|
+
analyzeOption.execute
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
|
2
|
+
module GitReviewer
|
3
|
+
class Configuration
|
4
|
+
attr_accessor :project_owner # String
|
5
|
+
attr_accessor :folder_owner # Array<FolderOwner>
|
6
|
+
attr_accessor :file_owner # Array<FileReOwner>
|
7
|
+
attr_accessor :ignore_files # Array<String>
|
8
|
+
attr_accessor :ignore_folders # Array<String>
|
9
|
+
|
10
|
+
def initialize(project_owner, folder_owner, file_owner, ignore_files, ignore_folders)
|
11
|
+
@project_owner = project_owner
|
12
|
+
@folder_owner = folder_owner.map { |hash| FolderOwner.new(hash["path"], hash["owner"]) }
|
13
|
+
@file_owner = file_owner.map { |hash| FileOwner.new(hash["path"], hash["owner"]) }
|
14
|
+
@ignore_files = ignore_files
|
15
|
+
@ignore_folders = ignore_folders
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
{
|
20
|
+
project_owner: @project_owner,
|
21
|
+
folder_owner: @folder_owner.map(&:to_hash),
|
22
|
+
file_owner: @file_owner.map(&:to_hash),
|
23
|
+
ignore_files: @ignore_files,
|
24
|
+
ignore_folders: @ignore_folders
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def is_ignore?(file_name)
|
29
|
+
if @ignore_files != nil && @ignore_files.include?(file_name)
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
if @ignore_folders != nil && @ignore_folders.any?{ |folder| !folder.empty? && file_name.start_with?(folder) }
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
return false
|
36
|
+
end
|
37
|
+
|
38
|
+
def reviewer_of_file(file_name)
|
39
|
+
if is_ignore?(file_name)
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
|
43
|
+
fowner = @file_owner.select { |owner| !owner.path.empty? && owner.path == file_name }.first
|
44
|
+
if fowner != nil && fowner.owner != nil
|
45
|
+
return fowner.owner
|
46
|
+
end
|
47
|
+
|
48
|
+
downer = @folder_owner.select { |owner| !owner.path.empty? && file_name.start_with?(owner.path) }.first
|
49
|
+
if downer != nil && downer.owner != nil
|
50
|
+
return downer.owner
|
51
|
+
end
|
52
|
+
|
53
|
+
if @project_owner == nil || @project_owner.empty?
|
54
|
+
return "<project owner>"
|
55
|
+
end
|
56
|
+
return @project_owner
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class FileOwner
|
61
|
+
attr_accessor :path
|
62
|
+
attr_accessor :owner
|
63
|
+
|
64
|
+
def initialize(path, owner)
|
65
|
+
@path = path
|
66
|
+
@owner = owner
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_hash
|
70
|
+
{
|
71
|
+
path: @path,
|
72
|
+
owner: @owner
|
73
|
+
}
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class FolderOwner
|
78
|
+
attr_accessor :path
|
79
|
+
attr_accessor :owner
|
80
|
+
|
81
|
+
def initialize(path, owner)
|
82
|
+
@path = path
|
83
|
+
@owner = owner
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_hash
|
87
|
+
{
|
88
|
+
path: @path,
|
89
|
+
owner: @owner
|
90
|
+
}
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require_relative '../analyze/analyzer'
|
2
|
+
require 'terminal-table'
|
3
|
+
|
4
|
+
module GitReviewer
|
5
|
+
|
6
|
+
class AnalyzeOption
|
7
|
+
attr_accessor :source
|
8
|
+
attr_accessor :target
|
9
|
+
attr_accessor :analyze_author
|
10
|
+
attr_accessor :analyze_reviewer
|
11
|
+
|
12
|
+
|
13
|
+
attr_accessor :analyzer
|
14
|
+
|
15
|
+
def initialize(source, target, analyze_author, analyze_reviewer, verbose)
|
16
|
+
@source = source
|
17
|
+
@target = target
|
18
|
+
@analyze_author = analyze_author
|
19
|
+
@analyze_reviewer = analyze_reviewer
|
20
|
+
@analyzer = Analyzer.new(source, target)
|
21
|
+
Printer.verbose = verbose
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
@analyzer.execute
|
26
|
+
|
27
|
+
if analyze_author
|
28
|
+
show_analyze_author
|
29
|
+
end
|
30
|
+
|
31
|
+
if analyze_reviewer
|
32
|
+
show_analyze_reviewer
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def show_analyze_author
|
37
|
+
results = @analyzer.author_results.values
|
38
|
+
if results.size <= 0
|
39
|
+
return
|
40
|
+
end
|
41
|
+
results = results.sort_by { |item| item.line_count }.reverse
|
42
|
+
total_file = results.sum { |item| item.file_count }
|
43
|
+
total_line = results.sum { |item| item.line_count }
|
44
|
+
output_rows = []
|
45
|
+
results.each do |item|
|
46
|
+
file_ratio = item.file_count.to_f / total_file.to_f * 100
|
47
|
+
line_ratio = item.line_count.to_f / total_line.to_f * 100
|
48
|
+
output_rows << [item.name, item.file_count, "#{file_ratio.round(2)}%", item.line_count, "#{line_ratio.round(2)}%"]
|
49
|
+
end
|
50
|
+
|
51
|
+
table = Terminal::Table.new do |t|
|
52
|
+
t.title = "Relevant authors involved in code changes"
|
53
|
+
t.headings = ["Related Author", "File Count", "File Ratio", "Line Count", "Line Ratio"]
|
54
|
+
t.rows = output_rows
|
55
|
+
end
|
56
|
+
puts table
|
57
|
+
puts "\n"
|
58
|
+
end
|
59
|
+
|
60
|
+
def show_analyze_reviewer
|
61
|
+
results = @analyzer.reviewer_results.values
|
62
|
+
if results.size <= 0
|
63
|
+
return
|
64
|
+
end
|
65
|
+
results = results.sort_by { |item| item.line_count }.reverse
|
66
|
+
total_file = results.sum { |item| item.file_count }
|
67
|
+
total_line = results.sum { |item| item.line_count }
|
68
|
+
output_rows = []
|
69
|
+
results.each do |item|
|
70
|
+
file_ratio = item.file_count.to_f / total_file.to_f * 100
|
71
|
+
line_ratio = item.line_count.to_f / total_line.to_f * 100
|
72
|
+
output_rows << [item.name, item.file_count, "#{file_ratio.round(2)}%", item.line_count, "#{line_ratio.round(2)}%"]
|
73
|
+
end
|
74
|
+
|
75
|
+
table = Terminal::Table.new do |t|
|
76
|
+
t.title = "Suggested reviewers for code changes."
|
77
|
+
t.headings = ["Suggested Reviewer", "File Count", "File Ratio", "Line Count", "Line Ratio"]
|
78
|
+
t.rows = output_rows
|
79
|
+
end
|
80
|
+
|
81
|
+
puts table
|
82
|
+
puts "\n"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require_relative '../config/configuration'
|
2
|
+
require_relative '../utils/printer'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
|
6
|
+
module GitReviewer
|
7
|
+
|
8
|
+
class InitOption
|
9
|
+
attr_accessor :fileExist
|
10
|
+
|
11
|
+
def execute
|
12
|
+
# 判断当前 .gitreviewer 文件是否存在
|
13
|
+
check_file_exist
|
14
|
+
|
15
|
+
# 如果不存在,则创建 .gitreviewer.yml
|
16
|
+
if !@fileExist
|
17
|
+
create_default_file
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def check_file_exist
|
22
|
+
file_name = ".gitreviewer.yml"
|
23
|
+
@fileExist = File.exist?(file_name)
|
24
|
+
if @fileExist
|
25
|
+
Printer.yellow "`.gitreviewer.yml` already exists. Please do not init again."
|
26
|
+
exit 1
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_default_file()
|
31
|
+
project_owner = "<project owner>"
|
32
|
+
folder_owner = FolderOwner.new("", "")
|
33
|
+
file_owner = FileOwner.new("", "")
|
34
|
+
config = Configuration.new(project_owner, [folder_owner], [file_owner], [""], [""])
|
35
|
+
data = config.to_hash
|
36
|
+
data = deep_transform_keys_to_strings(data)
|
37
|
+
yaml = YAML.dump(data)
|
38
|
+
head = "# `.gitreviewer.yml` is used for a git plugin: git-reviewer.\n# For detailed information about git-reviewer, please refer to https://github.com/baochuquan/git-reviewer\n"
|
39
|
+
content = head + yaml
|
40
|
+
File.open('.gitreviewer.yml', 'w') do |file|
|
41
|
+
file.write(content)
|
42
|
+
end
|
43
|
+
Printer.put "`.gitreviewer.yml` has been created. If you want to customize settings, please edit this file.\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
def deep_transform_keys_to_strings(value)
|
47
|
+
case value
|
48
|
+
when Hash
|
49
|
+
value.transform_keys(&:to_s).transform_values { |v| deep_transform_keys_to_strings(v) }
|
50
|
+
when Array
|
51
|
+
value.map { |v| deep_transform_keys_to_strings(v) }
|
52
|
+
else
|
53
|
+
value # 如果既不是哈希也不是数组,直接返回原值
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|