Refactor git repository model (#674)
- Create a new model class for operations of Git repository. - Refactor everything related with git. - Add unit tests for git functions.
This commit is contained in:
parent
5a1458e196
commit
a69c4d16b1
10 changed files with 421 additions and 181 deletions
|
|
@ -16,6 +16,7 @@ public enum AppError: Error, Equatable {
|
|||
case readingFile(fileName: String)
|
||||
case passwordDuplicated
|
||||
case gitReset
|
||||
case gitCommit
|
||||
case gitCreateSignature
|
||||
case gitPushNotSuccessful
|
||||
case pgpPublicKeyNotFound(keyID: String)
|
||||
|
|
|
|||
179
passKit/Models/GitRepository.swift
Normal file
179
passKit/Models/GitRepository.swift
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
//
|
||||
// GitRepository.swift
|
||||
// pass
|
||||
//
|
||||
// Created by Mingshen Sun on 1/25/25.
|
||||
// Copyright © 2025 Bob Sun. All rights reserved.
|
||||
//
|
||||
import ObjectiveGit
|
||||
|
||||
public typealias TransferProgressHandler = (UnsafePointer<git_transfer_progress>, UnsafeMutablePointer<ObjCBool>) -> Void
|
||||
public typealias CheckoutProgressHandler = (String, UInt, UInt) -> Void
|
||||
public typealias PushProgressHandler = (UInt32, UInt32, Int, UnsafeMutablePointer<ObjCBool>) -> Void
|
||||
public typealias CloneOptions = [AnyHashable: Any]
|
||||
public typealias PullOptions = [AnyHashable: Any]
|
||||
public typealias PushOptions = [String: Any]
|
||||
|
||||
public class GitRepository {
|
||||
let repository: GTRepository
|
||||
var branchName: String = "master"
|
||||
|
||||
public init(with localDir: URL) throws {
|
||||
guard FileManager.default.fileExists(atPath: localDir.path) else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
try self.repository = GTRepository(url: localDir)
|
||||
if let currentBranchName = try? repository.currentBranch().name {
|
||||
self.branchName = currentBranchName
|
||||
}
|
||||
}
|
||||
|
||||
public init(from remoteURL: URL, to workingDir: URL, branchName: String, options: CloneOptions, transferProgressBlock: @escaping TransferProgressHandler, checkoutProgressBlock: @escaping CheckoutProgressHandler) throws {
|
||||
self.repository = try GTRepository.clone(
|
||||
from: remoteURL,
|
||||
toWorkingDirectory: workingDir,
|
||||
options: options,
|
||||
transferProgressBlock: transferProgressBlock
|
||||
)
|
||||
self.branchName = branchName
|
||||
guard !repository.isHEADUnborn else {
|
||||
return
|
||||
}
|
||||
if (try repository.currentBranch().name) != branchName {
|
||||
try checkoutAndChangeBranch(branchName: branchName, progressBlock: checkoutProgressBlock)
|
||||
}
|
||||
}
|
||||
|
||||
public func checkoutAndChangeBranch(branchName: String, progressBlock: @escaping CheckoutProgressHandler) throws {
|
||||
self.branchName = branchName
|
||||
if let localBranch = try? repository.lookUpBranch(withName: branchName, type: .local, success: nil) {
|
||||
let checkoutOptions = GTCheckoutOptions(strategy: .force, progressBlock: progressBlock)
|
||||
try repository.checkoutReference(localBranch.reference, options: checkoutOptions)
|
||||
try repository.moveHEAD(to: localBranch.reference)
|
||||
} else {
|
||||
let remoteBranchName = "origin/\(branchName)"
|
||||
let remoteBranch = try repository.lookUpBranch(withName: remoteBranchName, type: .remote, success: nil)
|
||||
guard let remoteBranchOid = remoteBranch.oid else {
|
||||
throw AppError.repositoryRemoteBranchNotFound(branchName: remoteBranchName)
|
||||
}
|
||||
let localBranch = try repository.createBranchNamed(branchName, from: remoteBranchOid, message: nil)
|
||||
try localBranch.updateTrackingBranch(remoteBranch)
|
||||
let checkoutOptions = GTCheckoutOptions(strategy: .force, progressBlock: progressBlock)
|
||||
try repository.checkoutReference(localBranch.reference, options: checkoutOptions)
|
||||
try repository.moveHEAD(to: localBranch.reference)
|
||||
}
|
||||
}
|
||||
|
||||
public func pull(
|
||||
options: PullOptions,
|
||||
transferProgressBlock: @escaping TransferProgressHandler
|
||||
) throws {
|
||||
let remote = try GTRemote(name: "origin", in: repository)
|
||||
try repository.pull(repository.currentBranch(), from: remote, withOptions: options, progress: transferProgressBlock)
|
||||
}
|
||||
|
||||
public func getRecentCommits(count: Int) throws -> [GTCommit] {
|
||||
var commits = [GTCommit]()
|
||||
let enumerator = try GTEnumerator(repository: repository)
|
||||
if let targetOID = try repository.headReference().targetOID {
|
||||
try enumerator.pushSHA(targetOID.sha)
|
||||
}
|
||||
for _ in 0 ..< count {
|
||||
if let commit = try? enumerator.nextObject(withSuccess: nil) {
|
||||
commits.append(commit)
|
||||
}
|
||||
}
|
||||
return commits
|
||||
}
|
||||
|
||||
public func add(path: String) throws {
|
||||
try repository.index().addFile(path)
|
||||
try repository.index().write()
|
||||
}
|
||||
|
||||
public func rm(path: String) throws {
|
||||
guard let repoURL = repository.fileURL else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
|
||||
let url = repoURL.appendingPathComponent(path)
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
try repository.index().removeFile(path)
|
||||
try repository.index().write()
|
||||
}
|
||||
|
||||
public func mv(from: String, to: String) throws {
|
||||
guard let repoURL = repository.fileURL else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
|
||||
let fromURL = repoURL.appendingPathComponent(from)
|
||||
let toURL = repoURL.appendingPathComponent(to)
|
||||
try FileManager.default.moveItem(at: fromURL, to: toURL)
|
||||
try add(path: to)
|
||||
try rm(path: from)
|
||||
}
|
||||
|
||||
public func commit(name: String, email: String, message: String) throws -> GTCommit {
|
||||
guard let signature = GTSignature(name: name, email: email, time: Date()) else {
|
||||
throw AppError.gitCreateSignature
|
||||
}
|
||||
return try commit(signature: signature, message: message)
|
||||
}
|
||||
|
||||
public func commit(signature: GTSignature, message: String) throws -> GTCommit {
|
||||
let newTree = try repository.index().writeTree()
|
||||
if repository.isHEADUnborn {
|
||||
return try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: nil, updatingReferenceNamed: "HEAD")
|
||||
}
|
||||
let headReference = try repository.headReference()
|
||||
let commitEnum = try GTEnumerator(repository: repository)
|
||||
try commitEnum.pushSHA(headReference.targetOID!.sha)
|
||||
guard let parent = commitEnum.nextObject() as? GTCommit else {
|
||||
throw AppError.gitCommit
|
||||
}
|
||||
return try repository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
|
||||
}
|
||||
|
||||
public func push(
|
||||
options: [String: Any],
|
||||
transferProgressBlock: @escaping PushProgressHandler
|
||||
) throws {
|
||||
let branch = try repository.currentBranch()
|
||||
let remote = try GTRemote(name: "origin", in: repository)
|
||||
try repository.push(branch, to: remote, withOptions: options, progress: transferProgressBlock)
|
||||
}
|
||||
|
||||
public func getLocalCommits() throws -> [GTCommit] {
|
||||
let remoteBranchName = "origin/\(branchName)"
|
||||
let remoteBranch = try repository.lookUpBranch(withName: remoteBranchName, type: .remote, success: nil)
|
||||
return try repository.localCommitsRelative(toRemoteBranch: remoteBranch)
|
||||
}
|
||||
|
||||
public func numberOfCommits() -> Int {
|
||||
Int(repository.numberOfCommits(inCurrentBranch: nil))
|
||||
}
|
||||
|
||||
public func reset() throws {
|
||||
let localCommits = try getLocalCommits()
|
||||
if localCommits.isEmpty {
|
||||
return
|
||||
}
|
||||
guard let firstLocalCommit = localCommits.last,
|
||||
firstLocalCommit.parents.count == 1,
|
||||
let newHead = firstLocalCommit.parents.first else {
|
||||
throw AppError.gitReset
|
||||
}
|
||||
try repository.reset(to: newHead, resetType: .hard)
|
||||
}
|
||||
|
||||
public func lastCommitDate(path: String) throws -> Date {
|
||||
let blameHunks = try repository.blame(withFile: path, options: nil).hunks
|
||||
guard let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else {
|
||||
return Date(timeIntervalSince1970: 0)
|
||||
}
|
||||
return Date(timeIntervalSince1970: latestCommitTime)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +82,10 @@ public class Password {
|
|||
initEverything()
|
||||
}
|
||||
|
||||
public func fileURL(in directoryURL: URL) -> URL {
|
||||
directoryURL.appendingPathComponent(path)
|
||||
}
|
||||
|
||||
public func updatePassword(name: String, path: String, plainText: String) {
|
||||
guard self.plainText != plainText || self.path != path else {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ public final class PasswordEntity: NSManagedObject, Identifiable {
|
|||
getDirArray().joined(separator: " > ")
|
||||
}
|
||||
|
||||
public func fileURL(in directoryURL: URL) -> URL {
|
||||
directoryURL.appendingPathComponent(path)
|
||||
}
|
||||
|
||||
public func getDirArray() -> [String] {
|
||||
var parentEntity = parent
|
||||
var passwordCategoryArray: [String] = []
|
||||
|
|
|
|||
|
|
@ -23,11 +23,8 @@ public class PasswordStore {
|
|||
}()
|
||||
|
||||
public var storeURL: URL
|
||||
public var tempStoreURL: URL {
|
||||
URL(fileURLWithPath: "\(storeURL.path)-temp")
|
||||
}
|
||||
|
||||
public var storeRepository: GTRepository?
|
||||
public var gitRepository: GitRepository?
|
||||
|
||||
public var gitSignatureForNow: GTSignature? {
|
||||
let gitSignatureName = Defaults.gitSignatureName ?? Globals.gitSignatureDefaultName
|
||||
|
|
@ -54,6 +51,7 @@ public class PasswordStore {
|
|||
}
|
||||
|
||||
private let fileManager = FileManager.default
|
||||
private let notificationCenter = NotificationCenter.default
|
||||
private lazy var context: NSManagedObjectContext = PersistenceController.shared.viewContext()
|
||||
|
||||
public var numberOfPasswords: Int {
|
||||
|
|
@ -82,8 +80,8 @@ public class PasswordStore {
|
|||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
public var numberOfCommits: UInt? {
|
||||
storeRepository?.numberOfCommits(inCurrentBranch: nil)
|
||||
public var numberOfCommits: Int? {
|
||||
gitRepository?.numberOfCommits()
|
||||
}
|
||||
|
||||
init(url: URL = Globals.repositoryURL) {
|
||||
|
|
@ -94,7 +92,7 @@ public class PasswordStore {
|
|||
|
||||
do {
|
||||
if fileManager.fileExists(atPath: storeURL.path) {
|
||||
try self.storeRepository = GTRepository(url: storeURL)
|
||||
try self.gitRepository = GitRepository(with: storeURL)
|
||||
}
|
||||
} catch {
|
||||
print(error)
|
||||
|
|
@ -126,31 +124,20 @@ public class PasswordStore {
|
|||
public func cloneRepository(
|
||||
remoteRepoURL: URL,
|
||||
branchName: String,
|
||||
options: [AnyHashable: Any]? = nil,
|
||||
transferProgressBlock: @escaping (UnsafePointer<git_transfer_progress>, UnsafeMutablePointer<ObjCBool>) -> Void = { _, _ in },
|
||||
checkoutProgressBlock: @escaping (String, UInt, UInt) -> Void = { _, _, _ in }
|
||||
options: CloneOptions = [:],
|
||||
transferProgressBlock: @escaping TransferProgressHandler = { _, _ in },
|
||||
checkoutProgressBlock: @escaping CheckoutProgressHandler = { _, _, _ in }
|
||||
) throws {
|
||||
try? fileManager.removeItem(at: storeURL)
|
||||
try? fileManager.removeItem(at: tempStoreURL)
|
||||
gitPassword = nil
|
||||
gitSSHPrivateKeyPassphrase = nil
|
||||
do {
|
||||
storeRepository = try GTRepository.clone(
|
||||
from: remoteRepoURL,
|
||||
toWorkingDirectory: tempStoreURL,
|
||||
options: options,
|
||||
transferProgressBlock: transferProgressBlock
|
||||
)
|
||||
try fileManager.moveItem(at: tempStoreURL, to: storeURL)
|
||||
storeRepository = try GTRepository(url: storeURL)
|
||||
if (try storeRepository?.currentBranch().name) != branchName {
|
||||
try checkoutAndChangeBranch(withName: branchName, progressBlock: checkoutProgressBlock)
|
||||
}
|
||||
gitRepository = try GitRepository(from: remoteRepoURL, to: storeURL, branchName: branchName, options: options, transferProgressBlock: transferProgressBlock, checkoutProgressBlock: checkoutProgressBlock)
|
||||
} catch {
|
||||
Defaults.lastSyncedTime = nil
|
||||
DispatchQueue.main.async {
|
||||
self.deleteCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
self.notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
throw (error)
|
||||
}
|
||||
|
|
@ -158,41 +145,24 @@ public class PasswordStore {
|
|||
DispatchQueue.main.async {
|
||||
self.deleteCoreData()
|
||||
self.initPasswordEntityCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
self.notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func checkoutAndChangeBranch(withName localBranchName: String, progressBlock: @escaping (String, UInt, UInt) -> Void) throws {
|
||||
guard let storeRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
let remoteBranchName = "origin/\(localBranchName)"
|
||||
let remoteBranch = try storeRepository.lookUpBranch(withName: remoteBranchName, type: .remote, success: nil)
|
||||
guard let remoteBranchOid = remoteBranch.oid else {
|
||||
throw AppError.repositoryRemoteBranchNotFound(branchName: remoteBranchName)
|
||||
}
|
||||
let localBranch = try storeRepository.createBranchNamed(localBranchName, from: remoteBranchOid, message: nil)
|
||||
try localBranch.updateTrackingBranch(remoteBranch)
|
||||
let checkoutOptions = GTCheckoutOptions(strategy: .force, progressBlock: progressBlock)
|
||||
try storeRepository.checkoutReference(localBranch.reference, options: checkoutOptions)
|
||||
try storeRepository.moveHEAD(to: localBranch.reference)
|
||||
}
|
||||
|
||||
public func pullRepository(
|
||||
options: [String: Any],
|
||||
progressBlock: @escaping (UnsafePointer<git_transfer_progress>, UnsafeMutablePointer<ObjCBool>) -> Void = { _, _ in }
|
||||
options: PullOptions,
|
||||
progressBlock: @escaping TransferProgressHandler = { _, _ in }
|
||||
) throws {
|
||||
guard let storeRepository else {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
let remote = try GTRemote(name: "origin", in: storeRepository)
|
||||
try storeRepository.pull(storeRepository.currentBranch(), from: remote, withOptions: options, progress: progressBlock)
|
||||
try gitRepository.pull(options: options, transferProgressBlock: progressBlock)
|
||||
Defaults.lastSyncedTime = Date()
|
||||
setAllSynced()
|
||||
DispatchQueue.main.async {
|
||||
self.deleteCoreData()
|
||||
self.initPasswordEntityCoreData()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
self.notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -202,20 +172,10 @@ public class PasswordStore {
|
|||
}
|
||||
|
||||
public func getRecentCommits(count: Int) throws -> [GTCommit] {
|
||||
guard let storeRepository else {
|
||||
return []
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
var commits = [GTCommit]()
|
||||
let enumerator = try GTEnumerator(repository: storeRepository)
|
||||
if let targetOID = try storeRepository.headReference().targetOID {
|
||||
try enumerator.pushSHA(targetOID.sha)
|
||||
}
|
||||
for _ in 0 ..< count {
|
||||
if let commit = try? enumerator.nextObject(withSuccess: nil) {
|
||||
commits.append(commit)
|
||||
}
|
||||
}
|
||||
return commits
|
||||
return try gitRepository.getRecentCommits(count: count)
|
||||
}
|
||||
|
||||
public func fetchPasswordEntityCoreData(parent: PasswordEntity?) -> [PasswordEntity] {
|
||||
|
|
@ -240,42 +200,18 @@ public class PasswordStore {
|
|||
}
|
||||
|
||||
public func getLatestUpdateInfo(path: String) -> String {
|
||||
guard let storeRepository else {
|
||||
guard let gitRepository else {
|
||||
return "Unknown".localize()
|
||||
}
|
||||
guard let blameHunks = try? storeRepository.blame(withFile: path, options: nil).hunks else {
|
||||
guard let lastCommitDate = try? gitRepository.lastCommitDate(path: path) else {
|
||||
return "Unknown".localize()
|
||||
}
|
||||
guard let latestCommitTime = blameHunks.map({ $0.finalSignature?.time?.timeIntervalSince1970 ?? 0 }).max() else {
|
||||
return "Unknown".localize()
|
||||
}
|
||||
let lastCommitDate = Date(timeIntervalSince1970: latestCommitTime)
|
||||
if Date().timeIntervalSince(lastCommitDate) <= 60 {
|
||||
return "JustNow".localize()
|
||||
}
|
||||
return Self.dateFormatter.string(from: lastCommitDate)
|
||||
}
|
||||
|
||||
private func gitAdd(path: String) throws {
|
||||
guard let storeRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
try storeRepository.index().addFile(path)
|
||||
try storeRepository.index().write()
|
||||
}
|
||||
|
||||
private func gitRm(path: String) throws {
|
||||
guard let storeRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
let url = storeURL.appendingPathComponent(path)
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
try fileManager.removeItem(at: url)
|
||||
}
|
||||
try storeRepository.index().removeFile(path)
|
||||
try storeRepository.index().write()
|
||||
}
|
||||
|
||||
private func deleteDirectoryTree(at url: URL) throws {
|
||||
var tempURL = url.deletingLastPathComponent()
|
||||
while try fileManager.contentsOfDirectory(atPath: tempURL.path).isEmpty {
|
||||
|
|
@ -289,52 +225,14 @@ public class PasswordStore {
|
|||
try fileManager.createDirectory(at: tempURL, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
private func gitMv(from: String, to: String) throws {
|
||||
let fromURL = storeURL.appendingPathComponent(from)
|
||||
let toURL = storeURL.appendingPathComponent(to)
|
||||
try fileManager.moveItem(at: fromURL, to: toURL)
|
||||
try gitAdd(path: to)
|
||||
try gitRm(path: from)
|
||||
}
|
||||
|
||||
private func gitCommit(message: String) throws -> GTCommit? {
|
||||
guard let storeRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
let newTree = try storeRepository.index().writeTree()
|
||||
let headReference = try storeRepository.headReference()
|
||||
let commitEnum = try GTEnumerator(repository: storeRepository)
|
||||
try commitEnum.pushSHA(headReference.targetOID!.sha)
|
||||
let parent = commitEnum.nextObject() as! GTCommit
|
||||
guard let signature = gitSignatureForNow else {
|
||||
throw AppError.gitCreateSignature
|
||||
}
|
||||
return try storeRepository.createCommit(with: newTree, message: message, author: signature, committer: signature, parents: [parent], updatingReferenceNamed: headReference.name)
|
||||
}
|
||||
|
||||
private func getLocalBranch(withName branchName: String) throws -> GTBranch? {
|
||||
guard let storeRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
let reference = GTBranch.localNamePrefix().appending(branchName)
|
||||
let branches = try storeRepository.branches(withPrefix: reference)
|
||||
return branches.first
|
||||
}
|
||||
|
||||
public func pushRepository(
|
||||
options: [String: Any],
|
||||
transferProgressBlock: @escaping (UInt32, UInt32, Int, UnsafeMutablePointer<ObjCBool>) -> Void = { _, _, _, _ in }
|
||||
options: PushOptions,
|
||||
transferProgressBlock: @escaping PushProgressHandler = { _, _, _, _ in }
|
||||
) throws {
|
||||
guard let storeRepository else {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
if let branch = try getLocalBranch(withName: Defaults.gitBranchName) {
|
||||
let remote = try GTRemote(name: "origin", in: storeRepository)
|
||||
try storeRepository.push(branch, to: remote, withOptions: options, progress: transferProgressBlock)
|
||||
}
|
||||
if numberOfLocalCommits != 0 {
|
||||
throw AppError.gitPushNotSuccessful
|
||||
}
|
||||
try gitRepository.push(options: options, transferProgressBlock: transferProgressBlock)
|
||||
}
|
||||
|
||||
private func addPasswordEntities(password: Password) throws -> PasswordEntity? {
|
||||
|
|
@ -372,33 +270,34 @@ public class PasswordStore {
|
|||
}
|
||||
|
||||
public func add(password: Password, keyID: String? = nil) throws -> PasswordEntity? {
|
||||
let saveURL = storeURL.appendingPathComponent(password.path)
|
||||
let saveURL = password.fileURL(in: storeURL)
|
||||
try createDirectoryTree(at: saveURL)
|
||||
try encrypt(password: password, keyID: keyID).write(to: saveURL)
|
||||
try gitAdd(path: password.path)
|
||||
_ = try gitCommit(message: "AddPassword.".localize(password.path))
|
||||
try gitCommit(message: "AddPassword.".localize(password.path))
|
||||
let newPasswordEntity = try addPasswordEntities(password: password)
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
return newPasswordEntity
|
||||
}
|
||||
|
||||
public func delete(passwordEntity: PasswordEntity) throws {
|
||||
let deletedFileURL = storeURL.appendingPathComponent(passwordEntity.path)
|
||||
let deletedFileURL = passwordEntity.fileURL(in: storeURL)
|
||||
let deletedFilePath = passwordEntity.path
|
||||
try gitRm(path: passwordEntity.path)
|
||||
try deletePasswordEntities(passwordEntity: passwordEntity)
|
||||
try deleteDirectoryTree(at: deletedFileURL)
|
||||
_ = try gitCommit(message: "RemovePassword.".localize(passwordEntity.path))
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
try gitCommit(message: "RemovePassword.".localize(deletedFilePath))
|
||||
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
}
|
||||
|
||||
public func edit(passwordEntity: PasswordEntity, password: Password, keyID: String? = nil) throws -> PasswordEntity? {
|
||||
var newPasswordEntity: PasswordEntity? = passwordEntity
|
||||
let url = storeURL.appendingPathComponent(passwordEntity.path)
|
||||
let url = passwordEntity.fileURL(in: storeURL)
|
||||
|
||||
if password.changed & PasswordChange.content.rawValue != 0 {
|
||||
try encrypt(password: password, keyID: keyID).write(to: url)
|
||||
try gitAdd(path: password.path)
|
||||
_ = try gitCommit(message: "EditPassword.".localize(passwordEntity.path))
|
||||
try gitCommit(message: "EditPassword.".localize(passwordEntity.path))
|
||||
newPasswordEntity = passwordEntity
|
||||
newPasswordEntity?.isSynced = false
|
||||
}
|
||||
|
|
@ -406,7 +305,7 @@ public class PasswordStore {
|
|||
if password.changed & PasswordChange.path.rawValue != 0 {
|
||||
let deletedFileURL = url
|
||||
// add
|
||||
let newFileURL = storeURL.appendingPathComponent(password.path)
|
||||
let newFileURL = password.fileURL(in: storeURL)
|
||||
try createDirectoryTree(at: newFileURL)
|
||||
newPasswordEntity = try addPasswordEntities(password: password)
|
||||
|
||||
|
|
@ -415,11 +314,12 @@ public class PasswordStore {
|
|||
|
||||
// delete
|
||||
try deleteDirectoryTree(at: deletedFileURL)
|
||||
let deletedFilePath = passwordEntity.path
|
||||
try deletePasswordEntities(passwordEntity: passwordEntity)
|
||||
_ = try gitCommit(message: "RenamePassword.".localize(passwordEntity.path, password.path))
|
||||
try gitCommit(message: "RenamePassword.".localize(deletedFilePath, password.path))
|
||||
}
|
||||
saveUpdatedContext()
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
return newPasswordEntity
|
||||
}
|
||||
|
||||
|
|
@ -440,17 +340,16 @@ public class PasswordStore {
|
|||
public func eraseStoreData() {
|
||||
// Delete files.
|
||||
try? fileManager.removeItem(at: storeURL)
|
||||
try? fileManager.removeItem(at: tempStoreURL)
|
||||
|
||||
// Delete core data.
|
||||
deleteCoreData()
|
||||
|
||||
// Clean up variables inside PasswordStore.
|
||||
storeRepository = nil
|
||||
gitRepository = nil
|
||||
|
||||
// Broadcast.
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.post(name: .passwordStoreErased, object: nil)
|
||||
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
notificationCenter.post(name: .passwordStoreErased, object: nil)
|
||||
}
|
||||
|
||||
public func erase() {
|
||||
|
|
@ -469,50 +368,29 @@ public class PasswordStore {
|
|||
|
||||
// return the number of discarded commits
|
||||
public func reset() throws -> Int {
|
||||
guard let storeRepository else {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
// get a list of local commits
|
||||
let localCommits = try getLocalCommits()
|
||||
if localCommits.isEmpty {
|
||||
return 0 // no new commit
|
||||
}
|
||||
// get the oldest local commit
|
||||
guard let firstLocalCommit = localCommits.last,
|
||||
firstLocalCommit.parents.count == 1,
|
||||
let newHead = firstLocalCommit.parents.first else {
|
||||
throw AppError.gitReset
|
||||
}
|
||||
try storeRepository.reset(to: newHead, resetType: .hard)
|
||||
let localCommitsCount = try getLocalCommits().count
|
||||
try gitRepository.reset()
|
||||
setAllSynced()
|
||||
deleteCoreData()
|
||||
initPasswordEntityCoreData()
|
||||
|
||||
NotificationCenter.default.post(name: .passwordStoreUpdated, object: nil)
|
||||
NotificationCenter.default.post(name: .passwordStoreChangeDiscarded, object: nil)
|
||||
return localCommits.count
|
||||
notificationCenter.post(name: .passwordStoreUpdated, object: nil)
|
||||
notificationCenter.post(name: .passwordStoreChangeDiscarded, object: nil)
|
||||
return localCommitsCount
|
||||
}
|
||||
|
||||
private func getLocalCommits() throws -> [GTCommit] {
|
||||
guard let storeRepository else {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
// get the remote branch
|
||||
let remoteBranchName = Defaults.gitBranchName
|
||||
guard let remoteBranch = try storeRepository.remoteBranches().first(where: { $0.shortName == remoteBranchName }) else {
|
||||
throw AppError.repositoryRemoteBranchNotFound(branchName: remoteBranchName)
|
||||
}
|
||||
// check oid before calling localCommitsRelative
|
||||
guard remoteBranch.oid != nil else {
|
||||
throw AppError.repositoryRemoteBranchNotFound(branchName: remoteBranchName)
|
||||
}
|
||||
|
||||
// get a list of local commits
|
||||
return try storeRepository.localCommitsRelative(toRemoteBranch: remoteBranch)
|
||||
return try gitRepository.getLocalCommits()
|
||||
}
|
||||
|
||||
public func decrypt(passwordEntity: PasswordEntity, keyID: String? = nil, requestPGPKeyPassphrase: @escaping (String) -> String) throws -> Password {
|
||||
let url = storeURL.appendingPathComponent(passwordEntity.path)
|
||||
let url = passwordEntity.fileURL(in: storeURL)
|
||||
let encryptedData = try Data(contentsOf: url)
|
||||
let data: Data? = try {
|
||||
if Defaults.isEnableGPGIDOn {
|
||||
|
|
@ -539,7 +417,7 @@ public class PasswordStore {
|
|||
}
|
||||
|
||||
public func encrypt(password: Password, keyID: String? = nil) throws -> Data {
|
||||
let encryptedDataPath = storeURL.appendingPathComponent(password.path)
|
||||
let encryptedDataPath = password.fileURL(in: storeURL)
|
||||
let keyID = keyID ?? findGPGID(from: encryptedDataPath)
|
||||
if Defaults.isEnableGPGIDOn {
|
||||
return try PGPAgent.shared.encrypt(plainData: password.plainData, keyID: keyID)
|
||||
|
|
@ -557,6 +435,37 @@ public class PasswordStore {
|
|||
}
|
||||
}
|
||||
|
||||
extension PasswordStore {
|
||||
private func gitAdd(path: String) throws {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
try gitRepository.add(path: path)
|
||||
}
|
||||
|
||||
private func gitRm(path: String) throws {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
try gitRepository.rm(path: path)
|
||||
}
|
||||
|
||||
private func gitMv(from: String, to: String) throws {
|
||||
guard let gitRepository else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
try gitRepository.mv(from: from, to: to)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func gitCommit(message: String) throws -> GTCommit {
|
||||
guard let gitRepository, let gitSignatureForNow else {
|
||||
throw AppError.repositoryNotSet
|
||||
}
|
||||
return try gitRepository.commit(signature: gitSignatureForNow, message: message)
|
||||
}
|
||||
}
|
||||
|
||||
func findGPGID(from url: URL) -> String {
|
||||
var path = url
|
||||
while !FileManager.default.fileExists(atPath: path.appendingPathComponent(".gpg-id").path),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue