When select a valid branch then a GitDbBranch is returned
This commit is contained in:
parent
897f9e1bc4
commit
a0fc7c28c5
7 changed files with 137 additions and 249 deletions
|
@ -22,9 +22,7 @@
|
|||
package net.kemitix.gitdb;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.errors.RepositoryNotFoundException;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.lib.RepositoryBuilder;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
@ -57,22 +55,15 @@ public interface GitDB {
|
|||
* @return a GitDB instance for the local gitdb
|
||||
*/
|
||||
static GitDBLocal openLocal(final Path dbDir) {
|
||||
if (dbDir.toFile().isFile()) {
|
||||
throw new GitDBRepoNotFoundException("Not a directory", dbDir);
|
||||
}
|
||||
if (!dbDir.toFile().exists()) {
|
||||
throw new GitDBRepoNotFoundException("Directory not found", dbDir);
|
||||
}
|
||||
Repository repository = null;
|
||||
try {
|
||||
repository = Git.open(dbDir.toFile()).getRepository();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Optional.of(Git.open(dbDir.toFile()))
|
||||
.map(Git::getRepository)
|
||||
.filter(Repository::isBare)
|
||||
.map(GitDBLocal::new)
|
||||
.orElseThrow(() -> new InvalidRepositoryException("Not a bare repo", dbDir));
|
||||
} catch (IOException e) {
|
||||
throw new InvalidRepositoryException("Error opening repository", dbDir, e);
|
||||
}
|
||||
if (repository != null && repository.isBare()) {
|
||||
return new GitDBLocal(repository);
|
||||
}
|
||||
throw new InvalidRepositoryException("Not a bare repo", dbDir);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,6 +71,7 @@ public interface GitDB {
|
|||
*
|
||||
* @param name the branch to select
|
||||
* @return an Optional containing the branch if it exists
|
||||
* @throws IOException if there is an error accessing the branch name
|
||||
*/
|
||||
Optional<GitDBBranch> branch(String name);
|
||||
Optional<GitDBBranch> branch(String name) throws IOException;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
package net.kemitix.gitdb;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
|
||||
/**
|
||||
|
@ -28,8 +30,18 @@ import org.eclipse.jgit.lib.Ref;
|
|||
*
|
||||
* @author Paul Campbell (pcampbell@kemitix.net)
|
||||
*/
|
||||
public interface GitDBBranch {
|
||||
static GitDBBranch withRef(final Ref ref) {
|
||||
return null;
|
||||
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class GitDBBranch {
|
||||
|
||||
private final Ref ref;
|
||||
|
||||
/**
|
||||
* Create a new instance of GitDBBranch for the Ref.
|
||||
*
|
||||
* @param ref the Ref
|
||||
* @return a GitDBBranch
|
||||
*/
|
||||
public static GitDBBranch withRef(final Ref ref) {
|
||||
return new GitDBBranch(ref);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,13 +21,12 @@
|
|||
|
||||
package net.kemitix.gitdb;
|
||||
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.*;
|
||||
import org.eclipse.jgit.util.FS;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NotDirectoryException;
|
||||
|
@ -61,14 +60,7 @@ class GitDBLocal implements GitDB {
|
|||
* @param repository the Git repository
|
||||
*/
|
||||
GitDBLocal(final Repository repository) {
|
||||
this.repository = verifyIsBareRepo(repository);
|
||||
}
|
||||
|
||||
private static Repository verifyIsBareRepo(final Repository repository) {
|
||||
if (repository.isBare()) {
|
||||
return repository;
|
||||
}
|
||||
throw new InvalidRepositoryException("Not a bare repo", repository.getDirectory().toPath());
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
private void validateDbDir(final File dbDir) throws IOException {
|
||||
|
@ -92,129 +84,65 @@ class GitDBLocal implements GitDB {
|
|||
|
||||
private static Repository initRepo(final File dbDir) throws IOException {
|
||||
dbDir.mkdirs();
|
||||
final RepositoryCache.FileKey fileKey = RepositoryCache.FileKey.exact(dbDir, FS.DETECTED);
|
||||
final Repository repository = fileKey.open(false);
|
||||
final Repository repository = RepositoryCache.FileKey.exact(dbDir, FS.DETECTED).open(false);
|
||||
repository.create(true);
|
||||
createInitialBranchOnMaster(repository);
|
||||
return repository;
|
||||
}
|
||||
|
||||
private static void createInitialBranchOnMaster(final Repository repository) throws IOException {
|
||||
// create empty file
|
||||
final ObjectId objectId = insertAnEmptyBlob(repository);
|
||||
// create tree
|
||||
final ObjectId treeId = insertTree(repository, objectId);
|
||||
// create commit
|
||||
final ObjectId commitId = insertCommit(repository, treeId);
|
||||
// create branch
|
||||
writeBranch(repository, commitId, "master");
|
||||
}
|
||||
|
||||
private static void writeBranch(
|
||||
final Repository repository,
|
||||
final ObjectId commitId,
|
||||
final String branchName
|
||||
) throws IOException {
|
||||
final Path branchRefPath =
|
||||
repository.getDirectory().toPath().resolve("refs/heads/" + branchName).toAbsolutePath();
|
||||
final byte[] commitIdBytes = commitId.name().getBytes(StandardCharsets.UTF_8);
|
||||
Files.write(branchRefPath, commitIdBytes);
|
||||
}
|
||||
|
||||
private static ObjectId insertCommit(
|
||||
final Repository repository,
|
||||
final ObjectId treeId
|
||||
) throws IOException {
|
||||
final CommitBuilder commitBuilder = new CommitBuilder();
|
||||
commitBuilder.setTreeId(treeId);
|
||||
commitBuilder.setMessage("Initialise GitDB v1");
|
||||
final PersonIdent ident = new PersonIdent("GitDB", "pcampbell@kemitix.net");
|
||||
commitBuilder.setAuthor(ident);
|
||||
commitBuilder.setCommitter(ident);
|
||||
commitBuilder.setParentId(ObjectId.zeroId());
|
||||
return repository.getObjectDatabase().newInserter().insert(commitBuilder);
|
||||
}
|
||||
|
||||
private static ObjectId insertTree(
|
||||
final Repository repository,
|
||||
final ObjectId objectId
|
||||
) throws IOException {
|
||||
final TreeFormatter treeFormatter = new TreeFormatter();
|
||||
treeFormatter.append("isGitDB", FileMode.REGULAR_FILE, objectId);
|
||||
return repository.getObjectDatabase().newInserter().insert(treeFormatter);
|
||||
}
|
||||
|
||||
private static ObjectId insertAnEmptyBlob(final Repository repository) throws IOException {
|
||||
return repository.getObjectDatabase().newInserter().insert(Constants.OBJ_BLOB, new byte[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<GitDBBranch> branch(final String name) {
|
||||
try {
|
||||
public Optional<GitDBBranch> branch(final String name) throws IOException {
|
||||
return Optional.ofNullable(repository.findRef(name))
|
||||
.map(GitDBBranch::withRef);
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// @Override
|
||||
// @SneakyThrows
|
||||
// public <T> T get(Branch branch, Key key, Class<T> type) {
|
||||
// //branch
|
||||
// final RefDatabase refDatabase = repository.getRefDatabase();
|
||||
// final String branchValue = branch.getValue();
|
||||
// final Ref refDatabaseRef = refDatabase.getRef(branchValue);
|
||||
// final ObjectId commitId = refDatabaseRef.getObjectId();
|
||||
//
|
||||
// final RevCommit revCommit = repository.parseCommit(commitId);
|
||||
// final RevTree tree = revCommit.getTree();
|
||||
// tree.copyTo(System.out);
|
||||
//
|
||||
// final ObjectLoader open = repository.getObjectDatabase().open(objectId, Constants.OBJ_TREE);
|
||||
// final byte[] bytes = open.getBytes();
|
||||
// final String s = new String(bytes);
|
||||
// System.out.println("s = " + s);
|
||||
// //key
|
||||
// return null;
|
||||
// }
|
||||
|
||||
// @Override
|
||||
// @SneakyThrows
|
||||
// public String put(Branch branch, Message message, Document<String> document, Author author) {
|
||||
//// return document.getValue();
|
||||
//
|
||||
// final ObjectInserter objectInserter = repository.newObjectInserter();
|
||||
// final ObjectReader objectReader = repository.newObjectReader();
|
||||
// final RevWalk revWalk = new RevWalk(repository);
|
||||
//
|
||||
// //blob
|
||||
// System.out.println("document = " + document.getKey());
|
||||
// final ObjectId blobId = objectInserter.insert(Constants.OBJ_BLOB, document.getValue().getBytes(UTF_8));
|
||||
// //tree
|
||||
// final TreeFormatter treeFormatter = new TreeFormatter();
|
||||
// treeFormatter.append(document.getKey().getValue(), FileMode.REGULAR_FILE, blobId);
|
||||
// final ObjectId treeId = objectInserter.insert(treeFormatter);
|
||||
// //commit
|
||||
// final CommitBuilder commitBuilder = new CommitBuilder();
|
||||
// final PersonIdent ident = new PersonIdent(author.getName(), author.getEmail());
|
||||
// commitBuilder.setCommitter(ident);
|
||||
// commitBuilder.setAuthor(ident);
|
||||
// commitBuilder.setTreeId(treeId);
|
||||
// commitBuilder.setMessage(message.getValue());
|
||||
// //TODO: setParentId()
|
||||
// final ObjectId commitId = objectInserter.insert(commitBuilder);
|
||||
// //branch
|
||||
// final RevCommit revCommit = revWalk.parseCommit(commitId);
|
||||
// revCommit.getShortMessage();
|
||||
// git.branchCreate()
|
||||
// .setStartPoint(revCommit)
|
||||
// .setName(branch.getValue())
|
||||
// .setUpstreamMode(CreateBranchCommand.SetupUpstreamMode.NOTRACK)
|
||||
// .call();
|
||||
//
|
||||
// //READ
|
||||
//
|
||||
// //block
|
||||
// final String readBlob = new String(objectReader.open(blobId).getBytes());
|
||||
// System.out.println("readBlob = " + readBlob);
|
||||
// final RevBlob revBlob = revWalk.lookupBlob(blobId);
|
||||
// System.out.println("revBlob = " + revBlob);
|
||||
// final String blobName = revBlob.name();
|
||||
// System.out.println("blobName = " + blobName);
|
||||
// //tree
|
||||
// final RevTree revTree = revWalk.lookupTree(treeId);
|
||||
// System.out.println("revTree = " + revTree);
|
||||
// final String treeName = revTree.name();
|
||||
// System.out.println("treeName = " + treeName);
|
||||
// //commit
|
||||
// System.out.println("revCommit= " + revCommit);
|
||||
// final String commitName = revCommit.getName();
|
||||
// System.out.println("commitName= " + commitName);
|
||||
// //branch
|
||||
// final Ref branchRef = repository.getRefDatabase().getRef(branch.getValue());
|
||||
// System.out.println("branchRef = " + branchRef.getName());
|
||||
//
|
||||
//// final TreeWalk treeWalk = new TreeWalk(repository);
|
||||
//// treeWalk.addTree(treeId);
|
||||
//// treeWalk.next();
|
||||
//// final String nameString = treeWalk.getNameString();
|
||||
//// System.out.println("name = " + nameString);
|
||||
//// final ObjectId objectId = treeWalk.getObjectId(0);
|
||||
//// System.out.println("objectId = " + objectId);
|
||||
//
|
||||
//// final ObjectLoader openTree = repository.newObjectReader().open(treeId);
|
||||
//// final int type = openTree.openStream().getType();
|
||||
//// final long size = openTree.openStream().getSize();
|
||||
//// final String readTree = new String(openTree.getBytes());
|
||||
//
|
||||
////
|
||||
//// //commit
|
||||
//// final CommitBuilder commitBuilder = new CommitBuilder();
|
||||
//// commitBuilder.setAuthor(new PersonIdent(author.getName(), author.getEmail()));
|
||||
//// commitBuilder.setCommitter(new PersonIdent(author.getName(), author.getEmail()));
|
||||
//// commitBuilder.setMessage(message.getValue());
|
||||
//// findParentCommit(branch)
|
||||
//// .ifPresent(commitBuilder::setParentId);
|
||||
//// commitBuilder.setTreeId(treeId);
|
||||
//// final ObjectId commitId = repository.newObjectInserter().insert(commitBuilder);
|
||||
////
|
||||
//// //branch
|
||||
//// repository.updateRef(branch.getValue()).setNewObjectId(commitId);
|
||||
////
|
||||
//// //get
|
||||
//// return get(branch, document.getKey());
|
||||
// return document.getValue();
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Paul Campbell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies
|
||||
or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
|
||||
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.kemitix.gitdb;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Runtime exception thrown when attempting to open to location that is not a GitDB repo.
|
||||
*
|
||||
* @author Paul Campbell (pcampbell@kemitix.net)
|
||||
*/
|
||||
public class GitDBRepoNotFoundException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param message why the GitDB wasn't found
|
||||
* @param path the location where a GitDB repo was not found
|
||||
*/
|
||||
GitDBRepoNotFoundException(final String message, final Path path) {
|
||||
super(String.format("GitDB repo not found: %s: %s", message, path));
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
package net.kemitix.gitdb;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
|
@ -36,7 +37,32 @@ public class InvalidRepositoryException extends RuntimeException {
|
|||
* @param message the reason the repo is invalid
|
||||
* @param path the location of the repo
|
||||
*/
|
||||
public InvalidRepositoryException(final String message, final Path path) {
|
||||
super(String.format("Invalid GitDB repo: %s [%s]", message, path));
|
||||
public InvalidRepositoryException(
|
||||
final String message,
|
||||
final Path path
|
||||
) {
|
||||
super(message(message, path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param message the reason the repo is invalid
|
||||
* @param path the location of the repo
|
||||
* @param cause the cause
|
||||
*/
|
||||
public InvalidRepositoryException(
|
||||
final String message,
|
||||
final Path path,
|
||||
final IOException cause
|
||||
) {
|
||||
super(message(message, path), cause);
|
||||
}
|
||||
|
||||
private static String message(
|
||||
final String message,
|
||||
final Path path
|
||||
) {
|
||||
return String.format("Invalid GitDB repo: %s [%s]", message, path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Paul Campbell
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
and associated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies
|
||||
or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
|
||||
AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
package net.kemitix.gitdb;
|
||||
|
||||
/**
|
||||
* Unchecked exception thrown when JGit throws a an unexpected exception.
|
||||
*
|
||||
* @author Paul Campbell (pcampbell@kemitix.net)
|
||||
*/
|
||||
public class UnexpectedGitDbException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Constructs an instance of this class.
|
||||
*
|
||||
* @param message the detail message. The detail message is saved for
|
||||
* later retrieval by the {@link #getMessage()} method.
|
||||
* @param cause the cause (which is saved for later retrieval by the
|
||||
* {@link #getCause()} method). (A <tt>null</tt> value is
|
||||
* permitted, and indicates that the cause is nonexistent or
|
||||
* unknown.)
|
||||
*/
|
||||
public UnexpectedGitDbException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,6 @@ package net.kemitix.gitdb;
|
|||
|
||||
import org.assertj.core.api.WithAssertions;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
import org.eclipse.jgit.api.InitCommand;
|
||||
import org.eclipse.jgit.api.errors.GitAPIException;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
|
@ -10,13 +9,10 @@ import org.junit.jupiter.api.Test;
|
|||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GitDBTest implements WithAssertions {
|
||||
|
||||
|
@ -115,7 +111,7 @@ class GitDBTest implements WithAssertions {
|
|||
//given
|
||||
final Path dir = fileExists();
|
||||
//then
|
||||
assertThatExceptionOfType(GitDBRepoNotFoundException.class)
|
||||
assertThatExceptionOfType(InvalidRepositoryException.class)
|
||||
.isThrownBy(() -> GitDB.openLocal(dir))
|
||||
.withMessageContaining(dir.toString());
|
||||
}
|
||||
|
@ -126,7 +122,7 @@ class GitDBTest implements WithAssertions {
|
|||
//given
|
||||
final Path dir = dirDoesNotExist();
|
||||
//then
|
||||
assertThatExceptionOfType(GitDBRepoNotFoundException.class)
|
||||
assertThatExceptionOfType(InvalidRepositoryException.class)
|
||||
.isThrownBy(() -> GitDB.openLocal(dir))
|
||||
.withMessageContaining(dir.toString());
|
||||
}
|
||||
|
@ -170,20 +166,30 @@ class GitDBTest implements WithAssertions {
|
|||
// Given a valid GitDb handle
|
||||
// When select a branch that doesn't exist then an empty Optional is returned
|
||||
@Test
|
||||
void selectBranch_branchNotExist_thenEmptyOptional() throws IOException {
|
||||
void selectBranch_whenBranchNotExist_thenEmptyOptional() throws IOException {
|
||||
//given
|
||||
final GitDB gitDb = newGitDBRepo();
|
||||
final GitDB gitDb = newGitDBRepo(dirDoesNotExist());
|
||||
//when
|
||||
final Optional<GitDBBranch> branch = gitDb.branch("unknown");
|
||||
//then
|
||||
assertThat(branch).isEmpty();
|
||||
}
|
||||
|
||||
private GitDB newGitDBRepo() throws IOException {
|
||||
return GitDB.initLocal(dirDoesNotExist());
|
||||
private GitDB newGitDBRepo(final Path dbDir) throws IOException {
|
||||
return GitDB.initLocal(dbDir);
|
||||
}
|
||||
|
||||
// When select a valid branch then a GitDbBranch is returned
|
||||
@Test
|
||||
void selectBranch_branchExists_thenReturnBranch() throws IOException {
|
||||
//given
|
||||
final Path dbDir = dirDoesNotExist();
|
||||
final GitDB gitDb = newGitDBRepo(dbDir);
|
||||
//when
|
||||
final Optional<GitDBBranch> branch = gitDb.branch("master");
|
||||
//then
|
||||
assertThat(branch).as("Branch master exists").isNotEmpty();
|
||||
}
|
||||
|
||||
// Given a valid GitDbBranch handle
|
||||
// When getting a key that does not exist then return an empty Optional
|
||||
|
@ -242,4 +248,15 @@ class GitDBTest implements WithAssertions {
|
|||
// // .map(Path::toFile)
|
||||
// // .forEach(File::delete);
|
||||
// }
|
||||
|
||||
private static void tree(final Path dbDir, final PrintStream out) throws IOException {
|
||||
final Process treeProcess = new ProcessBuilder("tree", dbDir.toString()).start();
|
||||
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(treeProcess.getInputStream()))) {
|
||||
String line;
|
||||
while (null != (line = reader.readLine())) {
|
||||
out.println("line = " + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue