Present iPhone Git actions (#51)

This commit is contained in:
2026-08-14 17:24:41 +02:00
parent fcf649e56d
commit 65eb1da71c
8 changed files with 1054 additions and 90 deletions

View File

@@ -1099,11 +1099,15 @@ public protocol MobileHomeOperationProtocol: AnyObject, Sendable {
func cancel() func cancel()
func commit(message: String) throws -> MobileHomePage
func fetch() throws -> MobileHomePage
func progress() -> MobileHomeProgress func progress() -> MobileHomeProgress
func pull() throws -> MobileHomePage func pull() throws -> MobileHomePage
func refresh() throws -> MobileHomePage func push() throws -> MobileHomePage
func refreshIfStale() throws -> MobileHomePage func refreshIfStale() throws -> MobileHomePage
@@ -1178,6 +1182,25 @@ open func cancel() {try! rustCall() {
} }
} }
open func commit(message: String)throws -> MobileHomePage {
return try FfiConverterTypeMobileHomePage_lift(try rustCallWithError(FfiConverterTypeMobileHomeFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilehomeoperation_commit(
self.uniffiCloneHandle(),
FfiConverterString.lower(message),uniffiCallStatus
)
})
}
open func fetch()throws -> MobileHomePage {
return try FfiConverterTypeMobileHomePage_lift(try rustCallWithError(FfiConverterTypeMobileHomeFfiError_lift) {
uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilehomeoperation_fetch(
self.uniffiCloneHandle(),uniffiCallStatus
)
})
}
open func progress() -> MobileHomeProgress { open func progress() -> MobileHomeProgress {
return try! FfiConverterTypeMobileHomeProgress_lift(try! rustCall() { return try! FfiConverterTypeMobileHomeProgress_lift(try! rustCall() {
uniffiCallStatus in uniffiCallStatus in
@@ -1196,10 +1219,10 @@ open func pull()throws -> MobileHomePage {
}) })
} }
open func refresh()throws -> MobileHomePage { open func push()throws -> MobileHomePage {
return try FfiConverterTypeMobileHomePage_lift(try rustCallWithError(FfiConverterTypeMobileHomeFfiError_lift) { return try FfiConverterTypeMobileHomePage_lift(try rustCallWithError(FfiConverterTypeMobileHomeFfiError_lift) {
uniffiCallStatus in uniffiCallStatus in
uniffi_ironstorage_apple_fn_method_mobilehomeoperation_refresh( uniffi_ironstorage_apple_fn_method_mobilehomeoperation_push(
self.uniffiCloneHandle(),uniffiCallStatus self.uniffiCloneHandle(),uniffiCallStatus
) )
}) })
@@ -2527,6 +2550,64 @@ public func FfiConverterTypeMobileGitIdentity_lower(_ value: MobileGitIdentity)
} }
public struct MobileHomeAction: Equatable, Hashable {
public var kind: MobileHomeActionKind
public var title: String
public var systemImage: String
// Default memberwise initializers are never public by default, so we
// declare one manually.
public init(kind: MobileHomeActionKind, title: String, systemImage: String) {
self.kind = kind
self.title = title
self.systemImage = systemImage
}
}
#if compiler(>=6)
extension MobileHomeAction: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileHomeAction: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomeAction {
return
try MobileHomeAction(
kind: FfiConverterTypeMobileHomeActionKind.read(from: &buf),
title: FfiConverterString.read(from: &buf),
systemImage: FfiConverterString.read(from: &buf)
)
}
public static func write(_ value: MobileHomeAction, into buf: inout [UInt8]) {
FfiConverterTypeMobileHomeActionKind.write(value.kind, into: &buf)
FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.systemImage, into: &buf)
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileHomeAction_lift(_ buf: RustBuffer) throws -> MobileHomeAction {
return try FfiConverterTypeMobileHomeAction.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileHomeAction_lower(_ value: MobileHomeAction) -> RustBuffer {
return FfiConverterTypeMobileHomeAction.lower(value)
}
public struct MobileHomeChange: Equatable, Hashable { public struct MobileHomeChange: Equatable, Hashable {
public var id: String public var id: String
public var title: String public var title: String
@@ -2604,16 +2685,18 @@ public struct MobileHomeCommit: Equatable, Hashable {
public var systemImage: String public var systemImage: String
public var timestamp: Int64 public var timestamp: Int64
public var changes: [MobileHomeChange] public var changes: [MobileHomeChange]
public var actions: [MobileHomeAction]
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(id: String, title: String, detail: String, systemImage: String, timestamp: Int64, changes: [MobileHomeChange]) { public init(id: String, title: String, detail: String, systemImage: String, timestamp: Int64, changes: [MobileHomeChange], actions: [MobileHomeAction]) {
self.id = id self.id = id
self.title = title self.title = title
self.detail = detail self.detail = detail
self.systemImage = systemImage self.systemImage = systemImage
self.timestamp = timestamp self.timestamp = timestamp
self.changes = changes self.changes = changes
self.actions = actions
} }
@@ -2637,7 +2720,8 @@ public struct FfiConverterTypeMobileHomeCommit: FfiConverterRustBuffer {
detail: FfiConverterString.read(from: &buf), detail: FfiConverterString.read(from: &buf),
systemImage: FfiConverterString.read(from: &buf), systemImage: FfiConverterString.read(from: &buf),
timestamp: FfiConverterInt64.read(from: &buf), timestamp: FfiConverterInt64.read(from: &buf),
changes: FfiConverterSequenceTypeMobileHomeChange.read(from: &buf) changes: FfiConverterSequenceTypeMobileHomeChange.read(from: &buf),
actions: FfiConverterSequenceTypeMobileHomeAction.read(from: &buf)
) )
} }
@@ -2648,6 +2732,7 @@ public struct FfiConverterTypeMobileHomeCommit: FfiConverterRustBuffer {
FfiConverterString.write(value.systemImage, into: &buf) FfiConverterString.write(value.systemImage, into: &buf)
FfiConverterInt64.write(value.timestamp, into: &buf) FfiConverterInt64.write(value.timestamp, into: &buf)
FfiConverterSequenceTypeMobileHomeChange.write(value.changes, into: &buf) FfiConverterSequenceTypeMobileHomeChange.write(value.changes, into: &buf)
FfiConverterSequenceTypeMobileHomeAction.write(value.actions, into: &buf)
} }
} }
@@ -2804,13 +2889,15 @@ public func FfiConverterTypeMobileHomePage_lower(_ value: MobileHomePage) -> Rus
public struct MobileHomeProgress: Equatable, Hashable { public struct MobileHomeProgress: Equatable, Hashable {
public var action: MobileHomeActionKind?
public var phase: MobileHomePhase public var phase: MobileHomePhase
public var title: String public var title: String
public var detail: String public var detail: String
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(phase: MobileHomePhase, title: String, detail: String) { public init(action: MobileHomeActionKind?, phase: MobileHomePhase, title: String, detail: String) {
self.action = action
self.phase = phase self.phase = phase
self.title = title self.title = title
self.detail = detail self.detail = detail
@@ -2832,6 +2919,7 @@ public struct FfiConverterTypeMobileHomeProgress: FfiConverterRustBuffer {
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomeProgress { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomeProgress {
return return
try MobileHomeProgress( try MobileHomeProgress(
action: FfiConverterOptionTypeMobileHomeActionKind.read(from: &buf),
phase: FfiConverterTypeMobileHomePhase.read(from: &buf), phase: FfiConverterTypeMobileHomePhase.read(from: &buf),
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
detail: FfiConverterString.read(from: &buf) detail: FfiConverterString.read(from: &buf)
@@ -2839,6 +2927,7 @@ public struct FfiConverterTypeMobileHomeProgress: FfiConverterRustBuffer {
} }
public static func write(_ value: MobileHomeProgress, into buf: inout [UInt8]) { public static func write(_ value: MobileHomeProgress, into buf: inout [UInt8]) {
FfiConverterOptionTypeMobileHomeActionKind.write(value.action, into: &buf)
FfiConverterTypeMobileHomePhase.write(value.phase, into: &buf) FfiConverterTypeMobileHomePhase.write(value.phase, into: &buf)
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.detail, into: &buf) FfiConverterString.write(value.detail, into: &buf)
@@ -2866,14 +2955,16 @@ public struct MobileHomeSummaryRow: Equatable, Hashable {
public var title: String public var title: String
public var detail: String public var detail: String
public var systemImage: String public var systemImage: String
public var actions: [MobileHomeAction]
// Default memberwise initializers are never public by default, so we // Default memberwise initializers are never public by default, so we
// declare one manually. // declare one manually.
public init(id: String, title: String, detail: String, systemImage: String) { public init(id: String, title: String, detail: String, systemImage: String, actions: [MobileHomeAction]) {
self.id = id self.id = id
self.title = title self.title = title
self.detail = detail self.detail = detail
self.systemImage = systemImage self.systemImage = systemImage
self.actions = actions
} }
@@ -2895,7 +2986,8 @@ public struct FfiConverterTypeMobileHomeSummaryRow: FfiConverterRustBuffer {
id: FfiConverterString.read(from: &buf), id: FfiConverterString.read(from: &buf),
title: FfiConverterString.read(from: &buf), title: FfiConverterString.read(from: &buf),
detail: FfiConverterString.read(from: &buf), detail: FfiConverterString.read(from: &buf),
systemImage: FfiConverterString.read(from: &buf) systemImage: FfiConverterString.read(from: &buf),
actions: FfiConverterSequenceTypeMobileHomeAction.read(from: &buf)
) )
} }
@@ -2904,6 +2996,7 @@ public struct FfiConverterTypeMobileHomeSummaryRow: FfiConverterRustBuffer {
FfiConverterString.write(value.title, into: &buf) FfiConverterString.write(value.title, into: &buf)
FfiConverterString.write(value.detail, into: &buf) FfiConverterString.write(value.detail, into: &buf)
FfiConverterString.write(value.systemImage, into: &buf) FfiConverterString.write(value.systemImage, into: &buf)
FfiConverterSequenceTypeMobileHomeAction.write(value.actions, into: &buf)
} }
} }
@@ -4619,6 +4712,86 @@ public func FfiConverterTypeMobileEntrySectionKind_lower(_ value: MobileEntrySec
public enum MobileHomeActionKind: Equatable, Hashable {
case commit
case fetch
case pull
case push
}
#if compiler(>=6)
extension MobileHomeActionKind: Sendable {}
#endif
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public struct FfiConverterTypeMobileHomeActionKind: FfiConverterRustBuffer {
typealias SwiftType = MobileHomeActionKind
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> MobileHomeActionKind {
let variant: Int32 = try readInt(&buf)
switch variant {
case 1: return .commit
case 2: return .fetch
case 3: return .pull
case 4: return .push
default: throw UniffiInternalError.unexpectedEnumCase
}
}
public static func write(_ value: MobileHomeActionKind, into buf: inout [UInt8]) {
switch value {
case .commit:
writeInt(&buf, Int32(1))
case .fetch:
writeInt(&buf, Int32(2))
case .pull:
writeInt(&buf, Int32(3))
case .push:
writeInt(&buf, Int32(4))
}
}
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileHomeActionKind_lift(_ buf: RustBuffer) throws -> MobileHomeActionKind {
return try FfiConverterTypeMobileHomeActionKind.lift(buf)
}
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
public func FfiConverterTypeMobileHomeActionKind_lower(_ value: MobileHomeActionKind) -> RustBuffer {
return FfiConverterTypeMobileHomeActionKind.lower(value)
}
public enum MobileHomeChangeKind: Equatable, Hashable { public enum MobileHomeChangeKind: Equatable, Hashable {
case passwordEntry case passwordEntry
@@ -4779,6 +4952,7 @@ public enum MobileHomeErrorKind: Equatable, Hashable {
case authentication case authentication
case conflict case conflict
case dirtyLocalChanges case dirtyLocalChanges
case noChanges
case offline case offline
case interrupted case interrupted
case secureStorage case secureStorage
@@ -4815,15 +4989,17 @@ public struct FfiConverterTypeMobileHomeErrorKind: FfiConverterRustBuffer {
case 5: return .dirtyLocalChanges case 5: return .dirtyLocalChanges
case 6: return .offline case 6: return .noChanges
case 7: return .interrupted case 7: return .offline
case 8: return .secureStorage case 8: return .interrupted
case 9: return .repository case 9: return .secureStorage
case 10: return .partialProgress case 10: return .repository
case 11: return .partialProgress
default: throw UniffiInternalError.unexpectedEnumCase default: throw UniffiInternalError.unexpectedEnumCase
} }
@@ -4853,25 +5029,29 @@ public struct FfiConverterTypeMobileHomeErrorKind: FfiConverterRustBuffer {
writeInt(&buf, Int32(5)) writeInt(&buf, Int32(5))
case .offline: case .noChanges:
writeInt(&buf, Int32(6)) writeInt(&buf, Int32(6))
case .interrupted: case .offline:
writeInt(&buf, Int32(7)) writeInt(&buf, Int32(7))
case .secureStorage: case .interrupted:
writeInt(&buf, Int32(8)) writeInt(&buf, Int32(8))
case .repository: case .secureStorage:
writeInt(&buf, Int32(9)) writeInt(&buf, Int32(9))
case .partialProgress: case .repository:
writeInt(&buf, Int32(10)) writeInt(&buf, Int32(10))
case .partialProgress:
writeInt(&buf, Int32(11))
} }
} }
} }
@@ -5052,6 +5232,7 @@ public enum MobileHomePhase: Equatable, Hashable {
case authenticating case authenticating
case receiving case receiving
case integrating case integrating
case sending
case finishing case finishing
@@ -5082,7 +5263,9 @@ public struct FfiConverterTypeMobileHomePhase: FfiConverterRustBuffer {
case 4: return .integrating case 4: return .integrating
case 5: return .finishing case 5: return .sending
case 6: return .finishing
default: throw UniffiInternalError.unexpectedEnumCase default: throw UniffiInternalError.unexpectedEnumCase
} }
@@ -5108,9 +5291,13 @@ public struct FfiConverterTypeMobileHomePhase: FfiConverterRustBuffer {
writeInt(&buf, Int32(4)) writeInt(&buf, Int32(4))
case .finishing: case .sending:
writeInt(&buf, Int32(5)) writeInt(&buf, Int32(5))
case .finishing:
writeInt(&buf, Int32(6))
} }
} }
} }
@@ -6428,6 +6615,30 @@ fileprivate struct FfiConverterOptionTypeMobileTotpPage: FfiConverterRustBuffer
} }
} }
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterOptionTypeMobileHomeActionKind: FfiConverterRustBuffer {
typealias SwiftType = MobileHomeActionKind?
public static func write(_ value: SwiftType, into buf: inout [UInt8]) {
guard let value = value else {
writeInt(&buf, Int8(0))
return
}
writeInt(&buf, Int8(1))
FfiConverterTypeMobileHomeActionKind.write(value, into: &buf)
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType {
switch try readInt(&buf) as Int8 {
case 0: return nil
case 1: return try FfiConverterTypeMobileHomeActionKind.read(from: &buf)
default: throw UniffiInternalError.unexpectedOptionalTag
}
}
}
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
@@ -6553,6 +6764,31 @@ fileprivate struct FfiConverterSequenceTypeMobileEntrySection: FfiConverterRustB
} }
} }
#if swift(>=5.8)
@_documentation(visibility: private)
#endif
fileprivate struct FfiConverterSequenceTypeMobileHomeAction: FfiConverterRustBuffer {
typealias SwiftType = [MobileHomeAction]
public static func write(_ value: [MobileHomeAction], into buf: inout [UInt8]) {
let len = Int32(value.count)
writeInt(&buf, len)
for item in value {
FfiConverterTypeMobileHomeAction.write(item, into: &buf)
}
}
public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [MobileHomeAction] {
let len: Int32 = try readInt(&buf)
var seq = [MobileHomeAction]()
seq.reserveCapacity(Int(len))
for _ in 0 ..< len {
seq.append(try FfiConverterTypeMobileHomeAction.read(from: &buf))
}
return seq
}
}
#if swift(>=5.8) #if swift(>=5.8)
@_documentation(visibility: private) @_documentation(visibility: private)
#endif #endif
@@ -6784,10 +7020,11 @@ public func mobileAuthentication()throws -> MobileAuthentication {
) )
}) })
} }
public func mobileHomeOperation() -> MobileHomeOperation { public func mobileHomeOperation(authentication: MobileAuthentication) -> MobileHomeOperation {
return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() { return try! FfiConverterTypeMobileHomeOperation_lift(try! rustCall() {
uniffiCallStatus in uniffiCallStatus in
uniffi_ironstorage_apple_fn_func_mobile_home_operation(uniffiCallStatus uniffi_ironstorage_apple_fn_func_mobile_home_operation(
FfiConverterTypeMobileAuthentication_lower(authentication),uniffiCallStatus
) )
}) })
} }
@@ -6888,7 +7125,7 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_func_mobile_authentication() != 38258) { if (uniffi_ironstorage_apple_checksum_func_mobile_authentication() != 38258) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_func_mobile_home_operation() != 10595) { if (uniffi_ironstorage_apple_checksum_func_mobile_home_operation() != 64540) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_func_mobile_key_transfer() != 57389) { if (uniffi_ironstorage_apple_checksum_func_mobile_key_transfer() != 57389) {
@@ -7020,13 +7257,19 @@ private let initializationResult: InitializationResult = {
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cancel() != 13921) { if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cancel() != 13921) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_commit() != 28659) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_fetch() != 29855) {
return InitializationResult.apiChecksumMismatch
}
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_progress() != 4980) { if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_progress() != 4980) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_pull() != 9011) { if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_pull() != 9011) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh() != 50081) { if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_push() != 33176) {
return InitializationResult.apiChecksumMismatch return InitializationResult.apiChecksumMismatch
} }
if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale() != 27818) { if (uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh_if_stale() != 27818) {

View File

@@ -428,6 +428,16 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_cached(uint64_
void uniffi_ironstorage_apple_fn_method_mobilehomeoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status void uniffi_ironstorage_apple_fn_method_mobilehomeoperation_cancel(uint64_t ptr, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_COMMIT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_COMMIT
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_commit(uint64_t ptr, RustBuffer message, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_FETCH
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_FETCH
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_fetch(uint64_t ptr, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PROGRESS #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PROGRESS
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PROGRESS #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PROGRESS
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_progress(uint64_t ptr, RustCallStatus *_Nonnull out_status RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_progress(uint64_t ptr, RustCallStatus *_Nonnull out_status
@@ -438,9 +448,9 @@ RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_progress(uint6
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_pull(uint64_t ptr, RustCallStatus *_Nonnull out_status RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_pull(uint64_t ptr, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_REFRESH #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PUSH
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_REFRESH #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_PUSH
RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_refresh(uint64_t ptr, RustCallStatus *_Nonnull out_status RustBuffer uniffi_ironstorage_apple_fn_method_mobilehomeoperation_push(uint64_t ptr, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_REFRESH_IF_STALE #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_METHOD_MOBILEHOMEOPERATION_REFRESH_IF_STALE
@@ -551,8 +561,7 @@ uint64_t uniffi_ironstorage_apple_fn_func_mobile_authentication(RustCallStatus *
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_HOME_OPERATION
uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(RustCallStatus *_Nonnull out_status uint64_t uniffi_ironstorage_apple_fn_func_mobile_home_operation(uint64_t authentication, RustCallStatus *_Nonnull out_status
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_FN_FUNC_MOBILE_KEY_TRANSFER
@@ -1137,6 +1146,18 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cached(voi
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_CANCEL #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_CANCEL
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cancel(void uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_cancel(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_COMMIT
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_COMMIT
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_commit(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_FETCH
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_FETCH
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_fetch(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_PROGRESS #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_PROGRESS
@@ -1151,9 +1172,9 @@ uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_pull(void
); );
#endif #endif
#ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_REFRESH #ifndef UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_PUSH
#define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_REFRESH #define UNIFFI_FFIDEF_UNIFFI_IRONSTORAGE_APPLE_CHECKSUM_METHOD_MOBILEHOMEOPERATION_PUSH
uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_refresh(void uint16_t uniffi_ironstorage_apple_checksum_method_mobilehomeoperation_push(void
); );
#endif #endif

View File

@@ -93,8 +93,8 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
TotpListViewController(shellPage: page, authentication: authentication) TotpListViewController(shellPage: page, authentication: authentication)
case .preferences: case .preferences:
PreferencesViewController(page: page, authentication: authentication) PreferencesViewController(page: page, authentication: authentication)
default: case .home:
ShellViewController(page: page) ShellViewController(page: page, authentication: authentication)
} }
let navigation = UINavigationController(rootViewController: root) let navigation = UINavigationController(rootViewController: root)
navigation.navigationBar.prefersLargeTitles = true navigation.navigationBar.prefersLargeTitles = true
@@ -182,6 +182,7 @@ private final class AppContext: NSObject, UITabBarControllerDelegate {
@MainActor @MainActor
private final class ShellViewController: UITableViewController, MobileTabRoot { private final class ShellViewController: UITableViewController, MobileTabRoot {
fileprivate let shellTab: MobileTab fileprivate let shellTab: MobileTab
private let authentication: MobileAuthentication?
private var page: MobilePage private var page: MobilePage
private var loadTask: Task<Void, Never>? private var loadTask: Task<Void, Never>?
private var loadGeneration = 0 private var loadGeneration = 0
@@ -202,12 +203,20 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
private enum HomeRequest: Equatable { private enum HomeRequest: Equatable {
case refreshIfStale case refreshIfStale
case refresh case commit(String)
case fetch
case pull case pull
case push
var changesLocalStore: Bool {
if case .pull = self { return true }
return false
}
} }
init(page: MobilePage) { init(page: MobilePage, authentication: MobileAuthentication?) {
shellTab = page.tab shellTab = page.tab
self.authentication = authentication
self.page = page self.page = page
super.init(style: .insetGrouped) super.init(style: .insetGrouped)
title = page.title title = page.title
@@ -344,6 +353,43 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
) )
} }
override func tableView(
_ tableView: UITableView,
trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
) -> UISwipeActionsConfiguration? {
let actions = homeActions(at: indexPath).map { action in
let contextual = UIContextualAction(style: .normal, title: action.title) {
[weak self] _, _, completion in
self?.perform(action)
completion(true)
}
contextual.image = UIImage(systemName: action.systemImage)
contextual.backgroundColor = actionColor(action.kind)
return contextual
}
guard !actions.isEmpty else { return nil }
let configuration = UISwipeActionsConfiguration(actions: actions)
configuration.performsFirstActionWithFullSwipe = actions.count == 1
return configuration
}
override func tableView(
_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint
) -> UIContextMenuConfiguration? {
let actions = homeActions(at: indexPath)
guard !actions.isEmpty else { return nil }
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
UIMenu(children: actions.map { action in
UIAction(
title: action.title,
image: UIImage(systemName: action.systemImage)
) { [weak self] _ in self?.perform(action) }
})
}
}
@objc private func refreshRequested() { @objc private func refreshRequested() {
if shellTab == .home, page.state == .ready { if shellTab == .home, page.state == .ready {
guard !isHomeWorking else { guard !isHomeWorking else {
@@ -434,7 +480,13 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
} }
@objc private func statusRefreshRequested() { @objc private func statusRefreshRequested() {
runHome(.refresh) runHome(.fetch)
}
@objc private func cancelHomeRequested() {
homeOperation?.cancel()
navigationItem.leftBarButtonItem?.isEnabled = false
navigationItem.prompt = "Cancelling the current Git action…"
} }
private var homeSections: [HomeSection] { private var homeSections: [HomeSection] {
@@ -466,18 +518,25 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
content.text = row.title content.text = row.title
content.secondaryText = row.detail content.secondaryText = row.detail
cell.selectionStyle = .none cell.selectionStyle = .none
cell.accessibilityCustomActions = accessibilityActions(row.actions)
case .incoming: case .incoming:
configureCommit( configureCommit(
homePage.incoming[indexPath.row], homePage.incoming[indexPath.row],
content: &content, content: &content,
cell: cell cell: cell
) )
cell.accessibilityCustomActions = accessibilityActions(
homePage.incoming[indexPath.row].actions
)
case .outgoing: case .outgoing:
configureCommit( configureCommit(
homePage.outgoing[indexPath.row], homePage.outgoing[indexPath.row],
content: &content, content: &content,
cell: cell cell: cell
) )
cell.accessibilityCustomActions = accessibilityActions(
homePage.outgoing[indexPath.row].actions
)
case .empty: case .empty:
content.image = UIImage(systemName: "checkmark.circle") content.image = UIImage(systemName: "checkmark.circle")
content.text = "No Remote Activity" content.text = "No Remote Activity"
@@ -533,7 +592,11 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
guard homePage == nil, homeTask == nil, !isHomeWorking else { return } guard homePage == nil, homeTask == nil, !isHomeWorking else { return }
homeGeneration += 1 homeGeneration += 1
let current = homeGeneration let current = homeGeneration
let operation = mobileHomeOperation() guard let authentication else {
showHomeFailure(.unavailable)
return
}
let operation = mobileHomeOperation(authentication: authentication)
homeOperation = operation homeOperation = operation
isHomeWorking = true isHomeWorking = true
navigationItem.rightBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false
@@ -558,10 +621,14 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
private func runHome(_ request: HomeRequest) { private func runHome(_ request: HomeRequest) {
guard page.state == .ready, shellTab == .home, !isHomeWorking else { return } guard page.state == .ready, shellTab == .home, !isHomeWorking else { return }
guard let authentication else {
showHomeFailure(.unavailable)
return
}
cancelHomeWork() cancelHomeWork()
homeGeneration += 1 homeGeneration += 1
let current = homeGeneration let current = homeGeneration
let operation = mobileHomeOperation() let operation = mobileHomeOperation(authentication: authentication)
homeOperation = operation homeOperation = operation
isHomeWorking = true isHomeWorking = true
navigationItem.rightBarButtonItem?.isEnabled = false navigationItem.rightBarButtonItem?.isEnabled = false
@@ -582,8 +649,10 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
do { do {
let page: MobileHomePage = switch request { let page: MobileHomePage = switch request {
case .refreshIfStale: try operation.refreshIfStale() case .refreshIfStale: try operation.refreshIfStale()
case .refresh: try operation.refresh() case let .commit(message): try operation.commit(message: message)
case .fetch: try operation.fetch()
case .pull: try operation.pull() case .pull: try operation.pull()
case .push: try operation.push()
} }
return Result<MobileHomePage, HomeFailure>.success(page) return Result<MobileHomePage, HomeFailure>.success(page)
} catch let error as MobileHomeFfiError { } catch let error as MobileHomeFfiError {
@@ -609,24 +678,32 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
navigationItem.titleView = nil navigationItem.titleView = nil
navigationItem.prompt = nil navigationItem.prompt = nil
navigationItem.rightBarButtonItem?.isEnabled = true navigationItem.rightBarButtonItem?.isEnabled = true
navigationItem.leftBarButtonItem = nil
refreshControl?.endRefreshing() refreshControl?.endRefreshing()
switch result { switch result {
case let .success(homePage): case let .success(homePage):
self.homePage = homePage self.homePage = homePage
contentUnavailableConfiguration = nil contentUnavailableConfiguration = nil
tableView.reloadData() tableView.reloadData()
if request == .pull { if request?.changesLocalStore == true {
NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil) NotificationCenter.default.post(name: .ironStorageLocalStoreDidChange, object: nil)
NotificationCenter.default.post(
name: .ironStorageWatchSnapshotDidChange,
object: nil
)
}
if let notice = homePage.notice { if let notice = homePage.notice {
UIAccessibility.post(notification: .announcement, argument: notice.title) UIAccessibility.post(notification: .announcement, argument: notice.title)
} }
}
case let .failure(failure): case let .failure(failure):
tableView.reloadData() tableView.reloadData()
if homePage == nil { if homePage == nil {
showHomeFailure(failure) showHomeFailure(failure)
} else if failure.kind != .interrupted { presentHomeFailure(failure, retry: request ?? .fetch)
presentHomeFailure(failure) } else if failure.kind == .interrupted {
UIAccessibility.post(notification: .announcement, argument: failure.title)
} else {
presentHomeFailure(failure, retry: request ?? .fetch)
} }
} }
} }
@@ -646,7 +723,7 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
contentUnavailableConfiguration = configuration contentUnavailableConfiguration = configuration
} }
private func presentHomeFailure(_ failure: HomeFailure) { private func presentHomeFailure(_ failure: HomeFailure, retry: HomeRequest) {
let alert = UIAlertController( let alert = UIAlertController(
title: failure.title, title: failure.title,
message: failure.detail, message: failure.detail,
@@ -661,6 +738,9 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
} ?? tabs.selectedIndex } ?? tabs.selectedIndex
}) })
} }
alert.addAction(UIAlertAction(title: "Retry", style: .default) { [weak self] _ in
self?.runHome(retry)
})
alert.addAction(UIAlertAction(title: "OK", style: .cancel)) alert.addAction(UIAlertAction(title: "OK", style: .cancel))
present(alert, animated: true) present(alert, animated: true)
} }
@@ -677,6 +757,12 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
stack.spacing = 8 stack.spacing = 8
navigationItem.titleView = stack navigationItem.titleView = stack
navigationItem.prompt = progress.detail navigationItem.prompt = progress.detail
navigationItem.leftBarButtonItem = UIBarButtonItem(
title: "Cancel",
style: .plain,
target: self,
action: #selector(cancelHomeRequested)
)
} }
private func cancelHomeWork() { private func cancelHomeWork() {
@@ -690,10 +776,70 @@ private final class ShellViewController: UITableViewController, MobileTabRoot {
isHomeWorking = false isHomeWorking = false
navigationItem.titleView = nil navigationItem.titleView = nil
navigationItem.prompt = nil navigationItem.prompt = nil
navigationItem.leftBarButtonItem = nil
refreshControl?.endRefreshing() refreshControl?.endRefreshing()
navigationItem.rightBarButtonItem?.isEnabled = true navigationItem.rightBarButtonItem?.isEnabled = true
} }
private func homeActions(at indexPath: IndexPath) -> [MobileHomeAction] {
guard let homePage, homeSections.indices.contains(indexPath.section) else { return [] }
return switch homeSections[indexPath.section] {
case .summary where homePage.summaries.indices.contains(indexPath.row):
homePage.summaries[indexPath.row].actions
case .incoming where homePage.incoming.indices.contains(indexPath.row):
homePage.incoming[indexPath.row].actions
case .outgoing where homePage.outgoing.indices.contains(indexPath.row):
homePage.outgoing[indexPath.row].actions
default:
[]
}
}
private func perform(_ action: MobileHomeAction) {
switch action.kind {
case .commit: promptForCommitMessage()
case .fetch: runHome(.fetch)
case .pull: runHome(.pull)
case .push: runHome(.push)
}
}
private func promptForCommitMessage() {
let alert = UIAlertController(
title: "Commit Changes",
message: "Enter the message for all current password-store changes.",
preferredStyle: .alert
)
alert.addTextField { field in
field.placeholder = "Commit message"
field.autocapitalizationType = .sentences
field.returnKeyType = .done
}
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Commit", style: .default) { [weak self, weak alert] _ in
self?.runHome(.commit(alert?.textFields?.first?.text ?? ""))
})
present(alert, animated: true)
}
private func accessibilityActions(_ actions: [MobileHomeAction]) -> [UIAccessibilityCustomAction] {
actions.map { action in
UIAccessibilityCustomAction(name: action.title) { [weak self] _ in
self?.perform(action)
return true
}
}
}
private func actionColor(_ kind: MobileHomeActionKind) -> UIColor {
switch kind {
case .commit: .systemOrange
case .fetch: .systemGray
case .pull: .systemBlue
case .push: .systemGreen
}
}
private func stateImage(_ state: MobileShellState) -> String { private func stateImage(_ state: MobileShellState) -> String {
switch state { switch state {
case .loading: "hourglass" case .loading: "hourglass"
@@ -5275,6 +5421,12 @@ private struct HomeFailure: Error, Sendable {
detail: "IronStorage could not load the storage-provided Home page." detail: "IronStorage could not load the storage-provided Home page."
) )
static let unavailable = HomeFailure(
kind: .configuration,
title: "Home Is Unavailable",
detail: "Complete password-store setup before using Git actions."
)
private init(kind: MobileHomeErrorKind, title: String, detail: String) { private init(kind: MobileHomeErrorKind, title: String, detail: String) {
self.kind = kind self.kind = kind
self.title = title self.title = title

View File

@@ -26,7 +26,8 @@ use ironstorage::{
MobileEntrySectionKind as StorageEntrySectionKind, MobileEntrySectionKind as StorageEntrySectionKind,
}, },
mobile_home::{ mobile_home::{
self, MobileHomeChangeKind as StorageHomeChangeKind, self, MobileHomeActionKind as StorageHomeActionKind,
MobileHomeChangeKind as StorageHomeChangeKind,
MobileHomeChangeStatus as StorageHomeChangeStatus, MobileHomeError as StorageHomeError, MobileHomeChangeStatus as StorageHomeChangeStatus, MobileHomeError as StorageHomeError,
MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness, MobileHomeErrorKind as StorageHomeErrorKind, MobileHomeFreshness as StorageHomeFreshness,
MobileHomePhase as StorageHomePhase, MobileHomePhase as StorageHomePhase,
@@ -189,6 +190,32 @@ pub enum MobileHomeChangeStatus {
Deleted, Deleted,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq, uniffi::Enum)]
pub enum MobileHomeActionKind {
Commit,
Fetch,
Pull,
Push,
}
impl From<StorageHomeActionKind> for MobileHomeActionKind {
fn from(kind: StorageHomeActionKind) -> Self {
match kind {
StorageHomeActionKind::Commit => Self::Commit,
StorageHomeActionKind::Fetch => Self::Fetch,
StorageHomeActionKind::Pull => Self::Pull,
StorageHomeActionKind::Push => Self::Push,
}
}
}
#[derive(Clone, uniffi::Record)]
pub struct MobileHomeAction {
pub kind: MobileHomeActionKind,
pub title: String,
pub system_image: String,
}
impl From<StorageHomeChangeStatus> for MobileHomeChangeStatus { impl From<StorageHomeChangeStatus> for MobileHomeChangeStatus {
fn from(status: StorageHomeChangeStatus) -> Self { fn from(status: StorageHomeChangeStatus) -> Self {
match status { match status {
@@ -205,6 +232,7 @@ pub struct MobileHomeSummaryRow {
pub title: String, pub title: String,
pub detail: String, pub detail: String,
pub system_image: String, pub system_image: String,
pub actions: Vec<MobileHomeAction>,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -225,6 +253,7 @@ pub struct MobileHomeCommit {
pub system_image: String, pub system_image: String,
pub timestamp: i64, pub timestamp: i64,
pub changes: Vec<MobileHomeChange>, pub changes: Vec<MobileHomeChange>,
pub actions: Vec<MobileHomeAction>,
} }
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
@@ -259,6 +288,7 @@ impl From<mobile_home::MobileHomePage> for MobileHomePage {
title: row.title().to_owned(), title: row.title().to_owned(),
detail: row.detail().to_owned(), detail: row.detail().to_owned(),
system_image: row.system_image().to_owned(), system_image: row.system_image().to_owned(),
actions: row.actions().iter().map(mobile_home_action).collect(),
}) })
.collect(), .collect(),
incoming: page.incoming().iter().map(mobile_home_commit).collect(), incoming: page.incoming().iter().map(mobile_home_commit).collect(),
@@ -293,6 +323,15 @@ fn mobile_home_commit(commit: &mobile_home::MobileHomeCommit) -> MobileHomeCommi
status: change.status().into(), status: change.status().into(),
}) })
.collect(), .collect(),
actions: commit.actions().iter().map(mobile_home_action).collect(),
}
}
fn mobile_home_action(action: &mobile_home::MobileHomeAction) -> MobileHomeAction {
MobileHomeAction {
kind: action.kind().into(),
title: action.title().to_owned(),
system_image: action.system_image().to_owned(),
} }
} }
@@ -302,6 +341,7 @@ pub enum MobileHomePhase {
Authenticating, Authenticating,
Receiving, Receiving,
Integrating, Integrating,
Sending,
Finishing, Finishing,
} }
@@ -312,6 +352,7 @@ impl From<StorageHomePhase> for MobileHomePhase {
StorageHomePhase::Authenticating => Self::Authenticating, StorageHomePhase::Authenticating => Self::Authenticating,
StorageHomePhase::Receiving => Self::Receiving, StorageHomePhase::Receiving => Self::Receiving,
StorageHomePhase::Integrating => Self::Integrating, StorageHomePhase::Integrating => Self::Integrating,
StorageHomePhase::Sending => Self::Sending,
StorageHomePhase::Finishing => Self::Finishing, StorageHomePhase::Finishing => Self::Finishing,
} }
} }
@@ -319,6 +360,7 @@ impl From<StorageHomePhase> for MobileHomePhase {
#[derive(Clone, uniffi::Record)] #[derive(Clone, uniffi::Record)]
pub struct MobileHomeProgress { pub struct MobileHomeProgress {
pub action: Option<MobileHomeActionKind>,
pub phase: MobileHomePhase, pub phase: MobileHomePhase,
pub title: String, pub title: String,
pub detail: String, pub detail: String,
@@ -331,6 +373,7 @@ pub enum MobileHomeErrorKind {
Authentication, Authentication,
Conflict, Conflict,
DirtyLocalChanges, DirtyLocalChanges,
NoChanges,
Offline, Offline,
Interrupted, Interrupted,
SecureStorage, SecureStorage,
@@ -346,6 +389,7 @@ impl From<StorageHomeErrorKind> for MobileHomeErrorKind {
StorageHomeErrorKind::Authentication => Self::Authentication, StorageHomeErrorKind::Authentication => Self::Authentication,
StorageHomeErrorKind::Conflict => Self::Conflict, StorageHomeErrorKind::Conflict => Self::Conflict,
StorageHomeErrorKind::DirtyLocalChanges => Self::DirtyLocalChanges, StorageHomeErrorKind::DirtyLocalChanges => Self::DirtyLocalChanges,
StorageHomeErrorKind::NoChanges => Self::NoChanges,
StorageHomeErrorKind::Offline => Self::Offline, StorageHomeErrorKind::Offline => Self::Offline,
StorageHomeErrorKind::Interrupted => Self::Interrupted, StorageHomeErrorKind::Interrupted => Self::Interrupted,
StorageHomeErrorKind::SecureStorage => Self::SecureStorage, StorageHomeErrorKind::SecureStorage => Self::SecureStorage,
@@ -394,6 +438,7 @@ impl MobileHomeOperation {
pub fn progress(&self) -> MobileHomeProgress { pub fn progress(&self) -> MobileHomeProgress {
let progress = self.operation.progress(); let progress = self.operation.progress();
MobileHomeProgress { MobileHomeProgress {
action: progress.action().map(Into::into),
phase: progress.phase().into(), phase: progress.phase().into(),
title: progress.title().to_owned(), title: progress.title().to_owned(),
detail: progress.detail().to_owned(), detail: progress.detail().to_owned(),
@@ -411,14 +456,25 @@ impl MobileHomeOperation {
.map_err(Into::into) .map_err(Into::into)
} }
pub fn refresh(&self) -> Result<MobileHomePage, MobileHomeFfiError> { pub fn commit(&self, message: String) -> Result<MobileHomePage, MobileHomeFfiError> {
self.operation.refresh().map(Into::into).map_err(Into::into) self.operation
.commit(&message)
.map(Into::into)
.map_err(Into::into)
}
pub fn fetch(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
self.operation.fetch().map(Into::into).map_err(Into::into)
} }
pub fn pull(&self) -> Result<MobileHomePage, MobileHomeFfiError> { pub fn pull(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
self.operation.pull().map(Into::into).map_err(Into::into) self.operation.pull().map(Into::into).map_err(Into::into)
} }
pub fn push(&self) -> Result<MobileHomePage, MobileHomeFfiError> {
self.operation.push().map(Into::into).map_err(Into::into)
}
pub fn cancel(&self) { pub fn cancel(&self) {
self.operation.cancel(); self.operation.cancel();
} }
@@ -1322,7 +1378,7 @@ impl From<StorageAuthenticationError> for MobileAuthenticationFfiError {
#[derive(uniffi::Object)] #[derive(uniffi::Object)]
pub struct MobileAuthentication { pub struct MobileAuthentication {
authentication: ironstorage::mobile_authentication::MobileAuthentication, authentication: Arc<ironstorage::mobile_authentication::MobileAuthentication>,
} }
#[uniffi::export] #[uniffi::export]
@@ -1855,9 +1911,13 @@ pub fn set_selected_mobile_tab(tab: MobileTab) -> Result<(), MobilePreferenceErr
} }
#[uniffi::export] #[uniffi::export]
pub fn mobile_home_operation() -> Arc<MobileHomeOperation> { pub fn mobile_home_operation(
authentication: Arc<MobileAuthentication>,
) -> Arc<MobileHomeOperation> {
Arc::new(MobileHomeOperation { Arc::new(MobileHomeOperation {
operation: mobile_home::MobileHomeOperation::default(), operation: mobile_home::MobileHomeOperation::new(Arc::clone(
&authentication.authentication,
)),
}) })
} }
@@ -1887,7 +1947,7 @@ pub fn mobile_password_search(query: String) -> Result<MobilePasswordPage, Mobil
#[uniffi::export] #[uniffi::export]
pub fn mobile_authentication() -> Result<Arc<MobileAuthentication>, MobileAuthenticationFfiError> { pub fn mobile_authentication() -> Result<Arc<MobileAuthentication>, MobileAuthenticationFfiError> {
Ok(Arc::new(MobileAuthentication { Ok(Arc::new(MobileAuthentication {
authentication: ironstorage::mobile_authentication::MobileAuthentication::load()?, authentication: Arc::new(ironstorage::mobile_authentication::MobileAuthentication::load()?),
})) }))
} }
@@ -1931,8 +1991,8 @@ pub fn replace_configured_mobile_application_token(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
MobileHomePhase, MobileOnboardingErrorKind, MobileOnboardingFfiError, MobileShellState, MobileHomeActionKind, MobileOnboardingErrorKind, MobileOnboardingFfiError,
MobileTab, MobileShellState, MobileTab, StorageHomeActionKind,
}; };
#[test] #[test]
@@ -1976,10 +2036,10 @@ mod tests {
} }
#[test] #[test]
fn home_operation_exposes_typed_progress_and_cancellation() { fn home_bridge_preserves_storage_action_kinds() {
let operation = super::mobile_home_operation(); assert_eq!(
assert_eq!(operation.progress().phase, MobileHomePhase::Validating); MobileHomeActionKind::from(StorageHomeActionKind::Push),
operation.cancel(); MobileHomeActionKind::Push
assert_eq!(operation.progress().phase, MobileHomePhase::Validating); );
} }
} }

View File

@@ -2255,6 +2255,25 @@ impl GitRepository {
self.commit_tree(message, tree) self.commit_tree(message, tree)
} }
/// Stage every current worktree/index change and create one user-requested
/// commit without exposing staging policy to a frontend.
pub fn commit_all(&self, message: &str) -> Result<String, GitError> {
let status = self.status()?;
let paths = status
.staged()
.iter()
.chain(status.unstaged())
.map(|change| change.path().to_owned())
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>();
if paths.is_empty() {
return Err(GitError::NoChanges);
}
self.stage_and_commit(&paths, message)?
.ok_or(GitError::NoChanges)
}
fn commit_tree(&self, message: &str, tree: gix::hash::ObjectId) -> Result<String, GitError> { fn commit_tree(&self, message: &str, tree: gix::hash::ObjectId) -> Result<String, GitError> {
validate_commit_message(message)?; validate_commit_message(message)?;
let parent = self.repository.head_id().ok().map(|id| id.detach()); let parent = self.repository.head_id().ok().map(|id| id.detach());

View File

@@ -195,9 +195,38 @@ struct MobileAuthenticationStatus {
next_editor_id: u64, next_editor_id: u64,
editors: BTreeMap<u64, MobileEntryDraft>, editors: BTreeMap<u64, MobileEntryDraft>,
mutation_active: bool, mutation_active: bool,
repository_operation_active: bool,
watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>, watch_shared_totp_entries: std::collections::BTreeSet<EntryPath>,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MobileRepositoryOperation {
Commit,
Fetch,
Pull,
Push,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum MobileRepositoryOperationError {
Busy,
DirtyEditor,
OpenEditor,
Unavailable,
}
pub(crate) struct MobileRepositoryOperationGuard<'a> {
authentication: &'a MobileAuthentication,
}
impl Drop for MobileRepositoryOperationGuard<'_> {
fn drop(&mut self) {
if let Ok(mut status) = self.authentication.status.lock() {
status.repository_operation_active = false;
}
}
}
/// One process-wide mobile authentication lease shared by every tab and viewer. /// One process-wide mobile authentication lease shared by every tab and viewer.
pub struct MobileAuthentication { pub struct MobileAuthentication {
config: Config, config: Config,
@@ -232,6 +261,7 @@ impl MobileAuthentication {
next_editor_id: 0, next_editor_id: 0,
editors: BTreeMap::new(), editors: BTreeMap::new(),
mutation_active: false, mutation_active: false,
repository_operation_active: false,
watch_shared_totp_entries: config.watch_shared_totp_entries().clone(), watch_shared_totp_entries: config.watch_shared_totp_entries().clone(),
}), }),
config, config,
@@ -667,6 +697,7 @@ impl MobileAuthentication {
field: u64, field: u64,
value: String, value: String,
) -> Result<MobileEntryPage, MobileAuthenticationError> { ) -> Result<MobileEntryPage, MobileAuthenticationError> {
self.ensure_repository_idle()?;
let mut document = self.open_active_document(path)?; let mut document = self.open_active_document(path)?;
document document
.replace_field_value(EntryFieldId::from_value(field), value.into_bytes()) .replace_field_value(EntryFieldId::from_value(field), value.into_bytes())
@@ -824,6 +855,7 @@ impl MobileAuthentication {
fields: Vec<MobileEntryEditorInput>, fields: Vec<MobileEntryEditorInput>,
) -> Result<MobileEntryPage, MobileAuthenticationError> { ) -> Result<MobileEntryPage, MobileAuthenticationError> {
self.ensure_active()?; self.ensure_active()?;
self.ensure_repository_idle()?;
let mut draft = self let mut draft = self
.status()? .status()?
.editors .editors
@@ -939,10 +971,10 @@ impl MobileAuthentication {
draft: MobileEntryDraft, draft: MobileEntryDraft,
) -> Result<MobileEntryEditorSession, MobileAuthenticationError> { ) -> Result<MobileEntryEditorSession, MobileAuthenticationError> {
let mut status = self.status()?; let mut status = self.status()?;
if status.mutation_active { if status.mutation_active || status.repository_operation_active {
return Err(entry_detail( return Err(entry_detail(
"Password Action In Progress", "Password Action In Progress",
"wait for the current move, copy, or delete action to finish", "wait for the current password-store action to finish",
)); ));
} }
let id = status.next_editor_id; let id = status.next_editor_id;
@@ -960,7 +992,7 @@ impl MobileAuthentication {
fn reserve_entry_mutation(&self) -> Result<(), MobileAuthenticationError> { fn reserve_entry_mutation(&self) -> Result<(), MobileAuthenticationError> {
let mut status = self.status()?; let mut status = self.status()?;
// ponytail: serialize mobile mutations; use per-path reservations if concurrent UI needs it. // ponytail: serialize mobile mutations; use per-path reservations if concurrent UI needs it.
if status.mutation_active { if status.mutation_active || status.repository_operation_active {
return Err(MobileAuthenticationError::new( return Err(MobileAuthenticationError::new(
MobileAuthenticationErrorKind::Conflict, MobileAuthenticationErrorKind::Conflict,
"Password Action In Progress", "Password Action In Progress",
@@ -971,6 +1003,44 @@ impl MobileAuthentication {
Ok(()) Ok(())
} }
pub(crate) fn reserve_repository_operation(
&self,
operation: MobileRepositoryOperation,
) -> Result<MobileRepositoryOperationGuard<'_>, MobileRepositoryOperationError> {
let mut status = self
.status
.lock()
.map_err(|_| MobileRepositoryOperationError::Unavailable)?;
let dirty_editor = status
.editors
.values()
.any(|draft| draft.document().is_modified());
if let Some(error) = repository_operation_conflict(
operation,
status.mutation_active || status.repository_operation_active,
!status.editors.is_empty(),
dirty_editor,
) {
return Err(error);
}
status.repository_operation_active = true;
drop(status);
Ok(MobileRepositoryOperationGuard {
authentication: self,
})
}
fn ensure_repository_idle(&self) -> Result<(), MobileAuthenticationError> {
if self.status()?.repository_operation_active {
Err(entry_detail(
"Repository Action In Progress",
"wait for the current commit, fetch, pull, or push action to finish",
))
} else {
Ok(())
}
}
fn release_entry_mutation(&self) { fn release_entry_mutation(&self) {
if let Ok(mut status) = self.status.lock() { if let Ok(mut status) = self.status.lock() {
status.mutation_active = false; status.mutation_active = false;
@@ -1061,6 +1131,27 @@ impl MobileAuthentication {
} }
} }
fn repository_operation_conflict(
operation: MobileRepositoryOperation,
busy: bool,
open_editor: bool,
dirty_editor: bool,
) -> Option<MobileRepositoryOperationError> {
if busy {
Some(MobileRepositoryOperationError::Busy)
} else if operation == MobileRepositoryOperation::Pull && open_editor {
Some(if dirty_editor {
MobileRepositoryOperationError::DirtyEditor
} else {
MobileRepositoryOperationError::OpenEditor
})
} else if operation == MobileRepositoryOperation::Push && dirty_editor {
Some(MobileRepositoryOperationError::DirtyEditor)
} else {
None
}
}
struct KeyOnlyProvider<'a> { struct KeyOnlyProvider<'a> {
handle: NativeAuthenticationHandle, handle: NativeAuthenticationHandle,
fingerprint: &'a str, fingerprint: &'a str,
@@ -1198,3 +1289,34 @@ fn entry_detail(title: &str, error: impl fmt::Display) -> MobileAuthenticationEr
error.to_string(), error.to_string(),
) )
} }
#[cfg(test)]
mod tests {
use super::{
MobileRepositoryOperation, MobileRepositoryOperationError, repository_operation_conflict,
};
#[test]
fn repository_actions_protect_editors_and_serialize_mutations() {
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Pull, false, true, false),
Some(MobileRepositoryOperationError::OpenEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Pull, false, true, true),
Some(MobileRepositoryOperationError::DirtyEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Push, false, true, true),
Some(MobileRepositoryOperationError::DirtyEditor)
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Fetch, false, true, true),
None
);
assert_eq!(
repository_operation_conflict(MobileRepositoryOperation::Commit, true, false, false),
Some(MobileRepositoryOperationError::Busy)
);
}
}

View File

@@ -12,8 +12,11 @@ use std::{
use crate::{ use crate::{
config::{Config, ConfigError, GitRemote}, config::{Config, ConfigError, GitRemote},
git::{ git::{
GitChangeKind, GitCommitActivity, GitDivergence, GitError, GitOperationControl, FetchOutcome, GitChangeKind, GitCommitActivity, GitDivergence, GitError,
GitProgressPhase, GitRepository, PullOutcome, GitOperationControl, GitProgressPhase, GitRepository, PullOutcome, PushOutcome,
},
mobile_authentication::{
MobileAuthentication, MobileRepositoryOperation, MobileRepositoryOperationError,
}, },
repository::{Repository, RepositoryError}, repository::{Repository, RepositoryError},
secret_store::{ secret_store::{
@@ -46,6 +49,35 @@ pub enum MobileHomeChangeStatus {
Deleted, Deleted,
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MobileHomeActionKind {
Commit,
Fetch,
Pull,
Push,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileHomeAction {
kind: MobileHomeActionKind,
title: &'static str,
system_image: &'static str,
}
impl MobileHomeAction {
pub fn kind(&self) -> MobileHomeActionKind {
self.kind
}
pub fn title(&self) -> &str {
self.title
}
pub fn system_image(&self) -> &str {
self.system_image
}
}
impl From<GitChangeKind> for MobileHomeChangeStatus { impl From<GitChangeKind> for MobileHomeChangeStatus {
fn from(kind: GitChangeKind) -> Self { fn from(kind: GitChangeKind) -> Self {
match kind { match kind {
@@ -62,6 +94,7 @@ pub struct MobileHomeSummaryRow {
title: String, title: String,
detail: String, detail: String,
system_image: String, system_image: String,
actions: Vec<MobileHomeAction>,
} }
impl MobileHomeSummaryRow { impl MobileHomeSummaryRow {
@@ -80,6 +113,10 @@ impl MobileHomeSummaryRow {
pub fn system_image(&self) -> &str { pub fn system_image(&self) -> &str {
&self.system_image &self.system_image
} }
pub fn actions(&self) -> &[MobileHomeAction] {
&self.actions
}
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -126,6 +163,7 @@ pub struct MobileHomeCommit {
system_image: String, system_image: String,
timestamp: i64, timestamp: i64,
changes: Vec<MobileHomeChange>, changes: Vec<MobileHomeChange>,
actions: Vec<MobileHomeAction>,
} }
impl MobileHomeCommit { impl MobileHomeCommit {
@@ -152,6 +190,10 @@ impl MobileHomeCommit {
pub fn changes(&self) -> &[MobileHomeChange] { pub fn changes(&self) -> &[MobileHomeChange] {
&self.changes &self.changes
} }
pub fn actions(&self) -> &[MobileHomeAction] {
&self.actions
}
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@@ -227,6 +269,7 @@ pub enum MobileHomePhase {
Authenticating, Authenticating,
Receiving, Receiving,
Integrating, Integrating,
Sending,
Finishing, Finishing,
} }
@@ -236,7 +279,8 @@ impl From<GitProgressPhase> for MobileHomePhase {
GitProgressPhase::Validating => Self::Validating, GitProgressPhase::Validating => Self::Validating,
GitProgressPhase::Authenticating => Self::Authenticating, GitProgressPhase::Authenticating => Self::Authenticating,
GitProgressPhase::Receiving => Self::Receiving, GitProgressPhase::Receiving => Self::Receiving,
GitProgressPhase::Integrating | GitProgressPhase::Sending => Self::Integrating, GitProgressPhase::Integrating => Self::Integrating,
GitProgressPhase::Sending => Self::Sending,
GitProgressPhase::Refreshing => Self::Finishing, GitProgressPhase::Refreshing => Self::Finishing,
} }
} }
@@ -244,12 +288,17 @@ impl From<GitProgressPhase> for MobileHomePhase {
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct MobileHomeProgress { pub struct MobileHomeProgress {
action: Option<MobileHomeActionKind>,
phase: MobileHomePhase, phase: MobileHomePhase,
title: String, title: String,
detail: String, detail: String,
} }
impl MobileHomeProgress { impl MobileHomeProgress {
pub fn action(&self) -> Option<MobileHomeActionKind> {
self.action
}
pub fn phase(&self) -> MobileHomePhase { pub fn phase(&self) -> MobileHomePhase {
self.phase self.phase
} }
@@ -264,32 +313,34 @@ impl MobileHomeProgress {
} }
pub struct MobileHomeOperation { pub struct MobileHomeOperation {
authentication: Arc<MobileAuthentication>,
control: GitOperationControl, control: GitOperationControl,
phase: Arc<Mutex<MobileHomePhase>>, phase: Arc<Mutex<MobileHomePhase>>,
action: Mutex<Option<MobileHomeActionKind>>,
} }
impl Default for MobileHomeOperation { impl MobileHomeOperation {
fn default() -> Self { pub fn new(authentication: Arc<MobileAuthentication>) -> Self {
let phase = Arc::new(Mutex::new(MobileHomePhase::Validating)); let phase = Arc::new(Mutex::new(MobileHomePhase::Validating));
let observed = Arc::clone(&phase); let observed = Arc::clone(&phase);
Self { Self {
authentication,
control: GitOperationControl::new(move |phase| { control: GitOperationControl::new(move |phase| {
if let Ok(mut current) = observed.lock() { if let Ok(mut current) = observed.lock() {
*current = phase.into(); *current = phase.into();
} }
}), }),
phase, phase,
action: Mutex::new(None),
} }
} }
}
impl MobileHomeOperation {
pub fn cancel(&self) { pub fn cancel(&self) {
self.control.cancel(); self.control.cancel();
} }
pub fn progress(&self) -> MobileHomeProgress { pub fn progress(&self) -> MobileHomeProgress {
progress_copy( progress_copy(
self.action.lock().ok().and_then(|action| *action),
self.phase self.phase
.lock() .lock()
.map_or(MobileHomePhase::Validating, |phase| *phase), .map_or(MobileHomePhase::Validating, |phase| *phase),
@@ -320,15 +371,53 @@ impl MobileHomeOperation {
None, None,
); );
} }
storage.refresh(now, &self.control) self.run(MobileHomeActionKind::Fetch, || {
storage.fetch(now, &self.control)
})
} }
pub fn refresh(&self) -> Result<MobileHomePage, MobileHomeError> { pub fn commit(&self, message: &str) -> Result<MobileHomePage, MobileHomeError> {
MobileHomeStorage::load()?.refresh(unix_seconds()?, &self.control) if message.trim().is_empty() || message.contains('\0') {
return Err(MobileHomeError::invalid_commit_message());
}
self.run(MobileHomeActionKind::Commit, || {
MobileHomeStorage::load()?.commit(message, &self.control)
})
}
pub fn fetch(&self) -> Result<MobileHomePage, MobileHomeError> {
self.run(MobileHomeActionKind::Fetch, || {
MobileHomeStorage::load()?.fetch(unix_seconds()?, &self.control)
})
} }
pub fn pull(&self) -> Result<MobileHomePage, MobileHomeError> { pub fn pull(&self) -> Result<MobileHomePage, MobileHomeError> {
self.run(MobileHomeActionKind::Pull, || {
MobileHomeStorage::load()?.pull(unix_seconds()?, &self.control) MobileHomeStorage::load()?.pull(unix_seconds()?, &self.control)
})
}
pub fn push(&self) -> Result<MobileHomePage, MobileHomeError> {
self.run(MobileHomeActionKind::Push, || {
MobileHomeStorage::load()?.push(unix_seconds()?, &self.control)
})
}
fn run(
&self,
action: MobileHomeActionKind,
operation: impl FnOnce() -> Result<MobileHomePage, MobileHomeError>,
) -> Result<MobileHomePage, MobileHomeError> {
if let Ok(mut current) = self.action.lock() {
*current = Some(action);
}
let reservation = self
.authentication
.reserve_repository_operation(repository_operation(action))
.map_err(MobileHomeError::from_reservation)?;
let result = operation();
drop(reservation);
result
} }
} }
@@ -356,27 +445,50 @@ impl MobileHomeStorage {
}) })
} }
fn refresh( fn commit(
&self,
message: &str,
control: &GitOperationControl,
) -> Result<MobileHomePage, MobileHomeError> {
control
.report(GitProgressPhase::Validating)
.map_err(MobileHomeError::from_git)?;
let commit = self
.git
.commit_all(message)
.map_err(MobileHomeError::from_git)?;
control
.report(GitProgressPhase::Refreshing)
.map_err(MobileHomeError::from_git)?;
self.page(
MobileHomeFreshness::Cached,
self.config.mobile_home_refreshed_at(),
Some(commit_notice(&commit)),
)
.map_err(MobileHomeError::after_commit)
}
fn fetch(
&self, &self,
now: i64, now: i64,
control: &GitOperationControl, control: &GitOperationControl,
) -> Result<MobileHomePage, MobileHomeError> { ) -> Result<MobileHomePage, MobileHomeError> {
let store = self.credentials()?; let store = self.credentials()?;
let refreshed = self.git.fetch_with_transport_controlled( let fetched = self.git.fetch_with_transport_controlled(
&self.remote, &self.remote,
&store, &store,
&crate::git::EmbeddedFetchTransport, &crate::git::EmbeddedFetchTransport,
control, control,
); );
let locked = store.lock(); let locked = store.lock();
refreshed.map_err(MobileHomeError::from_git)?; let outcome = fetched.map_err(MobileHomeError::from_git)?;
if let Err(error) = locked { if let Err(error) = locked {
return Err(MobileHomeError::partial_secret(error)); return Err(MobileHomeError::partial_secret(error));
} }
control control
.report(GitProgressPhase::Refreshing) .report(GitProgressPhase::Refreshing)
.map_err(MobileHomeError::from_git)?; .map_err(MobileHomeError::from_git)?;
self.current_page(now, None) self.current_page(now, Some(fetch_notice(&outcome)))
} }
fn pull( fn pull(
@@ -404,6 +516,31 @@ impl MobileHomeStorage {
.map_err(MobileHomeError::after_pull) .map_err(MobileHomeError::after_pull)
} }
fn push(
&self,
now: i64,
control: &GitOperationControl,
) -> Result<MobileHomePage, MobileHomeError> {
let store = self.credentials()?;
let pushed = self.git.push_with_transport_controlled(
&self.remote,
None,
&store,
&crate::git::ReqwestGitTransport,
control,
);
let locked = store.lock();
let outcome = pushed.map_err(MobileHomeError::from_git)?;
if let Err(error) = locked {
return Err(MobileHomeError::partial_secret(error));
}
control
.report(GitProgressPhase::Refreshing)
.map_err(MobileHomeError::from_git)?;
self.current_page(now, Some(push_notice(&outcome)))
.map_err(MobileHomeError::after_push)
}
fn credentials(&self) -> Result<NativeSecretStore, MobileHomeError> { fn credentials(&self) -> Result<NativeSecretStore, MobileHomeError> {
let store = NativeSecretStore::system( let store = NativeSecretStore::system(
SecretCachePolicy::Disabled, SecretCachePolicy::Disabled,
@@ -463,6 +600,7 @@ pub enum MobileHomeErrorKind {
Authentication, Authentication,
Conflict, Conflict,
DirtyLocalChanges, DirtyLocalChanges,
NoChanges,
Offline, Offline,
Interrupted, Interrupted,
SecureStorage, SecureStorage,
@@ -557,6 +695,17 @@ impl MobileHomeError {
title: "Local Changes Need Attention", title: "Local Changes Need Attention",
detail: "Commit or discard local changes before pulling remote activity.".to_owned(), detail: "Commit or discard local changes before pulling remote activity.".to_owned(),
}, },
GitError::NoChanges => Self {
kind: MobileHomeErrorKind::NoChanges,
title: "Nothing to Commit",
detail: "The password-store working tree has no changes to commit.".to_owned(),
},
GitError::NonFastForward => Self {
kind: MobileHomeErrorKind::Conflict,
title: "Pull Before Pushing",
detail: "The remote branch contains commits that are not local. Pull and resolve any conflicts before retrying Push."
.to_owned(),
},
GitError::NetworkUnavailable => Self { GitError::NetworkUnavailable => Self {
kind: MobileHomeErrorKind::Offline, kind: MobileHomeErrorKind::Offline,
title: "Server Is Offline", title: "Server Is Offline",
@@ -589,6 +738,43 @@ impl MobileHomeError {
} }
} }
fn invalid_commit_message() -> Self {
Self {
kind: MobileHomeErrorKind::NoChanges,
title: "Commit Message Required",
detail: "Enter a non-empty commit message and try again.".to_owned(),
}
}
fn from_reservation(error: MobileRepositoryOperationError) -> Self {
match error {
MobileRepositoryOperationError::DirtyEditor => Self {
kind: MobileHomeErrorKind::DirtyLocalChanges,
title: "Unsaved Entry Edits",
detail: "Save or discard the open entry editor before pulling or pushing."
.to_owned(),
},
MobileRepositoryOperationError::OpenEditor => Self {
kind: MobileHomeErrorKind::DirtyLocalChanges,
title: "Close the Entry Editor",
detail: "Close the open entry editor before pulling so its source cannot change underneath it."
.to_owned(),
},
MobileRepositoryOperationError::Busy => Self {
kind: MobileHomeErrorKind::Interrupted,
title: "Password-Store Action in Progress",
detail: "Wait for the current password-store action to finish, then retry."
.to_owned(),
},
MobileRepositoryOperationError::Unavailable => Self {
kind: MobileHomeErrorKind::Repository,
title: "Password-Store State Is Unavailable",
detail: "The shared mobile repository state could not be locked. Restart IronStorage and retry."
.to_owned(),
},
}
}
fn from_secret(error: SecretStoreError) -> Self { fn from_secret(error: SecretStoreError) -> Self {
let detail = match error { let detail = match error {
SecretStoreError::Denied => "Access to the application token was denied.", SecretStoreError::Denied => "Access to the application token was denied.",
@@ -631,6 +817,24 @@ impl MobileHomeError {
.to_owned(), .to_owned(),
} }
} }
fn after_commit(_error: Self) -> Self {
Self {
kind: MobileHomeErrorKind::PartialProgress,
title: "Commit Completed, Status Unavailable",
detail: "The commit was created, but Home could not rebuild the current Git status. Reload Home to retry."
.to_owned(),
}
}
fn after_push(_error: Self) -> Self {
Self {
kind: MobileHomeErrorKind::PartialProgress,
title: "Push Completed, Status Unavailable",
detail: "The remote branch was updated, but Home could not rebuild the current Git status. Reload Home to retry."
.to_owned(),
}
}
} }
impl fmt::Display for MobileHomeError { impl fmt::Display for MobileHomeError {
@@ -674,6 +878,11 @@ fn page_from_divergence(
title: format!("{}/{}", divergence.remote().name(), divergence.branch()), title: format!("{}/{}", divergence.remote().name(), divergence.branch()),
detail: "Tracked HTTPS branch".to_owned(), detail: "Tracked HTTPS branch".to_owned(),
system_image: "point.3.connected.trianglepath.dotted".to_owned(), system_image: "point.3.connected.trianglepath.dotted".to_owned(),
actions: vec![
mobile_action(MobileHomeActionKind::Fetch),
mobile_action(MobileHomeActionKind::Pull),
mobile_action(MobileHomeActionKind::Push),
],
}, },
MobileHomeSummaryRow { MobileHomeSummaryRow {
id: "divergence".to_owned(), id: "divergence".to_owned(),
@@ -684,6 +893,13 @@ fn page_from_divergence(
} else { } else {
"arrow.triangle.2.circlepath".to_owned() "arrow.triangle.2.circlepath".to_owned()
}, },
actions: [
(behind > 0).then(|| mobile_action(MobileHomeActionKind::Pull)),
(ahead > 0).then(|| mobile_action(MobileHomeActionKind::Push)),
]
.into_iter()
.flatten()
.collect(),
}, },
MobileHomeSummaryRow { MobileHomeSummaryRow {
id: "working-tree".to_owned(), id: "working-tree".to_owned(),
@@ -702,6 +918,11 @@ fn page_from_divergence(
} else { } else {
"exclamationmark.triangle".to_owned() "exclamationmark.triangle".to_owned()
}, },
actions: if local_count > 0 {
vec![mobile_action(MobileHomeActionKind::Commit)]
} else {
Vec::new()
},
}, },
]; ];
MobileHomePage { MobileHomePage {
@@ -765,6 +986,11 @@ fn mobile_commit(activity: &GitCommitActivity, incoming: bool) -> MobileHomeComm
}, },
timestamp: commit.timestamp(), timestamp: commit.timestamp(),
changes, changes,
actions: vec![mobile_action(if incoming {
MobileHomeActionKind::Pull
} else {
MobileHomeActionKind::Push
})],
} }
} }
@@ -881,23 +1107,113 @@ fn pull_notice(outcome: PullOutcome) -> MobileHomeNotice {
} }
} }
fn progress_copy(phase: MobileHomePhase) -> MobileHomeProgress { fn commit_notice(commit: &str) -> MobileHomeNotice {
let (title, detail) = match phase { MobileHomeNotice {
MobileHomePhase::Validating => ("Checking Home", "Validating local and remote Git state."), title: "Changes Committed".to_owned(),
MobileHomePhase::Authenticating => ( detail: format!("Created commit {}.", &commit[..commit.len().min(12)]),
system_image: "checkmark.circle".to_owned(),
}
}
fn fetch_notice(outcome: &FetchOutcome) -> MobileHomeNotice {
MobileHomeNotice {
title: if outcome.received_pack() {
"Remote Status Updated"
} else {
"Remote Already Current"
}
.to_owned(),
detail: if outcome.received_pack() {
format!(
"Received new objects from {} without changing the local password store.",
outcome.remote()
)
} else {
format!("{} has no new objects for this clone.", outcome.remote())
},
system_image: "arrow.clockwise.circle".to_owned(),
}
}
fn push_notice(outcome: &PushOutcome) -> MobileHomeNotice {
let unchanged = outcome.old() == Some(outcome.new_id());
MobileHomeNotice {
title: if unchanged {
"Remote Already Current"
} else {
"Changes Pushed"
}
.to_owned(),
detail: if unchanged {
format!(
"{}/{} already points to the local commit.",
outcome.remote(),
outcome.branch()
)
} else {
format!(
"Updated {}/{} with local commits.",
outcome.remote(),
outcome.branch()
)
},
system_image: "arrow.up.circle".to_owned(),
}
}
fn mobile_action(kind: MobileHomeActionKind) -> MobileHomeAction {
let (title, system_image) = match kind {
MobileHomeActionKind::Commit => ("Commit", "checkmark.circle"),
MobileHomeActionKind::Fetch => ("Fetch", "arrow.clockwise"),
MobileHomeActionKind::Pull => ("Pull", "arrow.down.circle"),
MobileHomeActionKind::Push => ("Push", "arrow.up.circle"),
};
MobileHomeAction {
kind,
title,
system_image,
}
}
fn repository_operation(action: MobileHomeActionKind) -> MobileRepositoryOperation {
match action {
MobileHomeActionKind::Commit => MobileRepositoryOperation::Commit,
MobileHomeActionKind::Fetch => MobileRepositoryOperation::Fetch,
MobileHomeActionKind::Pull => MobileRepositoryOperation::Pull,
MobileHomeActionKind::Push => MobileRepositoryOperation::Push,
}
}
fn progress_copy(
action: Option<MobileHomeActionKind>,
phase: MobileHomePhase,
) -> MobileHomeProgress {
let (title, detail) = match (action, phase) {
(Some(MobileHomeActionKind::Commit), MobileHomePhase::Validating) => (
"Committing Changes",
"Staging current password-store changes and creating the commit in Rust.",
),
(_, MobileHomePhase::Validating) => {
("Checking Home", "Validating local and remote Git state.")
}
(_, MobileHomePhase::Authenticating) => (
"Authenticating", "Authenticating",
"Reading the application token from protected storage.", "Reading the application token from protected storage.",
), ),
MobileHomePhase::Receiving => ("Refreshing Remote", "Receiving remote Git objects."), (_, MobileHomePhase::Receiving) => ("Refreshing Remote", "Receiving remote Git objects."),
MobileHomePhase::Integrating => ( (_, MobileHomePhase::Integrating) => (
"Updating Password Store", "Updating Password Store",
"Integrating fetched commits into the local clone.", "Integrating fetched commits into the local clone.",
), ),
MobileHomePhase::Finishing => { (_, MobileHomePhase::Sending) => {
("Pushing Changes", "Sending local Git objects over HTTPS.")
}
(_, MobileHomePhase::Finishing) => {
("Updating Home", "Building current activity and divergence.") ("Updating Home", "Building current activity and divergence.")
} }
}; };
MobileHomeProgress { MobileHomeProgress {
action,
phase, phase,
title: title.to_owned(), title: title.to_owned(),
detail: detail.to_owned(), detail: detail.to_owned(),
@@ -935,8 +1251,8 @@ mod tests {
use crate::git::{GitChangeKind, GitError}; use crate::git::{GitChangeKind, GitError};
use super::{ use super::{
MobileHomeChangeKind, MobileHomeChangeStatus, MobileHomeErrorKind, display_text, is_stale, MobileHomeActionKind, MobileHomeChangeKind, MobileHomeChangeStatus, MobileHomeErrorKind,
mobile_change, MobileHomePhase, display_text, is_stale, mobile_action, mobile_change, progress_copy,
}; };
#[test] #[test]
@@ -974,6 +1290,20 @@ mod tests {
assert_eq!(offline.kind(), MobileHomeErrorKind::Offline); assert_eq!(offline.kind(), MobileHomeErrorKind::Offline);
let dirty = super::MobileHomeError::from_git(GitError::DirtyWorktree); let dirty = super::MobileHomeError::from_git(GitError::DirtyWorktree);
assert_eq!(dirty.kind(), MobileHomeErrorKind::DirtyLocalChanges); assert_eq!(dirty.kind(), MobileHomeErrorKind::DirtyLocalChanges);
let push_conflict = super::MobileHomeError::from_git(GitError::NonFastForward);
assert_eq!(push_conflict.kind(), MobileHomeErrorKind::Conflict);
let no_changes = super::MobileHomeError::from_git(GitError::NoChanges);
assert_eq!(no_changes.kind(), MobileHomeErrorKind::NoChanges);
assert!(!offline.to_string().contains("token-value")); assert!(!offline.to_string().contains("token-value"));
} }
#[test]
fn actions_and_progress_are_typed_before_swift_presentation() {
let push = mobile_action(MobileHomeActionKind::Push);
assert_eq!(push.title(), "Push");
assert_eq!(push.system_image(), "arrow.up.circle");
let progress = progress_copy(Some(MobileHomeActionKind::Push), MobileHomePhase::Sending);
assert_eq!(progress.action(), Some(MobileHomeActionKind::Push));
assert_eq!(progress.title(), "Pushing Changes");
}
} }

View File

@@ -98,6 +98,23 @@ fn local_git_workflow_stages_commits_diffs_logs_and_deletes() -> TestResult {
Ok(()) Ok(())
} }
#[test]
fn user_commit_stages_every_current_change_in_storage() -> TestResult {
let temporary = tempfile::tempdir()?;
let store = Repository::open(temporary.path())?;
let git = GitRepository::init(&store, identity())?;
fs::write(temporary.path().join(".gpg-id"), b"ALICE\n")?;
fs::write(temporary.path().join("mail.gpg"), b"ciphertext")?;
let commit = git.commit_all("Save mobile changes.")?;
assert_eq!(commit.len(), 40);
assert!(git.status()?.is_clean());
assert_eq!(git.log(Some(1))?[0].message(), "Save mobile changes.");
assert_eq!(git.commit_all("Nothing changed"), Err(GitError::NoChanges));
Ok(())
}
#[test] #[test]
fn nested_repository_selection_is_innermost() -> TestResult { fn nested_repository_selection_is_innermost() -> TestResult {
let temporary = tempfile::tempdir()?; let temporary = tempfile::tempdir()?;