├── .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 HEAD multi_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/master report-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 | }
--------------------------------------------------------------------------------