Merge branch 'standalone_develop' into pre_trustie_server

This commit is contained in:
yystopf 2022-05-20 17:28:25 +08:00
commit dc0d31330a
32 changed files with 1079 additions and 205 deletions

1
.gitignore vendored
View File

@ -85,3 +85,4 @@ Dockerfile
dump.rdb dump.rdb
.tags* .tags*
ceshi_user.xlsx ceshi_user.xlsx
public/trace_task_results

View File

@ -2,7 +2,7 @@ class Admins::ImportUsersController < Admins::BaseController
def create def create
return render_error('请上传正确的文件') if params[:file].blank? || !params[:file].is_a?(ActionDispatch::Http::UploadedFile) return render_error('请上传正确的文件') if params[:file].blank? || !params[:file].is_a?(ActionDispatch::Http::UploadedFile)
result = Admins::ImportUserService.call(params[:file].to_io) result = Admins::ImportUserFromExcelService.call(params[:file].to_io)
render_ok(result) render_ok(result)
rescue Admins::ImportUserService::Error => ex rescue Admins::ImportUserService::Error => ex
render_error(ex) render_error(ex)

View File

@ -12,7 +12,7 @@ class OwnersController < ApplicationController
def show def show
@owner = Owner.find_by(login: params[:id]) || Owner.find_by(id: params[:id]) @owner = Owner.find_by(login: params[:id]) || Owner.find_by(id: params[:id])
return render_ok(type: 'User') unless @owner.present? return render_not_found unless @owner.present?
# 组织 # 组织
if @owner.is_a?(Organization) if @owner.is_a?(Organization)
return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition return render_forbidden("没有查看组织的权限") if org_limited_condition || org_privacy_condition

View File

@ -22,6 +22,7 @@ class ProjectsController < ApplicationController
menu.append(menu_hash_by_name("devops")) if @project.has_menu_permission("devops") && @project.forge? menu.append(menu_hash_by_name("devops")) if @project.has_menu_permission("devops") && @project.forge?
menu.append(menu_hash_by_name("versions")) if @project.has_menu_permission("versions") menu.append(menu_hash_by_name("versions")) if @project.has_menu_permission("versions")
menu.append(menu_hash_by_name("wiki")) if @project.has_menu_permission("wiki") && @project.forge? menu.append(menu_hash_by_name("wiki")) if @project.has_menu_permission("wiki") && @project.forge?
menu.append(menu_hash_by_name("services")) if @project.has_menu_permission("services") && @project.forge? && (current_user.admin? || @project.member?(current_user.id))
menu.append(menu_hash_by_name("resources")) if @project.has_menu_permission("resources") && @project.forge? menu.append(menu_hash_by_name("resources")) if @project.has_menu_permission("resources") && @project.forge?
menu.append(menu_hash_by_name("activity")) menu.append(menu_hash_by_name("activity"))
menu.append(menu_hash_by_name("settings")) if user_is_admin && @project.forge? menu.append(menu_hash_by_name("settings")) if user_is_admin && @project.forge?

View File

@ -225,7 +225,8 @@ class RepositoriesController < ApplicationController
@path = Gitea.gitea_config[:domain]+"/#{@owner.login}/#{@repository.identifier}/raw/branch/#{params[:ref]}/" @path = Gitea.gitea_config[:domain]+"/#{@owner.login}/#{@repository.identifier}/raw/branch/#{params[:ref]}/"
@readme = result[:status] === :success ? result[:body] : nil @readme = result[:status] === :success ? result[:body] : nil
@readme['content'] = decode64_content(@readme, @owner, @repository, params[:ref], @path) @readme['content'] = decode64_content(@readme, @owner, @repository, params[:ref], @path)
render json: @readme.slice("type", "encoding", "size", "name", "path", "content", "sha") @readme['replace_content'] = readme_decode64_content(@readme, @owner, @repository, params[:ref], @path)
render json: @readme.slice("type", "encoding", "size", "name", "path", "content", "sha", "replace_content")
rescue rescue
render json: nil render json: nil
end end

View File

@ -0,0 +1,18 @@
class Traces::BaseController < ApplicationController
helper_method :observed_logged_user?, :observed_user
def observed_user
@_observed_user ||= (User.find_by_login(params[:user_id]) || User.find_by_id(params[:user_id]))
end
def observed_logged_user?
observed_user.id == User.current&.id
end
protected
def check_auth
return render_forbidden unless current_user.admin? || observed_logged_user?
end
end

View File

