Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parsing metadata information directly from git repo (in the absence of checkout files) #188

Merged
merged 1 commit into from
Oct 21, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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