Entities VC with Git - initial implementation

This commit is contained in:
Viacheslav Klimov 2022-04-02 19:42:48 +03:00
parent 335b4ef465
commit f82be0153b
12 changed files with 870 additions and 0 deletions

View File

@ -337,6 +337,10 @@
<artifactId>Java-WebSocket</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
</dependency>
</dependencies>
<build>

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -134,6 +134,7 @@
<!-- BLACKBOX TEST SCOPE -->
<testcontainers.version>1.16.0</testcontainers.version>
<zeroturnaround.version>1.12</zeroturnaround.version>
<jgit.version>6.1.0.202203080745-r</jgit.version>
</properties>
<modules>
@ -1875,6 +1876,11 @@
<version>${zeroturnaround.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>${jgit.version}</version>
</dependency>
</dependencies>
</dependencyManagement>