From f82be0153b4e77aba4d686face2b0861ff2af607 Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Sat, 2 Apr 2022 19:42:48 +0300 Subject: [PATCH] Entities VC with Git - initial implementation --- application/pom.xml | 4 + .../EntitiesVersionControlController.java | 118 ++++++++ .../DefaultEntitiesVersionControlService.java | 274 ++++++++++++++++++ .../vcs/EntitiesVersionControlService.java | 50 ++++ .../data/EntitiesVersionControlSettings.java | 28 ++ .../service/sync/vcs/data/EntityVersion.java | 29 ++ .../service/sync/vcs/data/GitSettings.java | 32 ++ .../server/utils/git/Repository.java | 256 ++++++++++++++++ .../server/utils/git/data/Branch.java | 23 ++ .../server/utils/git/data/Commit.java | 25 ++ .../server/utils/git/data/Diff.java | 25 ++ pom.xml | 6 + 12 files changed, 870 insertions(+) create mode 100644 application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/vcs/DefaultEntitiesVersionControlService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/vcs/EntitiesVersionControlService.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntitiesVersionControlSettings.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntityVersion.java create mode 100644 application/src/main/java/org/thingsboard/server/service/sync/vcs/data/GitSettings.java create mode 100644 application/src/main/java/org/thingsboard/server/utils/git/Repository.java create mode 100644 application/src/main/java/org/thingsboard/server/utils/git/data/Branch.java create mode 100644 application/src/main/java/org/thingsboard/server/utils/git/data/Commit.java create mode 100644 application/src/main/java/org/thingsboard/server/utils/git/data/Diff.java diff --git a/application/pom.xml b/application/pom.xml index 1b89752925..2bfab2c858 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -337,6 +337,10 @@ Java-WebSocket test + + org.eclipse.jgit + org.eclipse.jgit + diff --git a/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java new file mode 100644 index 0000000000..10928f1b51 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/controller/EntitiesVersionControlController.java @@ -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 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> 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> 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 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(); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vcs/DefaultEntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vcs/DefaultEntitiesVersionControlService.java new file mode 100644 index 0000000000..0d78496f16 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vcs/DefaultEntitiesVersionControlService.java @@ -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 entitiesIds, String branch, String versionName) throws Exception { + checkRepository(); + checkBranch(tenantId, branch); + + EntityExportSettings exportSettings = EntityExportSettings.builder() + .exportInboundRelations(false) + .exportOutboundRelations(false) + .build(); + List>> 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> 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 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 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 , I extends EntityId> EntityExportData 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>() {}); + } + + @Override + public , I extends EntityId> EntityImportResult loadEntityVersion(TenantId tenantId, I entityId, String versionId) throws Exception { + EntityExportData 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 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()); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vcs/EntitiesVersionControlService.java b/application/src/main/java/org/thingsboard/server/service/sync/vcs/EntitiesVersionControlService.java new file mode 100644 index 0000000000..a9a3c0e3e8 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vcs/EntitiesVersionControlService.java @@ -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 entitiesIds, String branch, String versionName) throws Exception; + + + List listEntityVersions(TenantId tenantId, EntityId entityId, String branch, int limit) throws Exception; + + List listEntityTypeVersions(TenantId tenantId, EntityType entityType, String branch, int limit) throws Exception; + + + , I extends EntityId> EntityExportData getEntityAtVersion(TenantId tenantId, I entityId, String versionId) throws Exception; + + , I extends EntityId> EntityImportResult loadEntityVersion(TenantId tenantId, I entityId, String versionId) throws Exception; + + + void saveSettings(EntitiesVersionControlSettings settings) throws Exception; + + EntitiesVersionControlSettings getSettings(); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntitiesVersionControlSettings.java b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntitiesVersionControlSettings.java new file mode 100644 index 0000000000..6a46606845 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntitiesVersionControlSettings.java @@ -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> allowedBranches; + private GitSettings gitSettings; +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntityVersion.java b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntityVersion.java new file mode 100644 index 0000000000..42a2e2f555 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/EntityVersion.java @@ -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; +} diff --git a/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/GitSettings.java b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/GitSettings.java new file mode 100644 index 0000000000..0d97bed2d1 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/sync/vcs/data/GitSettings.java @@ -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; +} diff --git a/application/src/main/java/org/thingsboard/server/utils/git/Repository.java b/application/src/main/java/org/thingsboard/server/utils/git/Repository.java new file mode 100644 index 0000000000..8099be8259 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/git/Repository.java @@ -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 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 listCommits(String branchName, int limit) throws IOException, GitAPIException { + return listCommits(branchName, null, limit); + } + + public List 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 listFilesAtCommit(Commit commit) throws IOException { + return listFilesAtCommit(commit, null); + } + + public List listFilesAtCommit(Commit commit, String path) throws IOException { + List 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 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 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 , 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); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/utils/git/data/Branch.java b/application/src/main/java/org/thingsboard/server/utils/git/data/Branch.java new file mode 100644 index 0000000000..a458f9f5aa --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/git/data/Branch.java @@ -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; +} diff --git a/application/src/main/java/org/thingsboard/server/utils/git/data/Commit.java b/application/src/main/java/org/thingsboard/server/utils/git/data/Commit.java new file mode 100644 index 0000000000..7567dba1c5 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/git/data/Commit.java @@ -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; +} diff --git a/application/src/main/java/org/thingsboard/server/utils/git/data/Diff.java b/application/src/main/java/org/thingsboard/server/utils/git/data/Diff.java new file mode 100644 index 0000000000..7572a003d2 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/utils/git/data/Diff.java @@ -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; +} diff --git a/pom.xml b/pom.xml index 9713e86993..ea5df86409 100755 --- a/pom.xml +++ b/pom.xml @@ -134,6 +134,7 @@ 1.16.0 1.12 + 6.1.0.202203080745-r @@ -1875,6 +1876,11 @@ ${zeroturnaround.version} test + + org.eclipse.jgit + org.eclipse.jgit + ${jgit.version} +