-
-
Notifications
You must be signed in to change notification settings - Fork 320
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
Add API to search for files in a group with path. Handles non-unique filenames in groups #671
Changes from 3 commits
4bbcf1d
8cd16c9
51abda8
2ad12d4
87d18b9
e3b1fe1
a3188c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,6 +117,8 @@ public extension PBXGroup { | |
} | ||
|
||
/// Returns the file in the group with the given name. | ||
/// - Note: Performs a linear search based on the file's name _only_. To search for a specific file | ||
/// based on file-system location, use the `file(with path:)` method. | ||
/// | ||
/// - Parameter name: file name. | ||
/// - Returns: file with the given name contained in the given parent group. | ||
|
@@ -126,6 +128,27 @@ public extension PBXGroup { | |
.first(where: { $0.name == name }) | ||
} | ||
|
||
/// Returns the file in the group with the given path, based on locations on-disk. | ||
/// - Note: Paths function as-in Xcode; with respect to the group's `PBXSourceTree`. | ||
/// Internally, identity is calculated based on normalized absolute paths for all files involved; i.e. actual | ||
/// location in the file system. | ||
/// | ||
/// - Parameters: | ||
/// - path: a file path to search for. | ||
/// - Returns: an existing reference to that file, or `nil` if not found | ||
func file(with path: Path) -> PBXFileReference? { | ||
func absolutePath(for fileRef: PBXFileElement) -> Path? { | ||
try? fileRef.fullPath(sourceRoot: ".") | ||
} | ||
|
||
guard let groupAbsPath = absolutePath(for: self) else { return nil } | ||
let fileNormalizedAbsolutePath = (groupAbsPath + path).normalize() | ||
return children.first { | ||
guard let candidateAbsPath = absolutePath(for: $0) else { return false } | ||
return candidateAbsPath.normalize() == fileNormalizedAbsolutePath | ||
} as? PBXFileReference | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The algorithm has now quite importantly changed, and is much improved, but requires some feedback:
This way, it's guaranteed that any kind of path (relative, absolute, traversals) work as input, since absolute paths are always generated internally as the file system "source of truth" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think using "." for the sourceRoot is wrong and will only work correctly for cases when the working dir is the same as project location. In other cases it will work in unpredictable way e.g. if you will have absolute file reference in project and a relative one which will resolve in incorrect way they will not be comparable. I'd advise to pass a real sourceRoot to the method. |
||
} | ||
|
||
/// Creates a group with the given name and returns it. | ||
/// | ||
/// - Parameters: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -199,6 +199,77 @@ final class PBXGroupTests: XCTestCase { | |
XCTAssertEqual(file!.sourceTree, .developerDir) | ||
} | ||
|
||
func test_whenSearchingForFilesByPath_thenNormalizedAbsolutePathsAreUsedInternallyForUniqueness() throws { | ||
let project = makeEmptyPBXProj() | ||
let subDirectoryName = UUID().uuidString | ||
|
||
func prepareDirectories() throws -> (projectDir: Path, groupDir: Path, group: PBXGroup) { | ||
let absolutePath = try Path.uniqueTemporary() | ||
let splitSize = 2 | ||
let projectDir = Path(components: absolutePath.components[0..<absolutePath.components.count-splitSize]) | ||
let groupDir = Path(components: absolutePath.components.reversed()[0..<splitSize].reversed()) | ||
XCTAssertEqual(projectDir + groupDir, absolutePath) | ||
|
||
let subDir = absolutePath + subDirectoryName | ||
try subDir.mkdir() | ||
|
||
let group = PBXGroup(children: [], sourceTree: .group, name: "group", path: ".") | ||
let parent = PBXGroup(children: [group], sourceTree: .absolute, name: "parent", path: projectDir.string) | ||
project.add(object: parent) | ||
project.add(object: group) | ||
|
||
return (projectDir, groupDir, group) | ||
} | ||
|
||
let (projectDir, groupDir, theGroup) = try prepareDirectories() | ||
|
||
let fileName = UUID().uuidString | ||
let file1Path = groupDir + fileName | ||
let file2Path = groupDir + subDirectoryName + fileName | ||
|
||
let fileReferenceFor = try [file1Path, file2Path] | ||
.reduce([Path: PBXFileReference]()) { map, filePath in | ||
let absolutePath = projectDir + filePath | ||
try Data().write(to: absolutePath.url) | ||
let file = try theGroup.addFile( | ||
at: absolutePath, | ||
sourceTree: .group, | ||
sourceRoot: projectDir) | ||
|
||
return map.merging([filePath: file]) { _, new in new } | ||
} | ||
|
||
func assert(filePath: Path, hasReference: PBXFileReference?, line: UInt = #line) { | ||
let actual = theGroup.file(with: filePath) | ||
if let expected = hasReference { | ||
XCTAssertEqual(actual, expected, file: #file, line: line) | ||
} else { | ||
XCTAssertNil(actual, file: #file, line: line) | ||
} | ||
} | ||
|
||
assert( | ||
filePath: file1Path, | ||
hasReference: fileReferenceFor[file1Path] | ||
) | ||
assert( | ||
filePath: file2Path, | ||
hasReference: fileReferenceFor[file2Path] | ||
) | ||
assert( | ||
filePath: groupDir + subDirectoryName + ".." + fileName, | ||
hasReference: fileReferenceFor[file1Path] | ||
) | ||
assert( | ||
filePath: groupDir + "foo/.." + subDirectoryName + fileName, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Testing traversals to parents |
||
hasReference: fileReferenceFor[file2Path] | ||
) | ||
assert( | ||
filePath: Path(UUID().uuidString), | ||
hasReference: nil | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This got a little involved, but the basic idea is to recreate Xcode-like scenarios:
|
||
} | ||
|
||
private func makeEmptyPBXProj() -> PBXProj { | ||
PBXProj( | ||
rootObject: nil, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note on the limitations, instead of deprecation