/*
* 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 Dispatch
import Foundation
import Vapor
public protocol ItemUpdater {
/// Filters the items to be updated
///
/// - Parameter items: All the items in the three
/// - Returns: The items that should be updated.
func itemsToUpdate(from items: [TreeItem]) -> [TreeItem]
/// Updates the filtered
///
/// - Parameters:
/// - item: The three item
/// - content: The content of the three item as
/// - Returns: The new content of the tree item that will be comitted
func update(_ item: TreeItem, content: String) throws -> String
}
private class BatchItemUpdater {
private let updater: ItemUpdater
private let github: GitHub
init(github: GitHub, updater: ItemUpdater) {
self.github = github
self.updater = updater
}
/// Each item is passed to the `ItemUpdater` to determine if it should be updated.
/// For `ThreeItem` that the `ItemUpdater` wants to update, the content is fetched and passed again to the `ItemUpdater`
/// The new content returned from by the `ItemUpdater` creates a new GitBlob its corresponding `TreeItem`
///
/// - Parameter items: The list of items passed to the Updater
/// - Returns: A future list of `ThreeItem` that were updated
func update(items: [TreeItem], on worker: Worker) throws -> Future<[TreeItem]> {
var updatedFutureItems = [Future]()
for item in updater.itemsToUpdate(from: items) {
updatedFutureItems.append(try update(item: item))
}
// wait for all items
return updatedFutureItems.flatten(on: worker)
}
private func update(item: TreeItem) throws -> Future {
let result = try github.gitBlob(sha: item.sha).map(to: String.self) { blob in
guard let content = blob.string else { throw "Could not convert blob content to string" }
return try self.updater.update(item, content: content)
}.flatMap { newContent in
try self.github.newBlob(data: newContent)
}.map { newBlob in
return TreeItem(path: item.path, mode: item.mode, type: item.type, sha: newBlob.sha)
}
return result
}
}
public extension GitHub {
struct CurrentState {
let items: [TreeItem]
let currentCommitSHA: GitHub.Repos.Commit.SHA
let treeSHA: Git.Tree.SHA
}
func currentState(on branch: BranchName) throws -> Future {
return try assertBranchExists(branch).flatMap { _ -> Future in
let commitSha = try self.currentCommitSHA(on: branch)
let treeSha = commitSha.flatMap { sha in
try self.treeSHA(forCommitSHA: sha)
}
let tree = treeSha.flatMap { sha in
try self.trees(for: sha)
}
return map(commitSha, treeSha, tree) { commitSha, treeSha, tree in
return CurrentState(items: tree.tree, currentCommitSHA: commitSha, treeSHA: treeSha)
}
}
}
/**
Helper methods that passes the latest files on a specified branch to a ItemUpdate and creates a new commit with the update items/
It
- fetches the current repo at the specified branch
- Passes the file list to the file updater
- Creates a new tree with the update files
- Creates a new commit
*/
func newCommit(updatingItemsWith updater: ItemUpdater, on branch: BranchName, by author: Author, message: String) throws -> Future {
// Get the repo state
let respositoryState = try currentState(on: branch)
// Pass the items to the updater
let updatedItems = respositoryState.flatMap(to: [TreeItem].self) { respositoryState in
let batchUpdater = BatchItemUpdater(github: self, updater: updater)
return try batchUpdater.update(items: respositoryState.items, on: self.worker)
}
// wait for both repo state and updated items to create a new tree
let newTree = flatMap(to: Tree.self, respositoryState, updatedItems) { respositoryState, updatedItems in
if updatedItems.isEmpty {
throw "The updater \(updater) did not match any items to update"
}
return try self.newTree(tree: Tree.New(baseTree: respositoryState.treeSHA, tree: updatedItems))
}
// wait for both repo state and the new tree
let commit = map(respositoryState, newTree) { state, newTree in
return (state.currentCommitSHA, newTree.sha)
}.flatMap { parentSHA, treeSHA in
// to create a new commit
return try self.newCommit(by: author, message: message, parentSHA: parentSHA, treeSHA: treeSHA)
}.flatMap { newCommit in
// and update the ref
return try self.updateRef(to: newCommit.sha, on: branch)
}
return commit
}
}