Browse Source

Add command support for applying a stashed commit

Applies the changes in a stashed commit to the local working
directory and index

Bug: 309355
Change-Id: I9fd5ede8affc7f0060ffa7c5cec34573b6fa2b1b
Signed-off-by: Chris Aniszczyk <zx@twitter.com>
tags/v2.0.0.201206130900-r
Kevin Sawicki 12 years ago
parent
commit
4de8a84671

+ 346
- 0
org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashApplyCommandTest.java View File

@@ -0,0 +1,346 @@
/*
* Copyright (C) 2012, GitHub Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.api;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.File;

import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RepositoryTestCase;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.FileUtils;
import org.junit.Before;
import org.junit.Test;

/**
* Unit tests of {@link StashApplyCommand}
*/
public class StashApplyCommandTest extends RepositoryTestCase {

private static final String PATH = "file.txt";

private RevCommit head;

private Git git;

private File committedFile;

@Before
public void setUp() throws Exception {
super.setUp();
git = Git.wrap(db);
committedFile = writeTrashFile(PATH, "content");
git.add().addFilepattern(PATH).call();
head = git.commit().setMessage("add file").call();
assertNotNull(head);
}

@Test
public void workingDirectoryDelete() throws Exception {
deleteTrashFile(PATH);
assertFalse(committedFile.exists());
RevCommit stashed = git.stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertFalse(committedFile.exists());

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getUntracked().isEmpty());
assertTrue(status.getRemoved().isEmpty());

assertEquals(1, status.getMissing().size());
assertTrue(status.getMissing().contains(PATH));
}

@Test
public void indexAdd() throws Exception {
String addedPath = "file2.txt";
File addedFile = writeTrashFile(addedPath, "content2");
git.add().addFilepattern(addedPath).call();

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertFalse(addedFile.exists());

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertTrue(addedFile.exists());
assertEquals("content2", read(addedFile));

Status status = git.status().call();
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getAdded().size());
assertTrue(status.getAdded().contains(addedPath));
}

@Test
public void indexDelete() throws Exception {
git.rm().addFilepattern("file.txt").call();

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertFalse(committedFile.exists());

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getRemoved().size());
assertTrue(status.getRemoved().contains(PATH));
}

@Test
public void workingDirectoryModify() throws Exception {
writeTrashFile("file.txt", "content2");

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertEquals("content2", read(committedFile));

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getModified().size());
assertTrue(status.getModified().contains(PATH));
}

@Test
public void workingDirectoryModifyInSubfolder() throws Exception {
String path = "d1/d2/f.txt";
File subfolderFile = writeTrashFile(path, "content");
git.add().addFilepattern(path).call();
head = git.commit().setMessage("add file").call();

writeTrashFile(path, "content2");

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(subfolderFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertEquals("content2", read(subfolderFile));

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getModified().size());
assertTrue(status.getModified().contains(path));
}

@Test
public void workingDirectoryModifyIndexChanged() throws Exception {
writeTrashFile("file.txt", "content2");
git.add().addFilepattern("file.txt").call();
writeTrashFile("file.txt", "content3");

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertEquals("content3", read(committedFile));

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getChanged().size());
assertTrue(status.getChanged().contains(PATH));
assertEquals(1, status.getModified().size());
assertTrue(status.getModified().contains(PATH));
}

@Test
public void workingDirectoryCleanIndexModify() throws Exception {
writeTrashFile("file.txt", "content2");
git.add().addFilepattern("file.txt").call();
writeTrashFile("file.txt", "content");

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertEquals("content2", read(committedFile));

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getChanged().size());
assertTrue(status.getChanged().contains(PATH));
}

@Test
public void workingDirectoryDeleteIndexAdd() throws Exception {
String path = "file2.txt";
File added = writeTrashFile(path, "content2");
assertTrue(added.exists());
git.add().addFilepattern(path).call();
FileUtils.delete(added);
assertFalse(added.exists());

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertFalse(added.exists());

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertEquals("content2", read(added));

Status status = git.status().call();
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getRemoved().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getAdded().size());
assertTrue(status.getAdded().contains(path));
}

@Test
public void workingDirectoryDeleteIndexEdit() throws Exception {
writeTrashFile(PATH, "content2");
git.add().addFilepattern(PATH).call();
FileUtils.delete(committedFile);
assertFalse(committedFile.exists());

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertEquals("content", read(committedFile));

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);
assertFalse(committedFile.exists());

Status status = git.status().call();
assertTrue(status.getAdded().isEmpty());
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getRemoved().size());
assertTrue(status.getRemoved().contains(PATH));
}

