|
| 1 | +/** |
| 2 | + Git.swift |
| 3 | + GitKit |
| 4 | + |
| 5 | + Created by Tibor Bödecs on 2019.01.02. |
| 6 | + Copyright Binary Birds. All rights reserved. |
| 7 | + */ |
| 8 | + |
| 9 | +import ShellKit |
| 10 | + |
| 11 | +/// a Git wrapper class |
| 12 | +public final class Git: Shell { |
| 13 | + |
| 14 | + /// Git aliases to make the API more convenient |
| 15 | + public enum Alias { |
| 16 | + case cmd(Command, String? = nil) |
| 17 | + case addAll |
| 18 | + case commit(message: String, Bool = false) |
| 19 | + case clone(url: String) |
| 20 | + case checkout(branch: String) |
| 21 | + case log(Int? = nil) |
| 22 | + case push(remote: String? = nil, branch: String? = nil) |
| 23 | + case pull(remote: String? = nil, branch: String? = nil) |
| 24 | + case merge(branch: String) |
| 25 | + case create(branch: String) |
| 26 | + case delete(branch: String) |
| 27 | + case tag(String) |
| 28 | + case raw(String) |
| 29 | + |
| 30 | + private func commandParams() -> [String] { |
| 31 | + var params: [String] = [] |
| 32 | + switch self { |
| 33 | + case .cmd(let command, let args): |
| 34 | + params = [command.rawValue] |
| 35 | + if let args = args { |
| 36 | + params.append(args) |
| 37 | + } |
| 38 | + case .addAll: |
| 39 | + params = [Command.add.rawValue, "."] |
| 40 | + case .commit(let message, let allowEmpty): |
| 41 | + params = [Command.commit.rawValue, "-m", "\"\(message)\""] |
| 42 | + if allowEmpty { |
| 43 | + params.append("--allow-empty") |
| 44 | + } |
| 45 | + case .clone(let url): |
| 46 | + params = [Command.clone.rawValue, url] |
| 47 | + case .checkout(let branch): |
| 48 | + params = [Command.checkout.rawValue, branch] |
| 49 | + case .log(let n): |
| 50 | + params = [Command.log.rawValue] |
| 51 | + if let n = n { |
| 52 | + params.append("-\(n)") |
| 53 | + } |
| 54 | + case .push(let remote, let branch): |
| 55 | + params = [Command.push.rawValue] |
| 56 | + if let remote = remote { |
| 57 | + params.append(remote) |
| 58 | + } |
| 59 | + if let branch = branch { |
| 60 | + params.append(branch) |
| 61 | + } |
| 62 | + case .pull(let remote, let branch): |
| 63 | + params = [Command.pull.rawValue] |
| 64 | + if let remote = remote { |
| 65 | + params.append(remote) |
| 66 | + } |
| 67 | + if let branch = branch { |
| 68 | + params.append(branch) |
| 69 | + } |
| 70 | + case .merge(let branch): |
| 71 | + params = [Command.merge.rawValue, branch] |
| 72 | + case .create(let branch): |
| 73 | + params = [Command.checkout.rawValue, "-b", branch] |
| 74 | + case .delete(let branch): |
| 75 | + params = [Command.branch.rawValue, "-D", branch] |
| 76 | + case .tag(let name): |
| 77 | + params = [Command.tag.rawValue, name] |
| 78 | + case .raw(let command): |
| 79 | + params.append(command) |
| 80 | + } |
| 81 | + return params |
| 82 | + } |
| 83 | + |
| 84 | + public var rawValue: String { |
| 85 | + self.commandParams().joined(separator: " ") |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + /// basic git commands |
| 90 | + public enum Command: String { |
| 91 | + |
| 92 | + // MARK: - start a working area (see also: git help tutorial) |
| 93 | + |
| 94 | + case config |
| 95 | + |
| 96 | + case clean |
| 97 | + /// Clone a repository into a new directory |
| 98 | + case clone |
| 99 | + /// Create an empty Git repository or reinitialize an existing one |
| 100 | + case initialize = "init" |
| 101 | + |
| 102 | + // MARK: - work on the current change (see also: git help everyday) |
| 103 | + |
| 104 | + /// Add file contents to the index |
| 105 | + case add |
| 106 | + /// Move or rename a file, a directory, or a symlink |
| 107 | + case mv |
| 108 | + /// Reset current HEAD to the specified state |
| 109 | + case reset |
| 110 | + /// Remove files from the working tree and from the index |
| 111 | + case rm |
| 112 | + |
| 113 | + // MARK: - examine the history and state (see also: git help revisions) |
| 114 | + |
| 115 | + /// Use binary search to find the commit that introduced a bug |
| 116 | + case bisect |
| 117 | + /// Print lines matching a pattern |
| 118 | + case grep |
| 119 | + /// Show commit logs |
| 120 | + case log |
| 121 | + /// Show various types of objects |
| 122 | + case show |
| 123 | + /// Show the working tree status |
| 124 | + case status |
| 125 | + |
| 126 | + // MARK: - grow, mark and tweak your common history |
| 127 | + |
| 128 | + /// List, create, or delete branches |
| 129 | + case branch |
| 130 | + /// Switch branches or restore working tree files |
| 131 | + case checkout |
| 132 | + /// Record changes to the repository |
| 133 | + case commit |
| 134 | + /// Show changes between commits, commit and working tree, etc |
| 135 | + case diff |
| 136 | + /// Join two or more development histories together |
| 137 | + case merge |
| 138 | + /// Reapply commits on top of another base tip |
| 139 | + case rebase |
| 140 | + /// Create, list, delete or verify a tag object signed with GPG |
| 141 | + case tag |
| 142 | + |
| 143 | + // MARK: - collaborate (see also: git help workflows) |
| 144 | + |
| 145 | + /// Download objects and refs from another repository |
| 146 | + case fetch |
| 147 | + /// Fetch from and integrate with another repository or a local branch |
| 148 | + case pull |
| 149 | + /// Update remote refs along with associated objects |
| 150 | + case push |
| 151 | + } |
| 152 | + |
| 153 | + // MARK: - private helper methods |
| 154 | + |
| 155 | + /** |
| 156 | + This method helps to assemble a Git command string from an alias |
| 157 | + |
| 158 | + If there is a git repo path (working directory) presented, proper directories |
| 159 | + will be used & created recursively if a new repository is being initialized. |
| 160 | + |
| 161 | + - Parameters: |
| 162 | + - alias: The git alias to be executed |
| 163 | + - args: Additional arguments for the Git alias |
| 164 | + |
| 165 | + - Returns: The Git command |
| 166 | + */ |
| 167 | + private func rawCommand(_ alias: Alias) -> String { |
| 168 | + var cmd: [String] = [] |
| 169 | + // if there is a path let's change directory first |
| 170 | + if let path = self.path { |
| 171 | + // try to create work dir at given path for init or clone commands |
| 172 | + if |
| 173 | + alias.rawValue.hasPrefix(Command.initialize.rawValue) || |
| 174 | + alias.rawValue.hasPrefix(Command.clone.rawValue) |
| 175 | + { |
| 176 | + cmd += ["mkdir", "-p", path, "&&"] |
| 177 | + } |
| 178 | + cmd += ["cd", path, "&&"] |
| 179 | + } |
| 180 | + cmd += ["git", alias.rawValue] |
| 181 | + |
| 182 | + let command = cmd.joined(separator: " ") |
| 183 | + |
| 184 | + if self.verbose { |
| 185 | + print(command) |
| 186 | + } |
| 187 | + return command |
| 188 | + } |
| 189 | + |
| 190 | + // MARK: - public api |
| 191 | + |
| 192 | + /// work directory, if peresent a directory change will occur before running any Git commands |
| 193 | + /// |
| 194 | + /// NOTE: if the git init command is called with a non-existing path, directories |
| 195 | + /// presented in the path string will be created recursively |
| 196 | + public var path: String? |
| 197 | + |
| 198 | + // prints git commands constructed from the alias before execution |
| 199 | + public var verbose = false |
| 200 | + |
| 201 | + /** |
| 202 | + Initializes a new Git object |
| 203 | + |
| 204 | + - Parameters: |
| 205 | + - path: The path of the Swift package (work directory) |
| 206 | + - type: The type of the shell, default: /bin/sh |
| 207 | + - env: Additional environment variables for the shell, default: empty |
| 208 | + |
| 209 | + */ |
| 210 | + public init(path: String? = nil, type: String = "/bin/sh", env: [String: String] = [:]) { |
| 211 | + self.path = path |
| 212 | + |
| 213 | + super.init(type, env: env) |
| 214 | + } |
| 215 | + |
| 216 | + /** |
| 217 | + Runs a specific Git alias through the current shell. |
| 218 | + |
| 219 | + - Parameters: |
| 220 | + - alias: The git command alias to be executed |
| 221 | + |
| 222 | + - Throws: |
| 223 | + `ShellError.outputData` if the command execution succeeded but the output is empty, |
| 224 | + otherwise `ShellError.generic(Int, String)` where the first parameter is the exit code, |
| 225 | + the second is the error message |
| 226 | + |
| 227 | + - Returns: The output string of the command without trailing newlines |
| 228 | + */ |
| 229 | + @discardableResult |
| 230 | + public func run(_ alias: Alias) throws -> String { |
| 231 | + try self.run(self.rawCommand(alias)) |
| 232 | + } |
| 233 | + |
| 234 | + /** |
| 235 | + Async version of the run function |
| 236 | + |
| 237 | + - Parameters: |
| 238 | + - alias: The git command alias to be executed |
| 239 | + - completion: The completion block with the output and error |
| 240 | + |
| 241 | + The command will be executed on a concurrent dispatch queue. |
| 242 | + */ |
| 243 | + public func run(_ alias: Alias, completion: @escaping ((String?, Swift.Error?) -> Void)) { |
| 244 | + self.run(self.rawCommand(alias), completion: completion) |
| 245 | + } |
| 246 | +} |
0 commit comments