@ -0,0 +1,83 @@
class Traces::ProjectsController < Traces::BaseController
include OperateProjectAbilityAble
before_action :require_login
before_action :load_project
before_action :authorizate_user_can_edit_project!, except: [:task_results]
def tasks
branch_name = params[:branch_name]
return render_error("无可用检测次数") if @project.user_trace_tasks.size >= 5
return render_error("分支名不能为空!") if branch_name.blank?
@all_branches = Gitea::Repository::Branches::ListNameService.call(@project&.owner, @project.identifier)
return render_error("请输入正确的分支名!") unless @all_branches["branch_name"].include?(branch_name)
code, data, error = Trace::CheckService.call(current_user.trace_token, @project, "1", branch_name)
if code == 200
UserTraceTask.create!(
user_id: current_user.id,
project_id: @project.id,
branch_tag: branch_name,
task_id: data["task_id"]
)
render_ok
else
render_error("检测失败 Error:#{error}")
end
rescue Exception => exception
puts exception.message
normal_status(-1, exception.message)
end
def task_results
limit = params[:limit] || params[:per_page]
limit = (limit.to_i.zero? || limit.to_i > 15) ? 15 : limit.to_i
page = params[:page].to_i.zero? ? 1 : params[:page].to_i
return render :json => {left_tasks_count: 5, data: []} if current_user.trace_user.nil?
code, data, error = Trace::CheckResultService.call(current_user.trace_token, @project, nil, page, limit)
if code == 200
render :json => {left_tasks_count: 5 - @project.user_trace_tasks.size, data: data}
else
render_error("获取检测记录失败 Error:#{error}")
end
rescue Exception => exception
puts exception.message
normal_status(-1, exception.message)
end
def reload_task
return render_error("project_id错误") if params[:project_id].blank?
branch_name = params[:branch_name]
return render_error("分支名不能为空!") if branch_name.blank?
@all_branches = Gitea::Repository::Branches::ListNameService.call(@project&.owner, @project.identifier)
return render_error("请输入正确的分支名!") unless @all_branches["branch_name"].include?(branch_name)
code, data, error = Trace::ReloadCheckService.call(current_user.trace_token, params[:project_id])
if code == 200
UserTraceTask.create!(
user_id: current_user.id,
project_id: @project.id,
branch_tag: branch_name,
task_id: data["task_id"]
)
render_ok
else
render_error("重新检测失败 Error:#{error}")
end
rescue Exception => exception
puts exception.message
normal_status(-1, exception.message)
end
def task_pdf
return render_error("task_id错误") if params[:task_id].blank?
result = Trace::PdfReportService.call(current_user.trace_token, params[:task_id])
if result.is_a?(Hash) && result[:code] == 200
redirect_to result[:download_url]
else
render_error("下载报告失败!")
end
rescue Exception => exception
puts exception.message
normal_status(-1, exception.message)
end
end

View File

@ -0,0 +1,14 @@
class Traces::TraceUsersController < Traces::BaseController
before_action :require_login
def create
if current_user.trace_token.present?
render_ok
else
render_error(-1, "代码溯源用户初始化失败")
end
rescue Exception => exception
puts exception.message
normal_status(-1, exception.message)
end
end

View File

@ -16,6 +16,7 @@ includes:
- users - users
- projects - projects
- repositories - repositories
- traces
- pulls - pulls
- issues - issues
- organizations - organizations

View File

@ -280,7 +280,7 @@ repo |是| |string |项目标识identifier
### 返回字段说明 ### 返回字段说明
参数 | 类型 | 字段说明 参数 | 类型 | 字段说明
--------- | ----------- | ----------- --------- | ----------- | -----------
menu_name |string|导航名称, home:主页,code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,activity:动态,setting:仓库设置 menu_name |string|导航名称, home:主页,code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,wiki:维基,services:服务,activity:动态,setting:仓库设置
> 返回的JSON示例: > 返回的JSON示例:
@ -408,7 +408,7 @@ await octokit.request('POST /api/yystopf/ceshi/project_units')
### 请求参数 ### 请求参数
参数 | 必选 | 默认 | 类型 | 字段说明 参数 | 必选 | 默认 | 类型 | 字段说明
--------- | ------- | ------- | -------- | ---------- --------- | ------- | ------- | -------- | ----------
|unit_types |是| |array | 项目模块内容, 支持以下参数:code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑 | |unit_types |是| |array | 项目模块内容, 支持以下参数:code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,wiki:维基,resources:资源库,services:服务 |
### 返回字段说明: ### 返回字段说明:

View File

