mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-26 19:53:44 +00:00
fix(servers): number existing duplicate names
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ _避免用词_:导出(导出指配置组或诊断报告等文件快照)。
|
||||
把剪贴板文本或文件中的服务器配置引入配置目录、形成独立手动子树的操作。来源资料中的标识只用于装配其内部结构,导入产生的每个节点都获得全新的本地持久身份;同一资料重复导入产生相互独立的服务器配置,不与既有节点合并或去重。
|
||||
|
||||
**节点复制(Node duplication)**:
|
||||
在原节点之后的同级位置创建独立手动副本。组复制包含整棵子树,保留层级、顺序、连接字段和空组;所有节点身份及凭据引用重新生成。支持手动服务器、手动组和根层订阅组,不支持订阅服务器或嵌套订阅子组。订阅副本不携带订阅来源资料;顶层副本名称加本地化后缀并在同级重名时追加序号。目录和凭据作为一次逻辑变更提交,失败不发布部分子树,凭据回滚不完整时必须报告。复制不切换活动目标;在活动组内新增的副本按普通目录变更进入运行时收敛。
|
||||
在原节点之后的同级位置创建独立手动副本。组复制包含整棵子树,保留层级、顺序、连接字段和空组;所有节点身份及凭据引用重新生成。支持手动服务器、手动组和根层订阅组,不支持订阅服务器或嵌套订阅子组。订阅副本不携带订阅来源资料;顶层副本名称加本地化后缀并在同级重名时追加序号;名称末尾已有“副本”或“Copy”(含编号)时沿用该后缀,以同级未占用的编号命名,避免重复追加后缀。目录和凭据作为一次逻辑变更提交,失败不发布部分子树,凭据回滚不完整时必须报告。复制不切换活动目标;在活动组内新增的副本按普通目录变更进入运行时收敛。
|
||||
|
||||
**导入来源(Import source)**:
|
||||
一次服务器导入中可独立处理的一份资料,例如剪贴板文本或单个文件。每个来源独立解码、独立提交,是成功、部分成功与失败报告的粒度单位;一个来源失败不影响其他来源。
|
||||
|
||||
@@ -17,9 +17,18 @@ extension CatalogWorkflow {
|
||||
let siblings = try source.children(of: parent)
|
||||
guard let position = siblings.firstIndex(of: id) else { throw CatalogError.nodeNotFound(id) }
|
||||
let names = Set(siblings.compactMap { source.entry(for: $0)?.displayName })
|
||||
let base = "\(entry.displayName) \(nameSuffix)"
|
||||
var base = "\(entry.displayName) \(nameSuffix)"
|
||||
var name = base
|
||||
var number = 2
|
||||
// 两种语言的已保存后缀都识别,切换语言后仍沿用原名称的复制序列。
|
||||
if let suffixRange = entry.displayName.range(
|
||||
of: #" (?:副本|Copy)(?: [0-9]+)?$"#, options: .regularExpression)
|
||||
{
|
||||
let suffix = entry.displayName[suffixRange].split(separator: " ")[0]
|
||||
base = "\(entry.displayName[..<suffixRange.lowerBound]) \(suffix)"
|
||||
name = "\(base) 2"
|
||||
number = 3
|
||||
}
|
||||
while names.contains(name) {
|
||||
name = "\(base) \(number)"
|
||||
number += 1
|
||||
|
||||
@@ -127,6 +127,28 @@ struct CatalogWorkflowDuplicateTests {
|
||||
#expect(fixture.runtime.convergeSnapshots.last?.catalog.rootChildren == [original, copy])
|
||||
}
|
||||
|
||||
@Test(arguments: ["副本", "Copy"])
|
||||
func copyingCopiesUsesOneSuffixAndSiblingNumbers(suffix: String) async throws {
|
||||
let fixture = try DuplicateFixture()
|
||||
let workflow = fixture.workflow
|
||||
let original = try await workflow.createGroup(named: "websocket", into: nil)
|
||||
let first = try await workflow.duplicate(original, nameSuffix: suffix)
|
||||
let second = try await workflow.duplicate(first, nameSuffix: suffix)
|
||||
let third = try await workflow.duplicate(second, nameSuffix: suffix)
|
||||
#expect(workflow.displayName(for: first) == "websocket \(suffix)")
|
||||
#expect(workflow.displayName(for: second) == "websocket \(suffix) 2")
|
||||
#expect(workflow.displayName(for: third) == "websocket \(suffix) 3")
|
||||
let otherSuffix = suffix == "Copy" ? "副本" : "Copy"
|
||||
let fourth = try await workflow.duplicate(third, nameSuffix: otherSuffix)
|
||||
#expect(workflow.displayName(for: fourth) == "websocket \(suffix) 4")
|
||||
let detached = try await workflow.createGroup(named: "Detached \(suffix) 2", into: nil)
|
||||
let detachedCopy = try await workflow.duplicate(detached, nameSuffix: suffix)
|
||||
#expect(workflow.displayName(for: detachedCopy) == "Detached \(suffix) 3")
|
||||
let interior = try await workflow.createGroup(named: "websocket \(suffix) backup", into: nil)
|
||||
let interiorCopy = try await workflow.duplicate(interior, nameSuffix: suffix)
|
||||
#expect(workflow.displayName(for: interiorCopy) == "websocket \(suffix) backup \(suffix)")
|
||||
}
|
||||
|
||||
@Test func serverCopyIsAdjacentAndIndependent() async throws {
|
||||
let fixture = try DuplicateFixture()
|
||||
let workflow = fixture.workflow
|
||||
|
||||
Reference in New Issue
Block a user