功能已稳定

This commit is contained in:
张家训
2026-06-19 18:26:52 +08:00
parent 0939c61c1d
commit dbc9902716
8 changed files with 195 additions and 64 deletions
+1 -1
View File
@@ -24,7 +24,7 @@
<!-- Maven 编译/测试启动时的 JVM 参数,确保 IDEA 内运行不乱码 -->
<argLine>-Dfile.encoding=UTF-8</argLine>
<!-- OpenJFX 版本 -->
<javafx.version>17.0.14</javafx.version>
<javafx.version>17.0.15</javafx.version>
<!-- AtlantaFX 版本 -->
<atlantafx.version>2.0.1</atlantafx.version>
<!-- 指定 JDK 17 路径(系统默认 JDK 8 时启用) -->
+10
View File
@@ -110,6 +110,16 @@ if %errorlevel% neq 0 (
pause
exit /b 1
)
::: 4.5 补充 JavaFX 原生 DLL(jlink 不会自动从 SDK bin/ 复制)
echo Copy JavaFX native DLLs...
if exist "%JFX_SDK_HOME%\bin\glass.dll" (
xcopy /y /q "%JFX_SDK_HOME%\bin\*.dll" "%TARGET%\jre\bin\" >nul
echo [OK] JavaFX DLLs copied
) else (
echo [WARN] No JavaFX DLLs found in %JFX_SDK_HOME%\bin
)
echo [OK] JRE generated : %TARGET%\jre
::: ============================================================
@@ -70,6 +70,8 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
private List<DamagedRepo> lastDamagedRepos;
/** 缓存上一次使用的目标分支 */
private String lastBranch;
/** 同步取消标记(供 GUI 终止按钮使用) */
private volatile boolean syncCancelled = false;
// ==================== 启动时从日志恢复缓存 ====================
@@ -257,11 +259,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
Map<String, String> baseUrlMap = new LinkedHashMap<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
String url = proj.getRemoteBaseUrl();
if (!url.endsWith("/")) {
url = url + "/";
}
baseUrlMap.put(normalizePath(proj.getPath()), url);
baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
log.info("从项目路径文件加载了 {} 条远程URL映射", baseUrlMap.size());
@@ -290,7 +288,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
int done = completed.incrementAndGet();
String tag;
switch (result) {
case OK: tag = "成功"; break;
case OK: tag = result.cloned ? "拉取" : "成功"; break;
case DAMAGED: tag = "损坏"; break;
default: tag = "失败"; break;
}
@@ -485,11 +483,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
List<FileOperationHelper.ProjectPathInfo> projects = fileOperationHelper.getProjectPathsWithStatus();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
String url = proj.getRemoteBaseUrl();
if (!url.endsWith("/")) {
url = url + "/";
}
map.put(normalizePath(proj.getPath()), url);
map.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
} catch (IOException e) {
@@ -498,15 +492,14 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return map;
}
/** 解析 clone URL:优先从项目路径文件的 BASE_URL 拼接,其次用缓存的 remoteUrl */
/** 解析 clone URL:BASE_URL 本身就是完整的 clone 地址,直接返回 */
private String resolveCloneUrl(DamagedRepo dr, Map<String, String> baseUrlMap) {
// 1. 从项目路径文件的 BASE_URL 拼接(项目路径文件为准)
// 1. 从项目路径文件的 BASE_URL(已是完整 clone URL,如 https://user@host/path/repo.git)
if (baseUrlMap != null) {
String normalizedPath = normalizePath(dr.path);
String baseUrl = baseUrlMap.get(normalizedPath);
if (baseUrl != null) {
String repoName = extractName(dr.path);
return baseUrl + repoName;
return baseUrl;
}
}
// 2. 兜底:缓存的 remoteUrl
@@ -520,9 +513,31 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
private SyncResult syncOneRepo(String repoPath, String branch, Map<String, String> baseUrlMap) {
File repoDir = new File(repoPath);
if (!repoDir.exists() || !new File(repoDir, ".git").exists()) {
// 目录不存在 或 空目录 → 自动 clone
if (!repoDir.exists() || (repoDir.isDirectory() && isEmptyDir(repoDir))) {
String reason = !repoDir.exists() ? "目录不存在" : "目录为空";
log.info("{},自动 clone: {}", reason, repoPath);
String cloneUrl = resolveSimpleCloneUrl(repoPath, baseUrlMap);
if (cloneUrl != null) {
if (doClone(repoPath, branch, cloneUrl)) {
SyncResult r = SyncResult.OK;
r.output = "auto-cloned";
r.cloned = true;
return r;
}
log.warn("自动 clone 失败: {} -> {}", repoPath, "exit code != 0");
}
SyncResult r = SyncResult.DAMAGED;
r.error = "目录不存在或无 .git";
r.error = reason + ",自动 clone 失败(远程URL: " + (cloneUrl != null ? cloneUrl : "未知") + ")";
r.remoteUrl = cloneUrl;
return r;
}
// 目录存在但无 .git → 仓库损坏
if (!new File(repoDir, ".git").exists()) {
SyncResult r = SyncResult.DAMAGED;
r.error = "目录存在但无 .git(仓库损坏)";
r.remoteUrl = getRemoteUrl(repoPath, baseUrlMap);
return r;
}
@@ -583,40 +598,49 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return result;
}
// ==================== 重拉(clone)逻辑 ====================
// ==================== clone 逻辑 ====================
private boolean doReclone(DamagedRepo dr, String cloneUrl) {
String repoPath = dr.path;
String branch = dr.branch;
// 删除旧目录
/** 执行 git clone(删除旧目录后克隆),供 auto-clone 和手动重拉共用 */
private boolean doClone(String repoPath, String branch, String cloneUrl) {
File dir = new File(repoPath);
// 确保父目录存在
if (dir.getParentFile() != null && !dir.getParentFile().exists()) {
dir.getParentFile().mkdirs();
}
// 删除旧目录(如果存在)
if (dir.exists()) {
try {
deleteRecursive(dir.toPath());
log.info("已删除仓库: {}", repoPath);
System.out.println(" 已删除旧目录");
} catch (IOException e) {
System.err.println(" 删除失败: " + e.getMessage());
log.error("删除失败: {}", repoPath, e);
return false;
}
}
// clone
String parentDir = dir.getParent();
System.out.println(" clone URL: " + cloneUrl);
String parentDir = dir.getParent() != null ? dir.getParent() : ".";
CmdResult cr = runGit(parentDir,
"git clone -b " + branch + " " + cloneUrl + " \"" + repoPath + "\"");
if (cr.ok) {
System.out.println(" clone 成功");
return true;
} else {
System.err.println(" clone 失败: " + cr.error);
log.error("clone 失败: {} -> {}", cloneUrl, cr.error);
return false;
}
}
/** 手动重拉(供 DamagedRepo 场景使用) */
private boolean doReclone(DamagedRepo dr, String cloneUrl) {
System.out.println(" clone URL: " + cloneUrl);
boolean ok = doClone(dr.path, dr.branch, cloneUrl);
if (ok) {
System.out.println(" clone 成功");
} else {
System.err.println(" clone 失败");
}
return ok;
}
// ==================== Git 命令执行 ====================
/**
@@ -721,16 +745,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
} catch (IOException ignored) {
}
}
// 3. 兜底: 从项目路径文件中的 BASE_URL 拼接完整 clone URL
// 格式: 项目路径文件 key=仓库路径, value=BASE_URL; clone URL = BASE_URL + cf/ + 仓库名
// 3. 兜底: 项目路径文件中的 BASE_URL 即完整的 clone URL
if (baseUrlMap != null) {
String normalizedPath = normalizePath(repoPath);
String baseUrl = baseUrlMap.get(normalizedPath);
if (baseUrl != null) {
String repoName = extractName(repoPath);
String fullUrl = baseUrl + repoName;
log.info("从项目路径文件拼接远程URL: {} -> {}", repoPath, fullUrl);
return fullUrl;
log.info("从项目路径文件获取远程URL: {} -> {}", repoPath, baseUrl);
return baseUrl;
}
}
return null;
@@ -766,6 +787,19 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return idx >= 0 ? path.substring(idx + 1) : path;
}
/** 判断目录是否为空 */
private boolean isEmptyDir(File dir) {
if (!dir.isDirectory()) return false;
String[] files = dir.list();
return files != null && files.length == 0;
}
/** 从 baseUrlMap 获取 clone URL(不拼仓库名,BASE_URL 即完整 clone URL) */
private String resolveSimpleCloneUrl(String repoPath, Map<String, String> baseUrlMap) {
if (baseUrlMap == null) return null;
return baseUrlMap.get(normalizePath(repoPath));
}
/** 路径规范化:统一用 / 分隔、小写,用于映射表匹配 */
private String normalizePath(String path) {
if (path == null || path.isEmpty()) return "";
@@ -801,6 +835,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
* 以回调方式同步所有项目(供可视窗口调用)
*/
public void syncWithCallback(String branch, SyncCallback cb) {
syncCancelled = false;
List<FileOperationHelper.ProjectPathInfo> projects;
try {
projects = fileOperationHelper.getProjectPathsWithStatus();
@@ -814,12 +849,12 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return;
}
if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); return; }
Map<String, String> baseUrlMap = new LinkedHashMap<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) {
String url = proj.getRemoteBaseUrl();
if (!url.endsWith("/")) url = url + "/";
baseUrlMap.put(normalizePath(proj.getPath()), url);
baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl());
}
}
@@ -835,7 +870,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
cb.onLog("并行线程: " + PARALLEL);
cb.onLog("开始同步...\n");
if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); pool.shutdownNow(); return; }
for (int i = 0; i < total; i++) {
if (syncCancelled) {
// 不再提交新任务,但已提交的会在 latch 中计完
break;
}
final FileOperationHelper.ProjectPathInfo proj = projects.get(i);
final String repoPath = proj.getPath();
final String repoName = extractName(repoPath);
@@ -850,7 +891,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
int done = completed.incrementAndGet();
String tag;
switch (result) {
case OK: tag = "成功"; break;
case OK: tag = result.cloned ? "拉取" : "成功"; break;
case DAMAGED: tag = "损坏"; break;
default: tag = "失败"; break;
}
@@ -869,10 +910,20 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
pool.shutdown();
try {
boolean allDone = latch.await(SYNC_TOTAL_TIMEOUT_MINUTES, TimeUnit.MINUTES);
if (!allDone) {
pool.shutdownNow();
cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止");
// 缩短每次 await 时间,以便及时响应取消
long checkIntervalMs = 500;
long deadline = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(SYNC_TOTAL_TIMEOUT_MINUTES);
while (!latch.await(checkIntervalMs, TimeUnit.MILLISECONDS)) {
if (syncCancelled) {
cb.onLog("\n[WARN] 用户取消了同步,正在终止...");
pool.shutdownNow();
break;
}
if (System.currentTimeMillis() > deadline) {
pool.shutdownNow();
cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止");
break;
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
@@ -881,6 +932,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
return;
}
// 汇总结果
int success = 0, failed = 0;
List<DamagedRepo> damagedRepos = new ArrayList<>();
for (FileOperationHelper.ProjectPathInfo proj : projects) {
@@ -914,6 +966,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
}
}
/** 取消当前正在执行的同步(供 GUI 终止按钮调用) */
public void cancelSync() {
this.syncCancelled = true;
}
/**
* 获取损坏仓库的 clone URL(供窗口使用)
*/
@@ -969,6 +1026,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
String output;
String error;
String remoteUrl;
boolean cloned; // 标记是否是通过 auto-clone 完成的,区别于正常 fetch+checkout+pull
}
/** 损坏仓库信息 */
@@ -20,7 +20,6 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Git 仓库同步可视化窗口
@@ -46,7 +45,6 @@ public class GitSyncWindow {
private ProgressBar progressBar;
private Label progressLabel;
private TextArea logArea;
private final AtomicBoolean stopRequested = new AtomicBoolean(false);
// Tab 3: 损坏仓库处理
private TableView<DamagedRow> damagedTable;
@@ -204,7 +202,11 @@ public class GitSyncWindow {
stopBtn = StyledButton.danger("终止", Theme.base(14));
stopBtn.setDisable(true);
stopBtn.setOnAction(e -> stopRequested.set(true));
stopBtn.setOnAction(e -> {
stopBtn.setDisable(true);
progressLabel.setText("正在终止...");
plugin.cancelSync();
});
branchRow.getChildren().addAll(branchLabel, branchField, syncBtn, stopBtn);
@@ -240,7 +242,6 @@ public class GitSyncWindow {
syncBtn.setDisable(true);
stopBtn.setDisable(false);
stopRequested.set(false);
logArea.clear();
progressBar.setProgress(0);
progressLabel.setText("正在同步...");
@@ -44,9 +44,16 @@ public class MyBatisExportWindow {
// 操作
private Button exportBtn;
private Button stopBtn;
private ProgressBar progressBar;
private Label progressLabel;
// 日志
private TextArea logArea;
// 取消标记
private volatile boolean exportCancelled = false;
// 历史记录
private TableView<HistoryRow> historyTable;
private final ObservableList<HistoryRow> historyRows = FXCollections.observableArrayList();
@@ -131,20 +138,40 @@ public class MyBatisExportWindow {
exportBtn = StyledButton.primary("导出", Theme.bold(fsBtn()));
exportBtn.setOnAction(e -> startExport());
filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn);
stopBtn = StyledButton.danger("终止", Theme.base(fsBtn()));
stopBtn.setDisable(true);
stopBtn.setOnAction(e -> {
stopBtn.setDisable(true);
progressLabel.setText("正在终止...");
exportCancelled = true;
});
filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn, stopBtn);
// === 进度 ===
HBox progressRow = new HBox(Theme.S2);
progressRow.setAlignment(Pos.CENTER_LEFT);
progressBar = new ProgressBar(0);
progressBar.setPrefWidth(300);
progressBar.setPrefHeight(16);
progressBar.setPrefWidth(400);
progressBar.setPrefHeight(20);
progressBar.setVisible(false);
progressLabel = new Label("");
progressLabel.setFont(Theme.base(fsBody() - 2));
progressLabel.setTextFill(Theme.TEXT_SECONDARY);
progressRow.getChildren().addAll(progressBar, progressLabel);
// === 日志区域 ===
Label logTitle = new Label("执行日志");
logTitle.setFont(Theme.heading(fsBody()));
logTitle.setTextFill(Theme.TEXT_PRIMARY);
logArea = new TextArea();
logArea.setEditable(false);
logArea.setFont(Theme.mono(12));
logArea.getStyleClass().add(Theme.CSS_LOG_AREA);
logArea.setPrefHeight(150);
logArea.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";");
VBox.setVgrow(logArea, Priority.NEVER);
// === 标题 ===
Label tableTitle = new Label("导出历史记录");
tableTitle.setFont(Theme.heading(fsSection()));
@@ -184,7 +211,7 @@ public class MyBatisExportWindow {
historyTable.getColumns().addAll(colAction, colIndex, colQuery, colFile, colCount, colTime);
VBox.setVgrow(historyTable, Priority.ALWAYS);
root.getChildren().addAll(filterRow, progressRow, tableTitle, historyTable);
root.getChildren().addAll(filterRow, progressRow, logTitle, logArea, tableTitle, historyTable);
stage.setScene(new Scene(root));
}
@@ -106,18 +106,28 @@ public class MyBatisExportService {
}
}
/** 导出进度回调 */
public interface ExportCallback {
void onProjectStart(int current, int total, String projectName);
void onProjectDone(int current, int total, String projectName, int recordCount);
/** 是否已请求取消 */
boolean isCancelled();
}
/**
* 按过滤条件导出并返回结果(含文件路径,供窗口展示 + 保存 CSV)
*/
public ExportResult exportWithResult(String fileNamePrefix, String filterMode, String filterValue) throws Exception {
SqlFilter filter = null;
if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
filter = new TableFilter(filterValue);
} else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
filter = new KeywordFilter(filterValue);
} else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
filter = new TagIdFilter(filterValue);
}
return exportWithCallback(fileNamePrefix, filterMode, filterValue, null);
}
/**
* 带进度回调的导出(供可视化窗口使用)
* @param cb 进度回调,传 null 则降级为无回调模式
*/
public ExportResult exportWithCallback(String fileNamePrefix, String filterMode, String filterValue,
ExportCallback cb) throws Exception {
SqlFilter filter = buildFilter(filterMode, filterValue);
List<String> projectPaths = fileOperationHelper.getProjectPaths();
if (projectPaths.isEmpty()) {
@@ -125,16 +135,30 @@ public class MyBatisExportService {
return new ExportResult(new ArrayList<>(), "");
}
int total = projectPaths.size();
List<SqlExportRow> resultList = new ArrayList<>();
int current = 0;
for (String projectPath : projectPaths) {
current++;
String projectName = fileOperationHelper.extractProjectName(projectPath);
if (cb != null) {
if (cb.isCancelled()) { break; }
cb.onProjectStart(current, total, projectName);
}
try {
List<String[]> projectRecords = processProject(projectPath, filter, null);
for (String[] r : projectRecords) {
resultList.add(new SqlExportRow(r[0], r[1], r[2], r[3], r[4]));
}
if (cb != null) {
cb.onProjectDone(current, total, projectName, projectRecords.size());
}
} catch (Exception e) {
log.error("处理项目时出错: " + projectPath, e);
if (cb != null) {
cb.onProjectDone(current, total, projectName + " (出错)", 0);
}
}
}
@@ -150,6 +174,18 @@ public class MyBatisExportService {
return new ExportResult(resultList, fileName);
}
/** 根据过滤模式和值构建过滤器 */
private SqlFilter buildFilter(String filterMode, String filterValue) {
if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
return new TableFilter(filterValue);
} else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
return new KeywordFilter(filterValue);
} else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) {
return new TagIdFilter(filterValue);
}
return null;
}
private void exportWithFilter(String fileNamePrefix, SqlFilter filter,
ProcessStrategy processStrategy) throws Exception {
List<String> projectPaths = fileOperationHelper.getProjectPaths();
@@ -231,7 +231,7 @@ public class WorkUtilsProperties {
* 文件不存在时自动创建空文件。
*/
public Path getProjectPathsFilePath() {
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "project_paths.txt");
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "data/project_paths.txt");
if (p.isAbsolute()) {
return p;
}
@@ -171,7 +171,6 @@ public class MainWindow {
stage.setScene(scene);
showWelcomePage();
if (!navItems.isEmpty()) selectNavItem(navItems.get(0));
}
// ==================== 统一顶部栏 ====================