@Test
public void multipleEdits() throws Exception {
String addedPath = "file2.txt";
git.rm().addFilepattern(PATH).call();
File addedFile = writeTrashFile(addedPath, "content2");
git.add().addFilepattern(addedPath).call();

RevCommit stashed = Git.wrap(db).stashCreate().call();
assertNotNull(stashed);
assertTrue(committedFile.exists());
assertFalse(addedFile.exists());

ObjectId unstashed = git.stashApply().call();
assertEquals(stashed, unstashed);

Status status = git.status().call();
assertTrue(status.getChanged().isEmpty());
assertTrue(status.getConflicting().isEmpty());
assertTrue(status.getMissing().isEmpty());
assertTrue(status.getModified().isEmpty());
assertTrue(status.getUntracked().isEmpty());

assertEquals(1, status.getRemoved().size());
assertTrue(status.getRemoved().contains(PATH));
assertEquals(1, status.getAdded().size());
assertTrue(status.getAdded().contains(addedPath));
}
}

+ 4
- 0
org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties View File

@@ -423,7 +423,11 @@ sourceRefDoesntResolveToAnyObject=Source ref {0} doesn't resolve to any object.
sourceRefNotSpecifiedForRefspec=Source ref not specified for refspec: {0}
staleRevFlagsOn=Stale RevFlags on {0}
startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read stage without written request data pending is not supported
stashApplyFailed=Applying stashed changes did not successfully complete
stashApplyOnUnsafeRepository=Cannot apply stashed commit on a repository with state: {0}
stashCommitMissingTwoParents=Stashed commit ''{0}'' does not have two parent commits
stashFailed=Stashing local changes did not successfully complete
stashResolveFailed=Reference ''{0}'' does not resolve to stashed commit
statelessRPCRequiresOptionToBeEnabled=stateless RPC requires {0} to be enabled
submoduleExists=Submodule ''{0}'' already exists in the index
submoduleParentRemoteUrlInvalid=Cannot remove segment from remote url ''{0}''

+ 4
- 0
org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java View File

