Skip to content

Commit f33a956

Browse files
supersonicbytedaveverwer
authored andcommitted
Implement forked from feature
1 parent 18e22dd commit f33a956

File tree

8 files changed

+185
-25
lines changed

8 files changed

+185
-25
lines changed

Sources/App/Commands/Ingest.swift

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,30 @@ func ingest(client: Client,
124124
Current.logger().warning("storeS3Readme failed")
125125
s3Readme = .error("\(error)")
126126
}
127+
128+
var fork: Fork?
129+
do {
130+
if let parentUrl = metadata.repository?.normalizedParentUrl {
131+
if let packageId = try await Package.query(on: database)
132+
.filter(\.$url == parentUrl)
133+
.first()?.id {
134+
fork = .parentId(packageId)
135+
} else {
136+
fork = .parentURL(parentUrl)
137+
}
138+
}
139+
} catch {
140+
Current.logger().warning("updating forked from failed")
141+
}
127142

128143
try await updateRepository(on: database,
129144
for: repo,
130145
metadata: metadata,
131146
licenseInfo: license,
132147
readmeInfo: readme,
133-
s3Readme: s3Readme)
148+
s3Readme: s3Readme,
149+
forkedFrom: fork)
150+
134151
return pkg
135152
}
136153

@@ -177,7 +194,8 @@ func updateRepository(on database: Database,
177194
metadata: Github.Metadata,
178195
licenseInfo: Github.License?,
179196
readmeInfo: Github.Readme?,
180-
s3Readme: S3Readme?) async throws {
197+
s3Readme: S3Readme?,
198+
forkedFrom: Fork? = nil) async throws {
181199
guard let repoMetadata = metadata.repository else {
182200
if repository.$package.value == nil {
183201
try await repository.$package.load(on: database)
@@ -208,6 +226,7 @@ func updateRepository(on database: Database,
208226
repository.releases = repoMetadata.releases.nodes.map(Release.init(from:))
209227
repository.stars = repoMetadata.stargazerCount
210228
repository.summary = repoMetadata.description
229+
repository.forkedFrom = forkedFrom
211230

212231
try await repository.save(on: database)
213232
}
@@ -224,3 +243,13 @@ private extension Github.Metadata.Repository {
224243
var repositoryOwner: String? { owner.login }
225244
var repositoryName: String? { name }
226245
}
246+
247+
private extension Github.Metadata.Repository {
248+
// Returns a normalized version of the URL. Adding a `.git` if not present.
249+
var normalizedParentUrl: String? {
250+
guard let url = parent.url else { return nil }
251+
guard !url.hasSuffix(".git") else { return url }
252+
let normalizedUrl = url + ".git"
253+
return normalizedUrl
254+
}
255+
}

Sources/App/Core/Github.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ extension Github {
284284
homepageUrl
285285
isArchived
286286
isFork
287+
parent {
288+
url
289+
}
287290
isInOrganization
288291
licenseInfo {
289292
name
@@ -348,6 +351,7 @@ extension Github {
348351
var isArchived: Bool
349352
// periphery:ignore
350353
var isFork: Bool
354+
var parent: Parent
351355
var isInOrganization: Bool
352356
var licenseInfo: LicenseInfo?
353357
var mergedPullRequests: IssueNodes
@@ -459,6 +463,10 @@ extension Github {
459463
}
460464
}
461465
}
466+
467+
struct Parent: Decodable, Equatable {
468+
var url: String?
469+
}
462470
}
463471

464472
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Fluent
16+
import SQLKit
17+
18+
19+
struct UpdateRepositoryAddFork: AsyncMigration {
20+
func prepare(on database: Database) async throws {
21+
try await database.schema("repositories")
22+
.field("forked_from", .json)
23+
// delete old `forked_from_id` field
24+
.deleteField("forked_from_id")
25+
.update()
26+
}
27+
28+
func revert(on database: Database) async throws {
29+
try await database.schema("repositories")
30+
.deleteField("forked_from")
31+
.field("forked_from_id", .uuid,
32+
.references("repositories", "id")).unique(on: "forked_from_id")
33+
.update()
34+
}
35+
}

Sources/App/Models/Repository.swift

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ final class Repository: @unchecked Sendable, Model, Content {
3434

3535
// reference fields
3636

37-
@OptionalParent(key: "forked_from_id") // TODO: remove or implement
38-
var forkedFrom: Repository?
39-
4037
@Parent(key: "package_id")
4138
var package: Package
4239

@@ -122,6 +119,9 @@ final class Repository: @unchecked Sendable, Model, Content {
122119

123120
@Field(key: "summary")
124121
var summary: String?
122+
123+
@Field(key: "forked_from")
124+
var forkedFrom: Fork?
125125

126126
// initializers
127127

@@ -135,7 +135,7 @@ final class Repository: @unchecked Sendable, Model, Content {
135135
firstCommitDate: Date? = nil,
136136
forks: Int = 0,
137137
fundingLinks: [FundingLink] = [],
138-
forkedFrom: Repository? = nil,
138+
forkedFrom: Fork? = nil,
139139
homepageUrl: String? = nil,
140140
isArchived: Bool = false,
141141
isInOrganization: Bool = false,
@@ -164,9 +164,7 @@ final class Repository: @unchecked Sendable, Model, Content {
164164
self.commitCount = commitCount
165165
self.firstCommitDate = firstCommitDate
166166
self.forks = forks
167-
if let forkId = forkedFrom?.id {
168-
self.$forkedFrom.id = forkId
169-
}
167+
self.forkedFrom = forkedFrom
170168
self.fundingLinks = fundingLinks
171169
self.homepageUrl = homepageUrl
172170
self.isArchived = isArchived
@@ -273,3 +271,8 @@ enum S3Readme: Codable, Equatable {
273271
}
274272
}
275273
}
274+
275+
enum Fork: Codable, Equatable {
276+
case parentId(Package.Id)
277+
case parentURL(String)
278+
}

Sources/App/configure.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ public func configure(_ app: Application) async throws -> String {
334334
do { // Migration 078 - Add `build_date` and `commit_hash` to `builds`
335335
app.migrations.add(UpdateBuildAddBuildDateCommitHash())
336336
}
337+
do { // Migration 079 - Add `fork` to `repositories`
338+
app.migrations.add(UpdateRepositoryAddFork())
339+
}
337340

338341
app.asyncCommands.use(Analyze.Command(), as: "analyze")
339342
app.asyncCommands.use(CreateRestfileCommand(), as: "create-restfile")

Tests/AppTests/IngestorTests.swift

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ class IngestorTests: AppTestCase {
125125
Date(timeIntervalSince1970: 1),
126126
],
127127
license: .mit,
128-
openIssues: 1,
128+
openIssues: 1,
129+
parentUrl: nil,
129130
openPullRequests: 2,
130131
owner: "foo",
131132
pullRequestsClosedAtDates: [
@@ -208,6 +209,7 @@ class IngestorTests: AppTestCase {
208209
issuesClosedAtDates: [],
209210
license: .mit,
210211
openIssues: 1,
212+
parentUrl: nil,
211213
openPullRequests: 2,
212214
owner: "foo",
213215
pullRequestsClosedAtDates: [],
@@ -353,6 +355,7 @@ class IngestorTests: AppTestCase {
353355
issuesClosedAtDates: [],
354356
license: .mit,
355357
openIssues: 0,
358+
parentUrl: nil,
356359
openPullRequests: 0,
357360
owner: "owner",
358361
pullRequestsClosedAtDates: [],
@@ -594,4 +597,92 @@ class IngestorTests: AppTestCase {
594597
let postMigrationFetchedRepo = try await XCTUnwrapAsync(try await Repository.query(on: app.db).first())
595598
XCTAssertEqual(postMigrationFetchedRepo.s3Readme, .cached(s3ObjectUrl: "object-url", githubEtag: ""))
596599
}
600+
601+
func test_ingest_storeForkedFromPackageInSPI() async throws {
602+
let pkg = Package(url: "https://github.com/foo/bar.git".url,
603+
processingStage: .analysis)
604+
let forkedPkg = Package(
605+
url: "https://github.com/taz/bar.git",
606+
processingStage: .reconciliation
607+
)
608+
try await pkg.save(on: app.db)
609+
try await forkedPkg.save(on: app.db)
610+
Current.fetchMetadata = { _, owner, repository in
611+
.mock(owner: owner, repository: repository, parentUrl: "https://github.com/foo/bar.git")
612+
}
613+
614+
// MUT
615+
try await ingest(client: app.client, database: app.db, mode: .limit(1))
616+
617+
guard let forkedId = forkedPkg.id else {
618+
XCTFail("Failed to get forked package id")
619+
return
620+
}
621+
622+
guard let id = pkg.id else {
623+
XCTFail("Failed to get package id")
624+
return
625+
}
626+
627+
let repo = try await Repository
628+
.query(on: app.db)
629+
.filter(\Repository.$package.$id == forkedId).first()
630+
631+
XCTAssertNotNil(repo?.forkedFrom)
632+
633+
XCTAssertEqual(repo?.forkedFrom, .parentId(id))
634+
635+
}
636+
637+
func test_ingest_storeForkedFromPackageNotInSPI() async throws {
638+
let forkedPkg = Package(
639+
url: "https://github.com/taz/bar.git",
640+
processingStage: .reconciliation
641+
)
642+
try await forkedPkg.save(on: app.db)
643+
Current.fetchMetadata = { _, owner, repository in
644+
.mock(owner: owner, repository: repository, parentUrl: "https://github.com/foo/bar.git")
645+
}
646+
647+
// MUT
648+
try await ingest(client: app.client, database: app.db, mode: .limit(1))
649+
650+
guard let forkedId = forkedPkg.id else {
651+
XCTFail("Failed to get forked package id")
652+
return
653+
}
654+
655+
let repo = try await Repository
656+
.query(on: app.db)
657+
.filter(\Repository.$package.$id == forkedId).first()
658+
659+
XCTAssertNotNil(repo?.forkedFrom)
660+
661+
XCTAssertEqual(repo?.forkedFrom, .parentURL("https://github.com/foo/bar.git"))
662+
}
663+
664+
func test_ingest_storeForkedFromShouldNeNil() async throws {
665+
let forkedPkg = Package(
666+
url: "https://github.com/taz/bar.git",
667+
processingStage: .reconciliation
668+
)
669+
try await forkedPkg.save(on: app.db)
670+
Current.fetchMetadata = { _, owner, repository in
671+
.mock(owner: owner, repository: repository, parentUrl: nil)
672+
}
673+
674+
// MUT
675+
try await ingest(client: app.client, database: app.db, mode: .limit(1))
676+
677+
guard let forkedId = forkedPkg.id else {
678+
XCTFail("Failed to get forked package id")
679+
return
680+
}
681+
682+
let repo = try await Repository
683+
.query(on: app.db)
684+
.filter(\Repository.$package.$id == forkedId).first()
685+
686+
XCTAssertNil(repo?.forkedFrom)
687+
}
597688
}

Tests/AppTests/Mocks/GithubMetadata+mock.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ extension Github.Metadata {
2525
issuesClosedAtDates: [],
2626
license: .mit,
2727
openIssues: 3,
28+
parentUrl: nil,
2829
openPullRequests: 0,
2930
owner: "packageOwner",
3031
pullRequestsClosedAtDates: [],
@@ -34,14 +35,15 @@ extension Github.Metadata {
3435
stars: 2,
3536
summary: "desc")
3637

37-
static func mock(owner: String, repository: String) -> Self {
38+
static func mock(owner: String, repository: String, parentUrl: String? = nil) -> Self {
3839
return .init(defaultBranch: "main",
3940
forks: owner.count + repository.count,
4041
homepageUrl: nil,
4142
isInOrganization: false,
4243
issuesClosedAtDates: [],
4344
license: .mit,
4445
openIssues: 3,
46+
parentUrl: parentUrl,
4547
openPullRequests: 0,
4648
owner: owner,
4749
pullRequestsClosedAtDates: [],
@@ -60,6 +62,7 @@ extension Github.Metadata {
6062
issuesClosedAtDates: [Date],
6163
license: License,
6264
openIssues: Int,
65+
parentUrl: String?,
6366
openPullRequests: Int,
6467
owner: String,
6568
pullRequestsClosedAtDates: [Date],
@@ -84,7 +87,8 @@ extension Github.Metadata {
8487
fundingLinks: fundingLinks,
8588
homepageUrl: homepageUrl,
8689
isArchived: false,
87-
isFork: false,
90+
isFork: false,
91+
parent: .init(url: parentUrl),
8892
isInOrganization: isInOrganization,
8993
licenseInfo: .init(name: license.fullName, key: license.rawValue),
9094
mergedPullRequests: .init(closedAtDates: []),

Tests/AppTests/RepositoryTests.swift

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,19 +178,6 @@ final class RepositoryTests: AppTestCase {
178178
}
179179
}
180180

181-
func test_forkedFrom_relationship() async throws {
182-
let p1 = Package(url: "p1")
183-
try await p1.save(on: app.db)
184-
let p2 = Package(url: "p2")
185-
try await p2.save(on: app.db)
186-
187-
// test forked from link
188-
let parent = try Repository(package: p1)
189-
try await parent.save(on: app.db)
190-
let child = try Repository(package: p2, forkedFrom: parent)
191-
try await child.save(on: app.db)
192-
}
193-
194181
func test_delete_cascade() async throws {
195182
// delete package must delete repository
196183
let pkg = Package(id: UUID(), url: "1")

0 commit comments

Comments
 (0)