12 changed files with 870 additions and 0 deletions
@ -0,0 +1,118 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.controller; |
||||
|
|
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import org.springframework.security.access.prepost.PreAuthorize; |
||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||
|
import org.springframework.web.bind.annotation.PathVariable; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestParam; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.ExportableEntity; |
||||
|
import org.thingsboard.server.common.data.exception.ThingsboardException; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
||||
|
import org.thingsboard.server.service.sync.importing.EntityImportResult; |
||||
|
import org.thingsboard.server.service.sync.vcs.DefaultEntitiesVersionControlService; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntitiesVersionControlSettings; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntityVersion; |
||||
|
|
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@RestController |
||||
|
@RequestMapping("/api/entities/vc") |
||||
|
@RequiredArgsConstructor |
||||
|
public class EntitiesVersionControlController extends BaseController { |
||||
|
|
||||
|
private final DefaultEntitiesVersionControlService versionControlService; |
||||
|
|
||||
|
|
||||
|
|
||||
|
@PostMapping("/version/{entityType}/{entityId}") |
||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
||||
|
public EntityVersion saveEntityVersion(@PathVariable EntityType entityType, |
||||
|
@PathVariable("entityId") UUID entityUuid, |
||||
|
@RequestParam String branch, |
||||
|
@RequestBody String versionName) throws Exception { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); |
||||
|
return versionControlService.saveEntityVersion(getTenantId(), entityId, branch, versionName); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/version/{entityType}/{entityId}") |
||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
||||
|
public List<EntityVersion> listEntityVersions(@PathVariable EntityType entityType, |
||||
|
@PathVariable("entityId") UUID entityUuid, |
||||
|
@RequestParam String branch) throws Exception { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); |
||||
|
return versionControlService.listEntityVersions(getTenantId(), entityId, branch, Integer.MAX_VALUE); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@GetMapping("/entity/{entityType}/{entityId}/{versionId}") |
||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
||||
|
public EntityExportData<ExportableEntity<EntityId>> getEntityAtVersion(@PathVariable EntityType entityType, |
||||
|
@PathVariable("entityId") UUID entityUuid, |
||||
|
@PathVariable String versionId) throws Exception { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); |
||||
|
return versionControlService.getEntityAtVersion(getTenantId(), entityId, versionId); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/entity/{entityType}/{entityId}/{versionId}") |
||||
|
@PreAuthorize("hasAuthority('TENANT_ADMIN')") |
||||
|
public EntityImportResult<ExportableEntity<EntityId>> loadEntityVersion(@PathVariable EntityType entityType, |
||||
|
@PathVariable("entityId") UUID entityUuid, |
||||
|
@PathVariable String versionId) throws Exception { |
||||
|
EntityId entityId = EntityIdFactory.getByTypeAndUuid(entityType, entityUuid); |
||||
|
return versionControlService.loadEntityVersion(getTenantId(), entityId, versionId); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@GetMapping("/branches") |
||||
|
public Set<String> getAllowedBranches() throws ThingsboardException { |
||||
|
return versionControlService.getAllowedBranches(getTenantId()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@PostMapping("/settings") |
||||
|
@PreAuthorize("hasAuthority('SYS_ADMIN')") |
||||
|
public void saveSettings(@RequestBody EntitiesVersionControlSettings settings) throws Exception { |
||||
|
versionControlService.saveSettings(settings); |
||||
|
} |
||||
|
|
||||
|
@GetMapping("/settings") |
||||
|
@PreAuthorize("hasAuthority('SYS_ADMIN')") |
||||
|
public EntitiesVersionControlSettings getSettings() { |
||||
|
return versionControlService.getSettings(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@PostMapping("/repository/reset") |
||||
|
@PreAuthorize("hasAuthority('SYS_ADMIN')") |
||||
|
public void resetLocalRepository() throws Exception { |
||||
|
versionControlService.resetRepository(); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,274 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vcs; |
||||
|
|
||||
|
import com.fasterxml.jackson.core.type.TypeReference; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import com.fasterxml.jackson.databind.ObjectWriter; |
||||
|
import com.fasterxml.jackson.databind.SerializationFeature; |
||||
|
import lombok.RequiredArgsConstructor; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.commons.io.FileUtils; |
||||
|
import org.springframework.scheduling.annotation.Scheduled; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.common.util.JacksonUtil; |
||||
|
import org.thingsboard.server.common.data.AdminSettings; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.ExportableEntity; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.dao.settings.AdminSettingsService; |
||||
|
import org.thingsboard.server.dao.tenant.TenantService; |
||||
|
import org.thingsboard.server.queue.util.AfterStartUp; |
||||
|
import org.thingsboard.server.queue.util.TbCoreComponent; |
||||
|
import org.thingsboard.server.service.sync.EntitiesExportImportService; |
||||
|
import org.thingsboard.server.service.sync.exporting.EntityExportSettings; |
||||
|
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
||||
|
import org.thingsboard.server.service.sync.importing.EntityImportResult; |
||||
|
import org.thingsboard.server.service.sync.importing.EntityImportSettings; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntitiesVersionControlSettings; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntityVersion; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.GitSettings; |
||||
|
import org.thingsboard.server.utils.git.Repository; |
||||
|
import org.thingsboard.server.utils.git.data.Commit; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.nio.file.Files; |
||||
|
import java.nio.file.Path; |
||||
|
import java.util.Collections; |
||||
|
import java.util.List; |
||||
|
import java.util.Optional; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.locks.Lock; |
||||
|
import java.util.concurrent.locks.ReentrantLock; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Service |
||||
|
@TbCoreComponent |
||||
|
@RequiredArgsConstructor |
||||
|
@Slf4j |
||||
|
public class DefaultEntitiesVersionControlService implements EntitiesVersionControlService { |
||||
|
// TODO [viacheslav]: start up only on one of the cores
|
||||
|
|
||||
|
private final TenantService tenantService; |
||||
|
private final EntitiesExportImportService exportImportService; |
||||
|
private final AdminSettingsService adminSettingsService; |
||||
|
|
||||
|
private final ObjectWriter jsonWriter = new ObjectMapper().writer(SerializationFeature.INDENT_OUTPUT); |
||||
|
private static final String SETTINGS_KEY = "vc"; |
||||
|
|
||||
|
private Repository repository; |
||||
|
private final ReentrantLock fetchLock = new ReentrantLock(); |
||||
|
private final Lock writeLock = new ReentrantLock(); |
||||
|
|
||||
|
@AfterStartUp |
||||
|
public void init() throws Exception { |
||||
|
try { |
||||
|
EntitiesVersionControlSettings settings = getSettings(); |
||||
|
if (settings != null && settings.getGitSettings() != null) { |
||||
|
this.repository = initRepository(settings.getGitSettings()); |
||||
|
} |
||||
|
} catch (Exception e) { |
||||
|
log.error("Failed to initialize entities version control service", e); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Scheduled(initialDelay = 10 * 1000, fixedDelay = 10 * 1000) |
||||
|
public void fetch() throws Exception { |
||||
|
if (repository == null) return; |
||||
|
|
||||
|
if (fetchLock.tryLock()) { |
||||
|
try { |
||||
|
log.info("Fetching remote repository"); |
||||
|
repository.fetch(); |
||||
|
} finally { |
||||
|
fetchLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public EntityVersion saveEntityVersion(TenantId tenantId, EntityId entityId, String branch, String versionName) throws Exception { |
||||
|
return saveEntitiesVersion(tenantId, List.of(entityId), branch, versionName); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntityVersion saveEntitiesVersion(TenantId tenantId, List<EntityId> entitiesIds, String branch, String versionName) throws Exception { |
||||
|
checkRepository(); |
||||
|
checkBranch(tenantId, branch); |
||||
|
|
||||
|
EntityExportSettings exportSettings = EntityExportSettings.builder() |
||||
|
.exportInboundRelations(false) |
||||
|
.exportOutboundRelations(false) |
||||
|
.build(); |
||||
|
List<EntityExportData<ExportableEntity<EntityId>>> entityDataList = entitiesIds.stream() |
||||
|
.map(entityId -> { |
||||
|
return exportImportService.exportEntity(tenantId, entityId, exportSettings); |
||||
|
}) |
||||
|
.collect(Collectors.toList()); |
||||
|
|
||||
|
if (fetchLock.tryLock()) { |
||||
|
try { |
||||
|
repository.fetch(); |
||||
|
} finally { |
||||
|
fetchLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
writeLock.lock(); |
||||
|
try { |
||||
|
if (repository.listBranches().contains(branch)) { |
||||
|
repository.checkout(branch); |
||||
|
repository.merge(branch); |
||||
|
} else { |
||||
|
repository.createAndCheckoutOrphanBranch(branch); |
||||
|
} |
||||
|
|
||||
|
for (EntityExportData<ExportableEntity<EntityId>> entityData : entityDataList) { |
||||
|
String entityDataJson = jsonWriter.writeValueAsString(entityData); |
||||
|
FileUtils.write(new File(repository.getDirectory() + "/" + getRelativePathForEntity(entityData.getEntity().getId())), |
||||
|
entityDataJson, StandardCharsets.UTF_8); |
||||
|
} |
||||
|
|
||||
|
Commit commit = repository.commit(versionName, ".", "Tenant " + tenantId); |
||||
|
repository.push(); |
||||
|
return new EntityVersion(commit.getId(), commit.getMessage(), commit.getAuthorName()); |
||||
|
} finally { |
||||
|
writeLock.unlock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersion> listEntityVersions(TenantId tenantId, EntityId entityId, String branch, int limit) throws Exception { |
||||
|
checkRepository(); |
||||
|
checkBranch(tenantId, branch); |
||||
|
|
||||
|
return repository.listCommits(branch, getRelativePathForEntity(entityId), limit).stream() |
||||
|
.map(commit -> new EntityVersion(commit.getId(), commit.getMessage(), commit.getAuthorName())) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public List<EntityVersion> listEntityTypeVersions(TenantId tenantId, EntityType entityType, String branch, int limit) throws Exception { |
||||
|
checkRepository(); |
||||
|
checkBranch(tenantId, branch); |
||||
|
|
||||
|
return repository.listCommits(branch, getRelativePathForEntityType(entityType), limit).stream() |
||||
|
.map(commit -> new EntityVersion(commit.getId(), commit.getMessage(), commit.getAuthorName())) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
@Override |
||||
|
public <E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> getEntityAtVersion(TenantId tenantId, I entityId, String versionId) throws Exception { |
||||
|
checkRepository(); |
||||
|
// FIXME [viacheslav]: validate access
|
||||
|
|
||||
|
String entityDataJson = repository.getFileContentAtCommit(getRelativePathForEntity(entityId), versionId); |
||||
|
return JacksonUtil.fromString(entityDataJson, new TypeReference<EntityExportData<E>>() {}); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public <E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> loadEntityVersion(TenantId tenantId, I entityId, String versionId) throws Exception { |
||||
|
EntityExportData<E> entityData = getEntityAtVersion(tenantId, entityId, versionId); |
||||
|
return exportImportService.importEntity(tenantId, entityData, EntityImportSettings.builder() |
||||
|
.importInboundRelations(false) |
||||
|
.importOutboundRelations(false) |
||||
|
.updateReferencesToOtherEntities(true) |
||||
|
.build()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
private String getRelativePathForEntity(EntityId entityId) { |
||||
|
return getRelativePathForEntityType(entityId.getEntityType()) |
||||
|
+ "/" + entityId.getId() + ".json"; |
||||
|
} |
||||
|
|
||||
|
private String getRelativePathForEntityType(EntityType entityType) { |
||||
|
return entityType.name().toLowerCase(); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
private void checkBranch(TenantId tenantId, String branch) { |
||||
|
if (!getAllowedBranches(tenantId).contains(branch)) { |
||||
|
throw new IllegalArgumentException("Tenant does not have access to this branch"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Set<String> getAllowedBranches(TenantId tenantId) { |
||||
|
return Optional.ofNullable(getSettings()) |
||||
|
.flatMap(settings -> Optional.ofNullable(settings.getAllowedBranches())) |
||||
|
.flatMap(tenantsAllowedBranches -> Optional.ofNullable(tenantsAllowedBranches.get(tenantId.getId()))) |
||||
|
.orElse(Collections.emptySet()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void saveSettings(EntitiesVersionControlSettings settings) throws Exception { |
||||
|
this.repository = initRepository(settings.getGitSettings()); |
||||
|
|
||||
|
AdminSettings adminSettings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, SETTINGS_KEY)) |
||||
|
.orElseGet(() -> { |
||||
|
AdminSettings newSettings = new AdminSettings(); |
||||
|
newSettings.setKey(SETTINGS_KEY); |
||||
|
return newSettings; |
||||
|
}); |
||||
|
adminSettings.setJsonValue(JacksonUtil.valueToTree(settings)); |
||||
|
adminSettingsService.saveAdminSettings(TenantId.SYS_TENANT_ID, adminSettings); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public EntitiesVersionControlSettings getSettings() { |
||||
|
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, SETTINGS_KEY)) |
||||
|
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), EntitiesVersionControlSettings.class)) |
||||
|
.orElse(null); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
private void checkRepository() { |
||||
|
if (repository == null) { |
||||
|
throw new IllegalStateException("Repository is not initialized"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static Repository initRepository(GitSettings gitSettings) throws Exception { |
||||
|
if (Files.exists(Path.of(gitSettings.getRepositoryDirectory()))) { |
||||
|
return Repository.open(gitSettings.getRepositoryDirectory(), |
||||
|
gitSettings.getUsername(), gitSettings.getPassword()); |
||||
|
} else { |
||||
|
Files.createDirectories(Path.of(gitSettings.getRepositoryDirectory())); |
||||
|
return Repository.clone(gitSettings.getRepositoryUri(), gitSettings.getRepositoryDirectory(), |
||||
|
gitSettings.getUsername(), gitSettings.getPassword()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void resetRepository() throws Exception { |
||||
|
if (this.repository != null) { |
||||
|
FileUtils.deleteDirectory(new File(repository.getDirectory())); |
||||
|
this.repository = null; |
||||
|
} |
||||
|
EntitiesVersionControlSettings settings = getSettings(); |
||||
|
this.repository = initRepository(settings.getGitSettings()); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vcs; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.ExportableEntity; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.service.sync.exporting.data.EntityExportData; |
||||
|
import org.thingsboard.server.service.sync.importing.EntityImportResult; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntitiesVersionControlSettings; |
||||
|
import org.thingsboard.server.service.sync.vcs.data.EntityVersion; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface EntitiesVersionControlService { |
||||
|
|
||||
|
EntityVersion saveEntityVersion(TenantId tenantId, EntityId entityId, String branch, String versionName) throws Exception; |
||||
|
|
||||
|
EntityVersion saveEntitiesVersion(TenantId tenantId, List<EntityId> entitiesIds, String branch, String versionName) throws Exception; |
||||
|
|
||||
|
|
||||
|
List<EntityVersion> listEntityVersions(TenantId tenantId, EntityId entityId, String branch, int limit) throws Exception; |
||||
|
|
||||
|
List<EntityVersion> listEntityTypeVersions(TenantId tenantId, EntityType entityType, String branch, int limit) throws Exception; |
||||
|
|
||||
|
|
||||
|
<E extends ExportableEntity<I>, I extends EntityId> EntityExportData<E> getEntityAtVersion(TenantId tenantId, I entityId, String versionId) throws Exception; |
||||
|
|
||||
|
<E extends ExportableEntity<I>, I extends EntityId> EntityImportResult<E> loadEntityVersion(TenantId tenantId, I entityId, String versionId) throws Exception; |
||||
|
|
||||
|
|
||||
|
void saveSettings(EntitiesVersionControlSettings settings) throws Exception; |
||||
|
|
||||
|
EntitiesVersionControlSettings getSettings(); |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vcs.data; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@Data |
||||
|
public class EntitiesVersionControlSettings { |
||||
|
private Map<UUID, Set<String>> allowedBranches; |
||||
|
private GitSettings gitSettings; |
||||
|
} |
||||
@ -0,0 +1,29 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vcs.data; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@NoArgsConstructor |
||||
|
@AllArgsConstructor |
||||
|
public class EntityVersion { |
||||
|
private String id; |
||||
|
private String name; |
||||
|
private String authorName; |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.sync.vcs.data; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.NoArgsConstructor; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
@NoArgsConstructor |
||||
|
@Builder |
||||
|
public class GitSettings { |
||||
|
private String repositoryUri; |
||||
|
private String repositoryDirectory; |
||||
|
private String username; |
||||
|
private String password; |
||||
|
} |
||||
@ -0,0 +1,256 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils.git; |
||||
|
|
||||
|
import com.google.common.collect.Streams; |
||||
|
import lombok.Getter; |
||||
|
import org.apache.commons.lang3.StringUtils; |
||||
|
import org.eclipse.jgit.api.Git; |
||||
|
import org.eclipse.jgit.api.GitCommand; |
||||
|
import org.eclipse.jgit.api.ListBranchCommand; |
||||
|
import org.eclipse.jgit.api.LogCommand; |
||||
|
import org.eclipse.jgit.api.RmCommand; |
||||
|
import org.eclipse.jgit.api.TransportCommand; |
||||
|
import org.eclipse.jgit.api.errors.GitAPIException; |
||||
|
import org.eclipse.jgit.lib.Constants; |
||||
|
import org.eclipse.jgit.lib.ObjectId; |
||||
|
import org.eclipse.jgit.lib.ObjectLoader; |
||||
|
import org.eclipse.jgit.lib.ObjectReader; |
||||
|
import org.eclipse.jgit.revwalk.RevCommit; |
||||
|
import org.eclipse.jgit.revwalk.filter.RevFilter; |
||||
|
import org.eclipse.jgit.transport.CredentialsProvider; |
||||
|
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider; |
||||
|
import org.eclipse.jgit.treewalk.TreeWalk; |
||||
|
import org.eclipse.jgit.treewalk.filter.PathFilter; |
||||
|
import org.thingsboard.server.utils.git.data.Commit; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.List; |
||||
|
import java.util.Set; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
public class Repository { |
||||
|
|
||||
|
private final Git git; |
||||
|
private final CredentialsProvider credentialsProvider; |
||||
|
|
||||
|
@Getter |
||||
|
private final String directory; |
||||
|
|
||||
|
private Repository(Git git, CredentialsProvider credentialsProvider, String directory) { |
||||
|
this.git = git; |
||||
|
this.credentialsProvider = credentialsProvider; |
||||
|
this.directory = directory; |
||||
|
} |
||||
|
|
||||
|
public static Repository clone(String uri, String directory, |
||||
|
String username, String password) throws GitAPIException { |
||||
|
CredentialsProvider credentialsProvider = newCredentialsProvider(username, password); |
||||
|
Git git = Git.cloneRepository() |
||||
|
.setURI(uri) |
||||
|
.setDirectory(new java.io.File(directory)) |
||||
|
.setNoCheckout(true) |
||||
|
.setCredentialsProvider(credentialsProvider) |
||||
|
.call(); |
||||
|
return new Repository(git, credentialsProvider, directory); |
||||
|
} |
||||
|
|
||||
|
public static Repository open(String directory, String username, String password) throws IOException { |
||||
|
Git git = Git.open(new java.io.File(directory)); |
||||
|
return new Repository(git, newCredentialsProvider(username, password), directory); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void fetch() throws GitAPIException { |
||||
|
execute(git.fetch() |
||||
|
.setRemoveDeletedRefs(true)); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public List<String> listBranches() throws GitAPIException { |
||||
|
return execute(git.branchList() |
||||
|
.setListMode(ListBranchCommand.ListMode.ALL)).stream() |
||||
|
.filter(ref -> !ref.getName().equals(Constants.HEAD)) |
||||
|
.map(ref -> org.eclipse.jgit.lib.Repository.shortenRefName(ref.getName())) |
||||
|
.map(name -> StringUtils.removeStart(name, "origin/")) |
||||
|
.distinct().collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public List<Commit> listCommits(String branchName, int limit) throws IOException, GitAPIException { |
||||
|
return listCommits(branchName, null, limit); |
||||
|
} |
||||
|
|
||||
|
public List<Commit> listCommits(String branchName, String path, int limit) throws IOException, GitAPIException { |
||||
|
ObjectId branchId = resolve("origin/" + branchName); |
||||
|
if (branchId == null) { |
||||
|
throw new IllegalArgumentException("Branch not found"); |
||||
|
} |
||||
|
LogCommand command = git.log() |
||||
|
.add(branchId).setMaxCount(limit) |
||||
|
.setRevFilter(RevFilter.NO_MERGES); |
||||
|
if (StringUtils.isNotEmpty(path)) { |
||||
|
command.addPath(path); |
||||
|
} |
||||
|
return Streams.stream(execute(command)) |
||||
|
.map(this::toCommit) |
||||
|
.collect(Collectors.toList()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public List<String> listFilesAtCommit(Commit commit) throws IOException { |
||||
|
return listFilesAtCommit(commit, null); |
||||
|
} |
||||
|
|
||||
|
public List<String> listFilesAtCommit(Commit commit, String path) throws IOException { |
||||
|
List<String> files = new ArrayList<>(); |
||||
|
RevCommit revCommit = resolveCommit(commit.getId()); |
||||
|
try (TreeWalk treeWalk = new TreeWalk(git.getRepository())) { |
||||
|
treeWalk.reset(revCommit.getTree().getId()); |
||||
|
if (StringUtils.isNotEmpty(path)) { |
||||
|
treeWalk.setFilter(PathFilter.create(path)); |
||||
|
} |
||||
|
treeWalk.setRecursive(true); |
||||
|
while (treeWalk.next()) { |
||||
|
files.add(treeWalk.getPathString()); |
||||
|
} |
||||
|
} |
||||
|
return files; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public String getFileContentAtCommit(String file, String commitId) throws IOException { |
||||
|
RevCommit revCommit = resolveCommit(commitId); |
||||
|
try (TreeWalk treeWalk = TreeWalk.forPath(git.getRepository(), file, revCommit.getTree())) { |
||||
|
if (treeWalk == null) { |
||||
|
throw new IllegalArgumentException("Not found"); |
||||
|
} |
||||
|
ObjectId blobId = treeWalk.getObjectId(0); |
||||
|
try (ObjectReader objectReader = git.getRepository().newObjectReader()) { |
||||
|
ObjectLoader objectLoader = objectReader.open(blobId); |
||||
|
byte[] bytes = objectLoader.getBytes(); |
||||
|
return new String(bytes, StandardCharsets.UTF_8); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void checkout(String branchName) throws GitAPIException { |
||||
|
execute(git.checkout() |
||||
|
.setName(branchName)); |
||||
|
} |
||||
|
|
||||
|
public void merge(String branchName) throws IOException, GitAPIException { |
||||
|
ObjectId branchId = resolve("origin/" + branchName); |
||||
|
if (branchId == null) { |
||||
|
throw new IllegalArgumentException("Branch not found"); |
||||
|
} |
||||
|
execute(git.merge() |
||||
|
.include(branchId)); |
||||
|
} |
||||
|
|
||||
|
public void createAndCheckoutOrphanBranch(String name) throws GitAPIException { |
||||
|
execute(git.checkout() |
||||
|
.setOrphan(true) |
||||
|
.setName(name)); |
||||
|
Set<String> uncommittedChanges = git.status().call().getUncommittedChanges(); |
||||
|
if (!uncommittedChanges.isEmpty()) { |
||||
|
RmCommand rm = git.rm(); |
||||
|
uncommittedChanges.forEach(rm::addFilepattern); |
||||
|
execute(rm); |
||||
|
} |
||||
|
execute(git.clean()); |
||||
|
} |
||||
|
|
||||
|
public void clean() throws GitAPIException { |
||||
|
execute(git.clean().setCleanDirectories(true)); |
||||
|
} |
||||
|
|
||||
|
public Commit commit(String message, String filePattern, String author) throws GitAPIException { |
||||
|
execute(git.add().addFilepattern(filePattern)); |
||||
|
RevCommit revCommit = execute(git.commit() |
||||
|
.setMessage(message) |
||||
|
.setAuthor(author, author)); |
||||
|
return toCommit(revCommit); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void push() throws GitAPIException { |
||||
|
execute(git.push()); |
||||
|
} |
||||
|
|
||||
|
|
||||
|
// public List<Diff> getCommitChanges(Commit commit) throws IOException, GitAPIException {
|
||||
|
// RevCommit revCommit = resolveCommit(commit.getId());
|
||||
|
// if (revCommit.getParentCount() == 0) {
|
||||
|
// return null; // just takes the first parent of a commit, but should find a parent in branch provided
|
||||
|
// }
|
||||
|
// return execute(git.diff()
|
||||
|
// .setOldTree(prepareTreeParser(git.getRepository().parseCommit(revCommit.getParent(0))))
|
||||
|
// .setNewTree(prepareTreeParser(revCommit))).stream()
|
||||
|
// .map(diffEntry -> new Diff(diffEntry.getChangeType().name(), diffEntry.getOldPath(), diffEntry.getNewPath()))
|
||||
|
// .collect(Collectors.toList());
|
||||
|
// }
|
||||
|
//
|
||||
|
//
|
||||
|
// private AbstractTreeIterator prepareTreeParser(RevCommit revCommit) throws IOException {
|
||||
|
// // from the commit we can build the tree which allows us to construct the TreeParser
|
||||
|
// //noinspection Duplicates
|
||||
|
// org.eclipse.jgit.lib.Repository repository = git.getRepository();
|
||||
|
// try (RevWalk walk = new RevWalk(repository)) {
|
||||
|
// RevTree tree = walk.parseTree(revCommit.getTree().getId());
|
||||
|
//
|
||||
|
// CanonicalTreeParser treeParser = new CanonicalTreeParser();
|
||||
|
// try (ObjectReader reader = repository.newObjectReader()) {
|
||||
|
// treeParser.reset(reader, tree.getId());
|
||||
|
// }
|
||||
|
//
|
||||
|
// walk.dispose();
|
||||
|
//
|
||||
|
// return treeParser;
|
||||
|
// }
|
||||
|
// }
|
||||
|
|
||||
|
private Commit toCommit(RevCommit revCommit) { |
||||
|
return new Commit(revCommit.getName(), revCommit.getFullMessage(), revCommit.getAuthorIdent().getName()); |
||||
|
} |
||||
|
|
||||
|
private RevCommit resolveCommit(String id) throws IOException { |
||||
|
return git.getRepository().parseCommit(resolve(id)); |
||||
|
} |
||||
|
|
||||
|
private ObjectId resolve(String rev) throws IOException { |
||||
|
return git.getRepository().resolve(rev); |
||||
|
} |
||||
|
|
||||
|
private <C extends GitCommand<T>, T> T execute(C command) throws GitAPIException { |
||||
|
if (command instanceof TransportCommand) { |
||||
|
((TransportCommand<?, ?>) command).setCredentialsProvider(credentialsProvider); |
||||
|
// SshSessionFactory sshSessionFactory = SshSessionFactory.getInstance();
|
||||
|
// transportCommand.setTransportConfigCallback(transport -> {
|
||||
|
// ((SshTransport) transport).setSshSessionFactory(sshSessionFactory);
|
||||
|
// });
|
||||
|
} |
||||
|
return command.call(); |
||||
|
} |
||||
|
|
||||
|
private static CredentialsProvider newCredentialsProvider(String username, String password) { |
||||
|
return new UsernamePasswordCredentialsProvider(username, password); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils.git.data; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class Branch { |
||||
|
private final String shortName; |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils.git.data; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class Commit { |
||||
|
private final String id; |
||||
|
private final String message; |
||||
|
private final String authorName; |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2022 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.utils.git.data; |
||||
|
|
||||
|
import lombok.Data; |
||||
|
|
||||
|
@Data |
||||
|
public class Diff { |
||||
|
private final String type; |
||||
|
private final String oldPath; |
||||
|
private final String newPath; |
||||
|
} |
||||
Loading…
Reference in new issue