/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.jabylon.team.git;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Service;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.SubMonitor;
import org.eclipse.emf.common.util.URI;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CleanCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.DiffCommand;
import org.eclipse.jgit.api.FetchCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.RebaseCommand;
import org.eclipse.jgit.api.RebaseCommand.Operation;
import org.eclipse.jgit.api.RebaseResult;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.ResetCommand.ResetType;
import org.eclipse.jgit.api.TransportConfigCallback;
import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.InvalidRefNameException;
import org.eclipse.jgit.api.errors.InvalidRemoteException;
import org.eclipse.jgit.api.errors.JGitInternalException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.api.errors.NoMessageException;
import org.eclipse.jgit.api.errors.RefAlreadyExistsException;
import org.eclipse.jgit.api.errors.RefNotFoundException;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.api.errors.WrongRepositoryStateException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.errors.AmbiguousObjectException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.errors.UnsupportedCredentialItem;
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.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
import org.eclipse.jgit.transport.OpenSshConfig.Host;
import org.eclipse.jgit.transport.PushResult;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.RemoteRefUpdate;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.SshTransport;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.util.FS;
import org.jabylon.common.team.TeamProvider;
import org.jabylon.common.team.TeamProviderException;
import org.jabylon.common.util.PreferencesUtil;
import org.jabylon.properties.DiffKind;
import org.jabylon.properties.Project;
import org.jabylon.properties.ProjectVersion;
import org.jabylon.properties.PropertiesFactory;
import org.jabylon.properties.PropertyFileDescriptor;
import org.jabylon.properties.PropertyFileDiff;
import org.jabylon.properties.Workspace;
import org.jabylon.team.git.util.ProgressMonitorWrapper;
import org.osgi.service.prefs.Preferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
@Component(enabled=true,immediate=true)
@Service
public class GitTeamProvider implements TeamProvider {
@Property(value="Git")
private static String KEY_KIND = TeamProvider.KEY_KIND;
private static final Logger LOGGER = LoggerFactory.getLogger(GitTeamProvider.class);
private Repository createRepository(ProjectVersion project) throws IOException {
FileRepositoryBuilder builder = new FileRepositoryBuilder();
File gitDir = new File(project.absoluteFilePath().path());
Repository repository = builder.setGitDir(new File(gitDir,".git")).build();
return repository;
}
@Override
public Collection<PropertyFileDiff> update(ProjectVersion project, IProgressMonitor monitor)
throws TeamProviderException {
SubMonitor subMon = SubMonitor.convert(monitor,100);
List<PropertyFileDiff> updatedFiles = new ArrayList<PropertyFileDiff>();
try {
Repository repository = createRepository(project);
Git git = Git.wrap(repository);
FetchCommand fetchCommand = git.fetch();
URI uri = project.getParent().getRepositoryURI();
if(uri!=null)
fetchCommand.setRemote(stripUserInfo(uri).toString());
String refspecString = "refs/heads/{0}:refs/remotes/origin/{0}";
refspecString = MessageFormat.format(refspecString, project.getName());
RefSpec spec = new RefSpec(refspecString);
fetchCommand.setRefSpecs(spec);
subMon.subTask("Fetching from remote");
if(!"https".equals(uri.scheme()) && ! "http".equals(uri.scheme()))
fetchCommand.setTransportConfigCallback(createTransportConfigCallback(project.getParent()));
fetchCommand.setCredentialsProvider(createCredentialsProvider(project.getParent()));
fetchCommand.setProgressMonitor(new ProgressMonitorWrapper(subMon.newChild(80)));
fetchCommand.call();
ObjectId remoteHead = repository.resolve("refs/remotes/origin/"+project.getName()+"^{tree}");
DiffCommand diff = git.diff();
subMon.subTask("Caculating Diff");
diff.setProgressMonitor(new ProgressMonitorWrapper(subMon.newChild(20)));
diff.setOldTree(new FileTreeIterator(repository));
CanonicalTreeParser p = new CanonicalTreeParser();
ObjectReader reader = repository.newObjectReader();
try {
p.reset(reader, remoteHead);
} finally {
reader.release();
}
diff.setNewTree(p);
checkCanceled(subMon);
List<DiffEntry> diffs = diff.call();
for (DiffEntry diffEntry : diffs) {
checkCanceled(subMon);
updatedFiles.add(convertDiffEntry(diffEntry));
LOGGER.trace(diffEntry.toString());
}
if(!updatedFiles.isEmpty())
{
checkCanceled(subMon);
//no more cancel after this point
ObjectId lastCommitID = repository.resolve("refs/remotes/origin/"+project.getName()+"^{commit}");
LOGGER.info("Merging remote commit {} to {}/{}", new Object[]{lastCommitID,project.getName(),project.getParent().getName()});
//TODO: use rebase here?
if(isRebase(project))
{
RebaseCommand rebase = git.rebase();
rebase.setUpstream("refs/remotes/origin/"+project.getName());
RebaseResult result = rebase.call();
if(result.getStatus().isSuccessful())
{
LOGGER.info("Rebase finished: {}",result.getStatus());
}
else
{
LOGGER.error("Rebase of {} failed. Attempting abort",project.relativePath());
rebase = git.rebase();
rebase.setOperation(Operation.ABORT);
result = rebase.call();
LOGGER.error("Abort finished with {}",result.getStatus());
}
}
else
{
MergeCommand merge = git.merge();
merge.include(lastCommitID);
MergeResult mergeResult = merge.call();
LOGGER.info("Merge finished: {}",mergeResult.getMergeStatus());
}
}
else
LOGGER.info("Update finished successfully. Nothing to merge, already up to date");
} catch (JGitInternalException e) {
throw new TeamProviderException(e);
} catch (InvalidRemoteException e) {
throw new TeamProviderException(e);
} catch (GitAPIException e) {
throw new TeamProviderException(e);
} catch (AmbiguousObjectException e) {
throw new TeamProviderException(e);
} catch (IOException e) {
throw new TeamProviderException(e);
}
finally{
monitor.done();
}
return updatedFiles;
}
private void checkCanceled(IProgressMonitor monitor) {
if(monitor.isCanceled())
throw new OperationCanceledException();
}
private PropertyFileDiff convertDiffEntry(DiffEntry diffEntry) {
PropertyFileDiff diff = PropertiesFactory.eINSTANCE.createPropertyFileDiff();
diff.setOldPath(diffEntry.getOldPath());
diff.setNewPath(diffEntry.getNewPath());
DiffKind kind = DiffKind.MODIFY;
switch(diffEntry.getChangeType())
{
case ADD:
kind = DiffKind.ADD;
break;
case COPY:
kind = DiffKind.COPY;
break;
case DELETE:
kind = DiffKind.REMOVE;
break;
case MODIFY:
kind = DiffKind.MODIFY;
break;
case RENAME:
kind = DiffKind.MOVE;
break;
}
diff.setKind(kind);
return diff;
}
@Override
public Collection<PropertyFileDiff> update(PropertyFileDescriptor descriptor, IProgressMonitor monitor)
throws TeamProviderException {
//TODO check if it needs to be implemented
return null;
}
@Override
public void checkout(ProjectVersion project, IProgressMonitor monitor) throws TeamProviderException {
try {
SubMonitor subMon = SubMonitor.convert(monitor, 100);
subMon.setTaskName("Checking out");
subMon.worked(20);
File repoDir = new File(project.absoluteFilePath().path());
CloneCommand clone = Git.cloneRepository();
clone.setBare(false);
clone.setNoCheckout(false);
// if(!"master".equals(project.getName()))
clone.setBranch("refs/heads/" + project.getName());
// clone.setCloneAllBranches(true);
clone.setBranchesToClone(Collections.singletonList("refs/heads/" + project.getName()));
clone.setDirectory(repoDir);
URI uri = project.getParent().getRepositoryURI();
if(!"https".equals(uri.scheme()) && ! "http".equals(uri.scheme()))
clone.setTransportConfigCallback(createTransportConfigCallback(project.getParent()));
clone.setCredentialsProvider(createCredentialsProvider(project.getParent()));
clone.setURI(stripUserInfo(uri).toString());
clone.setProgressMonitor(new ProgressMonitorWrapper(subMon.newChild(70)));
clone.call();
subMon.done();
if (monitor != null)
monitor.done();
} catch (TransportException e) {
throw new TeamProviderException(e);
} catch (InvalidRemoteException e) {
throw new TeamProviderException(e);
} catch (GitAPIException e) {
throw new TeamProviderException(e);
}
}
@Override
public void commit(ProjectVersion project, IProgressMonitor monitor) throws TeamProviderException {
try {
Repository repository = createRepository(project);
SubMonitor subMon = SubMonitor.convert(monitor, "Commit", 100);
Git git = new Git(repository);
// AddCommand addCommand = git.add();
List<String> changedFiles = addNewFiles(git, subMon.newChild(30));
if (!changedFiles.isEmpty())
{
checkCanceled(subMon);
CommitCommand commit = git.commit();
Preferences node = PreferencesUtil.scopeFor(project.getParent());
String username = node.get(GitConstants.KEY_USERNAME, "Jabylon");
String email = node.get(GitConstants.KEY_EMAIL, "jabylon@example.org");
String message = node.get(GitConstants.KEY_MESSAGE, "Auto Sync-up by Jabylon");
boolean insertChangeId = node.getBoolean(GitConstants.KEY_INSERT_CHANGE_ID, false);
commit.setAuthor(username, email);
commit.setCommitter(username, email);
commit.setInsertChangeId(insertChangeId);
commit.setMessage(message);
for (String path : changedFiles) {
checkCanceled(subMon);
commit.setOnly(path);
}
commit.call();
subMon.worked(10);
}
else
{
LOGGER.info("No changed files, skipping commit phase");
}
checkCanceled(subMon);
PushCommand push = git.push();
URI uri = project.getParent().getRepositoryURI();
if(uri!=null)
push.setRemote(stripUserInfo(uri).toString());
push.setProgressMonitor(new ProgressMonitorWrapper(subMon.newChild(60)));
if(!"https".equals(uri.scheme()) && ! "http".equals(uri.scheme()))
push.setTransportConfigCallback(createTransportConfigCallback(project.getParent()));
push.setCredentialsProvider(createCredentialsProvider(project.getParent()));
RefSpec spec = createRefSpec(project);
push.setRefSpecs(spec);
Iterable<PushResult> result = push.call();
for (PushResult r : result) {
for(RemoteRefUpdate rru : r.getRemoteUpdates()) {
if(rru.getStatus() != RemoteRefUpdate.Status.OK && rru.getStatus() != RemoteRefUpdate.Status.UP_TO_DATE) {
String error = "Push failed: "+rru.getStatus();
LOGGER.error(error);
throw new TeamProviderException(error);
}
}
}
Ref ref = repository.getRef(project.getName());
if(ref!=null)
{
LOGGER.info("Successfully pushed {} to {}",ref.getObjectId(),project.getParent().getRepositoryURI());
}
} catch (NoHeadException e) {
throw new TeamProviderException(e);
} catch (NoMessageException e) {
throw new TeamProviderException(e);
} catch (ConcurrentRefUpdateException e) {
throw new TeamProviderException(e);
} catch (JGitInternalException e) {
throw new TeamProviderException(e);
} catch (WrongRepositoryStateException e) {
throw new TeamProviderException(e);
} catch (InvalidRemoteException e) {
throw new TeamProviderException(e);
} catch (IOException e) {
throw new TeamProviderException(e);
} catch (GitAPIException e) {
throw new TeamProviderException(e);
} finally {
if(monitor!=null)
monitor.done();
}
}
private RefSpec createRefSpec(ProjectVersion version) {
Preferences node = PreferencesUtil.scopeFor(version);
String refSpecString = node.get(GitConstants.KEY_PUSH_REFSPEC, GitConstants.DEFAULT_PUSH_REFSPEC);
if(refSpecString.isEmpty())
refSpecString = GitConstants.DEFAULT_PUSH_REFSPEC;
refSpecString = MessageFormat.format(refSpecString, version.getName());
return new RefSpec(refSpecString);
}
private boolean isRebase(ProjectVersion version) {
Preferences node = PreferencesUtil.scopeFor(version);
return node.getBoolean(GitConstants.KEY_REBASE, GitConstants.DEFAULT_REBASE);
}
private List<String> addNewFiles(Git git, IProgressMonitor monitor) throws IOException, GitAPIException {
monitor.beginTask("Creating Diff", 100);
DiffCommand diffCommand = git.diff();
AddCommand addCommand = git.add();
List<String> changedFiles = new ArrayList<String>();
List<String> newFiles = new ArrayList<String>();
List<DiffEntry> result = diffCommand.call();
//TODO: delete won't work
for (DiffEntry diffEntry : result) {
checkCanceled(monitor);
if(diffEntry.getChangeType()==ChangeType.ADD)
{
addCommand.addFilepattern(diffEntry.getNewPath());
newFiles.add(diffEntry.getNewPath());
monitor.subTask(diffEntry.getNewPath());
}
else if(diffEntry.getChangeType()==ChangeType.MODIFY)
{
monitor.subTask(diffEntry.getOldPath());
changedFiles.add(diffEntry.getOldPath());
}
monitor.worked(0);
}
if(!newFiles.isEmpty())
addCommand.call();
changedFiles.addAll(newFiles);
monitor.done();
return changedFiles;
}
@Override
public void commit(PropertyFileDescriptor descriptor, IProgressMonitor monitor) throws TeamProviderException {
// TODO Auto-generated method stub
}
@Override
public Collection<PropertyFileDiff> reset(ProjectVersion project, IProgressMonitor monitor) throws TeamProviderException {
List<PropertyFileDiff> updatedFiles = new ArrayList<PropertyFileDiff>();
try {
Repository repository = createRepository(project);
SubMonitor subMon = SubMonitor.convert(monitor, "Reset", 100);
Git git = new Git(repository);
subMon.subTask("Calculating Diff");
DiffCommand diffCommand = git.diff();
diffCommand.setProgressMonitor(new ProgressMonitorWrapper(subMon.newChild(30)));
diffCommand.setOldTree(prepareTreeParser(repository, "refs/remotes/origin/"+project.getName()));
diffCommand.setNewTree(null);
List<DiffEntry> diffs = diffCommand.call();
for (DiffEntry diffEntry : diffs) {
checkCanceled(monitor);
PropertyFileDiff fileDiff = createDiff(diffEntry, monitor);
revertDiff(fileDiff);
updatedFiles.add(fileDiff);
}
subMon.subTask("Executing Reset");
ResetCommand reset = git.reset();
reset.setMode(ResetType.HARD);
reset.setRef("refs/remotes/origin/"+project.getName());
reset.call();
CleanCommand clean = git.clean();
clean.setCleanDirectories(true);
Set<String> call = clean.call();
LOGGER.info("cleaned "+call);
} catch (IOException e) {
LOGGER.error("reset failed",e);
throw new TeamProviderException(e);
} catch (GitAPIException e) {
LOGGER.error("reset failed",e);
throw new TeamProviderException(e);
}
return updatedFiles;
}
private PropertyFileDiff createDiff(DiffEntry diffEntry, IProgressMonitor monitor) {
PropertyFileDiff fileDiff = PropertiesFactory.eINSTANCE.createPropertyFileDiff();
switch (diffEntry.getChangeType()) {
case ADD:
monitor.subTask(diffEntry.getNewPath());
fileDiff.setKind(DiffKind.ADD);
fileDiff.setNewPath(diffEntry.getNewPath());
return fileDiff;
case COPY:
monitor.subTask(diffEntry.getNewPath());
fileDiff.setKind(DiffKind.COPY);
fileDiff.setNewPath(diffEntry.getNewPath());
fileDiff.setOldPath(diffEntry.getOldPath());
return fileDiff;
case DELETE:
monitor.subTask(diffEntry.getOldPath());
fileDiff.setKind(DiffKind.REMOVE);
fileDiff.setOldPath(diffEntry.getOldPath());
return fileDiff;
case MODIFY:
monitor.subTask(diffEntry.getNewPath());
fileDiff.setKind(DiffKind.MODIFY);
fileDiff.setOldPath(diffEntry.getOldPath());
fileDiff.setNewPath(diffEntry.getNewPath());
return fileDiff;
case RENAME:
monitor.subTask(diffEntry.getNewPath());
fileDiff.setKind(DiffKind.MOVE);
fileDiff.setOldPath(diffEntry.getOldPath());
fileDiff.setNewPath(diffEntry.getOldPath());
return fileDiff;
default:
break;
}
return fileDiff;
}
/**
* inverts the direction of a diff
* @param diff
*/
private void revertDiff(PropertyFileDiff diff) {
String path1 = diff.getNewPath();
String path2 = diff.getOldPath();
diff.setOldPath(path1);
diff.setNewPath(path2);
switch (diff.getKind()) {
case REMOVE:
diff.setKind(DiffKind.ADD);
break;
case ADD:
diff.setKind(DiffKind.REMOVE);
break;
default:
break;
}
}
private static AbstractTreeIterator prepareTreeParser(Repository repository, String ref) throws IOException, MissingObjectException,
IncorrectObjectTypeException {
// from the commit we can build the tree which allows us to construct
// the TreeParser
Ref head = repository.getRef(ref);
RevWalk walk = new RevWalk(repository);
RevCommit commit = walk.parseCommit(head.getObjectId());
RevTree tree = walk.parseTree(commit.getTree().getId());
CanonicalTreeParser oldTreeParser = new CanonicalTreeParser();
ObjectReader oldReader = repository.newObjectReader();
try {
oldTreeParser.reset(oldReader, tree.getId());
} finally {
oldReader.release();
}
return oldTreeParser;
}
public static void main(String[] args) throws IOException, JGitInternalException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException, JSchException {
Workspace workspace = PropertiesFactory.eINSTANCE.createWorkspace();
workspace.setRoot(URI.createFileURI(new File("target/test").getAbsolutePath()));
Project project = PropertiesFactory.eINSTANCE.createProject();
project.setName("jabylon3");
workspace.getChildren().add(project);
ProjectVersion version = PropertiesFactory.eINSTANCE.createProjectVersion();
version.setName("master");
project.getChildren().add(version);
GitTeamProvider provider = new GitTeamProvider();
provider.commit(version, null);
}
private CredentialsProvider createCredentialsProvider(Project project)
{
Preferences node = PreferencesUtil.scopeFor(project);
String username = node.get(GitConstants.KEY_USERNAME, "");
String password = node.get(GitConstants.KEY_PASSWORD, "");
return new UsernamePasswordCredentialsProvider(username, password) {
@Override
public boolean supports(CredentialItem... items) {
for (CredentialItem credentialItem : items) {
if (credentialItem instanceof CredentialItem.YesNoType) {
return true;
}
else
return super.supports(credentialItem);
}
return super.supports(items);
}
@Override
public boolean get(URIish uri, CredentialItem... items) throws UnsupportedCredentialItem {
List<CredentialItem> remaining = new ArrayList<CredentialItem>(Arrays.asList(items));
for (CredentialItem item : items) {
if (item instanceof CredentialItem.YesNoType) {
//unknown host warning
remaining.remove(item);
CredentialItem.YesNoType yesNo = (CredentialItem.YesNoType) item;
yesNo.setValue(true);
}
}
return super.get(uri, remaining.toArray(new CredentialItem[0]));
}
};
}
private TransportConfigCallback createTransportConfigCallback(Project project) {
Preferences node = PreferencesUtil.scopeFor(project);
final String username = node.get(GitConstants.KEY_USERNAME, "");
final String password = node.get(GitConstants.KEY_PASSWORD, "");
final String privateKey = node.get(GitConstants.KEY_PRIVATE_KEY, null);
final SshSessionFactory sshSessionFactory = new JschConfigSessionFactory() {
@Override
protected void configure(Host host, Session session) {
// do nothing
}
protected JSch createDefaultJSch(FS fs) throws JSchException {
JSch defaultJSch = super.createDefaultJSch(fs);
if(privateKey!=null) {
byte[] passphrase = null;
if(password!=null && !password.trim().isEmpty())
passphrase = password.getBytes(StandardCharsets.UTF_8);
defaultJSch.addIdentity(username, privateKey.getBytes(), null, passphrase);
}
return defaultJSch;
}
};
return new TransportConfigCallback() {
@Override
public void configure(Transport transport) {
SshTransport sshTransport = (SshTransport) transport;
sshTransport.setSshSessionFactory(sshSessionFactory);
}
};
}
private URI stripUserInfo(URI uri)
{
if(uri.userInfo()!=null && uri.userInfo().length()>0)
{
String userInfo = uri.userInfo();
URI strippedUri = URI.createHierarchicalURI(uri.scheme(), uri.authority().replace(userInfo+"@", ""), uri.device(), uri.segments(), uri.query(), uri.fragment());
return strippedUri;
}
return uri;
}
}