Skip to content

Commit

Permalink
Merge pull request #188 from czechboy0/hd/parsing_git_metadata_directly
Browse files Browse the repository at this point in the history
Parsing metadata information directly from git repo (in the absence of checkout files)
  • Loading branch information
czechboy0 committed Oct 21, 2015
2 parents 49dd3cf + 3b45633 commit 6cb2371
Show file tree
Hide file tree
Showing 8 changed files with 266 additions and 84 deletions.
54 changes: 54 additions & 0 deletions BuildaKit/BlueprintFileParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//
// BlueprintFileParser.swift
// Buildasaur
//
// Created by Honza Dvorsky on 10/21/15.
// Copyright © 2015 Honza Dvorsky. All rights reserved.
//

import Foundation
import BuildaUtils

class BlueprintFileParser: SourceControlFileParser {

func supportedFileExtensions() -> [String] {
return ["xcscmblueprint"]
}

func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata {

//JSON -> NSDictionary
let data = try NSData(contentsOfURL: url, options: NSDataReadingOptions())
let jsonObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
guard let dictionary = jsonObject as? NSDictionary else { throw Error.withInfo("Failed to parse \(url)") }

//parse our required keys
let projectName = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintNameKey")
let projectPath = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey")
let projectWCCIdentifier = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey")

var primaryRemoteRepositoryDictionary: NSDictionary?
if let wccId = projectWCCIdentifier {
if let wcConfigs = dictionary["DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey"] as? [NSDictionary] {
primaryRemoteRepositoryDictionary = wcConfigs.filter({
if let loopWccId = $0.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey") {
return loopWccId == wccId
}
return false
}).first
}
}

let projectURLString = primaryRemoteRepositoryDictionary?.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey")

var projectWCCName: String?
if
let copyPaths = dictionary["DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey"] as? [String: String],
let primaryRemoteRepoId = projectWCCIdentifier
{
projectWCCName = copyPaths[primaryRemoteRepoId]
}

return try WorkspaceMetadata(projectName: projectName, projectPath: projectPath, projectWCCIdentifier: projectWCCIdentifier, projectWCCName: projectWCCName, projectURLString: projectURLString)
}
}
47 changes: 47 additions & 0 deletions BuildaKit/CheckoutFileParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// CheckoutFileParser.swift
// Buildasaur
//
// Created by Honza Dvorsky on 10/21/15.
// Copyright © 2015 Honza Dvorsky. All rights reserved.
//

import Foundation
import BuildaUtils

class CheckoutFileParser: SourceControlFileParser {

func supportedFileExtensions() -> [String] {
return ["xccheckout"]
}

func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata {

//plist -> NSDictionary
guard let dictionary = NSDictionary(contentsOfURL: url) else { throw Error.withInfo("Failed to parse \(url)") }

//parse our required keys
let projectName = dictionary.optionalStringForKey("IDESourceControlProjectName")
let projectPath = dictionary.optionalStringForKey("IDESourceControlProjectPath")
let projectWCCIdentifier = dictionary.optionalStringForKey("IDESourceControlProjectWCCIdentifier")
let projectWCCName = { () -> String? in
if let wccId = projectWCCIdentifier {
if let wcConfigs = dictionary["IDESourceControlProjectWCConfigurations"] as? [NSDictionary] {
if let foundConfig = wcConfigs.filter({
if let loopWccId = $0.optionalStringForKey("IDESourceControlWCCIdentifierKey") {
return loopWccId == wccId
}
return false
}).first {
//so much effort for this little key...
return foundConfig.optionalStringForKey("IDESourceControlWCCName")
}
}
}
return nil
}()
let projectURLString = { dictionary.optionalStringForKey("IDESourceControlProjectURL") }()

return try WorkspaceMetadata(projectName: projectName, projectPath: projectPath, projectWCCIdentifier: projectWCCIdentifier, projectWCCName: projectWCCName, projectURLString: projectURLString)
}
}
138 changes: 138 additions & 0 deletions BuildaKit/GitRepoMetadataParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//
// GitRepoMetadataParser.swift
// Buildasaur
//
// Created by Honza Dvorsky on 10/21/15.
// Copyright © 2015 Honza Dvorsky. All rights reserved.
//

import Foundation
import BuildaUtils
import CryptoSwift