@@ -483,7 +483,11 @@ public class JGitText extends TranslationBundle {
/***/ public String sourceRefNotSpecifiedForRefspec;
/***/ public String staleRevFlagsOn;
/***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported;
/***/ public String stashApplyFailed;
/***/ public String stashApplyOnUnsafeRepository;
/***/ public String stashCommitMissingTwoParents;
/***/ public String stashFailed;
/***/ public String stashResolveFailed;
/***/ public String statelessRPCRequiresOptionToBeEnabled;
/***/ public String submoduleExists;
/***/ public String submodulesNotSupported;

+ 18
- 10
org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java View File

@@ -54,7 +54,8 @@ import org.eclipse.jgit.util.FS;
/**
* Offers a "GitPorcelain"-like API to interact with a git repository.
* <p>
* The GitPorcelain commands are described in the <a href="http://www.kernel.org/pub/software/scm/git/docs/git.html#_high_level_commands_porcelain"
* The GitPorcelain commands are described in the <a href=
* "http://www.kernel.org/pub/software/scm/git/docs/git.html#_high_level_commands_porcelain"
* >Git Documentation</a>.
* <p>
* This class only offers methods to construct so-called command classes. Each
@@ -109,9 +110,7 @@ public class Git {
RepositoryCache.FileKey key;

key = RepositoryCache.FileKey.lenient(dir, fs);
return wrap(new RepositoryBuilder()
.setFS(fs)
.setGitDir(key.getFile())
return wrap(new RepositoryBuilder().setFS(fs).setGitDir(key.getFile())
.setMustExist(true).build());
}

@@ -266,8 +265,8 @@ public class Git {
* @see <a
* href="http://www.kernel.org/pub/software/scm/git/docs/git-add.html"
* >Git documentation about Add</a>
* @return a {@link AddCommand} used to collect all optional parameters
* and to finally execute the {@code Add} command
* @return a {@link AddCommand} used to collect all optional parameters and
* to finally execute the {@code Add} command
*/
public AddCommand add() {
return new AddCommand(repo);
@@ -279,8 +278,8 @@ public class Git {
* @see <a
* href="http://www.kernel.org/pub/software/scm/git/docs/git-tag.html"
* >Git documentation about Tag</a>
* @return a {@link TagCommand} used to collect all optional parameters
* and to finally execute the {@code Tag} command
* @return a {@link TagCommand} used to collect all optional parameters and
* to finally execute the {@code Tag} command
*/
public TagCommand tag() {
return new TagCommand(repo);
@@ -331,8 +330,8 @@ public class Git {
* @see <a
* href="http://www.kernel.org/pub/software/scm/git/docs/git-revert.html"
* >Git documentation about reverting changes</a>
* @return a {@link RevertCommand} used to collect all optional
* parameters and to finally execute the {@code cherry-pick} command
* @return a {@link RevertCommand} used to collect all optional parameters
* and to finally execute the {@code cherry-pick} command
*/
public RevertCommand revert() {
return new RevertCommand(repo);
@@ -582,6 +581,15 @@ public class Git {
return new StashCreateCommand(repo);
}

/**
* Returns a command object used to apply a stashed commit
*
* @return a {@link StashApplyCommand}
*/
public StashApplyCommand stashApply() {
return new StashApplyCommand(repo);
}

/**
* @return the git repository this class is interacting with
*/

+ 195
- 0
org.eclipse.jgit/src/org/eclipse/jgit/api/StashApplyCommand.java View File

@@ -0,0 +1,195 @@
/*
* Copyright (C) 2012, GitHub Inc.
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.api;

import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;

import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheCheckout;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FileUtils;

/**
* Command class to apply a stashed commit.
*
* @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
* >Git documentation about Stash</a>
*/
public class StashApplyCommand extends GitCommand<ObjectId> {

private static final String DEFAULT_REF = Constants.STASH + "@{0}";

private String stashRef;

/**
* Create command to apply the changes of a stashed commit
*
* @param repo
*/
public StashApplyCommand(final Repository repo) {
super(repo);
}

/**
* Set the stash reference to apply
* <p>
* This will default to apply the latest stashed commit (stash@{0}) if
* unspecified
*
* @param stashRef
* @return {@code this}
*/
public StashApplyCommand setStashRef(final String stashRef) {
this.stashRef = stashRef;
return this;
}

/**
* Apply the changes in a stashed commit to the working directory and index
*
* @return id of stashed commit that was applied
*/
public ObjectId call() throws GitAPIException, JGitInternalException {
checkCallable();

if (repo.getRepositoryState() != RepositoryState.SAFE)
throw new WrongRepositoryStateException(MessageFormat.format(
JGitText.get().stashApplyOnUnsafeRepository,
repo.getRepositoryState()));

final String revision = stashRef != null ? stashRef : DEFAULT_REF;
final ObjectId stashId;
try {
stashId = repo.resolve(revision);
} catch (IOException e) {
throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
}
if (stashId == null)
throw new InvalidRefNameException(MessageFormat.format(
JGitText.get().stashResolveFailed, revision));

ObjectReader reader = repo.newObjectReader();
try {
RevWalk revWalk = new RevWalk(reader);
RevCommit wtCommit = revWalk.parseCommit(stashId);
if (wtCommit.getParentCount() != 2)
throw new JGitInternalException(MessageFormat.format(
JGitText.get().stashCommitMissingTwoParents,
stashId.name()));

// Apply index changes
RevTree indexTree = revWalk.parseCommit(wtCommit.getParent(1))
.getTree();
DirCacheCheckout dco = new DirCacheCheckout(repo,
repo.lockDirCache(), indexTree, new FileTreeIterator(repo));
dco.setFailOnConflict(true);
dco.checkout();

// Apply working directory changes
RevTree headTree = revWalk.parseCommit(wtCommit.getParent(0))
.getTree();
DirCache cache = repo.lockDirCache();
DirCacheEditor editor = cache.editor();
try {
TreeWalk treeWalk = new TreeWalk(reader);
treeWalk.setRecursive(true);
treeWalk.addTree(headTree);
treeWalk.addTree(indexTree);
treeWalk.addTree(wtCommit.getTree());
treeWalk.setFilter(TreeFilter.ANY_DIFF);
File workingTree = repo.getWorkTree();
while (treeWalk.next()) {
String path = treeWalk.getPathString();
File file = new File(workingTree, path);
AbstractTreeIterator headIter = treeWalk.getTree(0,
AbstractTreeIterator.class);
AbstractTreeIterator indexIter = treeWalk.getTree(1,
AbstractTreeIterator.class);
AbstractTreeIterator wtIter = treeWalk.getTree(2,
AbstractTreeIterator.class);
if (wtIter != null) {
DirCacheEntry entry = new DirCacheEntry(
treeWalk.getRawPath());
entry.setObjectId(wtIter.getEntryObjectId());
DirCacheCheckout.checkoutEntry(repo, file, entry);
} else {
if (indexIter != null && headIter != null
&& !indexIter.idEqual(headIter))
editor.add(new DeletePath(path));
FileUtils.delete(file, FileUtils.RETRY
| FileUtils.SKIP_MISSING);
}
}
} finally {
editor.commit();
cache.unlock();
}
} catch (IOException e) {
throw new JGitInternalException(JGitText.get().stashApplyFailed, e);
} finally {
reader.release();
}
return stashId;
}
}

Loading…
Cancel
Save