DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,565 @@
|
||||
// Package services provides archive management and editing services.
|
||||
// Author: flobit
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"pvfine/internal/pvf"
|
||||
pvfversion "pvfine/internal/version"
|
||||
)
|
||||
|
||||
// ArchiveDiffMergeResult 记录批量合并操作的结果汇总
|
||||
type ArchiveDiffMergeResult struct {
|
||||
SuccessCount int `json:"successCount"`
|
||||
FailedCount int `json:"failedCount"`
|
||||
MergedPaths []string `json:"mergedPaths"`
|
||||
Errors []string `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
// MergeFiles 将选中的差异文件从对比归档合并到本地当前打开的归档中。
|
||||
// 遵循核心锁在前、diff 锁在后的顺序防止死锁。
|
||||
// @author flobit
|
||||
func (s *ArchiveDiffService) MergeFiles(id string, paths []string) (*ArchiveDiffMergeResult, error) {
|
||||
if len(paths) == 0 {
|
||||
return &ArchiveDiffMergeResult{}, nil
|
||||
}
|
||||
|
||||
// 锁顺序:先 core 后 diff
|
||||
s.c.mu.Lock()
|
||||
|
||||
a := s.c.archive
|
||||
if a == nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, ErrNoArchive
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
p, err := s.findLocked(id, true)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
otherArchive := p.other
|
||||
if otherArchive == nil {
|
||||
s.mu.Unlock()
|
||||
s.c.mu.Unlock()
|
||||
return nil, errors.New("对比归档已失效,请重新比对")
|
||||
}
|
||||
actualPaths := expandSelectedLeafPaths(p.changes, paths)
|
||||
s.mu.Unlock()
|
||||
|
||||
if len(actualPaths) == 0 {
|
||||
s.c.mu.Unlock()
|
||||
return &ArchiveDiffMergeResult{}, nil
|
||||
}
|
||||
|
||||
mutationCheckpoint := a.MutationCheckpoint()
|
||||
var before pvfversion.ContentSnapshot
|
||||
if s.c.versionRepo != nil {
|
||||
before, _ = pvfversion.ContentSnapshotFromArchive(a, actualPaths)
|
||||
}
|
||||
|
||||
mergedPaths, structuralChanged, res := applyDiffMergesLocked(a, otherArchive, actualPaths)
|
||||
|
||||
if structuralChanged {
|
||||
if err := s.c.rebuildArchiveIndexesLocked(a); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
stage := a.CloneForBatch()
|
||||
s.finalizeMergeMutationLocked(a, mutationCheckpoint, before, mergedPaths, -1, structuralChanged, "合并归档差异", "files-merged")
|
||||
s.updateSessionAfterMerge(id, mergedPaths, stage)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// applyDiffMergesLocked 批量应用差异合并或删除操作,返回成功路径与是否触发结构性变更。
|
||||
// @author flobit
|
||||
func applyDiffMergesLocked(target, other *pvf.Archive, paths []string) ([]string, bool, *ArchiveDiffMergeResult) {
|
||||
res := &ArchiveDiffMergeResult{MergedPaths: make([]string, 0, len(paths))}
|
||||
structuralChanged := false
|
||||
|
||||
for _, rawPath := range paths {
|
||||
canonicalKey := pvfversion.CanonicalPath(rawPath)
|
||||
tIdx, hasOther := other.Find(canonicalKey)
|
||||
|
||||
var structural bool
|
||||
var mergeErr error
|
||||
if hasOther {
|
||||
structural, mergeErr = mergeDiffFileEntry(target, other, rawPath, tIdx)
|
||||
} else {
|
||||
structural, mergeErr = deleteDiffFileEntry(target, rawPath)
|
||||
}
|
||||
|
||||
if mergeErr != nil {
|
||||
res.FailedCount++
|
||||
res.Errors = append(res.Errors, fmt.Sprintf("%s: %v", rawPath, mergeErr))
|
||||
continue
|
||||
}
|
||||
if structural {
|
||||
structuralChanged = true
|
||||
}
|
||||
res.SuccessCount++
|
||||
res.MergedPaths = append(res.MergedPaths, rawPath)
|
||||
}
|
||||
return res.MergedPaths, structuralChanged, res
|
||||
}
|
||||
|
||||
// MergeText 将单文件编辑或段落合入后的最终文本写回到当前工作区归档。
|
||||
// @author flobit
|
||||
func (s *ArchiveDiffService) MergeText(id, path, text string) error {
|
||||
cleanPath := strings.TrimSpace(path)
|
||||
if cleanPath == "" {
|
||||
return errors.New("文件路径不能为空")
|
||||
}
|
||||
|
||||
s.c.mu.Lock()
|
||||
|
||||
a := s.c.archive
|
||||
if a == nil {
|
||||
s.c.mu.Unlock()
|
||||
return ErrNoArchive
|
||||
}
|
||||
if err := s.c.ensureVersionReadyLocked(); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
_, err := s.findLocked(id, true)
|
||||
s.mu.Unlock()
|
||||
if err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
mutationCheckpoint := a.MutationCheckpoint()
|
||||
var before pvfversion.ContentSnapshot
|
||||
if s.c.versionRepo != nil {
|
||||
before, _ = pvfversion.ContentSnapshotFromArchive(a, []string{cleanPath})
|
||||
}
|
||||
|
||||
canonicalKey := pvfversion.CanonicalPath(cleanPath)
|
||||
targetIdx, exists := a.Find(canonicalKey)
|
||||
structuralChanged := false
|
||||
|
||||
if exists {
|
||||
if err := a.SetText(targetIdx, text); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dataType := int32(pvf.TypeScript)
|
||||
if strings.EqualFold(filepath.Ext(cleanPath), ".str") {
|
||||
dataType = pvf.TypeUnicode
|
||||
}
|
||||
newIdx, err := a.AddFileText(cleanPath, text, dataType)
|
||||
if err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
targetIdx = newIdx
|
||||
structuralChanged = true
|
||||
}
|
||||
|
||||
if structuralChanged {
|
||||
if err := s.c.rebuildArchiveIndexesLocked(a); err != nil {
|
||||
s.c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
stage := a.CloneForBatch()
|
||||
s.finalizeMergeMutationLocked(a, mutationCheckpoint, before, []string{cleanPath}, targetIdx, structuralChanged, "合并脚本内容", "file-updated")
|
||||
s.updateSessionAfterMerge(id, []string{cleanPath}, stage)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// finalizeMergeMutationLocked 统一完成合并成功后的版本记录、缓存失效及前端事件派发。
|
||||
// 注意:该函数执行期间会释放 s.c.mu 互斥锁。
|
||||
// @author flobit
|
||||
func (s *ArchiveDiffService) finalizeMergeMutationLocked(
|
||||
a *pvf.Archive,
|
||||
checkpoint int,
|
||||
before pvfversion.ContentSnapshot,
|
||||
paths []string,
|
||||
targetIdx int32,
|
||||
structuralChanged bool,
|
||||
actionName string,
|
||||
versionAction string,
|
||||
) {
|
||||
if s.c.versionRepo != nil && len(paths) > 0 {
|
||||
after, _ := pvfversion.ContentSnapshotFromArchive(a, paths)
|
||||
_ = s.c.recordVersionMutationLocked(actionName, before, after)
|
||||
}
|
||||
|
||||
s.c.invalidateScriptLocked()
|
||||
s.c.editorAnnotation = editorAnnotationCache{}
|
||||
s.c.annotationRelations = make(map[string]map[string]*relationTarget)
|
||||
s.c.invalidateAdvancedSearchLocked()
|
||||
|
||||
mutationSummary := a.MutationsSince(checkpoint)
|
||||
a.ClearMutations()
|
||||
info := a.Info()
|
||||
versioned := s.c.versionRepo != nil
|
||||
s.c.mu.Unlock()
|
||||
|
||||
s.c.scheduleArchiveMutations(a, mutationSummary)
|
||||
emitEvent("archive:changed", info)
|
||||
emitEvent("archive:advanced-search-stale", map[string]any{"batch": true})
|
||||
payload := map[string]any{
|
||||
"source": "archive-diff",
|
||||
"paths": paths,
|
||||
"structural": structuralChanged,
|
||||
"modifiedCount": info.ModifiedCount,
|
||||
}
|
||||
if targetIdx >= 0 {
|
||||
payload["fileIndexes"] = []int32{targetIdx}
|
||||
}
|
||||
emitEvent("archive:batch-applied", payload)
|
||||
if versioned {
|
||||
emitVersionState(s.c, versionAction)
|
||||
}
|
||||
}
|
||||
|
||||
// diffPathBase 提取路径的纯文件名,兼容 Windows 与正斜杠路径
|
||||
func diffPathBase(p string) string {
|
||||
return path.Base(strings.ReplaceAll(p, "\\", "/"))
|
||||
}
|
||||
|
||||
// FlatFiles 按照搜索词与操作类型返回所有差异文件的平铺列表,供双栏工作台过滤检索
|
||||
func (s *ArchiveDiffService) FlatFiles(id, query, operation string) ([]ArchiveDiffNode, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, err := s.findLocked(id, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := strings.ToLower(strings.TrimSpace(query))
|
||||
opFilter := strings.ToLower(strings.TrimSpace(operation))
|
||||
|
||||
result := make([]ArchiveDiffNode, 0)
|
||||
for _, change := range p.changes {
|
||||
op := string(change.Operation)
|
||||
if opFilter != "" && opFilter != "all" && op != opFilter {
|
||||
continue
|
||||
}
|
||||
display := change.DisplayPath
|
||||
if q != "" && !strings.Contains(strings.ToLower(display), q) {
|
||||
continue
|
||||
}
|
||||
result = append(result, ArchiveDiffNode{
|
||||
Path: display,
|
||||
Name: diffPathBase(display),
|
||||
IsDir: false,
|
||||
Operation: op,
|
||||
Count: 1,
|
||||
})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
pi, pj := strings.ToLower(result[i].Path), strings.ToLower(result[j].Path)
|
||||
if pi != pj {
|
||||
return pi < pj
|
||||
}
|
||||
return result[i].Path < result[j].Path
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// updateSessionAfterMerge 在合并写入后更新对比会话的 internal changes 缓存与统计。
|
||||
// 对于局部修改的文件,精确复核其与对比归档之间是否仍然存在差异,保留剩余差异条目。
|
||||
// @author flobit
|
||||
func (s *ArchiveDiffService) updateSessionAfterMerge(id string, mergedPaths []string, stage *pvf.Archive) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
p, err := s.findLocked(id, false)
|
||||
if err != nil || p.changes == nil {
|
||||
return
|
||||
}
|
||||
if p.drafts != nil {
|
||||
for _, mp := range mergedPaths {
|
||||
delete(p.drafts, pvfversion.CanonicalPath(mp))
|
||||
}
|
||||
}
|
||||
if stage != nil {
|
||||
p.current = stage
|
||||
if p.current != nil && len(p.drafts) > 0 {
|
||||
for canonicalKey, draft := range p.drafts {
|
||||
if idx, ok := p.current.Find(canonicalKey); ok {
|
||||
_ = p.current.SetText(idx, draft.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A previously merged file may have disappeared from changes and then be edited again.
|
||||
for _, mp := range mergedPaths {
|
||||
key := pvfversion.CanonicalPath(mp)
|
||||
if change, hasDiff := archiveDiffCheckSingleFileChange(p.other, p.current, key, mp); hasDiff {
|
||||
p.changes[key] = change
|
||||
} else {
|
||||
delete(p.changes, key)
|
||||
}
|
||||
}
|
||||
|
||||
remainingChanges := make([]pvfversion.FileChange, 0, len(p.changes))
|
||||
var added, deleted, modified int
|
||||
|
||||
for _, change := range p.changes {
|
||||
remainingChanges = append(remainingChanges, change)
|
||||
switch change.Operation {
|
||||
case pvfversion.OperationAdd:
|
||||
added++
|
||||
case pvfversion.OperationDelete:
|
||||
deleted++
|
||||
default:
|
||||
modified++
|
||||
}
|
||||
}
|
||||
|
||||
changeIndex := make(map[string]pvfversion.FileChange, len(remainingChanges))
|
||||
for _, change := range remainingChanges {
|
||||
changeIndex[change.Path] = change
|
||||
}
|
||||
p.changes = changeIndex
|
||||
p.status.Added, p.status.Deleted, p.status.Modified = added, deleted, modified
|
||||
p.detail = nil
|
||||
|
||||
children, filteredChildren, err := buildAllDiffTrees(context.Background(), remainingChanges, nil)
|
||||
if err == nil {
|
||||
p.children = children
|
||||
p.filteredChildren = filteredChildren
|
||||
}
|
||||
}
|
||||
|
||||
// archiveDiffCheckSingleFileChange 检查单个文件在合并或修改后,与对比归档之间是否仍然存在差异。
|
||||
// 若无差异则返回 false,通知会话从差异树中移除该文件;若仍有差异则返回更新后的变更信息。
|
||||
func archiveDiffCheckSingleFileChange(before, after *pvf.Archive, canonicalKey, displayFallback string) (pvfversion.FileChange, bool) {
|
||||
if before == nil || after == nil {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
bi, bok := before.Find(canonicalKey)
|
||||
ai, aok := after.Find(canonicalKey)
|
||||
if !bok && !aok {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
if !bok && aok {
|
||||
aType, _, err := after.FileContentInfo(ai)
|
||||
if err != nil {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
disp := after.Path(ai)
|
||||
if disp == "" {
|
||||
disp = displayFallback
|
||||
}
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationAdd,
|
||||
AfterType: aType,
|
||||
}, true
|
||||
}
|
||||
if bok && !aok {
|
||||
bType, _, err := before.FileContentInfo(bi)
|
||||
if err != nil {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
disp := before.Path(bi)
|
||||
if disp == "" {
|
||||
disp = displayFallback
|
||||
}
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationDelete,
|
||||
BeforeType: bType,
|
||||
}, true
|
||||
}
|
||||
|
||||
// 双方均存在条目
|
||||
bType, bSize, err1 := before.FileContentInfo(bi)
|
||||
aType, aSize, err2 := after.FileContentInfo(ai)
|
||||
if err1 != nil || err2 != nil {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
disp := after.Path(ai)
|
||||
if disp == "" {
|
||||
disp = before.Path(bi)
|
||||
}
|
||||
if disp == "" {
|
||||
disp = displayFallback
|
||||
}
|
||||
if bType != aType {
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationModify,
|
||||
BeforeType: bType,
|
||||
AfterType: aType,
|
||||
}, true
|
||||
}
|
||||
|
||||
isText := bType == pvf.TypeScript || bType == pvf.TypeUnicode
|
||||
if !isText {
|
||||
if bSize != aSize {
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationModify,
|
||||
BeforeType: bType,
|
||||
AfterType: aType,
|
||||
}, true
|
||||
}
|
||||
bRaw, err1 := before.RawBytes(bi)
|
||||
aRaw, err2 := after.RawBytes(ai)
|
||||
if err1 != nil || err2 != nil || !bytes.Equal(bRaw, aRaw) {
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationModify,
|
||||
BeforeType: bType,
|
||||
AfterType: aType,
|
||||
BeforeHash: archiveDiffContentHash(bType, bRaw),
|
||||
AfterHash: archiveDiffContentHash(aType, aRaw),
|
||||
}, true
|
||||
}
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
|
||||
// 文本或脚本文件,比较反编译后的标准文本
|
||||
bRaw, err1 := before.RawBytes(bi)
|
||||
aRaw, err2 := after.RawBytes(ai)
|
||||
if err1 != nil || err2 != nil {
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
bText, err1 := before.CanonicalTextFromRaw(bi, bRaw)
|
||||
aText, err2 := after.CanonicalTextFromRaw(ai, aRaw)
|
||||
if err1 != nil || err2 != nil || bText != aText {
|
||||
return pvfversion.FileChange{
|
||||
Path: canonicalKey,
|
||||
DisplayPath: pvfversion.DisplayPath(disp),
|
||||
Operation: pvfversion.OperationModify,
|
||||
BeforeType: bType,
|
||||
AfterType: aType,
|
||||
BeforeHash: archiveDiffContentHash(bType, []byte(bText)),
|
||||
AfterHash: archiveDiffContentHash(aType, []byte(aText)),
|
||||
}, true
|
||||
}
|
||||
|
||||
return pvfversion.FileChange{}, false
|
||||
}
|
||||
|
||||
// mergeDiffFileEntry 合并单个文件条目,跨归档文本文件使用反编译文本写入确保字符串表入池
|
||||
func mergeDiffFileEntry(target, source *pvf.Archive, rawPath string, sourceIdx int32) (bool, error) {
|
||||
sourceType, _, err := source.FileContentInfo(sourceIdx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
canonicalKey := pvfversion.CanonicalPath(rawPath)
|
||||
targetIdx, hasTarget := target.Find(canonicalKey)
|
||||
|
||||
// 文本文件:通过 CanonicalText 反编译获取真实文本,再 SetText 写入目标归档的字符串池
|
||||
if sourceType == pvf.TypeScript || sourceType == pvf.TypeUnicode {
|
||||
text, err := source.CanonicalText(sourceIdx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("反编译源文本失败: %w", err)
|
||||
}
|
||||
if hasTarget {
|
||||
targetType, _, _ := target.FileContentInfo(targetIdx)
|
||||
if targetType != sourceType {
|
||||
_ = target.SetDataType(targetIdx, sourceType)
|
||||
}
|
||||
if err := target.SetText(targetIdx, text); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
_, err = target.AddFileText(rawPath, text, sourceType)
|
||||
return true, err
|
||||
}
|
||||
|
||||
// 二进制文件:直接复制 RawBytes
|
||||
raw, err := source.RawBytes(sourceIdx)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("读取源二进制数据失败: %w", err)
|
||||
}
|
||||
if hasTarget {
|
||||
targetType, _, _ := target.FileContentInfo(targetIdx)
|
||||
if targetType != sourceType {
|
||||
_ = target.SetDataType(targetIdx, sourceType)
|
||||
}
|
||||
if err := target.SetRawBytes(targetIdx, raw); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
target.AddFile(rawPath, raw, sourceType)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// deleteDiffFileEntry 从当前归档删除文件
|
||||
func deleteDiffFileEntry(a *pvf.Archive, rawPath string) (bool, error) {
|
||||
canonicalKey := pvfversion.CanonicalPath(rawPath)
|
||||
idx, exists := a.Find(canonicalKey)
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
deleted, err := a.RemoveFiles([]int32{idx})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return len(deleted) > 0, nil
|
||||
}
|
||||
|
||||
// expandSelectedLeafPaths 将选中的路径列表(包含目录路径或具体文件)展开为去重后的叶子差异文件路径
|
||||
func expandSelectedLeafPaths(changes map[string]pvfversion.FileChange, paths []string) []string {
|
||||
if len(paths) == 0 || len(changes) == 0 {
|
||||
return nil
|
||||
}
|
||||
resultMap := make(map[string]string)
|
||||
|
||||
for _, rawPath := range paths {
|
||||
cleaned := strings.TrimSpace(rawPath)
|
||||
if cleaned == "" {
|
||||
continue
|
||||
}
|
||||
canonical := pvfversion.CanonicalPath(cleaned)
|
||||
if change, exists := changes[canonical]; exists {
|
||||
resultMap[canonical] = change.DisplayPath
|
||||
continue
|
||||
}
|
||||
dirPrefix := canonical + "/"
|
||||
for key, change := range changes {
|
||||
if strings.HasPrefix(key, dirPrefix) {
|
||||
resultMap[key] = change.DisplayPath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leaves := make([]string, 0, len(resultMap))
|
||||
for _, display := range resultMap {
|
||||
leaves = append(leaves, display)
|
||||
}
|
||||
return leaves
|
||||
}
|
||||
Reference in new issue
Block a user