diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java new file mode 100644 index 000000000..b60af64a8 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/StashDropCommandTest.java @@ -0,0 +1,298 @@ +/* + * 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.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.File; +import java.util.List; + +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.storage.file.ReflogEntry; +import org.eclipse.jgit.storage.file.ReflogReader; +import org.junit.Before; +import org.junit.Test; + +/** + * Unit tests of {@link StashCreateCommand} + */ +public class StashDropCommandTest extends RepositoryTestCase { + + private RevCommit head; + + private Git git; + + private File committedFile; + + @Before + public void setUp() throws Exception { + super.setUp(); + git = Git.wrap(db); + committedFile = writeTrashFile("file.txt", "content"); + git.add().addFilepattern("file.txt").call(); + head = git.commit().setMessage("add file").call(); + assertNotNull(head); + } + + @Test(expected = IllegalArgumentException.class) + public void dropNegativeRef() { + git.stashDrop().setStashRef(-1); + } + + @Test + public void dropWithNoStashedCommits() throws Exception { + assertNull(git.stashDrop().call()); + } + + @Test + public void dropWithInvalidLogIndex() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit stashed = git.stashCreate().call(); + assertNotNull(stashed); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertEquals(stashed, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + try { + assertNull(git.stashDrop().setStashRef(100).call()); + fail("Exception not thrown"); + } catch (JGitInternalException e) { + assertNotNull(e.getMessage()); + assertTrue(e.getMessage().length() > 0); + } + } + + @Test + public void dropSingleStashedCommit() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit stashed = git.stashCreate().call(); + assertNotNull(stashed); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertEquals(stashed, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + assertNull(git.stashDrop().call()); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + + ReflogReader reader = new ReflogReader(git.getRepository(), + Constants.R_STASH); + assertTrue(reader.getReverseEntries().isEmpty()); + } + + @Test + public void dropAll() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit firstStash = git.stashCreate().call(); + assertNotNull(firstStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(firstStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content3"); + RevCommit secondStash = git.stashCreate().call(); + assertNotNull(secondStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(secondStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + assertNull(git.stashDrop().setAll(true).call()); + assertNull(git.getRepository().getRef(Constants.R_STASH)); + + ReflogReader reader = new ReflogReader(git.getRepository(), + Constants.R_STASH); + assertTrue(reader.getReverseEntries().isEmpty()); + } + + @Test + public void dropFirstStashedCommit() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit firstStash = git.stashCreate().call(); + assertNotNull(firstStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(firstStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content3"); + RevCommit secondStash = git.stashCreate().call(); + assertNotNull(secondStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(secondStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + assertEquals(firstStash, git.stashDrop().call()); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(firstStash, stashRef.getObjectId()); + + ReflogReader reader = new ReflogReader(git.getRepository(), + Constants.R_STASH); + List entries = reader.getReverseEntries(); + assertEquals(1, entries.size()); + assertEquals(ObjectId.zeroId(), entries.get(0).getOldId()); + assertEquals(firstStash, entries.get(0).getNewId()); + assertTrue(entries.get(0).getComment().length() > 0); + } + + @Test + public void dropMiddleStashCommit() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit firstStash = git.stashCreate().call(); + assertNotNull(firstStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(firstStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content3"); + RevCommit secondStash = git.stashCreate().call(); + assertNotNull(secondStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(secondStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content4"); + RevCommit thirdStash = git.stashCreate().call(); + assertNotNull(thirdStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(thirdStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + assertEquals(thirdStash, git.stashDrop().setStashRef(1).call()); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(thirdStash, stashRef.getObjectId()); + + ReflogReader reader = new ReflogReader(git.getRepository(), + Constants.R_STASH); + List entries = reader.getReverseEntries(); + assertEquals(2, entries.size()); + assertEquals(ObjectId.zeroId(), entries.get(1).getOldId()); + assertEquals(firstStash, entries.get(1).getNewId()); + assertTrue(entries.get(1).getComment().length() > 0); + assertEquals(entries.get(0).getOldId(), entries.get(1).getNewId()); + assertEquals(thirdStash, entries.get(0).getNewId()); + assertTrue(entries.get(0).getComment().length() > 0); + } + + @Test + public void dropBoundaryStashedCommits() throws Exception { + write(committedFile, "content2"); + Ref stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNull(stashRef); + RevCommit firstStash = git.stashCreate().call(); + assertNotNull(firstStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(firstStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content3"); + RevCommit secondStash = git.stashCreate().call(); + assertNotNull(secondStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(secondStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content4"); + RevCommit thirdStash = git.stashCreate().call(); + assertNotNull(thirdStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(thirdStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + write(committedFile, "content5"); + RevCommit fourthStash = git.stashCreate().call(); + assertNotNull(fourthStash); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(fourthStash, git.getRepository().getRef(Constants.R_STASH) + .getObjectId()); + + assertEquals(thirdStash, git.stashDrop().call()); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(thirdStash, stashRef.getObjectId()); + + assertEquals(thirdStash, git.stashDrop().setStashRef(2).call()); + stashRef = git.getRepository().getRef(Constants.R_STASH); + assertNotNull(stashRef); + assertEquals(thirdStash, stashRef.getObjectId()); + + ReflogReader reader = new ReflogReader(git.getRepository(), + Constants.R_STASH); + List entries = reader.getReverseEntries(); + assertEquals(2, entries.size()); + assertEquals(ObjectId.zeroId(), entries.get(1).getOldId()); + assertEquals(secondStash, entries.get(1).getNewId()); + assertTrue(entries.get(1).getComment().length() > 0); + assertEquals(entries.get(0).getOldId(), entries.get(1).getNewId()); + assertEquals(thirdStash, entries.get(0).getNewId()); + assertTrue(entries.get(0).getComment().length() > 0); + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 2f07c97ac..0554efacd 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -429,6 +429,9 @@ startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read st 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 +stashDropDeleteRefFailed=Deleting stash reference failed with result: {0} +stashDropFailed=Dropping stashed commit failed +stashDropMissingReflog=Stash reflog does not contain entry ''{0}'' 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 diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 6c44d955f..f52358633 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -590,6 +590,15 @@ public class Git { return new StashApplyCommand(repo); } + /** + * Returns a command object used to drop a stashed commit + * + * @return a {@link StashDropCommand} + */ + public StashDropCommand stashDrop() { + return new StashDropCommand(repo); + } + /** * Returns a command object to execute a {@code apply} command * diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java new file mode 100644 index 000000000..85bb987ff --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/StashDropCommand.java @@ -0,0 +1,236 @@ +/* + * 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.eclipse.jgit.lib.Constants.R_STASH; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.List; + +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.errors.LockFailedException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.storage.file.ReflogEntry; +import org.eclipse.jgit.storage.file.ReflogReader; +import org.eclipse.jgit.storage.file.ReflogWriter; +import org.eclipse.jgit.util.FileUtils; + +/** + * Command class to delete a stashed commit reference + * + * @see Git documentation about Stash + */ +public class StashDropCommand extends GitCommand { + + private int stashRefEntry; + + private boolean all; + + /** + * @param repo + */ + public StashDropCommand(Repository repo) { + super(repo); + } + + /** + * Set the stash reference to drop (0-based). + *

+ * This will default to drop the latest stashed commit (stash@{0}) if + * unspecified + * + * @param stashRef + * @return {@code this} + */ + public StashDropCommand setStashRef(final int stashRef) { + if (stashRef < 0) + throw new IllegalArgumentException(); + + stashRefEntry = stashRef; + return this; + } + + /** + * Set wheter drop all stashed commits + * + * @param all + * true to drop all stashed commits, false to drop only the + * stashed commit set via calling {@link #setStashRef(int)} + * @return {@code this} + */ + public StashDropCommand setAll(final boolean all) { + this.all = all; + return this; + } + + private Ref getRef() throws GitAPIException { + try { + return repo.getRef(R_STASH); + } catch (IOException e) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().cannotRead, R_STASH), e); + } + } + + private RefUpdate createRefUpdate(final Ref stashRef) throws IOException { + RefUpdate update = repo.updateRef(R_STASH); + update.disableRefLog(); + update.setExpectedOldObjectId(stashRef.getObjectId()); + update.setForceUpdate(true); + return update; + } + + private void deleteRef(final Ref stashRef) { + try { + Result result = createRefUpdate(stashRef).delete(); + if (Result.FORCED != result) + throw new JGitInternalException(MessageFormat.format( + JGitText.get().stashDropDeleteRefFailed, result)); + } catch (IOException e) { + throw new JGitInternalException(JGitText.get().stashDropFailed, e); + } + } + + private void updateRef(Ref stashRef, ObjectId newId) { + try { + RefUpdate update = createRefUpdate(stashRef); + update.setNewObjectId(newId); + Result result = update.update(); + switch (result) { + case FORCED: + case NEW: + case NO_CHANGE: + return; + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, R_STASH, newId, + result)); + } + } catch (IOException e) { + throw new JGitInternalException(JGitText.get().stashDropFailed, e); + } + } + + /** + * Drop the configured entry from the stash reflog and return value of the + * stash reference after the drop occurs + * + * @return commit id of stash reference or null if no more stashed changes + */ + public ObjectId call() throws GitAPIException, JGitInternalException { + checkCallable(); + + Ref stashRef = getRef(); + if (stashRef == null) + return null; + + if (all) { + deleteRef(stashRef); + return null; + } + + ReflogReader reader = new ReflogReader(repo, R_STASH); + List entries; + try { + entries = reader.getReverseEntries(); + } catch (IOException e) { + throw new JGitInternalException(JGitText.get().stashDropFailed, e); + } + + if (stashRefEntry >= entries.size()) + throw new JGitInternalException( + JGitText.get().stashDropMissingReflog); + + if (entries.size() == 1) { + deleteRef(stashRef); + return null; + } + + ReflogWriter writer = new ReflogWriter(repo, true); + String stashLockRef = ReflogWriter.refLockFor(R_STASH); + File stashLockFile = writer.logFor(stashLockRef); + File stashFile = writer.logFor(R_STASH); + if (stashLockFile.exists()) + throw new JGitInternalException(JGitText.get().stashDropFailed, + new LockFailedException(stashFile)); + + entries.remove(stashRefEntry); + ObjectId entryId = ObjectId.zeroId(); + try { + for (int i = entries.size() - 1; i >= 0; i--) { + ReflogEntry entry = entries.get(i); + writer.log(stashLockRef, entryId, entry.getNewId(), + entry.getWho(), entry.getComment()); + entryId = entry.getNewId(); + } + if (!stashLockFile.renameTo(stashFile)) { + FileUtils.delete(stashFile); + if (!stashLockFile.renameTo(stashFile)) + throw new JGitInternalException(MessageFormat.format( + JGitText.get().couldNotWriteFile, + stashLockFile.getPath(), stashFile.getPath())); + } + } catch (IOException e) { + throw new JGitInternalException(JGitText.get().stashDropFailed, e); + } + updateRef(stashRef, entryId); + + try { + Ref newStashRef = repo.getRef(R_STASH); + return newStashRef != null ? newStashRef.getObjectId() : null; + } catch (IOException e) { + throw new InvalidRefNameException(MessageFormat.format( + JGitText.get().cannotRead, R_STASH), e); + } + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index b51fc4b17..c9f5d338f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -489,6 +489,9 @@ public class JGitText extends TranslationBundle { /***/ public String stashApplyFailed; /***/ public String stashApplyOnUnsafeRepository; /***/ public String stashCommitMissingTwoParents; + /***/ public String stashDropDeleteRefFailed; + /***/ public String stashDropFailed; + /***/ public String stashDropMissingReflog; /***/ public String stashFailed; /***/ public String stashResolveFailed; /***/ public String statelessRPCRequiresOptionToBeEnabled; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java index 0aad1d3c5..a152e86fb 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectory.java @@ -48,15 +48,11 @@ package org.eclipse.jgit.storage.file; import static org.eclipse.jgit.lib.Constants.CHARSET; import static org.eclipse.jgit.lib.Constants.HEAD; -import static org.eclipse.jgit.lib.Constants.LOGS; import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; import static org.eclipse.jgit.lib.Constants.PACKED_REFS; import static org.eclipse.jgit.lib.Constants.R_HEADS; import static org.eclipse.jgit.lib.Constants.R_REFS; -import static org.eclipse.jgit.lib.Constants.R_REMOTES; -import static org.eclipse.jgit.lib.Constants.R_STASH; import static org.eclipse.jgit.lib.Constants.R_TAGS; -import static org.eclipse.jgit.lib.Constants.encode; import static org.eclipse.jgit.lib.Ref.Storage.LOOSE; import static org.eclipse.jgit.lib.Ref.Storage.NEW; import static org.eclipse.jgit.lib.Ref.Storage.PACKED; @@ -65,11 +61,8 @@ import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.text.MessageFormat; import java.util.Arrays; import java.util.LinkedList; @@ -84,10 +77,8 @@ import org.eclipse.jgit.errors.ObjectWritingException; import org.eclipse.jgit.events.RefsChangedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; -import org.eclipse.jgit.lib.CoreConfig; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectIdRef; -import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.RefComparator; import org.eclipse.jgit.lib.RefDatabase; @@ -141,9 +132,7 @@ public class RefDirectory extends RefDatabase { private final File refsDir; - private final File logsDir; - - private final File logsRefsDir; + private final ReflogWriter logWriter; private final File packedRefsFile; @@ -180,9 +169,8 @@ public class RefDirectory extends RefDatabase { final FS fs = db.getFS(); parent = db; gitDir = db.getDirectory(); + logWriter = new ReflogWriter(db); refsDir = fs.resolve(gitDir, R_REFS); - logsDir = fs.resolve(gitDir, LOGS); - logsRefsDir = fs.resolve(gitDir, LOGS + '/' + R_REFS); packedRefsFile = fs.resolve(gitDir, PACKED_REFS); looseRefs.set(RefList. emptyList()); @@ -193,15 +181,15 @@ public class RefDirectory extends RefDatabase { return parent; } + ReflogWriter getLogWriter() { + return logWriter; + } + public void create() throws IOException { FileUtils.mkdir(refsDir); - FileUtils.mkdir(logsDir); - FileUtils.mkdir(logsRefsDir); - FileUtils.mkdir(new File(refsDir, R_HEADS.substring(R_REFS.length()))); FileUtils.mkdir(new File(refsDir, R_TAGS.substring(R_REFS.length()))); - FileUtils.mkdir(new File(logsRefsDir, - R_HEADS.substring(R_REFS.length()))); + logWriter.create(); } @Override @@ -585,7 +573,7 @@ public class RefDirectory extends RefDatabase { } while (!looseRefs.compareAndSet(curLoose, newLoose)); int levels = levelsIn(name) - 2; - delete(logFor(name), levels); + delete(logWriter.logFor(name), levels); if (dst.getStorage().isLoose()) { update.unlock(); delete(fileFor(name), levels); @@ -597,83 +585,7 @@ public class RefDirectory extends RefDatabase { void log(final RefUpdate update, final String msg, final boolean deref) throws IOException { - final ObjectId oldId = update.getOldObjectId(); - final ObjectId newId = update.getNewObjectId(); - final Ref ref = update.getRef(); - - PersonIdent ident = update.getRefLogIdent(); - if (ident == null) - ident = new PersonIdent(parent); - else - ident = new PersonIdent(ident); - - final StringBuilder r = new StringBuilder(); - r.append(ObjectId.toString(oldId)); - r.append(' '); - r.append(ObjectId.toString(newId)); - r.append(' '); - r.append(ident.toExternalString()); - r.append('\t'); - r.append(msg); - r.append('\n'); - final byte[] rec = encode(r.toString()); - - if (deref && ref.isSymbolic()) { - log(ref.getName(), rec); - log(ref.getLeaf().getName(), rec); - } else { - log(ref.getName(), rec); - } - } - - private void log(final String refName, final byte[] rec) throws IOException { - final File log = logFor(refName); - final boolean write; - if (isLogAllRefUpdates() && shouldAutoCreateLog(refName)) - write = true; - else if (log.isFile()) - write = true; - else - write = false; - - if (write) { - WriteConfig wc = getRepository().getConfig().get(WriteConfig.KEY); - FileOutputStream out; - try { - out = new FileOutputStream(log, true); - } catch (FileNotFoundException err) { - final File dir = log.getParentFile(); - if (dir.exists()) - throw err; - if (!dir.mkdirs() && !dir.isDirectory()) - throw new IOException(MessageFormat.format(JGitText.get().cannotCreateDirectory, dir)); - out = new FileOutputStream(log, true); - } - try { - if (wc.getFSyncRefFiles()) { - FileChannel fc = out.getChannel(); - ByteBuffer buf = ByteBuffer.wrap(rec); - while (0 < buf.remaining()) - fc.write(buf); - fc.force(true); - } else { - out.write(rec); - } - } finally { - out.close(); - } - } - } - - private boolean isLogAllRefUpdates() { - return parent.getConfig().get(CoreConfig.KEY).isLogAllRefUpdates(); - } - - private boolean shouldAutoCreateLog(final String refName) { - return refName.equals(HEAD) // - || refName.startsWith(R_HEADS) // - || refName.startsWith(R_REMOTES) // - || refName.equals(R_STASH); + logWriter.log(update, msg, deref); } private Ref resolve(final Ref ref, int depth, String prefix, @@ -966,22 +878,6 @@ public class RefDirectory extends RefDatabase { return new File(gitDir, name); } - /** - * Locate the log file on disk for a single reference name. - * - * @param name - * name of the ref, relative to the Git repository top level - * directory (so typically starts with refs/). - * @return the log file location. - */ - File logFor(String name) { - if (name.startsWith(R_REFS)) { - name = name.substring(R_REFS.length()); - return new File(logsRefsDir, name); - } - return new File(logsDir, name); - } - static int levelsIn(final String name) { int count = 0; for (int p = name.indexOf('/'); p >= 0; p = name.indexOf('/', p + 1)) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryRename.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryRename.java index 4fb225e08..60ee2b19a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryRename.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/RefDirectoryRename.java @@ -183,8 +183,8 @@ class RefDirectoryRename extends RefRename { } private boolean renameLog(RefUpdate src, RefUpdate dst) { - File srcLog = refdb.logFor(src.getName()); - File dstLog = refdb.logFor(dst.getName()); + File srcLog = refdb.getLogWriter().logFor(src.getName()); + File dstLog = refdb.getLogWriter().logFor(dst.getName()); if (!srcLog.exists()) return true; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ReflogWriter.java b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ReflogWriter.java new file mode 100644 index 000000000..684e97d13 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/storage/file/ReflogWriter.java @@ -0,0 +1,289 @@ +/* + * Copyright (C) 2007, Dave Watson + * Copyright (C) 2009-2010, Google Inc. + * Copyright (C) 2007, Robin Rosenberg + * Copyright (C) 2006, Shawn O. Pearce + * 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.storage.file; + +import static org.eclipse.jgit.lib.Constants.HEAD; +import static org.eclipse.jgit.lib.Constants.LOGS; +import static org.eclipse.jgit.lib.Constants.R_HEADS; +import static org.eclipse.jgit.lib.Constants.R_REFS; +import static org.eclipse.jgit.lib.Constants.R_REMOTES; +import static org.eclipse.jgit.lib.Constants.R_STASH; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.text.MessageFormat; + +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.CoreConfig; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FileUtils; + +/** + * Utility for writing reflog entries + */ +public class ReflogWriter { + + /** + * Get the ref name to be used for when locking a ref's log for rewriting + * + * @param name + * name of the ref, relative to the Git repository top level + * directory (so typically starts with refs/). + * @return the name of the ref's lock ref + */ + public static String refLockFor(final String name) { + return name + LockFile.SUFFIX; + } + + private final Repository parent; + + private final File logsDir; + + private final File logsRefsDir; + + private final boolean forceWrite; + + /** + * Create write for repository + * + * @param repository + */ + public ReflogWriter(final Repository repository) { + this(repository, false); + } + + /** + * Create write for repository + * + * @param repository + * @param forceWrite + * true to write to disk all entries logged, false to respect the + * repository's config and current log file status + */ + public ReflogWriter(final Repository repository, final boolean forceWrite) { + final FS fs = repository.getFS(); + parent = repository; + File gitDir = repository.getDirectory(); + logsDir = fs.resolve(gitDir, LOGS); + logsRefsDir = fs.resolve(gitDir, LOGS + '/' + R_REFS); + this.forceWrite = forceWrite; + } + + /** + * Get repository that reflog is being written for + * + * @return file repository + */ + public Repository getRepository() { + return parent; + } + + /** + * Create the log directories + * + * @throws IOException + * @return this writer + */ + public ReflogWriter create() throws IOException { + FileUtils.mkdir(logsDir); + FileUtils.mkdir(logsRefsDir); + FileUtils.mkdir(new File(logsRefsDir, + R_HEADS.substring(R_REFS.length()))); + return this; + } + + /** + * Locate the log file on disk for a single reference name. + * + * @param name + * name of the ref, relative to the Git repository top level + * directory (so typically starts with refs/). + * @return the log file location. + */ + public File logFor(String name) { + if (name.startsWith(R_REFS)) { + name = name.substring(R_REFS.length()); + return new File(logsRefsDir, name); + } + return new File(logsDir, name); + } + + /** + * Write the given {@link ReflogEntry} entry to the ref's log + * + * @param refName + * + * @param entry + * @return this writer + * @throws IOException + */ + public ReflogWriter log(final String refName, final ReflogEntry entry) + throws IOException { + return log(refName, entry.getOldId(), entry.getNewId(), entry.getWho(), + entry.getComment()); + } + + /** + * Write the given entry information to the ref's log + * + * @param refName + * @param oldId + * @param newId + * @param ident + * @param message + * @return this writer + * @throws IOException + */ + public ReflogWriter log(final String refName, final ObjectId oldId, + final ObjectId newId, final PersonIdent ident, final String message) + throws IOException { + byte[] encoded = encode(oldId, newId, ident, message); + return log(refName, encoded); + } + + /** + * Write the given ref update to the ref's log + * + * @param update + * @param msg + * @param deref + * @return this writer + * @throws IOException + */ + public ReflogWriter log(final RefUpdate update, final String msg, + final boolean deref) throws IOException { + final ObjectId oldId = update.getOldObjectId(); + final ObjectId newId = update.getNewObjectId(); + final Ref ref = update.getRef(); + + PersonIdent ident = update.getRefLogIdent(); + if (ident == null) + ident = new PersonIdent(parent); + else + ident = new PersonIdent(ident); + + final byte[] rec = encode(oldId, newId, ident, msg); + if (deref && ref.isSymbolic()) { + log(ref.getName(), rec); + log(ref.getLeaf().getName(), rec); + } else + log(ref.getName(), rec); + + return this; + } + + private byte[] encode(ObjectId oldId, ObjectId newId, PersonIdent ident, + String message) { + final StringBuilder r = new StringBuilder(); + r.append(ObjectId.toString(oldId)); + r.append(' '); + r.append(ObjectId.toString(newId)); + r.append(' '); + r.append(ident.toExternalString()); + r.append('\t'); + r.append(message); + r.append('\n'); + return Constants.encode(r.toString()); + } + + private ReflogWriter log(final String refName, final byte[] rec) + throws IOException { + final File log = logFor(refName); + final boolean write = forceWrite + || (isLogAllRefUpdates() && shouldAutoCreateLog(refName)) + || log.isFile(); + if (!write) + return this; + + WriteConfig wc = getRepository().getConfig().get(WriteConfig.KEY); + FileOutputStream out; + try { + out = new FileOutputStream(log, true); + } catch (FileNotFoundException err) { + final File dir = log.getParentFile(); + if (dir.exists()) + throw err; + if (!dir.mkdirs() && !dir.isDirectory()) + throw new IOException(MessageFormat.format( + JGitText.get().cannotCreateDirectory, dir)); + out = new FileOutputStream(log, true); + } + try { + if (wc.getFSyncRefFiles()) { + FileChannel fc = out.getChannel(); + ByteBuffer buf = ByteBuffer.wrap(rec); + while (0 < buf.remaining()) + fc.write(buf); + fc.force(true); + } else + out.write(rec); + } finally { + out.close(); + } + return this; + } + + private boolean isLogAllRefUpdates() { + return parent.getConfig().get(CoreConfig.KEY).isLogAllRefUpdates(); + } + + private boolean shouldAutoCreateLog(final String refName) { + return refName.equals(HEAD) // + || refName.startsWith(R_HEADS) // + || refName.startsWith(R_REMOTES) // + || refName.equals(R_STASH); + } +} \ No newline at end of file