/*
* Copyright (c) 2017 N26 GmbH.
*
* This file is part of Bob.
*
* Bob is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Bob is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Bob. If not, see .
*/
import Foundation
import HTTP
import Vapor
enum GitHubError: LocalizedError {
case invalidBranch(name: String)
case invalidParam(String)
case invalidStatus(httpStatus: UInt, body: String?)
case decoding(String)
public var errorDescription: String? {
switch self {
case .decoding(let message):
return "Decoding error: \(message)"
case .invalidBranch(let name):
return "The branch '\(name)' does not exists"
case .invalidParam(let param):
return "Invalid parameter '\(param)'"
case .invalidStatus(let httpStatus, let body):
var message = "Invalid response status '\(httpStatus)')"
body.flatMap { message += " body: \($0)" }
return message
}
}
}
/// Used for communicating with the GitHub api
public class GitHub {
/// Configuration needed for authentication with the api
public struct Configuration {
public let username: String
public let personalAccessToken: String
public let repoUrl: String
/// Initializer for the configuration
///
/// - Parameters:
/// - username: Username of a user
/// - personalAccessToken: Personal access token for that user. Make sure it has repo read/write for the repo you intend to use
/// - repoUrl: Url of the repo. Alogn the lines of https://api.github.com/repos/{owner}/{repo}
public init(username: String, personalAccessToken: String, repoUrl: String) {
self.username = username
self.personalAccessToken = personalAccessToken
self.repoUrl = repoUrl
}
}
private let authorization: BasicAuthorization
private let repoUrl: String
private let container: Container
public var worker: Worker {
return container
}
public init(config: Configuration, container: Container) {
self.authorization = BasicAuthorization(username: config.username, password: config.personalAccessToken)
self.repoUrl = config.repoUrl
self.container = container
}
private func uri(at path: String) -> String {
return self.repoUrl + path
}
// MARK: Repository APIs
public func branches() throws -> Future<[GitHub.Repos.Branch]> {
return try get(uri(at: "/branches?per_page=100"))
}
public func branch(_ branch: GitHub.Repos.Branch.BranchName) throws -> Future {
return try get(uri(at: "/branches/" + branch))
}
/// Lists the content of a directory
public func contents(at path: String, on branch: GitHub.Repos.Branch.BranchName) throws -> Future<[GitHub.Repos.GitContent]> {
return try get(uri(at: "/contents/\(path)?ref=" + branch))
}
/// Content of a single file
public func content(at path: String, on branch: GitHub.Repos.Branch.BranchName) throws -> Future {
return try get(uri(at: "/contents/\(path)?ref=" + branch))
}
public func tags() throws -> Future<[GitHub.Repos.Tag]> {
return try get(uri(at: "/tags"))
}
/// Returns a list of commits in reverse chronological order
///
/// - Parameters:
/// - sha: Starting commit
/// - page: Index of the requested page
/// - perPage: Number of commits per page
/// - path: Directory within repository (optional). Only commits with files touched within path will be returned
public func commits(after sha: String? = nil, page: Int? = nil, perPage: Int? = nil, path: String? = nil) throws -> Future<[GitHub.Repos.Commit]> {
var components = URLComponents(string: "")!
var items = [URLQueryItem]()
components.path = "/commits"
if let sha = sha {
items.append(URLQueryItem(name: "sha", value: sha))
}
if let page = page {
items.append(URLQueryItem(name: "page", value: "\(page)"))
}
if let perPage = perPage {
items.append(URLQueryItem(name: "per_page", value: "\(perPage)"))
}
if let path = path {
items.append(URLQueryItem(name: "path", value: "\(path)"))
}
components.queryItems = items
guard let url = components.url else { throw GitHubError.invalidParam("Could not create commit URL") }
let uri = self.uri(at: url.absoluteString)
return try get(uri)
}
// MARK: - Git APIs
public func gitCommit(sha: GitHub.Git.Commit.SHA) throws -> Future {
return try get(uri(at: "/git/commits/" + sha))
}
public func gitBlob(sha: Git.TreeItem.SHA) throws -> Future {
return try get(uri(at: "/git/blobs/" + sha))
}
public func newBlob(data: String) throws -> Future {
let blob = GitHub.Git.Blob.New(content: data)
return try post(body: blob, to: uri(at: "/git/blobs"))
}
public func trees(for treeSHA: GitHub.Git.Tree.SHA) throws -> Future {
return try self.get(uri(at: "/git/trees/" + treeSHA + "?recursive=1"))
}
public func newTree(tree: Tree.New) throws -> Future {
return try post(body: tree, to: uri(at: "/git/trees"))
}
/// https://developer.github.com/v3/git/commits/#create-a-commit
public func newCommit(by author: Author, message: String, parentSHA: String, treeSHA: String) throws -> Future {
let body = GitCommit.New(message: message, tree: treeSHA, parents: [parentSHA], author: author)
return try post(body: body, to: uri(at: "/git/commits"))
}
public func updateRef(to sha: GitHub.Git.Commit.SHA, on branch: GitHub.Repos.Branch.BranchName) throws -> Future {
let body = GitHub.Git.Reference.Patch(sha: sha)
return try post(body: body, to: uri(at: "/git/refs/heads/" + branch), patch: true)
}
// MARK: - Private
private func get(_ uri: String) throws -> Future {
return try container.client().get(uri, using: GitHub.decoder, authorization: authorization)
}
private func post(body: Body, to uri: String, patch: Bool = false ) throws -> Future {
return try container.client().post(body: body, to: uri, encoder: GitHub.encoder, using: GitHub.decoder, method: patch ? .PATCH : .POST, authorization: authorization)
}
}