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 2cf46707af..ce41f6ac88 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 @@ -2,13 +2,10 @@ import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import com.atlassian.jira.issue.Issue; +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; @@ -16,11 +13,8 @@ import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.EditList; import org.eclipse.jgit.diff.RawTextComparator; -import org.eclipse.jgit.lib.ConfigConstants; +import org.eclipse.jgit.lib.*; import org.eclipse.jgit.lib.CoreConfig.AutoCRLF; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; -import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.transport.RemoteConfig; @@ -36,53 +30,126 @@ * @decision Only use jGit. * @pro The jGit library is open source. * @alternative Both, the jgit library and the git integration for JIRA plugin - * were used to access git repositories. + * were used to access git repositories. * @con An application link and oAuth is needed to call REST API on Java side. + * + * This implementation works well only with configuration for one remote git server. + * Multiple instances of this class are "thread-safe" in the limited way that + * the checked out branch files are stored in dedicated branch folders and can be read, + * modifing files is not safe and not supported. */ public class GitClientImpl implements GitClient { private Git git; + private boolean repoInitSuccess = false; // will be later made readable with upcoming features + private Ref defaultBranch; // TODO: should come from configuration of the project + private List defaultBranchCommits; // will be later needed for upcoming features + private GitRepositoryFSManager fsManager; private static final Logger LOGGER = LoggerFactory.getLogger(GitClientImpl.class); public GitClientImpl() { } - public GitClientImpl(String uri, File directory) { - pullOrClone(uri, directory); + public GitClientImpl(File directory) { + repoInitSuccess = initRepository(directory); + } + + public GitClientImpl(String uri, String defaultDirectory, String projectKey) { + // TODO: the last parameter should be a setting retrievable with ConfigPersistenceM + repoInitSuccess = pullOrCloneRepository(projectKey, defaultDirectory, uri, "develop"); } public GitClientImpl(String uri, String projectKey) { - File directory = new File(DEFAULT_DIR + projectKey); - pullOrClone(uri, directory); + // TODO: the last parameter should be a setting retrievable with ConfigPersistenceManager + repoInitSuccess = pullOrCloneRepository(projectKey, DEFAULT_DIR, uri, "develop"); } public GitClientImpl(String projectKey) { - File directory = new File(DEFAULT_DIR + projectKey); String uri = ConfigPersistenceManager.getGitUri(projectKey); - pullOrClone(uri, directory); + // TODO: the last parameter should be a setting retrievable with ConfigPersistenceManager + repoInitSuccess = pullOrCloneRepository(projectKey, DEFAULT_DIR, uri, "develop"); + } + + private boolean pullOrCloneRepository(String projectKey, String defaultDirectory, String uri, String defaultBranchFolderName) { + fsManager = new GitRepositoryFSManager(defaultDirectory, projectKey, uri, defaultBranchFolderName); + File directory = new File(fsManager.getDefaultBranchPath()); + return pullOrClone(uri, directory); } - private void pullOrClone(String uri, File directory) { - boolean isGitDirectory = directory.exists(); // && RepositoryCache.FileKey.isGitRepository(directory, - // FS.DETECTED); - if (isGitDirectory) { - openRepository(directory); - pull(); + private boolean pullOrClone(String uri, File directory) { + if (isGitDirectory(directory)) { + if (openRepository(directory)) { + if (!pull()) { + LOGGER.error("failed Git pull " + directory); + return false; + } + } else { + LOGGER.error("Could not open repository: " + directory.getAbsolutePath()); + return false; + } } else { - cloneRepository(uri, directory); + if (!cloneRepository(uri, directory)) { + LOGGER.error("Could not clone repository " + uri + " to " + directory.getAbsolutePath()); + return false; + } + } + + // get name of current branch, should be later replaced by project setting + defaultBranch = getCurrentBranch(); + if (defaultBranch == null) { + return false; + } + defaultBranchCommits = getCommitsFromDefaultBranch(); + return true; + } + + private boolean isGitDirectory(File directory) { + File gitDir = new File(directory, ".git/"); + return directory.exists() && (gitDir.isDirectory()); + } + + private Ref getCurrentBranch() { + String branchName = null; + Ref branch = null; + Repository repository; + + try { + repository = this.getRepository(); + } catch (Exception e) { + LOGGER.error("getRepository: " + e.getMessage()); + return null; + } + + if (repository == null) { + LOGGER.error("Git repository does not seem to exist"); + return null; + } + try { + branchName = repository.getFullBranch(); + branch = repository.findRef(branchName); + if (branch == null) { + LOGGER.error("Git repository does not seem to be on a branch"); + return null; + } + } catch (Exception e) { + LOGGER.error("Git client has thrown error while getting branch name. " + e.getMessage()); + return null; } + return branch; } - private void openRepository(File directory) { + private boolean openRepository(File directory) { try { git = Git.open(directory); } catch (IOException e) { - LOGGER.error("Git repository could not be opened. Message: " + e.getMessage()); - initRepository(directory); + LOGGER.error("Git repository could not be opened: " + directory.getAbsolutePath() + + "\n\t" + e.getMessage()); + return false; } + return true; } - private void pull() { + private boolean pull() { try { git.pull().call(); List remotes = git.remoteList().call(); @@ -90,33 +157,40 @@ private void pull() { git.fetch().setRemote(remote.getName()).setRefSpecs(remote.getFetchRefSpecs()).call(); } } catch (GitAPIException e) { - LOGGER.error("Git repository could not be pulled. Message: " + e.getMessage()); + LOGGER.error("Issue occurred while pulling from a remote." + + "\n\t" + e.getMessage()); + return false; } + return true; } - private void cloneRepository(String uri, File directory) { + private boolean cloneRepository(String uri, File directory) { if (uri == null || uri.isEmpty()) { - return; + return false; } try { git = Git.cloneRepository().setURI(uri).setDirectory(directory).setCloneAllBranches(true).call(); setConfig(); } catch (GitAPIException e) { - LOGGER.error( - "Git repository could not be cloned. Bare repository will be created. Message: " + e.getMessage()); - initRepository(directory); + LOGGER.error("Git repository could not be cloned: " + uri + " " + directory.getAbsolutePath() + + "\n\t" + e.getMessage()); + return false; } + // TODO checkoutDefault branch + return true; } - private void initRepository(File directory) { + private boolean initRepository(File directory) { try { git = Git.init().setDirectory(directory).call(); } catch (IllegalStateException | GitAPIException e) { - LOGGER.error("Git repository could not be initialized. Message: " + e.getMessage()); + LOGGER.error("Bare git repository could not be initiated: " + directory.getAbsolutePath()); + return false; } + return true; } - private void setConfig() { + private boolean setConfig() { Repository repository = this.getRepository(); StoredConfig config = repository.getConfig(); // @issue The internal representation of a file might add system dependent new @@ -127,7 +201,9 @@ private void setConfig() { config.save(); } catch (IOException e) { LOGGER.error("Git configuration could not be set. Message: " + e.getMessage()); + return false; } + return true; } @Override @@ -143,7 +219,7 @@ public Map getDiff(List commits) { @Override - public Map getDiff(Issue jiraIssue){ + public Map getDiff(Issue jiraIssue) { if (jiraIssue == null) { return null; } @@ -255,7 +331,7 @@ private static void deleteFolder(File directory) { } @Override - public List getCommits(Issue jiraIssue){ + public List getCommits(Issue jiraIssue) { if (jiraIssue == null) { return new LinkedList(); } @@ -284,12 +360,36 @@ public List getCommits(Issue jiraIssue){ @Override public List getCommits() { List commits = new ArrayList(); - for (Ref branch : getAllRefs()) { + for (Ref branch : getOnlyRemoteRefs()) { + /* @issue: All branches will be created in separate file system + * folders for this method's loop. How can this be prevented? + * + * @alternative: remove this method completely, + * fetching commits from all branches is not sensible! + * @pro: this method seems to be used only for code testing (TestGetCommits) + * @con: scraping it would require coding improvement in test code (TestGetCommits), + * but who wants to spend time on that;) + * + * @alternative: We could check whether the JIRA issue key is part of the branch name + * and - if so - only use the commits from this branch. + * + * @decision: release branch folders if possible, + * so that in best case only one folder will be used! + * @pro: implementation does not seem to be complex at all. + * @pro: until discussion are not finished, seems like a cheap workaround + * @con: a workaround which has potential to stay forever in the code base + * @con: still some more code will be written + * @con: scraping it, would require coding improvement in test code (TestGetCommits) + */ commits.addAll(getCommits(branch)); } return commits; } + /* + * TODO: This method and getCommits(Issue jiraIssue) need refactoring and + * deeper discussions! + */ private Ref getRef(String jiraIssueKey) { List refs = getAllRefs(); Ref branch = null; @@ -306,33 +406,77 @@ private Ref getRef(String jiraIssueKey) { } private List getAllRefs() { + return getRefs(ListBranchCommand.ListMode.ALL); + } + + private List getOnlyRemoteRefs() { + return getRefs(ListBranchCommand.ListMode.REMOTE); + } + + private List getRefs(ListBranchCommand.ListMode listMode) { List refs = new ArrayList(); try { - refs = git.branchList().setListMode(ListBranchCommand.ListMode.ALL).call(); + refs = git.branchList().setListMode(listMode).call(); } catch (GitAPIException e) { - LOGGER.error("Git could not get all references. Message: " + e.getMessage()); + LOGGER.error("Git could not get references. Message: " + e.getMessage()); } return refs; } + private List getCommitsFromDefaultBranch() { + return getCommits(defaultBranch, true); + } + private List getCommits(Ref branch) { + return getCommits(branch, false); + } + + private List getCommits(Ref branch, boolean isDefaultBranch) { List commits = new ArrayList(); if (branch == null) { return commits; } + + File directory; + String branchNameComponents[] = branch.getName().split("/"); + String branchShortName = branchNameComponents[branchNameComponents.length - 1]; + boolean canReleaseRepoDirectory = false; + + if (isDefaultBranch) { + directory = new File(fsManager.getDefaultBranchPath()); + } else { + canReleaseRepoDirectory = !fsManager.isBranchDirectoryInUse(branchShortName); + directory = new File(fsManager.prepareBranchDirectory(branchShortName)); + } try { - git.checkout().setName(branch.getName()).call(); + git.close(); + git = git.open(directory); + git.checkout().setName(branchShortName).call(); Iterable iterable = git.log().call(); for (RevCommit commit : iterable) { commits.add(commit); } - } catch (GitAPIException e) { - LOGGER.error( - "Git could not get commits for the branch: " + branch.getName() + " Message: " + e.getMessage()); + } catch (IOException | GitAPIException e) { + LOGGER.error("Git could not get commits for the branch: " + + branch.getName() + " Message: " + e.getMessage()); } + if (canReleaseRepoDirectory) { + fsManager.releaseBranchDirectoryNameToTemp(branchShortName); + } + switchGitClientBackToDefaultDirectory(); return commits; } + private void switchGitClientBackToDefaultDirectory() { + File directory = new File(fsManager.getDefaultBranchPath()); + try { + git.close(); + git = git.open(directory); + } catch (IOException e) { + LOGGER.error("Git could not get back to default branch. Message: " + e.getMessage()); + } + } + @Override public int getNumberOfCommits(Issue jiraIssue) { if (jiraIssue == null) { 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 e4b84893ee..7919ff22bb 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 @@ -31,6 +31,7 @@ public GitRepositoryFSManager(String home, String project, String repoUri, Strin /** * Returns target directory path for the default branch of the repository. + * * @return absolute path to directory of the default branch */ public String getDefaultBranchPath() { @@ -54,26 +55,24 @@ public String releaseBranchDirectoryNameToTemp(String branchShortName) { File oldDir = new File(getBranchPath(branchShortName)); if (!oldDir.isDirectory()) { return null; - } - else { - Date date= new Date(); + } else { + Date date = new Date(); long time = date.getTime(); String tempDirString = baseProjectUriPath - +File.separator - +TEMP_DIR_PREFIX+String.valueOf(time); + + File.separator + + TEMP_DIR_PREFIX + String.valueOf(time); File tempDir = new File(tempDirString); boolean renameResult = false; try { renameResult = oldDir.renameTo(tempDir); - } - catch (Exception e) { - LOGGER.error("Could not rename "+oldDir - +" to "+tempDirString+". "+e.getMessage()); + } catch (Exception e) { + LOGGER.error("Could not rename " + oldDir + + " to " + tempDirString + ". " + e.getMessage()); return null; } if (!renameResult) { - LOGGER.error("Could not rename "+oldDir - +" to "+tempDirString+". The reason is not known."); + LOGGER.error("Could not rename " + oldDir + + " to " + tempDirString + ". The reason is not known."); return null; } removeBranchPathMarker(branchShortName); @@ -85,9 +84,9 @@ public String releaseBranchDirectoryNameToTemp(String branchShortName) { * Provides filesystem directory for targeted branch. * Best case: branch already exists, costs no I/O operations. * Good case: temporary folder exists and can be renamed to branch's - * target folder name. + * target folder name. * Bad case: branch folder is copied in I/O heavy operation - * from default branch. + * from default branch. * * @param branchShortName branch name * @return null on failure, absolute path to branch's directory @@ -119,9 +118,8 @@ private String getShortHash(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) { LOGGER.error("MD5 does not exist??"); return ""; } @@ -137,10 +135,10 @@ private String getShortHash(String text) { */ private void maintainNotUsedBranchPaths() { String[] notUsedBranchPaths = findOutdatedBranchPaths(); - if (notUsedBranchPaths!=null) + if (notUsedBranchPaths != null) for (String branch : notUsedBranchPaths) { releaseBranchDirectoryNameToTemp(branch); - LOGGER.info("Returned "+branch+" to temporary directory pool."); + LOGGER.info("Returned " + branch + " to temporary directory pool."); } } @@ -152,14 +150,13 @@ private void rememberBranchPathRequest(String branchShortName) { // ignore the last marker removeBranchPathMarker(branchShortName); // add new marker - File file = new File(baseProjectUriPath+File.separator+branchShortName); + File file = new File(baseProjectUriPath, branchShortName); file.setWritable(true); try { // assumes branch names are valid file names FileUtils.writeStringToFile(file, getShortHash(branchShortName), Charset.forName("UTF-8")); - } - catch (IOException ex) { - LOGGER.info(ex.getMessage()); + } catch (IOException ex) { + LOGGER.info(ex.getMessage()); } } @@ -167,27 +164,25 @@ private void rememberBranchPathRequest(String branchShortName) { * shall not try to recycle the branch folder */ private void removeBranchPathMarker(String branchShortName) { - File file = new File(baseProjectUriPath,branchShortName); + File file = new File(baseProjectUriPath, branchShortName); file.delete(); } private String getBranchPath(String branchShortName) { - return baseProjectUriPath+File.separator+getShortHash(branchShortName); + return baseProjectUriPath + File.separator + getShortHash(branchShortName); } private boolean useFromDefaultFolder(String branchShortName) { File defaultDir = new File(baseProjectUriDefaultPath); if (!defaultDir.isDirectory()) { return false; - } - else { + } else { try { File newDir = new File(getBranchPath(branchShortName)); - FileUtils.copyDirectory(defaultDir,newDir); - } - catch (Exception e) { - LOGGER.error("Could not copy "+defaultDir - +" to "+getBranchPath(branchShortName)+".\n\t"+e.getMessage()); + FileUtils.copyDirectory(defaultDir, newDir); + } catch (Exception e) { + LOGGER.error("Could not copy " + defaultDir + + " to " + getBranchPath(branchShortName) + ".\n\t" + e.getMessage()); return false; } } @@ -201,18 +196,16 @@ private boolean useFromExistingBranchFolder(String branchShortName) { private boolean useFromTemporaryFolder(String branchShortName) { String[] tempDirs = findTemporaryDirectoryNames(); - if (tempDirs==null || tempDirs.length<1) { + if (tempDirs == null || tempDirs.length < 1) { return false; } try { - File dir = new File(baseProjectUriPath+File.separator - +tempDirs[0]); // get the 1st of temp dirs + File dir = new File(baseProjectUriPath, tempDirs[0]); // get the 1st of temp dirs, but is 1st the best? File newDir = new File(getBranchPath(branchShortName)); dir.renameTo(newDir); - } - catch (Exception e) { - LOGGER.error("Could not rename "+tempDirs[0] - +" to "+getBranchPath(branchShortName)+". "+e.getMessage()); + } catch (Exception e) { + LOGGER.error("Could not rename " + tempDirs[0] + + " to " + getBranchPath(branchShortName) + ". " + e.getMessage()); return false; } return true; @@ -230,14 +223,34 @@ private String[] findTemporaryDirectoryNames() { * and looks at their creation dates */ private String[] findOutdatedBranchPaths() { + return findBranchPathFiles(true); + } + + private String[] findBranchPathFiles(boolean getOutdated) { File file = new File(baseProjectUriPath); Date date = new Date(); - String[] branchTouchFiles = file.list((current, name) -> + String[] branchFilteredTouchFiles = file.list((current, name) -> { - boolean outDated = (date.getTime()-current.lastModified())>BRANCH_OUTDATED_AFTER; + long fileLifespan = date.getTime() - current.lastModified(); + boolean lifeSpanCondition = fileLifespan > BRANCH_OUTDATED_AFTER; + if (!getOutdated) { + lifeSpanCondition = !lifeSpanCondition; + } boolean isFile = new File(current, name).isFile(); - return isFile && outDated; + return isFile && lifeSpanCondition; }); - return branchTouchFiles; + return branchFilteredTouchFiles; + } + + public boolean isBranchDirectoryInUse(String branchShortName) { + String[] inUseList = findBranchPathFiles(false); + if (inUseList != null) { + for (String touchFileName : inUseList) { + if (touchFileName.equals(branchShortName)) { + return true; + } + } + } + return false; } } 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 f92b290155..7f6e99d2c5 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 @@ -33,7 +33,7 @@ public class TestSetUpGit extends TestSetUpWithIssues { public static void setUpBeforeClass() throws IOException { File directory = getExampleDirectory(); String uri = getExampleUri(); - gitClient = new GitClientImpl(uri, directory); + gitClient = new GitClientImpl(uri, directory.getAbsolutePath(), "TEST"); makeExampleCommit("readMe.txt", "TODO Write ReadMe", "Init Commit"); makeExampleCommit("readMe.txt", "Self-explanatory, ReadMe not necessary.", "TEST-12: Explain how the great software works"); 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 193c144fee..5e266ccf4c 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 @@ -56,6 +56,13 @@ public void setUp() { projectName, repoUri, folderForDefaultBranchName); } + @Test + public void testIsBranchDirectoryInUse() { + assertFalse(FSmanager.isBranchDirectoryInUse(branchName)); + addBranchMarker(branchName); + assertTrue(FSmanager.isBranchDirectoryInUse(branchName)); + } + @Test public void testReleaseBranchDirectoryNameToTemp() { // setup @@ -218,6 +225,18 @@ private String[] findTemporaryDirectoryNames() { /* helpers for adding files */ + private void addBranchMarker(String branchName) { + File dir = new File (baseProjectUriDir); + File touchFile = new File(dir, branchName); + try { + dir.mkdirs(); + touchFile.createNewFile(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + private void addDistinctFileInDefaultDir() { addDistinctFile("default"); }