package com.hdweiss.morgand.synchronizer.git;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;
import com.hdweiss.morgand.utils.FileUtils;
import com.hdweiss.morgand.utils.Utils;
import org.eclipse.jgit.api.CloneCommand;
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.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.ProgressMonitor;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.merge.MergeStrategy;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshSessionFactory;
import org.eclipse.jgit.transport.URIish;
import java.io.File;
import java.net.URISyntaxException;
public class JGitWrapper {
private Git git;
private final String localPath;
private final String remotePath;
private final String branch;
private final String commitAuthor;
private final String commitEmail;
private CredentialsProvider credentialsProvider;
private MergeStrategy mergeStrategy;
// TODO Externalize strings
public JGitWrapper(SharedPreferences preferences) throws Exception {
localPath = preferences.getString("git_local_path", "");
if (TextUtils.isEmpty(localPath))
throw new IllegalArgumentException("Must specify local git path");
String url = preferences.getString("git_url", "");
if (TextUtils.isEmpty(url))
throw new IllegalArgumentException("Must specify remote git url");
try {
URIish urIish = new URIish(url);
if (urIish.getUser() == null) {
String username = preferences.getString("git_username", "");
urIish = urIish.setUser(username);
}
remotePath = urIish.toString();
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid remote git url");
}
branch = preferences.getString("git_branch", "master");
if (branch.isEmpty())
throw new IllegalArgumentException("Must specify a git branch");
commitAuthor = preferences.getString("git_commit_author", "");
commitEmail = preferences.getString("git_commit_email", "");
String mergeStrategyString = preferences.getString("git_merge_strategy", "theirs");
mergeStrategy = MergeStrategy.get(mergeStrategyString);
if (mergeStrategy == null)
throw new IllegalArgumentException("Invalid merge strategy: " + mergeStrategyString);
setupJGitAuthentication(preferences);
}
private void setupJGitAuthentication(SharedPreferences preferences) {
String username = preferences.getString("git_username", "");
String password = preferences.getString("git_password", "");
String keyLocation = preferences.getString("git_key_path", "");
JGitConfigSessionFactory session = new JGitConfigSessionFactory(username, password, keyLocation);
SshSessionFactory.setInstance(session);
credentialsProvider = new JGitCredentialsProvider(username, password);
}
private Git initGitRepo(ProgressMonitor monitor) throws Exception {
if (new File(localPath).exists() == false)
createNewRepo(monitor);
FileRepository fileRepository = new FileRepository(localPath + "/.git");
return new Git(fileRepository);
}
private void createNewRepo(ProgressMonitor monitor) throws GitAPIException, IllegalArgumentException {
File localRepo = new File(localPath);
if (localRepo.exists()) // Safety check so we don't accidentally delete directory
throw new IllegalStateException("Directory already exists");
try {
CloneCommand cloneCommand = Git.cloneRepository()
.setCredentialsProvider(credentialsProvider)
.setURI(remotePath)
.setBranch(branch)
.setDirectory(localRepo)
.setBare(false);
if (monitor != null)
cloneCommand.setProgressMonitor(monitor);
cloneCommand.call();
} catch (GitAPIException e) {
FileUtils.deleteDirectory(localRepo);
throw e;
}
}
public Git getGit(ProgressMonitor monitor) throws Exception {
if (this.git == null)
this.git = initGitRepo(monitor);
boolean hasConflicts = false;
try {
hasConflicts = this.git.status().call().getConflicting().isEmpty() == false;
} catch (Exception ex) {}
if (hasConflicts)
throw new IllegalStateException("Unresolved conflict(s) in git repository");
return this.git;
}
public void commitAllChanges(String commitMessage) throws Exception {
Git git = getGit(null);
Status status = git.status().call();
if (status.getModified().isEmpty()) {
Log.d("JGitWrapper", "No files modified, not committing");
return;
}
git.add().addFilepattern(".").call();
git.commit().setMessage(commitMessage).setAuthor(commitAuthor, commitEmail).call();
Log.d("JGitWrapper", "Committed changes");
}
public void updateChanges(ProgressMonitor monitor) throws Exception {
Git git = getGit(monitor);
FetchCommand fetch = git.fetch();
fetch.setCredentialsProvider(credentialsProvider);
if (monitor != null)
fetch.setProgressMonitor(monitor);
fetch.call();
SyncState state = getSyncState(git);
Ref fetchHead = git.getRepository().getRef("FETCH_HEAD");
switch (state) {
case Equal:
// Do nothing
Log.d("Git", "Local branch is up-to-date");
break;
case Ahead:
Log.d("Git", "Local branch ahead, pushing changes to remote");
git.push().setCredentialsProvider(credentialsProvider).setRemote(remotePath).call();
break;
case Behind:
Log.d("Git", "Local branch behind, fast forwarding changes");
MergeResult result = git.merge().include(fetchHead).setFastForward(MergeCommand.FastForwardMode.FF_ONLY).call();
if (result.getMergeStatus().isSuccessful() == false)
throw new IllegalStateException("Fast forward failed on behind merge");
break;
case Diverged:
Log.d("Git", "Branches are diverged, merging with strategy " + mergeStrategy.getName());
MergeResult mergeResult = git.merge().include(fetchHead).setStrategy(mergeStrategy).call();
if (mergeResult.getMergeStatus().isSuccessful()) {
git.push().setCredentialsProvider(credentialsProvider).setRemote(remotePath).call();
} else
throw new IllegalStateException("Merge failed for diverged branches using strategy " + mergeStrategy.getName());
break;
}
}
private enum SyncState {
Equal, Ahead, Behind, Diverged
}
private SyncState getSyncState(Git git) throws Exception {
Ref fetchHead = git.getRepository().getRef("FETCH_HEAD");
Ref head = git.getRepository().getRef("HEAD");
if (fetchHead == null)
throw new IllegalStateException("fetchHead not found!");
if (head == null)
throw new IllegalStateException("head not found!");
Iterable<RevCommit> call = git.log().addRange(fetchHead.getObjectId(), head.getObjectId()).call();
int originToHead = Utils.getIteratorSize(call.iterator());
Iterable<RevCommit> call2 = git.log().addRange(head.getObjectId(), fetchHead.getObjectId()).call();
int headToOrigin = Utils.getIteratorSize(call2.iterator());
Log.d("JGitWrapper", "fetchHead->head: " + originToHead + " head->fetchHead: " + headToOrigin);
if (originToHead == 0 && headToOrigin == 0)
return SyncState.Equal;
else if (originToHead == 0)
return SyncState.Behind;
else if (headToOrigin == 0)
return SyncState.Ahead;
else
return SyncState.Diverged;
}
public void cleanup() {
if (git != null)
git.close();
}
}