@ -0,0 +1,217 @@
# Traces
## 代码溯源初始化
用户同意协议后请求的接口,创建代码溯源的账号
> 示例:
```shell
curl -X POST \
http://localhost:3000/api/traces/trace_users.json
```
```javascript
await octokit.request('POST /api/traces/trace_users.json')
```
### HTTP 请求
`POST api/traces/trace_users.json`
> 返回的JSON示例:
```json
{
"status": 0,
"message": "success"
}
```
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
## 代码分析结果列表
查询项目下代码分析的结果
> 示例:
```shell
curl -X GET \
http://localhost:3000/api/traces/yystopf/many_branch/task_results.json
```
```javascript
await octokit.request('GET /api/traces/:owner/:repo/task_results.json')
```
### HTTP 请求
`GET api/traces/:owner/:repo/task_results.json`
### 请求参数
参数 | 必选 | 默认 | 类型 | 字段说明
--------- | ------- | ------- | -------- | ----------
owner|是|否|string | 项目所有者标识|
repo|是 | 否|string | 项目标识 |
page |否| 1 | int | 页码 |
limit |否| 15 | int | 每页数量 |
### 返回字段说明(暂缺)
<!-- 参数 | 类型 | 字段说明
--------- | ----------- | -----------
total_count |int |总数 |
public_keys.id |int |ID|
public_keys.name |string|密钥标题|
public_keys.content |string|密钥内容|
public_keys.fingerprint |string|密钥标识|
public_keys.created_time |string|密钥创建时间| -->
> 返回的JSON示例:
```json
{
"data": [
{
"accuracy": "20",
"code_type": "C",
"depth": "2",
"detect_flag": "快速-组件级",
"detect_rule": "快速-组件级,2,20,,开源软件,50,10",
"detect_startdate": "2022-05-10 15:59:46 ",
"detect_status": "fail",
"detectflag": "快速-组件级",
"fail_reason": "Invalid package type",
"file_name": "many_branch.zip",
"license_process": "100",
"licenseparam": "开源软件",
"package_type": "",
"product_name": "84727546110",
"project_id": "6dbc3e42-5857-4ca4-a54d-58fd9dbf6dc5",
"sim_process": "100",
"similarity_process": "2",
"task_id": "15139171-091b-4316-98b1-6068970efa44",
"totalsize": 5,
"uid": "78",
"vuln_process": "",
"vulnlevel": ""
}
]
}
```
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
## 新建分析
用户选择仓库分支进行代码分析的接口
> 示例:
```shell
curl -X POST \
http://localhost:3000/api/traces/yystopf/many_branch/tasks.json
```
```javascript
await octokit.request('POST /api/traces/:owner/:repo/tasks.json')
```
### HTTP 请求
`POST api/traces/:owner/:repo/tasks.json`
### 请求参数
参数 | 必选 | 默认 | 类型 | 字段说明
--------- | ------- | ------- | -------- | ----------
owner |是 | 否 | string | 项目所有者标识 |
repo |是 | 否 | string | 项目标识 |
branch_name|是 | 否| string | 分支名称 |
> 返回的JSON示例:
```json
{
"status": 0,
"message": "success"
}
```
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
## 重新扫描
对代码分析结果进行再次分析
> 示例:
```shell
curl -X GET \
http://localhost:3000/api/traces/yystopf/many_branch/reload_task.json
```
```javascript
await octokit.request('GET /api/traces/:owner/:repo/reload_task.json')
```
### HTTP 请求
`GET api/traces/:owner/:repo/reload_task.json`
### 请求参数
参数 | 必选 | 默认 | 类型 | 字段说明
--------- | ------- | ------- | -------- | ----------
owner |是 | 否 | string | 项目所有者标识 |
repo |是 | 否 | string | 项目标识 |
project_id|是 | 否| string | 代码分析结果里的project_id |
branch_name|是 | 否| string | 分支名称 |
> 返回的JSON示例:
```json
{
"status": 0,
"message": "success"
}
```
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
## 下载报告
把代码分析的结果下载到本地
> 示例:
```shell
curl -X GET \
http://localhost:3000/api/traces/yystopf/many_branch/task_pdf.json
```
```javascript
await octokit.request('GET /api/traces/:owner/:repo/task_pdf.json')
```
### HTTP 请求
`GET api/traces/:owner/:repo/task_pdf.json`
### 请求参数
参数 | 必选 | 默认 | 类型 | 字段说明
--------- | ------- | ------- | -------- | ----------
owner |是 | 否 | string | 项目所有者标识 |
repo |是 | 否 | string | 项目标识 |
task_id|是 | 否| string | 代码分析结果里的task_id |
> 返回的JSON示例:
```json
{
"status": 0,
"message": "success"
}
```
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>

View File

@ -103,6 +103,46 @@ module RepositoriesHelper
end end
end end
# author hui.he
def new_readme_render_decode64_content(str, owner, repo, ref, readme_path, readme_name)
file_path = readme_path.include?('/') ? readme_path.gsub("/#{readme_name}", '') : readme_path.gsub("#{readme_name}", '')
return nil if str.blank?
content = Base64.decode64(str).force_encoding('UTF-8')
s_regex = /\[.*?\]\((.*?)\)/
src_regex = /src=\"(.*?)\"/
ss = content.to_s.scan(s_regex)
ss_src = content.to_s.scan(src_regex)
total_sources = ss + ss_src
total_sources.uniq!
total_sources.each do |s|
begin
s_content = s[0]
# 链接直接跳过不做替换
next if s_content.starts_with?('http://') || s_content.starts_with?('https://') || s_content.starts_with?('mailto:')
ext = File.extname(s_content)[1..-1]
if image_type?(ext) || download_type(ext)
s_content = File.expand_path(s_content, file_path)
s_content = s_content.split("#{Rails.root}/")[1]
# content = content.gsub(s[0], "/#{s_content}")
s_content = [base_url, "/api/#{owner&.login}/#{repo.identifier}/raw?filepath=#{s_content}&ref=#{ref}"].join
content = content.gsub(s[0], s_content)
else
path = [owner&.login, repo&.identifier, 'tree', ref, file_path].join("/")
s_content = File.expand_path(s_content, path)
s_content = s_content.split("#{Rails.root}/")[1]
content = content.gsub(s[0], "/#{s_content}")
end
rescue
next
end
end
return content
rescue
return str
end
# unix_time values for example: 1604382982 # unix_time values for example: 1604382982
def render_format_time_with_unix(unix_time) def render_format_time_with_unix(unix_time)
Time.at(unix_time).strftime("%Y-%m-%d %H:%M") Time.at(unix_time).strftime("%Y-%m-%d %H:%M")
@ -113,12 +153,21 @@ module RepositoriesHelper
date.to_time.strftime("%Y-%m-%d %H:%M") date.to_time.strftime("%Y-%m-%d %H:%M")
end end
def readme_decode64_content(entry, owner, repo, ref, path=nil)
Rails.logger.info("entry===#{entry["type"]} #{entry["name"]}")
content = Gitea::Repository::Entries::GetService.call(owner, repo.identifier, URI.escape(entry['path']), ref: ref)['content']
Rails.logger.info("content===#{content}")
# readme_render_decode64_content(content, owner, repo, ref)
new_readme_render_decode64_content(content, owner, repo, ref, entry['path'], entry['name'])
end
def decode64_content(entry, owner, repo, ref, path=nil) def decode64_content(entry, owner, repo, ref, path=nil)
if is_readme?(entry['type'], entry['name']) if is_readme?(entry['type'], entry['name'])
# content = Gitea::Repository::Entries::GetService.call(owner, repo.identifier, URI.escape(entry['path']), ref: ref)['content'] Rails.logger.info("entry===#{entry["type"]} #{entry["name"]}")
content = entry['content'] content = Gitea::Repository::Entries::GetService.call(owner, repo.identifier, URI.escape(entry['path']), ref: ref)['content']
path = URI.escape(entry['path']).to_s.downcase.gsub("/readme.md","") Rails.logger.info("content===#{content}")
readme_render_decode64_content(content, owner, repo, ref, path) # readme_render_decode64_content(content, owner, repo, ref)
return Base64.decode64(content).force_encoding('UTF-8')
else else
file_type = File.extname(entry['name'].to_s)[1..-1] file_type = File.extname(entry['name'].to_s)[1..-1]
if image_type?(file_type) if image_type?(file_type)

