git-reviewer 0.1.0

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