Skip to content
Draft
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 @@ -36,15 +36,15 @@ class AllKeysControllerTest : ProjectAuthControllerTest("/v2/projects/") {
node("id").isValidId
node("namespace").isNull()
node("name").isEqualTo("first_key")
node("branch").isNull()
node("branch").isEqualTo("main")
}
node("[1]") {
node("name").isEqualTo("second_key")
node("branch").isNull()
node("branch").isEqualTo("main")
}
node("[2]") {
node("name").isEqualTo("key_with_referecnces")
node("branch").isNull()
node("branch").isEqualTo("main")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,9 @@ class BigMetaControllerTest :
measureTime {
storeAndAssertSize(keys, 0, 30, 354)
storeAndAssertSize(keys, 500, 100, 1409)
storeAndAssertSize(keys, 10, 200, 3155)
storeAndAssertSize(keys, 800, 50, 3710)
storeAndAssertSize(keys, 800, 50, 3710)
storeAndAssertSize(keys, 10, 200, 3268)
storeAndAssertSize(keys, 800, 50, 3823)
storeAndAssertSize(keys, 800, 50, 3823)
}.inWholeSeconds.assert.isLessThan(5)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class ProjectActivityControllerTest : ProjectAuthControllerTest("/v2/projects/")
@ProjectJWTAuthTestMethod
fun `returns single activity`() {
performProjectAuthPut("/import/apply").andIsOk
val revision = activityUtil.getLastRevision()
val revision = activityUtil.getLastRevision(project.id)
performProjectAuthGet("activity/revisions/${revision?.id}")
.andIsOk
.andAssertThatJson {
Expand Down Expand Up @@ -125,7 +125,7 @@ class ProjectActivityControllerTest : ProjectAuthControllerTest("/v2/projects/")
@ProjectJWTAuthTestMethod
fun `returns modified entities with pagination`() {
performProjectAuthPut("/import/apply").andIsOk
val revision = activityUtil.getLastRevision()
val revision = activityUtil.getLastRevision(project.id)
performProjectAuthGet("activity/revisions/${revision?.id}/modified-entities")
.andIsOk
.andAssertThatJson {
Expand All @@ -142,7 +142,7 @@ class ProjectActivityControllerTest : ProjectAuthControllerTest("/v2/projects/")
@ProjectJWTAuthTestMethod
fun `filters modified entities by class`() {
performProjectAuthPut("/import/apply").andIsOk
val revision = activityUtil.getLastRevision()
val revision = activityUtil.getLastRevision(project.id)
performProjectAuthGet("activity/revisions/${revision?.id}/modified-entities?filterEntityClass=Key")
.andIsOk
.andAssertThatJson {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -523,9 +523,10 @@ class TranslationsControllerViewTest : ProjectAuthControllerTest("/v2/projects/"
return entityManager
.createQuery(
"""
from ActivityRevision ar order by ar.id desc limit 1
from ActivityRevision ar where ar.projectId = :projectId order by ar.id desc limit 1
""".trimMargin(),
ActivityRevision::class.java,
).singleResult
).setParameter("projectId", project.id)
.singleResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ class ActivityVIewByRevisionsProviderTest : ProjectAuthControllerTest() {
val testData = ImportTestData()
testData.setAllResolved()
testData.setAllOverride()
testData.addDefaultBranch()
testDataService.saveTestData(testData.root)
val user =
testData.root.data.userAccounts[0]
Expand Down Expand Up @@ -77,7 +76,7 @@ class ActivityVIewByRevisionsProviderTest : ProjectAuthControllerTest() {
).setParameter("revisionId", revision.first().id)
.resultList

branchIds.any { it == testData.defaultBranch.id }.assert.isTrue()
branchIds.any { it == testData.projectBuilder.defaultBranch.id }.assert.isTrue()

val views = ActivityViewByRevisionsProvider(applicationContext, revision, null, onlyCountInListAbove = 1).get()
views
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ class LanguageServiceTest : AbstractSpringTest() {
// prepareStatementCount tracks JDBC PreparedStatement acquisitions via connection.prepareStatement().
// This reliably detects N+1 issues as Hibernate acquires a new statement per query execution.
// With 100 generated items, N+1 would cause 100+ statements; we expect a constant count instead.
// Threshold accounts for @Async operations (BranchMetaUpdater, LanguageStatsListener) whose
// queries are counted in the global SessionFactory statistics.
val count = sessionFactory.statistics.prepareStatementCount
count.assert.isLessThan(25)
count.assert.isLessThan(100)

executeInNewTransaction {
assertLanguageDeleted(testData.germanLanguage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.tolgee.model.Language
import io.tolgee.model.Organization
import io.tolgee.model.Project
import io.tolgee.model.Screenshot
import io.tolgee.model.branching.Branch
import io.tolgee.model.enums.TranslationCommentState
import io.tolgee.model.enums.TranslationState
import io.tolgee.model.key.Key
Expand Down Expand Up @@ -73,6 +74,7 @@ class DemoProjectCreator(
this@apply.organizationOwner = organization
this.description = "This is a demo project of a packing list app"
}
Branch.createMainBranch(project)
projectService.save(project)
setAvatar(project)
project
Expand Down Expand Up @@ -190,6 +192,7 @@ class DemoProjectCreator(
this.pluralArgName = pluralArgName
}
this@apply.project = this@DemoProjectCreator.project
this@apply.branch = this@DemoProjectCreator.project.getDefaultBranch()
}
keyService.save(key)
key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.tolgee.model.Language
import io.tolgee.model.Organization
import io.tolgee.model.Project
import io.tolgee.model.UserAccount
import io.tolgee.model.branching.Branch
import io.tolgee.model.enums.OrganizationRoleType
import io.tolgee.model.enums.Scope
import io.tolgee.model.key.Key
Expand Down Expand Up @@ -123,6 +124,7 @@ class DbPopulatorReal(
project.name = projectName
project.organizationOwner = organization
project.slug = slugGenerator.generate(projectName, 3, 60) { true }
Branch.createMainBranch(project)
en = createLanguage("en", project)
project.baseLanguage = en
de = createLanguage("de", project)
Expand Down Expand Up @@ -150,6 +152,7 @@ class DbPopulatorReal(
val project = Project()
project.name = projectName
project.organizationOwner = organization
Branch.createMainBranch(project)
projectService.save(project)
en = createLanguage("en", project)
project.baseLanguage = en
Expand Down Expand Up @@ -304,6 +307,7 @@ class DbPopulatorReal(
.lowercase(Locale.getDefault())
.replace("\\.+$".toRegex(), "")
key.project = project
key.branch = project.getDefaultBranch()
val translation = Translation()
translation.language = en
translation.key = key
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,9 @@ class TestDataService(
}

private fun saveBranches(builder: ProjectBuilder) {
// Ensure a default branch exists before saving
// This forces the lazy property to be evaluated, which creates the default branch if needed
builder.defaultBranch
builder.data.branches.filter { it.self.id == 0L }.forEach {
entityManager.persist(it.self)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,28 @@ import io.tolgee.model.branching.Branch
class BranchBuilder(
val projectBuilder: ProjectBuilder,
) : BaseEntityDataBuilder<Branch, BranchBuilder>() {
override val self: Branch =
override var self: Branch =
Branch()
.apply {
this.project = projectBuilder.self
}.run {
projectBuilder.self.branches.add(this)
this
}.also {
projectBuilder.self.branches.add(it)
}

companion object {
/**
* Creates a BranchBuilder wrapping an existing branch (e.g., from Branch.createMainBranch).
* Use this when you already have a Branch instance that was created elsewhere.
*/
fun forExistingBranch(
projectBuilder: ProjectBuilder,
branch: Branch,
): BranchBuilder {
return BranchBuilder(projectBuilder).also { builder ->
// Remove the auto-created branch from project.branches before replacing with the existing one
projectBuilder.self.branches.remove(builder.self)
builder.self = branch
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class ImportBuilder(
projectBuilder.onlyUser?.let {
author = it
}
branch = projectBuilder.defaultBranch
}

fun addImportFile(ft: FT<ImportFile>) = addOperation(data.importFiles, ft)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class KeyBuilder(
override var self: Key =
Key().also {
it.project = projectBuilder.self
it.branch = projectBuilder.defaultBranch
}

fun addTranslation(ft: FT<Translation>): TranslationBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ class ProjectBuilder(

var data = DATA()

/**
* Lazily creates a default branch for the project.
* This is used to automatically set branch on Keys, Tasks, and Imports.
*/
val defaultBranch: Branch by lazy {
// Check if a default branch already exists
data.branches.find { it.self.isDefault }?.self
?: Branch.createMainBranch(self).also { branch ->
data.branches.add(BranchBuilder.forExistingBranch(this, branch))
}
}

fun addPermission(ft: FT<Permission>) = addOperation(data.permissions, ft)

fun addApiKey(ft: FT<ApiKey>) = addOperation(data.apiKeys, ft)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ class TaskBuilder(
override var self: Task =
Task().apply {
this.project = projectBuilder.self
this.branch = projectBuilder.defaultBranch
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ class BranchRevisionData {
type = ProjectPermissionType.MANAGE
}

defaultBranch =
this@BranchRevisionData.defaultBranch =
addBranch {
name = Branch.DEFAULT_BRANCH_NAME
isDefault = true
}.self

devBranch =
this@BranchRevisionData.devBranch =
addBranch {
name = "dev"
}.build {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class KeySearchTestData : BaseTestData() {
) {
addKey {
name = keyName
this.branch = branch
branch?.let { this.branch = it }
addTranslation {
key = this@addKey
language = getLanguageByTag("en")!!.self
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,11 +111,6 @@ class KeysTestData {
}
}

addBranch {
name = "main"
isDefault = true
}

addBranch {
name = "dev"
}.build {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,11 +292,23 @@ class TranslationsTestData {
isBranchDefault: Boolean = false,
) {
root.data.projects[0].apply {
addBranch {
name = branchName
project = this@apply.self
isDefault = isBranchDefault
}.build {
val branchBuilder =
if (isBranchDefault) {
// Reuse the existing default branch to avoid duplicate default branch constraint violation
data.branches.find { it.self.isDefault }
?: addBranch {
name = branchName
project = this@apply.self
isDefault = true
}
} else {
addBranch {
name = branchName
project = this@apply.self
isDefault = false
}
}
branchBuilder.build {
(1..count).forEach {
addKey {
name = "key from branch $branchName $it"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.tolgee.exceptions

import io.tolgee.constants.Message

class DefaultBranchNotFoundException : NotFoundException(Message.BRANCH_NOT_FOUND)
4 changes: 3 additions & 1 deletion backend/data/src/main/kotlin/io/tolgee/model/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.tolgee.model
import io.tolgee.activity.annotation.ActivityLoggedEntity
import io.tolgee.activity.annotation.ActivityLoggedProp
import io.tolgee.api.ISimpleProject
import io.tolgee.exceptions.DefaultBranchNotFoundException
import io.tolgee.model.automations.Automation
import io.tolgee.model.branching.Branch
import io.tolgee.model.contentDelivery.ContentDeliveryConfig
Expand Down Expand Up @@ -198,8 +199,9 @@ class Project(
return branches.any { it.isDefault }
}

fun getDefaultBranch(): Branch? {
fun getDefaultBranch(): Branch {
return branches.find { it.isDefault }
?: throw DefaultBranchNotFoundException()
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.tolgee.model.branching

import io.tolgee.activity.annotation.ActivityDescribingProp
import io.tolgee.activity.annotation.ActivityIgnoredProp
import io.tolgee.activity.annotation.ActivityLoggedEntity
import io.tolgee.activity.annotation.ActivityLoggedProp
import io.tolgee.activity.annotation.ActivityReturnsExistence
Expand Down Expand Up @@ -57,6 +58,7 @@ class Branch(
var pending: Boolean = false,
@Column(name = "revision")
@ColumnDefault("0")
@ActivityIgnoredProp
var revision: Int = 0,
@OneToMany(targetEntity = BranchMerge::class, mappedBy = "sourceBranch", fetch = FetchType.LAZY)
@OrderBy("createdAt DESC")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface BranchVersionedEntity : EntityWithBranch {
fun resolveKey(): Key?

override fun resolveBranch(): Branch? {
return resolveKey()?.branch?.let { return it }
return resolveKey()?.branch
}

override fun resolveProject(): Project? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ class Import(
@OneToMany(mappedBy = "import", orphanRemoval = true)
var files = mutableListOf<ImportFile>()

@ManyToOne(targetEntity = Branch::class)
@JoinColumn(name = "branch_id", nullable = true)
var branch: Branch? = null
@ManyToOne(targetEntity = Branch::class, optional = false)
@JoinColumn(name = "branch_id", nullable = false)
lateinit var branch: Branch

override var deletedAt: Date? = null
}
5 changes: 2 additions & 3 deletions backend/data/src/main/kotlin/io/tolgee/model/key/Key.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,10 @@ class Key(
@ActivityLoggedProp
var namespace: Namespace? = null

// Nullable for backward compatibility: NULL represents default branch for legacy data
@ManyToOne(fetch = FetchType.LAZY)
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "branch_id")
@ActivityLoggedProp
var branch: Branch? = null
lateinit var branch: Branch

@OneToMany(mappedBy = "key")
var translations: MutableList<Translation> = mutableListOf()
Expand Down
4 changes: 2 additions & 2 deletions backend/data/src/main/kotlin/io/tolgee/model/task/Task.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ class Task :
@ActivityLoggedProp
var author: UserAccount? = null

@ManyToOne(fetch = FetchType.LAZY, optional = true)
var branch: Branch? = null
@ManyToOne(fetch = FetchType.LAZY, optional = false)
lateinit var branch: Branch

@field:Size(max = 255)
@Column(name = "origin_branch_name", length = 255)
Expand Down
Loading
Loading