blob: bdf87b265427cefc83acf4cc15991255e42657f9 [file] [log] [blame]
/*
* Copyright 2000-2010 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.history;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePathImpl;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.diff.ItemLatestState;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.ExceptionUtil;
import com.intellij.util.Function;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitFileRevision;
import git4idea.GitRevisionNumber;
import git4idea.history.browser.SHAHash;
import git4idea.test.GitSingleRepoTest;
import org.jetbrains.annotations.NotNull;
import org.testng.annotations.Test;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static com.intellij.dvcs.DvcsUtil.getShortHash;
import static com.intellij.openapi.vcs.Executor.overwrite;
import static com.intellij.openapi.vcs.Executor.touch;
import static git4idea.test.GitExecutor.*;
import static git4idea.test.GitTestUtil.USER_EMAIL;
import static git4idea.test.GitTestUtil.USER_NAME;
/**
* Tests for low-level history methods in GitHistoryUtils.
* There are some known problems with newlines and whitespaces in commit messages, these are ignored by the tests for now.
* (see #convertWhitespacesToSpacesAndRemoveDoubles).
*/
public class GitHistoryUtilsTest extends GitSingleRepoTest {
private File bfile;
private List<GitTestRevision> myRevisions;
private List<GitTestRevision> myRevisionsAfterRename;
@Override
protected void setUp() throws Exception {
super.setUp();
try {
initTest();
}
catch (Exception e) {
super.tearDown();
throw e;
}
}
private void initTest() throws IOException {
myRevisions = new ArrayList<GitTestRevision>(7);
myRevisionsAfterRename = new ArrayList<GitTestRevision>(4);
// 1. create a file
// 2. simple edit with a simple commit message
// 3. move & rename
// 4. make 4 edits with commit messages of different complexity
// (note: after rename, because some GitHistoryUtils methods don't follow renames).
final String[] commitMessages = {
"initial commit",
"simple commit",
"moved a.txt to dir/b.txt",
"simple commit after rename",
"commit with {%n} some [%ct] special <format:%H%at> characters including --pretty=tformat:%x00%x01%x00%H%x00%ct%x00%an%x20%x3C%ae%x3E%x00%cn%x20%x3C%ce%x3E%x00%x02%x00%s%x00%b%x00%x02%x01",
"commit subject\n\ncommit body which is \n multilined.",
"first line\nsecond line\nthird line\n\nfifth line\n\nseventh line & the end.",
};
final String[] contents = {
"initial content",
"second content",
"second content", // content is the same after rename
"fourth content",
"fifth content",
"sixth content",
"seventh content",
};
// initial
int commitIndex = 0;
File afile = touch("a.txt", contents[commitIndex]);
addCommit(commitMessages[commitIndex]);
commitIndex++;
// modify
overwrite(afile, contents[commitIndex]);
addCommit(commitMessages[commitIndex]);
int RENAME_COMMIT_INDEX = commitIndex;
commitIndex++;
// mv to dir
File dir = mkdir("dir");
bfile = new File(dir.getPath(), "b.txt");
assertFalse("File " + bfile + " shouldn't have existed", bfile.exists());
mv(afile, bfile);
assertTrue("File " + bfile + " was not created by mv command", bfile.exists());
commit(commitMessages[commitIndex]);
commitIndex++;
// modifications
for (int i = 0; i < 4; i++) {
overwrite(bfile, contents[commitIndex]);
addCommit(commitMessages[commitIndex]);
commitIndex++;
}
// Retrieve hashes and timestamps
String[] revisions = log("--pretty=format:%H#%at#%P", "-M").split("\n");
assertEquals("Incorrect number of revisions", commitMessages.length, revisions.length);
// newer revisions go first in the log output
for (int i = revisions.length - 1, j = 0; i >= 0; i--, j++) {
String[] details = revisions[j].trim().split("#");
String[] parents;
if (details.length > 2) {
parents = details[2].split(" ");
} else {
parents = ArrayUtil.EMPTY_STRING_ARRAY;
}
final GitTestRevision revision = new GitTestRevision(details[0], details[1], parents, commitMessages[i],
USER_NAME, USER_EMAIL, USER_NAME, USER_EMAIL, null,
contents[i]);
myRevisions.add(revision);
if (i > RENAME_COMMIT_INDEX) {
myRevisionsAfterRename.add(revision);
}
}
assertEquals(myRevisionsAfterRename.size(), 5);
cd(myProjectPath);
}
@Override
protected boolean makeInitialCommit() {
return false;
}
// Inspired by IDEA-89347
@Test
public void testCyclicRename() throws Exception {
List<TestCommit> commits = new ArrayList<TestCommit>();
File source = mkdir("source");
File initialFile = touch("source/PostHighlightingPass.java", "Initial content");
String initMessage = "Created PostHighlightingPass.java in source";
addCommit(initMessage);
String hash = last();
commits.add(new TestCommit(hash, initMessage, initialFile.getPath()));
String filePath = initialFile.getPath();
commits.add(modify(filePath));
TestCommit commit = move(filePath, mkdir("codeInside-impl"), "Moved from source to codeInside-impl");
filePath = commit.myPath;
commits.add(commit);
commits.add(modify(filePath));
commit = move(filePath, mkdir("codeInside"), "Moved from codeInside-impl to codeInside");
filePath = commit.myPath;
commits.add(commit);
commits.add(modify(filePath));
commit = move(filePath, mkdir("lang-impl"), "Moved from codeInside to lang-impl");
filePath = commit.myPath;
commits.add(commit);
commits.add(modify(filePath));
commit = move(filePath, source, "Moved from lang-impl back to source");
filePath = commit.myPath;
commits.add(commit);
commits.add(modify(filePath));
commit = move(filePath, mkdir("java"), "Moved from source to java");
filePath = commit.myPath;
commits.add(commit);
commits.add(modify(filePath));
Collections.reverse(commits);
VirtualFile vFile = VcsUtil.getVirtualFileWithRefresh(new File(filePath));
assertNotNull(vFile);
List<VcsFileRevision> history = GitHistoryUtils.history(myProject, new FilePathImpl(vFile));
assertEquals("History size doesn't match. Actual history: \n" + toReadable(history), commits.size(), history.size());
assertEquals("History is different.", toReadable(commits), toReadable(history));
}
private static class TestCommit {
private final String myHash;
private final String myMessage;
private final String myPath;
public TestCommit(String hash, String message, String path) {
myHash = hash;
myMessage = message;
myPath = path;
}
public String getHash() {
return myHash;
}
public String getCommitMessage() {
return myMessage;
}
}
private static TestCommit move(String file, File dir, String message) throws Exception {
final String NAME = "PostHighlightingPass.java";
mv(file, dir.getPath());
file = new File(dir, NAME).getPath();
addCommit(message);
String hash = last();
return new TestCommit(hash, message, file);
}
private static TestCommit modify(String file) throws IOException {
editAppend(file, "Modified");
String message = "Modified PostHighlightingPass";
addCommit(message);
String hash = last();
return new TestCommit(hash, message, file);
}
private static void editAppend(String file, String content) throws IOException {
FileUtil.appendToFile(new File(file), content);
}
@NotNull
private String toReadable(@NotNull Collection<VcsFileRevision> history) {
int maxSubjectLength = findMaxLength(history, new Function<VcsFileRevision, String>() {
@Override
public String fun(VcsFileRevision revision) {
return revision.getCommitMessage();
}
});
StringBuilder sb = new StringBuilder();
for (VcsFileRevision revision : history) {
GitFileRevision rev = (GitFileRevision)revision;
String relPath = FileUtil.getRelativePath(new File(myProjectPath), rev.getPath().getIOFile());
sb.append(String.format("%s %-" + maxSubjectLength + "s %s%n", getShortHash(rev.getHash()), rev.getCommitMessage(), relPath));
}
return sb.toString();
}
private String toReadable(List<TestCommit> commits) {
int maxSubjectLength = findMaxLength(commits, new Function<TestCommit, String>() {
@Override
public String fun(TestCommit revision) {
return revision.getCommitMessage();
}
});
StringBuilder sb = new StringBuilder();
for (TestCommit commit : commits) {
String relPath = FileUtil.getRelativePath(new File(myProjectPath), new File(commit.myPath));
sb.append(String.format("%s %-" + maxSubjectLength + "s %s%n", getShortHash(commit.getHash()), commit.getCommitMessage(), relPath));
}
return sb.toString();
}
private static <T> int findMaxLength(@NotNull Collection<T> list, @NotNull Function<T, String> convertor) {
int max = 0;
for (T element : list) {
int length = convertor.fun(element).length();
if (length > max) {
max = length;
}
}
return max;
}
@Test
public void testGetCurrentRevision() throws Exception {
GitRevisionNumber revisionNumber = (GitRevisionNumber) GitHistoryUtils.getCurrentRevision(myProject, toFilePath(bfile), null);
assertEquals(revisionNumber.getRev(), myRevisions.get(0).myHash);
assertEquals(revisionNumber.getTimestamp(), myRevisions.get(0).myDate);
}
@Test
public void testGetCurrentRevisionInMasterBranch() throws Exception {
GitRevisionNumber revisionNumber = (GitRevisionNumber) GitHistoryUtils.getCurrentRevision(myProject, toFilePath(bfile), "master");
assertEquals(revisionNumber.getRev(), myRevisions.get(0).myHash);
assertEquals(revisionNumber.getTimestamp(), myRevisions.get(0).myDate);
}
@Test
public void testGetCurrentRevisionInOtherBranch() throws Exception {
checkout("-b feature");
overwrite(bfile, "new content");
addCommit("new content");
final String[] output = log("master --pretty=%H#%at", "-n1").trim().split("#");
GitRevisionNumber revisionNumber = (GitRevisionNumber) GitHistoryUtils.getCurrentRevision(myProject, toFilePath(bfile), "master");
assertEquals(revisionNumber.getRev(), output[0]);
assertEquals(revisionNumber.getTimestamp(), GitTestRevision.gitTimeStampToDate(output[1]));
}
@NotNull
private static FilePathImpl toFilePath(@NotNull File file) {
return new FilePathImpl(file, file.isDirectory());
}
@Test(enabled = false)
public void testGetLastRevisionForExistingFile() throws Exception {
final ItemLatestState state = GitHistoryUtils.getLastRevision(myProject, toFilePath(bfile));
assertTrue(state.isItemExists());
final GitRevisionNumber revisionNumber = (GitRevisionNumber) state.getNumber();
assertEquals(revisionNumber.getRev(), myRevisions.get(0).myHash);
assertEquals(revisionNumber.getTimestamp(), myRevisions.get(0).myDate);
}
public void testGetLastRevisionForNonExistingFile() throws Exception {
git("remote add origin git://example.com/repo.git");
git("config branch.master.remote origin");
git("config branch.master.merge refs/heads/master");
git("rm " + bfile.getPath());
commit("removed bfile");
String[] hashAndDate = log("--pretty=format:%H#%ct", "-n1").split("#");
git("update-ref refs/remotes/origin/master HEAD"); // to avoid pushing to this fake origin
touch("dir/b.txt", "content");
addCommit("recreated bfile");
refresh();
myRepo.update();
final ItemLatestState state = GitHistoryUtils.getLastRevision(myProject, toFilePath(bfile));
assertTrue(!state.isItemExists());
final GitRevisionNumber revisionNumber = (GitRevisionNumber)state.getNumber();
assertEquals(revisionNumber.getRev(), hashAndDate[0]);
assertEquals(revisionNumber.getTimestamp(), GitTestRevision.gitTimeStampToDate(hashAndDate[1]));
}
@Test
public void testHistory() throws Exception {
List<VcsFileRevision> revisions = GitHistoryUtils.history(myProject, toFilePath(bfile));
assertHistory(revisions);
}
@Test
public void testAppendableHistory() throws Exception {
final List<GitFileRevision> revisions = new ArrayList<GitFileRevision>(3);
Consumer<GitFileRevision> consumer = new Consumer<GitFileRevision>() {
@Override
public void consume(GitFileRevision gitFileRevision) {
revisions.add(gitFileRevision);
}
};
Consumer<VcsException> exceptionConsumer = new Consumer<VcsException>() {
@Override
public void consume(VcsException exception) {
fail("No exception expected " + ExceptionUtil.getThrowableText(exception));
}
};
GitHistoryUtils.history(myProject, toFilePath(bfile), null, consumer, exceptionConsumer);
assertHistory(revisions);
}
@Test
public void testOnlyHashesHistory() throws Exception {
final List<Pair<SHAHash,Date>> history = GitHistoryUtils.onlyHashesHistory(myProject, toFilePath(bfile), myProjectRoot);
assertEquals(history.size(), myRevisionsAfterRename.size());
Iterator<GitTestRevision> itAfterRename = myRevisionsAfterRename.iterator();
for (Pair<SHAHash, Date> pair : history) {
GitTestRevision revision = itAfterRename.next();
assertEquals(pair.first.toString(), revision.myHash);
assertEquals(pair.second, revision.myDate);
}
}
private void assertHistory(@NotNull List<? extends VcsFileRevision> actualRevisions) throws IOException, VcsException {
assertEquals("Incorrect number of commits in history", myRevisions.size(), actualRevisions.size());
for (int i = 0; i < actualRevisions.size(); i++) {
assertEqualRevisions((GitFileRevision) actualRevisions.get(i), myRevisions.get(i));
}
}
private static void assertEqualRevisions(GitFileRevision actual, GitTestRevision expected) throws IOException, VcsException {
String actualRev = ((GitRevisionNumber)actual.getRevisionNumber()).getRev();
assertEquals(expected.myHash, actualRev);
assertEquals(expected.myDate, ((GitRevisionNumber)actual.getRevisionNumber()).getTimestamp());
// TODO: whitespaces problem is known, remove convertWhitespaces... when it's fixed
assertEquals(convertWhitespacesToSpacesAndRemoveDoubles(expected.myCommitMessage),
convertWhitespacesToSpacesAndRemoveDoubles(actual.getCommitMessage()));
assertEquals(expected.myAuthorName, actual.getAuthor());
assertEquals(expected.myBranchName, actual.getBranchName());
assertNotNull("No content in revision " + actualRev, actual.getContent());
assertEquals(new String(expected.myContent), new String(actual.getContent()));
}
private static String convertWhitespacesToSpacesAndRemoveDoubles(String s) {
return s.replaceAll("[\\s^ ]", " ").replaceAll(" +", " ");
}
private static class GitTestRevision {
final String myHash;
final Date myDate;
final String myCommitMessage;
final String myAuthorName;
final String myAuthorEmail;
final String myCommitterName;
final String myCommitterEmail;
final String myBranchName;
final byte[] myContent;
private String[] myParents;
public GitTestRevision(String hash, String gitTimestamp, String[] parents, String commitMessage, String authorName, String authorEmail, String committerName, String committerEmail, String branch, String content) {
myHash = hash;
myDate = gitTimeStampToDate(gitTimestamp);
myParents = parents;
myCommitMessage = commitMessage;
myAuthorName = authorName;
myAuthorEmail = authorEmail;
myCommitterName = committerName;
myCommitterEmail = committerEmail;
myBranchName = branch;
myContent = content.getBytes();
}
@Override
public String toString() {
return myHash;
}
public static Date gitTimeStampToDate(String gitTimestamp) {
return new Date(Long.parseLong(gitTimestamp)*1000);
}
}
}