diff --git a/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/BlobIndexUtil.java b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/BlobIndexUtil.java new file mode 100644 index 000000000000..b7f36edb28e5 --- /dev/null +++ b/platform/vcs-impl/src/com/intellij/openapi/vcs/changes/patch/BlobIndexUtil.java @@ -0,0 +1,79 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * 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 com.intellij.openapi.vcs.changes.patch; + +import com.google.common.base.Charsets; +import com.google.common.hash.Hashing; +import com.google.common.io.Files; +import com.intellij.openapi.util.Couple; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.BinaryContentRevision; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.util.ObjectUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; + +import static com.intellij.util.ArrayUtil.EMPTY_BYTE_ARRAY; + +public class BlobIndexUtil { + + public static final String NOT_COMMITTED_HASH = StringUtil.repeat("0", 40); + + + @NotNull + public static String getSha1(@NotNull File file) throws IOException { + return getSha1(Files.toByteArray(file)); + } + + /** + * Generate sha1 for file content using git-like algorithm + */ + @NotNull + public static String getSha1(@NotNull byte[] bytes) { + String prefix = "blob " + bytes.length + '\u0000'; + return Hashing.sha1().newHasher().putBytes(prefix.getBytes(Charsets.UTF_8)).putBytes(bytes).hash().toString(); + } + + @NotNull + public static Couple getBeforeAfterSha1(@NotNull Change change) throws VcsException { + ContentRevision beforeRevision = change.getBeforeRevision(); + ContentRevision afterRevision = change.getAfterRevision(); + + //noinspection ConstantConditions + Charset detectCharset = ObjectUtils.chooseNotNull(afterRevision, beforeRevision).getFile().getCharset(); + String before = beforeRevision == null ? NOT_COMMITTED_HASH : getSha1(getContentBytes(beforeRevision, detectCharset)); + String after = afterRevision == null ? NOT_COMMITTED_HASH : getSha1(getContentBytes(afterRevision, detectCharset)); + return new Couple<>(before, after); + } + + @NotNull + private static byte[] getContentBytes(@NotNull ContentRevision revision, @NotNull Charset charset) throws VcsException { + byte[] binaryContent; + if (revision instanceof BinaryContentRevision) { + binaryContent = ((BinaryContentRevision)revision).getBinaryContent(); + } + else { + String stringContent = revision.getContent(); + binaryContent = stringContent != null ? stringContent.getBytes(charset) : EMPTY_BYTE_ARRAY; + } + return binaryContent != null ? binaryContent : EMPTY_BYTE_ARRAY; + } +} diff --git a/plugins/git4idea/src/git4idea/GitRevisionNumber.java b/plugins/git4idea/src/git4idea/GitRevisionNumber.java index c6683eeb400a..cb8bfdb5fcd5 100644 --- a/plugins/git4idea/src/git4idea/GitRevisionNumber.java +++ b/plugins/git4idea/src/git4idea/GitRevisionNumber.java @@ -18,8 +18,8 @@ package git4idea; import com.intellij.dvcs.DvcsUtil; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.patch.BlobIndexUtil; import com.intellij.openapi.vcs.history.ShortVcsRevisionNumber; import com.intellij.openapi.vcs.history.VcsRevisionNumber; import com.intellij.openapi.vfs.VirtualFile; @@ -38,7 +38,7 @@ public class GitRevisionNumber implements ShortVcsRevisionNumber { /** * the hash from 40 zeros representing not yet created commit */ - public static final String NOT_COMMITTED_HASH = StringUtil.repeat("0", 40); + public static final String NOT_COMMITTED_HASH = BlobIndexUtil.NOT_COMMITTED_HASH; public static final GitRevisionNumber HEAD = new GitRevisionNumber("HEAD"); diff --git a/plugins/git4idea/tests/git4idea/log/GitSha1Test.kt b/plugins/git4idea/tests/git4idea/log/GitSha1Test.kt new file mode 100644 index 000000000000..7dc4bc8c4625 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/log/GitSha1Test.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * 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 git4idea.log + +import com.intellij.openapi.vcs.Executor.cd +import com.intellij.openapi.vcs.Executor.touch +import com.intellij.openapi.vcs.changes.patch.BlobIndexUtil +import git4idea.test.GitExecutor.* +import git4idea.test.GitSingleRepoTest +import git4idea.test.GitTestUtil + +class GitSha1Test : GitSingleRepoTest() { + var A_FILE = "a.txt" + + @Throws(Exception::class) + override fun setUp() { + super.setUp() + GitTestUtil.createFileStructure(myProjectRoot, A_FILE) + addCommit("initial") + } + + fun `test sha for add`() { + cd(myProjectPath) + val newFile = "newFile.txt" + touch(newFile, "Hello World!") + add(newFile) + checkSha1ForSingleChange(BlobIndexUtil.NOT_COMMITTED_HASH, git("hash-object $newFile")) + } + + fun `test sha for del`() { + cd(myProjectPath) + val path = myProjectRoot.findChild(A_FILE)!!.path + val expectedBefore = git("hash-object $path") + git("rm $path") + checkSha1ForSingleChange(expectedBefore, BlobIndexUtil.NOT_COMMITTED_HASH) + } + + fun `test sha for modified`() { + cd(myProjectPath) + val virtualFile = myProjectRoot.findChild(A_FILE) + val path = virtualFile!!.path + val expectedBefore = git("hash-object $path") + setFileText(virtualFile, "echo content\n with line separator") + checkSha1ForSingleChange(expectedBefore, git("hash-object $path")) + } + + private fun checkSha1ForSingleChange(expectedBefore: String?, expectedAfter: String?) { + updateChangeListManager() + val changes = changeListManager.allChanges + assertTrue(changes.size == 1) + val beforeAfterSha1 = BlobIndexUtil.getBeforeAfterSha1(changes.first()) + assertEquals(expectedBefore, beforeAfterSha1.first) + assertEquals(expectedAfter, beforeAfterSha1.second) + } +} +