feat(reposync): support custom directory name for repo sync (#132)
This commit is contained in:
@@ -40,6 +40,7 @@ type Config struct {
|
||||
CommentToTask string
|
||||
PreCommand string
|
||||
PostCommand string
|
||||
RepoName string
|
||||
}
|
||||
|
||||
func Run(args []string) {
|
||||
@@ -66,6 +67,7 @@ func Run(args []string) {
|
||||
fs.StringVar(&cfg.CommentToTask, "commenttotask", "false", "Compatible with QL format script comment parsing (true/false)")
|
||||
fs.StringVar(&cfg.PreCommand, "pre-command", "", "Default pre-command for discovered tasks")
|
||||
fs.StringVar(&cfg.PostCommand, "post-command", "", "Default post-command for discovered tasks")
|
||||
fs.StringVar(&cfg.RepoName, "repo-name", "", "Custom repository directory name")
|
||||
|
||||
printHelp := func() {
|
||||
fmt.Fprintf(os.Stderr, "\n白虎面板仓库同步工具 (Reposync)\n\n")
|
||||
@@ -162,7 +164,13 @@ func Run(args []string) {
|
||||
|
||||
func getActualRepoDir(cfg Config) string {
|
||||
if cfg.SourceType == "git" {
|
||||
repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch)
|
||||
repoName := cfg.RepoName
|
||||
if repoName == "" {
|
||||
repoName = utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch)
|
||||
}
|
||||
if repoName == "." {
|
||||
return cfg.TargetPath
|
||||
}
|
||||
return filepath.Join(cfg.TargetPath, repoName)
|
||||
}
|
||||
return cfg.TargetPath
|
||||
@@ -208,10 +216,17 @@ func syncGit(cfg Config) {
|
||||
|
||||
gitDir := filepath.Join(dest, ".git")
|
||||
if isDir(dest) && !pathExists(gitDir) {
|
||||
repoName := utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch)
|
||||
dest = filepath.Join(dest, repoName)
|
||||
fmt.Printf("目标路径自动追加仓库名: %s\n", dest)
|
||||
gitDir = filepath.Join(dest, ".git")
|
||||
repoName := cfg.RepoName
|
||||
if repoName == "" {
|
||||
repoName = utils.GetRepoIdentifier(cfg.SourceURL, cfg.Branch)
|
||||
}
|
||||
if repoName != "." {
|
||||
dest = filepath.Join(dest, repoName)
|
||||
fmt.Printf("目标路径自动追加仓库名: %s\n", dest)
|
||||
gitDir = filepath.Join(dest, ".git")
|
||||
} else {
|
||||
fmt.Printf("目标路径使用当前目录 (不追加仓库名): %s\n", dest)
|
||||
}
|
||||
}
|
||||
|
||||
restore := preserve(dest, cfg.WhitelistPaths)
|
||||
|
||||
@@ -56,6 +56,49 @@ func resolveWorkDir(workDir string) string {
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
// isValidDirName 校验目录名是否合法
|
||||
func isValidDirName(dirName string) bool {
|
||||
if strings.Contains(dirName, "/") || strings.Contains(dirName, "\\") || strings.Contains(dirName, "..") {
|
||||
return false
|
||||
}
|
||||
for _, ch := range dirName {
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '.') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// getRepoPhysicalPath 计算仓库任务的最终物理绝对路径
|
||||
func getRepoPhysicalPath(targetPath, dirName, sourceURL, branch string) string {
|
||||
if dirName == "." {
|
||||
return "" // 如果不追加目录,此逻辑不负责判断其根目录(共享的 scripts 目录)
|
||||
}
|
||||
finalDirName := dirName
|
||||
if finalDirName == "" {
|
||||
finalDirName = utils.GetRepoIdentifier(sourceURL, branch)
|
||||
}
|
||||
if finalDirName == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
basePath := targetPath
|
||||
if basePath == "" || basePath == constant.ScriptsDirPlaceholder {
|
||||
basePath = constant.ScriptsWorkDir
|
||||
} else if strings.HasPrefix(basePath, constant.ScriptsDirPlaceholder) {
|
||||
basePath = filepath.Join(constant.ScriptsWorkDir, strings.TrimPrefix(basePath, constant.ScriptsDirPlaceholder))
|
||||
} else if !filepath.IsAbs(basePath) {
|
||||
basePath = filepath.Join(constant.ScriptsWorkDir, basePath)
|
||||
}
|
||||
|
||||
fullPath := filepath.Join(basePath, finalDirName)
|
||||
absPath, err := filepath.Abs(fullPath)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
// CreateTask 创建任务
|
||||
// @Summary 创建任务
|
||||
// @Description 创建一个新的任务
|
||||
@@ -98,11 +141,37 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
||||
// 如果是仓库同步任务,根据 URL 生成 SourceID 用于去重
|
||||
if req.Type == constant.TaskTypeRepo && req.Config != "" {
|
||||
var repoCfg struct {
|
||||
SourceURL string `json:"source_url"`
|
||||
Branch string `json:"branch"`
|
||||
SourceURL string `json:"source_url"`
|
||||
Branch string `json:"branch"`
|
||||
DirName string `json:"dir_name"`
|
||||
TargetPath string `json:"target_path"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" {
|
||||
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
|
||||
if repoCfg.DirName != "" && repoCfg.DirName != "." {
|
||||
if !isValidDirName(repoCfg.DirName) {
|
||||
utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame
|
||||
if repoCfg.DirName != "" && repoCfg.DirName != "." {
|
||||
sourceID = "repo_" + repoCfg.DirName
|
||||
} else {
|
||||
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
|
||||
}
|
||||
|
||||
// 如果是全新任务,校验物理目录是否存在
|
||||
existingTask := tc.taskService.GetTaskBySourceID(sourceID)
|
||||
if existingTask == nil {
|
||||
newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch)
|
||||
if newAbsPath != "" {
|
||||
if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() {
|
||||
utils.BadRequest(c, "本地已存在同名仓库文件夹,请更换自定义目录名或清理残留文件")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,11 +331,49 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
||||
var sourceID string
|
||||
if req.Type == constant.TaskTypeRepo && req.Config != "" {
|
||||
var repoCfg struct {
|
||||
SourceURL string `json:"source_url"`
|
||||
Branch string `json:"branch"`
|
||||
SourceURL string `json:"source_url"`
|
||||
Branch string `json:"branch"`
|
||||
DirName string `json:"dir_name"`
|
||||
TargetPath string `json:"target_path"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(req.Config), &repoCfg); err == nil && repoCfg.SourceURL != "" {
|
||||
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
|
||||
if repoCfg.DirName != "" && repoCfg.DirName != "." {
|
||||
if !isValidDirName(repoCfg.DirName) {
|
||||
utils.BadRequest(c, "自定义目录名只能包含字母、数字、下划线、短划线和点,且不能包含路径逻辑")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果配置了自定义名字,使用配置的名字。没有配置的话,使用以前的username_reponame
|
||||
if repoCfg.DirName != "" && repoCfg.DirName != "." {
|
||||
sourceID = "repo_" + repoCfg.DirName
|
||||
} else {
|
||||
sourceID = "repo_" + utils.GetRepoIdentifier(repoCfg.SourceURL, repoCfg.Branch)
|
||||
}
|
||||
|
||||
// 计算新的物理路径
|
||||
newAbsPath := getRepoPhysicalPath(repoCfg.TargetPath, repoCfg.DirName, repoCfg.SourceURL, repoCfg.Branch)
|
||||
|
||||
var oldAbsPath string
|
||||
if oldTask != nil && oldTask.Type == constant.TaskTypeRepo && oldTask.Config != "" {
|
||||
var oldCfg struct {
|
||||
SourceURL string `json:"source_url"`
|
||||
Branch string `json:"branch"`
|
||||
DirName string `json:"dir_name"`
|
||||
TargetPath string `json:"target_path"`
|
||||
}
|
||||
if json.Unmarshal([]byte(oldTask.Config), &oldCfg) == nil {
|
||||
oldAbsPath = getRepoPhysicalPath(oldCfg.TargetPath, oldCfg.DirName, oldCfg.SourceURL, oldCfg.Branch)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果路径发生了改变(或者是个全新计算的路径),并且新路径已存在,则报错拦截
|
||||
if newAbsPath != "" && newAbsPath != oldAbsPath {
|
||||
if info, err := os.Stat(newAbsPath); err == nil && info.IsDir() {
|
||||
utils.BadRequest(c, "目标目录在本地已存在同名文件夹,请更换目录名或清理残留文件")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if oldTask != nil {
|
||||
sourceID = oldTask.SourceID
|
||||
|
||||
@@ -59,6 +59,7 @@ type RepoConfig struct {
|
||||
AutoAddCron bool `json:"auto_add_cron"` // 自动解析脚本注释添加定时任务
|
||||
CommentToTask string `json:"commenttotask"` // 兼容 QL 格式任务脚本注释解析
|
||||
RepoSource string `json:"repo_source"` // 仓库来源,如果是选择了这个 ql 导入的仓库,= ql
|
||||
DirName string `json:"dir_name"` // 自定义仓库目录名 (可填 "." 表示不追加子目录)
|
||||
}
|
||||
|
||||
// TaskConfig 任务配置 RepoConfig+TaskConfig=task.config
|
||||
|
||||
@@ -1267,6 +1267,9 @@ func BuildRepoCommand(task *models.Task) (string, string) {
|
||||
if config.Extensions != "" {
|
||||
args = append(args, "--extensions", config.Extensions)
|
||||
}
|
||||
if config.DirName != "" {
|
||||
args = append(args, "--repo-name", config.DirName)
|
||||
}
|
||||
if string(task.PreCommand) != "" {
|
||||
args = append(args, "--pre-command", string(task.PreCommand))
|
||||
}
|
||||
|
||||
@@ -487,6 +487,7 @@ export interface RepoConfig {
|
||||
commenttotask?: string
|
||||
concurrency?: number
|
||||
repo_source?: string
|
||||
dir_name?: string
|
||||
}
|
||||
|
||||
export interface ExecutionResult {
|
||||
|
||||
@@ -126,6 +126,9 @@ export function parseBaihuCommand(command: string): ParsedRepoResult | null {
|
||||
console.error('Parse task-langs failed', e)
|
||||
}
|
||||
break
|
||||
case '--repo-name':
|
||||
repoConfig.dir_name = value
|
||||
break
|
||||
case '--pre-command':
|
||||
task.pre_command = value
|
||||
break
|
||||
@@ -213,6 +216,9 @@ export function generateBaihuCommand(task: Task): string {
|
||||
if (config.branch) {
|
||||
args.push('--branch', config.branch)
|
||||
}
|
||||
if (config.dir_name) {
|
||||
args.push('--repo-name', config.dir_name)
|
||||
}
|
||||
if (config.sparse_path) {
|
||||
args.push('--path', config.sparse_path)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ const repoConfig = ref<RepoConfig>({
|
||||
commenttotask: 'false',
|
||||
concurrency: 1,
|
||||
repo_source: '',
|
||||
proxy: ''
|
||||
proxy: '',
|
||||
dir_name: ''
|
||||
})
|
||||
|
||||
const allAgents = ref<Agent[]>([])
|
||||
@@ -98,6 +99,7 @@ function exportBaihuCommand() {
|
||||
if (repoConfig.value.source_url) parts.push(`--source-url "${repoConfig.value.source_url}"`)
|
||||
if (repoConfig.value.target_path) parts.push(`--target-path "${repoConfig.value.target_path}"`)
|
||||
if (repoConfig.value.branch) parts.push(`--branch "${repoConfig.value.branch}"`)
|
||||
if (repoConfig.value.dir_name) parts.push(`--repo-name "${repoConfig.value.dir_name}"`)
|
||||
if (repoConfig.value.sparse_path) parts.push(`--path "${repoConfig.value.sparse_path}"`)
|
||||
if (repoConfig.value.single_file) parts.push(`--single-file`)
|
||||
if (repoConfig.value.proxy && repoConfig.value.proxy !== 'none') parts.push(`--proxy ${repoConfig.value.proxy}`)
|
||||
@@ -228,7 +230,8 @@ watch(() => props.open, async (val: boolean) => {
|
||||
auto_add_cron: false,
|
||||
commenttotask: 'false',
|
||||
concurrency: 1,
|
||||
repo_source: ''
|
||||
repo_source: '',
|
||||
dir_name: ''
|
||||
}
|
||||
const configStr = props.task?.config
|
||||
if (configStr) {
|
||||
@@ -412,6 +415,13 @@ async function save() {
|
||||
<Input v-else v-model="repoConfig.target_path" placeholder="Agent 上的目标路径" class="h-9 bg-muted/30 border-muted-foreground/20" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="repoConfig.source_type === 'git'" class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">
|
||||
<Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-medium">目录名定制</Label>
|
||||
<div class="sm:col-span-3 relative">
|
||||
<Input v-model="repoConfig.dir_name" placeholder="自定义生成目录名 (输入 . 表示不追加子目录)" class="h-9 bg-muted/30 border-muted-foreground/20 focus:bg-background transition-all" autocomplete="off" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="repoConfig.source_type === 'git'" class="grid grid-cols-1 sm:grid-cols-4 items-center gap-3">
|
||||
<Label class="sm:text-right text-xs text-foreground/70 uppercase tracking-wider font-medium">分支</Label>
|
||||
<Input v-model="repoConfig.branch" placeholder="main (默认)" class="sm:col-span-3 h-9 bg-muted/30 border-muted-foreground/20 focus:bg-background transition-all" autocomplete="off" />
|
||||
|
||||
Reference in New Issue
Block a user