mirror of
https://github.com/softinio/Fishee.git
synced 2025-02-22 05:26:05 -08:00
swift format
This commit is contained in:
parent
f69cec36ac
commit
54edbfc36c
8 changed files with 407 additions and 395 deletions
|
@ -1,12 +1,12 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -15,30 +15,30 @@
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Data Structure/Schema representing an entry in a fish history file.
|
/// Data Structure/Schema representing an entry in a fish history file.
|
||||||
struct FishHistoryEntry: Equatable {
|
struct FishHistoryEntry: Equatable {
|
||||||
let cmd: String
|
let cmd: String
|
||||||
let when: Int
|
let when: Int
|
||||||
let paths: [String]
|
let paths: [String]
|
||||||
|
|
||||||
/// Converts time to Date object
|
/// Converts time to Date object
|
||||||
func getDate() -> Date {
|
func getDate() -> Date {
|
||||||
Date(timeIntervalSince1970: TimeInterval(when))
|
Date(timeIntervalSince1970: TimeInterval(when))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts structure back to list of strings as expected in a fish history file.
|
/// Converts structure back to list of strings as expected in a fish history file.
|
||||||
func writeEntry() -> [String] {
|
func writeEntry() -> [String] {
|
||||||
var output: [String] = []
|
var output: [String] = []
|
||||||
|
|
||||||
output.append("- cmd: \(cmd)")
|
output.append("- cmd: \(cmd)")
|
||||||
output.append(" when: \(when)")
|
output.append(" when: \(when)")
|
||||||
|
|
||||||
if !paths.isEmpty {
|
if !paths.isEmpty {
|
||||||
output.append(" paths:")
|
output.append(" paths:")
|
||||||
paths.forEach { output.append(" - \(String($0))") }
|
paths.forEach { output.append(" - \(String($0))") }
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
/// Gets the full file path to a given file.
|
/// Gets the full file path to a given file.
|
||||||
///
|
///
|
||||||
/// - Parameters
|
/// - Parameters
|
||||||
|
@ -25,20 +23,20 @@ import Foundation
|
||||||
///
|
///
|
||||||
/// - Returns: A Valid file URL or None if invalid.
|
/// - Returns: A Valid file URL or None if invalid.
|
||||||
func getPath(_ pathStr: String) -> URL? {
|
func getPath(_ pathStr: String) -> URL? {
|
||||||
let userHomeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
|
let userHomeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
|
||||||
var filePath: String = pathStr
|
var filePath: String = pathStr
|
||||||
|
|
||||||
if pathStr.hasPrefix("~") {
|
if pathStr.hasPrefix("~") {
|
||||||
filePath = (pathStr as NSString).expandingTildeInPath
|
filePath = (pathStr as NSString).expandingTildeInPath
|
||||||
}
|
}
|
||||||
|
|
||||||
if pathStr.hasPrefix("$HOME") {
|
if pathStr.hasPrefix("$HOME") {
|
||||||
filePath = filePath.replacingOccurrences(of: "$HOME", with: userHomeDirectory)
|
filePath = filePath.replacingOccurrences(of: "$HOME", with: userHomeDirectory)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !FileManager.default.fileExists(atPath: filePath) {
|
if !FileManager.default.fileExists(atPath: filePath) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return URL(fileURLWithPath: filePath)
|
return URL(fileURLWithPath: filePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@ -22,81 +21,83 @@ let DEFAULT_FISH_HISTORY_LOCATION: String = "~/.local/share/fish/fish_history"
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct Fishee: ParsableCommand {
|
struct Fishee: ParsableCommand {
|
||||||
@Option(name: [.short, .customLong("history-file")], help: "Location of your fish history file. Will default to ~/.local/share/fish/fish_history")
|
@Option(
|
||||||
var fishHistoryLocationStr: String?
|
name: [.short, .customLong("history-file")],
|
||||||
|
help: "Location of your fish history file. Will default to ~/.local/share/fish/fish_history")
|
||||||
@Option(name: .shortAndLong, help: "File path to file to merge with history file.")
|
var fishHistoryLocationStr: String?
|
||||||
var mergeFile: String?
|
|
||||||
|
@Option(name: .shortAndLong, help: "File path to file to merge with history file.")
|
||||||
@Option(
|
var mergeFile: String?
|
||||||
name: [.short, .customLong("output-file")],
|
|
||||||
help: "File to write to. Default: same as current history file."
|
@Option(
|
||||||
)
|
name: [.short, .customLong("output-file")],
|
||||||
var writeFileStr: String?
|
help: "File to write to. Default: same as current history file."
|
||||||
|
)
|
||||||
@Flag(
|
var writeFileStr: String?
|
||||||
name: .shortAndLong,
|
|
||||||
help: "Dry run. Will only print to the console without actually modifying the history file."
|
@Flag(
|
||||||
)
|
name: .shortAndLong,
|
||||||
var dryRun: Bool = false
|
help: "Dry run. Will only print to the console without actually modifying the history file."
|
||||||
|
)
|
||||||
@Flag(
|
var dryRun: Bool = false
|
||||||
name: .shortAndLong,
|
|
||||||
help: "Remove duplicates from combined history. Default: false"
|
@Flag(
|
||||||
)
|
name: .shortAndLong,
|
||||||
var removeDuplicates: Bool = false
|
help: "Remove duplicates from combined history. Default: false"
|
||||||
|
)
|
||||||
@Flag(
|
var removeDuplicates: Bool = false
|
||||||
name: .shortAndLong,
|
|
||||||
inversion: .prefixedNo,
|
@Flag(
|
||||||
help: "Backup fish history file given before writing."
|
name: .shortAndLong,
|
||||||
)
|
inversion: .prefixedNo,
|
||||||
var backup: Bool = true
|
help: "Backup fish history file given before writing."
|
||||||
|
)
|
||||||
var fishHistoryLocation: URL? {
|
var backup: Bool = true
|
||||||
let pathStr = fishHistoryLocationStr ?? DEFAULT_FISH_HISTORY_LOCATION
|
|
||||||
return getPath(pathStr)
|
var fishHistoryLocation: URL? {
|
||||||
}
|
let pathStr = fishHistoryLocationStr ?? DEFAULT_FISH_HISTORY_LOCATION
|
||||||
|
return getPath(pathStr)
|
||||||
var writeFileLocation: URL? {
|
}
|
||||||
let pathStr = writeFileStr ?? DEFAULT_FISH_HISTORY_LOCATION
|
|
||||||
return getPath(pathStr)
|
var writeFileLocation: URL? {
|
||||||
}
|
let pathStr = writeFileStr ?? DEFAULT_FISH_HISTORY_LOCATION
|
||||||
|
return getPath(pathStr)
|
||||||
public func run() throws {
|
}
|
||||||
let mergeFileLocation = mergeFile.flatMap { getPath($0) }
|
|
||||||
let finalHistory: [FishHistoryEntry] = switch (fishHistoryLocation, mergeFileLocation) {
|
public func run() throws {
|
||||||
case let (fishHistoryLocation?, mergeFileLocation?):
|
let mergeFileLocation = mergeFile.flatMap { getPath($0) }
|
||||||
{
|
let finalHistory: [FishHistoryEntry] =
|
||||||
let currentHistory = parseFishHistory(from: fishHistoryLocation.path) ?? []
|
switch (fishHistoryLocation, mergeFileLocation) {
|
||||||
let toMergeHistory = parseFishHistory(from: mergeFileLocation.path) ?? []
|
case let (fishHistoryLocation?, mergeFileLocation?):
|
||||||
return mergeFishHistory(currentHistory, toMergeHistory, removeDuplicates: removeDuplicates)
|
{
|
||||||
}()
|
let currentHistory = parseFishHistory(from: fishHistoryLocation.path) ?? []
|
||||||
case let (fishHistoryLocation?, nil):
|
let toMergeHistory = parseFishHistory(from: mergeFileLocation.path) ?? []
|
||||||
parseFishHistory(from: fishHistoryLocation.path) ?? []
|
return mergeFishHistory(
|
||||||
default:
|
currentHistory, toMergeHistory, removeDuplicates: removeDuplicates)
|
||||||
[]
|
}()
|
||||||
|
case let (fishHistoryLocation?, nil):
|
||||||
|
parseFishHistory(from: fishHistoryLocation.path) ?? []
|
||||||
|
default:
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
if dryRun {
|
||||||
|
finalHistory.forEach { print("\($0.writeEntry().joined(separator: "\n"))") }
|
||||||
|
} else {
|
||||||
|
if let writePath = writeFileLocation?.path {
|
||||||
|
let result = writeFishHistory(
|
||||||
|
to: writePath,
|
||||||
|
history: finalHistory,
|
||||||
|
backup: backup
|
||||||
|
)
|
||||||
|
if result {
|
||||||
|
print("Succussfully updated \(writePath)")
|
||||||
|
} else {
|
||||||
|
print("Failed to update \(writePath)")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if dryRun {
|
|
||||||
finalHistory.forEach { print("\($0.writeEntry().joined(separator: "\n"))") }
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if let writePath = writeFileLocation?.path {
|
|
||||||
let result = writeFishHistory(
|
|
||||||
to: writePath,
|
|
||||||
history: finalHistory,
|
|
||||||
backup: backup
|
|
||||||
)
|
|
||||||
if result {
|
|
||||||
print("Succussfully updated \(writePath)")
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
print("Failed to update \(writePath)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,21 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
/// Make a backup of the fish history.
|
/// Make a backup of the fish history.
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
|
@ -31,32 +29,32 @@ import Foundation
|
||||||
///
|
///
|
||||||
/// - Returns: true if backup copy successful, false if not.
|
/// - Returns: true if backup copy successful, false if not.
|
||||||
func backupHistory(_ path: String) -> Bool {
|
func backupHistory(_ path: String) -> Bool {
|
||||||
let fileManager = FileManager.default
|
let fileManager = FileManager.default
|
||||||
|
|
||||||
guard fileManager.fileExists(atPath: path) else {
|
|
||||||
print("File does not exist at path: \(path)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let fileURL = URL(fileURLWithPath: path)
|
|
||||||
let fileExtension = fileURL.pathExtension
|
|
||||||
let fileNameWithoutExtension = fileURL.deletingPathExtension().lastPathComponent
|
|
||||||
let directory = fileURL.deletingLastPathComponent()
|
|
||||||
|
|
||||||
let newFileName = "\(fileNameWithoutExtension)_copy"
|
|
||||||
let newFileURL = directory.appendingPathComponent(newFileName).appendingPathExtension(fileExtension)
|
|
||||||
|
|
||||||
do {
|
|
||||||
try? fileManager.removeItem(at: newFileURL)
|
|
||||||
try fileManager.copyItem(at: fileURL, to: newFileURL)
|
|
||||||
print("File duplicated successfully to: \(newFileURL.path)")
|
|
||||||
return true
|
|
||||||
} catch {
|
|
||||||
print("error making a backup of \(path), got error: \(error)")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
guard fileManager.fileExists(atPath: path) else {
|
||||||
|
print("File does not exist at path: \(path)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURL = URL(fileURLWithPath: path)
|
||||||
|
let fileExtension = fileURL.pathExtension
|
||||||
|
let fileNameWithoutExtension = fileURL.deletingPathExtension().lastPathComponent
|
||||||
|
let directory = fileURL.deletingLastPathComponent()
|
||||||
|
|
||||||
|
let newFileName = "\(fileNameWithoutExtension)_copy"
|
||||||
|
let newFileURL = directory.appendingPathComponent(newFileName).appendingPathExtension(
|
||||||
|
fileExtension)
|
||||||
|
|
||||||
|
do {
|
||||||
|
try? fileManager.removeItem(at: newFileURL)
|
||||||
|
try fileManager.copyItem(at: fileURL, to: newFileURL)
|
||||||
|
print("File duplicated successfully to: \(newFileURL.path)")
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("error making a backup of \(path), got error: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Write fish history to file.
|
/// Write fish history to file.
|
||||||
///
|
///
|
||||||
|
@ -67,32 +65,31 @@ func backupHistory(_ path: String) -> Bool {
|
||||||
///
|
///
|
||||||
/// - Returns: true if writing to file copy successful, false if not.
|
/// - Returns: true if writing to file copy successful, false if not.
|
||||||
func writeFishHistory(to path: String, history: [FishHistoryEntry], backup: Bool = true) -> Bool {
|
func writeFishHistory(to path: String, history: [FishHistoryEntry], backup: Bool = true) -> Bool {
|
||||||
var output = ""
|
var output = ""
|
||||||
|
|
||||||
if backup {
|
if backup {
|
||||||
let result = backupHistory(path)
|
let result = backupHistory(path)
|
||||||
if !result {
|
if !result {
|
||||||
print("Failed to backup \(path) so aborting!")
|
print("Failed to backup \(path) so aborting!")
|
||||||
return false
|
return false
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
history.forEach { output += $0.writeEntry().joined(separator: "\n") + "\n" }
|
|
||||||
|
history.forEach { output += $0.writeEntry().joined(separator: "\n") + "\n" }
|
||||||
if !output.isEmpty {
|
|
||||||
do {
|
if !output.isEmpty {
|
||||||
try output.write(toFile: path, atomically: true, encoding: .utf8)
|
do {
|
||||||
print("Successfully wrote merged history to \(path)")
|
try output.write(toFile: path, atomically: true, encoding: .utf8)
|
||||||
return true
|
print("Successfully wrote merged history to \(path)")
|
||||||
} catch {
|
return true
|
||||||
print("Error writing merged history: \(error)")
|
} catch {
|
||||||
return false
|
print("Error writing merged history: \(error)")
|
||||||
}
|
return false
|
||||||
}
|
|
||||||
else {
|
|
||||||
print("Nothing to write to \(path)")
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print("Nothing to write to \(path)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the fish history file.
|
/// Parse the fish history file.
|
||||||
|
@ -102,41 +99,45 @@ func writeFishHistory(to path: String, history: [FishHistoryEntry], backup: Bool
|
||||||
///
|
///
|
||||||
/// - Returns: List of ``FishHistoryEntry`` entries from history file.
|
/// - Returns: List of ``FishHistoryEntry`` entries from history file.
|
||||||
func parseFishHistory(from filePath: String) -> [FishHistoryEntry]? {
|
func parseFishHistory(from filePath: String) -> [FishHistoryEntry]? {
|
||||||
guard let fileContents = try? String(contentsOfFile: filePath, encoding: .utf8) else {
|
guard let fileContents = try? String(contentsOfFile: filePath, encoding: .utf8) else {
|
||||||
print("Failed to open file.")
|
print("Failed to open file.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let lines = fileContents.split(separator: "\n").map { String($0).trimmingCharacters(in: .whitespaces) }
|
|
||||||
|
|
||||||
let initialState: (entries: [FishHistoryEntry], currentCmd: String?, currentWhen: Int?, currentPaths: [String]) = ([], nil, nil, [])
|
let lines = fileContents.split(separator: "\n").map {
|
||||||
|
String($0).trimmingCharacters(in: .whitespaces)
|
||||||
let result = lines.reduce(into: initialState) { state, line in
|
}
|
||||||
if line.starts(with: "- cmd:") {
|
|
||||||
if let cmd = state.currentCmd, let when = state.currentWhen {
|
let initialState:
|
||||||
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: state.currentPaths)
|
(entries: [FishHistoryEntry], currentCmd: String?, currentWhen: Int?, currentPaths: [String]) =
|
||||||
state.entries.append(entry)
|
([], nil, nil, [])
|
||||||
state.currentPaths = []
|
|
||||||
}
|
let result = lines.reduce(into: initialState) { state, line in
|
||||||
state.currentCmd = String(line.dropFirst("- cmd:".count).trimmingCharacters(in: .whitespaces))
|
if line.starts(with: "- cmd:") {
|
||||||
} else if line.starts(with: "when:") {
|
if let cmd = state.currentCmd, let when = state.currentWhen {
|
||||||
if let whenValue = Int(line.dropFirst("when:".count).trimmingCharacters(in: .whitespaces)) {
|
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: state.currentPaths)
|
||||||
state.currentWhen = whenValue
|
state.entries.append(entry)
|
||||||
}
|
state.currentPaths = []
|
||||||
} else if line.starts(with: "paths:") {
|
}
|
||||||
return
|
state.currentCmd = String(line.dropFirst("- cmd:".count).trimmingCharacters(in: .whitespaces))
|
||||||
} else if line.starts(with: "- ") {
|
} else if line.starts(with: "when:") {
|
||||||
let path = String(line.dropFirst("- ".count).trimmingCharacters(in: .whitespaces))
|
if let whenValue = Int(line.dropFirst("when:".count).trimmingCharacters(in: .whitespaces)) {
|
||||||
state.currentPaths.append(path)
|
state.currentWhen = whenValue
|
||||||
}
|
}
|
||||||
|
} else if line.starts(with: "paths:") {
|
||||||
|
return
|
||||||
|
} else if line.starts(with: "- ") {
|
||||||
|
let path = String(line.dropFirst("- ".count).trimmingCharacters(in: .whitespaces))
|
||||||
|
state.currentPaths.append(path)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let cmd = result.currentCmd, let when = result.currentWhen {
|
|
||||||
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: result.currentPaths)
|
if let cmd = result.currentCmd, let when = result.currentWhen {
|
||||||
return result.entries + [entry]
|
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: result.currentPaths)
|
||||||
}
|
return result.entries + [entry]
|
||||||
|
}
|
||||||
return result.entries
|
|
||||||
|
return result.entries
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge two given ``FishHistoryEntry`` lists into one list.
|
/// Merge two given ``FishHistoryEntry`` lists into one list.
|
||||||
|
@ -147,18 +148,20 @@ func parseFishHistory(from filePath: String) -> [FishHistoryEntry]? {
|
||||||
/// - removeDuplicates: if true, remove any duplicates found after merging the two lists.
|
/// - removeDuplicates: if true, remove any duplicates found after merging the two lists.
|
||||||
///
|
///
|
||||||
/// - Returns: Single list of ``FishHistoryEntry`` entries.
|
/// - Returns: Single list of ``FishHistoryEntry`` entries.
|
||||||
func mergeFishHistory(_ left: [FishHistoryEntry], _ right: [FishHistoryEntry], removeDuplicates: Bool = false) -> [FishHistoryEntry] {
|
func mergeFishHistory(
|
||||||
|
_ left: [FishHistoryEntry], _ right: [FishHistoryEntry], removeDuplicates: Bool = false
|
||||||
let merged = left + right
|
) -> [FishHistoryEntry] {
|
||||||
|
|
||||||
if removeDuplicates {
|
let merged = left + right
|
||||||
let finalList = merged.reduce(into: [String:FishHistoryEntry]()) { result, entry in
|
|
||||||
if result[entry.cmd] == nil {
|
if removeDuplicates {
|
||||||
result[entry.cmd] = entry
|
let finalList = merged.reduce(into: [String: FishHistoryEntry]()) { result, entry in
|
||||||
}
|
if result[entry.cmd] == nil {
|
||||||
}
|
result[entry.cmd] = entry
|
||||||
return Array(finalList.values)
|
}
|
||||||
} else {
|
|
||||||
return merged
|
|
||||||
}
|
}
|
||||||
|
return Array(finalList.values)
|
||||||
|
} else {
|
||||||
|
return merged
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,44 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import Fishee
|
@testable import Fishee
|
||||||
|
|
||||||
@Suite
|
@Suite
|
||||||
final class AlgebraTests {
|
final class AlgebraTests {
|
||||||
let historyItem = FishHistoryEntry(cmd: "cd Projects/Fishee/", when: 1727545693, paths: ["Projects/Fishee/"])
|
let historyItem = FishHistoryEntry(
|
||||||
|
cmd: "cd Projects/Fishee/", when: 1_727_545_693, paths: ["Projects/Fishee/"])
|
||||||
|
|
||||||
|
@Test func dateFromHistoryTest() {
|
||||||
|
let gotDate = historyItem.getDate()
|
||||||
|
#expect(gotDate == Date(timeIntervalSince1970: 1_727_545_693))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func writeEntryTest() {
|
||||||
|
let entry = historyItem.writeEntry()
|
||||||
|
#expect(entry.count > 0)
|
||||||
|
let expectedEntry = """
|
||||||
|
- cmd: cd Projects/Fishee/
|
||||||
|
when: 1727545693
|
||||||
|
paths:
|
||||||
|
- Projects/Fishee/
|
||||||
|
"""
|
||||||
|
#expect(entry.joined(separator: "\n") == expectedEntry)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func dateFromHistoryTest() {
|
|
||||||
let gotDate = historyItem.getDate()
|
|
||||||
#expect(gotDate == Date(timeIntervalSince1970: 1727545693))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func writeEntryTest() {
|
|
||||||
let entry = historyItem.writeEntry()
|
|
||||||
#expect(entry.count > 0)
|
|
||||||
let expectedEntry = """
|
|
||||||
- cmd: cd Projects/Fishee/
|
|
||||||
when: 1727545693
|
|
||||||
paths:
|
|
||||||
- Projects/Fishee/
|
|
||||||
"""
|
|
||||||
#expect(entry.joined(separator: "\n") == expectedEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,50 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
@testable import Fishee
|
|
||||||
|
|
||||||
|
@testable import Fishee
|
||||||
|
|
||||||
@Suite(.serialized)
|
@Suite(.serialized)
|
||||||
final class FileHelpersTests {
|
final class FileHelpersTests {
|
||||||
let filePath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("myfile.txt")
|
let filePath = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(
|
||||||
|
"myfile.txt")
|
||||||
init() {
|
|
||||||
try? "this is a test".write(
|
init() {
|
||||||
to: filePath,
|
try? "this is a test".write(
|
||||||
atomically: true,
|
to: filePath,
|
||||||
encoding: .utf8
|
atomically: true,
|
||||||
)
|
encoding: .utf8
|
||||||
}
|
)
|
||||||
|
}
|
||||||
deinit {
|
|
||||||
try? FileManager.default.removeItem(at: filePath)
|
deinit {
|
||||||
}
|
try? FileManager.default.removeItem(at: filePath)
|
||||||
|
}
|
||||||
@Test(arguments: [
|
|
||||||
"$HOME/myfile.txt",
|
@Test(arguments: [
|
||||||
"~/myfile.txt",
|
"$HOME/myfile.txt",
|
||||||
"\(FileManager.default.homeDirectoryForCurrentUser.path)/myfile.txt"
|
"~/myfile.txt",
|
||||||
])
|
"\(FileManager.default.homeDirectoryForCurrentUser.path)/myfile.txt",
|
||||||
func getPathTest(testPath: String) {
|
])
|
||||||
let path = getPath(testPath)
|
func getPathTest(testPath: String) {
|
||||||
let expected = URL(fileURLWithPath: "\(FileManager.default.homeDirectoryForCurrentUser.path)/myfile.txt")
|
let path = getPath(testPath)
|
||||||
#expect(path == expected)
|
let expected = URL(
|
||||||
}
|
fileURLWithPath: "\(FileManager.default.homeDirectoryForCurrentUser.path)/myfile.txt")
|
||||||
|
#expect(path == expected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,77 +1,77 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import Fishee
|
@testable import Fishee
|
||||||
|
|
||||||
@Suite
|
@Suite
|
||||||
final class FisheeTests {
|
final class FisheeTests {
|
||||||
@Test func DryRunTest() {
|
@Test func DryRunTest() {
|
||||||
do {
|
do {
|
||||||
let help = try #require(Fishee.parse(["--dry-run"]) as Fishee)
|
let help = try #require(Fishee.parse(["--dry-run"]) as Fishee)
|
||||||
#expect(help.dryRun)
|
#expect(help.dryRun)
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Test failed! \(error)")
|
Issue.record("Test failed! \(error)")
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func HistoryFileTest() {
|
|
||||||
do {
|
|
||||||
let help = try #require(Fishee.parse(["--history-file", "/tmp/fishtest.txt"]) as Fishee)
|
|
||||||
#expect(help.fishHistoryLocationStr == "/tmp/fishtest.txt")
|
|
||||||
} catch {
|
|
||||||
Issue.record("Test failed! \(error)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func OutputFileTest() {
|
@Test func HistoryFileTest() {
|
||||||
do {
|
do {
|
||||||
let help = try #require(Fishee.parse(["--output-file", "/tmp/fishtest.txt"]) as Fishee)
|
let help = try #require(Fishee.parse(["--history-file", "/tmp/fishtest.txt"]) as Fishee)
|
||||||
#expect(help.writeFileStr == "/tmp/fishtest.txt")
|
#expect(help.fishHistoryLocationStr == "/tmp/fishtest.txt")
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Test failed! \(error)")
|
Issue.record("Test failed! \(error)")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func RemoveDuplicatesTest() {
|
@Test func OutputFileTest() {
|
||||||
do {
|
do {
|
||||||
let help = try #require(Fishee.parse(["--remove-duplicates"]) as Fishee)
|
let help = try #require(Fishee.parse(["--output-file", "/tmp/fishtest.txt"]) as Fishee)
|
||||||
#expect(help.removeDuplicates)
|
#expect(help.writeFileStr == "/tmp/fishtest.txt")
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Test failed! \(error)")
|
Issue.record("Test failed! \(error)")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func BackupTest() {
|
@Test func RemoveDuplicatesTest() {
|
||||||
do {
|
do {
|
||||||
let help = try #require(Fishee.parse(["--backup"]) as Fishee)
|
let help = try #require(Fishee.parse(["--remove-duplicates"]) as Fishee)
|
||||||
#expect(help.backup)
|
#expect(help.removeDuplicates)
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Test failed! \(error)")
|
Issue.record("Test failed! \(error)")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func NoBackupTest() {
|
@Test func BackupTest() {
|
||||||
do {
|
do {
|
||||||
let help = try #require(Fishee.parse(["--no-backup"]) as Fishee)
|
let help = try #require(Fishee.parse(["--backup"]) as Fishee)
|
||||||
#expect(!help.backup)
|
#expect(help.backup)
|
||||||
} catch {
|
} catch {
|
||||||
Issue.record("Test failed! \(error)")
|
Issue.record("Test failed! \(error)")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func NoBackupTest() {
|
||||||
|
do {
|
||||||
|
let help = try #require(Fishee.parse(["--no-backup"]) as Fishee)
|
||||||
|
#expect(!help.backup)
|
||||||
|
} catch {
|
||||||
|
Issue.record("Test failed! \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,96 +1,102 @@
|
||||||
//
|
//
|
||||||
// Copyright © 2024 Salar Rahmanian.
|
// Copyright © 2024 Salar Rahmanian.
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
// You may obtain a copy of the License at
|
// You may obtain a copy of the License at
|
||||||
//
|
//
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
//
|
//
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Testing
|
import Testing
|
||||||
|
|
||||||
@testable import Fishee
|
@testable import Fishee
|
||||||
|
|
||||||
@Suite(.serialized)
|
@Suite(.serialized)
|
||||||
final class ParserTests {
|
final class ParserTests {
|
||||||
let fishHistoryFile = Bundle.module.path(forResource: "fish_history_test", ofType: "txt")
|
let fishHistoryFile = Bundle.module.path(forResource: "fish_history_test", ofType: "txt")
|
||||||
let historyItem = FishHistoryEntry(cmd: "cd Projects/Fishee/", when: 1727545693, paths: ["Projects/Fishee/"])
|
let historyItem = FishHistoryEntry(
|
||||||
let historyItem2 = FishHistoryEntry(cmd: "swift package tools-version", when: 1727545709, paths: [])
|
cmd: "cd Projects/Fishee/", when: 1_727_545_693, paths: ["Projects/Fishee/"])
|
||||||
let filePathforWriteTest = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("myfile.txt")
|
let historyItem2 = FishHistoryEntry(
|
||||||
let filePathforFileBackupTest = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("myfile_copy.txt")
|
cmd: "swift package tools-version", when: 1_727_545_709, paths: [])
|
||||||
|
let filePathforWriteTest = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(
|
||||||
deinit {
|
"myfile.txt")
|
||||||
if FileManager.default.fileExists(atPath: filePathforWriteTest.path) {
|
let filePathforFileBackupTest = FileManager.default.homeDirectoryForCurrentUser
|
||||||
_ = try? FileManager.default.removeItem(at: filePathforWriteTest)
|
.appendingPathComponent("myfile_copy.txt")
|
||||||
}
|
|
||||||
if FileManager.default.fileExists(atPath: filePathforFileBackupTest.path) {
|
|
||||||
_ = try? FileManager.default.removeItem(at: filePathforFileBackupTest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func parseFishHistoryTest() {
|
deinit {
|
||||||
#expect(fishHistoryFile != nil)
|
if FileManager.default.fileExists(atPath: filePathforWriteTest.path) {
|
||||||
let fishHistory = parseFishHistory(from: fishHistoryFile!)
|
_ = try? FileManager.default.removeItem(at: filePathforWriteTest)
|
||||||
#expect(fishHistory!.count > 0)
|
|
||||||
let expectedHistory = [historyItem, historyItem2]
|
|
||||||
#expect(fishHistory == expectedHistory)
|
|
||||||
}
|
}
|
||||||
|
if FileManager.default.fileExists(atPath: filePathforFileBackupTest.path) {
|
||||||
@Test func writeFishHistoryTest() {
|
_ = try? FileManager.default.removeItem(at: filePathforFileBackupTest)
|
||||||
let written = writeFishHistory(
|
|
||||||
to: filePathforWriteTest.path,
|
|
||||||
history: [historyItem],
|
|
||||||
backup: false
|
|
||||||
)
|
|
||||||
#expect(written)
|
|
||||||
|
|
||||||
let fileContent = try? String(contentsOf: filePathforWriteTest, encoding: .utf8)
|
|
||||||
let expectedEntry = """
|
|
||||||
- cmd: cd Projects/Fishee/
|
|
||||||
when: 1727545693
|
|
||||||
paths:
|
|
||||||
- Projects/Fishee/
|
|
||||||
|
|
||||||
"""
|
|
||||||
#expect(fileContent == expectedEntry)
|
|
||||||
|
|
||||||
// confirm backup functionality is working
|
|
||||||
#expect(FileManager.default.fileExists(atPath: filePathforWriteTest.path))
|
|
||||||
|
|
||||||
let write_again = writeFishHistory(
|
|
||||||
to: filePathforWriteTest.path,
|
|
||||||
history: [historyItem],
|
|
||||||
backup: true
|
|
||||||
)
|
|
||||||
#expect(write_again)
|
|
||||||
#expect(FileManager.default.fileExists(atPath: filePathforFileBackupTest.path))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func mergeFishHistoryTest() {
|
|
||||||
let merged = mergeFishHistory([historyItem], [historyItem2])
|
|
||||||
#expect(merged.count == 2)
|
|
||||||
#expect(merged.contains(historyItem))
|
|
||||||
#expect(merged.contains(historyItem2))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test func mergeFishHistoryWithDuplicateTest() {
|
|
||||||
let merged = mergeFishHistory([historyItem], [historyItem, historyItem2])
|
|
||||||
#expect(merged.count == 3)
|
|
||||||
#expect(merged.contains(historyItem))
|
|
||||||
#expect(merged.contains(historyItem2))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test func mergeFishHistoryRemoveDuplicateTest() {
|
@Test func parseFishHistoryTest() {
|
||||||
let merged = mergeFishHistory([historyItem], [historyItem, historyItem2], removeDuplicates: true)
|
#expect(fishHistoryFile != nil)
|
||||||
#expect(merged.count == 2)
|
let fishHistory = parseFishHistory(from: fishHistoryFile!)
|
||||||
#expect(merged.contains(historyItem))
|
#expect(fishHistory!.count > 0)
|
||||||
#expect(merged.contains(historyItem2))
|
let expectedHistory = [historyItem, historyItem2]
|
||||||
}
|
#expect(fishHistory == expectedHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func writeFishHistoryTest() {
|
||||||
|
let written = writeFishHistory(
|
||||||
|
to: filePathforWriteTest.path,
|
||||||
|
history: [historyItem],
|
||||||
|
backup: false
|
||||||
|
)
|
||||||
|
#expect(written)
|
||||||
|
|
||||||
|
let fileContent = try? String(contentsOf: filePathforWriteTest, encoding: .utf8)
|
||||||
|
let expectedEntry = """
|
||||||
|
- cmd: cd Projects/Fishee/
|
||||||
|
when: 1727545693
|
||||||
|
paths:
|
||||||
|
- Projects/Fishee/
|
||||||
|
|
||||||
|
"""
|
||||||
|
#expect(fileContent == expectedEntry)
|
||||||
|
|
||||||
|
// confirm backup functionality is working
|
||||||
|
#expect(FileManager.default.fileExists(atPath: filePathforWriteTest.path))
|
||||||
|
|
||||||
|
let write_again = writeFishHistory(
|
||||||
|
to: filePathforWriteTest.path,
|
||||||
|
history: [historyItem],
|
||||||
|
backup: true
|
||||||
|
)
|
||||||
|
#expect(write_again)
|
||||||
|
#expect(FileManager.default.fileExists(atPath: filePathforFileBackupTest.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func mergeFishHistoryTest() {
|
||||||
|
let merged = mergeFishHistory([historyItem], [historyItem2])
|
||||||
|
#expect(merged.count == 2)
|
||||||
|
#expect(merged.contains(historyItem))
|
||||||
|
#expect(merged.contains(historyItem2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func mergeFishHistoryWithDuplicateTest() {
|
||||||
|
let merged = mergeFishHistory([historyItem], [historyItem, historyItem2])
|
||||||
|
#expect(merged.count == 3)
|
||||||
|
#expect(merged.contains(historyItem))
|
||||||
|
#expect(merged.contains(historyItem2))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func mergeFishHistoryRemoveDuplicateTest() {
|
||||||
|
let merged = mergeFishHistory(
|
||||||
|
[historyItem], [historyItem, historyItem2], removeDuplicates: true)
|
||||||
|
#expect(merged.count == 2)
|
||||||
|
#expect(merged.contains(historyItem))
|
||||||
|
#expect(merged.contains(historyItem2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue