├── .gitignore ├── Git.sketch ├── Git.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings │ └── xcuserdata │ │ ├── vaux.xcuserdatad │ │ └── WorkspaceSettings.xcsettings │ │ └── vin.xcuserdatad │ │ └── WorkspaceSettings.xcsettings ├── xcshareddata │ └── xcschemes │ │ ├── Mac.xcscheme │ │ └── iOS.xcscheme └── xcuserdata │ └── vin.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Git ├── Check.swift ├── Commit.swift ├── Config.swift ├── Content.swift ├── Differ.swift ├── Dispatch.swift ├── Factory.swift ├── Failure.swift ├── Fetch.swift ├── Hash.swift ├── Head.swift ├── History.swift ├── Hub.swift ├── Ignore.swift ├── Index.swift ├── Merger.swift ├── Pack.swift ├── Packer.swift ├── Parse.swift ├── Press.swift ├── Repository.swift ├── Rest.swift ├── Serial.swift ├── Session.swift ├── Stage.swift ├── Status.swift ├── Tree.swift └── User.swift ├── LICENSE ├── Mac ├── About.swift ├── Add.swift ├── Alert.swift ├── App.swift ├── Button.swift ├── Cloud.swift ├── Display.swift ├── Extensions.swift ├── File.swift ├── Help.swift ├── History.swift ├── Home.swift ├── Mac.entitlements ├── Mac.plist ├── Mac.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── brand1024w.png │ │ ├── brand128w.png │ │ ├── brand16w.png │ │ ├── brand256w-1.png │ │ ├── brand256w.png │ │ ├── brand32w-1.png │ │ ├── brand32w.png │ │ ├── brand512w-1.png │ │ ├── brand512w.png │ │ └── brand64w.png │ └── Contents.json ├── Market.swift ├── Reset.swift ├── Settings.swift └── Timeline.swift ├── README.md ├── Shared ├── SFMono-Bold.otf ├── SFMono-Light.otf ├── Shared.swift ├── Shared.xcassets │ ├── Contents.json │ ├── add.imageset │ │ ├── Contents.json │ │ └── add.pdf │ ├── checkOff.imageset │ │ ├── Contents.json │ │ └── checkOff.pdf │ ├── checkOn.imageset │ │ ├── Contents.json │ │ └── checkOn.pdf │ ├── close.imageset │ │ ├── Contents.json │ │ └── close.pdf │ ├── cloud.imageset │ │ ├── Contents.json │ │ └── cloud.pdf │ ├── dot.imageset │ │ ├── Contents.json │ │ └── dot.pdf │ ├── error.imageset │ │ ├── Contents.json │ │ └── error.pdf │ ├── history.imageset │ │ ├── Contents.json │ │ └── history.pdf │ ├── home.imageset │ │ ├── Contents.json │ │ └── home.pdf │ ├── loading.imageset │ │ ├── Contents.json │ │ └── loading.pdf │ ├── logo.imageset │ │ ├── Contents.json │ │ └── logo.pdf │ ├── logotouch.imageset │ │ ├── Contents.json │ │ └── logotouch.pdf │ ├── market.imageset │ │ ├── Contents.json │ │ └── market.pdf │ ├── reset.imageset │ │ ├── Contents.json │ │ └── reset.pdf │ ├── settings.imageset │ │ ├── Contents.json │ │ └── settings.pdf │ ├── timeline.imageset │ │ ├── Contents.json │ │ └── timeline.pdf │ └── updated.imageset │ │ ├── Contents.json │ │ └── updated.pdf └── en.lproj │ └── Localizable.strings ├── Tests ├── MockRest.swift ├── TestAdd.swift ├── TestCheckout.swift ├── TestClone.swift ├── TestCommit.swift ├── TestConfig.swift ├── TestContents.swift ├── TestDiff.swift ├── TestFetch.swift ├── TestHash.swift ├── TestHead.swift ├── TestHub.swift ├── TestIgnore.swift ├── TestIndex.swift ├── TestList.swift ├── TestLog.swift ├── TestMerge.swift ├── TestPack.swift ├── TestPress.swift ├── TestPull.swift ├── TestPush.swift ├── TestRefresh.swift ├── TestRepack.swift ├── TestRepository.swift ├── TestReset.swift ├── TestRest.swift ├── TestRestoreIndex.swift ├── TestSession.swift ├── TestStaging.swift ├── TestStatus.swift ├── TestTree.swift ├── TestUnpack.swift ├── TestUser.swift ├── Tests.plist ├── blob0 ├── commit0 ├── commit1 ├── commit2 ├── commit3 ├── compressed0 ├── config0 ├── fetch0 ├── fetch1 ├── fetch2 ├── fetch3 ├── fetchPull0 ├── fetchPush0 ├── index0 ├── index1 ├── index2 ├── index3 ├── index4 ├── pack-0.idx ├── pack-0.pack ├── pack-1.idx ├── pack-1.pack ├── pack-2.idx ├── pack-2.pack ├── packed-refs0 ├── tree0 ├── tree1 ├── tree2 ├── tree3 └── tree4 └── iOS ├── Add.swift ├── Alert.swift ├── App.swift ├── Button.swift ├── Cloud.swift ├── Create.swift ├── Credentials.swift ├── Delete.swift ├── Display.swift ├── Extensions.swift ├── File.swift ├── Help.swift ├── History.swift ├── Home.swift ├── Market.swift ├── Pop.swift ├── Reset.swift ├── Settings.swift ├── Sheet.swift ├── Tab.swift ├── Timeline.swift ├── iOS.plist ├── iOS.storyboard └── iOS.xcassets ├── AppIcon.appiconset ├── Contents.json ├── brand.png ├── brand120w-1.png ├── brand120w.png ├── brand152w.png ├── brand167w.png ├── brand180w.png ├── brand20w.png ├── brand29w.png ├── brand40w-1.png ├── brand40w-2.png ├── brand40w.png ├── brand58w-1.png ├── brand58w.png ├── brand60w.png ├── brand76w.png ├── brand80w-1.png ├── brand80w.png └── brand87w.png └── Contents.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .pbxuser 3 | .mode1 4 | .mode1v3 5 | .mode2v3 6 | .perspective 7 | .perspectivev3 8 | .xccheckout 9 | .moved-aside 10 | .hmap 11 | .o 12 | .dat 13 | .dep 14 | .LinkFileList 15 | .ipa 16 | .dSYM.zip 17 | .dSYM 18 | .swp 19 | *.xcuserstate 20 | xcdebugger 21 | ~.nib/ 22 | *~ 23 | timeline.xctimeline 24 | playground.xcworkspace 25 | Pods/ 26 | Podfile.lock 27 | -------------------------------------------------------------------------------- /Git.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Git.sketch -------------------------------------------------------------------------------- /Git.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Git.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Git.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Git.xcodeproj/project.xcworkspace/xcuserdata/vaux.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | EnabledFullIndexStoreVisibility 12 | 13 | IssueFilterStyle 14 | ShowAll 15 | LiveSourceIssuesEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Git.xcodeproj/project.xcworkspace/xcuserdata/vin.xcuserdatad/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BuildLocationStyle 6 | UseAppPreferences 7 | CustomBuildLocationType 8 | RelativeToDerivedData 9 | DerivedDataLocationStyle 10 | Default 11 | EnabledFullIndexStoreVisibility 12 | 13 | IssueFilterStyle 14 | ShowAll 15 | LiveSourceIssuesEnabled 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Git.xcodeproj/xcshareddata/xcschemes/Mac.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Git.xcodeproj/xcshareddata/xcschemes/iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Git.xcodeproj/xcuserdata/vin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Mac.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 2 11 | 12 | Tests.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 2 16 | 17 | iOS.xcscheme_^#shared#^_ 18 | 19 | orderHint 20 | 3 21 | 22 | 23 | SuppressBuildableAutocreation 24 | 25 | 20F6E092225C72A100418216 26 | 27 | primary 28 | 29 | 30 | 20F6E0A0225C72AC00418216 31 | 32 | primary 33 | 34 | 35 | 20F6E0AB225C72BD00418216 36 | 37 | primary 38 | 39 | 40 | 20F6E0BA225C732200418216 41 | 42 | primary 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Git/Check.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Check { 4 | weak var repository: Repository? 5 | 6 | func reset() throws { 7 | guard let url = repository?.url, let tree = try? Hub.head.tree(url) else { return } 8 | try check(tree) 9 | } 10 | 11 | func check(_ id: String) throws { 12 | guard let url = repository?.url else { return } 13 | try check(Tree(Commit(id, url: url).tree, url: url)) 14 | try Hub.head.update(url, id: id) 15 | } 16 | 17 | func merge(_ id: String) throws { 18 | guard let url = repository?.url else { return } 19 | var tree = try Hub.head.tree(url).items 20 | try Tree(Commit(id, url: url).tree, url: url).items.forEach { item in 21 | if !tree.contains(where: { $0.id == item.id }) { 22 | tree.append(item) 23 | } 24 | } 25 | let index = Index() 26 | try extract(tree, index: index) 27 | index.save(url) 28 | } 29 | 30 | private func check(_ tree: Tree) throws { 31 | guard let url = repository?.url else { return } 32 | try remove(tree) 33 | let index = Index() 34 | try extract(tree.items, index: index) 35 | index.save(url) 36 | } 37 | 38 | private func remove(_ tree: Tree) throws { 39 | guard let url = repository?.url, let list = repository?.state.list(tree) else { return } 40 | try list.filter({ $0.1 != .deleted }).forEach { 41 | let path = $0.0.deletingLastPathComponent().path.dropFirst(url.path.count) 42 | if !path.isEmpty { 43 | let dir = url.appendingPathComponent(String(path)) 44 | if FileManager.default.fileExists(atPath: dir.path) { 45 | try FileManager.default.removeItem(at: dir) 46 | } 47 | } else { 48 | if FileManager.default.fileExists(atPath: $0.0.path) { 49 | try FileManager.default.removeItem(at: $0.0) 50 | } 51 | } 52 | } 53 | } 54 | 55 | private func extract(_ tree: [Tree.Item], index: Index) throws { 56 | guard let url = repository?.url else { return } 57 | try tree.forEach { 58 | switch $0.category { 59 | case .tree: 60 | if !FileManager.default.fileExists(atPath: $0.url.path) { 61 | try FileManager.default.createDirectory(at: $0.url, withIntermediateDirectories: true) 62 | } 63 | try extract(Tree($0.id, url: url, trail: $0.url).items, index: index) 64 | default: 65 | try Hub.content.file($0.id, url: url).write(to: $0.url, options: .atomic) 66 | index.entry($0.id, url: $0.url) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Git/Commit.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class Commit { 4 | public internal(set) var author = User() 5 | public internal(set) var message = "" 6 | var committer = User() 7 | var parent = [String]() 8 | var tree = "" 9 | var gpg = "" 10 | 11 | convenience init(_ id: String, url: URL) throws { try self.init(Hub.content.get(id, url: url)) } 12 | 13 | init(_ data: Data) throws { 14 | let string = String(decoding: data, as: UTF8.self) 15 | let split = string.components(separatedBy: "\n\n") 16 | let signed = split.first!.components(separatedBy: "\ngpgsig") 17 | var lines = signed.first!.components(separatedBy: "\n") 18 | guard 19 | split.count > 1, 20 | lines.count >= 3, 21 | let tree = lines.removeFirst().components(separatedBy: "tree ").last, 22 | tree.count == 40, 23 | let committer = try? User(lines.removeLast()), 24 | let author = try? User(lines.removeLast()) 25 | else { throw Failure.Commit.unreadable } 26 | while !lines.isEmpty { 27 | guard let parent = lines.removeFirst().components(separatedBy: "parent ").last, parent.count == 40 28 | else { throw Failure.Commit.unreadable } 29 | self.parent.append(parent) 30 | } 31 | if signed.count == 2 { 32 | gpg = "\ngpgsig" + signed[1] 33 | } 34 | self.tree = tree 35 | self.author = author 36 | self.committer = committer 37 | self.message = split.dropFirst().joined(separator: "\n\n") 38 | } 39 | 40 | init() { } 41 | 42 | var serial: String { 43 | var result = "tree \(tree)\n" 44 | parent.forEach { 45 | result += "parent \($0)\n" 46 | } 47 | result += "author \(author.serial)\ncommitter \(committer.serial)\(gpg)\n\n\(message)" 48 | return result 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Git/Config.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Config { 4 | struct Remote { 5 | fileprivate(set) var url = "" 6 | fileprivate(set) var fetch = "" 7 | } 8 | 9 | struct Branch { 10 | fileprivate(set) var remote = "" 11 | fileprivate(set) var merge = "" 12 | } 13 | 14 | private(set) var remote = [String: Remote]() 15 | private(set) var branch = [String: Branch]() 16 | 17 | init(_ url: String) { 18 | var remote = Remote() 19 | remote.url = "https://" + url 20 | remote.fetch = "+refs/heads/*:refs/remotes/origin/*" 21 | var branch = Branch() 22 | branch.remote = "origin" 23 | branch.merge = "refs/heads/master" 24 | self.remote["origin"] = remote 25 | self.branch["master"] = branch 26 | } 27 | 28 | init(_ url: URL) throws { 29 | let lines = String(decoding: try Data(contentsOf: url.appendingPathComponent(".git/config")), as: UTF8.self).components(separatedBy: "\n") 30 | var index = 0 31 | while index < lines.count { 32 | if lines[index].prefix(7) == "[remote" { 33 | var remote = Remote() 34 | remote.url = lines[index + 1].components(separatedBy: "= ")[1] 35 | remote.fetch = lines[index + 2].components(separatedBy: "= ")[1] 36 | self.remote[lines[index].components(separatedBy: "\"")[1]] = remote 37 | index += 3 38 | } else if lines[index].prefix(7) == "[branch" { 39 | var branch = Branch() 40 | branch.remote = lines[index + 1].components(separatedBy: "= ")[1] 41 | branch.merge = lines[index + 2].components(separatedBy: "= ")[1] 42 | self.branch[lines[index].components(separatedBy: "\"")[1]] = branch 43 | index += 3 44 | } else { 45 | repeat { 46 | index += 1 47 | } while index < lines.count && !lines[index].isEmpty && lines[index].first != "[" 48 | } 49 | } 50 | } 51 | 52 | func save(_ url: URL) throws { try Data(serial.utf8).write(to: url.appendingPathComponent(".git/config"), options: .atomic) } 53 | 54 | var serial: String { 55 | var result = "" 56 | remote.forEach { 57 | result += """ 58 | [remote \"\($0.0)\"] 59 | url = \($0.1.url) 60 | fetch = \($0.1.fetch) 61 | 62 | """ 63 | } 64 | branch.forEach { 65 | result += """ 66 | [branch \"\($0.0)\"] 67 | remote = \($0.1.remote) 68 | merge = \($0.1.merge) 69 | 70 | """ 71 | } 72 | return result 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Git/Content.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Content { 4 | @discardableResult func add(_ commit: Commit, url: URL) throws -> String { 5 | return try { 6 | try add($0.1, data: $0.0, url: url) 7 | return $0.1 8 | } (Hub.hash.commit(commit.serial)) 9 | } 10 | 11 | @discardableResult func add(_ tree: Tree, url: URL) throws -> String { 12 | return try { 13 | try add($0.1, data: $0.0, url: url) 14 | return $0.1 15 | } (Hub.hash.tree(tree.serial)) 16 | } 17 | 18 | @discardableResult func add(_ file: URL, url: URL) throws -> String { 19 | return try { 20 | try add($0.1, data: $0.0, url: url) 21 | return $0.1 22 | } (Hub.hash.file(file)) 23 | } 24 | 25 | @discardableResult func add(_ blob: Data, url: URL) throws -> String { 26 | return try { 27 | try add($0.1, data: $0.0, url: url) 28 | return $0.1 29 | } (Hub.hash.blob(blob)) 30 | } 31 | 32 | func file(_ id: String, url: URL) throws -> Data { 33 | let parse = Parse(try Hub.content.get(id, url: url)) 34 | _ = try parse.variable() 35 | return parse.data.subdata(in: parse.index ..< parse.data.count) 36 | } 37 | 38 | func get(_ id: String, url: URL) throws -> Data { 39 | return Hub.press.decompress(try Data(contentsOf: url.appendingPathComponent(".git/objects/\(id.prefix(2))/\(id.dropFirst(2))"))) 40 | } 41 | 42 | func objects(_ url: URL) -> [String] { 43 | return FileManager.default.enumerator(at: url.appendingPathComponent(".git/objects/"), includingPropertiesForKeys: nil)! 44 | .filter({ !($0 as! URL).hasDirectoryPath }).map({ ($0 as! URL) 45 | .resolvingSymlinksInPath().path.dropFirst(url.path.count + 13).replacingOccurrences(of: "/", with: "") }) 46 | } 47 | 48 | private func add(_ id: String, data: Data, url: URL) throws { 49 | let folder = url.appendingPathComponent(".git/objects/\(id.prefix(2))") 50 | let location = folder.appendingPathComponent(String(id.dropFirst(2))) 51 | if !FileManager.default.fileExists(atPath: location.path) { 52 | if !FileManager.default.fileExists(atPath: folder.path) { 53 | try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) 54 | } 55 | try Hub.press.compress(data).write(to: location, options: .atomic) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Git/Differ.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Differ { 4 | weak var repository: Repository? 5 | 6 | func previous(_ url: URL) throws -> (Date, Data)? { 7 | guard 8 | let repository = self.repository, 9 | let id = try Hub.head.tree(repository.url).list(repository.url).first(where: { $0.url.path == url.path })?.id 10 | else { return nil } 11 | if let current = try? Hub.hash.blob(Data(contentsOf: url)).1 { 12 | if current == id { throw Failure.Diff.unchanged } 13 | } 14 | return try ((Hub.head.commit(repository.url)).author.date, Hub.content.file(id, url: repository.url)) 15 | } 16 | 17 | func timeline(_ url: URL) throws -> [(Date, Data)] { 18 | guard let repository = self.repository else { return [] } 19 | return try History(repository.url).result.reduce(into: { [Hub.hash.blob($0).1: (Date(), $0)] } ((try? Data(contentsOf: url)) ?? Data()), { 20 | guard 21 | let id = try Tree($1.tree, url: repository.url).list(repository.url).first(where: { $0.url.path == url.path })?.id, 22 | $0[id] == nil 23 | else { return } 24 | $0[id] = try ($1.author.date, Hub.content.file(id, url: repository.url)) 25 | }).values.sorted(by: { $0.0 < $1.0 }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Git/Dispatch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Dispatch { 4 | private let queue = DispatchQueue(label: "", qos: .background, target: .global(qos: .background)) 5 | 6 | func background(_ send: @escaping(() -> Void)) { 7 | queue.async { 8 | send() 9 | } 10 | } 11 | 12 | func background(_ send: @escaping(() throws -> Void), error: @escaping((Error) -> Void)) { 13 | queue.async { 14 | do { 15 | try send() 16 | } catch let exception { 17 | DispatchQueue.main.async { 18 | error(exception) 19 | } 20 | } 21 | } 22 | } 23 | 24 | func background(_ send: @escaping(() -> R), success: @escaping((R) -> Void)) { 25 | queue.async { 26 | let result = send() 27 | DispatchQueue.main.async { 28 | success(result) 29 | } 30 | } 31 | } 32 | 33 | func background(_ send: @escaping(() throws -> R), error: @escaping((Error) -> Void), 34 | success: @escaping((R) -> Void)) { 35 | queue.async { 36 | do { 37 | let result = try send() 38 | DispatchQueue.main.async { 39 | success(result) 40 | } 41 | } catch let exception { 42 | DispatchQueue.main.async { 43 | error(exception) 44 | } 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Git/Failure.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Failure: LocalizedError { 4 | public struct Repository { 5 | public static let duplicating = Failure("This is already a repository.") 6 | public static let invalid = Failure("This is not a repository.") 7 | } 8 | 9 | public struct Parsing { 10 | public static let malformed = Failure("Unable to read file.") 11 | } 12 | 13 | public struct Press { 14 | public static let unreadable = Failure("Unable to read compressed file.") 15 | } 16 | 17 | public struct Tree { 18 | public static let unreadable = Failure("Unable to read tree.") 19 | } 20 | 21 | public struct Commit { 22 | public static let unreadable = Failure("Unable to read commit.") 23 | public static let empty = Failure("Nothing to commit.") 24 | public static let credentials = Failure("Username and Email need to be configured.") 25 | public static let message = Failure("Commit message can't be empty.") 26 | public static let ignored = Failure("Attemping to commit ignored file.") 27 | public static let none = Failure("Invalid url.") 28 | } 29 | 30 | public struct Add { 31 | public static let not = Failure("File does not exists.") 32 | public static let outside = Failure("File is not in project's directory.") 33 | } 34 | 35 | public struct User { 36 | public static let name = Failure("Invalid name.") 37 | public static let email = Failure("Invalid email.") 38 | } 39 | 40 | public struct Pack { 41 | public static let packNotFound = Failure("Pack file not found.") 42 | public static let invalidIndex = Failure("Index file for pack malformed.") 43 | public static let invalidPack = Failure("Pack file malformed.") 44 | public static let invalidDelta = Failure("Pack delta malformed.") 45 | public static let object = Failure("Unreadable pack object.") 46 | public static let size = Failure("Size not match.") 47 | public static let read = Failure("Can't read packed data.") 48 | public static let adler = Failure("Decompression checksum failed.") 49 | } 50 | 51 | public struct Fetch { 52 | public static let advertisement = Failure("Invalid advertisement.") 53 | public static let empty = Failure("No references in advertisement.") 54 | } 55 | 56 | public struct Request { 57 | public static let invalid = Failure("Invalid URL.") 58 | public static let empty = Failure("Empty response from server.") 59 | public static let auth = Failure("Couldn't authenticate to server, please review the url provided or your credentials.") 60 | public static let response = Failure("Invalid response from server.") 61 | public static let none = Failure("Couldn't find this resource or doesn't exists.") 62 | } 63 | 64 | public struct Remote { 65 | public static let none = Failure("No remote specified for this repository.") 66 | public static let changes = Failure("There are changes in the current repository. Commit or Revert them and try again.") 67 | public static let push = Failure("Failed sending changes to remote.") 68 | public static let empty = Failure("This repository is empty.") 69 | public static let already = Failure("There is already a repository in this directory.") 70 | } 71 | 72 | public struct Config { 73 | public static let none = Failure("Configuration file not found.") 74 | } 75 | 76 | public struct Merge { 77 | public static let common = Failure("These repositories are not compatible.") 78 | public static let unknown = Failure("Remote repository is different than local, try Pull before Push.") 79 | } 80 | 81 | public struct Diff { 82 | public static let unchanged = Failure("Couldn't find the requested file.") 83 | } 84 | 85 | public var errorDescription: String? { return string } 86 | private let string: String 87 | private init(_ string: String) { self.string = string } 88 | } 89 | -------------------------------------------------------------------------------- /Git/Fetch.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Fetch { 4 | final class Pull: Fetch { 5 | init(_ data: Data) throws { 6 | super.init() 7 | var lines = String(decoding: data, as: UTF8.self).components(separatedBy: "\n") 8 | guard lines.count > 3, lines.removeFirst() == "001e# service=git-upload-pack", lines.removeLast() == "0000" 9 | else { throw Failure.Fetch.advertisement } 10 | lines.removeFirst() 11 | try lines.forEach { 12 | guard $0.count > 44 else { throw Failure.Fetch.advertisement } 13 | branch.append(String($0.dropFirst(4).prefix(40))) 14 | } 15 | } 16 | } 17 | 18 | final class Push: Fetch { 19 | init(_ data: Data) throws { 20 | super.init() 21 | var lines = String(decoding: data, as: UTF8.self).components(separatedBy: "\n") 22 | guard lines.count > 2 , lines.removeFirst() == "001f# service=git-receive-pack", lines.removeLast() == "0000" 23 | else { throw Failure.Fetch.advertisement } 24 | try lines.forEach { 25 | guard $0.count > 48 else { throw Failure.Fetch.advertisement } 26 | branch.append(String($0.dropFirst(8).prefix(40))) 27 | } 28 | } 29 | } 30 | 31 | final var branch = [String]() 32 | init() { } 33 | } 34 | -------------------------------------------------------------------------------- /Git/Hash.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CommonCrypto 3 | 4 | final class Hash { 5 | private var digest = [UInt8](repeating: 0, count: Int(CC_SHA1_DIGEST_LENGTH)) 6 | 7 | func file(_ url: URL) -> (Data, String) { 8 | return blob(try! Data(contentsOf: url)) 9 | } 10 | 11 | func blob(_ data: Data) -> (Data, String) { 12 | let packed = "blob \(data.count)\u{0000}".utf8 + data 13 | return (packed, hash(packed)) 14 | } 15 | 16 | func tree(_ data: Data) -> (Data, String) { 17 | let packed = "tree \(data.count)\u{0000}".utf8 + data 18 | return (packed, hash(packed)) 19 | } 20 | 21 | func commit(_ serial: String) -> (Data, String) { 22 | return { 23 | let packed = "commit \($0.count)\u{0000}".utf8 + $0 24 | return (packed, hash(packed)) 25 | } (Data(serial.utf8)) 26 | } 27 | 28 | func digest(_ data: Data) -> Data { 29 | _ = data.withUnsafeBytes { CC_SHA1($0.baseAddress, CC_LONG(data.count), &digest) } 30 | return Data(digest) 31 | } 32 | 33 | private func hash(_ data: Data) -> String { 34 | return digest(data).map { String(format: "%02hhx", $0) }.joined() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Git/Head.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Head { 4 | func branch(_ url: URL) -> String? { 5 | return try? self.reference(url).replacingOccurrences(of: "refs/heads/", with: "") 6 | } 7 | 8 | func tree(_ url: URL) throws -> Tree { 9 | return try Tree((commit(url)).tree, url: url) 10 | } 11 | 12 | func commit(_ url: URL) throws -> Commit { 13 | return try Commit(id(url), url: url) 14 | } 15 | 16 | func id(_ url: URL) throws -> String { 17 | return String(decoding: try Data(contentsOf: self.url(url)), as: UTF8.self).replacingOccurrences(of: "\n", with: "") 18 | } 19 | 20 | func update(_ url: URL, id: String) throws { 21 | try verify(url) 22 | try Data(id.utf8).write(to: self.url(url), options: .atomic) 23 | } 24 | 25 | func verify(_ url: URL) throws { 26 | if !FileManager.default.fileExists(atPath: try self.url(url).deletingLastPathComponent().path) { 27 | try FileManager.default.createDirectory(at: self.url(url).deletingLastPathComponent(), withIntermediateDirectories: true) 28 | } 29 | } 30 | 31 | func url(_ url: URL) throws -> URL { 32 | return try url.appendingPathComponent(".git/" + (reference(url))) 33 | } 34 | 35 | func reference(_ url: URL) throws -> String { 36 | return try String(String(decoding: Data(contentsOf: url.appendingPathComponent(".git/HEAD")), as: 37 | UTF8.self).dropFirst(5)).replacingOccurrences(of: "\n", with: "") 38 | } 39 | 40 | func origin(_ url: URL, id: String) throws { 41 | let remotes = url.appendingPathComponent(".git/refs/remotes/origin/") 42 | if !FileManager.default.fileExists(atPath: remotes.path) { 43 | try FileManager.default.createDirectory(at: remotes, withIntermediateDirectories: true) 44 | } 45 | try Data(id.utf8).write(to: remotes.appendingPathComponent("master"), options: .atomic) 46 | } 47 | 48 | func origin(_ url: URL) -> String? { 49 | if let data = try? Data(contentsOf: url.appendingPathComponent(".git/refs/remotes/origin/master")) { 50 | return String(decoding: data, as: UTF8.self) 51 | } 52 | return nil 53 | } 54 | 55 | func remote(_ url: URL) -> String { 56 | if let raw = (try? Config(url))?.remote.first?.1.url, raw.hasPrefix("https://") { 57 | return String(raw.dropFirst(8)) 58 | } 59 | return "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Git/History.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class History { 4 | private(set) var result = [Commit]() 5 | private(set) var map = [String: Commit]() 6 | private let url: URL 7 | 8 | convenience init(_ url: URL) throws { 9 | try self.init(Hub.head.id(url), url: url) 10 | } 11 | 12 | init(_ id: String, url: URL) throws { 13 | self.url = url 14 | try commits(id) 15 | result = map.values.sorted { 16 | if $0.author.date > $1.author.date { 17 | return true 18 | } else if $0.author.date == $1.author.date { 19 | return $0.parent.count > $1.parent.count 20 | } 21 | return false 22 | } 23 | } 24 | 25 | private func commits(_ id: String) throws { 26 | guard map[id] == nil else { return } 27 | let item = try Commit(id, url: url) 28 | map[id] = item 29 | try item.parent.forEach { 30 | try commits($0) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Git/Hub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class Hub { 4 | public internal(set) static var session = Session() 5 | static let dispatch = Dispatch() 6 | static let hash = Hash() 7 | static let press = Press() 8 | static let content = Content() 9 | static let head = Head() 10 | static let factory = Factory() 11 | 12 | public class func repository(_ url: URL, result: @escaping((Bool) -> Void)) { 13 | dispatch.background({ factory.repository(url) }, success: result) 14 | } 15 | 16 | public class func create(_ url: URL, error: @escaping((Error) -> Void) = { _ in }, result: @escaping((Repository) -> Void) = { _ in }) { 17 | dispatch.background({ try factory.create(url) }, error: error, success: result) 18 | } 19 | 20 | public class func open(_ url: URL, error: @escaping((Error) -> Void) = { _ in }, result: @escaping((Repository) -> Void)) { 21 | dispatch.background({ try factory.open(url) }, error: error, success: result) 22 | } 23 | 24 | public class func delete(_ repository: Repository, error: @escaping((Error) -> Void) = { _ in }, done: @escaping(() -> Void) = { }) { 25 | dispatch.background({ try factory.delete(repository) }, error: error, success: done) 26 | } 27 | 28 | public class func clone(_ remote: String, local: URL, error: @escaping((Error) -> Void) = { _ in }, done: @escaping(() -> Void) = { }) { 29 | dispatch.background({ try factory.clone(remote, local: local, error: error, done: done) }, error: error) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Git/Ignore.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Ignore { 4 | private var contains = ["/.git/"] 5 | private var suffix = ["/.git"] 6 | 7 | init(_ url: URL) { 8 | guard let data = try? Data(contentsOf: url.appendingPathComponent(".gitignore")) else { return } 9 | String(decoding: data, as: UTF8.self).components(separatedBy: "\n").filter({ !$0.isEmpty }).forEach { 10 | switch $0.first { 11 | case "*": 12 | suffix.append(String($0.dropFirst())) 13 | contains.append({ $0.last == "/" ? $0 : $0 + "/" } (String($0.dropFirst()))) 14 | case "/": suffix.append($0) 15 | default: suffix.append("/" + $0) 16 | } 17 | switch $0.last { 18 | case "*": contains.append({ $0.first == "/" ? $0 : "/" + $0 } (String($0.dropLast()))) 19 | case "/": contains.append($0.first == "/" ? $0 : "/" + $0) 20 | default: contains.append(($0.first == "/" ? $0 : "/" + $0) + "/") 21 | } 22 | } 23 | } 24 | 25 | func url(_ url: URL) -> Bool { 26 | guard 27 | !url.hasDirectoryPath, 28 | !contains.contains(where: { url.path.contains($0) }), 29 | !suffix.contains(where: { url.path.hasSuffix($0) }) 30 | else { return true } 31 | return false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Git/Index.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Index { 4 | struct Entry { 5 | fileprivate(set) var created = Date() 6 | fileprivate(set) var modified = Date() 7 | fileprivate(set) var id = "" 8 | fileprivate(set) var url = URL(fileURLWithPath: "") 9 | fileprivate(set) var size = 0 10 | fileprivate(set) var device = 0 11 | fileprivate(set) var inode = 0 12 | fileprivate(set) var mode = 33188 13 | fileprivate(set) var user = 0 14 | fileprivate(set) var group = 0 15 | fileprivate(set) var conflicts = false 16 | } 17 | 18 | private(set) var id = "" 19 | private(set) var version = 2 20 | private(set) var entries = [Entry]() 21 | 22 | init() { } 23 | 24 | init?(_ url: URL) { 25 | guard 26 | let parse = Parse(url.appendingPathComponent(".git/index")), 27 | "DIRC" == (try? parse.string()), 28 | let version = try? parse.number(), 29 | let count = try? parse.number(), 30 | let entries = try? (0 ..< count).map({ _ in try entry(parse, url: url) }) 31 | else { return nil } 32 | parse.skipExtensions() 33 | id = (try? parse.hash()) ?? "" 34 | self.version = version 35 | self.entries = entries 36 | } 37 | 38 | func entry(_ id: String, url: URL) { 39 | var entry = Entry() 40 | entry.id = id 41 | entry.url = url 42 | entry.size = try! Data(contentsOf: url).count 43 | entries.removeAll(where: { $0.url.path == url.path }) 44 | entries.append(entry) 45 | } 46 | 47 | func save(_ url: URL) { 48 | let serial = Serial() 49 | serial.string("DIRC") 50 | serial.number(UInt32(version)) 51 | serial.number(UInt32(entries.count)) 52 | entries.sorted(by: { $0.url.path.compare($1.url.path, options: .caseInsensitive) != .orderedDescending }).forEach { 53 | serial.date($0.created) 54 | serial.date($0.modified) 55 | serial.number(UInt32($0.device)) 56 | serial.number(UInt32($0.inode)) 57 | serial.number(UInt32($0.mode)) 58 | serial.number(UInt32($0.user)) 59 | serial.number(UInt32($0.group)) 60 | serial.number(UInt32($0.size)) 61 | serial.hex($0.id) 62 | serial.number(UInt8(0)) 63 | 64 | let name = String($0.url.path.dropFirst(url.path.count + 1)) 65 | var size = name.count 66 | serial.number(UInt8(size)) 67 | serial.nulled(name) 68 | while (size + 7) % 8 != 0 { 69 | serial.string("\u{0000}") 70 | size += 1 71 | } 72 | } 73 | serial.hash() 74 | try! serial.data.write(to: url.appendingPathComponent(".git/index"), options: .atomic) 75 | } 76 | 77 | private func entry(_ parse: Parse, url: URL) throws -> Entry { 78 | var entry = Entry() 79 | entry.created = try parse.date() 80 | entry.modified = try parse.date() 81 | entry.device = try parse.number() 82 | entry.inode = try parse.number() 83 | entry.mode = try parse.number() 84 | entry.user = try parse.number() 85 | entry.group = try parse.number() 86 | entry.size = try parse.number() 87 | entry.id = try parse.hash() 88 | entry.conflicts = try parse.conflict() 89 | entry.url = url.appendingPathComponent(try parse.name()) 90 | return entry 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Git/Merger.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Merger { 4 | weak var repository: Repository? 5 | 6 | func needs(_ id: String) throws -> Bool { 7 | guard let url = repository?.url, let local = try? History(url), let remote = try? History(id, url: url) else { return false } 8 | var same = false 9 | var index = 0 10 | let keys = Array(local.map.keys) 11 | while !same && index < keys.count { 12 | same = remote.map[keys[index]] != nil 13 | index += 1 14 | } 15 | if !same { 16 | throw Failure.Merge.common 17 | } 18 | return remote.map[try Hub.head.id(url)] == nil && local.map[id] == nil 19 | } 20 | 21 | func known(_ id: String) throws { 22 | guard let url = repository?.url, let history = try? History(url) else { return } 23 | if history.map[id] == nil { 24 | throw Failure.Merge.unknown 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Git/Packer.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Packer { 4 | weak var repository: Repository? 5 | 6 | var packed: Bool { 7 | if let url = repository?.url { 8 | if (try? FileManager.default.contentsOfDirectory(at: url.appendingPathComponent(".git/objects/pack/"), includingPropertiesForKeys: nil))?.first( where: { $0.pathExtension == "pack" }) != nil { 9 | return true 10 | } 11 | if FileManager.default.fileExists(atPath: url.appendingPathComponent(".git/packed-refs").path) { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | func unpack() throws { 19 | guard let url = repository?.url else { return } 20 | try Pack.pack(url).forEach { 21 | try $0.1.unpack(url) 22 | try $0.1.remove(url, id: $0.0) 23 | } 24 | try references() 25 | if let tree = try? Hub.head.tree(url) { 26 | let index = Index() 27 | try tree.map(index, url: url) 28 | index.save(url) 29 | } 30 | } 31 | 32 | private func references() throws { 33 | guard let url = repository?.url, FileManager.default.fileExists(atPath: url.appendingPathComponent(".git/packed-refs").path) else { return } 34 | try String(decoding: Data(contentsOf: url.appendingPathComponent(".git/packed-refs")), as: UTF8.self).components(separatedBy: "\n").forEach { 35 | if $0.first != "#" { 36 | let reference = $0.components(separatedBy: " ") 37 | guard reference.count >= 2 else { return } 38 | let location = url.appendingPathComponent(".git/" + reference[1]) 39 | if !FileManager.default.fileExists(atPath: location.deletingLastPathComponent().path) { 40 | try FileManager.default.createDirectory(at: location.deletingLastPathComponent(), withIntermediateDirectories: true) 41 | } 42 | try Data(reference[0].utf8).write(to: location, options: .atomic) 43 | } 44 | } 45 | try FileManager.default.removeItem(at: url.appendingPathComponent(".git/packed-refs")) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Git/Parse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Parse { 4 | let data: Data 5 | private(set) var index = 0 6 | 7 | init?(_ url: URL) { 8 | if let data = try? Data(contentsOf: url) { 9 | self.data = data 10 | } else { 11 | return nil 12 | } 13 | } 14 | 15 | init(_ data: Data) { 16 | self.data = data 17 | } 18 | 19 | func ascii(_ limiter: String) throws -> String { 20 | var result = "" 21 | var character = "" 22 | while character != limiter { 23 | result += character 24 | character = try self.character() 25 | } 26 | return result 27 | } 28 | 29 | func variable() throws -> String { 30 | var result = "" 31 | var byte = "" 32 | repeat { 33 | result += byte 34 | byte = try character() 35 | } while byte != "\u{0000}" 36 | return result 37 | } 38 | 39 | func name() throws -> String { 40 | return try { 41 | discard($0 ? 4 : 2) 42 | let result = String(decoding: try advance($1), as: UTF8.self) 43 | clean() 44 | return result 45 | } (try not2(), try length()) 46 | } 47 | 48 | func discard(_ until: String) throws { 49 | while String(decoding: try advance(until.count), as: UTF8.self) != until { 50 | index -= until.count - 1 51 | } 52 | } 53 | 54 | func byte() throws -> UInt8 { return try advance(1).first! } 55 | func string() throws -> String { return String(decoding: try advance(4), as: UTF8.self) } 56 | func character() throws -> String { return String(decoding: try advance(1), as: UTF8.self) } 57 | func hash() throws -> String { return (try advance(20)).map { String(format: "%02hhx", $0) }.joined() } 58 | func skipExtensions() { discard((data.count - 20) - index) } 59 | func decompress(_ amount: Int) throws -> Data { return Hub.press.decompress(try advance(amount)) } 60 | func discard(_ bytes: Int) { index += bytes } 61 | 62 | func number() throws -> Int { 63 | if let result = Int(try advance(4).map { String(format: "%02hhx", $0) }.joined(), radix: 16) { 64 | return result 65 | } 66 | throw Failure.Parsing.malformed 67 | } 68 | 69 | func date() throws -> Date { 70 | let result = Date(timeIntervalSince1970: TimeInterval(try number())) 71 | discard(4) 72 | return result 73 | } 74 | 75 | func conflict() throws -> Bool { 76 | var byte = data.subdata(in: index ..< index + 1).first! 77 | byte >>= 2 78 | if (byte & 0x01) == 1 { 79 | return true 80 | } 81 | byte >>= 1 82 | if (byte & 0x01) == 1 { 83 | return true 84 | } 85 | return false 86 | } 87 | 88 | func size(_ carry: Int = 0, shift: Int = 0) throws -> Int { 89 | var byte = 0 90 | var result = carry 91 | var shift = shift 92 | repeat { 93 | byte = Int(try self.byte()) 94 | result += (byte & 127) << shift 95 | shift += 7 96 | } while byte >= 128 97 | return result 98 | } 99 | 100 | func offset() throws -> Int { 101 | var byte = 0 102 | var result = 0 103 | var times = 0 104 | repeat { 105 | byte = Int(try self.byte()) 106 | if times > 0 { 107 | result <<= 7 108 | } 109 | result += (byte & 127) 110 | times += 1 111 | } while byte >= 128 112 | if times > 1 { 113 | (1 ..< times).forEach { 114 | result += Int(pow(2, (7 * Double($0)))) 115 | } 116 | } 117 | return result 118 | } 119 | 120 | func advance(_ bytes: Int) throws -> Data { 121 | let index = self.index + bytes 122 | guard data.count >= index else { throw Failure.Parsing.malformed } 123 | let result = data.subdata(in: self.index ..< index) 124 | self.index = index 125 | return result 126 | } 127 | 128 | private func clean() { 129 | while (String(decoding: data.subdata(in: index ..< index + 1), as: UTF8.self) == "\u{0000}") { discard(1) } 130 | } 131 | 132 | private func not2() throws -> Bool { 133 | var byte = data.subdata(in: 134 | index ..< index + 1).withUnsafeBytes { $0.baseAddress!.bindMemory(to: UInt8.self, capacity: 1).pointee } 135 | byte >>= 1 136 | return (byte & 0x01) == 1 137 | } 138 | 139 | private func length() throws -> Int { 140 | guard let result = Int(data.subdata(in: index + 1 ..< index + 2).map { String(format: "%02hhx", $0) }.joined(), 141 | radix: 16) else { throw Failure.Parsing.malformed } 142 | return result 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Git/Press.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Compression 3 | 4 | final class Press { 5 | private let prime = UInt32(65521) 6 | 7 | func decompress(_ data: Data) -> Data { 8 | return data.withUnsafeBytes { 9 | let buffer = UnsafeMutablePointer.allocate(capacity: data.count * 10) 10 | let result = Data(bytes: buffer, count: compression_decode_buffer( 11 | buffer, data.count * 10, $0.baseAddress!.bindMemory( 12 | to: UInt8.self, capacity: 1).advanced(by: 2), data.count - 2, nil, COMPRESSION_ZLIB)) 13 | buffer.deallocate() 14 | return result 15 | } 16 | } 17 | 18 | func compress(_ source: Data) -> Data { 19 | var data = Data([0x78, 0x1]) 20 | data.append(source.withUnsafeBytes { 21 | let buffer = UnsafeMutablePointer.allocate(capacity: source.count * 10) 22 | let wrote = compression_encode_buffer(buffer, source.count * 10, $0.baseAddress!.bindMemory(to: UInt8.self, capacity: 1), 23 | source.count, nil, COMPRESSION_ZLIB) 24 | let result = Data(bytes: buffer, count: wrote) 25 | buffer.deallocate() 26 | return result 27 | }) 28 | data.append(adler32(source)) 29 | return data 30 | } 31 | 32 | func unpack(_ size: Int, data: Data) throws -> (Int, Data) { 33 | var index = max(compress(decompress(data)).count - max(size / 30, 9), 0) 34 | let result = try data.withUnsafeBytes { 35 | var stream = UnsafeMutablePointer.allocate(capacity: 1).pointee 36 | let buffer = UnsafeMutablePointer.allocate(capacity: size) 37 | var status = compression_stream_init(&stream, COMPRESSION_STREAM_DECODE, COMPRESSION_ZLIB) 38 | var advance = 2 39 | var read = index + 1 40 | var result = Data() 41 | repeat { 42 | index += 1 43 | stream.dst_ptr = buffer 44 | stream.dst_size = size 45 | stream.src_size = read 46 | stream.src_ptr = $0.baseAddress!.bindMemory(to: UInt8.self, capacity: 1).advanced(by: advance) 47 | status = compression_stream_process(&stream, 0) 48 | result += Data(bytes: buffer, count: size - stream.dst_size) 49 | read = 1 50 | advance = 2 + index 51 | } while status == COMPRESSION_STATUS_OK 52 | buffer.deallocate() 53 | compression_stream_destroy(&stream) 54 | guard status == COMPRESSION_STATUS_END else { throw Failure.Pack.read } 55 | guard result.count == size else { throw Failure.Pack.size } 56 | return result 57 | } as Data 58 | 59 | let adler = adler32(result) 60 | var found = false 61 | var drift = 0 62 | repeat { 63 | if drift > 3 { throw Failure.Pack.adler } 64 | if adler[0] == data[index + drift], 65 | adler[1] == data[index + drift + 1], 66 | adler[2] == data[index + drift + 2], 67 | adler[3] == data[index + drift + 3] { 68 | found = true 69 | } 70 | drift += 1 71 | } while !found 72 | 73 | index += 6 74 | return (index, result) 75 | } 76 | 77 | private func adler32(_ data: Data) -> Data { 78 | var s1 = UInt32(1 & 0xffff) 79 | var s2 = UInt32((1 >> 16) & 0xffff) 80 | data.forEach { 81 | s1 += UInt32($0) 82 | if s1 >= prime { s1 = s1 % prime } 83 | s2 += s1 84 | if s2 >= prime { s2 = s2 % prime } 85 | } 86 | var result = ((s2 << 16) | s1).bigEndian 87 | return Data(bytes: &result, count: MemoryLayout.size) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Git/Rest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | class Rest: NSObject, URLSessionTaskDelegate { 4 | private var session: URLSession! 5 | 6 | override init() { 7 | super.init() 8 | session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: OperationQueue()) 9 | } 10 | 11 | func urlSession(_: URLSession, task: URLSessionTask, didReceive: URLAuthenticationChallenge, completionHandler: @escaping 12 | (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 13 | completionHandler(.useCredential, didReceive.previousFailureCount == 0 ? Hub.session.credentials : nil) 14 | } 15 | 16 | func download(_ remote: String, error: @escaping((Error) -> Void), result: @escaping((Fetch) throws -> Void)) throws { 17 | session.dataTask(with: URLRequest(url: try url(remote, suffix: "/info/refs?service=git-upload-pack"), cachePolicy: 18 | .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 20)) { [weak self] in 19 | self?.validate($0, $1, $2, error: error) { try result(.Pull($0)) } 20 | }.resume() 21 | } 22 | 23 | func upload(_ remote: String, error: @escaping((Error) -> Void), result: @escaping((Fetch) throws -> Void)) throws { 24 | session.dataTask(with: URLRequest(url: try url(remote, suffix: "/info/refs?service=git-receive-pack"), cachePolicy: 25 | .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 20)) { [weak self] in 26 | self?.validate($0, $1, $2, error: error) { try result(.Push($0)) } 27 | }.resume() 28 | } 29 | 30 | func pull(_ remote: String, want: String, have: String = "", error: @escaping((Error) -> Void), result: @escaping((Pack) throws -> Void)) throws { 31 | session.dataTask(with: { 32 | var request = URLRequest(url: $0, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 90) 33 | request.httpMethod = "POST" 34 | request.setValue("application/x-git-upload-pack-request", forHTTPHeaderField: "Content-Type") 35 | request.httpBody = Data(""" 36 | 0032want \(want) 37 | 0000\(have)0009done 38 | 39 | """.utf8) 40 | return request 41 | } (try url(remote, suffix: "/git-upload-pack")) as URLRequest) { [weak self] in 42 | self?.validate($0, $1, $2, error: error) { try result(Pack($0)) } 43 | }.resume() 44 | } 45 | 46 | func push(_ remote: String, old: String, new: String, pack: Data, error: @escaping((Error) -> Void), done: @escaping((String) throws -> Void)) throws { 47 | session.dataTask(with: { 48 | var request = URLRequest(url: $0, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 90) 49 | request.httpMethod = "POST" 50 | request.setValue("application/x-git-receive-pack-request", forHTTPHeaderField: "Content-Type") 51 | request.httpBody = """ 52 | 0077\(old) \(new) refs/heads/master\0 report-status 53 | 0000 54 | """.utf8 + pack 55 | return request 56 | } (try url(remote, suffix: "/git-receive-pack")) as URLRequest) { [weak self] in 57 | self?.validate($0, $1, $2, error: error) { try done(String(decoding: $0, as: UTF8.self)) } 58 | }.resume() 59 | } 60 | 61 | func url(_ remote: String, suffix: String) throws -> URL { 62 | guard !remote.isEmpty, !remote.hasPrefix("http://"), !remote.hasPrefix("https://"), remote.hasSuffix(".git"), 63 | let remote = remote.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed), 64 | let url = URL(string: "https://" + remote + suffix) 65 | else { throw Failure.Request.invalid } 66 | return url 67 | } 68 | 69 | private func validate(_ data: Data?, _ response: URLResponse?, _ fail: Error?, 70 | error: @escaping((Error) -> Void), result: @escaping((Data) throws -> Void)) { 71 | Hub.dispatch.background({ 72 | if let fail = fail { 73 | throw fail 74 | } else { 75 | switch (response as? HTTPURLResponse)?.statusCode { 76 | case 200, 201: 77 | if let data = data, !data.isEmpty { 78 | try result(data) 79 | } else { 80 | throw Failure.Request.empty 81 | } 82 | case 401: throw Failure.Request.auth 83 | case 404: throw Failure.Request.none 84 | default: throw Failure.Request.response 85 | } 86 | } 87 | }, error: error) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Git/Serial.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Serial { 4 | private(set) var data = Data() 5 | private static let map = [ 6 | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567 7 | 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>? 8 | 0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG 9 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 // HIJKLMNO 10 | ] as [UInt8] 11 | 12 | func date(_ date: Date) { 13 | number(UInt32(date.timeIntervalSince1970)) 14 | number(UInt32(0)) 15 | } 16 | 17 | func hex(_ string: String) { 18 | data.append(contentsOf: string.utf8.reduce(into: ([UInt8](), [UInt8]())) { 19 | $0.0.append(Serial.map[Int($1 & 0x1F ^ 0x10)]) 20 | if $0.0.count == 2 { 21 | $0.1.append($0.0[0] << 4 | $0.0[1]) 22 | $0.0 = [] 23 | } 24 | }.1) 25 | } 26 | 27 | func compress(_ data: Data) { self.data.append(Hub.press.compress(data)) } 28 | func serial(_ serial: Serial) { data.append(serial.data) } 29 | func nulled(_ string: String) { self.string(string + "\u{0000}") } 30 | func string(_ string: String) { data.append(contentsOf: string.utf8) } 31 | func number(_ number: T) { withUnsafeBytes(of: number) { data.append(contentsOf: $0.reversed()) } } 32 | func hash() { data.append(contentsOf: Hub.hash.digest(data)) } 33 | } 34 | -------------------------------------------------------------------------------- /Git/Session.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class Session: Codable { 4 | public enum Purchase: String, Codable, CaseIterable { 5 | case cloud 6 | case timeline 7 | } 8 | 9 | public internal(set) var url = URL(fileURLWithPath: "") 10 | public internal(set) var purchase = [Purchase]() 11 | public internal(set) var bookmark = Data() 12 | public internal(set) var name = "" 13 | public internal(set) var email = "" 14 | public internal(set) var user = "" 15 | public internal(set) var password: String { 16 | get { return recover ?? "" } 17 | set { 18 | if recover == nil { 19 | var query = self.query 20 | query[kSecValueData as String] = Data(newValue.utf8) 21 | SecItemAdd(query as CFDictionary, nil) 22 | } else { 23 | SecItemUpdate(query as CFDictionary, [kSecValueData: Data(newValue.utf8)] as CFDictionary) 24 | } 25 | } 26 | } 27 | 28 | var credentials: URLCredential? { return URLCredential(user: user, password: password, persistence: .forSession) } 29 | 30 | private var recover: String? { 31 | var result: CFTypeRef? = [String: Any]() as CFTypeRef 32 | var query = self.query 33 | query[kSecReturnData as String] = true 34 | query[kSecReturnAttributes as String] = true 35 | query[kSecMatchLimit as String] = kSecMatchLimitOne 36 | SecItemCopyMatching(query as CFDictionary, &result) 37 | if let data = (result as? [String: Any])?[String(kSecValueData)] as? Data { 38 | return String(decoding: data, as: UTF8.self) 39 | } 40 | return nil 41 | } 42 | 43 | private var query: [String: Any] { 44 | return [kSecClass: kSecClassGenericPassword, kSecAttrKeyType: 42, kSecAttrAccount: "user.key", kSecAttrService: "Git"] as [String: Any] 45 | } 46 | 47 | public func load(_ result: @escaping(() -> Void) = { }) { 48 | Hub.dispatch.background({ 49 | guard let data = UserDefaults.standard.data(forKey: "session"), 50 | let decoded = try? JSONDecoder().decode(Session.self, from: data) 51 | else { return } 52 | self.name = decoded.name 53 | self.email = decoded.email 54 | self.url = decoded.url 55 | self.bookmark = decoded.bookmark 56 | self.user = decoded.user 57 | self.purchase = decoded.purchase 58 | }, success: result) 59 | } 60 | 61 | public func update(_ name: String, email: String, error: @escaping((Error) -> Void) = { _ in }, done: @escaping(() -> Void) = { }) { 62 | Hub.dispatch.background({ 63 | guard !name.isEmpty else { throw Failure.User.name } 64 | 65 | try name.forEach { 66 | switch $0 { 67 | case "<", ">", "\n", "\t": throw Failure.User.name 68 | default: break 69 | } 70 | } 71 | 72 | try email.forEach { 73 | switch $0 { 74 | case " ", "*", "\\", "/", "$", "%", ";", ",", "!", "?", "~", "<", ">", "\n", "\t": throw Failure.User.email 75 | default: break 76 | } 77 | } 78 | 79 | let at = email.components(separatedBy: "@") 80 | let dot = at.last!.components(separatedBy: ".") 81 | guard at.count == 2, !at.first!.isEmpty, dot.count > 1, !dot.first!.isEmpty, !dot.last!.isEmpty 82 | else { throw Failure.User.email } 83 | 84 | self.name = name 85 | self.email = email 86 | self.save() 87 | }, error: error, success: done) 88 | } 89 | 90 | public func update(_ user: String, password: String, done: @escaping(() -> Void) = { }) { 91 | Hub.dispatch.background ({ 92 | self.user = user 93 | self.password = password 94 | self.save() 95 | }, success: done) 96 | } 97 | 98 | public func update(_ url: URL, bookmark: Data, done: @escaping(() -> Void) = { }) { 99 | Hub.dispatch.background({ 100 | self.url = url 101 | self.bookmark = bookmark 102 | self.save() 103 | }, success: done) 104 | } 105 | 106 | public func purchase(_ id: String, done: @escaping(() -> Void) = { }) { 107 | Hub.dispatch.background ({ 108 | let item = Purchase(rawValue: id.components(separatedBy: ".").last!)! 109 | if !self.purchase.contains(where: { $0 == item }) { 110 | self.purchase.append(item) 111 | self.save() 112 | } 113 | }, success: done) 114 | } 115 | 116 | func save() { UserDefaults.standard.set(try! JSONEncoder().encode(self), forKey: "session") } 117 | } 118 | -------------------------------------------------------------------------------- /Git/Stage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Stage { 4 | weak var repository: Repository? 5 | 6 | func commit(_ files: [URL], message: String) throws { 7 | guard let url = repository?.url, let list = repository?.state.list else { return } 8 | guard !files.isEmpty else { throw Failure.Commit.empty } 9 | guard !Hub.session.name.isEmpty else { throw Failure.Commit.credentials } 10 | guard !Hub.session.email.isEmpty else { throw Failure.Commit.credentials } 11 | guard !message.isEmpty else { throw Failure.Commit.message } 12 | try files.forEach { file in 13 | if !list.contains(where: { $0.0.path == file.path }) { 14 | throw Failure.Commit.none 15 | } 16 | } 17 | let index = Index(url) ?? Index() 18 | let ignore = Ignore(url) 19 | let tree = Tree(url, ignore: ignore, update: files, entries: index.entries) 20 | let treeId = try tree.save(url) 21 | try files.forEach { 22 | guard !ignore.url($0) else { throw Failure.Commit.ignored } 23 | try? add($0, index: index) 24 | } 25 | let commit = Commit() 26 | commit.author.name = Hub.session.name 27 | commit.author.email = Hub.session.email 28 | commit.committer.name = Hub.session.name 29 | commit.committer.email = Hub.session.email 30 | commit.tree = treeId 31 | commit.message = message 32 | if let parent = try? Hub.head.id(url) { 33 | commit.parent.append(parent) 34 | } 35 | try Hub.head.update(url, id: Hub.content.add(commit, url: url)) 36 | index.save(url) 37 | } 38 | 39 | func merge(_ id: String) throws { 40 | guard let url = repository?.url, let files = repository?.state.list.map({ $0.0 }) else { return } 41 | guard !Hub.session.name.isEmpty else { throw Failure.Commit.credentials } 42 | guard !Hub.session.email.isEmpty else { throw Failure.Commit.credentials } 43 | let index = Index(url) ?? Index() 44 | let ignore = Ignore(url) 45 | let tree = Tree(url, ignore: ignore, update: files, entries: index.entries) 46 | let treeId = try tree.save(url) 47 | try files.forEach { 48 | guard !ignore.url($0) else { throw Failure.Commit.ignored } 49 | try? add($0, index: index) 50 | } 51 | let commit = Commit() 52 | commit.author.name = Hub.session.name 53 | commit.author.email = Hub.session.email 54 | commit.committer.name = Hub.session.name 55 | commit.committer.email = Hub.session.email 56 | commit.tree = treeId 57 | commit.message = "Merge.\n" 58 | commit.parent.append(try Hub.head.id(url)) 59 | commit.parent.append(id) 60 | try Hub.head.update(url, id: Hub.content.add(commit, url: url)) 61 | index.save(url) 62 | } 63 | 64 | func add(_ file: URL, index: Index) throws { 65 | guard let url = repository?.url else { return } 66 | guard file.path.contains(url.path) else { throw Failure.Add.outside } 67 | guard FileManager.default.fileExists(atPath: file.path) else { throw Failure.Add.not } 68 | let hash = try Hub.content.add(file, url: url) 69 | index.entry(hash, url: file) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Git/Status.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum Status { 4 | case untracked 5 | case added 6 | case modified 7 | case deleted 8 | } 9 | 10 | final class State { 11 | weak var repository: Repository? 12 | var last = Date.distantPast 13 | var delta = TimeInterval(1) 14 | let timer = DispatchSource.makeTimerSource(queue: .global(qos: .background)) 15 | 16 | init() { 17 | timer.resume() 18 | timer.schedule(deadline: .distantFuture) 19 | timer.setEventHandler { 20 | Hub.dispatch.background({ [weak self] in 21 | return self?.needs == true ? self?.list : nil 22 | }) { [weak self] in 23 | guard let delta = self?.delta else { return } 24 | if let changes = $0 { 25 | self?.repository?.status?(changes) 26 | } 27 | self?.timer.schedule(deadline: .now() + delta) 28 | } 29 | } 30 | } 31 | 32 | var needs: Bool { 33 | guard let url = repository?.url else { return false } 34 | if let modified = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.modificationDate] as? Date, 35 | modified > last { return true } 36 | return modified([url]) 37 | } 38 | 39 | var list: [(URL, Status)] { 40 | guard let url = repository?.url else { return [] } 41 | last = Date() 42 | return list((try? Hub.head.tree(url))) 43 | } 44 | 45 | func refresh() { 46 | last = .distantPast 47 | timer.schedule(deadline: .now() + delta) 48 | } 49 | 50 | func delay() { 51 | last = .distantFuture 52 | timer.schedule(deadline: .distantFuture) 53 | } 54 | 55 | func list(_ tree: Tree?) -> [(URL, Status)] { 56 | guard let url = repository?.url else { return [] } 57 | let contents = self.contents 58 | let index = Index(url) 59 | var tree = tree?.list(url) ?? [] 60 | return contents.reduce(into: [(URL, Status)]()) { result, url in 61 | if let entries = index?.entries.filter({ $0.url == url }), !entries.isEmpty { 62 | let hash = Hub.hash.file(url).1 63 | if entries.contains(where: { $0.id == hash }) { 64 | if !tree.contains(where: { $0.id == hash }) { 65 | result.append((url, .added)) 66 | } 67 | } else { 68 | result.append((url, .modified)) 69 | } 70 | tree.removeAll { $0.url == url } 71 | } else { 72 | result.append((url, .untracked)) 73 | } 74 | } + tree.map({ ($0.url, .deleted) }) 75 | } 76 | 77 | private var contents: [URL] { 78 | guard let url = repository?.url else { return [] } 79 | let ignore = Ignore(url) 80 | return FileManager.default.enumerator(at: url, includingPropertiesForKeys: nil)? 81 | .map({ ($0 as! URL).resolvingSymlinksInPath() }) 82 | .filter({ !ignore.url($0) }) 83 | .sorted(by: { $0.path.compare($1.path, options: .caseInsensitive) != .orderedDescending }) ?? [] 84 | } 85 | 86 | private func modified(_ urls: [URL]) -> Bool { 87 | var urls = urls 88 | guard 89 | !urls.isEmpty, 90 | let contents = try? FileManager.default.contentsOfDirectory(at: urls.first!, includingPropertiesForKeys: 91 | [.contentModificationDateKey]) 92 | else { return false } 93 | for item in contents { 94 | if item.hasDirectoryPath { 95 | if let modified = try? item.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate, 96 | modified > last { 97 | return true 98 | } 99 | } 100 | urls.append(item) 101 | } 102 | return modified(Array(urls.dropFirst())) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Git/Tree.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | final class Tree { 4 | enum Category: String { 5 | case blob = "100644" 6 | case exec = "100755" 7 | case tree = "40000" 8 | case unknown 9 | } 10 | 11 | final class Item { 12 | var id = "" 13 | var url = URL(fileURLWithPath: "") 14 | var category = Category.unknown 15 | } 16 | 17 | private(set) var items = [Item]() 18 | private(set) var children = [String: Tree]() 19 | 20 | convenience init(_ id: String, url: URL, trail: URL? = nil) throws { 21 | try self.init(Hub.content.get(id, url: url), url: trail ?? url) 22 | } 23 | 24 | convenience init(_ data: Data, url: URL) throws { 25 | let parse = Parse(data) 26 | guard "tree" == (try? parse.ascii(" ")) else { throw Failure.Tree.unreadable } 27 | _ = try parse.variable() 28 | try self.init(parse, url: url) 29 | } 30 | 31 | convenience init(_ data: Data) throws { 32 | try self.init(Parse(data), url: URL(fileURLWithPath: "")) 33 | } 34 | 35 | init(_ url: URL, ignore: Ignore, update: [URL], entries: [Index.Entry]) { 36 | try! FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil).forEach { 37 | let content = $0.resolvingSymlinksInPath() 38 | if content.hasDirectoryPath { 39 | let child = Tree(content, ignore: ignore, update: update, entries: entries) 40 | if !child.items.isEmpty { 41 | let item = Item() 42 | item.category = .tree 43 | item.url = content 44 | item.id = Hub.hash.tree(child.serial).1 45 | items.append(item) 46 | children[item.id] = child 47 | } 48 | } else if !ignore.url(content) { 49 | if update.contains(where: { $0.path == content.path }) { 50 | let item = Item() 51 | item.category = .blob 52 | item.url = content 53 | item.id = Hub.hash.file(content).1 54 | items.append(item) 55 | } else if let entry = entries.first(where: { $0.url.path == content.path }) { 56 | let item = Item() 57 | item.category = .blob 58 | item.url = entry.url 59 | item.id = entry.id 60 | items.append(item) 61 | } 62 | } 63 | } 64 | } 65 | 66 | init() { } 67 | 68 | private init(_ parse: Parse, url: URL) throws { 69 | while parse.index < parse.data.count { 70 | let item = Item() 71 | item.category = Category(rawValue: try parse.ascii(" ")) ?? .unknown 72 | item.url = url.appendingPathComponent(try parse.variable()) 73 | item.id = try parse.hash() 74 | items.append(item) 75 | } 76 | } 77 | 78 | func list(_ url: URL) -> [Item] { 79 | return items.flatMap { $0.category != .tree ? [$0] : (try? Tree($0.id, url: url, trail: $0.url))?.list(url) ?? [] } 80 | } 81 | 82 | func map(_ index: Index, url: URL) throws { 83 | try items.forEach { 84 | if $0.category == .tree { 85 | try Tree($0.id, url: url, trail: $0.url).map(index, url: url) 86 | } else { 87 | index.entry($0.id, url: $0.url) 88 | } 89 | } 90 | } 91 | 92 | @discardableResult func save(_ url: URL) throws -> String { 93 | try children.values.forEach({ try $0.save(url) }) 94 | return try Hub.content.add(self, url: url) 95 | } 96 | 97 | var serial: Data { 98 | let serial = Serial() 99 | items.sorted(by: { 100 | let left = $0.url.lastPathComponent + ($0.category == .tree ? "/" : "") 101 | let right = $1.url.lastPathComponent + ($1.category == .tree ? "/" : "") 102 | return left.compare(right) != .orderedDescending 103 | }).forEach { 104 | serial.string($0.category.rawValue + " ") 105 | serial.nulled($0.url.lastPathComponent) 106 | serial.hex($0.id) 107 | } 108 | return serial.data 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Git/User.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct User { 4 | public internal(set) var name = "" 5 | public internal(set) var email = "" 6 | public internal(set) var date = Date() 7 | var timezone = "" 8 | 9 | init() { } 10 | 11 | init(_ string: String) throws { 12 | let first = string.components(separatedBy: " <") 13 | let second = first.last?.components(separatedBy: "> ") 14 | let third = second?.last?.components(separatedBy: " ") 15 | guard 16 | first.count == 2, 17 | second?.count == 2, 18 | third?.count == 2, 19 | let names = first.first?.components(separatedBy: " "), 20 | names.count > 1, 21 | let seconds = TimeInterval(third![0]) 22 | else { throw Failure.Commit.unreadable } 23 | name = names.dropFirst().joined(separator: " ") 24 | email = second![0] 25 | date = Date(timeIntervalSince1970: seconds) 26 | timezone = third![1] 27 | } 28 | 29 | var serial: String { return "\(name) <\(email)> \(Int(date.timeIntervalSince1970)) " + (timezone.isEmpty ? { 30 | $0.dateFormat = "xx" 31 | return $0.string(from: date) 32 | } (DateFormatter()) : timezone) } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Git 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Mac/About.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class About: Window { 4 | init() { 5 | super.init(200, 200) 6 | border.isHidden = true 7 | 8 | let image = NSImageView() 9 | image.translatesAutoresizingMaskIntoConstraints = false 10 | image.imageScaling = .scaleNone 11 | image.image = NSImage(named: "logo") 12 | contentView!.addSubview(image) 13 | 14 | let label = Label(.key("About.label")) 15 | label.textColor = .halo 16 | label.font = .bold(20) 17 | contentView!.addSubview(label) 18 | 19 | let version = Label((Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? "") 20 | version.textColor = .halo 21 | version.font = .light(12) 22 | contentView!.addSubview(version) 23 | 24 | image.centerYAnchor.constraint(equalTo: contentView!.centerYAnchor, constant: -25).isActive = true 25 | image.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 26 | 27 | label.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 28 | label.topAnchor.constraint(equalTo: contentView!.centerYAnchor, constant: 20).isActive = true 29 | 30 | version.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 31 | version.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Mac/Alert.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class Alert: NSWindow { 4 | private weak var back: NSView! 5 | 6 | init(_ title: String? = nil, message: String) { 7 | super.init(contentRect: .init(x: app.home.frame.midX - 200, y: app.home.frame.midY - 75, width: 400, height: 150), 8 | styleMask: [.fullSizeContentView], backing: .buffered, defer: false) 9 | titlebarAppearsTransparent = true 10 | titleVisibility = .hidden 11 | backgroundColor = .clear 12 | isReleasedWhenClosed = false 13 | 14 | let back = NSView() 15 | back.translatesAutoresizingMaskIntoConstraints = false 16 | back.wantsLayer = true 17 | back.layer!.backgroundColor = NSColor.halo.withAlphaComponent(0.96).cgColor 18 | back.layer!.cornerRadius = 6 19 | back.layer!.borderWidth = 1 20 | back.layer!.borderColor = NSColor.black.cgColor 21 | back.alphaValue = 0 22 | contentView!.addSubview(back) 23 | self.back = back 24 | 25 | let label = Label() 26 | label.textColor = .black 27 | label.alignment = .center 28 | label.attributedStringValue = { 29 | if let title = title { 30 | $0.append(NSAttributedString(string: title + "\n", attributes: [.font: NSFont.systemFont(ofSize: 16, weight: .bold)])) 31 | } 32 | $0.append(NSAttributedString(string: message, attributes: [.font: NSFont.systemFont(ofSize: 14, weight: .regular)])) 33 | return $0 34 | } (NSMutableAttributedString()) 35 | back.addSubview(label) 36 | 37 | back.centerYAnchor.constraint(equalTo: contentView!.centerYAnchor).isActive = true 38 | back.leftAnchor.constraint(equalTo: contentView!.leftAnchor).isActive = true 39 | back.rightAnchor.constraint(equalTo: contentView!.rightAnchor).isActive = true 40 | 41 | label.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 20).isActive = true 42 | label.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -20).isActive = true 43 | label.topAnchor.constraint(equalTo: back.topAnchor, constant: 20).isActive = true 44 | label.bottomAnchor.constraint(equalTo: back.bottomAnchor, constant: -20).isActive = true 45 | 46 | NSAnimationContext.runAnimationGroup({ 47 | $0.duration = 0.5 48 | $0.allowsImplicitAnimation = true 49 | back.alphaValue = 1 50 | }) { DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in self?.dismiss() } } 51 | } 52 | 53 | override func mouseDown(with: NSEvent) { dismiss() } 54 | 55 | private func dismiss() { 56 | NSAnimationContext.runAnimationGroup({ 57 | $0.duration = 0.5 58 | $0.allowsImplicitAnimation = true 59 | back.alphaValue = 0 60 | }) { [weak self] in self?.close() } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Mac/Display.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | import Quartz 3 | 4 | final class Display { 5 | private final class Text: NSTextView { 6 | private weak var height: NSLayoutConstraint! 7 | 8 | required init?(coder: NSCoder) { return nil } 9 | init() { 10 | let storage = NSTextStorage() 11 | super.init(frame: .zero, textContainer: { 12 | storage.addLayoutManager($1) 13 | $1.addTextContainer($0) 14 | $0.lineBreakMode = .byCharWrapping 15 | return $0 16 | } (NSTextContainer(), NSLayoutManager()) ) 17 | translatesAutoresizingMaskIntoConstraints = false 18 | drawsBackground = false 19 | isRichText = false 20 | font = .light(16) 21 | textColor = .white 22 | isEditable = false 23 | textContainerInset = .init(width: 10, height: 10) 24 | height = heightAnchor.constraint(greaterThanOrEqualToConstant: 0) 25 | height.isActive = true 26 | } 27 | 28 | override func resize(withOldSuperviewSize: NSSize) { 29 | super.resize(withOldSuperviewSize: withOldSuperviewSize) 30 | adjust() 31 | } 32 | 33 | override func viewDidEndLiveResize() { 34 | super.viewDidEndLiveResize() 35 | adjust() 36 | } 37 | 38 | private func adjust() { 39 | textContainer!.size.width = superview!.superview!.frame.width - (textContainerInset.width * 2) 40 | layoutManager!.ensureLayout(for: textContainer!) 41 | height.constant = layoutManager!.usedRect(for: textContainer!).size.height + (textContainerInset.height * 2) + 30 42 | } 43 | } 44 | 45 | class func make(_ url: URL, data: Data) -> NSView { 46 | switch url.pathExtension.lowercased() { 47 | case "png", "jpg", "jpeg", "gif", "bmp": return image(data) 48 | case "pdf": return pdf(data) 49 | default: return text(data) 50 | } 51 | } 52 | 53 | private class func text(_ data: Data) -> Scroll { 54 | let text = Text() 55 | text.string = String(decoding: data, as: UTF8.self) 56 | 57 | let scroll = Scroll() 58 | scroll.documentView = text 59 | 60 | text.widthAnchor.constraint(equalTo: scroll.widthAnchor).isActive = true 61 | text.heightAnchor.constraint(greaterThanOrEqualTo: scroll.heightAnchor).isActive = true 62 | 63 | return scroll 64 | } 65 | 66 | private class func image(_ data: Data) -> NSImageView { 67 | let image = NSImageView() 68 | image.translatesAutoresizingMaskIntoConstraints = false 69 | image.imageScaling = .scaleProportionallyDown 70 | image.image = NSImage(data: data) 71 | return image 72 | } 73 | 74 | private class func pdf(_ data: Data) -> PDFView { 75 | let pdf = PDFView() 76 | pdf.translatesAutoresizingMaskIntoConstraints = false 77 | pdf.backgroundColor = .clear 78 | pdf.document = PDFDocument(data: data) 79 | return pdf 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Mac/Help.swift: -------------------------------------------------------------------------------- 1 | import AppKit 2 | 3 | final class Help: Window { 4 | private weak var label: Label! 5 | private weak var centerX: NSLayoutConstraint! 6 | private var buttons = [Button]() 7 | private var images = [NSImageView]() 8 | private var index = 0 9 | 10 | init() { 11 | super.init(350, 420) 12 | border.isHidden = true 13 | 14 | let label = Label() 15 | label.textColor = .white 16 | label.font = .systemFont(ofSize: 14, weight: .regular) 17 | contentView!.addSubview(label) 18 | self.label = label 19 | 20 | var rightImage: NSLayoutXAxisAnchor! 21 | var rightButton = contentView!.leftAnchor 22 | let steps = ["logo", "checkOn", "add", "reset", "cloud"] 23 | steps.enumerated().forEach { 24 | let image = NSImageView() 25 | image.translatesAutoresizingMaskIntoConstraints = false 26 | image.image = NSImage(named: $0.1) 27 | image.imageScaling = .scaleNone 28 | image.alphaValue = 0 29 | contentView!.addSubview(image) 30 | images.append(image) 31 | 32 | let button = Button.Image(self, action: #selector(show(_:))) 33 | button.image.image = NSImage(named: "dot") 34 | contentView!.addSubview(button) 35 | buttons.append(button) 36 | 37 | image.centerYAnchor.constraint(equalTo: contentView!.centerYAnchor, constant: -90).isActive = true 38 | image.heightAnchor.constraint(equalToConstant: 200).isActive = true 39 | image.widthAnchor.constraint(equalToConstant: 400).isActive = true 40 | 41 | button.heightAnchor.constraint(equalToConstant: 80).isActive = true 42 | button.widthAnchor.constraint(equalTo: contentView!.widthAnchor, multiplier: 1 / CGFloat(steps.count), constant: -16).isActive = true 43 | button.bottomAnchor.constraint(equalTo: contentView!.bottomAnchor).isActive = true 44 | 45 | if $0.0 == 0 { 46 | button.leftAnchor.constraint(equalTo: rightButton, constant: 40).isActive = true 47 | centerX = image.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor) 48 | centerX.isActive = true 49 | } else { 50 | button.leftAnchor.constraint(equalTo: rightButton).isActive = true 51 | image.leftAnchor.constraint(equalTo: rightImage, constant: 100).isActive = true 52 | } 53 | rightImage = image.rightAnchor 54 | rightButton = button.rightAnchor 55 | } 56 | 57 | label.topAnchor.constraint(equalTo: contentView!.centerYAnchor, constant: -10).isActive = true 58 | label.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 59 | label.widthAnchor.constraint(lessThanOrEqualToConstant: 280).isActive = true 60 | 61 | DispatchQueue.main.async { [weak self] in self?.display(0) } 62 | } 63 | 64 | override func keyDown(with: NSEvent) { 65 | switch with.keyCode { 66 | case 36: close() 67 | case 123: display(index > 0 ? index - 1 : images.count - 1) 68 | case 124: display(index < images.count - 1 ? index + 1 : 0) 69 | default: super.keyDown(with: with) 70 | } 71 | } 72 | 73 | private func display(_ index: Int) { 74 | self.index = index 75 | buttons.enumerated().forEach { 76 | $0.1.alphaValue = $0.0 == index ? 1 : 0.3 77 | } 78 | label.stringValue = .key("Onboard.mac\(index)") 79 | centerX.constant = CGFloat(-500 * index) 80 | NSAnimationContext.runAnimationGroup({ 81 | $0.duration = 1 82 | $0.allowsImplicitAnimation = true 83 | images.enumerated().forEach { 84 | $0.1.alphaValue = $0.0 == index ? 1 : 0 85 | } 86 | contentView!.layoutSubtreeIfNeeded() 87 | }) { } 88 | } 89 | 90 | @objc private func show(_ button: Button) { display(buttons.firstIndex(of: button)!) } 91 | } 92 | -------------------------------------------------------------------------------- /Mac/Mac.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-write 8 | 9 | com.apple.security.network.client 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Mac/Mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ATSApplicationFontsPath 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Git 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | Git 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | 492 23 | CFBundleVersion 24 | 492 25 | LSApplicationCategoryType 26 | public.app-category.developer-tools 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSPrincipalClass 30 | GitMac.App 31 | NSUserNotificationAlertStyle 32 | alert 33 | 34 | 35 | -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "brand16w.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "brand32w.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "brand32w-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "brand64w.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "brand128w.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "brand256w.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "brand256w-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "brand512w.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "brand512w-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "brand1024w.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand1024w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand1024w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand128w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand128w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand16w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand16w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand256w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand256w-1.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand256w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand256w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand32w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand32w-1.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand32w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand32w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand512w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand512w-1.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand512w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand512w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/AppIcon.appiconset/brand64w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Mac/Mac.xcassets/AppIcon.appiconset/brand64w.png -------------------------------------------------------------------------------- /Mac/Mac.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Mac/Reset.swift: -------------------------------------------------------------------------------- 1 | import Git 2 | import AppKit 3 | 4 | final class Reset: Window { 5 | init() { 6 | super.init(250, 250) 7 | border.isHidden = true 8 | 9 | let image = NSImageView() 10 | image.image = NSImage(named: "error") 11 | image.translatesAutoresizingMaskIntoConstraints = false 12 | image.imageScaling = .scaleNone 13 | contentView!.addSubview(image) 14 | 15 | let label = Label() 16 | label.textColor = .white 17 | label.attributedStringValue = { 18 | $0.append(NSAttributedString(string: .key("Reset.title"), attributes: [.font: NSFont.systemFont(ofSize: 14, weight: .bold)])) 19 | $0.append(NSAttributedString(string: .key("Reset.subtitle"), attributes: [.font: NSFont.systemFont(ofSize: 12, weight: .light)])) 20 | return $0 21 | } (NSMutableAttributedString()) 22 | contentView!.addSubview(label) 23 | 24 | let confirm = Button.Yes(self, action: #selector(self.confirm)) 25 | confirm.label.stringValue = .key("Reset.confirm") 26 | contentView!.addSubview(confirm) 27 | 28 | image.topAnchor.constraint(equalTo: contentView!.topAnchor, constant: 40).isActive = true 29 | image.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 30 | image.widthAnchor.constraint(equalToConstant: 60).isActive = true 31 | image.heightAnchor.constraint(equalToConstant: 60).isActive = true 32 | 33 | label.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 34 | label.topAnchor.constraint(equalTo: image.bottomAnchor, constant: 20).isActive = true 35 | label.widthAnchor.constraint(lessThanOrEqualToConstant: 200).isActive = true 36 | 37 | confirm.centerXAnchor.constraint(equalTo: contentView!.centerXAnchor).isActive = true 38 | confirm.bottomAnchor.constraint(equalTo: contentView!.bottomAnchor, constant: -20).isActive = true 39 | } 40 | 41 | @objc private func confirm() { 42 | app.home.update(.loading) 43 | app.repository?.reset({ 44 | app.refresh() 45 | app.alert(.key("Alert.error"), message: $0.localizedDescription) 46 | }) { [weak self] in 47 | app.alert(.key("Alert.success"), message: .key("Reset.success")) 48 | self?.close() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Find git on the AppStore: https://apps.apple.com/us/app/git/id1459215293 2 | 3 | Embrace change in your workflow. 4 | A version control system allows you to track changes on your files and documents and edit them without fear, you will always be able to go back in time and recover a previous version or compare the differences. 5 | 6 | Just like using git from the Terminal, but better. 7 | 8 | Features 9 | - Create or open a repository. 10 | - Commit changes. 11 | - Review the history/log. 12 | - Revert to a previous state. 13 | - Clone, Pull, Push, Edit, remotes. 14 | - Unpack a repository. 15 | - View the difference of an edited file. 16 | 17 | No requirements 18 | Seriously, fully native for MacOS and iOS, and you need nothing additional to create a repository and start tracking your files and documents. 19 | 20 | Privacy and analytics 21 | We value your privacy above all, and we are very serious about it. Git does NOT track your activity at any time, in fact, it is fully functional without an internet connection. Your credentials (name and email) are only used to sign your commits, but these are only stored in your device. 22 | 23 | Git is a distributed version-control system for tracking changes in source code during software development. It is designed for coordinating work among programmers, but it can be used to track changes in any set of files. Its goals include speed, data integrity, and support for distributed, non-linear workflows. 24 | - Wikipedia 25 | 26 | Try it yourself and let us know what you think. 27 | -------------------------------------------------------------------------------- /Shared/SFMono-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/SFMono-Bold.otf -------------------------------------------------------------------------------- /Shared/SFMono-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/SFMono-Light.otf -------------------------------------------------------------------------------- /Shared/Shared.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension String { 4 | static func key(_ key: String) -> String { return NSLocalizedString(key, comment: "") } 5 | } 6 | 7 | enum State { 8 | case loading 9 | case ready 10 | case packed 11 | case create 12 | case first 13 | } 14 | -------------------------------------------------------------------------------- /Shared/Shared.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/add.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "add.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/add.imageset/add.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/add.imageset/add.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/checkOff.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "checkOff.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/checkOff.imageset/checkOff.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/checkOff.imageset/checkOff.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/checkOn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "checkOn.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/checkOn.imageset/checkOn.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/checkOn.imageset/checkOn.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/close.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "close.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/close.imageset/close.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/close.imageset/close.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/cloud.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "cloud.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/cloud.imageset/cloud.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/cloud.imageset/cloud.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/dot.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "dot.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/dot.imageset/dot.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/dot.imageset/dot.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/error.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "error.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/error.imageset/error.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/error.imageset/error.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/history.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "history.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/history.imageset/history.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/history.imageset/history.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "home.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/home.imageset/home.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/home.imageset/home.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/loading.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "loading.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/loading.imageset/loading.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/loading.imageset/loading.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/logo.imageset/logo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/logo.imageset/logo.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/logotouch.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logotouch.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/logotouch.imageset/logotouch.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/logotouch.imageset/logotouch.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/market.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "market.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/market.imageset/market.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/market.imageset/market.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/reset.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "reset.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/reset.imageset/reset.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/reset.imageset/reset.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/settings.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "settings.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/settings.imageset/settings.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/settings.imageset/settings.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/timeline.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "timeline.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/timeline.imageset/timeline.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/timeline.imageset/timeline.pdf -------------------------------------------------------------------------------- /Shared/Shared.xcassets/updated.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "updated.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true 14 | } 15 | } -------------------------------------------------------------------------------- /Shared/Shared.xcassets/updated.imageset/updated.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Shared/Shared.xcassets/updated.imageset/updated.pdf -------------------------------------------------------------------------------- /Tests/MockRest.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | @testable import Git 3 | 4 | class MockRest: Rest { 5 | var _error: Error? 6 | var _fetch: Fetch? 7 | var _pull: Pack? 8 | var _push = "000eunpack ok" 9 | var onDownload: ((String) -> Void)? 10 | var onUpload: ((String) -> Void)? 11 | var onPull: ((String, String, String) -> Void)? 12 | var onPush: ((String, String, String, Data) -> Void)? 13 | 14 | override func download(_ remote: String, error: @escaping ((Error) -> Void), result: @escaping ((Fetch) throws -> Void)) { 15 | if let _fetch = self._fetch { 16 | do { 17 | try result(_fetch) 18 | } catch let exception { 19 | error(exception) 20 | } 21 | } else if let _error = self._error { 22 | error(_error) 23 | } 24 | onDownload?(remote) 25 | } 26 | 27 | override func upload(_ remote: String, error: @escaping ((Error) -> Void), result: @escaping ((Fetch) throws -> Void)) throws { 28 | if let _fetch = self._fetch { 29 | do { 30 | try result(_fetch) 31 | } catch let exception { 32 | error(exception) 33 | } 34 | } else if let _error = self._error { 35 | error(_error) 36 | } 37 | onUpload?(remote) 38 | } 39 | 40 | override func pull(_ remote: String, want: String, have: String = "", error: @escaping ((Error) -> Void), result: @escaping ((Pack) throws -> Void)) throws { 41 | if let _pull = self._pull { 42 | do { 43 | try result(_pull) 44 | } catch let exception { 45 | error(exception) 46 | } 47 | } else if let _error = self._error { 48 | error(_error) 49 | } 50 | onPull?(remote, want, have) 51 | } 52 | 53 | override func push(_ remote: String, old: String, new: String, pack: Data, error: @escaping ((Error) -> Void), done: @escaping ((String) throws -> Void)) throws { 54 | if let _error = self._error { 55 | error(_error) 56 | } else { 57 | try done(_push) 58 | } 59 | onPush?(remote, old, new, pack) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/TestAdd.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestAdd: XCTestCase { 5 | private var repository: Repository! 6 | private var url: URL! 7 | 8 | override func setUp() { 9 | Hub.session = Session() 10 | Hub.factory.rest = MockRest() 11 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 12 | try! FileManager.default.createDirectory(at: url.appendingPathComponent(".git/objects"), 13 | withIntermediateDirectories: true) 14 | repository = Repository(url) 15 | } 16 | 17 | override func tearDown() { 18 | try! FileManager.default.removeItem(at: url) 19 | } 20 | 21 | func testFirstFile() { 22 | let file = url.appendingPathComponent("myfile.txt") 23 | try! Data("hello world".utf8).write(to: file) 24 | let i = Index(url) ?? Index() 25 | try? repository.stage.add(file, index: i) 26 | i.save(url) 27 | let data = try? Data(contentsOf: url.appendingPathComponent(".git/index")) 28 | let index = Index(url) 29 | XCTAssertTrue(FileManager.default.fileExists(atPath: url.appendingPathComponent(".git/index").path)) 30 | XCTAssertTrue(FileManager.default.fileExists(atPath: 31 | url.appendingPathComponent(".git/objects/95/d09f2b10159347eece71399a7e2e907ea3df4f").path)) 32 | XCTAssertEqual(27, (try? Data(contentsOf: 33 | url.appendingPathComponent(".git/objects/95/d09f2b10159347eece71399a7e2e907ea3df4f")))?.count) 34 | XCTAssertEqual(112, data?.count) 35 | XCTAssertEqual(2, index?.version) 36 | XCTAssertEqual(40, index?.id.count) 37 | XCTAssertEqual(1, index?.entries.count) 38 | XCTAssertEqual("myfile.txt", index?.entries.first?.url.path.dropFirst(url.path.count + 1)) 39 | XCTAssertEqual("95d09f2b10159347eece71399a7e2e907ea3df4f", index?.entries.first?.id) 40 | XCTAssertEqual(11, index?.entries.first?.size) 41 | } 42 | 43 | func testDoubleAdd() { 44 | let file = url.appendingPathComponent("myfile.txt") 45 | try! Data("hello world".utf8).write(to: file) 46 | let index = Index(url) ?? Index() 47 | try? repository.stage.add(file, index: index) 48 | try? repository.stage.add(file, index: index) 49 | XCTAssertEqual(1, index.entries.count) 50 | } 51 | 52 | func testCompressDecompress() { 53 | try! Data("hello world".utf8).write(to: url.appendingPathComponent("myfile.txt")) 54 | let press = Press() 55 | XCTAssertEqual("hello world", String(decoding: press.decompress(press.compress( 56 | try! Data(contentsOf: url.appendingPathComponent("myfile.txt")))), as: UTF8.self)) 57 | } 58 | 59 | func testNonExistingFile() { 60 | let file = url.appendingPathComponent("myfile.txt") 61 | let index = Index(url) ?? Index() 62 | XCTAssertThrowsError(try repository.stage.add(file, index: index)) 63 | } 64 | 65 | func testOutsideProject() { 66 | let file = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("myfile.txt") 67 | try! Data("hello world".utf8).write(to: file) 68 | let index = Index(url) ?? Index() 69 | XCTAssertThrowsError(try repository.stage.add(file, index: index)) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tests/TestCheckout.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestCheckout: XCTestCase { 5 | private var url: URL! 6 | private var file: URL! 7 | private var rest: MockRest! 8 | 9 | override func setUp() { 10 | rest = MockRest() 11 | Hub.session = Session() 12 | Hub.session.name = "hello" 13 | Hub.session.email = "world" 14 | Hub.factory.rest = rest 15 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 16 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 17 | file = url.appendingPathComponent("myfile.txt") 18 | try! Data("hello world\n".utf8).write(to: file) 19 | } 20 | 21 | override func tearDown() { 22 | try! FileManager.default.removeItem(at: url) 23 | } 24 | 25 | func testTwoCommits() { 26 | let expect = expectation(description: "") 27 | var repository: Repository! 28 | Hub.create(url) { 29 | repository = $0 30 | repository.commit([self.file], message: "hello world") { 31 | let first = try! Hub.head.id(self.url) 32 | try! Data("lorem ipsum\n".utf8).write(to: self.file) 33 | repository.commit([self.file], message: "lorem ipsum") { 34 | let second = try! Hub.head.id(self.url) 35 | XCTAssertNotEqual(first, second) 36 | XCTAssertEqual("lorem ipsum\n", String(decoding: try! Data(contentsOf: self.file), as: UTF8.self)) 37 | DispatchQueue.global(qos: .background).async { 38 | repository.check(first) { 39 | XCTAssertEqual(.main, Thread.current) 40 | XCTAssertEqual("hello world\n", String(decoding: try! Data(contentsOf: self.file), as: UTF8.self)) 41 | XCTAssertEqual(first, try! Hub.head.id(self.url)) 42 | expect.fulfill() 43 | } 44 | } 45 | } 46 | } 47 | } 48 | waitForExpectations(timeout: 1) 49 | } 50 | 51 | func testThreeCommits() { 52 | let expect = expectation(description: "") 53 | let secondfile = url.appendingPathComponent("secondfile.txt") 54 | let thirdfile = url.appendingPathComponent("thirdfile.txt") 55 | var repository: Repository! 56 | Hub.create(url) { 57 | repository = $0 58 | repository.commit([self.file], message: "hello world") { 59 | let first = try! Hub.head.id(self.url) 60 | try! Data("this is a second file\n".utf8).write(to: secondfile) 61 | repository.commit([secondfile], message: "lorem ipsum 2") { 62 | try! Data("this is a third file\n".utf8).write(to: thirdfile) 63 | try! Data("a modified hello world\n".utf8).write(to: self.file) 64 | repository.commit([self.file, thirdfile], message: "lorem ipsum 3") { 65 | repository.check(first) { 66 | XCTAssertEqual("hello world\n", String(decoding: try! Data(contentsOf: self.file), as: UTF8.self)) 67 | XCTAssertFalse(FileManager.default.fileExists(atPath: secondfile.path)) 68 | XCTAssertFalse(FileManager.default.fileExists(atPath: thirdfile.path)) 69 | expect.fulfill() 70 | } 71 | } 72 | } 73 | } 74 | } 75 | waitForExpectations(timeout: 1) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Tests/TestConfig.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestConfig: XCTestCase { 5 | private var url: URL! 6 | 7 | override func setUp() { 8 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 9 | try! FileManager.default.createDirectory(at: url.appendingPathComponent(".git/"), withIntermediateDirectories: true) 10 | } 11 | 12 | override func tearDown() { 13 | try! FileManager.default.removeItem(at: url) 14 | } 15 | 16 | func testInvalid() { 17 | XCTAssertThrowsError(try Config(url)) 18 | } 19 | 20 | func testParse() { 21 | try? Data(contentsOf: Bundle(for: TestConfig.self).url(forResource: "config0", withExtension: nil)!).write(to: url.appendingPathComponent(".git/config")) 22 | let config = try? Config(url) 23 | XCTAssertEqual(1, config?.remote.count) 24 | XCTAssertEqual(1, config?.branch.count) 25 | XCTAssertEqual("origin", config?.remote.first?.0) 26 | XCTAssertEqual("https://github.com/vauxhall/merge.git", config?.remote.first?.1.url) 27 | XCTAssertEqual("+refs/heads/*:refs/remotes/origin/*", config?.remote.first?.1.fetch) 28 | XCTAssertEqual("master", config?.branch.first?.0) 29 | XCTAssertEqual("origin", config?.branch.first?.1.remote) 30 | XCTAssertEqual("refs/heads/master", config?.branch.first?.1.merge) 31 | XCTAssertEqual(""" 32 | [remote "origin"] 33 | url = https://github.com/vauxhall/merge.git 34 | fetch = +refs/heads/*:refs/remotes/origin/* 35 | [branch "master"] 36 | remote = origin 37 | merge = refs/heads/master 38 | 39 | """, config?.serial) 40 | } 41 | 42 | func testSave() { 43 | try? Config("lorem ipsum").save(url) 44 | XCTAssertEqual(""" 45 | [remote "origin"] 46 | url = https://lorem ipsum 47 | fetch = +refs/heads/*:refs/remotes/origin/* 48 | [branch "master"] 49 | remote = origin 50 | merge = refs/heads/master 51 | 52 | """, String(decoding: (try? Data(contentsOf: url.appendingPathComponent(".git/config"))) ?? Data(), as: UTF8.self)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/TestContents.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestContents: XCTestCase { 5 | private var repository: Repository! 6 | private var url: URL! 7 | 8 | override func setUp() { 9 | Hub.session = Session() 10 | Hub.factory.rest = MockRest() 11 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 12 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 13 | } 14 | 15 | override func tearDown() { 16 | try! FileManager.default.removeItem(at: url) 17 | } 18 | 19 | func testInitial() { 20 | let expect = expectation(description: "") 21 | Hub.create(url) { 22 | self.repository = $0 23 | XCTAssertTrue($0.state.needs) 24 | expect.fulfill() 25 | } 26 | waitForExpectations(timeout: 1) 27 | } 28 | 29 | func testAfterStatus() { 30 | let expect = expectation(description: "") 31 | Hub.create(url) { 32 | _ = $0.state.list 33 | XCTAssertFalse($0.state.needs) 34 | expect.fulfill() 35 | } 36 | waitForExpectations(timeout: 1) 37 | } 38 | 39 | func testAfterEdition() { 40 | let expect = expectation(description: "") 41 | Hub.create(url) { 42 | _ = $0.state.list 43 | try! "hello\n".write(to: self.url.appendingPathComponent("file.txt"), atomically: true, encoding: .utf8) 44 | XCTAssertTrue($0.state.needs) 45 | expect.fulfill() 46 | } 47 | waitForExpectations(timeout: 1) 48 | } 49 | 50 | func testAfterContentEdition() { 51 | let expect = expectation(description: "") 52 | let file = url.appendingPathComponent("file.txt") 53 | try! "hello\n".write(to: file, atomically: true, encoding: .utf8) 54 | Hub.create(url) { 55 | _ = $0.state.list 56 | try! "world\n".write(to: file, atomically: true, encoding: .utf8) 57 | _ = $0.state.list 58 | try! "lorem ipsum\n".write(to: file, atomically: true, encoding: .utf8) 59 | XCTAssertTrue($0.state.needs) 60 | expect.fulfill() 61 | } 62 | waitForExpectations(timeout: 1) 63 | } 64 | 65 | func testAfterSubtreeEdition() { 66 | let expect = expectation(description: "") 67 | let dir = url.appendingPathComponent("adir") 68 | try! FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) 69 | let file = dir.appendingPathComponent("file.txt") 70 | try! "hello\n".write(to: file, atomically: true, encoding: .utf8) 71 | Hub.create(url) { 72 | _ = $0.state.list 73 | try! "world\n".write(to: file, atomically: true, encoding: .utf8) 74 | _ = $0.state.list 75 | try! "lorem ipsum\n".write(to: file, atomically: true, encoding: .utf8) 76 | XCTAssertTrue($0.state.needs) 77 | expect.fulfill() 78 | } 79 | waitForExpectations(timeout: 1) 80 | } 81 | 82 | func testAfterSubSubtreeEdition() { 83 | let expect = expectation(description: "") 84 | let dir = url.appendingPathComponent("adir/inside/another") 85 | try! FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) 86 | let file = dir.appendingPathComponent("file.txt") 87 | try! "hello\n".write(to: file, atomically: true, encoding: .utf8) 88 | Hub.create(url) { 89 | _ = $0.state.list 90 | try! "world\n".write(to: file, atomically: true, encoding: .utf8) 91 | _ = $0.state.list 92 | try! "lorem ipsum\n".write(to: file, atomically: true, encoding: .utf8) 93 | XCTAssertTrue($0.state.needs) 94 | expect.fulfill() 95 | } 96 | waitForExpectations(timeout: 1) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Tests/TestFetch.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestFetch: XCTestCase { 5 | func testPull() { 6 | let fetch = try? Fetch.Pull(try! Data(contentsOf: Bundle(for: TestFetch.self).url(forResource: "fetchPull0", withExtension: nil)!)) 7 | XCTAssertNotNil(fetch) 8 | XCTAssertEqual(1, fetch?.branch.count) 9 | XCTAssertEqual("54cac1e1086e2709a52d7d1727526b14efec3a77", fetch?.branch.first) 10 | } 11 | 12 | func testPush() { 13 | let fetch = try? Fetch.Push(try! Data(contentsOf: Bundle(for: TestFetch.self).url(forResource: "fetchPush0", withExtension: nil)!)) 14 | XCTAssertNotNil(fetch) 15 | XCTAssertEqual(1, fetch?.branch.count) 16 | XCTAssertEqual("21641afd04cd878a8e5d0275d25524499805569d", fetch?.branch.first) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/TestHash.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestHash: XCTestCase { 5 | private var hasher: Hash! 6 | private var url: URL! 7 | private var file: URL! 8 | 9 | override func setUp() { 10 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 11 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 12 | file = url.appendingPathComponent("file.json") 13 | try! "hello world\n".write(to: file, atomically: true, encoding: .utf8) 14 | hasher = Hash() 15 | } 16 | 17 | override func tearDown() { 18 | try! FileManager.default.removeItem(at: url) 19 | } 20 | 21 | func testFile() { 22 | XCTAssertEqual("3b18e512dba79e4c8300dd08aeb37f8e728b8dad", hasher.file(file).1) 23 | } 24 | 25 | func testTree() { 26 | XCTAssertEqual("4b825dc642cb6eb9a060e54bf8d69288fbee4904", hasher.tree(Data()).1) 27 | } 28 | 29 | func testCommit() { 30 | XCTAssertEqual("dcf5b16e76cce7425d0beaef62d79a7d10fce1f5", hasher.commit("").1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/TestHead.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestHead: XCTestCase { 5 | private var url: URL! 6 | 7 | override func setUp() { 8 | Hub.session = Session() 9 | Hub.factory.rest = MockRest() 10 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 11 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 12 | } 13 | 14 | override func tearDown() { 15 | try! FileManager.default.removeItem(at: url) 16 | } 17 | 18 | func testHEAD() { 19 | let expect = expectation(description: "") 20 | Hub.create(url) { _ in 21 | XCTAssertEqual("refs/heads/master", try? Hub.head.reference(self.url)) 22 | expect.fulfill() 23 | } 24 | waitForExpectations(timeout: 1) 25 | } 26 | 27 | func testHeadNone() { 28 | let expect = expectation(description: "") 29 | Hub.create(url) { _ in 30 | XCTAssertNil(try? Hub.head.commit(self.url)) 31 | expect.fulfill() 32 | } 33 | waitForExpectations(timeout: 1) 34 | } 35 | 36 | func testTreeNone() { 37 | let expect = expectation(description: "") 38 | Hub.create(url) { _ in 39 | XCTAssertNil(try? Hub.head.tree(self.url)) 40 | expect.fulfill() 41 | } 42 | waitForExpectations(timeout: 1) 43 | } 44 | 45 | func testLastCommit() { 46 | let date = Date(timeIntervalSinceNow: -1) 47 | let expect = expectation(description: "") 48 | let file = url.appendingPathComponent("myfile.txt") 49 | try! Data("hello world".utf8).write(to: file) 50 | var repository: Repository! 51 | Hub.create(url) { 52 | repository = $0 53 | Hub.session.name = "ab" 54 | Hub.session.email = "cd" 55 | repository.commit([file], message: "hello world\n") { 56 | let commit = try? Hub.head.commit(self.url) 57 | XCTAssertEqual("ab", commit?.author.name) 58 | XCTAssertEqual("ab", commit?.committer.name) 59 | XCTAssertEqual("cd", commit?.author.email) 60 | XCTAssertEqual("cd", commit?.committer.email) 61 | XCTAssertLessThan(date, commit!.author.date) 62 | XCTAssertLessThan(date, commit!.committer.date) 63 | XCTAssertEqual("hello world\n", commit?.message) 64 | XCTAssertEqual("007a8ffce38213667b95957dc505ef30dac0248d", commit?.tree) 65 | expect.fulfill() 66 | } 67 | } 68 | waitForExpectations(timeout: 1) 69 | } 70 | 71 | func testTreeAfterCommit() { 72 | let expect = expectation(description: "") 73 | let file = url.appendingPathComponent("myfile.txt") 74 | try! Data("hello world".utf8).write(to: file) 75 | var repository: Repository! 76 | Hub.create(url) { 77 | repository = $0 78 | Hub.session.name = "ab" 79 | Hub.session.email = "cd" 80 | repository.commit([file], message: "hello world") { 81 | let tree = try? Hub.head.tree(self.url) 82 | XCTAssertEqual(1, tree?.items.count) 83 | XCTAssertEqual(.blob, tree?.items.first?.category) 84 | XCTAssertEqual(file, tree?.items.first?.url) 85 | XCTAssertEqual("95d09f2b10159347eece71399a7e2e907ea3df4f", tree?.items.first?.id) 86 | expect.fulfill() 87 | } 88 | } 89 | waitForExpectations(timeout: 1) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/TestIgnore.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestIgnore: XCTestCase { 5 | private var url: URL! 6 | private var ignore: Ignore! 7 | 8 | override func setUp() { 9 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 10 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 11 | try! """ 12 | .DS_Store 13 | *.xcuserstate 14 | .dSYM* 15 | /something 16 | Pods/ 17 | /More/ 18 | weird 19 | 20 | """.write(to: url.appendingPathComponent(".gitignore"), atomically: true, encoding: .utf8) 21 | ignore = Ignore(url) 22 | } 23 | 24 | override func tearDown() { 25 | try! FileManager.default.removeItem(at: url) 26 | } 27 | 28 | func testAccept() { 29 | XCTAssertFalse(ignore.url(url)) 30 | XCTAssertFalse(ignore.url(url.appendingPathComponent("afile.txt"))) 31 | } 32 | 33 | func testGit() { 34 | XCTAssertFalse(ignore.url(url.appendingPathComponent("test.git"))) 35 | XCTAssertFalse(ignore.url(url.appendingPathComponent("git"))) 36 | XCTAssertFalse(ignore.url(url.appendingPathComponent(".gito"))) 37 | XCTAssertFalse(ignore.url(url.appendingPathComponent(".gitignore"))) 38 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".git"))) 39 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".git/HEAD"))) 40 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".git/some/other/thing"))) 41 | } 42 | 43 | func testExplicit() { 44 | XCTAssertFalse(ignore.url(url.appendingPathComponent(".DS_Storea"))) 45 | XCTAssertFalse(ignore.url(url.appendingPathComponent("DS_Store"))) 46 | XCTAssertFalse(ignore.url(url.appendingPathComponent("world.DS_Store"))) 47 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".DS_Store"))) 48 | XCTAssertTrue(ignore.url(url.appendingPathComponent("hello/.DS_Store"))) 49 | XCTAssertTrue(ignore.url(url.appendingPathComponent("something"))) 50 | } 51 | 52 | func testFolders() { 53 | XCTAssertFalse(ignore.url(url.appendingPathComponent("Any/other"))) 54 | XCTAssertTrue(ignore.url(url.appendingPathComponent("Any", isDirectory: true))) 55 | XCTAssertTrue(ignore.url(url.appendingPathComponent("Other", isDirectory: true))) 56 | XCTAssertTrue(ignore.url(url.appendingPathComponent("Any/Other", isDirectory: true))) 57 | } 58 | 59 | func testFolderContents() { 60 | XCTAssertFalse(ignore.url(url.appendingPathComponent("aPods/thing"))) 61 | XCTAssertTrue(ignore.url(url.appendingPathComponent("Pods/thing"))) 62 | XCTAssertTrue(ignore.url(url.appendingPathComponent("More/thing"))) 63 | XCTAssertTrue(ignore.url(url.appendingPathComponent("a/weird/thing"))) 64 | } 65 | 66 | func testPrefixStar() { 67 | XCTAssertFalse(ignore.url(url.appendingPathComponent(".xcuserstatea"))) 68 | XCTAssertTrue(ignore.url(url.appendingPathComponent("hallo.xcuserstate"))) 69 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".xcuserstate"))) 70 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".xcuserstate/a"))) 71 | XCTAssertTrue(ignore.url(url.appendingPathComponent("hello/world/.xcuserstate"))) 72 | } 73 | 74 | func testSuffixStar() { 75 | XCTAssertFalse(ignore.url(url.appendingPathComponent("a.dSYM"))) 76 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".dSYM.zip"))) 77 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".dSYM"))) 78 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".dSYM/addas"))) 79 | XCTAssertTrue(ignore.url(url.appendingPathComponent(".dSYM/x/y/z"))) 80 | XCTAssertTrue(ignore.url(url.appendingPathComponent("asdsa/.dSYM"))) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/TestLog.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestLog: XCTestCase { 5 | private var url: URL! 6 | private var file: URL! 7 | private var repository: Repository! 8 | 9 | override func setUp() { 10 | Hub.session = Session() 11 | Hub.factory.rest = MockRest() 12 | Hub.session.name = "hello" 13 | Hub.session.email = "my@email.com" 14 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 15 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 16 | file = url.appendingPathComponent("myfile.txt") 17 | try! Data("hello world\n".utf8).write(to: file) 18 | } 19 | 20 | override func tearDown() { 21 | try! FileManager.default.removeItem(at: url) 22 | } 23 | 24 | func testOneCommit() { 25 | let expect = expectation(description: "") 26 | Hub.create(url) { 27 | self.repository = $0 28 | self.repository.commit([self.file], message: "Lorem ipsum\n") { 29 | DispatchQueue.global(qos: .background).async { 30 | self.repository.log { 31 | XCTAssertEqual(1, $0.count) 32 | XCTAssertEqual("hello", $0.first?.author.name) 33 | XCTAssertEqual("hello", $0.first?.committer.name) 34 | XCTAssertEqual("my@email.com", $0.first?.author.email) 35 | XCTAssertEqual("my@email.com", $0.first?.committer.email) 36 | XCTAssertEqual("Lorem ipsum\n", $0.first?.message) 37 | XCTAssertEqual("84b5f2f96994db6b67f8a0ee508b1ebb8b633c15", $0.first?.tree) 38 | XCTAssertNil($0.first?.parent.first) 39 | XCTAssertEqual(.main, Thread.current) 40 | expect.fulfill() 41 | } 42 | } 43 | } 44 | } 45 | waitForExpectations(timeout: 1) 46 | } 47 | 48 | func testTwoCommits() { 49 | let expect = expectation(description: "") 50 | Hub.create(url) { 51 | self.repository = $0 52 | self.repository.commit([self.file], message: "Lorem ipsum\n") { 53 | try! Data("lorem ipsum\n".utf8).write(to: self.file) 54 | self.repository.commit([self.file], message: "The rebels, the misfits\n") { 55 | self.repository.log { 56 | XCTAssertEqual(2, $0.count) 57 | XCTAssertEqual("The rebels, the misfits\n", $0.first?.message) 58 | XCTAssertEqual("a9b8f695fe7d66da97114df1c3a14df9070d2eae", $0.first?.tree) 59 | XCTAssertNotNil($0.first?.parent) 60 | XCTAssertEqual("Lorem ipsum\n", $0.last?.message) 61 | XCTAssertEqual("84b5f2f96994db6b67f8a0ee508b1ebb8b633c15", $0.last?.tree) 62 | XCTAssertNil($0.last?.parent.first) 63 | expect.fulfill() 64 | } 65 | } 66 | } 67 | } 68 | waitForExpectations(timeout: 1) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/TestMerge.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestMerge: XCTestCase { 5 | private var url: URL! 6 | private var rest: MockRest! 7 | 8 | override func setUp() { 9 | rest = MockRest() 10 | Hub.session = Session() 11 | Hub.factory.rest = MockRest() 12 | Hub.factory.rest = rest 13 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 14 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 15 | Hub.session.name = "hello" 16 | Hub.session.email = "world" 17 | } 18 | 19 | override func tearDown() { 20 | try! FileManager.default.removeItem(at: url) 21 | } 22 | 23 | func testMerging() { 24 | let expect = expectation(description: "") 25 | var repository: Repository! 26 | Hub.create(url) { 27 | repository = $0 28 | let file1 = self.url.appendingPathComponent("file1.txt") 29 | try! Data("hello world\n".utf8).write(to: file1) 30 | repository.commit([file1], message: "First commit.\n") { 31 | let first = try! Hub.head.id(self.url) 32 | let file2 = self.url.appendingPathComponent("file2.txt") 33 | try! Data("lorem ipsum\n".utf8).write(to: file2) 34 | repository.commit([file2], message: "Second commit.\n") { 35 | let second = try! Hub.head.id(self.url) 36 | try? repository.stage.merge(first) 37 | let merged = try! Hub.head.commit(self.url) 38 | XCTAssertEqual(second, merged.parent.first) 39 | XCTAssertEqual(first, merged.parent.last) 40 | XCTAssertEqual("Merge.\n", merged.message) 41 | XCTAssertEqual(3, try? History(self.url).result.count) 42 | expect.fulfill() 43 | } 44 | } 45 | } 46 | waitForExpectations(timeout: 1) 47 | } 48 | 49 | func testSynch() { 50 | let expect = expectation(description: "") 51 | var repository: Repository! 52 | let fetch = Fetch() 53 | fetch.branch.append("335a33ae387dc24f057852fdb92e5abc71bf6b85") 54 | rest._fetch = fetch 55 | rest._pull = try? Pack(Data(contentsOf: Bundle(for: TestPull.self).url(forResource: "fetch2", withExtension: nil)!)) 56 | Hub.create(url) { 57 | repository = $0 58 | try? Config("lorem ipsum").save(self.url) 59 | repository.pull { 60 | let file = self.url.appendingPathComponent("control.txt") 61 | try! Data("hello world\n".utf8).write(to: file) 62 | repository.commit([file], message: "First commit") { 63 | repository.pull { 64 | repository.push { 65 | XCTAssertEqual(Hub.head.origin(self.url)!, try? Hub.head.id(self.url)) 66 | expect.fulfill() 67 | } 68 | } 69 | } 70 | } 71 | } 72 | waitForExpectations(timeout: 1) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/TestRefresh.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestRefresh: XCTestCase { 5 | private var url: URL! 6 | 7 | override func setUp() { 8 | Hub.session = Session() 9 | Hub.factory.rest = MockRest() 10 | Hub.session.name = "hello" 11 | Hub.session.email = "world" 12 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 13 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 14 | } 15 | 16 | override func tearDown() { 17 | try! FileManager.default.removeItem(at: url) 18 | } 19 | 20 | func testAfterCommit() { 21 | let expect = expectation(description: "") 22 | var repository: Repository! 23 | Hub.create(url) { 24 | repository = $0 25 | let file = self.url.appendingPathComponent("myfile.txt") 26 | try! Data("hello world\n".utf8).write(to: file) 27 | repository.status = { _ in 28 | expect.fulfill() 29 | } 30 | repository.state.delta = 0 31 | repository.commit([file], message: "hello world\n") 32 | } 33 | waitForExpectations(timeout: 1) 34 | } 35 | 36 | func testAfterReset() { 37 | let expect = expectation(description: "") 38 | var repository: Repository! 39 | Hub.create(url) { 40 | repository = $0 41 | let file = self.url.appendingPathComponent("myfile.txt") 42 | try! Data("hello world\n".utf8).write(to: file) 43 | repository.commit([file], message: "My first commit\n") { 44 | repository.status = { _ in 45 | expect.fulfill() 46 | } 47 | repository.state.delta = 0 48 | repository.reset() 49 | } 50 | } 51 | waitForExpectations(timeout: 1) 52 | } 53 | 54 | func testAfterUnpack() { 55 | let expect = expectation(description: "") 56 | var repository: Repository! 57 | Hub.create(url) { 58 | repository = $0 59 | repository.status = { _ in 60 | expect.fulfill() 61 | } 62 | repository.state.delta = 0 63 | repository.unpack() 64 | } 65 | waitForExpectations(timeout: 1) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Tests/TestRepository.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestRepository: XCTestCase { 5 | private var url: URL! 6 | 7 | override func setUp() { 8 | Hub.session = Session() 9 | Hub.factory.rest = MockRest() 10 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 11 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 12 | } 13 | 14 | override func tearDown() { 15 | try! FileManager.default.removeItem(at: url) 16 | } 17 | 18 | func testRefresh() { 19 | let repository = Repository(URL(fileURLWithPath: "")) 20 | repository.state.last = Date() 21 | repository.refresh() 22 | XCTAssertEqual(Date.distantPast, repository.state.last) 23 | } 24 | 25 | func testBranch() { 26 | let expect = expectation(description: "") 27 | var repository: Repository! 28 | Hub.create(url) { 29 | repository = $0 30 | DispatchQueue.global(qos: .background).async { 31 | repository.branch { 32 | XCTAssertEqual(.main, Thread.current) 33 | XCTAssertEqual("master", $0) 34 | expect.fulfill() 35 | } 36 | } 37 | } 38 | waitForExpectations(timeout: 1) 39 | } 40 | 41 | func testRemoteNone() { 42 | let expect = expectation(description: "") 43 | var repository: Repository! 44 | Hub.create(url) { 45 | repository = $0 46 | DispatchQueue.global(qos: .background).async { 47 | repository.remote { 48 | XCTAssertEqual(.main, Thread.current) 49 | XCTAssertEqual("", $0) 50 | expect.fulfill() 51 | } 52 | } 53 | } 54 | waitForExpectations(timeout: 1) 55 | } 56 | 57 | func testRemote() { 58 | let expect = expectation(description: "") 59 | var repository: Repository! 60 | Hub.create(url) { 61 | repository = $0 62 | try? Config("hello world").save(self.url) 63 | repository.remote { 64 | XCTAssertEqual(.main, Thread.current) 65 | XCTAssertEqual(""" 66 | [remote "origin"] 67 | url = https://hello world 68 | fetch = +refs/heads/*:refs/remotes/origin/* 69 | [branch "master"] 70 | remote = origin 71 | merge = refs/heads/master 72 | 73 | """, String(decoding: try! Data(contentsOf: self.url.appendingPathComponent(".git/config")), as: UTF8.self)) 74 | XCTAssertEqual("hello world", $0) 75 | expect.fulfill() 76 | } 77 | } 78 | waitForExpectations(timeout: 1) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/TestRest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestRest: XCTestCase { 5 | func testEmpty() { 6 | XCTAssertThrowsError(try Rest().url("", suffix: "")) 7 | } 8 | 9 | func testSuccess() { 10 | XCTAssertNoThrow(try Rest().url("github.com/some/repository.git", suffix: "")) 11 | } 12 | 13 | func testProtocol() { 14 | XCTAssertThrowsError(try Rest().url("https://github.com/some/repository.git", suffix: "")) 15 | XCTAssertThrowsError(try Rest().url("http://github.com/some/repository.git", suffix: "")) 16 | } 17 | 18 | func testEnding() { 19 | XCTAssertThrowsError(try Rest().url("github.com/some/repository.git/", suffix: "")) 20 | XCTAssertThrowsError(try Rest().url("github.com/some/repository", suffix: "")) 21 | } 22 | 23 | func testValidity() { 24 | let url = (try? Rest().url("github.com/some/repository.git", suffix: "")) ?? URL(fileURLWithPath: "") 25 | XCTAssertEqual("https://github.com/some/repository.git", url.absoluteString) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/TestRestoreIndex.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestRestoreIndex: XCTestCase { 5 | private var url: URL! 6 | 7 | override func setUp() { 8 | Hub.session = Session() 9 | Hub.factory.rest = MockRest() 10 | Hub.session.name = "hello" 11 | Hub.session.email = "world" 12 | url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) 13 | try! FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) 14 | } 15 | 16 | override func tearDown() { 17 | try! FileManager.default.removeItem(at: url) 18 | } 19 | 20 | func testAfterReset() { 21 | let expect = expectation(description: "") 22 | var repository: Repository! 23 | Hub.create(url) { 24 | repository = $0 25 | let file = self.url.appendingPathComponent("myfile.txt") 26 | try! Data("hello world\n".utf8).write(to: file) 27 | repository.commit([file], message: "hello world\n") { 28 | var index = Index(self.url) 29 | let id = index?.id 30 | XCTAssertEqual(40, id?.count) 31 | XCTAssertEqual(1, index?.entries.count) 32 | XCTAssertEqual("3b18e512dba79e4c8300dd08aeb37f8e728b8dad", index?.entries.first?.id) 33 | try! FileManager.default.removeItem(at: self.url.appendingPathComponent(".git/index")) 34 | XCTAssertFalse(FileManager.default.fileExists(atPath: self.url.appendingPathComponent(".git/index").path)) 35 | repository.reset { 36 | XCTAssertTrue(FileManager.default.fileExists(atPath: self.url.appendingPathComponent(".git/index").path)) 37 | index = Index(self.url) 38 | XCTAssertEqual(id, index?.id) 39 | XCTAssertEqual(1, index?.entries.count) 40 | XCTAssertEqual("3b18e512dba79e4c8300dd08aeb37f8e728b8dad", index?.entries.first?.id) 41 | expect.fulfill() 42 | } 43 | } 44 | } 45 | waitForExpectations(timeout: 1) 46 | } 47 | 48 | func testAfterUnpack() { 49 | let expect = expectation(description: "") 50 | var repository: Repository! 51 | Hub.create(url) { 52 | repository = $0 53 | let file = self.url.appendingPathComponent("myfile.txt") 54 | try! Data("hello world\n".utf8).write(to: file) 55 | repository.commit([file], message: "hello world\n") { 56 | var index = Index(self.url) 57 | let id = index?.id 58 | XCTAssertEqual(40, id?.count) 59 | XCTAssertEqual(1, index?.entries.count) 60 | XCTAssertEqual("3b18e512dba79e4c8300dd08aeb37f8e728b8dad", index?.entries.first?.id) 61 | try! FileManager.default.removeItem(at: self.url.appendingPathComponent(".git/index")) 62 | XCTAssertFalse(FileManager.default.fileExists(atPath: self.url.appendingPathComponent(".git/index").path)) 63 | repository.unpack { 64 | XCTAssertTrue(FileManager.default.fileExists(atPath: self.url.appendingPathComponent(".git/index").path)) 65 | index = Index(self.url) 66 | XCTAssertEqual(id, index?.id) 67 | XCTAssertEqual(1, index?.entries.count) 68 | XCTAssertEqual("3b18e512dba79e4c8300dd08aeb37f8e728b8dad", index?.entries.first?.id) 69 | expect.fulfill() 70 | } 71 | } 72 | } 73 | waitForExpectations(timeout: 1) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/TestSession.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestSession: XCTestCase { 5 | override func setUp() { 6 | Hub.session = Session() 7 | Hub.factory.rest = MockRest() 8 | UserDefaults.standard.removeObject(forKey: "session") 9 | } 10 | 11 | override func tearDown() { 12 | UserDefaults.standard.removeObject(forKey: "session") 13 | } 14 | 15 | func testLoadFromGit() { 16 | let expect = expectation(description: "") 17 | XCTAssertTrue(Hub.session.email.isEmpty) 18 | XCTAssertTrue(Hub.session.name.isEmpty) 19 | let data = "hasher\n".data(using: .utf8)! 20 | let url = URL(fileURLWithPath: "hello/world") 21 | let session = Session() 22 | session.name = "lorem ipsum" 23 | session.email = "lorem@world.com" 24 | session.user = "pablo@mousaka.com" 25 | session.bookmark = data 26 | session.url = url 27 | session.save() 28 | Hub.session.load { 29 | XCTAssertEqual("lorem ipsum", Hub.session.name) 30 | XCTAssertEqual("lorem@world.com", Hub.session.email) 31 | XCTAssertEqual("pablo@mousaka.com", Hub.session.user) 32 | XCTAssertEqual(data, Hub.session.bookmark) 33 | XCTAssertEqual(url.path, Hub.session.url.path) 34 | XCTAssertEqual(.main, Thread.current) 35 | expect.fulfill() 36 | } 37 | waitForExpectations(timeout: 1) 38 | } 39 | 40 | func testUpdateName() { 41 | let expect = expectation(description: "") 42 | XCTAssertTrue(Hub.session.email.isEmpty) 43 | XCTAssertTrue(Hub.session.name.isEmpty) 44 | Hub.session.update("pablo", email: "mousaka@mail.com") { 45 | Hub.session.name = "" 46 | Hub.session.email = "" 47 | Hub.session.load { 48 | XCTAssertEqual("pablo", Hub.session.name) 49 | XCTAssertEqual("mousaka@mail.com", Hub.session.email) 50 | expect.fulfill() 51 | } 52 | } 53 | waitForExpectations(timeout: 1) 54 | } 55 | 56 | func testUpdateUrl() { 57 | let expect = expectation(description: "") 58 | XCTAssertTrue(Hub.session.email.isEmpty) 59 | XCTAssertTrue(Hub.session.name.isEmpty) 60 | let data = "hasher\n".data(using: .utf8)! 61 | let url = URL(fileURLWithPath: "hello/world") 62 | Hub.session.update(url, bookmark: data) { 63 | Hub.session.url = URL(fileURLWithPath: "") 64 | Hub.session.bookmark = Data() 65 | Hub.session.load { 66 | XCTAssertEqual(data, Hub.session.bookmark) 67 | XCTAssertEqual(url.path, Hub.session.url.path) 68 | expect.fulfill() 69 | } 70 | } 71 | waitForExpectations(timeout: 1) 72 | } 73 | 74 | func testPurchase() { 75 | let expect = expectation(description: "") 76 | XCTAssertTrue(Hub.session.purchase.isEmpty) 77 | DispatchQueue.global(qos: .background).async { 78 | Hub.session.purchase("hello.cloud") { 79 | XCTAssertEqual(.main, Thread.current) 80 | Hub.session.purchase = [] 81 | Hub.session.load { 82 | XCTAssertEqual(.cloud, Hub.session.purchase.first) 83 | expect.fulfill() 84 | } 85 | } 86 | } 87 | waitForExpectations(timeout: 1) 88 | } 89 | 90 | func testPurchaseAgain() { 91 | let expect = expectation(description: "") 92 | XCTAssertTrue(Hub.session.purchase.isEmpty) 93 | Hub.session.purchase("hello.cloud") { 94 | Hub.session.purchase("hello.cloud") { 95 | Hub.session.load { 96 | XCTAssertEqual(1, Hub.session.purchase.count) 97 | expect.fulfill() 98 | } 99 | } 100 | } 101 | waitForExpectations(timeout: 1) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/TestUser.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Git 3 | 4 | class TestUser: XCTestCase { 5 | override func setUp() { 6 | Hub.session = Session() 7 | Hub.factory.rest = MockRest() 8 | } 9 | 10 | func testNonEmpty() { 11 | update("", email: "") 12 | update("", email: "test@mail.com") 13 | update("test", email: "") 14 | } 15 | 16 | func testCommitCharacters() { 17 | update("hello", email: "test<@mail.com") 18 | update("hello", email: "test>@mail.com") 19 | update("hello", email: "test@mail.com") 21 | update("hello", email: "test@mail.com\n") 22 | update("hello\n", email: "test@mail.com") 23 | update("hello", email: "test@mail.com\t") 24 | update("hello\t", email: "test@mail.com") 25 | } 26 | 27 | func testAt() { 28 | update("test", email: "testmail.com") 29 | update("test", email: "test@@mail.com") 30 | update("test", email: "@mail.com") 31 | } 32 | 33 | func testDot() { 34 | update("test", email: "test@mailcom") 35 | update("test", email: "test@mailcom.") 36 | update("test", email: "test@.mailcom") 37 | } 38 | 39 | func testWeird() { 40 | update("test", email: "test@ mail.com") 41 | update("test", email: "test @mail.com") 42 | update("test", email: "te st@mail.com") 43 | update("test", email: " test@mail.com") 44 | update("test", email: "test@mail.com ") 45 | } 46 | 47 | private func update(_ user: String, email: String) { 48 | let expect = expectation(description: "") 49 | Hub.session.update(user, email: email, error: { _ in 50 | expect.fulfill() 51 | }) { XCTFail() } 52 | waitForExpectations(timeout: 1) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/Tests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/blob0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/blob0 -------------------------------------------------------------------------------- /Tests/commit0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/commit0 -------------------------------------------------------------------------------- /Tests/commit1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/commit1 -------------------------------------------------------------------------------- /Tests/commit2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/commit2 -------------------------------------------------------------------------------- /Tests/commit3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/commit3 -------------------------------------------------------------------------------- /Tests/compressed0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/compressed0 -------------------------------------------------------------------------------- /Tests/config0: -------------------------------------------------------------------------------- 1 | [core] 2 | repositoryformatversion = 0 3 | filemode = true 4 | bare = false 5 | logallrefupdates = true 6 | ignorecase = true 7 | precomposeunicode = true 8 | [remote "origin"] 9 | url = https://github.com/vauxhall/merge.git 10 | fetch = +refs/heads/*:refs/remotes/origin/* 11 | [branch "master"] 12 | remote = origin 13 | merge = refs/heads/master 14 | -------------------------------------------------------------------------------- /Tests/fetch0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/fetch0 -------------------------------------------------------------------------------- /Tests/fetch1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/fetch1 -------------------------------------------------------------------------------- /Tests/fetch2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/fetch2 -------------------------------------------------------------------------------- /Tests/fetch3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/fetch3 -------------------------------------------------------------------------------- /Tests/fetchPull0: -------------------------------------------------------------------------------- 1 | 001e# service=git-upload-pack 2 | 0000010854cac1e1086e2709a52d7d1727526b14efec3a77 HEADmulti_ack thin-pack side-band side-band-64k ofs-delta shallow deepen-since deepen-not deepen-relative no-progress include-tag multi_ack_detailed no-done symref=HEAD:refs/heads/master agent=git/github-g14823918a5db 3 | 003f54cac1e1086e2709a52d7d1727526b14efec3a77 refs/heads/master 4 | 0000 -------------------------------------------------------------------------------- /Tests/fetchPush0: -------------------------------------------------------------------------------- 1 | 001f# service=git-receive-pack 2 | 0000009d21641afd04cd878a8e5d0275d25524499805569d refs/heads/masterreport-status delete-refs side-band-64k quiet atomic ofs-delta agent=git/github-g7965902fb167 3 | 0000 -------------------------------------------------------------------------------- /Tests/index0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/index0 -------------------------------------------------------------------------------- /Tests/index1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/index1 -------------------------------------------------------------------------------- /Tests/index2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/index2 -------------------------------------------------------------------------------- /Tests/index3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/index3 -------------------------------------------------------------------------------- /Tests/index4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/index4 -------------------------------------------------------------------------------- /Tests/pack-0.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-0.idx -------------------------------------------------------------------------------- /Tests/pack-0.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-0.pack -------------------------------------------------------------------------------- /Tests/pack-1.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-1.idx -------------------------------------------------------------------------------- /Tests/pack-1.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-1.pack -------------------------------------------------------------------------------- /Tests/pack-2.idx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-2.idx -------------------------------------------------------------------------------- /Tests/pack-2.pack: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/pack-2.pack -------------------------------------------------------------------------------- /Tests/packed-refs0: -------------------------------------------------------------------------------- 1 | # pack-refs with: peeled fully-peeled sorted 2 | 335a33ae387dc24f057852fdb92e5abc71bf6b85 refs/heads/master 3 | 335a33ae387dc24f057852fdb92e5abc71bf6b85 refs/remotes/origin/master 4 | -------------------------------------------------------------------------------- /Tests/tree0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/tree0 -------------------------------------------------------------------------------- /Tests/tree1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/tree1 -------------------------------------------------------------------------------- /Tests/tree2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/tree2 -------------------------------------------------------------------------------- /Tests/tree3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/tree3 -------------------------------------------------------------------------------- /Tests/tree4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/Tests/tree4 -------------------------------------------------------------------------------- /iOS/Alert.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Alert: UIControl { 4 | private weak var bottom: NSLayoutConstraint! 5 | 6 | required init?(coder: NSCoder) { return nil } 7 | @discardableResult init(_ title: String? = nil, message: String) { 8 | super.init(frame: .zero) 9 | translatesAutoresizingMaskIntoConstraints = false 10 | backgroundColor = UIColor.halo.withAlphaComponent(0.96) 11 | layer.cornerRadius = 6 12 | layer.borderColor = UIColor.black.cgColor 13 | layer.borderWidth = 1 14 | alpha = 0 15 | 16 | let label = UILabel() 17 | label.translatesAutoresizingMaskIntoConstraints = false 18 | label.attributedText = { 19 | if let title = title { 20 | $0.append(NSAttributedString(string: title + "\n", attributes: [.font: UIFont.systemFont(ofSize: 13, weight: .bold)])) 21 | } 22 | $0.append(NSAttributedString(string: message, attributes: [.font: UIFont.systemFont(ofSize: 13, weight: .regular)])) 23 | return $0 24 | } (NSMutableAttributedString()) 25 | label.numberOfLines = 0 26 | label.textColor = .black 27 | addSubview(label) 28 | 29 | app.view.addSubview(self) 30 | 31 | widthAnchor.constraint(equalToConstant: 340).isActive = true 32 | centerXAnchor.constraint(equalTo: app.view.centerXAnchor).isActive = true 33 | bottom = bottomAnchor.constraint(equalTo: app.view.topAnchor) 34 | bottom.isActive = true 35 | 36 | label.topAnchor.constraint(equalTo: topAnchor, constant: 20).isActive = true 37 | label.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20).isActive = true 38 | label.leftAnchor.constraint(equalTo: leftAnchor, constant: 20).isActive = true 39 | label.rightAnchor.constraint(equalTo: rightAnchor, constant: -20).isActive = true 40 | app.view.layoutIfNeeded() 41 | bottom.constant = 50 + bounds.height 42 | 43 | UIView.animate(withDuration: 0.35) { [weak self] in 44 | self?.alpha = 1 45 | app.view.layoutIfNeeded() 46 | } 47 | DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [weak self] in self?.dismiss() } 48 | addTarget(self, action: #selector(dismiss), for: .touchUpInside) 49 | } 50 | 51 | override var isHighlighted: Bool { didSet { hover() } } 52 | override var isSelected: Bool { didSet { hover() } } 53 | private func hover() { alpha = isSelected || isHighlighted ? 0.4 : 1 } 54 | 55 | @objc private func dismiss() { 56 | bottom.constant = 0 57 | UIView.animate(withDuration: 0.3, animations: { [weak self] in 58 | self?.alpha = 0 59 | app.view.layoutIfNeeded() 60 | }, completion: { [weak self] _ in self?.removeFromSuperview() }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /iOS/Button.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Button: UIButton { 4 | final class Yes: Button { 5 | required init?(coder: NSCoder) { return nil } 6 | override init(_ title: String) { 7 | super.init(title) 8 | setTitleColor(.black, for: .normal) 9 | setTitleColor(.init(white: 1, alpha: 0.2), for: .highlighted) 10 | layer.cornerRadius = 4 11 | backgroundColor = .halo 12 | } 13 | } 14 | 15 | final class No: Button { 16 | required init?(coder: NSCoder) { return nil } 17 | override init(_ title: String) { 18 | super.init(title) 19 | setTitleColor(.halo, for: .normal) 20 | setTitleColor(UIColor.halo.withAlphaComponent(0.2), for: .highlighted) 21 | } 22 | } 23 | 24 | required init?(coder: NSCoder) { return nil } 25 | fileprivate init(_ title: String) { 26 | super.init(frame: .zero) 27 | translatesAutoresizingMaskIntoConstraints = false 28 | titleLabel!.font = .systemFont(ofSize: 12, weight: .medium) 29 | setTitle(title, for: []) 30 | widthAnchor.constraint(equalToConstant: 90).isActive = true 31 | heightAnchor.constraint(equalToConstant: 30).isActive = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /iOS/Create.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Create: Sheet, UITextFieldDelegate { 4 | private let result: ((URL?) -> Void) 5 | private weak var name: UITextField! 6 | 7 | required init?(coder: NSCoder) { return nil } 8 | init(_ result: @escaping((URL?) -> Void)) { 9 | self.result = result 10 | super.init() 11 | let name = UITextField() 12 | name.translatesAutoresizingMaskIntoConstraints = false 13 | name.tintColor = .halo 14 | name.textColor = .white 15 | name.delegate = self 16 | name.font = .systemFont(ofSize: 18, weight: .medium) 17 | name.autocorrectionType = .no 18 | name.autocapitalizationType = .none 19 | name.spellCheckingType = .no 20 | name.clearButtonMode = .never 21 | name.keyboardAppearance = .dark 22 | name.keyboardType = .alphabet 23 | name.textAlignment = .center 24 | base.addSubview(name) 25 | self.name = name 26 | 27 | let border = UIView() 28 | border.isUserInteractionEnabled = true 29 | border.translatesAutoresizingMaskIntoConstraints = false 30 | border.backgroundColor = .halo 31 | base.addSubview(border) 32 | 33 | let create = Button.Yes(.key("Create.save")) 34 | create.addTarget(self, action: #selector(self.create), for: .touchUpInside) 35 | base.addSubview(create) 36 | 37 | let cancel = Button.No(.key("Create.cancel")) 38 | cancel.addTarget(self, action: #selector(self.cancel), for: .touchUpInside) 39 | base.addSubview(cancel) 40 | 41 | name.topAnchor.constraint(equalTo: base.topAnchor, constant: 20).isActive = true 42 | name.heightAnchor.constraint(equalToConstant: 50).isActive = true 43 | name.leftAnchor.constraint(equalTo: base.leftAnchor, constant: 20).isActive = true 44 | name.rightAnchor.constraint(equalTo: base.rightAnchor, constant: -20).isActive = true 45 | 46 | border.topAnchor.constraint(equalTo: name.bottomAnchor).isActive = true 47 | border.leftAnchor.constraint(equalTo: base.leftAnchor, constant: 20).isActive = true 48 | border.rightAnchor.constraint(equalTo: base.rightAnchor, constant: -20).isActive = true 49 | border.heightAnchor.constraint(equalToConstant: 1).isActive = true 50 | 51 | create.topAnchor.constraint(equalTo: border.bottomAnchor, constant: 20).isActive = true 52 | create.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 53 | 54 | cancel.topAnchor.constraint(equalTo: create.bottomAnchor, constant: 20).isActive = true 55 | cancel.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 56 | cancel.bottomAnchor.constraint(equalTo: base.bottomAnchor, constant: -20).isActive = true 57 | 58 | name.becomeFirstResponder() 59 | } 60 | 61 | func textFieldShouldReturn(_: UITextField) -> Bool { 62 | name.resignFirstResponder() 63 | return true 64 | } 65 | 66 | @objc private func create() { 67 | let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(name.text!.isEmpty ? .key("Create.untitled") : name.text!) 68 | FileManager.default.createFile(atPath: url.path, contents: Data("\n".utf8)) 69 | result(url) 70 | close() 71 | } 72 | 73 | @objc private func cancel() { 74 | result(nil) 75 | close() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /iOS/Delete.swift: -------------------------------------------------------------------------------- 1 | import Git 2 | import UIKit 3 | 4 | final class Delete: Sheet { 5 | required init?(coder: NSCoder) { return nil } 6 | @discardableResult override init() { 7 | super.init() 8 | let image = UIImageView(image: UIImage(named: "error")) 9 | image.translatesAutoresizingMaskIntoConstraints = false 10 | image.contentMode = .center 11 | image.clipsToBounds = true 12 | base.addSubview(image) 13 | 14 | let label = UILabel() 15 | label.numberOfLines = 0 16 | label.translatesAutoresizingMaskIntoConstraints = false 17 | label.textColor = .white 18 | label.attributedText = { 19 | $0.append(NSAttributedString(string: .key("Delete.title"), attributes: [.font: UIFont.systemFont(ofSize: 14, weight: .bold)])) 20 | $0.append(NSAttributedString(string: .key("Delete.subtitle"), attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .light)])) 21 | return $0 22 | } (NSMutableAttributedString()) 23 | base.addSubview(label) 24 | 25 | let confirm = Button.Yes(.key("Delete.confirm")) 26 | confirm.addTarget(self, action: #selector(self.confirm), for: .touchUpInside) 27 | base.addSubview(confirm) 28 | 29 | let cancel = Button.No(.key("Delete.cancel")) 30 | cancel.addTarget(self, action: #selector(close), for: .touchUpInside) 31 | base.addSubview(cancel) 32 | 33 | image.topAnchor.constraint(equalTo: base.topAnchor, constant: 40).isActive = true 34 | image.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 35 | image.widthAnchor.constraint(equalToConstant: 60).isActive = true 36 | image.heightAnchor.constraint(equalToConstant: 60).isActive = true 37 | 38 | label.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 39 | label.topAnchor.constraint(equalTo: image.bottomAnchor, constant: 20).isActive = true 40 | label.widthAnchor.constraint(lessThanOrEqualToConstant: 260).isActive = true 41 | 42 | confirm.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 43 | confirm.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20).isActive = true 44 | 45 | cancel.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 46 | cancel.topAnchor.constraint(equalTo: confirm.bottomAnchor, constant: 20).isActive = true 47 | cancel.bottomAnchor.constraint(equalTo: base.bottomAnchor, constant: -20).isActive = true 48 | } 49 | 50 | @objc private func confirm() { 51 | try? FileManager.default.contentsOfDirectory(at: Hub.session.url, includingPropertiesForKeys: nil).forEach { 52 | try? FileManager.default.removeItem(at: $0) 53 | } 54 | app.repository = nil 55 | app.alert(.key("Alert.success"), message: .key("Delete.success")) 56 | close() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /iOS/Display.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import PDFKit 3 | 4 | final class Display { 5 | class func make(_ url: URL, data: Data) -> UIView { 6 | switch url.pathExtension.lowercased() { 7 | case "png", "jpg", "jpeg", "gif", "bmp": return image(data) 8 | case "pdf": return pdf(data) 9 | default: return text(data) 10 | } 11 | } 12 | 13 | private class func text(_ data: Data) -> UITextView { 14 | let text = UITextView() 15 | text.translatesAutoresizingMaskIntoConstraints = false 16 | text.text = String(decoding: data, as: UTF8.self) 17 | text.backgroundColor = .clear 18 | text.alwaysBounceVertical = true 19 | text.textColor = .white 20 | text.font = .light(14) 21 | text.textContainerInset = UIEdgeInsets(top: 20, left: 12, bottom: 40, right: 12) 22 | text.indicatorStyle = .white 23 | text.isEditable = false 24 | return text 25 | } 26 | 27 | private class func image(_ data: Data) -> UIImageView { 28 | let image = UIImageView() 29 | image.translatesAutoresizingMaskIntoConstraints = false 30 | image.contentMode = .scaleAspectFit 31 | image.clipsToBounds = true 32 | image.image = UIImage(data: data) 33 | return image 34 | } 35 | 36 | private class func pdf(_ data: Data) -> UIView { 37 | let view: UIView 38 | if #available(iOS 11.0, *) { 39 | view = PDFView() 40 | view.backgroundColor = .clear 41 | (view as! PDFView).document = PDFDocument(data: data) 42 | } else { 43 | view = UIView() 44 | } 45 | view.translatesAutoresizingMaskIntoConstraints = false 46 | return view 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /iOS/Extensions.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | extension UIFont { 4 | class func light(_ size: CGFloat) -> UIFont { return UIFont(name: "SFMono-Light", size: size)! } 5 | class func bold(_ size: CGFloat) -> UIFont { return UIFont(name: "SFMono-Bold", size: size)! } 6 | } 7 | 8 | extension UIColor { 9 | static let halo = #colorLiteral(red: 0.231372549, green: 0.7215686275, blue: 1, alpha: 1) 10 | static let untracked = #colorLiteral(red: 0.8874064701, green: 0.8861742914, blue: 0, alpha: 1) 11 | static let added = #colorLiteral(red: 0, green: 0.8377037809, blue: 0.7416605177, alpha: 1) 12 | static let modified = #colorLiteral(red: 0.802871919, green: 0.7154764525, blue: 1, alpha: 1) 13 | static let deleted = #colorLiteral(red: 0.2274509804, green: 0.7803921569, blue: 0.9176470588, alpha: 1) 14 | } 15 | -------------------------------------------------------------------------------- /iOS/Help.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Help: Sheet { 4 | private weak var label: UILabel! 5 | private weak var centerX: NSLayoutConstraint! 6 | private var buttons = [UIButton]() 7 | private var images = [UIImageView]() 8 | private var index = 0 9 | 10 | required init?(coder: NSCoder) { return nil } 11 | @discardableResult override init() { 12 | super.init() 13 | let label = UILabel() 14 | label.translatesAutoresizingMaskIntoConstraints = false 15 | label.textColor = .white 16 | label.font = .systemFont(ofSize: 14, weight: .light) 17 | label.numberOfLines = 0 18 | base.addSubview(label) 19 | self.label = label 20 | 21 | let close = Button.No(.key("Help.close")) 22 | close.addTarget(self, action: #selector(self.close), for: .touchUpInside) 23 | base.addSubview(close) 24 | 25 | var rightImage: NSLayoutXAxisAnchor! 26 | var rightButton = base.leftAnchor 27 | let steps = ["logo", "checkOn", "add", "reset", "cloud"] 28 | steps.enumerated().forEach { 29 | let image = UIImageView(image: UIImage(named: $0.1)) 30 | image.translatesAutoresizingMaskIntoConstraints = false 31 | image.contentMode = .center 32 | image.clipsToBounds = true 33 | image.alpha = 0 34 | base.addSubview(image) 35 | images.append(image) 36 | 37 | let button = UIButton() 38 | button.translatesAutoresizingMaskIntoConstraints = false 39 | button.addTarget(self, action: #selector(show(_:)), for: .touchUpInside) 40 | button.setImage(UIImage(named: "dot"), for: []) 41 | button.imageView!.clipsToBounds = true 42 | button.imageView!.contentMode = .center 43 | base.addSubview(button) 44 | buttons.append(button) 45 | 46 | image.topAnchor.constraint(equalTo: base.topAnchor, constant: 20).isActive = true 47 | image.heightAnchor.constraint(equalToConstant: 200).isActive = true 48 | image.widthAnchor.constraint(equalToConstant: 300).isActive = true 49 | 50 | button.heightAnchor.constraint(equalToConstant: 90).isActive = true 51 | button.widthAnchor.constraint(equalTo: base.widthAnchor, multiplier: 1 / CGFloat(steps.count), constant: -16).isActive = true 52 | button.bottomAnchor.constraint(equalTo: base.bottomAnchor, constant: -60).isActive = true 53 | 54 | if $0.0 == 0 { 55 | button.leftAnchor.constraint(equalTo: rightButton, constant: 40).isActive = true 56 | centerX = image.centerXAnchor.constraint(equalTo: base.centerXAnchor) 57 | centerX.isActive = true 58 | } else { 59 | button.leftAnchor.constraint(equalTo: rightButton).isActive = true 60 | image.leftAnchor.constraint(equalTo: rightImage, constant: 100).isActive = true 61 | } 62 | rightImage = image.rightAnchor 63 | rightButton = button.rightAnchor 64 | } 65 | 66 | label.topAnchor.constraint(equalTo: base.topAnchor, constant: 230).isActive = true 67 | label.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 68 | label.widthAnchor.constraint(lessThanOrEqualToConstant: 290).isActive = true 69 | 70 | close.topAnchor.constraint(equalTo: base.topAnchor, constant: 430).isActive = true 71 | close.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 72 | close.bottomAnchor.constraint(equalTo: base.bottomAnchor, constant: -20).isActive = true 73 | display(0) 74 | } 75 | 76 | private func display(_ index: Int) { 77 | label.text = .key("Onboard.ios\(index)") 78 | base.layoutIfNeeded() 79 | self.index = index 80 | buttons.enumerated().forEach { $0.1.alpha = $0.0 == index ? 1 : 0.4 } 81 | centerX.constant = CGFloat(-400 * index) 82 | UIView.animate(withDuration: 0.4) { [weak self] in 83 | self?.images.enumerated().forEach { 84 | $0.1.alpha = $0.0 == index ? 1 : 0 85 | } 86 | self?.base.layoutIfNeeded() 87 | } 88 | } 89 | 90 | @objc private func show(_ button: Button) { display(buttons.firstIndex(of: button)!) } 91 | } 92 | -------------------------------------------------------------------------------- /iOS/Pop.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Pop: UIView { 4 | private(set) weak var name: UILabel! 5 | private(set) weak var separator: UIView! 6 | private(set) weak var loading: UIImageView! 7 | private(set) weak var close: UIButton! 8 | private weak var top: NSLayoutConstraint! 9 | 10 | required init?(coder: NSCoder) { return nil } 11 | init() { 12 | app.view.isUserInteractionEnabled = false 13 | super.init(frame: .zero) 14 | translatesAutoresizingMaskIntoConstraints = false 15 | backgroundColor = .black 16 | app.view.addSubview(self) 17 | 18 | let border = UIView() 19 | border.isUserInteractionEnabled = false 20 | border.translatesAutoresizingMaskIntoConstraints = false 21 | border.backgroundColor = .halo 22 | addSubview(border) 23 | 24 | let separator = UIView() 25 | separator.isUserInteractionEnabled = false 26 | separator.translatesAutoresizingMaskIntoConstraints = false 27 | separator.backgroundColor = .halo 28 | addSubview(separator) 29 | self.separator = separator 30 | 31 | let close = UIButton() 32 | close.addTarget(self, action: #selector(closing), for: .touchUpInside) 33 | close.translatesAutoresizingMaskIntoConstraints = false 34 | close.setImage(UIImage(named: "close"), for: .normal) 35 | close.imageView!.contentMode = .center 36 | close.imageView!.clipsToBounds = true 37 | addSubview(close) 38 | self.close = close 39 | 40 | let name = UILabel() 41 | name.translatesAutoresizingMaskIntoConstraints = false 42 | name.textColor = .halo 43 | name.font = .systemFont(ofSize: 14, weight: .bold) 44 | addSubview(name) 45 | self.name = name 46 | 47 | let loading = UIImageView(image: UIImage(named: "loading")) 48 | loading.translatesAutoresizingMaskIntoConstraints = false 49 | loading.clipsToBounds = true 50 | loading.contentMode = .center 51 | addSubview(loading) 52 | self.loading = loading 53 | 54 | leftAnchor.constraint(equalTo: app.view.leftAnchor).isActive = true 55 | widthAnchor.constraint(equalTo: app.view.widthAnchor).isActive = true 56 | heightAnchor.constraint(equalTo: app.view.heightAnchor, constant: 3).isActive = true 57 | top = topAnchor.constraint(equalTo: app.view.topAnchor, constant: app.view.bounds.height) 58 | top.isActive = true 59 | 60 | border.topAnchor.constraint(equalTo: topAnchor).isActive = true 61 | border.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 62 | border.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 63 | border.heightAnchor.constraint(equalToConstant: 3).isActive = true 64 | 65 | separator.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 66 | separator.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 67 | separator.heightAnchor.constraint(equalToConstant: 1).isActive = true 68 | 69 | close.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 70 | close.bottomAnchor.constraint(equalTo: separator.topAnchor).isActive = true 71 | close.heightAnchor.constraint(equalToConstant: 55).isActive = true 72 | close.widthAnchor.constraint(equalToConstant: 50).isActive = true 73 | 74 | name.centerYAnchor.constraint(equalTo: close.centerYAnchor, constant: 1).isActive = true 75 | name.leftAnchor.constraint(equalTo: close.rightAnchor).isActive = true 76 | 77 | loading.widthAnchor.constraint(equalToConstant: 100).isActive = true 78 | loading.heightAnchor.constraint(equalToConstant: 40).isActive = true 79 | loading.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 80 | loading.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true 81 | 82 | if #available(iOS 11.0, *) { 83 | separator.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor, constant: 58).isActive = true 84 | } else { 85 | separator.topAnchor.constraint(equalTo: topAnchor, constant: 58).isActive = true 86 | } 87 | 88 | app.view.layoutIfNeeded() 89 | top.constant = -3 90 | 91 | UIView.animate(withDuration: 0.4, animations: { 92 | app.view.layoutIfNeeded() 93 | }) { [weak self] _ in self?.ready() } 94 | } 95 | 96 | func ready() { app.view.isUserInteractionEnabled = true } 97 | 98 | @objc private func closing() { 99 | top.constant = bounds.height 100 | UIView.animate(withDuration: 0.3, animations: { 101 | app.view.layoutIfNeeded() 102 | }) { [weak self] _ in 103 | self?.removeFromSuperview() 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /iOS/Reset.swift: -------------------------------------------------------------------------------- 1 | import Git 2 | import UIKit 3 | 4 | final class Reset: Sheet { 5 | required init?(coder: NSCoder) { return nil } 6 | @discardableResult override init() { 7 | super.init() 8 | let image = UIImageView(image: UIImage(named: "error")) 9 | image.translatesAutoresizingMaskIntoConstraints = false 10 | image.contentMode = .center 11 | image.clipsToBounds = true 12 | base.addSubview(image) 13 | 14 | let label = UILabel() 15 | label.numberOfLines = 0 16 | label.translatesAutoresizingMaskIntoConstraints = false 17 | label.textColor = .white 18 | label.attributedText = { 19 | $0.append(NSAttributedString(string: .key("Reset.title"), attributes: [.font: UIFont.systemFont(ofSize: 14, weight: .bold)])) 20 | $0.append(NSAttributedString(string: .key("Reset.subtitle"), attributes: [.font: UIFont.systemFont(ofSize: 12, weight: .light)])) 21 | return $0 22 | } (NSMutableAttributedString()) 23 | base.addSubview(label) 24 | 25 | let confirm = Button.Yes(.key("Reset.confirm")) 26 | confirm.addTarget(self, action: #selector(self.confirm), for: .touchUpInside) 27 | base.addSubview(confirm) 28 | 29 | let cancel = Button.No(.key("Reset.cancel")) 30 | cancel.addTarget(self, action: #selector(close), for: .touchUpInside) 31 | base.addSubview(cancel) 32 | 33 | image.topAnchor.constraint(equalTo: base.topAnchor, constant: 40).isActive = true 34 | image.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 35 | image.widthAnchor.constraint(equalToConstant: 60).isActive = true 36 | image.heightAnchor.constraint(equalToConstant: 60).isActive = true 37 | 38 | label.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 39 | label.topAnchor.constraint(equalTo: image.bottomAnchor, constant: 20).isActive = true 40 | label.widthAnchor.constraint(lessThanOrEqualToConstant: 260).isActive = true 41 | 42 | confirm.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 43 | confirm.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 20).isActive = true 44 | 45 | cancel.centerXAnchor.constraint(equalTo: base.centerXAnchor).isActive = true 46 | cancel.topAnchor.constraint(equalTo: confirm.bottomAnchor, constant: 20).isActive = true 47 | cancel.bottomAnchor.constraint(equalTo: base.bottomAnchor, constant: -20).isActive = true 48 | } 49 | 50 | @objc private func confirm() { 51 | app.repository?.reset({ 52 | app.alert(.key("Alert.error"), message: $0.localizedDescription) 53 | }) { [weak self] in 54 | app.alert(.key("Alert.success"), message: .key("Reset.success")) 55 | self?.close() 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /iOS/Settings.swift: -------------------------------------------------------------------------------- 1 | import Git 2 | import UIKit 3 | 4 | final class Settings: UIView { 5 | required init?(coder: NSCoder) { return nil } 6 | init() { 7 | super.init(frame: .zero) 8 | translatesAutoresizingMaskIntoConstraints = false 9 | 10 | let image = UIImageView(image: UIImage(named: "logo")) 11 | image.translatesAutoresizingMaskIntoConstraints = false 12 | image.contentMode = .center 13 | image.clipsToBounds = true 14 | addSubview(image) 15 | 16 | let label = UILabel() 17 | label.translatesAutoresizingMaskIntoConstraints = false 18 | label.text = .key("About.label") 19 | label.textColor = .halo 20 | label.font = .bold(22) 21 | addSubview(label) 22 | 23 | let version = UILabel() 24 | version.translatesAutoresizingMaskIntoConstraints = false 25 | version.text = (Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) 26 | version.textColor = .halo 27 | version.font = .light(12) 28 | addSubview(version) 29 | 30 | let sign = Button.Yes(.key("Settings.buttonSign")) 31 | sign.addTarget(self, action: #selector(self.sign), for: .touchUpInside) 32 | 33 | let key = Button.Yes(.key("Settings.buttonKey")) 34 | key.addTarget(self, action: #selector(self.key), for: .touchUpInside) 35 | 36 | let delete = Button.Yes(.key("Settings.buttonDelete")) 37 | delete.addTarget(self, action: #selector(remove), for: .touchUpInside) 38 | delete.backgroundColor = .init(red: 1, green: 0.2, blue: 0.2, alpha: 1) 39 | 40 | let help = Button.No(.key("Settings.help")) 41 | help.addTarget(self, action: #selector(self.help), for: .touchUpInside) 42 | 43 | image.topAnchor.constraint(equalTo: topAnchor, constant: 50).isActive = true 44 | image.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 45 | image.widthAnchor.constraint(equalToConstant: 150).isActive = true 46 | image.heightAnchor.constraint(equalToConstant: 150).isActive = true 47 | 48 | label.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 49 | label.topAnchor.constraint(equalTo: image.bottomAnchor).isActive = true 50 | 51 | version.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 52 | version.topAnchor.constraint(equalTo: label.bottomAnchor).isActive = true 53 | 54 | var top = version.bottomAnchor 55 | [sign, key, delete, help].forEach { 56 | addSubview($0) 57 | $0.topAnchor.constraint(equalTo: top, constant: 30).isActive = true 58 | $0.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 59 | top = $0.bottomAnchor 60 | } 61 | } 62 | 63 | @objc private func help() { app.help() } 64 | @objc private func remove() { Delete() } 65 | 66 | @objc func sign() { 67 | let credentials = Credentials() 68 | credentials.title.text = .key("Settings.labelSign") 69 | credentials.first.label.text = .key("Settings.signName") 70 | credentials.first.field.keyboardType = .alphabet 71 | credentials.first.field.text = Hub.session.name 72 | credentials.second.label.text = .key("Settings.signEmail") 73 | credentials.second.field.keyboardType = .emailAddress 74 | credentials.second.field.text = Hub.session.email 75 | credentials.done = { 76 | Hub.session.update($0, email: $1, error: { 77 | app.alert(.key("Alert.error"), message: $0.localizedDescription) 78 | }) { [weak credentials] in 79 | app.alert(.key("Alert.success"), message: .key("Settings.signSuccess")) 80 | credentials?.close() 81 | } 82 | } 83 | } 84 | 85 | @objc private func key() { 86 | let credentials = Credentials() 87 | credentials.title.text = .key("Settings.labelKey") 88 | credentials.first.label.text = .key("Settings.keyUser") 89 | credentials.first.field.keyboardType = .emailAddress 90 | credentials.first.field.text = Hub.session.user 91 | credentials.second.label.text = .key("Settings.keyPassword") 92 | credentials.second.field.isSecureTextEntry = true 93 | credentials.second.field.keyboardType = .alphabet 94 | credentials.second.field.text = Hub.session.password 95 | credentials.done = { 96 | Hub.session.update($0, password: $1) { [weak credentials] in 97 | app.alert(.key("Alert.success"), message: .key("Settings.keySuccess")) 98 | credentials?.close() 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /iOS/Sheet.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | class Sheet: UIView { 4 | private(set) weak var base: UIView! 5 | 6 | required init?(coder: NSCoder) { return nil } 7 | init() { 8 | super.init(frame: .zero) 9 | translatesAutoresizingMaskIntoConstraints = false 10 | backgroundColor = .init(white: 0, alpha: 0) 11 | let parent = app.presentedViewController?.view ?? app.view! 12 | parent.addSubview(self) 13 | 14 | let background = UIView() 15 | background.translatesAutoresizingMaskIntoConstraints = false 16 | background.backgroundColor = UIColor.halo.withAlphaComponent(0) 17 | background.isUserInteractionEnabled = false 18 | addSubview(background) 19 | 20 | let base = UIView() 21 | base.translatesAutoresizingMaskIntoConstraints = false 22 | base.backgroundColor = .black 23 | base.layer.cornerRadius = 6 24 | base.clipsToBounds = true 25 | addSubview(base) 26 | self.base = base 27 | 28 | topAnchor.constraint(equalTo: parent.topAnchor).isActive = true 29 | bottomAnchor.constraint(equalTo: parent.bottomAnchor).isActive = true 30 | leftAnchor.constraint(equalTo: parent.leftAnchor).isActive = true 31 | rightAnchor.constraint(equalTo: parent.rightAnchor).isActive = true 32 | 33 | background.topAnchor.constraint(equalTo: topAnchor).isActive = true 34 | background.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 35 | background.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 36 | background.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 37 | 38 | base.leftAnchor.constraint(equalTo: leftAnchor, constant: 16).isActive = true 39 | base.rightAnchor.constraint(equalTo: rightAnchor, constant: -16).isActive = true 40 | let bottom = base.bottomAnchor.constraint(equalTo: topAnchor) 41 | bottom.isActive = true 42 | 43 | DispatchQueue.main.async { 44 | parent.layoutIfNeeded() 45 | bottom.constant = 40 + base.frame.height 46 | UIView.animate(withDuration: 0.35) { [weak self] in 47 | self?.backgroundColor = .init(white: 0, alpha: 0.9) 48 | background.backgroundColor = UIColor.halo.withAlphaComponent(0.3) 49 | self?.layoutIfNeeded() 50 | } 51 | } 52 | } 53 | 54 | @objc func close() { 55 | app.window!.endEditing(true) 56 | UIView.animate(withDuration: 0.3, animations: { [weak self] in 57 | self?.alpha = 0 58 | }) { [weak self] _ in self?.removeFromSuperview() } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /iOS/Tab.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class Tab: UIView { 4 | final class Button: UIControl { 5 | fileprivate let target: (() -> Void) 6 | fileprivate weak var tab: Tab! 7 | private weak var indicator: UIView! 8 | private weak var image: UIImageView! 9 | 10 | required init?(coder: NSCoder) { return nil } 11 | init(_ image: String, target: @escaping(() -> Void)) { 12 | self.target = target 13 | super.init(frame: .zero) 14 | translatesAutoresizingMaskIntoConstraints = false 15 | addTarget(self, action: #selector(choose), for: .touchUpInside) 16 | 17 | let indicator = UIView() 18 | indicator.translatesAutoresizingMaskIntoConstraints = false 19 | indicator.isUserInteractionEnabled = false 20 | indicator.backgroundColor = .halo 21 | addSubview(indicator) 22 | self.indicator = indicator 23 | 24 | let image = UIImageView(image: UIImage(named: image)) 25 | image.translatesAutoresizingMaskIntoConstraints = false 26 | image.clipsToBounds = true 27 | image.contentMode = .center 28 | addSubview(image) 29 | self.image = image 30 | 31 | image.topAnchor.constraint(equalTo: topAnchor).isActive = true 32 | image.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 33 | image.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 34 | image.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 35 | 36 | indicator.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 37 | indicator.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true 38 | indicator.widthAnchor.constraint(equalToConstant: 30).isActive = true 39 | indicator.heightAnchor.constraint(equalToConstant: 1).isActive = true 40 | } 41 | 42 | @objc func choose() { tab.choose(self) } 43 | override var isHighlighted: Bool { didSet { hover() } } 44 | override var isSelected: Bool { didSet { hover() } } 45 | 46 | private func hover() { 47 | if isSelected || isHighlighted { 48 | image.alpha = 1 49 | indicator.isHidden = false 50 | } else { 51 | image.alpha = 0.4 52 | indicator.isHidden = true 53 | } 54 | } 55 | } 56 | 57 | private(set) weak var settings: Button! 58 | private(set) weak var market: Button! 59 | private(set) weak var home: Button! 60 | private(set) weak var add: Button! 61 | private(set) weak var history: Button! 62 | 63 | required init?(coder: NSCoder) { return nil } 64 | init() { 65 | super.init(frame: .zero) 66 | translatesAutoresizingMaskIntoConstraints = false 67 | 68 | let border = UIView() 69 | border.isUserInteractionEnabled = true 70 | border.translatesAutoresizingMaskIntoConstraints = false 71 | border.backgroundColor = .halo 72 | addSubview(border) 73 | 74 | let settings = Button("settings", target: app.settings) 75 | self.settings = settings 76 | 77 | let market = Button("market", target: app.market) 78 | self.market = market 79 | 80 | let home = Button("home", target: app.home) 81 | self.home = home 82 | 83 | let add = Button("add", target: app.add) 84 | self.add = add 85 | 86 | let history = Button("history", target: app.history) 87 | self.history = history 88 | 89 | var left = leftAnchor 90 | [settings, market, home, add, history].forEach { 91 | $0.tab = self 92 | addSubview($0) 93 | 94 | $0.leftAnchor.constraint(equalTo: left).isActive = true 95 | $0.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.2).isActive = true 96 | $0.topAnchor.constraint(equalTo: topAnchor).isActive = true 97 | $0.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true 98 | left = $0.rightAnchor 99 | } 100 | 101 | home.choose() 102 | heightAnchor.constraint(equalToConstant: 62).isActive = true 103 | 104 | border.leftAnchor.constraint(equalTo: leftAnchor).isActive = true 105 | border.rightAnchor.constraint(equalTo: rightAnchor).isActive = true 106 | border.heightAnchor.constraint(equalToConstant: 1).isActive = true 107 | border.topAnchor.constraint(equalTo: topAnchor).isActive = true 108 | } 109 | 110 | private func choose(_ button: Button) { 111 | guard !button.isSelected else { return } 112 | subviews.compactMap({ $0 as? Button }).forEach({ $0.isSelected = $0 === button }) 113 | button.target() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /iOS/iOS.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Git 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIcons 12 | 13 | CFBundleIcons~ipad 14 | 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | Git 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(MARKETING_VERSION) 25 | CFBundleVersion 26 | $(CURRENT_PROJECT_VERSION) 27 | LSRequiresIPhoneOS 28 | 29 | LSSupportsOpeningDocumentsInPlace 30 | 31 | NSPhotoLibraryAddUsageDescription 32 | Save selected images to photo library. 33 | UIAppFonts 34 | 35 | SFMono-Light.otf 36 | SFMono-Bold.otf 37 | 38 | UILaunchStoryboardName 39 | iOS 40 | UIRequiredDeviceCapabilities 41 | 42 | armv7 43 | 44 | UIStatusBarHidden 45 | 46 | UIStatusBarHidden~ipad 47 | 48 | UISupportedInterfaceOrientations 49 | 50 | UIInterfaceOrientationPortrait 51 | UIInterfaceOrientationLandscapeLeft 52 | UIInterfaceOrientationLandscapeRight 53 | UIInterfaceOrientationPortraitUpsideDown 54 | 55 | UISupportedInterfaceOrientations~ipad 56 | 57 | UIInterfaceOrientationPortrait 58 | UIInterfaceOrientationPortraitUpsideDown 59 | UIInterfaceOrientationLandscapeLeft 60 | UIInterfaceOrientationLandscapeRight 61 | 62 | UISupportsDocumentBrowser 63 | 64 | UIViewControllerBasedStatusBarAppearance 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /iOS/iOS.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "brand40w.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "brand60w.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "brand58w-1.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "brand87w.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "brand80w-1.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "brand120w-1.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "brand120w.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "brand180w.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "brand20w.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "brand40w-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "brand29w.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "brand58w.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "brand40w-2.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "brand80w.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "brand76w.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "brand152w.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "brand167w.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "brand.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand120w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand120w-1.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand120w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand120w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand152w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand152w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand167w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand167w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand180w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand180w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand20w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand20w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand29w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand29w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand40w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand40w-1.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand40w-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand40w-2.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand40w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand40w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand58w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand58w-1.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand58w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand58w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand60w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand60w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand76w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand76w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand80w-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand80w-1.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand80w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand80w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/AppIcon.appiconset/brand87w.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitmeta/git/8ca918eff0b3cab7829077de8de6f46f75c92605/iOS/iOS.xcassets/AppIcon.appiconset/brand87w.png -------------------------------------------------------------------------------- /iOS/iOS.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } --------------------------------------------------------------------------------