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