Skip to content
代码片段 群组 项目
未验证 提交 866d9c6e 编辑于 作者: Leaminn Ma's avatar Leaminn Ma 提交者: GitLab
浏览文件

Reproduce Repository X-Ray functionality - Introduce config file parser

This MR introduces the ConfigFileParser class which
searches through the project's repository for
dependency manager configuration files and parses
them.  This is part of the effort to migrate the
Repository X-Ray functionality to the monolith.
上级 054c9eaf
No related branches found
No related tags found
无相关合并请求
# frozen_string_literal: true
module Ai
module Context
module Dependencies
# This class finds and parses dependency configuration files on the repository's default branch
class ConfigFileParser
include Gitlab::Utils::StrongMemoize
include ConfigFiles::Constants
# This limits the directory levels we search through, which reduces the number of glob
# comparisons we need to make with `File.fnmatch?` (via `ConfigFiles::Base.matches?`).
MAX_SEARCH_DEPTH = 2
def initialize(project)
@project = project
@repository = project.repository
end
def extract_config_files
config_file_path_to_class_map = find_config_file_paths_with_class
return [] unless config_file_path_to_class_map.present?
blobs = fetch_blobs(config_file_path_to_class_map.keys)
config_files = blobs.map do |blob|
config_file_path_to_class_map[blob.path].new(blob)
end
config_files.each(&:parse!)
end
private
attr_reader :project, :repository
def worktree_paths
paths = repository.ls_files(default_branch)
paths.reject { |path| path.count('/') > MAX_SEARCH_DEPTH }
end
strong_memoize_attr :worktree_paths
def default_branch
project.default_branch
end
strong_memoize_attr :default_branch
def latest_commit_sha
project.commit(default_branch).sha
end
strong_memoize_attr :latest_commit_sha
# Returns a hash in the form: { 'path1' => <config_file_class1>, 'path2' => 'config_file_class2', ... }
# For each language, we return the first matching config file class,
# processed in the order as they appear in `CONFIG_FILE_CLASSES`.
def find_config_file_paths_with_class
CONFIG_FILE_CLASSES.group_by(&:lang).each_with_object({}) do |(_lang, klasses), hash|
klasses.each do |klass|
matching_path = worktree_paths.find { |path| klass.matches?(path) }
if matching_path
hash[matching_path] = klass
break
end
end
end
end
def fetch_blobs(paths)
paths_with_sha = paths.map { |path| [latest_commit_sha, path] }
repository.blobs_at(paths_with_sha)
end
end
end
end
end
......@@ -5,6 +5,9 @@ module Context
module Dependencies
module ConfigFiles
module Constants
# List classes by language (alphabetically), then by precedence. Lock files
# should always appear first before non-lock files. This ordering affects
# the result of ConfigFileParser#find_config_file_paths_with_class.
CONFIG_FILE_CLASSES = [
ConfigFiles::GoModules,
ConfigFiles::JavaMaven,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ai::Context::Dependencies::ConfigFileParser, feature_category: :code_suggestions do
let(:config_file_parser) { described_class.new(project) }
describe '#extract_config_files' do
subject(:extract_config_files) { config_file_parser.extract_config_files }
context 'when the repository does not contain a dependency config file' do
let_it_be(:project) do
create(:project, :custom_repo, files:
{
'a.txt' => 'foo',
'dir1/b.rb' => 'bar'
})
end
it 'returns an empty array' do
expect(extract_config_files).to eq([])
end
end
context 'when the repository contains dependency config files' do
let_it_be(:project) do
create(:project, :custom_repo, files:
{
'a.txt' => 'foo',
'pom.xml' => '', # Only one of the two pom.xml files is processed
'dir1/pom.xml' => '',
'dir1/dir2/go.mod' => # Valid go.mod file
<<~CONTENT,
require abc.org/mylib v1.3.0
require golang.org/x/mod v0.5.0
require github.com/pmezard/go-difflib v1.0.0 // indirect
CONTENT
'dir1/dir2/dir3/Gemfile.lock' => # Valid Gemfile.lock but path is too deep
<<~CONTENT
GEM
remote: https://rubygems.org/
specs:
bcrypt (3.1.20)
CONTENT
})
end
it 'returns config file objects up to MAX_DEPTH with the expected attributes' do
result = extract_config_files.map do |config_file|
{
lang: config_file.class.lang,
valid: config_file.valid?,
error_message: config_file.error_message,
payload: config_file.payload
}
end
expect(result.size).to eq(2)
expect(result).to contain_exactly(
{
lang: 'java',
valid: false,
error_message: 'Error(s) while parsing file `dir1/pom.xml`: file empty',
payload: nil
},
{
lang: 'go',
valid: true,
error_message: nil,
payload: a_hash_including(libs: [{ name: 'abc.org/mylib (1.3.0)' }, { name: 'golang.org/x/mod (0.5.0)' }])
}
)
end
end
end
end
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册