class GitRepoMetadataParser: SourceControlFileParser {

func supportedFileExtensions() -> [String] {
return [] //takes anything
}

private typealias ScriptRun = (String) throws -> String

private func parseOrigin(run: ScriptRun) throws -> String {

//find the first origin ending with "(fetch)"
let remotes = try run("git remote -v")
let fetchRemote = remotes.split("\n").filter { $0.hasSuffix("(fetch)") }.first
guard let remoteLine = fetchRemote else {
throw Error.withInfo("No fetch remote found in \(remotes)")
}

//parse the fetch remote, which is
//e.g. "origin\[email protected]:czechboy0/BuildaUtils.git (fetch)"
let comps = remoteLine
.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "\t "))
.filter { !$0.isEmpty }

//we need at least 2 comps, take the second
guard comps.count >= 2 else {
throw Error.withInfo("Cannot parse origin url from components \(comps)")
}

let remote = comps[1]

//we got it!
return remote
}

private func parseProjectName(url: NSURL) throws -> String {

//that's the name of the passed-in project/workspace (most of the times)
let projectName = ((url.lastPathComponent ?? "") as NSString).stringByDeletingPathExtension

guard !projectName.isEmpty else {
throw Error.withInfo("Failed to parse project name from url \(url)")
}
return projectName
}

private func parseProjectPath(url: NSURL, run: ScriptRun) throws -> String {

//relative path from the root of the git repo of the passed-in project
//or workspace file
let absolutePath = url.path!
let relativePath = "git ls-tree --full-name --name-only HEAD \"\(absolutePath)\""
let outPath = try run(relativePath)
let trimmed = outPath.trim()
guard !trimmed.isEmpty else {
throw Error.withInfo("Failed to detect relative path of project \(url), output: \(outPath)")
}
return trimmed
}

private func parseProjectWCCName(url: NSURL, projectPath: String) throws -> String {

//this is the folder name containing the git repo
//it's the folder name before the project path
//e.g. if project path is b/c/hello.xcodeproj, and the whole path
//to the project is /Users/me/a/b/c/hello.xcodeproj, the project wcc name
//would be "a"

var projectPathComponents = projectPath.split("/")
var pathComponents = url.pathComponents ?? []

//delete from the end from both lists when components equal
while projectPathComponents.count > 0 {
if pathComponents.last == projectPathComponents.last {
pathComponents.removeLast()
projectPathComponents.removeLast()
} else {
throw Error.withInfo("Logic error in parsing project WCC name, url: \(url), projectPath: \(projectPath)")
}
}

let containingFolder = pathComponents.last! + "/"
return containingFolder
}

private func parseProjectWCCIdentifier(projectUrl: String) throws -> String {

//something reproducible, but i can't figure out how Xcode generates this.
//also - it doesn't matter, AFA it's unique
let hashed = projectUrl.sha1().uppercaseString
return hashed
}

func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata {

let run = { (script: String) throws -> String in

let cd = "cd \"\(url.path!)\""
let all = [cd, script].joinWithSeparator("\n")
let response = Script.runTemporaryScript(all)
if response.terminationStatus != 0 {
throw Error.withInfo("Parsing git repo metadata failed, executing \"\(all)\", status: \(response.terminationStatus), output: \(response.standardOutput), error: \(response.standardError)")
}
return response.standardOutput
}

let origin = try self.parseOrigin(run)
let projectName = try self.parseProjectName(url)
let projectPath = try self.parseProjectPath(url, run: run)
let projectWCCName = try self.parseProjectWCCName(url, projectPath: projectPath)
let projectWCCIdentifier = try self.parseProjectWCCIdentifier(origin)

return try WorkspaceMetadata(projectName: projectName, projectPath: projectPath, projectWCCIdentifier: projectWCCIdentifier, projectWCCName: projectWCCName, projectURLString: origin)
}
}

extension String {

func split(separator: String) -> [String] {
return self.componentsSeparatedByString(separator)
}

func trim() -> String {
return self.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
}
}

82 changes: 0 additions & 82 deletions BuildaKit/SourceControlFileParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,92 +7,10 @@
//

import Foundation
import BuildaUtils

protocol SourceControlFileParser {

func supportedFileExtensions() -> [String]
func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata
}

