From e8851668ade53fd2f8bb3e29ebac99314530977c Mon Sep 17 00:00:00 2001 From: Lukasz Date: Sun, 9 Jun 2019 20:02:31 +0200 Subject: [PATCH] ConDec-501: Extract decision knowledge elements from commit messages of a feature branch (#141) * GitDecXtract passes feature branch commit message text to GitCommitMessageDecXtract, which will return a list of decision knowledge elements from message texts. GitDecXtract then fill project and key attributes for the dec. know. elements * returns empty results for bad git branch * added specialized GitCommitMessageExtractor class to extract rationale from commit message texts * consult test code for expectations on extraction results * improved GitClientImpl's getBranch() code * further improved gitClient and its tests setup * moved helper methods up in the class hierarchy --- .../management/jira/extraction/GitClient.java | 35 +++- .../jira/extraction/impl/GitClientImpl.java | 132 ++++++++++-- .../GitCommitMessageExtractor.java | 175 ++++++++++++++++ .../versioncontrol/GitDecXtract.java | 61 ++++++ .../GitRepositoryFSManager.java | 2 +- .../extraction/gitclient/TestGetCommits.java | 7 +- .../TestGetFeatureBranchCommits.java | 54 +++++ .../extraction/gitclient/TestSetUpGit.java | 100 ++++++++- .../TestGitCommitMessageExtractor.java | 190 ++++++++++++++++++ .../versioncontrol/TestGitDecXtract.java | 41 ++++ .../TestGitRepositoryFSManager.java | 79 ++++---- 11 files changed, 813 insertions(+), 63 deletions(-) create mode 100644 src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitCommitMessageExtractor.java create mode 100644 src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitDecXtract.java create mode 100644 src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetFeatureBranchCommits.java create mode 100644 src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitCommitMessageExtractor.java create mode 100644 src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitDecXtract.java diff --git a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/GitClient.java b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/GitClient.java index f0cdda1fae..eb1e66593f 100644 --- a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/GitClient.java +++ b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/GitClient.java @@ -1,14 +1,13 @@ package de.uhd.ifi.se.decision.management.jira.extraction; import java.io.File; -import java.util.List; -import java.util.Locale; -import java.util.Map; +import java.util.*; import com.atlassian.jira.issue.Issue; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.EditList; +import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; @@ -90,6 +89,36 @@ public interface GitClient { */ Map getDiff(RevCommit firstCommit, RevCommit lastCommit); + /** + * Get a list of remote branches in repository. + * + * @return Refs list + */ + List getRemoteBranches(); + + /** + * Get a list of all commits of a "feature" branch, + * which do not exist in the "default" branch. + * Commits are sorted by age, beginning with the oldest. + * + * @param featureBranchName + * name of the feature branch + * @return ordered list of commits unique to this branch. + */ + List getFeatureBranchCommits(String featureBranchName); + + /** + * Get a list of all commits of a "feature" branch, + * which do not exist in the "default" branch. + * Commits are sorted by age, beginning with the oldest. + * + * @param featureBranch + * ref of the feature branch + * @return ordered list of commits unique to this branch. + */ + List getFeatureBranchCommits(Ref featureBranch); + + /** * Get the jgit repository object. * diff --git a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/impl/GitClientImpl.java b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/impl/GitClientImpl.java index ce41f6ac88..585f5c7ed1 100644 --- a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/impl/GitClientImpl.java +++ b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/impl/GitClientImpl.java @@ -5,10 +5,11 @@ import java.util.*; import com.atlassian.jira.issue.Issue; +import com.google.common.collect.Lists; import de.uhd.ifi.se.decision.management.jira.extraction.versioncontrol.GitRepositoryFSManager; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.ListBranchCommand; -import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.*; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.EditList; @@ -217,7 +218,6 @@ public Map getDiff(List commits) { return getDiff(firstCommit, lastCommit); } - @Override public Map getDiff(Issue jiraIssue) { if (jiraIssue == null) { @@ -259,6 +259,66 @@ public Map getDiff(RevCommit revCommit) { return getDiff(revCommit, revCommit); } + /** + * @param featureBranch ref of the feature branch + */ + @Override + public List getFeatureBranchCommits(Ref featureBranch) { + List branchUniqueCommits = new ArrayList(); + List branchCommits = getCommits(featureBranch); + RevCommit lastCommonAncestor = null; + for (RevCommit commit : branchCommits) { + if (defaultBranchCommits.contains(commit)) { + LOGGER.info("Found last common commit " + commit.toString()); + lastCommonAncestor = commit; + break; + } + branchUniqueCommits.add(commit); + } + + if (lastCommonAncestor == null) { + branchUniqueCommits = null; + } else { + branchUniqueCommits = Lists.reverse(branchUniqueCommits); + } + + return branchUniqueCommits; + } + + @Override + public List getFeatureBranchCommits(String featureBranchName) { + Ref featureBranch = getBranch(featureBranchName); + if (null == featureBranch) { + /* + [issue] What is the return value of methods that would normally return a collection (e.g. list) with an invalid input parameter? [/issue] + [alternative] Methods with an invalid input parameter return an empty list! [/alternative] + [pro] Prevents a null pointer exception. [/pro] + [con] Is misleading since it is not clear whether the list is empty but has a valid input parameter or because of an invalid parameter. [/con] + [alternative] Methods with an invalid input parameter return null! [/alternative] + */ + return (List) null; + } + return getFeatureBranchCommits(featureBranch); + } + + private Ref getBranch(String featureBranchName) { + if (featureBranchName == null || featureBranchName.length() == 0) { + LOGGER.info("Null or empty branch name was passed."); + return null; + } + List remoteBranches = getRemoteBranches(); + if (remoteBranches != null) { + for (Ref branch : remoteBranches) { + String branchName = branch.getName(); + if (branchName.endsWith(featureBranchName)) { + return branch; + } + } + } + LOGGER.info("Could not find branch " + featureBranchName); + return null; + } + private DiffFormatter getDiffFormater() { DiffFormatter diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE); Repository repository = this.getRepository(); @@ -360,7 +420,7 @@ public List getCommits(Issue jiraIssue) { @Override public List getCommits() { List commits = new ArrayList(); - for (Ref branch : getOnlyRemoteRefs()) { + for (Ref branch : getRemoteBranches()) { /* @issue: All branches will be created in separate file system * folders for this method's loop. How can this be prevented? * @@ -409,7 +469,8 @@ private List getAllRefs() { return getRefs(ListBranchCommand.ListMode.ALL); } - private List getOnlyRemoteRefs() { + @Override + public List getRemoteBranches() { return getRefs(ListBranchCommand.ListMode.REMOTE); } @@ -448,17 +509,23 @@ private List getCommits(Ref branch, boolean isDefaultBranch) { canReleaseRepoDirectory = !fsManager.isBranchDirectoryInUse(branchShortName); directory = new File(fsManager.prepareBranchDirectory(branchShortName)); } - try { - git.close(); - git = git.open(directory); - git.checkout().setName(branchShortName).call(); - Iterable iterable = git.log().call(); - for (RevCommit commit : iterable) { - commits.add(commit); + + if (switchGitDirectory(directory) + && createLocalBranchIfNotExists(branchShortName) + && checkoutBranch(branchShortName) + && pull()) { + Iterable iterable = null; + try { + iterable = git.log().call(); + } catch (GitAPIException e) { + LOGGER.error("Git could not get commits for the branch: " + + branch.getName() + " Message: " + e.getMessage()); + } + if (iterable != null) { + for (RevCommit commit : iterable) { + commits.add(commit); + } } - } catch (IOException | GitAPIException e) { - LOGGER.error("Git could not get commits for the branch: " - + branch.getName() + " Message: " + e.getMessage()); } if (canReleaseRepoDirectory) { fsManager.releaseBranchDirectoryNameToTemp(branchShortName); @@ -467,6 +534,43 @@ private List getCommits(Ref branch, boolean isDefaultBranch) { return commits; } + private boolean checkoutBranch(String branchShortName) { + try { + git.checkout().setName(branchShortName).call(); + } catch (GitAPIException e) { + LOGGER.error("Could not checkout branch. " + e.getMessage()); + return false; + } + return true; + } + + private boolean createLocalBranchIfNotExists(String branchShortName) { + try { + git.branchCreate().setName(branchShortName).call(); + } catch (RefAlreadyExistsException e) { + return true; + } catch (InvalidRefNameException | RefNotFoundException e) { + LOGGER.error("Could not create local branch. " + e.getMessage()); + return false; + } catch (GitAPIException e) { + LOGGER.error("Could not create local branch. " + e.getMessage()); + return false; + } + return true; + } + + private boolean switchGitDirectory(File gitDirectory) { + git.close(); + try { + git = git.open(gitDirectory); + } catch (IOException e) { + LOGGER.error("Could not switch into git directory " + gitDirectory.getAbsolutePath() + + "\r\n" + e.getMessage()); + return false; + } + return true; + } + private void switchGitClientBackToDefaultDirectory() { File directory = new File(fsManager.getDefaultBranchPath()); try { diff --git a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitCommitMessageExtractor.java b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitCommitMessageExtractor.java new file mode 100644 index 0000000000..c230ca22be --- /dev/null +++ b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitCommitMessageExtractor.java @@ -0,0 +1,175 @@ +package de.uhd.ifi.se.decision.management.jira.extraction.versioncontrol; + +import de.uhd.ifi.se.decision.management.jira.model.DecisionKnowledgeElement; +import de.uhd.ifi.se.decision.management.jira.model.DocumentationLocation; +import de.uhd.ifi.se.decision.management.jira.model.KnowledgeType; +import de.uhd.ifi.se.decision.management.jira.model.impl.DecisionKnowledgeElementImpl; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * purpose: extract decision knowledge elements from single git + * commit fullMessage text. + * + * + * Decision knowledge can be documented in commit messages + * using following syntax: + * """ + * [decKnowledgeTag]knowledge summary text + + * knowledge description text after empty[/decKnowledgeTag] + * """ + * + * where [decKnowledgeTag] belongs to set of know Knowledge Types, + * for example issue, alternative, decision etc. + */ +public class GitCommitMessageExtractor { + + private final static List decKnowTags = KnowledgeType.toList(); + /** + * DecisionKnowledgeElement's key part to be replaced, probably by object + * higher in hierarchy than this object, with commit ish. + */ + public static final String COMMIT_PLACEHOLDER = "commitish"; + private final Pattern START_TAGS_SEARCH_PATTERN; + private final Pattern END_TAGS_SEARCH_PATTERN; + private List extractedElements; + private String parseError; + private List parseWarnings; + private String fullMessage; + + GitCommitMessageExtractor(String message) { + extractedElements = new ArrayList<>(); + parseError = null; + parseWarnings = new ArrayList<>(); + fullMessage = message; + + String startTagSearch = String.join("|", decKnowTags.stream() + .map(tag -> "\\[" + tag + "\\]") + .collect(Collectors.toList())); + + String endTagSearch = String.join("|", decKnowTags.stream() + .map(tag -> "\\[\\/" + tag + "\\]") + .collect(Collectors.toList())); + + START_TAGS_SEARCH_PATTERN = Pattern.compile(startTagSearch, Pattern.CASE_INSENSITIVE); + END_TAGS_SEARCH_PATTERN = Pattern.compile(endTagSearch, Pattern.CASE_INSENSITIVE); + + extract(); + } + + /** + * extracts decision knowledge elements one by one + * in their order of appearance. + */ + private void extract() { + if (fullMessage == null || fullMessage.trim().equals("")) { + return; + } + if (hasNoDecisionKnowledgeStartTags()) { + return; + } + extractSequences(); + } + + private void extractSequences() { + Matcher startTagMatcher = START_TAGS_SEARCH_PATTERN.matcher(fullMessage); + /* parsers position in the message, can only move forward */ + int cursorPosition = 0; + + while (startTagMatcher.find() + && cursorPosition <= startTagMatcher.start()) { + cursorPosition = extractElementAndMoveCursor(startTagMatcher); + } + checkOrphanCloseTags(cursorPosition); + } + + private int extractElementAndMoveCursor(Matcher startTagMatcher) { + String rationaleTypeStartTag = startTagMatcher.group(); + String rationaleType = getRatTypeFromStartTag(rationaleTypeStartTag); + String messageRest = fullMessage.substring(startTagMatcher.end()); + + int cursorPosition = startTagMatcher.end(); + int textEnd = getEndingTagPosition(messageRest, rationaleTypeStartTag); + if (textEnd > 0) { + String rationaleText = messageRest.substring(0, textEnd); + int textStart = cursorPosition + rationaleTypeStartTag.length(); + + cursorPosition += textEnd + getEndingTagForStartTag(rationaleTypeStartTag).length(); + + DecisionKnowledgeElement element = createElement(textStart, rationaleType, rationaleText, textEnd); + extractedElements.add(element); + } else { + parseError = rationaleType + " has no end tag"; + cursorPosition = fullMessage.length() - 1; //ends further parsing + } + return cursorPosition; + } + + private String getRatTypeFromStartTag(String rationaleTypeStartTag) { + return rationaleTypeStartTag.substring(1, rationaleTypeStartTag.length() - 2); + } + + private DecisionKnowledgeElement createElement(int start, String rationaleType, String rationaleText, int end) { + return new DecisionKnowledgeElementImpl(0 + , getSummary(rationaleText) + , getDescription(rationaleText) + , rationaleType.toUpperCase() + , "" // unknown, not needed at the moment + , COMMIT_PLACEHOLDER + String.valueOf(start) + ":" + String.valueOf(end) + , DocumentationLocation.COMMIT); + } + + private String getDescription(String rationaleText) { + return rationaleText.substring(getSummaryEndPosition(rationaleText)); + } + + private String getSummary(String rationaleText) { + return rationaleText.substring(0,getSummaryEndPosition(rationaleText)); + } + + // TODO: implement logic for split between summary and description + private int getSummaryEndPosition(String rationaleText) { + return rationaleText.length(); + } + + /* checks the rest of the message for orphan closing tags */ + private void checkOrphanCloseTags(int cursor) { + Matcher matcher = END_TAGS_SEARCH_PATTERN.matcher(fullMessage); + while (matcher.find()) { + if (cursor <= matcher.start()) { + parseWarnings.add(matcher.group() + " has no start tag"); + } + } + } + + private int getEndingTagPosition(String txt, String startTag) { + String endTagElement = getEndingTagForStartTag(startTag); + return txt.toLowerCase().indexOf(endTagElement.toLowerCase()); + } + + private String getEndingTagForStartTag(String startTag) { + return "[/" + startTag.substring(1); + } + + private boolean hasNoDecisionKnowledgeStartTags() { + Matcher matcher = START_TAGS_SEARCH_PATTERN.matcher(fullMessage); + return !matcher.find(); + } + + public String getParseError() { + return parseError; + } + + public List getParseWarnings() { + return parseWarnings; + } + + public List getElements() { + return extractedElements; + } +} diff --git a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitDecXtract.java b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitDecXtract.java new file mode 100644 index 0000000000..234c5ab731 --- /dev/null +++ b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitDecXtract.java @@ -0,0 +1,61 @@ +package de.uhd.ifi.se.decision.management.jira.extraction.versioncontrol; + +import de.uhd.ifi.se.decision.management.jira.extraction.GitClient; +import de.uhd.ifi.se.decision.management.jira.extraction.impl.GitClientImpl; +import de.uhd.ifi.se.decision.management.jira.model.DecisionKnowledgeElement; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * purpose: extract decision knowledge elements stored in git repository + * out-of-scope linking decision knowledge elements among each other + */ +public class GitDecXtract { + + private static final String COMMIT_POSITION_SEPARATOR = "_"; + private final GitClient gitClient; + private final String projecKey; + + public GitDecXtract(String projecKey) { + this.projecKey = projecKey; + gitClient = new GitClientImpl(projecKey); + } + + public GitDecXtract(String projecKey, String uri) { + this.projecKey = projecKey; + gitClient = new GitClientImpl(uri, projecKey); + } + + // TODO: below method signature will further improve + public List getElements(String featureBranchShortName) { + List gatheredElements = new ArrayList<>(); + List featureCommits = gitClient.getFeatureBranchCommits(featureBranchShortName); + if (featureCommits == null || featureCommits.size() == 0) { + return gatheredElements; + } + for (RevCommit commit : featureCommits) { + gatheredElements.addAll(getElementsFromMessage(commit)); + } + return gatheredElements; + } + + private List getElementsFromMessage(RevCommit commit) { + GitCommitMessageExtractor extractorFromMessage = new GitCommitMessageExtractor(commit.getFullMessage()); + List elementsFromMessage = extractorFromMessage.getElements() + .stream().map(element -> { // need to update project and key attributes + element.setProject(projecKey); + element.setKey(updateKeyFroMessageExtractedElement(element.getKey(), commit.getId())); + return element; + }).collect(Collectors.toList()); + return elementsFromMessage; + } + + private String updateKeyFroMessageExtractedElement(String keyWithoutCommitish, ObjectId id) { + return keyWithoutCommitish.replace(GitCommitMessageExtractor.COMMIT_PLACEHOLDER, + String.valueOf(id) + COMMIT_POSITION_SEPARATOR); + } +} diff --git a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitRepositoryFSManager.java b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitRepositoryFSManager.java index 7919ff22bb..7faf203459 100644 --- a/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitRepositoryFSManager.java +++ b/src/main/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/GitRepositoryFSManager.java @@ -43,7 +43,7 @@ public String getDefaultBranchPath() { * It can significantly contribute to improving the speed * of check-outs of other branches as folder renaming * is not costly compared to copying. - * + * * This mehod stays public as the developer might intend * to release the folder and not wait for maintenance * strategy to trigger it. diff --git a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetCommits.java b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetCommits.java index d8b8a126bc..e85f88f77e 100644 --- a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetCommits.java +++ b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetCommits.java @@ -11,7 +11,10 @@ public class TestGetCommits extends TestSetUpGit { @Test public void testRepositoryExisting() { - List commits = gitClient.getCommits(); - assertEquals(3, commits.size()); + List allCommits = gitClient.getCommits(); + int expectedOnDefaultBranch = 3; + int expectedOnFeatureBranch = 6; /* all = unique to the branch + parent branch's commits*/ + int expectedAllCommitsNumber = expectedOnDefaultBranch + expectedOnFeatureBranch; + assertEquals(expectedAllCommitsNumber, allCommits.size()); } } diff --git a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetFeatureBranchCommits.java b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetFeatureBranchCommits.java new file mode 100644 index 0000000000..d77d89b51a --- /dev/null +++ b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestGetFeatureBranchCommits.java @@ -0,0 +1,54 @@ +package de.uhd.ifi.se.decision.management.jira.extraction.gitclient; + +import de.uhd.ifi.se.decision.management.jira.extraction.impl.GitClientImpl; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; + +public class TestGetFeatureBranchCommits extends TestSetUpGit { + + private final String repoBaseDirectory; + private final String uri; + private GitClientImpl testGitClient; + + private String featureBranch = "featureBranch"; + private String expectedFirstCommitMessage = "First message"; + + public TestGetFeatureBranchCommits() { + repoBaseDirectory = super.getRepoBaseDirectory(); + uri = super.getRepoUri(); + } + + @Test + public void testGetFeatureBranchCommitsByString() { + // fetches the 'default' branch commits. Do not use TestSetUpGit' gitClient + testGitClient = new GitClientImpl(uri, repoBaseDirectory, "TEST"); + + List commits = testGitClient.getFeatureBranchCommits(featureBranch); + assertEquals(3, commits.size()); + assertEquals(expectedFirstCommitMessage, commits.get(0).getFullMessage()); + } + + @Test + public void testGetFeatureBranchCommitsByRef() { + // fetches the 'default' branch commits. Do not use TestSetUpGit' gitClient + testGitClient = new GitClientImpl(uri, repoBaseDirectory, "TEST"); + + // get the Ref + List remoteBranches = testGitClient.getRemoteBranches(); + List branchCandidates = remoteBranches.stream() + .filter(ref -> ref.getName().endsWith(featureBranch)) + .collect(Collectors.toList()); + + assertEquals(1, branchCandidates.size()); + + List commits = testGitClient.getFeatureBranchCommits(branchCandidates.get(0)); + assertEquals(3, commits.size()); + assertEquals(expectedFirstCommitMessage, commits.get(0).getFullMessage()); + } +} diff --git a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestSetUpGit.java b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestSetUpGit.java index 7f6e99d2c5..0d315e483c 100644 --- a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestSetUpGit.java +++ b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/gitclient/TestSetUpGit.java @@ -5,11 +5,14 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; +import java.util.List; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.RepositoryCache; +import org.eclipse.jgit.transport.RemoteConfig; +import org.eclipse.jgit.transport.URIish; import org.eclipse.jgit.util.FS; import org.junit.AfterClass; import org.junit.Before; @@ -34,10 +37,15 @@ public static void setUpBeforeClass() throws IOException { File directory = getExampleDirectory(); String uri = getExampleUri(); gitClient = new GitClientImpl(uri, directory.getAbsolutePath(), "TEST"); + // above line will log errors for pulling from still empty remote repositry. makeExampleCommit("readMe.txt", "TODO Write ReadMe", "Init Commit"); makeExampleCommit("readMe.txt", "Self-explanatory, ReadMe not necessary.", "TEST-12: Explain how the great software works"); makeExampleCommit("GodClass.java", "public class GodClass {}", "TEST-12: Develop great software"); + setupBranchWithDecKnowledge(); + + gitClient.close(); + gitClient = new GitClientImpl(uri, directory.getAbsolutePath(), "TEST"); } @Before @@ -75,7 +83,7 @@ private static String getExampleUri() { return uri; } - private static void makeExampleCommit(String filename, String content, String commitMessage) { + protected static void makeExampleCommit(String filename, String content, String commitMessage) { Git git = gitClient.getGit(); try { File inputFile = new File(gitClient.getDirectory().getParent(), filename); @@ -90,8 +98,98 @@ private static void makeExampleCommit(String filename, String content, String co } } + private static void setupBranchWithDecKnowledge() { + String featureBranch = "featureBranch"; + String firstCommitMessage = "First message"; + String currentBranch = null; + Git git = gitClient.getGit(); + try { + currentBranch = git.getRepository().getBranch(); + git.branchCreate().setName(featureBranch).call(); + git.checkout().setName(featureBranch).call(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + makeExampleCommit("readMe.featureBranch.txt" + , "First content" + , firstCommitMessage); + + makeExampleCommit("readMe.featureBranch.txt" + , "Second content" + , "Second message"); + + makeExampleCommit("GodClass.java", "public class GodClass {}" + , "TEST-12: Develop great software" + + "//[issue]Huston we have a small problem..[/issue]" + + "\r\n"+ + "//[alternative]ignore it![/alternative]" + + "\r\n"+ + "//[pro]ignorance is bliss[/pro]" + + "\r\n"+ + "//[decision]solve it ASAP![/decision]" + + "\r\n"+ + "//[pro]life is valuable, prevent even smallest risks[/pro]" + ); + returnToPreviousBranch(currentBranch, git); + } + + private static void returnToPreviousBranch(String branch, Git git) { + if (branch==null) { + return; + } + else { + try { + git.checkout().setName(branch).call(); + git.pull(); + } + catch (Exception ex) { + ex.printStackTrace(); + } + } + } + @AfterClass public static void tidyUp() { gitClient.deleteRepository(); } + + // helpers + + protected String getRepoUri() { + List remoteList = null; + try { + remoteList = gitClient.getGit().remoteList().call(); + + } + catch (Exception ex) { + ex.printStackTrace(); + } + if (remoteList==null) { + return ""; + } + else { + RemoteConfig remoteHead = remoteList.get(0); + URIish uriHead = remoteHead.getURIs().get(0); + + return uriHead.toString(); + } + + } + + protected String getRepoBaseDirectory() { + Repository repo = gitClient.getGit().getRepository(); + File dir = repo.getDirectory(); + String projectUriSomeBranchPath = dir.getAbsolutePath(); + String regExSplit = File.separator; + if (("\\").equals(regExSplit)) { + regExSplit="\\\\"; + } + String[] projectUriSomeBranchPathComponents = projectUriSomeBranchPath.split(regExSplit); + String[] projectUriPathComponents = new String[projectUriSomeBranchPathComponents.length-4]; + for (int i = 0; i tags = KnowledgeType.toList(); + private GitCommitMessageExtractor gitCommitMessageX; + + @Test + public void emptyMessage() { + String msg = ""; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + + gitCommitMessageX = new GitCommitMessageExtractor(null); + + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + + @Test + public void withoutAnyRationaleTags() { + String msg = "I am just a simple message without any rationale tags"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + + @Test + public void simpleRationaleTagTest() { + String msg = "[Issue]I am just a simple message without any rationale tags[/Issue]"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + + @Test + public void noEndTags() { + for (String tag1 : tags) { + String msg = "[" + tag1 + "]Missing ending tag"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNotNull(gitCommitMessageX.getParseError()); + + for (String tag2 : tags) { + msg = "[" + tag1 + "]Correct[/" + tag1 + "][" + tag2 + "]No end."; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNotNull(gitCommitMessageX.getParseError()); + } + + for (String tag2 : tags) { + if (tag1.equals(tag2)) { + continue; + } + msg = "[" + tag1 + "]Incorrect. [" + tag2 + "]Previous tag did not end," + + " will be ignored as rationale element[/" + tag2 + "]No end."; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNotNull(gitCommitMessageX.getParseError()); + } + } + } + + @Test + public void noStartTags() { + for (String tag1 : tags) { + String msg = "Missing start tag[/" + tag1 + "]"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + + + for (String tag2 : tags) { + msg = "some text without start tag[/" + tag1 + "]" + + "[" + tag2 + "]rationale element[/" + tag2 + "]"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + } + } + + @Test + public void nestedTags() { + for (String tag1 : tags) { + for (String tag2 : tags) { + if (tag1.equals(tag2)) { + continue; + } + + String msg = "[" + tag1 + "]DecKnowElement[" + tag2 + "]" + + "[/" + tag1 + "]Not a DecKnowElement[/" + tag2 + "]"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(1, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + + msg = "[" + tag1 + "]DecKnowElement[" + tag2 + "]still same element" + + "[/" + tag2 + "]still same element[/" + tag1 + "]"; + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + } + } + + @Test + public void misspelledTags() { + for (String tag : tags) { + String msg = "[" + tag + "Z]DecKnowElement[/" + tag + "]"; + + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + + msg = "[" + tag + "]DecKnowElement[/" + tag + "Z]"; + + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNotNull(gitCommitMessageX.getParseError()); + + msg = "[A" + tag + "]DecKnowElement[/" + tag + "]"; + + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + + msg = "[" + tag + "]DecKnowElement[/A" + tag + "]"; + + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(0, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNotNull(gitCommitMessageX.getParseError()); + } + } + + @Test + public void tagCharCases() { + for (String tag : tags) { + //change case of each letter in the tag + for (int pos = 0; pos < tag.length(); pos++) { + String tagModified = flipLetter(tag, pos); + String msg = "[" + tagModified + "]DecKnowElement[/" + tagModified + "]"; + + gitCommitMessageX = new GitCommitMessageExtractor(msg); + Assert.assertEquals(1, gitCommitMessageX.getElements().size()); + Assert.assertEquals(0, gitCommitMessageX.getParseWarnings().size()); + Assert.assertNull(gitCommitMessageX.getParseError()); + } + } + } + + // helpers + private String flipLetter(String tag, int pos) { + String letter = tag.substring(pos, pos + 1); + + if (letter == letter.toLowerCase()) { + letter = letter.toUpperCase(); + } else { + letter = letter.toUpperCase(); + } + + String tagModified = tag.substring(0, pos) + + letter + + tag.substring(pos + 1); + return tagModified; + } +} \ No newline at end of file diff --git a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitDecXtract.java b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitDecXtract.java new file mode 100644 index 0000000000..d7578266c1 --- /dev/null +++ b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitDecXtract.java @@ -0,0 +1,41 @@ +package de.uhd.ifi.se.decision.management.jira.extraction.versioncontrol; + +import de.uhd.ifi.se.decision.management.jira.extraction.gitclient.TestSetUpGit; +import de.uhd.ifi.se.decision.management.jira.model.DecisionKnowledgeElement; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class TestGitDecXtract extends TestSetUpGit { + private final String uri; + private GitDecXtract gitDecX; + + public TestGitDecXtract() { + uri = super.getRepoUri(); + } + + @Test + public void nullOrEmptyFeatureBranchCommits() { + // git repository is setup already + gitDecX = new GitDecXtract("TEST", uri); + int numberExpectedElements = 0; + List gotElements = gitDecX.getElements(null); + Assert.assertEquals(numberExpectedElements, gotElements.size()); + + gotElements = gitDecX.getElements(""); + Assert.assertEquals(numberExpectedElements, gotElements.size()); + + gotElements = gitDecX.getElements("doesNotExistBranch"); + Assert.assertEquals(numberExpectedElements, gotElements.size()); + } + + @Test + public void fromFeatureBranchCommits() { + // git repository is setup already + gitDecX = new GitDecXtract("TEST", uri); + int numberExpectedElements = 5; + List gotElements = gitDecX.getElements("featureBranch"); + Assert.assertEquals(numberExpectedElements, gotElements.size()); + } +} diff --git a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitRepositoryFSManager.java b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitRepositoryFSManager.java index 5e266ccf4c..ebc42bfdf4 100644 --- a/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitRepositoryFSManager.java +++ b/src/test/java/de/uhd/ifi/se/decision/management/jira/extraction/versioncontrol/TestGitRepositoryFSManager.java @@ -37,19 +37,19 @@ public void setUp() { // add base dir String baseDir = directory.getAbsolutePath(); - baseProjectDir = new File(baseDir+File.separator+projectName); + baseProjectDir = new File(baseDir + File.separator + projectName); baseProjectDir.mkdirs(); // prepare paths for subdirectories - baseProjectUriDir = baseDir+File.separator+projectName - +File.separator+getHash(repoUri); + baseProjectUriDir = baseDir + File.separator + projectName + + File.separator + getHash(repoUri); baseProjectUriDefaultDir = baseProjectUriDir - +File.separator+ folderForDefaultBranchName; + + File.separator + folderForDefaultBranchName; baseProjectUriTempDir = baseProjectUriDir - +File.separator+ folderForTempBranchDirName; + + File.separator + folderForTempBranchDirName; expectedBaseProjectUriBranchDir = baseProjectUriDir - +File.separator - +getHash(branchName); + + File.separator + + getHash(branchName); // init FS Manager FSmanager = new GitRepositoryFSManager(baseDir, @@ -70,7 +70,7 @@ public void testReleaseBranchDirectoryNameToTemp() { // branch and temp folders do not exist assertFalse(new File(expectedBaseProjectUriBranchDir).isDirectory()); - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); // create branch folder from default FSmanager.prepareBranchDirectory(branchName); @@ -79,9 +79,9 @@ public void testReleaseBranchDirectoryNameToTemp() { // test releasing branch folder to temporary folders pool FSmanager.releaseBranchDirectoryNameToTemp(branchName); assertFalse(new File(expectedBaseProjectUriBranchDir).isDirectory()); - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); - String expectedFileName = baseProjectUriDir+File.separator + String expectedFileName = baseProjectUriDir + File.separator + findTemporaryDirectoryNames()[0] + File.separator + distinctFileInDefaultFolder; @@ -90,8 +90,8 @@ public void testReleaseBranchDirectoryNameToTemp() { } @Test - public void testGetDefaultBranchPath(){ - assertEquals(baseProjectUriDefaultDir,FSmanager.getDefaultBranchPath()); + public void testGetDefaultBranchPath() { + assertEquals(baseProjectUriDefaultDir, FSmanager.getDefaultBranchPath()); } @Test @@ -108,11 +108,11 @@ public void testPrepareBranchDirectoryFromBranch() { + File.separator + distinctFileInBranchFolder; - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); // test testBranchFolderPreparation(branchName, expectedBaseProjectUriBranchDir, expectedContentPath); // preparation from branch directory itself should not affect temporary folders - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); } @Test @@ -125,11 +125,11 @@ public void testPrepareBranchDirectoryFromBranchPrecedenceOverDefaultAndTemp() { + File.separator + distinctFileInBranchFolder; - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); // test testBranchFolderPreparation(branchName, expectedBaseProjectUriBranchDir, expectedContentPath); - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); } @Test @@ -140,15 +140,15 @@ public void testPrepareBranchDirectoryFromTemp() { + File.separator + distinctFileInTempFolder; - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); // test testBranchFolderPreparation(branchName, expectedBaseProjectUriBranchDir, expectedContentPath); // preparation should take the folder from temporary folders pool - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); addDistinctFileInTempDir(); - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); } @Test @@ -160,12 +160,13 @@ public void testPrepareBranchDirectoryFromTempPrecedenceOverDefault() { + File.separator + distinctFileInTempFolder; - assertEquals(1,findTemporaryDirectoryNames().length); + assertEquals(1, findTemporaryDirectoryNames().length); // test testBranchFolderPreparation(branchName, expectedBaseProjectUriBranchDir, expectedContentPath); - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); } + @Test public void testPrepareBranchDirectoryFromDefault() { // setup @@ -174,17 +175,17 @@ public void testPrepareBranchDirectoryFromDefault() { + File.separator + distinctFileInDefaultFolder; - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); // test testBranchFolderPreparation(branchName, expectedBaseProjectUriBranchDir, expectedContentPath); // preparation from default folder should not affect temporary folders - assertEquals(0,findTemporaryDirectoryNames().length); + assertEquals(0, findTemporaryDirectoryNames().length); } private void testBranchFolderPreparation(String branchName, String expectedDir, String expectedContentPath) { String actualDir = FSmanager.prepareBranchDirectory(branchName); assertNotNull(actualDir); - assertEquals(expectedDir,actualDir); + assertEquals(expectedDir, actualDir); assertTrue(new File(expectedDir).isDirectory()); assertTrue(new File(expectedContentPath).isFile()); } @@ -206,9 +207,8 @@ private static String getHash(String text) { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(text.getBytes()); byte[] digest = md.digest(); - return DatatypeConverter.printHexBinary(digest).toUpperCase().substring(0,5); - } - catch (NoSuchAlgorithmException e) { + return DatatypeConverter.printHexBinary(digest).toUpperCase().substring(0, 5); + } catch (NoSuchAlgorithmException e) { e.printStackTrace(); return ""; } @@ -226,13 +226,12 @@ private String[] findTemporaryDirectoryNames() { /* helpers for adding files */ private void addBranchMarker(String branchName) { - File dir = new File (baseProjectUriDir); + File dir = new File(baseProjectUriDir); File touchFile = new File(dir, branchName); try { dir.mkdirs(); touchFile.createNewFile(); - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); } } @@ -252,29 +251,25 @@ private void addDistinctFileInBranchDir() { private boolean addDistinctFile(String target) { File file; File dir; - if (target.endsWith("branch")){ - dir = new File (expectedBaseProjectUriBranchDir); + if (target.endsWith("branch")) { + dir = new File(expectedBaseProjectUriBranchDir); // add distinct file name to default branch directory file = new File(dir, distinctFileInBranchFolder); - } - else if (target.endsWith("default")){ - dir = new File (baseProjectUriDefaultDir); + } else if (target.endsWith("default")) { + dir = new File(baseProjectUriDefaultDir); // add distinct file name to default branch directory file = new File(dir, distinctFileInDefaultFolder); - } - else if (target.endsWith("temp")) { - dir = new File (baseProjectUriTempDir); + } else if (target.endsWith("temp")) { + dir = new File(baseProjectUriTempDir); // add distinct file name to temporary branch directory file = new File(dir, distinctFileInTempFolder); - } - else { + } else { return false; } try { dir.mkdirs(); file.createNewFile(); - } - catch (Exception e) { + } catch (Exception e) { e.printStackTrace(); return false; }