View File

@ -0,0 +1,15 @@
class Admins::NewImportUserFromExcel < BaseImportXlsx
UserData = Struct.new(:login, :email, :password, :nickname)
def read_each(&block)
sheet.each_row_streaming(pad_cells: true, offset: 1) do |row|
data = row.map(&method(:cell_value))[0..3]
block.call UserData.new(*data)
end
end
private
def cell_value(obj)
obj&.cell_value
end
end

View File

@ -1,7 +1,7 @@
module CustomRegexp module CustomRegexp
PHONE = /1\d{10}/ PHONE = /1\d{10}/
EMAIL = /\A[a-zA-Z0-9]+([._\\]*[a-zA-Z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+\z/ EMAIL = /\A[a-zA-Z0-9]+([._\\]*[a-zA-Z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+\z/
LOGIN = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]+$/ #只含有数字、字母、下划线不能以下划线开头和结尾 LOGIN = /^(?!_)(?!.*?_$)[a-zA-Z0-9_-]{4,15}/ #只含有数字、字母、下划线不能以下划线开头和结尾
LASTNAME = /\A[a-zA-Z0-9\u4e00-\u9fa5]+\z/ LASTNAME = /\A[a-zA-Z0-9\u4e00-\u9fa5]+\z/
NICKNAME = /\A[\u4e00-\u9fa5_a-zA-Z0-9]+\z/ NICKNAME = /\A[\u4e00-\u9fa5_a-zA-Z0-9]+\z/
PASSWORD = /\A[a-z_A-Z0-9\-\.!@#\$%\\\^&\*\)\(\+=\{\}\[\]\/",'_<>~\·`\?:;|]{8,16}\z/ PASSWORD = /\A[a-z_A-Z0-9\-\.!@#\$%\\\^&\*\)\(\+=\{\}\[\]\/",'_<>~\·`\?:;|]{8,16}\z/

View File

@ -141,6 +141,7 @@ class MessageTemplate::ProjectSettingChanged < MessageTemplate
navbar.gsub!('devops', '工作流') navbar.gsub!('devops', '工作流')
navbar.gsub!('versions', '里程碑') navbar.gsub!('versions', '里程碑')
navbar.gsub!('resources', '资源库') navbar.gsub!('resources', '资源库')
navbar.gsub!('services', '服务')
if change_count > 1 if change_count > 1
content.sub!('{ifnavbar}', '<br/>') content.sub!('{ifnavbar}', '<br/>')
else else
@ -290,6 +291,7 @@ class MessageTemplate::ProjectSettingChanged < MessageTemplate
navbar.gsub!('devops', '工作流') navbar.gsub!('devops', '工作流')
navbar.gsub!('versions', '里程碑') navbar.gsub!('versions', '里程碑')
navbar.gsub!('resources', '资源库') navbar.gsub!('resources', '资源库')
navbar.gsub!('services', '服务')
if change_count > 1 if change_count > 1
content.sub!('{ifnavbar}', '<br/>') content.sub!('{ifnavbar}', '<br/>')
else else

View File

@ -124,6 +124,7 @@ class Project < ApplicationRecord
has_many :pinned_projects, dependent: :destroy has_many :pinned_projects, dependent: :destroy
has_many :has_pinned_users, through: :pinned_projects, source: :user has_many :has_pinned_users, through: :pinned_projects, source: :user
has_many :webhooks, class_name: "Gitea::Webhook", primary_key: :gpid, foreign_key: :repo_id has_many :webhooks, class_name: "Gitea::Webhook", primary_key: :gpid, foreign_key: :repo_id
has_many :user_trace_tasks, dependent: :destroy
after_create :incre_user_statistic, :incre_platform_statistic after_create :incre_user_statistic, :incre_platform_statistic
after_save :check_project_members after_save :check_project_members
before_save :set_invite_code, :reset_unmember_followed, :set_recommend_and_is_pinned, :reset_cache_data before_save :set_invite_code, :reset_unmember_followed, :set_recommend_and_is_pinned, :reset_cache_data

View File

@ -16,7 +16,7 @@
class ProjectUnit < ApplicationRecord class ProjectUnit < ApplicationRecord
belongs_to :project belongs_to :project
enum unit_type: {code: 1, issues: 2, pulls: 3, wiki:4, devops: 5, versions: 6, resources: 7} enum unit_type: {code: 1, issues: 2, pulls: 3, wiki:4, devops: 5, versions: 6, resources: 7, services: 8}
validates :unit_type, uniqueness: { scope: :project_id} validates :unit_type, uniqueness: { scope: :project_id}

View File

@ -175,6 +175,7 @@ class User < Owner
has_many :system_notification_histories has_many :system_notification_histories
has_many :system_notifications, through: :system_notification_histories has_many :system_notifications, through: :system_notification_histories
has_one :trace_user, dependent: :destroy has_one :trace_user, dependent: :destroy
has_many :user_trace_tasks, dependent: :destroy
# Groups and active users # Groups and active users
scope :active, lambda { where(status: [STATUS_ACTIVE, STATUS_EDIT_INFO]) } scope :active, lambda { where(status: [STATUS_ACTIVE, STATUS_EDIT_INFO]) }

View File

@ -0,0 +1,25 @@
# == Schema Information
#
# Table name: user_trace_tasks
#
# id :integer not null, primary key
# user_id :integer
# project_id :integer
# branch_tag :string(255)
# task_id :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_user_trace_tasks_on_project_id (project_id)
# index_user_trace_tasks_on_user_id (user_id)
#
class UserTraceTask < ApplicationRecord
belongs_to :user
belongs_to :project
end

View File

@ -0,0 +1,71 @@
class Admins::ImportUserFromExcelService < ApplicationService
Error = Class.new(StandardError)
attr_reader :file, :result
def initialize(file)
@file = file
@result = { success: 0, fail: [] }
end
def call
raise Error, '文件不存在' if file.blank?
excel = Admins::NewImportUserFromExcel.new(file)
excel.read_each(&method(:save_user))
result
rescue ApplicationImport::Error => ex
raise Error, ex.message
end
private
def save_user(data)
user = find_user(data)
if user.blank?
create_user(data)
result[:success] +=1
else
fail_data = data.as_json
fail_data[:data] = fail_data.values.join(",")
fail_data[:message] = '用户已存在'
result[:fail] << fail_data
end
rescue Exception => ex
fail_data = data.as_json
fail_data[:data] = fail_data.values.join(",")
fail_data[:message] = ex.message
result[:fail] << fail_data
end
def create_user(data)
ActiveRecord::Base.transaction do
username = data.login&.gsub(/\s+/, "")
email = data.email&.gsub(/\s+/, "")
password = data.password
nickname = data.nickname&.gsub(/\s+/, "")
raise Error, "无法使用以下关键词:#{username},请重新命名" if ReversedKeyword.check_exists?(data.login)
Register::RemoteForm.new({username: username, email: email, password: password, platform: 'forge'}).validate!
user = User.new(admin: false, login: username, mail: email, nickname: nickname, platform: 'forge' , type: "User")
user.password = password
user.activate
raise Error, user.errors.full_messages.join(",") unless user.valid?
interactor = Gitea::RegisterInteractor.call({username: username, email: email, password: password})
if interactor.success?
gitea_user = interactor.result
result = Gitea::User::GenerateTokenService.call(username, password)
user.gitea_token = result['sha1']
user.gitea_uid = gitea_user[:body]['id']
UserExtension.create!(user_id: user.id) if user.save!
else
raise interactor.error, 'gitea user create error'
end
user
end
end
def find_user(data)
User.find_by(login: data.login)
end
end

View File

@ -1,11 +1,11 @@
# 代码溯源 查询检测结果 # 代码溯源 查询检测结果
class Trace::CheckResultService < Trace::ClientService class Trace::CheckResultService < Trace::ClientService
attr_accessor :token, :project_name, :file_name, :page_num, :page_size attr_accessor :token, :project, :file_name, :page_num, :page_size
def initialize(token, project_name=nil, file_name=nil, page_num=1, page_size=15) def initialize(token, project, file_name=nil, page_num=1, page_size=15)
@token = token @token = token
@project_name = project_name @project = project
@file_name = file_name @file_name = file_name
@page_num = page_num @page_num = page_num
@page_size = page_size @page_size = page_size
@ -19,7 +19,7 @@ class Trace::CheckResultService < Trace::ClientService
private private
def request_params def request_params
{ {
product_name: project_name, product_name: Digest::MD5.hexdigest(project&.id.to_s)[0...20],
file_name: file_name, file_name: file_name,
pageNum: page_num, pageNum: page_num,
pageSize: page_size, pageSize: page_size,

View File

@ -11,26 +11,25 @@ class Trace::CheckService < Trace::ClientService
end end
def call def call
result = authed_post(token, url, {data: request_params}) result = http_authed_post(token, url, {data: request_params})
reponse = render_response(result) reponse = render_response(result)
end end
private private
def request_params def request_params
repo = Gitea::Repository::GetService.call(project&.owner&.login, project&.identifier) repo = Gitea::Repository::GetService.call(project&.owner, project&.identifier)
{ {
product_name: project&.name, product_name: Digest::MD5.hexdigest(project&.id.to_s)[0...20],
product_type: project&.category&.name, product_type: project&.project_category&.name || '其他',
code_type: project&.language&.name, code_type: project&.project_language&.name || '其他',
product_desc: project&.description, product_desc: project&.description,
git_url: repo['clone_url'], git_url: repo['clone_url'],
if_branch: if_branch, if_branch: if_branch,
branch_tag: branch_tag branch_tag: branch_tag
} }.compact
end end
def url def url
"/user/check".freeze "/user/check".freeze
end end
end end

View File

@ -12,6 +12,19 @@ class Trace::ClientService < ApplicationService
conn.post(full_url(url), params[:data]) conn.post(full_url(url), params[:data])
end end
def http_authed_post(token, url, params={})
puts "[trace][POST] request params: #{params}"
puts "[trace][POST] request token: #{token}"
url = URI("#{full_url(url)}")
http = Net::HTTP.new(url.host, url.port)
http.read_timeout = 1200
request = Net::HTTP::Post.new(url)
request["Authorization"] = token
form_data = params[:data].stringify_keys.to_a
request.set_form form_data, 'multipart/form-data'
http.request(request)
end
def get(url, params={}) def get(url, params={})
puts "[trace][GET] request params: #{params}" puts "[trace][GET] request params: #{params}"
conn.get do |req| conn.get do |req|
@ -100,11 +113,22 @@ class Trace::ClientService < ApplicationService
end end
def render_response(response) def render_response(response)
status = response.status if response.is_a?(Faraday::Response)
body = JSON.parse(response&.body) status = response.status
body = JSON.parse(response&.body)
log_error(status, body) log_error(status, body)
return [body["code"], body["data"], body["error"]] return [body["code"], body["data"], body["error"]]
end
if response.is_a?(Net::HTTPOK)
status = 200
body = JSON.parse(response&.body)
log_error(status, body)
return [body["code"], body["data"], body["error"]]
end
end end
end end

View File

@ -1,4 +1,7 @@
# 代码溯源 导出pdf # 代码溯源 导出pdf
require 'open-uri'
require 'fileutils'
class Trace::PdfReportService < Trace::ClientService class Trace::PdfReportService < Trace::ClientService
attr_accessor :token, :task_id attr_accessor :token, :task_id
@ -9,15 +12,23 @@ class Trace::PdfReportService < Trace::ClientService
end end
def call def call
result = authed_get(token, url, request_params) content = open("#{domain}#{base_url}#{url}?task_id=#{task_id}", "Authorization" => token)
response = render_response(result) if content.is_a?(Tempfile)
check_file_path
IO.copy_stream(content, "#{save_path}/#{task_id}.pdf")
return {code: 200, download_url: "/trace_task_results/#{task_id}.pdf"}
else
return {code: 404}
end
end end
private private
def request_params def check_file_path
{ FileUtils.mkdir_p save_path
task_id: task_id end
}
def save_path
"public/trace_task_results"
end end
def url def url

View File

@ -31,7 +31,9 @@
<%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %> <%= submit_tag('搜索', class: 'btn btn-primary ml-3', 'data-disable-with': '搜索中...') %>
<% end %> <% end %>
<%= javascript_void_link '导入用户', class: 'btn btn-secondary btn-sm', data: { toggle: 'modal', target: '.admin-import-user-modal'} %> <%= link_to '下载导入模板', "/导入用户模板.xlsx", class: 'btn btn-secondary mr-3' %>
<%= javascript_void_link '导入用户', class: 'btn btn-secondary', data: { toggle: 'modal', target: '.admin-import-user-modal'} %>
</div> </div>

View File

@ -9,7 +9,10 @@ if @project.forge?
json.path entry['path'] json.path entry['path']
json.type entry['type'] json.type entry['type']
json.size entry['size'] json.size entry['size']
is_readme = is_readme?(entry['type'], entry['name'])
if is_readme
json.replace_content readme_decode64_content(entry, @owner, @repository, @ref, @path)
end
json.content (direct_download || image_type || is_dir) ? nil : decode64_content(entry, @owner, @repository, @ref, @path) json.content (direct_download || image_type || is_dir) ? nil : decode64_content(entry, @owner, @repository, @ref, @path)
json.target entry['target'] json.target entry['target']
@ -25,7 +28,7 @@ if @project.forge?
json.direct_download direct_download json.direct_download direct_download
json.image_type image_type json.image_type image_type
json.is_readme_file is_readme?(entry['type'], entry['name']) json.is_readme_file is_readme
json.commit do json.commit do
json.partial! 'last_commit', latest_commit: entry['latest_commit'] json.partial! 'last_commit', latest_commit: entry['latest_commit']
end end

View File

@ -25,3 +25,4 @@ json.description @user.description
json.super_description @user.super_description json.super_description @user.super_description
json.(@user, :show_email, :show_department, :show_location, :show_super_description) json.(@user, :show_email, :show_department, :show_location, :show_super_description)
json.message_unread_total @message_unread_total json.message_unread_total @message_unread_total
json.has_trace_user @user.trace_user.present?

View File

@ -427,6 +427,20 @@ Rails.application.routes.draw do
end end
end end
namespace :traces do
resources :trace_users, only: [:create]
scope "/:owner/:repo" do
resource :projects, path: '/', only: [:index] do
member do
post :tasks
get :task_results
get :reload_task
get :task_pdf
end
end
end
end
# Project Area START # Project Area START
scope "/:owner/:repo" do scope "/:owner/:repo" do
scope do scope do

View File

@ -0,0 +1,12 @@
class CreateUserTraceTasks < ActiveRecord::Migration[5.2]
def change
create_table :user_trace_tasks do |t|
t.references :user
t.references :project
t.string :branch_tag
t.string :task_id
t.timestamps
end
end
end

View File

@ -543,6 +543,26 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>
<a href="#traces" class="toc-h1 toc-link" data-title="Traces">Traces</a>
<ul class="toc-list-h2">
<li>
<a href="#ca438fc3ca" class="toc-h2 toc-link" data-title="代码溯源初始化">代码溯源初始化</a>
</li>
<li>
<a href="#bb16c601f1" class="toc-h2 toc-link" data-title="代码分析结果列表">代码分析结果列表</a>
</li>
<li>
<a href="#32497859e0" class="toc-h2 toc-link" data-title="新建分析">新建分析</a>
</li>
<li>
<a href="#7b3a48e274" class="toc-h2 toc-link" data-title="重新扫描">重新扫描</a>
</li>
<li>
<a href="#87775b1430" class="toc-h2 toc-link" data-title="下载报告">下载报告</a>
</li>
</ul>
</li>
<li> <li>
<a href="#pulls" class="toc-h1 toc-link" data-title="Pulls">Pulls</a> <a href="#pulls" class="toc-h1 toc-link" data-title="Pulls">Pulls</a>
<ul class="toc-list-h2"> <ul class="toc-list-h2">
@ -4968,7 +4988,7 @@ http://localhost:3000/api/yystopf/ceshi/menu_list | jq
<tr> <tr>
<td>menu_name</td> <td>menu_name</td>
<td>string</td> <td>string</td>
<td>导航名称, home:主页,code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,activity:动态,setting:仓库设置</td> <td>导航名称, home:主页,code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,wiki:维基,services:服务,activity:动态,setting:仓库设置</td>
</tr> </tr>
</tbody></table> </tbody></table>
@ -5131,7 +5151,7 @@ http://localhost:3000/api/yystopf/ceshi/project_units.json
<td></td> <td></td>
<td></td> <td></td>
<td>array</td> <td>array</td>
<td>项目模块内容, 支持以下参数:code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑</td> <td>项目模块内容, 支持以下参数:code:代码库,issues:疑修,pulls:合并请求,devops:工作流,versions:里程碑,wiki:维基,resources:资源库,services:服务</td>
</tr> </tr>
</tbody></table> </tbody></table>
<h3 id='7447e4874e-2'>返回字段说明:</h3> <h3 id='7447e4874e-2'>返回字段说明:</h3>
@ -9302,6 +9322,289 @@ http://localhost:3000/api/yystopf/ceshi/webhooks/3/test.json
<aside class="success"> <aside class="success">
Success Data. Success Data.
</aside> </aside>
<h1 id='traces'>Traces</h1><h2 id='ca438fc3ca'>代码溯源初始化</h2>
<p>用户同意协议后请求的接口,创建代码溯源的账号</p>
<blockquote>
<p>示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">-X</span> POST <span class="se">\</span>
http://localhost:3000/api/traces/trace_users.json
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="dl">'</span><span class="s1">POST /api/traces/trace_users.json</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div><h3 id='http'>HTTP 请求</h3>
<p><code>POST api/traces/trace_users.json</code></p>
<blockquote>
<p>返回的JSON示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight json tab-json"><code><span class="p">{</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
<h2 id='bb16c601f1'>代码分析结果列表</h2>
<p>查询项目下代码分析的结果</p>
<blockquote>
<p>示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">-X</span> GET <span class="se">\</span>
http://localhost:3000/api/traces/yystopf/many_branch/task_results.json
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="dl">'</span><span class="s1">GET /api/traces/:owner/:repo/task_results.json</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div><h3 id='http-2'>HTTP 请求</h3>
<p><code>GET api/traces/:owner/:repo/task_results.json</code></p>
<h3 id='1f9ac54b15'>请求参数</h3>
<table><thead>
<tr>
<th>参数</th>
<th>必选</th>
<th>默认</th>
<th>类型</th>
<th>字段说明</th>
</tr>
</thead><tbody>
<tr>
<td>owner</td>
<td></td>
<td></td>
<td>string</td>
<td>项目所有者标识</td>
</tr>
<tr>
<td>repo</td>
<td></td>
<td></td>
<td>string</td>
<td>项目标识</td>
</tr>
<tr>
<td>page</td>
<td></td>
<td>1</td>
<td>int</td>
<td>页码</td>
</tr>
<tr>
<td>limit</td>
<td></td>
<td>15</td>
<td>int</td>
<td>每页数量</td>
</tr>
</tbody></table>
<h3 id='90889036d2'>返回字段说明(暂缺)</h3>
<!-- 参数 | 类型 | 字段说明
--------- | ----------- | -----------
total_count |int |总数 |
public_keys.id |int |ID|
public_keys.name |string|密钥标题|
public_keys.content |string|密钥内容|
public_keys.fingerprint |string|密钥标识|
public_keys.created_time |string|密钥创建时间| -->
<blockquote>
<p>返回的JSON示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight json tab-json"><code><span class="p">{</span><span class="w">
</span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"accuracy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20"</span><span class="p">,</span><span class="w">
</span><span class="nl">"code_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"C"</span><span class="p">,</span><span class="w">
</span><span class="nl">"depth"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"detect_flag"</span><span class="p">:</span><span class="w"> </span><span class="s2">"快速-组件级"</span><span class="p">,</span><span class="w">
</span><span class="nl">"detect_rule"</span><span class="p">:</span><span class="w"> </span><span class="s2">"快速-组件级,2,20,,开源软件,50,10"</span><span class="p">,</span><span class="w">
</span><span class="nl">"detect_startdate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-05-10 15:59:46 "</span><span class="p">,</span><span class="w">
</span><span class="nl">"detect_status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fail"</span><span class="p">,</span><span class="w">
</span><span class="nl">"detectflag"</span><span class="p">:</span><span class="w"> </span><span class="s2">"快速-组件级"</span><span class="p">,</span><span class="w">
</span><span class="nl">"fail_reason"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid package type"</span><span class="p">,</span><span class="w">
</span><span class="nl">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"many_branch.zip"</span><span class="p">,</span><span class="w">
</span><span class="nl">"license_process"</span><span class="p">:</span><span class="w"> </span><span class="s2">"100"</span><span class="p">,</span><span class="w">
</span><span class="nl">"licenseparam"</span><span class="p">:</span><span class="w"> </span><span class="s2">"开源软件"</span><span class="p">,</span><span class="w">
</span><span class="nl">"package_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"product_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"84727546110"</span><span class="p">,</span><span class="w">
</span><span class="nl">"project_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"6dbc3e42-5857-4ca4-a54d-58fd9dbf6dc5"</span><span class="p">,</span><span class="w">
</span><span class="nl">"sim_process"</span><span class="p">:</span><span class="w"> </span><span class="s2">"100"</span><span class="p">,</span><span class="w">
</span><span class="nl">"similarity_process"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2"</span><span class="p">,</span><span class="w">
</span><span class="nl">"task_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"15139171-091b-4316-98b1-6068970efa44"</span><span class="p">,</span><span class="w">
</span><span class="nl">"totalsize"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span><span class="w">
</span><span class="nl">"uid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"78"</span><span class="p">,</span><span class="w">
</span><span class="nl">"vuln_process"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"vulnlevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
<h2 id='32497859e0'>新建分析</h2>
<p>用户选择仓库分支进行代码分析的接口</p>
<blockquote>
<p>示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">-X</span> POST <span class="se">\</span>
http://localhost:3000/api/traces/yystopf/many_branch/tasks.json
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="dl">'</span><span class="s1">POST /api/traces/:owner/:repo/tasks.json</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div><h3 id='http-3'>HTTP 请求</h3>
<p><code>POST api/traces/:owner/:repo/tasks.json</code></p>
<h3 id='1f9ac54b15-2'>请求参数</h3>
<table><thead>
<tr>
<th>参数</th>
<th>必选</th>
<th>默认</th>
<th>类型</th>
<th>字段说明</th>
</tr>
</thead><tbody>
<tr>
<td>owner</td>
<td></td>
<td></td>
<td>string</td>
<td>项目所有者标识</td>
</tr>
<tr>
<td>repo</td>
<td></td>
<td></td>
<td>string</td>
<td>项目标识</td>
</tr>
<tr>
<td>branch_name</td>
<td></td>
<td></td>
<td>string</td>
<td>分支名称</td>
</tr>
</tbody></table>
<blockquote>
<p>返回的JSON示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight json tab-json"><code><span class="p">{</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
<h2 id='7b3a48e274'>重新扫描</h2>
<p>对代码分析结果进行再次分析</p>
<blockquote>
<p>示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">-X</span> GET <span class="se">\</span>
http://localhost:3000/api/traces/yystopf/many_branch/reload_task.json
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="dl">'</span><span class="s1">GET /api/traces/:owner/:repo/reload_task.json</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div><h3 id='http-4'>HTTP 请求</h3>
<p><code>GET api/traces/:owner/:repo/reload_task.json</code></p>
<h3 id='1f9ac54b15-3'>请求参数</h3>
<table><thead>
<tr>
<th>参数</th>
<th>必选</th>
<th>默认</th>
<th>类型</th>
<th>字段说明</th>
</tr>
</thead><tbody>
<tr>
<td>owner</td>
<td></td>
<td></td>
<td>string</td>
<td>项目所有者标识</td>
</tr>
<tr>
<td>repo</td>
<td></td>
<td></td>
<td>string</td>
<td>项目标识</td>
</tr>
<tr>
<td>project_id</td>
<td></td>
<td></td>
<td>string</td>
<td>代码分析结果里的project_id</td>
</tr>
</tbody></table>
<blockquote>
<p>返回的JSON示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight json tab-json"><code><span class="p">{</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
<h2 id='87775b1430'>下载报告</h2>
<p>把代码分析的结果下载到本地</p>
<blockquote>
<p>示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight shell tab-shell"><code>curl <span class="nt">-X</span> GET <span class="se">\</span>
http://localhost:3000/api/traces/yystopf/many_branch/task_pdf.json
</code></pre></div><div class="highlight"><pre class="highlight javascript tab-javascript"><code><span class="k">await</span> <span class="nx">octokit</span><span class="p">.</span><span class="nx">request</span><span class="p">(</span><span class="dl">'</span><span class="s1">GET /api/traces/:owner/:repo/task_pdf.json</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div><h3 id='http-5'>HTTP 请求</h3>
<p><code>GET api/traces/:owner/:repo/task_pdf.json</code></p>
<h3 id='1f9ac54b15-4'>请求参数</h3>
<table><thead>
<tr>
<th>参数</th>
<th>必选</th>
<th>默认</th>
<th>类型</th>
<th>字段说明</th>
</tr>
</thead><tbody>
<tr>
<td>owner</td>
<td></td>
<td></td>
<td>string</td>
<td>项目所有者标识</td>
</tr>
<tr>
<td>repo</td>
<td></td>
<td></td>
<td>string</td>
<td>项目标识</td>
</tr>
<tr>
<td>task_id</td>
<td></td>
<td></td>
<td>string</td>
<td>代码分析结果里的task_id</td>
</tr>
</tbody></table>
<blockquote>
<p>返回的JSON示例:</p>
</blockquote>
<div class="highlight"><pre class="highlight json tab-json"><code><span class="p">{</span><span class="w">
</span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"success"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>
<aside class="success">
Success — a happy kitten is an authenticated kitten!
</aside>
<h1 id='pulls'>Pulls</h1><h2 id='get-a-pull-request'>Get a pull request</h2> <h1 id='pulls'>Pulls</h1><h2 id='get-a-pull-request'>Get a pull request</h2>
<p>获取合并请求详情接口</p> <p>获取合并请求详情接口</p>

Binary file not shown.

View File

@ -0,0 +1,5 @@
require 'rails_helper'
RSpec.describe UserTraceTask, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end