Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ interface IModelClientV2 {
suspend fun pullIfExists(branch: BranchReference): IVersion?

suspend fun pullHash(branch: BranchReference): String
suspend fun pullHashIfExists(branch: BranchReference): String?

/**
* While `pull` returns immediately `poll` returns as soon as a new version, that is different from the given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -699,14 +699,31 @@ class ModelClientV2(
}

override suspend fun pullHash(branch: BranchReference): String {
val response = httpClient.get {
return httpClient.prepareGet {
url {
takeFrom(baseUrl)
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "hash")
}
}.execute { response ->
response.body<String>()
.also { LOG.debug { "${clientId.toString(16)}.pullHash($branch) -> $it" } }
}
}

override suspend fun pullHashIfExists(branch: BranchReference): String? {
return httpClient.prepareGet {
expectSuccess = false
url {
takeFrom(baseUrl)
appendPathSegmentsEncodingSlash("repositories", branch.repositoryId.id, "branches", branch.branchName, "hash")
}
}.execute { response ->
when (response.status) {
HttpStatusCode.NotFound -> null
HttpStatusCode.OK -> response.body<String>()
else -> throw ResponseException(response, response.bodyAsText())
}.also { LOG.debug { "${clientId.toString(16)}.pullHashIfExists($branch) -> $it" } }
}
val receivedHash: String = response.body()
return receivedHash
}

override suspend fun pollHash(branch: BranchReference, lastKnownVersion: IVersion?): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

@RequiresTransaction
fun getVersionHash(branch: BranchReference): String?
suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String
suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String?

Check warning

Code scanning / detekt

The function pollVersionHash is missing documentation. Warning

The function pollVersionHash is missing documentation.

@RequiresTransaction
fun forcePush(branch: BranchReference, newVersionHash: String)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,13 @@ class ModelReplicationServer(
checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull)
val branchRef = repositoryId(repository).getBranchReference(branch)
val newVersionHash = repositoriesManager.pollVersionHash(branchRef, lastKnown)
if (newVersionHash == null) {
call.respond(
HttpStatusCode.NotFound,
"Branch $branch in repository $repository doesn't exist",
)
return
}
call.respondDelta(RepositoryId(repository), newVersionHash, ObjectDeltaFilter(lastKnown))
}

Expand Down Expand Up @@ -531,6 +538,13 @@ class ModelReplicationServer(
checkPermission(ModelServerPermissionSchema.repository(repository).branch(branch).pull)
val branchRef = repositoryId(repository).getBranchReference(branch)
val newVersionHash = repositoriesManager.pollVersionHash(branchRef, lastKnown)
if (newVersionHash == null) {
call.respond(
HttpStatusCode.NotFound,
"Branch $branch in repository $repository doesn't exist",
)
return
}
call.respondText(newVersionHash)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,8 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager {
}
}

override suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String {
override suspend fun pollVersionHash(branch: BranchReference, lastKnown: String?): String? {
return pollEntry(stores.genericStore, branchKey(branch), lastKnown)
?: throw IllegalStateException("No version found for branch '${branch.branchName}' in repository '${branch.repositoryId}'")
}

override suspend fun computeDelta(repository: RepositoryId?, versionHash: String, filter: ObjectDeltaFilter): ObjectData {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ class ModelReplicationServerTest {
response.shouldHaveContentType(branchV1ContentType)
response.body<BranchV1>() shouldBe BranchV1(
"master",
fixture.repositoriesManager.pollVersionHash(repositoryId.getBranchReference("master"), null),
fixture.repositoriesManager.pollVersionHash(repositoryId.getBranchReference("master"), null)!!,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class BindingWorker(
if (invalidatingListener?.hasAnyInvalidations() != false) return "There are pending changes in MPS"
forEachTargetIndexed { index ->
val version = versions[index]
val remoteVersion = client().pullHash(branchRef)
val remoteVersion = client().pullHashIfExists(branchRef)
if (remoteVersion != version.remoteVersion.getContentHash()) {
return "Local version ($version differs from remote version ($remoteVersion)"
}
Expand Down Expand Up @@ -277,19 +277,16 @@ class BindingWorker(
?: initialVersionHash?.let { client().loadVersion(branchRef.repositoryId, it, null) }
}

// The first binding is the primary binding where new modules are created.
// All other bindings are considered libraries where only existing modules are synchronized. They are not
// relevant for the decision in which direction we have to synchronize initially.
val primaryBaseVersion = baseVersions.first()
if (primaryBaseVersion == null) {
// Binding was never activated before. Overwrite local changes or do initial upload.
if (baseVersions.contains(null)) {
// Binding was never activated before or branch was switched.
// Overwrite local changes or do initial upload.

val existingRemoteVersions = forEachTargetIndexed { client().pullIfExists(branchRef) }
val createdRemoteVersions = forEachTargetIndexed { index ->
existingRemoteVersions[index] ?: client().initRepository(branchRef.repositoryId)
existingRemoteVersions[index] ?: createBranch(this)
}

if (existingRemoteVersions.first().let { it == null || it.isInitialVersion() }) {
if (existingRemoteVersions.all { it == null || it.isInitialVersion() }) {
LOG.debug { "Repository doesn't exist. Will copy the local project to the server." }
// repository doesn't exist -> copy the local project to the server
doSyncToServer(createdRemoteVersions.map { SynchronizedVersions(it, it) }, incremental = false)
Expand All @@ -303,7 +300,7 @@ class BindingWorker(
// Binding was activated before. Preserve local changes.

val createdBaseVersions: List<SynchronizedVersions> = forEachTargetIndexed { index ->
baseVersions[index] ?: client().initRepository(branchRef.repositoryId)
baseVersions[index] ?: createBranch(this)
}.map { SynchronizedVersions(it, it) }

// push local changes that happened while the binding was deactivated
Expand Down Expand Up @@ -335,6 +332,22 @@ class BindingWorker(
}
}

private suspend fun createBranch(syncTarget: SyncTarget): IVersion {
val client = syncTarget.client()
val branch = syncTarget.branchRef
val masterBranch = branch.repositoryId.getBranchReference()
// branch already exists
return client.pullIfExists(branch)
// clone from master
?: client.pullIfExists(masterBranch)?.also { client.push(branch, it, it) }
// create new repository
?: client.initRepository(branch.repositoryId).also {
if (branch != masterBranch) {
client.push(branch, it, it)
}
}
}

suspend fun syncToMPS(incremental: Boolean): List<SynchronizedVersions> {
return runSync { oldVersions ->
val oldVersions = oldVersions ?: syncTargets.map { null }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class ModelSyncService(val project: Project) :

@Synchronized
override fun switchBranch(oldBranchRef: BranchReference, newBranchRef: BranchReference, dropLocalChanges: Boolean) {
updateCurrentVersions()
updateState {
it.bindings.none { it.key.branchRef == oldBranchRef } &&
throw IllegalArgumentException("No binding for $oldBranchRef")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import jetbrains.mps.smodel.adapter.structure.MetaAdapterFactory
import org.modelix.datastructures.model.MutationParameters
import org.modelix.model.api.BuiltinLanguages
import org.modelix.model.api.IWritableNode
import org.modelix.model.api.getName
import org.modelix.model.client2.ModelClientV2
import org.modelix.model.client2.runWriteOnTree
import org.modelix.model.lazy.RepositoryId
Expand All @@ -30,7 +31,7 @@ import kotlin.io.path.writeText
class LibraryRepositoryTest : ProjectSyncTestBase() {

private val branchRefMain = RepositoryId("main-repository").getBranchReference()
private val branchRefLib = RepositoryId("lib-repository").getBranchReference()
private var branchRefLib = RepositoryId("lib-repository").getBranchReference()
private val service: IModelSyncService get() = IModelSyncService.getInstance(mpsProject)

fun `test checkout`() = runTest { port, client ->
Expand Down Expand Up @@ -61,6 +62,54 @@ class LibraryRepositoryTest : ProjectSyncTestBase() {
assertEquals(expectedLibHash, client.pullHash(branchRefLib))
}

fun `test checkout with non-existing library branch`() = runTest { port, client ->
branchRefLib = branchRefLib.repositoryId.getBranchReference("non-existing-branch")
openProjectWithBindings(port)
service.getBindings().forEach { it.flush() }

assertContainsElements(
readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() },
"main.module1",
"main.module2",
"lib.module3",
"lib.module4",
)
assertEquals(
setOf(
"main.module1" to false,
"main.module2" to false,
"lib.module3" to false,
"lib.module4" to false,
),
readAction { mpsProject.projectModules.map { it.moduleName to it.isReadOnly }.toSet() },
)
}

fun `test checkout with non-existing library repository`() = runTest { port, client ->
branchRefLib = RepositoryId("non-existing-repository").getBranchReference("non-existing-branch")
openProjectWithBindings(port)
service.getBindings().forEach { it.flush() }

val modulesInMpsRepo = readAction { mpsProject.repository.modules.map { it.moduleName }.toSet() }
assertContainsElements(
modulesInMpsRepo,
"main.module1",
"main.module2",
)
assertDoesntContain(
modulesInMpsRepo,
"lib.module3",
"lib.module4",
)
assertEquals(
setOf(
"main.module1" to false,
"main.module2" to false,
),
readAction { mpsProject.projectModules.map { it.moduleName to it.isReadOnly }.toSet() },
)
}

fun `test add module in main repository`() = runTest { port, client ->
openProjectWithBindings(port)

Expand Down Expand Up @@ -305,6 +354,49 @@ class LibraryRepositoryTest : ProjectSyncTestBase() {
}
}

fun `test switch lib branch`() = runTest { port, client ->
// create new branch from master
val branchRefLib2 = branchRefLib.repositoryId.getBranchReference("second-branch")
client.pull(branchRefLib, null).let {
client.push(branchRefLib2, it, it)
}
// make some changes so that it's different from the master branch
client.runWriteOnTree(branchRefLib2, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree ->
val repo = tree.getRootNode()
val model = repo.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference())
.single { it.getName() == "lib.module3" }
.getChildren(BuiltinLanguages.MPSRepositoryConcepts.Module.models.toReference())
.single { it.getName() == "lib.module3.modelA" }
val classConcept = MetaAdapterFactory.getConcept(-0xcf9e5ac6dd9b33bL, -0x5bbc06ad3150a7eaL, 0xf8c108ca66L, "jetbrains.mps.baseLanguage.structure.ClassConcept").toModelix()
model
.addNewChild(BuiltinLanguages.MPSRepositoryConcepts.Model.rootNodes.toReference(), 0, classConcept.getReference())
.setPropertyValue(BuiltinLanguages.jetbrains_mps_lang_core.INamedConcept.name.toReference(), "MyNewClass")
}

openProjectWithBindings(port)

assertEquals(2, service.getBindings().size)
service.getBindings().forEach { it.flush() }

readAction {
assertEquals(
null,
mpsProject.projectModules.single { it.moduleName == "lib.module3" }.models.single { it.name.longName == "lib.module3.modelA" }.rootNodes.firstOrNull()?.name,
)
}

IModelSyncService.getInstance(mpsProject).switchBranch(branchRefLib, branchRefLib2, dropLocalChanges = true)

service.getBindings().forEach { it.flush() }

readAction {
assertEquals(
"MyNewClass",
mpsProject.projectModules.single { it.moduleName == "lib.module3" }.models.single { it.name.longName == "lib.module3.modelA" }.rootNodes.single().name,
)
}
}

private fun IWritableNode.addNewModule(name: String, modelNames: List<String> = emptyList()): IWritableNode {
return addNewChild(
BuiltinLanguages.MPSRepositoryConcepts.Repository.modules.toReference(),
Expand Down Expand Up @@ -335,7 +427,7 @@ class LibraryRepositoryTest : ProjectSyncTestBase() {
}

private fun runTest(body: suspend (port: Int, client: ModelClientV2) -> Unit) = runWithModelServer { port ->
val client = ModelClientV2.builder().url("http://localhost:$port").lazyAndBlockingQueries().build()
val client = ModelClientV2.builder().url("http://localhost:$port").lazyAndBlockingQueries().build().also { it.init() }
client.initRepository(branchRefMain.repositoryId)
client.initRepository(branchRefLib.repositoryId)
client.runWriteOnTree(branchRefMain, nodeIdGenerator = { MPSIdGenerator(client.getIdGenerator(), it) }) { tree ->
Expand Down
Loading