Skip to content

Commit

Permalink
Filter dependencies by target (#6)
Browse files Browse the repository at this point in the history
* parse the podfile yaml format with target information

* run zsh as an interactive login shell

* Revert "parse the podfile yaml format with target information"

This reverts commit 4d976ec.

* get dependencies for every target

* filter dependencies by target

* fix test

* remove dependency

* make request changes

* adapt history command

* update graph command

* update documentation

* fix podfile and podfile method names

* improve test clarity

* add some more asserts for podfile parsing test

* Update README.md

Co-authored-by: Shammi Didla <sdidla@gmail.com>

* Update README.md

Co-authored-by: Shammi Didla <sdidla@gmail.com>

* Update README.md

Co-authored-by: Shammi Didla <sdidla@gmail.com>

* Update README.md

Co-authored-by: Shammi Didla <sdidla@gmail.com>

* extract podfile from the history

* fix history command

Co-authored-by: Shammi Didla <sdidla@gmail.com>
  • Loading branch information
osrufung and sdidla authored Oct 18, 2022
1 parent c91fbab commit 962d87f
Show file tree
Hide file tree
Showing 11 changed files with 380 additions and 51 deletions.
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ let package = Package(
.executable(name: "jungle", targets: ["jungle"]),
.library(name: "PodExtractor", targets: ["PodExtractor"]),
.library(name: "DependencyGraph", targets: ["DependencyGraph"]),
.library(name: "Shell", targets: ["Shell"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.1.3"),
Expand All @@ -23,7 +24,8 @@ let package = Package(
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.target(name: "PodExtractor"),
.target(name: "DependencyGraph")
.target(name: "DependencyGraph"),
.target(name: "Shell")
]
),
.testTarget(
Expand All @@ -36,6 +38,7 @@ let package = Package(
name: "PodExtractor",
dependencies: [
.target(name: "DependencyModule"),
.target(name: "Shell"),
.product(name: "Yams", package: "Yams")
]
),
Expand All @@ -57,6 +60,11 @@ let package = Package(
// DependencyModule
.target(
name: "DependencyModule"
),

// Shell
.target(
name: "Shell"
)
]
)
31 changes: 17 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A Swift command line tool to extract dependency information from a CocoaPods-bas
- Number of dependant modules
- Compare stats between different branches or even through the git history

You can read more information about dependency complexity in our Technical article ["How to control your dependencies"](https://medium.com/@OswaldoRubio/how-to-control-your-dependencies-7690cc7b1c40).
You can read more information about dependency complexity in our Technical article ["How to control your dependencies"](https://tech.xing.com/how-to-control-your-ios-dependencies-7690cc7b1c40).

## Table of contents

Expand Down Expand Up @@ -48,14 +48,15 @@ swift build -c release
```shell
OVERVIEW: Displays historic complexity of the dependency graph

USAGE: jungle history [--since <since>] [--pod <pod>] [--output-format <output-format>] [<directory-path>]
USAGE: jungle history [--since <since>] [--pod <pod>] --target <target> [--output-format <output-format>] [<directory-path>]

ARGUMENTS:
<directory-path> Path to the directory where Podfile.lock is located (default: .)

OPTIONS:
--since <since> Equivalent to git-log --since: Eg: '6 months ago' (default: 6 months ago)
--pod <pod> The Pod to generate a report for. Omitting this generates a report for a virtual `App` target that imports all Pods
--pod <pod> The Pod to generate a report for. Specifying a pod disregards the target parameter
--target <target> The target in your Podfile file to be used
--output-format <output-format>
csv or json (default: csv)
--version Show the version.
Expand All @@ -66,7 +67,7 @@ OPTIONS:
Example:

```shell
jungle history ProjectDirectory/ --since '1 week ago'
jungle history --target App ProjectDirectory/ --since '1 week ago'

2022-08-30T15:12:14+02:00;cdb9d2ce64a;124;21063;Author;commit message
2022-09-02T11:02:12+02:00;4fdf3a157a4;124;21063;Author;commit message
Expand All @@ -78,14 +79,15 @@ Now;Current;124;21063;;
```shell
OVERVIEW: Compares the current complexity of the dependency graph to others versions in git

USAGE: jungle compare [--to <git-object> ...] [--pod <pod>] [<directory-path>]
USAGE: jungle compare [--to <git-object> ...] [--pod <pod>] --target <target> [<directory-path>]

ARGUMENTS:
<directory-path> Path to the directory where Podfile.lock is located (default: .)

OPTIONS:
--to <git-object> The git objects to compare the current graph to. Eg: - 'main', 'my_branch', 'some_commit_hash'. (default: HEAD, main, master)
--pod <pod> The Pod to compare. Omitting this generates compares a virtual `App` target that imports all Pods
--to <git-object> The git objects to compare the current graph to. Eg: - 'main', 'my_branch', 'some_commit_hash'. (default: HEAD, main)
--pod <pod> The Pod to compare. Specifying a pod disregards the target parameter
--target <target> The target in your Podfile file to be used
--version Show the version.
-h, --help Show help information.
```
Expand All @@ -94,7 +96,7 @@ Example:

```shell

jungle compare ProjectDirectory/ --to main
jungle compare --target App ProjectDirectory/ --to main
[
{
"modules" : 124,
Expand All @@ -116,16 +118,17 @@ jungle compare ProjectDirectory/ --to main
```shell
OVERVIEW: Outputs the dependency graph in DOT format

USAGE: jungle graph [--of <git-object>] [--pod <pod>] [<directory-path>]
USAGE: jungle graph [--of <git-object>] [--pod <pod>] --target <target> [<directory-path>]

ARGUMENTS:
<directory-path> Path to the directory where Podfile.lock is located (default: .)

OPTIONS:
--of <git-object> A git object representing the version to draw the graph for. Eg: - 'main', 'my_branch', 'some_commit_hash'.
--pod <pod> The Pod to graph. Omitting this generates compares a virtual `App` target that imports all Pods
--pod <pod> The Pod to compare. Specifying a pod disregards the target parameter
--target <target> The target in your Podfile file to be used
--version Show the version.
-h, --help Show help information.
-h, --help Show help information

```

Expand All @@ -137,16 +140,16 @@ Outputs DOT format which can be viewed using http://viz-js.com
💡 Copy CSV (to paste in a spreadsheet) or DOT (to paste at http://viz-js.com) to the clipboard using `pbcopy`

```shell
jungle graph | pbcopy
jungle history | pbcopy
jungle graph --target App ProjectDirectory/ | pbcopy
jungle history --target App ProjectDirectory/ | pbcopy
```


💡 Use Graphviz tool to generate your own graphs

```shell
brew install graphviz
jungle graph | dot -Tpng -o graph.png && open graph.png
jungle graph --target App ProjectDirectory/ | dot -Tpng -o graph.png && open graph.png
```

## Contributing
Expand Down
6 changes: 5 additions & 1 deletion Sources/DependencyGraph/Graph+Make.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ public enum GraphError: Error {
}

public extension Graph {
static func makeForVirtualAppModule(name: String, dependencies: [Module]) throws -> Graph {
static func make(rootTargetName name: String, dependencies: [Module], targetDependencies: [String]?) throws -> Graph {

let dependencies = dependencies
.filter { targetDependencies?.contains($0.name) ?? true }

let appModule = Module(name: name, dependencies: dependencies.map(\.name))

return try makeForModule(name: name, dependencies: [appModule] + dependencies)
Expand Down
81 changes: 78 additions & 3 deletions Sources/PodExtractor/Module+Podfile.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,90 @@
import Yams
import DependencyModule
import Foundation
import Shell

public enum PodError: Error {
case yamlParsingFailed
case missingPodsDictionary
case missingSpecReposDictionary
case failedParsingPod
case failedParsingPod(String)
case failedParsingPodName
case podTargetNotFound
}

public func extractModulesFromPodfile(_ contents: String) throws -> [Module] {

struct Podfile: Decodable {
let sources: [String]?
let targetDefinitions: [TargetDefinition]

struct TargetDefinition: Decodable {
let abstract: Bool
let name: String
let children: [ChildrenDefinition]
}

struct ChildrenDefinition: Decodable {
let name: String
let dependencies: [Dependency]
let children: [ChildrenDefinition]?
var asTarget: [Module] {
let target = Module(name: name, dependencies: dependencies.compactMap(\.name))
let children = children ?? []
return children.reduce([target]) { $0 + $1.asTarget }
}

struct Dependency: Decodable {
let name: String?

init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let name = try? container.decode(String.self) {
self.name = name
} else if let keyName = try? container.decode([String: [String]].self).keys.first {
self.name = keyName

} else if let keyName = try? container.decode([String: [[String: String]]].self).keys.first {
self.name = keyName
} else {
self.name = nil
}
}

}
}
}

public func moduleFromPodfile(_ contents: String, on target: String) throws -> Module? {
let tmp_podfile = try shell("mktemp PodfileXXXX").trimmingCharacters(in: .newlines)
try contents.write(toFile: tmp_podfile, atomically: true, encoding: .utf8)
let podfileJSON = try shell("pod ipc podfile-json \(tmp_podfile) --silent")
_ = try shell("rm \(tmp_podfile)")
return try moduleFromJSONPodfile(podfileJSON, onTarget: target)
}

public func moduleFromJSONPodfile(_ contents: String, onTarget target: String) throws -> Module? {
try modulesFromJSONPodfile(contents)
.first(where: { $0.name == target })
}

public func modulesFromJSONPodfile(_ contents: String) throws -> [Module] {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let data = contents.data(using: .utf8),
let pod = try? decoder.decode(Podfile.self, from: data)
else {
throw PodError.failedParsingPod(contents)
}
//first target is always Pods
guard let targetsRaw = pod.targetDefinitions.first?.children
else {
throw PodError.podTargetNotFound
}

return targetsRaw.flatMap(\.asTarget)
}

public func extractModulesFromPodfileLock(_ contents: String) throws -> [Module] {
// parse YAML to JSON
guard let yaml = try? Yams.load(yaml: contents) else {
throw PodError.yamlParsingFailed
Expand Down Expand Up @@ -51,7 +126,7 @@ private func extractPodFromJSON(_ json: Any) throws -> Module {
)

} else {
throw PodError.failedParsingPod
throw PodError.failedParsingPod(json as? String ?? "")
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import Foundation

func shell(_ command: String, at currentDirectoryURL: URL) throws -> String {
public func shell(_ command: String, at currentDirectoryURL: URL? = nil) throws -> String {
let task = Process()
let pipe = Pipe()

task.standardOutput = pipe
task.standardError = pipe
task.arguments = ["-c", command]
task.arguments = ["--login", "-c", command]
task.launchPath = "/bin/zsh"
task.standardInput = nil
task.currentDirectoryURL = currentDirectoryURL

if let currentDirectoryURL = currentDirectoryURL {
task.currentDirectoryURL = currentDirectoryURL
}

try task.run()

let data = pipe.fileHandleForReading.readDataToEndOfFile()
let output = String(data: data, encoding: .utf8)!

return output
}

48 changes: 41 additions & 7 deletions Sources/jungle/Commands/CompareCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ import ArgumentParser
import Foundation
import DependencyGraph
import PodExtractor
import DependencyModule
import Shell

public enum CompareError: Error {
case targetNotFound(target: String)
}

extension CompareError: CustomStringConvertible {
public var description: String {
switch self {
case .targetNotFound(let target):
return "\"\(target)\" target not found!. Please, provide an existent target in your Podfile."
}
}
}

struct CompareCommand: ParsableCommand {
static var configuration = CommandConfiguration(
Expand All @@ -16,8 +31,11 @@ struct CompareCommand: ParsableCommand {
)
var gitObjects: [String] = ["HEAD", "main", "master"]

@Option(help: "The Pod to compare. Omitting this generates compares a virtual `App` target that imports all Pods")
@Option(help: "The Pod to compare. If you specify something, target parameter will be ommited")
var pod: String?

@Option(help: "The target in your Podfile file to be used")
var target: String

@Argument(help: "Path to the directory where Podfile.lock is located")
var directoryPath: String = "."
Expand All @@ -26,17 +44,33 @@ struct CompareCommand: ParsableCommand {
let directoryPath = (directoryPath as NSString).expandingTildeInPath
let directoryURL = URL(fileURLWithPath: directoryPath, isDirectory: true)

// Choose the target to analyze
let podfileJSON = try shell("pod ipc podfile-json Podfile --silent", at: directoryURL)

guard let currentTargetDependencies = try moduleFromJSONPodfile(podfileJSON, onTarget: target) else {
throw CompareError.targetNotFound(target: target)
}

let current = try process(
label: "Current",
pod: pod,
podfile: String(contentsOf: directoryURL.appendingPathComponent("Podfile.lock"))
podfile: String(contentsOf: directoryURL.appendingPathComponent("Podfile.lock")),
target: currentTargetDependencies
)

let outputs = [current] + gitObjects.compactMap {
try? process(
guard
let podfile = try? shell("git show \($0):Podfile"),
let entryTargetDependencies = try? moduleFromPodfile(podfile, on: target)
else {
return nil
}

return try? process(
label: $0,
pod: pod,
podfile: shell("git show \($0):Podfile.lock", at: directoryURL)
podfile: shell("git show \($0):Podfile.lock", at: directoryURL),
target: entryTargetDependencies
)
}

Expand All @@ -48,14 +82,14 @@ struct CompareCommand: ParsableCommand {
}
}

func process(label: String, pod: String?, podfile: String) throws -> CompareStatsOutput {
let dependencies = try extractModulesFromPodfile(podfile)
func process(label: String, pod: String?, podfile: String, target: Module) throws -> CompareStatsOutput {
let dependencies = try extractModulesFromPodfileLock(podfile)

let graph: Graph
if let pod = pod {
graph = try Graph.makeForModule(name: pod, dependencies: dependencies)
} else {
graph = try Graph.makeForVirtualAppModule(name: "App", dependencies: dependencies)
graph = try Graph.make(rootTargetName: target.name, dependencies: dependencies, targetDependencies: target.dependencies)
}

return CompareStatsOutput(label: label, graph: graph)
Expand Down
Loading

0 comments on commit 962d87f

Please sign in to comment.