git-reviewer 0.1.0
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/.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
|