Skip to content

Commit 0c51480

Browse files
abstraktorslisson
authored andcommitted
feat(model-server): implement /repositories/{repo}/branches/{branch} with failIfExists=true
1 parent 68dd14c commit 0c51480

File tree

3 files changed

+105
-6
lines changed

3 files changed

+105
-6
lines changed

model-server-openapi/specifications/model-server-v2.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,13 @@ paths:
359359
required: false
360360
schema:
361361
type: boolean
362+
default: false
363+
- name: failIfExists
364+
in: "query"
365+
required: false
366+
schema:
367+
type: boolean
368+
default: false
362369
# this was autogenerated but will break tests
363370
# we need to manually design the content to allow
364371
# type safe access on our APIs
@@ -372,6 +379,12 @@ paths:
372379
responses:
373380
"200":
374381
$ref: '#/components/responses/versionDelta'
382+
"409":
383+
description: "Branch already exists and `failIfExists` was set to true."
384+
content:
385+
application/problem+json:
386+
schema:
387+
$ref: 'problem.yaml#/Problem'
375388
default:
376389
$ref: '#/components/responses/GeneralError'
377390
/repositories/{repository}/branches/{branch}/frontend:

model-server/src/main/kotlin/org/modelix/model/server/handlers/ModelReplicationServer.kt

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -396,8 +396,10 @@ class ModelReplicationServer(
396396
repository: String,
397397
branch: String,
398398
force: Boolean?,
399+
failIfExists: Boolean?,
399400
) {
400401
val force = force == true
402+
val failIfExists = failIfExists == true
401403
checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).run { if (force) forcePush else push })
402404

403405
val branchRef = repositoryId(repository).getBranchReference(branch)
@@ -408,23 +410,44 @@ class ModelReplicationServer(
408410
@OptIn(RequiresTransaction::class) // no transactions required for immutable store
409411
repositoriesManager.getStoreClient(RepositoryId(repository), true).putAll(objectsFromClient)
410412
}
413+
suspend fun <R : Any> writeToBranch(writeAction: () -> R, onSuccess: suspend (R) -> Unit) {
414+
val result =
415+
@OptIn(RequiresTransaction::class)
416+
runWrite {
417+
if (failIfExists && repositoriesManager.getVersionHash(branchRef) != null) {
418+
null
419+
} else {
420+
writeAction()
421+
}
422+
}
423+
if (result == null) {
424+
call.respond(
425+
HttpStatusCode.Conflict,
426+
"Branch $branch in repository $repository already exists",
427+
)
428+
} else {
429+
onSuccess(result)
430+
}
431+
}
411432

412433
if (force) {
413434
@OptIn(RequiresTransaction::class)
414-
runWrite {
435+
writeToBranch({
415436
repositoriesManager.forcePush(branchRef, deltaFromClient.versionHash)
416-
}
417-
call.respondDelta(RepositoryId(repository), deltaFromClient.versionHash, ObjectDeltaFilter(deltaFromClient.versionHash))
437+
}, {
438+
call.respondDelta(RepositoryId(repository), deltaFromClient.versionHash, ObjectDeltaFilter(deltaFromClient.versionHash))
439+
})
418440
} else {
419441
// Run a merge outside a transaction to keep the transaction for the actual merge smaller.
420442
// If there are no concurrent pushes on the same branch, then all the work is done here.
421443
val preMergedVersion = repositoriesManager.mergeChangesWithoutPush(branchRef, deltaFromClient.versionHash)
422444

423445
@OptIn(RequiresTransaction::class)
424-
val mergedHash = runWrite {
446+
writeToBranch({
425447
repositoriesManager.mergeChanges(branchRef, preMergedVersion)
426-
}
427-
call.respondDelta(RepositoryId(repository), mergedHash, ObjectDeltaFilter(deltaFromClient.versionHash))
448+
}, { mergedHash ->
449+
call.respondDelta(RepositoryId(repository), mergedHash, ObjectDeltaFilter(deltaFromClient.versionHash))
450+
})
428451
}
429452
}
430453

