Entities VC with Git - initial implementation
This commit is contained in:
parent
335b4ef465
commit
f82be0153b
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
6
pom.xml
6
pom.xml
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user