代码提交
This commit is contained in:
@@ -20,14 +20,10 @@ import java.util.LinkedHashMap;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
import java.util.concurrent.CancellationException;
|
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.Future;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
@@ -558,11 +554,18 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
|||||||
|
|
||||||
// ==================== Git 命令执行 ====================
|
// ==================== Git 命令执行 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行一条 Git 命令,总超时 CMD_TIMEOUT_SECONDS + 10 秒。
|
||||||
|
* <p>
|
||||||
|
* 关键:进程输出在独立线程中读取,主线程通过 process.waitFor(timeout) 等待。
|
||||||
|
* 超时后强制杀进程 + 中断读线程,杜绝僵尸进程 / 永久阻塞。
|
||||||
|
*/
|
||||||
private CmdResult runGit(String workingDir, String command) {
|
private CmdResult runGit(String workingDir, String command) {
|
||||||
// 用 Future 包一层,防止 readLine() 因进程卡死而永久阻塞
|
|
||||||
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
|
|
||||||
Future<CmdResult> future = singleExecutor.submit(() -> {
|
|
||||||
CmdResult result = new CmdResult();
|
CmdResult result = new CmdResult();
|
||||||
|
Process process = null;
|
||||||
|
Thread readerThread = null;
|
||||||
|
StringBuilder output = new StringBuilder();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ProcessBuilder pb = isWindows()
|
ProcessBuilder pb = isWindows()
|
||||||
? new ProcessBuilder("cmd.exe", "/c", command)
|
? new ProcessBuilder("cmd.exe", "/c", command)
|
||||||
@@ -572,25 +575,36 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
|||||||
pb.directory(new File(workingDir));
|
pb.directory(new File(workingDir));
|
||||||
}
|
}
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
|
process = pb.start();
|
||||||
|
final Process proc = process; // effectively final 副本供 lambda 引用
|
||||||
|
|
||||||
Process process = pb.start();
|
// 独立线程读取进程输出,避免 readLine() 阻塞主逻辑
|
||||||
StringBuilder output = new StringBuilder();
|
readerThread = new Thread(() -> {
|
||||||
try (BufferedReader reader = new BufferedReader(
|
try (BufferedReader reader = new BufferedReader(
|
||||||
new InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
|
new InputStreamReader(proc.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
output.append(line).append("\n");
|
output.append(line).append("\n");
|
||||||
}
|
}
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// 进程被杀时流关闭会抛异常,正常情况
|
||||||
}
|
}
|
||||||
|
}, "git-reader");
|
||||||
|
readerThread.setDaemon(true);
|
||||||
|
readerThread.start();
|
||||||
|
|
||||||
|
// 等待进程结束(带超时)
|
||||||
boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
process.destroyForcibly();
|
log.warn("Git 命令超时({}s): {} -> {}", CMD_TIMEOUT_SECONDS, workingDir, command);
|
||||||
result.ok = false;
|
result.ok = false;
|
||||||
result.error = "命令超时(" + CMD_TIMEOUT_SECONDS + "s)";
|
result.error = "命令超时(" + CMD_TIMEOUT_SECONDS + "s)";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 进程已结束,等待读线程收尾(最多10秒)
|
||||||
|
readerThread.join(10_000);
|
||||||
|
|
||||||
int exitCode = process.exitValue();
|
int exitCode = process.exitValue();
|
||||||
result.ok = (exitCode == 0);
|
result.ok = (exitCode == 0);
|
||||||
result.output = output.toString().trim();
|
result.output = output.toString().trim();
|
||||||
@@ -600,31 +614,28 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
|||||||
? "exit code " + exitCode
|
? "exit code " + exitCode
|
||||||
: "exit code " + exitCode + ": " + detail;
|
: "exit code " + exitCode + ": " + detail;
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
result.ok = false;
|
||||||
|
result.error = "命令被中断";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
result.ok = false;
|
|
||||||
result.error = e.getMessage();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 整体超时比内部 waitFor 多给 10 秒缓冲
|
|
||||||
return future.get(CMD_TIMEOUT_SECONDS + 10, TimeUnit.SECONDS);
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
future.cancel(true);
|
|
||||||
CmdResult result = new CmdResult();
|
|
||||||
result.ok = false;
|
|
||||||
result.error = "命令超时(" + (CMD_TIMEOUT_SECONDS + 10) + "s,进程被强制终止)";
|
|
||||||
return result;
|
|
||||||
} catch (InterruptedException | ExecutionException | CancellationException e) {
|
|
||||||
CmdResult result = new CmdResult();
|
|
||||||
result.ok = false;
|
result.ok = false;
|
||||||
result.error = "执行异常: " + e.getMessage();
|
result.error = "执行异常: " + e.getMessage();
|
||||||
return result;
|
|
||||||
} finally {
|
} finally {
|
||||||
singleExecutor.shutdownNow();
|
// 确保进程和读线程都被清理
|
||||||
|
if (process != null) {
|
||||||
|
if (process.isAlive()) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
try { process.waitFor(5, TimeUnit.SECONDS); } catch (InterruptedException ignored) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (readerThread != null && readerThread.isAlive()) {
|
||||||
|
readerThread.interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private String getRemoteUrl(String repoPath, Map<String, String> baseUrlMap) {
|
private String getRemoteUrl(String repoPath, Map<String, String> baseUrlMap) {
|
||||||
// 1. 尝试 git remote get-url
|
// 1. 尝试 git remote get-url
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -15,6 +16,8 @@ import java.util.List;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@@ -22,6 +25,9 @@ import java.util.regex.Pattern;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
public class ScannerService {
|
public class ScannerService {
|
||||||
|
|
||||||
|
/** 单个项目扫描超时时间(分钟) */
|
||||||
|
private static final long SCAN_TIMEOUT_MINUTES = 5;
|
||||||
|
|
||||||
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
|
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
|
||||||
@Autowired
|
@Autowired
|
||||||
private FileOperationHelper fileOperationHelper;
|
private FileOperationHelper fileOperationHelper;
|
||||||
@@ -30,11 +36,25 @@ public class ScannerService {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private WorkUtilsProperties properties;
|
private WorkUtilsProperties properties;
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void shutdown() {
|
||||||
|
log.info("正在关闭扫描线程池...");
|
||||||
|
executorService.shutdown();
|
||||||
|
try {
|
||||||
|
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
executorService.shutdownNow();
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
log.info("扫描线程池已关闭");
|
||||||
|
}
|
||||||
|
|
||||||
public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception {
|
public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception {
|
||||||
List<String> projectPaths = fileOperationHelper.getProjectPaths();
|
List<String> projectPaths = fileOperationHelper.getProjectPaths();
|
||||||
if (projectPaths.isEmpty()) {
|
if (projectPaths.isEmpty()) {
|
||||||
log.warn("No project paths found");
|
throw new IllegalStateException("未配置项目路径,请先在「系统设置」中添加要扫描的项目路径");
|
||||||
return new ScanResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ScanResult result = new ScanResult();
|
ScanResult result = new ScanResult();
|
||||||
@@ -53,9 +73,25 @@ public class ScannerService {
|
|||||||
futures.add(future);
|
futures.add(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int total = futures.size();
|
||||||
|
int idx = 0;
|
||||||
for (CompletableFuture<ProjectScanResult> future : futures) {
|
for (CompletableFuture<ProjectScanResult> future : futures) {
|
||||||
ProjectScanResult projectResult = future.get();
|
idx++;
|
||||||
|
String pathHint = (idx <= projectPaths.size())
|
||||||
|
? " (" + fileOperationHelper.extractProjectName(projectPaths.get(idx - 1)) + ")"
|
||||||
|
: "";
|
||||||
|
try {
|
||||||
|
ProjectScanResult projectResult = future.get(SCAN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
||||||
result.addProjectResult(projectResult);
|
result.addProjectResult(projectResult);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
log.error("项目扫描超时({}分钟)第{}/{}个项目{},已跳过",
|
||||||
|
SCAN_TIMEOUT_MINUTES, idx, total, pathHint);
|
||||||
|
// 超时后仍尝试取消该任务
|
||||||
|
future.cancel(true);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取扫描结果异常 第{}/{}个项目{}: {}",
|
||||||
|
idx, total, pathHint, e.getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -25,8 +25,10 @@ public class ConfigInitializer implements ApplicationListener<ApplicationReadyEv
|
|||||||
log.info("发现外部配置文件: {}", configManager.getConfigFile().toAbsolutePath());
|
log.info("发现外部配置文件: {}", configManager.getConfigFile().toAbsolutePath());
|
||||||
properties.mergeExternal(configManager.load());
|
properties.mergeExternal(configManager.load());
|
||||||
} else {
|
} else {
|
||||||
log.info("未找到外部配置文件 workutils.config,使用 application.yml 默认值");
|
log.info("未找到外部配置文件 workutils.config,正在使用 application.yml 默认值生成...");
|
||||||
log.info("可通过「系统设置(S)」创建并编辑 workutils.config");
|
configManager.saveSectioned(configManager.generateDefaults(properties));
|
||||||
|
properties.mergeExternal(configManager.load());
|
||||||
|
log.info("已生成默认外部配置文件: {}", configManager.getConfigFile().toAbsolutePath());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,14 @@ public class ExternalConfigManager {
|
|||||||
for (String k : mybatisKeys) {
|
for (String k : mybatisKeys) {
|
||||||
props.setProperty(k, "");
|
props.setProperty(k, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 字体大小默认值
|
||||||
|
props.setProperty(KEY_FONT_SECTION_TITLE, String.valueOf(appProps.getFontSectionTitle()));
|
||||||
|
props.setProperty(KEY_FONT_FIELD_LABEL, String.valueOf(appProps.getFontFieldLabel()));
|
||||||
|
props.setProperty(KEY_FONT_BUTTON, String.valueOf(appProps.getFontButton()));
|
||||||
|
props.setProperty(KEY_FONT_BODY, String.valueOf(appProps.getFontBody()));
|
||||||
|
props.setProperty(KEY_FONT_MONO, String.valueOf(appProps.getFontMono()));
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ public class SettingsWindow {
|
|||||||
private TextArea taDamagedCache;
|
private TextArea taDamagedCache;
|
||||||
private Label lblProjectPathsFile, lblDamagedCacheFile;
|
private Label lblProjectPathsFile, lblDamagedCacheFile;
|
||||||
|
|
||||||
|
// --- 项目路径替换 ---
|
||||||
|
private TextField tfReplaceParentDir, tfReplaceAccount;
|
||||||
|
|
||||||
// --- 字体大小 ---
|
// --- 字体大小 ---
|
||||||
private int fontSectionTitle;
|
private int fontSectionTitle;
|
||||||
private int fontFieldLabel;
|
private int fontFieldLabel;
|
||||||
@@ -270,7 +273,7 @@ public class SettingsWindow {
|
|||||||
btnOpenPP.setOnAction(e -> openInNotepad(properties.getProjectPathsFilePath()));
|
btnOpenPP.setOnAction(e -> openInNotepad(properties.getProjectPathsFilePath()));
|
||||||
|
|
||||||
HBox ppBtns = new HBox(10, btnLoadPP, btnSavePP, btnOpenPP);
|
HBox ppBtns = new HBox(10, btnLoadPP, btnSavePP, btnOpenPP);
|
||||||
ppBox.getChildren().addAll(lblProjectPathsFile, taProjectPaths, ppBtns);
|
ppBox.getChildren().addAll(lblProjectPathsFile, taProjectPaths, ppBtns, createReplaceTool());
|
||||||
|
|
||||||
Tab ppTab = new Tab("项目路径");
|
Tab ppTab = new Tab("项目路径");
|
||||||
ppTab.setContent(ppBox);
|
ppTab.setContent(ppBox);
|
||||||
@@ -724,6 +727,237 @@ public class SettingsWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 项目路径替换工具 ====================
|
||||||
|
|
||||||
|
private VBox createReplaceTool() {
|
||||||
|
VBox box = new VBox(8);
|
||||||
|
box.setPadding(new Insets(8, 0, 0, 0));
|
||||||
|
box.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 1 0 0 0; -fx-padding: 10 0 0 0;");
|
||||||
|
|
||||||
|
Label title = new Label("批量字符替换");
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px;");
|
||||||
|
titleLabels.add(title);
|
||||||
|
box.getChildren().add(title);
|
||||||
|
|
||||||
|
// 父目录替换行
|
||||||
|
HBox parentRow = new HBox(8);
|
||||||
|
parentRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
Label lblParent = new Label("父目录: ");
|
||||||
|
lblParent.setFont(font(fontFieldLabel));
|
||||||
|
fieldLabels.add(lblParent);
|
||||||
|
tfReplaceParentDir = new TextField();
|
||||||
|
tfReplaceParentDir.setPromptText("自动检测当前父目录,在此输入新父目录");
|
||||||
|
tfReplaceParentDir.setPrefWidth(260);
|
||||||
|
textInputs.add(tfReplaceParentDir);
|
||||||
|
Button btnBrowseParent = addSmallButton("浏览...");
|
||||||
|
btnBrowseParent.setOnAction(e -> browseForParentDir());
|
||||||
|
Button btnDetectParent = addSmallButton("检测");
|
||||||
|
btnDetectParent.setOnAction(e -> detectParentDir());
|
||||||
|
Button btnApplyParent = addSmallButton("替换");
|
||||||
|
btnApplyParent.setOnAction(e -> applyReplaceParentDir());
|
||||||
|
parentRow.getChildren().addAll(lblParent, tfReplaceParentDir, btnBrowseParent, btnDetectParent, btnApplyParent);
|
||||||
|
|
||||||
|
// 账号替换行
|
||||||
|
HBox accountRow = new HBox(8);
|
||||||
|
accountRow.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
Label lblAccount = new Label("账 号: ");
|
||||||
|
lblAccount.setFont(font(fontFieldLabel));
|
||||||
|
fieldLabels.add(lblAccount);
|
||||||
|
tfReplaceAccount = new TextField();
|
||||||
|
tfReplaceAccount.setPromptText("自动检测当前账号,在此输入新账号");
|
||||||
|
tfReplaceAccount.setPrefWidth(300);
|
||||||
|
textInputs.add(tfReplaceAccount);
|
||||||
|
Button btnDetectAccount = addSmallButton("检测");
|
||||||
|
btnDetectAccount.setOnAction(e -> detectAccount());
|
||||||
|
Button btnApplyAccount = addSmallButton("替换");
|
||||||
|
btnApplyAccount.setOnAction(e -> applyReplaceAccount());
|
||||||
|
accountRow.getChildren().addAll(lblAccount, tfReplaceAccount, btnDetectAccount, btnApplyAccount);
|
||||||
|
|
||||||
|
box.getChildren().addAll(parentRow, accountRow);
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从第一条有效项目路径中检测父目录 */
|
||||||
|
private void detectParentDir() {
|
||||||
|
String firstPath = findFirstProjectPath();
|
||||||
|
if (firstPath == null) {
|
||||||
|
showAlert(AlertType.WARNING, "项目路径为空,无法检测父目录。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 取 / 分割后的第一个非空部分作为父目录,如 D:/bestpay-srdcloud/ft-xxx 中的 D:/bestpay-srdcloud
|
||||||
|
int slashIdx = firstPath.indexOf('/');
|
||||||
|
if (slashIdx < 0) {
|
||||||
|
tfReplaceParentDir.setText(firstPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 找到第一个 / 后,继续找 /ft-xxx 的 /,即项目目录前一个 /
|
||||||
|
int nextSlash = firstPath.indexOf('/', slashIdx + 1);
|
||||||
|
if (nextSlash > 0) {
|
||||||
|
tfReplaceParentDir.setText(firstPath.substring(0, nextSlash));
|
||||||
|
} else {
|
||||||
|
tfReplaceParentDir.setText(firstPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 浏览选择父目录 */
|
||||||
|
private void browseForParentDir() {
|
||||||
|
DirectoryChooser chooser = new DirectoryChooser();
|
||||||
|
chooser.setTitle("选择父目录");
|
||||||
|
String current = tfReplaceParentDir.getText().trim();
|
||||||
|
if (!current.isEmpty()) {
|
||||||
|
File f = new File(current);
|
||||||
|
if (f.exists()) {
|
||||||
|
chooser.setInitialDirectory(f);
|
||||||
|
} else {
|
||||||
|
// 尝试取已存在的上级目录
|
||||||
|
File parent = f.getParentFile();
|
||||||
|
while (parent != null && !parent.exists()) {
|
||||||
|
parent = parent.getParentFile();
|
||||||
|
}
|
||||||
|
if (parent != null) {
|
||||||
|
chooser.setInitialDirectory(parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File selected = chooser.showDialog(stage);
|
||||||
|
if (selected != null) {
|
||||||
|
tfReplaceParentDir.setText(selected.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从第一条带 URL 的项目路径中检测账号 */
|
||||||
|
private void detectAccount() {
|
||||||
|
String firstLine = findFirstLineWithUrl();
|
||||||
|
if (firstLine == null) {
|
||||||
|
showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径,请确保路径格式为 path=URL。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 格式: path=https://account@code.srdcloud.cn/...
|
||||||
|
int eqIdx = firstLine.indexOf('=');
|
||||||
|
if (eqIdx < 0) return;
|
||||||
|
String url = firstLine.substring(eqIdx + 1).trim();
|
||||||
|
// 提取 @ 前面的账号:https://xxx → 取 // 和 @ 之间的部分
|
||||||
|
int atIdx = url.indexOf('@');
|
||||||
|
int protoIdx = url.indexOf("://");
|
||||||
|
if (atIdx > 0 && protoIdx > 0) {
|
||||||
|
String account = url.substring(protoIdx + 3, atIdx);
|
||||||
|
tfReplaceAccount.setText(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量替换父目录 */
|
||||||
|
private void applyReplaceParentDir() {
|
||||||
|
String newParent = tfReplaceParentDir.getText().trim();
|
||||||
|
if (newParent.isEmpty()) {
|
||||||
|
showAlert(AlertType.WARNING, "请输入新的父目录。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检测当前父目录
|
||||||
|
String firstPath = findFirstProjectPath();
|
||||||
|
if (firstPath == null) {
|
||||||
|
showAlert(AlertType.WARNING, "项目路径为空。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int slashIdx = firstPath.indexOf('/');
|
||||||
|
if (slashIdx < 0) {
|
||||||
|
showAlert(AlertType.WARNING, "无法解析父目录。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int nextSlash = firstPath.indexOf('/', slashIdx + 1);
|
||||||
|
String oldParent = (nextSlash > 0) ? firstPath.substring(0, nextSlash) : firstPath;
|
||||||
|
|
||||||
|
// 规范化:去掉末尾 /
|
||||||
|
if (newParent.endsWith("/") || newParent.endsWith("\\")) {
|
||||||
|
newParent = newParent.substring(0, newParent.length() - 1);
|
||||||
|
}
|
||||||
|
if (oldParent.endsWith("/") || oldParent.endsWith("\\")) {
|
||||||
|
oldParent = oldParent.substring(0, oldParent.length() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = replaceInTextArea(oldParent, newParent);
|
||||||
|
showAlert(AlertType.INFORMATION,
|
||||||
|
"已将 " + count + " 条路径中的父目录从\n" + oldParent + "\n替换为\n" + newParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量替换账号 */
|
||||||
|
private void applyReplaceAccount() {
|
||||||
|
String newAccount = tfReplaceAccount.getText().trim();
|
||||||
|
if (newAccount.isEmpty()) {
|
||||||
|
showAlert(AlertType.WARNING, "请输入新的账号。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 检测当前账号
|
||||||
|
String firstLine = findFirstLineWithUrl();
|
||||||
|
if (firstLine == null) {
|
||||||
|
showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int eqIdx = firstLine.indexOf('=');
|
||||||
|
if (eqIdx < 0) return;
|
||||||
|
String url = firstLine.substring(eqIdx + 1).trim();
|
||||||
|
int atIdx = url.indexOf('@');
|
||||||
|
int protoIdx = url.indexOf("://");
|
||||||
|
if (atIdx <= 0 || protoIdx <= 0) {
|
||||||
|
showAlert(AlertType.WARNING, "无法解析当前账号。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String oldAccount = url.substring(protoIdx + 3, atIdx);
|
||||||
|
|
||||||
|
int count = replaceInTextArea(oldAccount, newAccount);
|
||||||
|
showAlert(AlertType.INFORMATION,
|
||||||
|
"已将 " + count + " 条URL中的账号从\n" + oldAccount + "\n替换为\n" + newAccount);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在 TextArea 中替换文本,返回替换行数 */
|
||||||
|
private int replaceInTextArea(String oldStr, String newStr) {
|
||||||
|
String text = taProjectPaths.getText();
|
||||||
|
int count = 0;
|
||||||
|
String[] lines = text.split("\n");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String line : lines) {
|
||||||
|
if (!line.trim().isEmpty() && !line.trim().startsWith("#") && line.contains(oldStr)) {
|
||||||
|
sb.append(line.replace(oldStr, newStr)).append("\n");
|
||||||
|
count++;
|
||||||
|
} else {
|
||||||
|
sb.append(line).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
taProjectPaths.setText(sb.toString());
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 找到第一条有效项目路径(只取路径部分,不含 URL) */
|
||||||
|
private String findFirstProjectPath() {
|
||||||
|
String text = taProjectPaths.getText();
|
||||||
|
if (text == null || text.trim().isEmpty()) return null;
|
||||||
|
for (String line : text.split("\n")) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
int eqIdx = line.indexOf('=');
|
||||||
|
if (eqIdx >= 0) {
|
||||||
|
return line.substring(0, eqIdx).trim();
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 找到第一条同时包含路径和 URL 的行 */
|
||||||
|
private String findFirstLineWithUrl() {
|
||||||
|
String text = taProjectPaths.getText();
|
||||||
|
if (text == null || text.trim().isEmpty()) return null;
|
||||||
|
for (String line : text.split("\n")) {
|
||||||
|
line = line.trim();
|
||||||
|
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||||
|
int eqIdx = line.indexOf('=');
|
||||||
|
if (eqIdx >= 0) {
|
||||||
|
String url = line.substring(eqIdx + 1).trim();
|
||||||
|
if (!url.isEmpty()) return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void openInNotepad(Path file) {
|
private void openInNotepad(Path file) {
|
||||||
try {
|
try {
|
||||||
if (!Files.exists(file)) {
|
if (!Files.exists(file)) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.springframework.core.io.ClassPathResource;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
@@ -69,6 +70,24 @@ public class WorkUtilsProperties {
|
|||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
void snapshotDefaults() {
|
void snapshotDefaults() {
|
||||||
|
// 基础目录:未配置则默认取运行目录(user.dir)
|
||||||
|
if (baseDirectory == null || baseDirectory.trim().isEmpty()) {
|
||||||
|
baseDirectory = System.getProperty("user.dir");
|
||||||
|
log.info("基础目录未配置,默认取运行目录: {}", baseDirectory);
|
||||||
|
}
|
||||||
|
// 导出目录:未配置则默认 {user.dir}/export
|
||||||
|
if (exportDirectory == null || exportDirectory.trim().isEmpty()) {
|
||||||
|
exportDirectory = System.getProperty("user.dir") + "/export";
|
||||||
|
log.info("导出目录未配置,默认: {}", exportDirectory);
|
||||||
|
ensureExportDirExists();
|
||||||
|
} else if (!Paths.get(exportDirectory).isAbsolute()) {
|
||||||
|
// 相对路径 → 解析到 user.dir 下
|
||||||
|
exportDirectory = Paths.get(System.getProperty("user.dir"), exportDirectory).toString();
|
||||||
|
ensureExportDirExists();
|
||||||
|
} else {
|
||||||
|
ensureExportDirExists();
|
||||||
|
}
|
||||||
|
|
||||||
defaults.put(ExternalConfigManager.KEY_BASE_DIR, baseDirectory);
|
defaults.put(ExternalConfigManager.KEY_BASE_DIR, baseDirectory);
|
||||||
defaults.put(ExternalConfigManager.KEY_EXPORT_DIR, exportDirectory);
|
defaults.put(ExternalConfigManager.KEY_EXPORT_DIR, exportDirectory);
|
||||||
defaults.put(ExternalConfigManager.KEY_PROJECT_PATHS, projectPathsFile);
|
defaults.put(ExternalConfigManager.KEY_PROJECT_PATHS, projectPathsFile);
|
||||||
@@ -77,6 +96,19 @@ public class WorkUtilsProperties {
|
|||||||
ensureDataFilesExist();
|
ensureDataFilesExist();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 确保导出目录存在 */
|
||||||
|
private void ensureExportDirExists() {
|
||||||
|
try {
|
||||||
|
Path dir = Paths.get(exportDirectory);
|
||||||
|
if (!Files.exists(dir)) {
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
log.info("已创建导出目录: {}", dir.toAbsolutePath());
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.warn("创建导出目录失败: {} ({})", exportDirectory, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合并外部配置 —— 外部值覆盖 application.yml 值
|
* 合并外部配置 —— 外部值覆盖 application.yml 值
|
||||||
*/
|
*/
|
||||||
@@ -155,7 +187,7 @@ public class WorkUtilsProperties {
|
|||||||
* 文件不存在时自动创建空文件。
|
* 文件不存在时自动创建空文件。
|
||||||
*/
|
*/
|
||||||
public Path getProjectPathsFilePath() {
|
public Path getProjectPathsFilePath() {
|
||||||
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "代码仓库项目路径.txt");
|
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "project_paths.txt");
|
||||||
if (p.isAbsolute()) {
|
if (p.isAbsolute()) {
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -183,7 +215,7 @@ public class WorkUtilsProperties {
|
|||||||
* 无法读取模板时则创建空文件。
|
* 无法读取模板时则创建空文件。
|
||||||
*/
|
*/
|
||||||
private void ensureDataFilesExist() {
|
private void ensureDataFilesExist() {
|
||||||
ensureDataFile(getProjectPathsFilePath(), "config/代码仓库项目路径.txt", "项目路径文件");
|
ensureDataFile(getProjectPathsFilePath(), "config/project_paths.txt", "项目路径文件");
|
||||||
ensureDataFile(getDamagedReposFilePath(), "config/damaged_repos.cache", "损坏缓存文件");
|
ensureDataFile(getDamagedReposFilePath(), "config/damaged_repos.cache", "损坏缓存文件");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ spring:
|
|||||||
name: workutils
|
name: workutils
|
||||||
|
|
||||||
workutils:
|
workutils:
|
||||||
base-directory: "D:/workspace/workutils"
|
# base dir, empty = defaults to user.dir
|
||||||
export-directory: "D:/workspace/workutils/export"
|
base-directory: ""
|
||||||
# 数据文件路径(相对路径 → user.dir 同级目录,绝对路径 → 直接使用)
|
# export dir, empty = defaults to {user.dir}/export (auto-created on startup)
|
||||||
project-paths-file: "代码仓库项目路径.txt"
|
export-directory: ""
|
||||||
|
# data file paths (relative -> under user.dir, absolute -> as-is)
|
||||||
|
project-paths-file: "project_paths.txt"
|
||||||
damaged-repos-file: "damaged_repos.cache"
|
damaged-repos-file: "damaged_repos.cache"
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
Reference in New Issue
Block a user