class CheckoutFileParser: SourceControlFileParser {

func supportedFileExtensions() -> [String] {
return ["xccheckout"]
}

func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata {

//plist -> NSDictionary
guard let dictionary = NSDictionary(contentsOfURL: url) else { throw Error.withInfo("Failed to parse \(url)") }

//parse our required keys
let projectName = dictionary.optionalStringForKey("IDESourceControlProjectName")
let projectPath = dictionary.optionalStringForKey("IDESourceControlProjectPath")
let projectWCCIdentifier = dictionary.optionalStringForKey("IDESourceControlProjectWCCIdentifier")
let projectWCCName = { () -> String? in
if let wccId = projectWCCIdentifier {
if let wcConfigs = dictionary["IDESourceControlProjectWCConfigurations"] as? [NSDictionary] {
if let foundConfig = wcConfigs.filter({
if let loopWccId = $0.optionalStringForKey("IDESourceControlWCCIdentifierKey") {
return loopWccId == wccId
}
return false
}).first {
//so much effort for this little key...
return foundConfig.optionalStringForKey("IDESourceControlWCCName")
}
}
}
return nil
}()
let projectURLString = { dictionary.optionalStringForKey("IDESourceControlProjectURL") }()

return try WorkspaceMetadata(projectName: projectName, projectPath: projectPath, projectWCCIdentifier: projectWCCIdentifier, projectWCCName: projectWCCName, projectURLString: projectURLString)
}
}

class BlueprintFileParser: SourceControlFileParser {

func supportedFileExtensions() -> [String] {
return ["xcscmblueprint"]
}

func parseFileAtUrl(url: NSURL) throws -> WorkspaceMetadata {

//JSON -> NSDictionary
let data = try NSData(contentsOfURL: url, options: NSDataReadingOptions())
let jsonObject = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions())
guard let dictionary = jsonObject as? NSDictionary else { throw Error.withInfo("Failed to parse \(url)") }

//parse our required keys
let projectName = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintNameKey")
let projectPath = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey")
let projectWCCIdentifier = dictionary.optionalStringForKey("DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey")

var primaryRemoteRepositoryDictionary: NSDictionary?
if let wccId = projectWCCIdentifier {
if let wcConfigs = dictionary["DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey"] as? [NSDictionary] {
primaryRemoteRepositoryDictionary = wcConfigs.filter({
if let loopWccId = $0.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey") {
return loopWccId == wccId
}
return false
}).first
}
}

let projectURLString = primaryRemoteRepositoryDictionary?.optionalStringForKey("DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey")

var projectWCCName: String?
if
let copyPaths = dictionary["DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey"] as? [String: String],
let primaryRemoteRepoId = projectWCCIdentifier
{
projectWCCName = copyPaths[primaryRemoteRepoId]
}

return try WorkspaceMetadata(projectName: projectName, projectPath: projectPath, projectWCCIdentifier: projectWCCIdentifier, projectWCCName: projectWCCName, projectURLString: projectURLString)
}
}

13 changes: 11 additions & 2 deletions BuildaKit/XcodeProjectParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class XcodeProjectParser {

static private var sourceControlFileParsers: [SourceControlFileParser] = [
CheckoutFileParser(),
BlueprintFileParser()
BlueprintFileParser(),
]

private class func firstItemMatchingTestRecursive(url: NSURL, test: (itemUrl: NSURL) -> Bool) throws -> NSURL? {
Expand Down Expand Up @@ -91,7 +91,16 @@ public class XcodeProjectParser {
let parsed = try self.parseCheckoutOrBlueprintFile(checkoutUrl)
return parsed
} catch {
throw Error.withInfo("Cannot find the Checkout/Blueprint file, please make sure to open this project in Xcode at least once (it will generate the required Checkout/Blueprint file) and create at least one Bot from Xcode. Then please try again. Create an issue on GitHub is this issue persists. (Error \((error as NSError).localizedDescription))")

//failed to find a checkout/blueprint file, attempt to parse from repo manually
let parser = GitRepoMetadataParser()

do {
return try parser.parseFileAtUrl(url)
} catch {
//no we're definitely unable to parse workspace metadata
throw Error.withInfo("Cannot find the Checkout/Blueprint file and failed to parse repository metadata directly. Please create an issue on GitHub with anonymized information about your repository. (Error \((error as NSError).localizedDescription))")
}
}
}

Expand Down
Loading

0 comments on commit 6cb2371

Please sign in to comment.