model-server/src/test/kotlin/org/modelix/model/server/handlers/ModelReplicationServerTest.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import kotlinx.coroutines.flow.flow
4141
import kotlinx.coroutines.flow.onEmpty
4242
import kotlinx.coroutines.test.runTest
4343
import kotlinx.coroutines.withTimeout
44+
import org.modelix.model.IVersion
4445
import org.modelix.model.ObjectDeltaFilter
4546
import org.modelix.model.api.IConceptReference
4647
import org.modelix.model.client2.ModelClientV2
@@ -49,6 +50,7 @@ import org.modelix.model.client2.runWrite
4950
import org.modelix.model.client2.useVersionStreamFormat
5051
import org.modelix.model.lazy.RepositoryId
5152
import org.modelix.model.server.api.BranchInfo
53+
import org.modelix.model.server.api.v2.VersionDelta
5254
import org.modelix.model.server.api.v2.VersionDeltaStream
5355
import org.modelix.model.server.api.v2.VersionDeltaStreamV2
5456
import org.modelix.model.server.installDefaultServerPlugins
@@ -603,6 +605,67 @@ class ModelReplicationServerTest {
603605
response shouldHaveStatus HttpStatusCode.NotFound
604606
problem.type shouldBe "/problems/object-value-not-found"
605607
}
608+
609+
@Test
610+
fun `postRepositoryBranch with failIfExists true returns 409 if branch exists`() = runWithTestModelServer { _, fixture ->
611+
// Arrange
612+
val repositoryId = RepositoryId("repo1")
613+
val branch = "existingBranch"
614+
var version: IVersion? = null
615+
@OptIn(RequiresTransaction::class)
616+
fixture.repositoriesManager.getTransactionManager().runWrite {
617+
version = fixture.repositoriesManager.createRepository(repositoryId, null)
618+
fixture.repositoriesManager.forcePush(
619+
repositoryId.getBranchReference(branch),
620+
version.getContentHash(),
621+
)
622+
}
623+
624+
// Act
625+
val client = createClient { install(ContentNegotiation) { json() } }
626+
val response = client.post {
627+
url {
628+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branch)
629+
parameters.append("failIfExists", "true")
630+
}
631+
useVersionStreamFormat()
632+
contentType(ContentType.Application.Json)
633+
setBody(VersionDelta(version!!.getContentHash(), null, objectsMap = mapOf<String, String>()))
634+
}
635+
636+
// Assert
637+
assertEquals(HttpStatusCode.Conflict, response.status)
638+
assertContains(response.bodyAsText(), "already exists")
639+
}
640+
641+
@Test
642+
fun `postRepositoryBranch with failIfExists true creates branch if not exists`() = runWithTestModelServer { _, fixture ->
643+
// Arrange
644+
val repositoryId = RepositoryId("repo1")
645+
val branch = "newBranch"
646+
647+
var version: IVersion? = null
648+
@OptIn(RequiresTransaction::class)
649+
fixture.repositoriesManager.getTransactionManager().runWrite {
650+
version = fixture.repositoriesManager.createRepository(repositoryId, null)
651+
}
652+
653+
// Act
654+
val client = createClient { install(ContentNegotiation) { json() } }
655+
val response = client.post {
656+
url {
657+
appendPathSegments("v2", "repositories", repositoryId.id, "branches", branch)
658+
parameters.append("failIfExists", "true")
659+
}
660+
useVersionStreamFormat()
661+
contentType(ContentType.Application.Json)
662+
setBody(VersionDelta(version!!.getContentHash(), null, objectsMap = mapOf<String, String>()))
663+
}
664+
665+
// Assert
666+
assertEquals(HttpStatusCode.OK, response.status)
667+
assertContains(response.bodyAsText(), version!!.getContentHash())
668+
}
606669
}
607670

608671
private fun ApplicationTestBuilder.jsonClient(): HttpClient = createClient {

0 commit comments

Comments
 (0)