/* * 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 } }