blob: 11eba79559f17f295d2e27f7904a6b6524be7bf6 [file] [log] [blame]
/*********************************************************************
* Copyright (c) 2019 Boeing
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Boeing - initial API and implementation
**********************************************************************/
package org.eclipse.osee.define.rest;
import static org.eclipse.osee.framework.core.enums.CoreArtifactTokens.DefaultHierarchyRoot;
import static org.eclipse.osee.framework.core.enums.CoreArtifactTypes.Folder;
import static org.eclipse.osee.framework.core.enums.CoreArtifactTypes.GitCommit;
import static org.eclipse.osee.framework.core.enums.CoreArtifactTypes.GitRepository;
import static org.eclipse.osee.framework.core.enums.CoreAttributeTypes.DefaultTrackingBranch;
import static org.eclipse.osee.framework.core.enums.CoreAttributeTypes.FileSystemPath;
import static org.eclipse.osee.framework.core.enums.CoreAttributeTypes.GitChangeId;
import static org.eclipse.osee.framework.core.enums.CoreAttributeTypes.GitCommitAuthorDate;
import static org.eclipse.osee.framework.core.enums.CoreAttributeTypes.RepositoryUrl;
import static org.eclipse.osee.framework.core.enums.CoreRelationTypes.GitRepositoryCommit_GitCommit;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.TransportCommand;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.RawTextComparator;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.NetRCCredentialsProvider;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.TagOpt;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.io.DisabledOutputStream;
import org.eclipse.osee.define.api.GitOperations;
import org.eclipse.osee.framework.core.data.ArtifactId;
import org.eclipse.osee.framework.core.data.ArtifactToken;
import org.eclipse.osee.framework.core.data.BranchId;
import org.eclipse.osee.framework.core.data.OseeClient;
import org.eclipse.osee.framework.core.data.TransactionToken;
import org.eclipse.osee.framework.core.data.UserId;
import org.eclipse.osee.framework.core.enums.CoreArtifactTokens;
import org.eclipse.osee.framework.core.enums.CoreArtifactTypes;
import org.eclipse.osee.framework.core.enums.CoreAttributeTypes;
import org.eclipse.osee.framework.core.enums.CoreRelationTypes;
import org.eclipse.osee.framework.core.enums.SystemUser;
import org.eclipse.osee.framework.jdk.core.type.OseeArgumentException;
import org.eclipse.osee.framework.jdk.core.type.OseeCoreException;
import org.eclipse.osee.framework.jdk.core.type.OseeStateException;
import org.eclipse.osee.orcs.OrcsApi;
import org.eclipse.osee.orcs.SystemProperties;
import org.eclipse.osee.orcs.data.ArtifactReadable;
import org.eclipse.osee.orcs.search.QueryFactory;
import org.eclipse.osee.orcs.transaction.TransactionBuilder;
/**
* @author Ryan D. Brooks
*/
public final class GitOperationsImpl implements GitOperations {
private final OrcsApi orcsApi;
private final QueryFactory queryFactory;
private final SystemProperties systemPrefs;
private final Map<String, ArtifactId> pathToCodeunitReferenceMap = new HashMap<>();
private static final Pattern changeIdPattern = Pattern.compile("\\s+Change-Id: (I\\w{40})");
private final Matcher changeIdMatcher = changeIdPattern.matcher("");
public GitOperationsImpl(OrcsApi orcsApi, SystemProperties systemPrefs) {
this.orcsApi = orcsApi;
this.queryFactory = orcsApi.getQueryFactory();
this.systemPrefs = systemPrefs;
}
@Override
public ArtifactToken getCommitArtifactId(BranchId branch, String changeId) {
List<ArtifactReadable> commits =
queryFactory.fromBranch(branch).andAttributeIs(GitChangeId, changeId).andTypeEquals(
GitCommit).getResults().getList();
ArtifactToken latestCommit = ArtifactToken.SENTINEL;
Date lastestAuthorDate = new Date(0);
for (ArtifactReadable commit : commits) {
Date authorDate;
try {
authorDate = new SimpleDateFormat().parse(commit.getSoleAttributeValue(GitCommitAuthorDate));
if (authorDate.after(lastestAuthorDate)) {
lastestAuthorDate = authorDate;
latestCommit = commit;
}
} catch (ParseException ex) {
ex.printStackTrace();
}
}
return latestCommit;
}
@Override
public ArtifactReadable getRepoArtifact(BranchId branch, String repositoryName) {
return queryFactory.fromBranch(branch).andNameEquals(repositoryName).andTypeEquals(
CoreArtifactTypes.GitRepository).getArtifact();
}
private Repository getLocalRepoReference(String repoPath) {
File gitDirPath = new File(repoPath + File.separator + ".git");
try {
return new FileRepositoryBuilder().setGitDir(gitDirPath).readEnvironment().findGitDir().setMustExist(
true).build();
} catch (IOException ex) {
throw OseeCoreException.wrap(ex);
}
}
@Override
public void fetch(ArtifactReadable repoArtifact, String passphrase) {
Repository jgitRepo = getLocalRepoReference(repoArtifact.getSoleAttributeValue(FileSystemPath));
fetch(jgitRepo, passphrase);
}
private void fetch(Repository localRepo, String passphrase) {
try (Git git = new Git(localRepo)) {
FetchCommand fetchCommand = git.fetch().setCheckFetchedObjects(true).setTagOpt(TagOpt.FETCH_TAGS);
configurateAuthentication(localRepo, fetchCommand, passphrase);
FetchResult result = fetchCommand.call();
} catch (GitAPIException ex) {
throw OseeCoreException.wrap(ex);
}
}
private void configurateAuthentication(Repository repo, TransportCommand<?, ?> transportCommand, String passphrase) {
String gitRepoUrl = repo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, "origin", "url");
configurateAuthentication(gitRepoUrl, transportCommand, passphrase);
}
private void configurateAuthentication(String gitRepoUrl, TransportCommand<?, ?> transportCommand, String passphrase) {
if (gitRepoUrl.startsWith("ssh")) {
configureSsh(transportCommand, passphrase);
} else {
transportCommand.setCredentialsProvider(new NetRCCredentialsProvider());
}
}
private void configureSsh(TransportCommand<?, ?> transportCommand, String passphrase) {
SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
@Override
protected void configure(Host host, Session session) {
session.setPassword(passphrase);
}
@Override
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch defaultJSch = super.createDefaultJSch(fs);
defaultJSch.addIdentity("~/.ssh/id_rsa", passphrase);
return defaultJSch;
}
};
transportCommand.setTransportConfigCallback(new TransportConfigCallback() {
@Override
public void configure(Transport transport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
}
});
}
@Override
public ArtifactId trackGitBranch(String gitRepoUrl, BranchId branch, UserId account, String gitBranchName, boolean clone, String passphrase) {
ArtifactReadable repoArtifact = clone(gitRepoUrl, branch, account, gitBranchName, clone, passphrase);
return updateGitTrackingBranch(branch, repoArtifact, account, gitBranchName, !clone, passphrase, true);
}
@Override
public ArtifactId updateGitTrackingBranch(BranchId branch, ArtifactReadable repoArtifact, UserId account, String gitBranchName, boolean fetch, String passphrase, boolean initialImport) {
Repository jgitRepo = getLocalRepoReference(repoArtifact.getSoleAttributeValue(FileSystemPath));
if (fetch) {
fetch(jgitRepo, passphrase);
}
if (gitBranchName == null) {
gitBranchName = repoArtifact.getSoleAttributeValue(DefaultTrackingBranch);
}
try {
String fromString = "remotes/origin/" + gitBranchName;
ObjectId from = jgitRepo.resolve(fromString);
if (from == null) {
throw new OseeStateException("Failed to resolve commit [%s]", fromString);
}
ObjectId to = null;
ArtifactReadable latestCommit =
repoArtifact.getRelated(GitRepositoryCommit_GitCommit).getAtMostOneOrDefault(ArtifactReadable.SENTINEL);
if (latestCommit.isValid()) {
String latestImportedSHA = latestCommit.getSoleAttributeValue(CoreAttributeTypes.GitCommitSha);
to = ObjectId.fromString(latestImportedSHA);
if (to == null) {
throw new OseeStateException("Failed to resolve commit [%s]", latestImportedSHA);
}
}
TransactionBuilder tx = orcsApi.getTransactionFactory().createTransaction(repoArtifact.getBranch(), account,
"updateGitTrackingBranch repo [" + repoArtifact + "]");
List<ArtifactReadable> currentCommits =
queryFactory.fromBranch(branch).andIsOfType(CoreArtifactTypes.CodeUnit).andRelatedRecursive(
CoreRelationTypes.DefaultHierarchical_Child, repoArtifact).asArtifacts();
for (ArtifactReadable singleCommit : currentCommits) {
String fullPathName = "";
try {
fullPathName = singleCommit.getSoleAttributeAsString(CoreAttributeTypes.FileSystemPath);
} catch (Exception e) {
fullPathName = setFullPathName(branch, singleCommit, repoArtifact);
tx.setSoleAttributeFromString(singleCommit, CoreAttributeTypes.FileSystemPath, fullPathName);
}
if (fullPathName.isEmpty()) {
throw new OseeArgumentException(
"Attribute FileSystemPath on code unit %s - art id [%d] is missing and cannot be determined",
singleCommit.getName(), singleCommit.getId());
}
pathToCodeunitReferenceMap.put(fullPathName, singleCommit);
}
HistoryImportStrategy importStrategy =
new FastHistoryStrategy(repoArtifact, orcsApi, tx, initialImport, pathToCodeunitReferenceMap);
walkTree(repoArtifact, jgitRepo, to, from, repoArtifact.getBranch(), account, importStrategy);
} catch (RevisionSyntaxException | IOException ex) {
throw OseeCoreException.wrap(ex);
}
return repoArtifact;
}
@Override
public List<String> getChangeIdBetweenTags(BranchId branch, ArtifactReadable repoArtifact, String startTag, String endTag) {
Repository jgitRepo = getLocalRepoReference(repoArtifact.getSoleAttributeValue(FileSystemPath));
/* fetch second arg (passPhrase) provide a key or password to enter repo. Here we have no pass phrase. */
fetch(jgitRepo, "");
try (Git git = new Git(jgitRepo)) {
Ref tag1 = git.getRepository().exactRef("refs/tags/" + startTag);
Ref tag2 = git.getRepository().exactRef("refs/tags/" + endTag);
Iterable<RevCommit> commits = git.log().addRange(tag1.getPeeledObjectId(), tag2.getPeeledObjectId()).call();
// parse through commits to get specific tags with specific commits and change ids
List<String> changeIdList = new ArrayList<>();
for (RevCommit revCommit : commits) {
if (revCommit.getShortMessage() != "") {
String commitSHA = revCommit.getId().name();
if (changeIdMatcher.reset(revCommit.getFullMessage()).find()) {
String changeId = changeIdMatcher.group(1);
changeIdList.add(changeId);
} else {
changeIdList.add(commitSHA);
}
}
}
return changeIdList;
} catch (Exception ex) {
throw OseeCoreException.wrap(ex);
}
}
public ArtifactReadable clone(String gitRepoUrl, BranchId branch, UserId account, String gitBranchName, boolean clone, String passphrase) {
String serverDataLocation = systemPrefs.getValue(OseeClient.OSEE_APPLICATION_SERVER_DATA);
String repoName = gitRepoUrl.substring(gitRepoUrl.lastIndexOf('/') + 1).replaceAll("\\.git$", "");
File localPath = new File(serverDataLocation + File.separator + "git", repoName);
String branchToClone = "refs/heads/" + gitBranchName;
if (clone) {
CloneCommand jgitClone = Git.cloneRepository().setURI(gitRepoUrl).setDirectory(localPath).setBranchesToClone(
Arrays.asList(branchToClone)).setBranch(branchToClone).setNoCheckout(true);
configurateAuthentication(gitRepoUrl, jgitClone, passphrase);
try {
jgitClone.call();
} catch (GitAPIException ex) {
throw OseeCoreException.wrap(ex);
}
}
if (queryFactory.fromBranch(branch).andNameEquals(repoName).andTypeEquals(GitRepository).exists()) {
throw new OseeStateException("A repository named %s already exists on branch %s", repoName, branch);
}
TransactionBuilder tx =
orcsApi.getTransactionFactory().createTransaction(branch, account, "GitOperationsImpl.createGitRepository()");
if (!queryFactory.fromBranch(branch).andNameEquals("Git Repositories").andTypeEquals(Folder).exists()) {
tx.createArtifact(DefaultHierarchyRoot, CoreArtifactTokens.GitRepoFolder);
}
ArtifactId repoArtifact = tx.createArtifact(CoreArtifactTokens.GitRepoFolder, GitRepository, repoName);
tx.setSoleAttributeValue(repoArtifact, RepositoryUrl, gitRepoUrl);
try {
tx.setSoleAttributeValue(repoArtifact, FileSystemPath, localPath.getCanonicalPath());
} catch (IOException ex) {
throw OseeCoreException.wrap(ex);
}
tx.commit();
return queryFactory.fromBranch(branch).andId(repoArtifact).getArtifact();
}
private TransactionToken walkTree(ArtifactReadable repoArtifact, Repository jgitRepo, ObjectId to, ObjectId from, BranchId branch, UserId account, HistoryImportStrategy importStrategy) {
try (RevWalk revWalk = new RevWalk(jgitRepo)) {
revWalk.markStart(revWalk.parseCommit(from)); //newest commit
if (to != null) {
RevCommit toRev = revWalk.parseCommit(to);
revWalk.markUninteresting(toRev); // oldest commit - the last one we previously imported
}
revWalk.sort(RevSort.TOPO, true);
revWalk.sort(RevSort.REVERSE, true);
DiffFormatter df = new DiffFormatter(DisabledOutputStream.INSTANCE);
df.setRepository(jgitRepo);
df.setDiffComparator(RawTextComparator.DEFAULT);
df.setDetectRenames(true);
setPathFilter(repoArtifact, df);
ArtifactId lastValidCommit = ArtifactId.SENTINEL;
ObjectReader objectReader = revWalk.getObjectReader();
for (RevCommit revCommit : revWalk) {
ArtifactId lastCommit =
parseGitCommit(objectReader, df, repoArtifact, revCommit, branch, account, importStrategy);
if (lastCommit.isValid()) {
lastValidCommit = lastCommit;
}
}
if (lastValidCommit.isValid()) {
TransactionBuilder tx = importStrategy.getTransactionBuilder(orcsApi, branch, account);
tx.unrelateFromAll(GitRepositoryCommit_GitCommit.getOpposite(), repoArtifact);
tx.relate(repoArtifact, GitRepositoryCommit_GitCommit, lastValidCommit);
importStrategy.finishGitCommit(tx);
}
return importStrategy.finishImport();
} catch (IOException ex) {
throw OseeCoreException.wrap(ex);
}
}
private void setPathFilter(ArtifactReadable repoArtifact, DiffFormatter df) {
List<String> paths = repoArtifact.getAttributeValues(CoreAttributeTypes.ExcludePath);
if (!paths.isEmpty()) {
List<TreeFilter> filters = new ArrayList<>();
for (String path : paths) {
filters.add(PathFilter.create(path));
}
if (filters.size() > 1) {
df.setPathFilter(AndTreeFilter.create(filters).negate());
} else {
df.setPathFilter(filters.get(0).negate());
}
}
}
private ArtifactId createCommitArtifact(RevCommit revCommit, TransactionBuilder tx, BranchId branch) {
String commitSHA = revCommit.getId().name();
String commitId;
if (changeIdMatcher.reset(revCommit.getFullMessage()).find()) {
String changeId = changeIdMatcher.group(1);
commitId = changeId;
} else {
commitId = commitSHA;
}
try {
return queryFactory.fromBranch(branch).andIsOfType(CoreArtifactTypes.GitCommit).andNameEquals(
revCommit.getShortMessage()).asArtifact();
} catch (Exception ex) {
ArtifactId commitArtifact = tx.createArtifact(GitCommit, revCommit.getShortMessage());
tx.setSoleAttributeValue(commitArtifact, CoreAttributeTypes.GitCommitSha, commitSHA);
tx.setSoleAttributeValue(commitArtifact, CoreAttributeTypes.UserArtifactId, SystemUser.OseeSystem); //TODO: this must convert author to the corresponding user artifact
tx.setSoleAttributeValue(commitArtifact, CoreAttributeTypes.GitCommitAuthorDate,
revCommit.getAuthorIdent().getWhen());
tx.setSoleAttributeValue(commitArtifact, CoreAttributeTypes.GitCommitMessage, revCommit.getFullMessage());
tx.setSoleAttributeValue(commitArtifact, GitChangeId, commitId);
return commitArtifact;
}
}
private ArtifactId parseGitCommit(ObjectReader objectReader, DiffFormatter df, ArtifactReadable repoArtifact, RevCommit revCommit, BranchId branch, UserId account, HistoryImportStrategy importStrategy) {
try {
TransactionBuilder tx = importStrategy.getTransactionBuilder(orcsApi, branch, account);
ArtifactId commitArtifact = createCommitArtifact(revCommit, tx, branch);
importFileChanges(objectReader, df, repoArtifact, revCommit, revCommit.getId().name(), commitArtifact, branch,
tx, importStrategy);
importStrategy.finishGitCommit(tx);
return commitArtifact;
} finally {
revCommit.disposeBody();
}
}
private void importFileChanges(ObjectReader objectReader, DiffFormatter df, ArtifactId repoArtifact, RevCommit revCommit, String commitSHA, ArtifactId commitArtifact, BranchId branch, TransactionBuilder tx, HistoryImportStrategy importStrategy) {
if (revCommit.getParents().length > 1) {
return;
}
RevTree parentTree = revCommit.getParentCount() > 0 ? revCommit.getParent(0).getTree() : null;
List<DiffEntry> diffs = null;
try {
diffs = df.scan(parentTree, revCommit.getTree());
for (DiffEntry entry : diffs) {
ChangeType changeType = entry.getChangeType();
String path = entry.getOldPath();
String newPath = entry.getNewPath();
ArtifactId codeUnit = importStrategy.getCodeUnit(branch, tx, commitSHA, changeType, path, newPath);
if (codeUnit.isValid()) {
importStrategy.handleCodeUnit(branch, codeUnit, tx, repoArtifact, commitArtifact, changeType);
}
}
} catch (IOException ex) {
throw OseeCoreException.wrap(ex);
}
}
private String setFullPathName(BranchId branch, ArtifactReadable singleCommit, ArtifactReadable repoArtifact) {
ArtifactReadable art = singleCommit;
String wholePath = art.getName();
while (!art.getParent().equals(repoArtifact)) {
art =
queryFactory.fromBranch(branch).andIsOfType(CoreArtifactTypes.Folder).andId(art.getParent()).asArtifact();
wholePath = art.getName() + "/" + wholePath;
}
return wholePath;
}
}