/*
* Copyright 2011 Ronald Kurniawan.
*
* This file is part of CodeTraq.
*
* CodeTraq is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* CodeTraq is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with CodeTraq. If not, see <http://www.gnu.org/licenses/>.
*/
package net.mobid.codetraq.runnables;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Level;
import net.mobid.codetraq.VersionControlChecker;
import net.mobid.codetraq.VersionControlType;
import net.mobid.codetraq.persistence.ServerDTO;
import net.mobid.codetraq.persistence.ServerRevision;
import net.mobid.codetraq.persistence.UserDTO;
import net.mobid.codetraq.persistence.UserRevision;
import net.mobid.codetraq.utils.DbUtility;
import net.mobid.codetraq.utils.LogService;
import net.mobid.codetraq.utils.Utilities;
import org.eclipse.jgit.api.CheckoutCommand;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.MergeCommand;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.PullResult;
import org.eclipse.jgit.lib.ConfigConstants;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.TextProgressMonitor;
import org.eclipse.jgit.storage.file.FileRepository;
import org.eclipse.jgit.transport.FetchResult;
import org.eclipse.jgit.transport.TrackingRefUpdate;
/**
* This is a worker class whose purpose is to monitor changes on a given git server.
* A local repository would be created and the project would be cloned. If a local
* repository already exists, then a "pull" is performed. Revision comparison is
* then performed locally.
*
* @author Ronald Kurniawan
* @version 0.1
*/
public class GitChecker extends VersionControlChecker implements Runnable {
private FileRepository repo = null;
private Git mGit = null;
/**
* Creates a new GitChecker.
* @param server - a <code>ServerDTO</code> instance
* @param user - a <code>UserDTO</code> instance
* @param db - a <code>DbUtility</code> instance
*/
public GitChecker(ServerDTO server, UserDTO user, DbUtility db) {
super(server, user, db);
}
/**
* Checks the git repository directory, clone a project if local repository
* does not exist, otherwise do a pull to update to latest repository.
*/
public void run() {
// we need to make sure that a project repository dir can be created or
// already exists
Utilities.createGitProjectDir(_server.getShortName());
File gitProjectDir = new File("gitrepos/" + _server.getShortName());
if (!gitProjectDir.exists()) {
System.out.printf("Cannot create local repositories for server %s. Please check read/write access daemon directory.%n",
_server.getServerAddress());
return;
}
// try to perform clone if it's a fresh repo, otherwise do a pull
File dotGitPath = new File("gitrepos/" + _server.getShortName() + "/.git");
if (!dotGitPath.exists()) {
clone("gitrepos/" + _server.getShortName());
} else {
pull("gitrepos/" + _server.getShortName() + "/.git");
}
try {
compareLatestRevisionHistory();
} catch (Exception ex) {
LogService.getLogger(SvnChecker.class.getName()).log(Level.SEVERE, null, ex);
LogService.writeLog(Level.SEVERE, ex);
}
}
/**
* Checks if user already has the latest revision on records. If user's version of the revision
* is older than the version the server monitor has, we update the user's revision and send
* a new message to user.
* @return <code>true</code> if user already has latest revision, <code>false</code>
* otherwise
* @throws Exception
*/
@Override
public boolean compareLatestRevisionHistory() throws Exception {
checkServerInUserRecord();
UserRevision ur = _db.getUserLatestRevision(_server.getServerAddress(), _user.getId());
ServerRevision sr = _db.getServerRevisionByAddress(_server.getServerAddress());
if (sr == null) {
throw new Exception("Cannot find server " + _server.getServerAddress() +
" in the server revision list.");
}
if (sr.getVersionControlType() == VersionControlType.GIT) {
if (ur.getLastRevisionId() == null || (!ur.getLastRevisionId().equals(sr.getLastRevisionId()))) {
LogService.writeMessage("Updating user revision for " + ur.getServerAddress() +
"(owner " + ur.getOwner() + ")");
// update the UserRevision object
ur.setLastRevisionId(sr.getLastRevisionId());
_db.updateUserLatestRevision(ur);
sendRevisionMessage(sr);
return true;
}
}
return false;
}
/*
* Does a pull from a GIT repository.
* @param path - path to local repository
*/
private void pull(String path) {
LogService.writeMessage("GitChecker is trying to do a pull from " +
_server.getServerAddress());
//System.out.printf("GitChecker is trying to do a pull from %s%n", _server.getServerAddress());
try {
File gitDir = new File(path);
repo = new FileRepository(gitDir);
if (mGit == null) {
mGit = new Git(repo);
}
if (mGit.getRepository().getFullBranch() == null ||
Utilities.isHexString(mGit.getRepository().getFullBranch())) {
attachHead(mGit, _server.getServerBranch());
}
PullCommand puller = mGit.pull();
puller.setTimeout(60);
puller.setProgressMonitor(new TextProgressMonitor());
PullResult pullResult = puller.call();
if (pullResult != null) {
LogService.writeMessage("GitChecker has something to pull from " +
_server.getServerAddress());
FetchResult result = pullResult.getFetchResult();
if (result.getTrackingRefUpdates().isEmpty()) {
return;
}
showFetchResult(result, true);
} else {
LogService.writeMessage("GitChecker did not find anything to pull from " +
_server.getServerAddress());
}
} catch(Exception ex) {
LogService.getLogger(GitChecker.class.getName()).log(Level.SEVERE, null, ex);
LogService.writeLog(Level.SEVERE, ex);
}
}
/*
* Clones a GIT repository.
* @param path - path to local repository
*/
private void clone(String path) {
LogService.writeMessage("GitChecker is trying to do a clone from " +
_server.getServerAddress());
System.out.printf("GitChecker is trying to do a clone from %s%n", _server.getServerAddress());
try {
File gitDir = new File(path);
CloneCommand cloner = new CloneCommand();
cloner.setBare(false);
cloner.setDirectory(gitDir);
cloner.setProgressMonitor(new TextProgressMonitor());
cloner.setRemote("origin");
cloner.setURI(_server.getServerAddress());
mGit = cloner.call();
// for some reason, repository cloned with jgit always has HEAD detached.
// we need to create a "temporary" branch, then create a "master" branch.
// we then merge the two...
if (!isMasterBranchDefined(mGit.getRepository())) {
// save the remote and merge config values
mGit.getRepository().getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
_server.getServerBranch(), ConfigConstants.CONFIG_KEY_REMOTE, "origin");
mGit.getRepository().getConfig().setString(ConfigConstants.CONFIG_BRANCH_SECTION,
_server.getServerBranch(), ConfigConstants.CONFIG_KEY_MERGE, _server.getServerBranch());
mGit.getRepository().getConfig().save();
}
if (mGit.getRepository().getFullBranch() == null ||
Utilities.isHexString(mGit.getRepository().getFullBranch())) {
// HEAD is detached and we need to reattach it
attachHead(mGit, _server.getServerBranch());
}
} catch(Exception ex) {
LogService.getLogger(GitChecker.class.getName()).log(Level.SEVERE, null, ex);
LogService.writeLog(Level.SEVERE, ex);
}
}
/*
* Checks whether master branch is defined in local repository's configuration.
* @param r - a GIT repository
* @return <code>true</code> if master branch is defined, <code>false</code>
* otherwise
*/
private boolean isMasterBranchDefined(Repository r) {
if (r.getConfig().getString(ConfigConstants.CONFIG_BRANCH_SECTION,
_server.getServerBranch(), ConfigConstants.CONFIG_KEY_REMOTE) == null) {
return false;
}
return true;
}
/*
* Attaches a "detached" HEAD on a local GIT repository.
* @param g - a <code>Git</code> object
* @param branch - branch name
*/
private void attachHead(Git g, String branch) {
LogService.writeMessage("Trying to attach HEAD to " + g.getRepository().toString());
try {
CheckoutCommand temp = g.checkout();
temp.setCreateBranch(true);
temp.setName("temp");
Ref tRef = temp.call();
CheckoutCommand b = g.checkout();
b.setName(branch);
b.setCreateBranch(true);
b.call();
MergeCommand merge = g.merge();
merge.include(tRef);
merge.call();
} catch (Exception ex) {
LogService.getLogger(GitChecker.class.getName()).log(Level.SEVERE, null, ex);
LogService.writeLog(Level.SEVERE, ex);
}
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand and modified to suit our need.
*/
private void showFetchResult(final FetchResult r, boolean logOnly) {
ObjectReader reader = repo.newObjectReader();
PrintWriter out = new PrintWriter(System.out);
try {
boolean shownURI = false;
for (final TrackingRefUpdate u : r.getTrackingRefUpdates()) {
if (u.getResult() == RefUpdate.Result.NO_CHANGE) {
continue;
}
final char type = shortTypeOf(u.getResult());
final String longType = longTypeOf(reader, u);
final String src = abbreviateRef(u.getRemoteName(), false);
final String dst = abbreviateRef(u.getLocalName(), true);
if (!shownURI) {
out.println("jGIT::from " + r.getURI());
shownURI = true;
}
if (!logOnly) {
out.format(" %c %-17s %-10s -> %s", type, longType, src, dst);
out.println();
}
}
} finally {
reader.release();
}
showRemoteMessages(r.getMessages());
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand and modified to suit our
* need.
*/
static void showRemoteMessages(String pkt) {
PrintWriter writer = new PrintWriter(System.err);
while (0 < pkt.length()) {
final int lf = pkt.indexOf('\n');
final int cr = pkt.indexOf('\r');
final int s;
if (0 <= lf && 0 <= cr)
s = Math.min(lf, cr);
else if (0 <= lf)
s = lf;
else if (0 <= cr)
s = cr;
else {
writer.print("jGIT::remote reply: " + pkt);
writer.println();
break;
}
if (pkt.charAt(s) == '\r') {
writer.print("jGIT::remote reply: " + pkt.substring(0, s));
writer.print('\r');
} else {
writer.print("jGIT::remote reply: " + pkt.substring(0, s));
writer.println();
}
pkt = pkt.substring(s + 1);
}
writer.flush();
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand.
*/
private String longTypeOf(ObjectReader reader, final TrackingRefUpdate u) {
final RefUpdate.Result r = u.getResult();
if (r == RefUpdate.Result.LOCK_FAILURE)
return "[lock fail]";
if (r == RefUpdate.Result.IO_FAILURE)
return "[i/o error]";
if (r == RefUpdate.Result.REJECTED)
return "[rejected]";
if (ObjectId.zeroId().equals(u.getNewObjectId()))
return "[deleted]";
if (r == RefUpdate.Result.NEW) {
if (u.getRemoteName().startsWith(Constants.R_HEADS))
return "[new branch]";
else if (u.getLocalName().startsWith(Constants.R_TAGS))
return "[new tag]";
return "[new]";
}
if (r == RefUpdate.Result.FORCED) {
final String aOld = safeAbbreviate(reader, u.getOldObjectId());
final String aNew = safeAbbreviate(reader, u.getNewObjectId());
return aOld + "..." + aNew;
}
if (r == RefUpdate.Result.FAST_FORWARD) {
final String aOld = safeAbbreviate(reader, u.getOldObjectId());
final String aNew = safeAbbreviate(reader, u.getNewObjectId());
return aOld + ".." + aNew;
}
if (r == RefUpdate.Result.NO_CHANGE)
return "[up to date]";
return "[" + r.name() + "]";
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand.
*/
private String safeAbbreviate(ObjectReader reader, ObjectId id) {
try {
return reader.abbreviate(id).name();
} catch (IOException cannotAbbreviate) {
return id.name();
}
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand.
*/
String abbreviateRef(String dst, boolean abbreviateRemote) {
if (dst.startsWith(Constants.R_HEADS))
dst = dst.substring(Constants.R_HEADS.length());
else if (dst.startsWith(Constants.R_TAGS))
dst = dst.substring(Constants.R_TAGS.length());
else if (abbreviateRemote && dst.startsWith(Constants.R_REMOTES))
dst = dst.substring(Constants.R_REMOTES.length());
return dst;
}
/*
* This method is borrowed from org.eclipse.jgit.pgm.AbstractFetchCommand.
*/
private static char shortTypeOf(final RefUpdate.Result r) {
if (r == RefUpdate.Result.LOCK_FAILURE)
return '!';
if (r == RefUpdate.Result.IO_FAILURE)
return '!';
if (r == RefUpdate.Result.NEW)
return '*';
if (r == RefUpdate.Result.FORCED)
return '+';
if (r == RefUpdate.Result.FAST_FORWARD)
return ' ';
if (r == RefUpdate.Result.REJECTED)
return '!';
if (r == RefUpdate.Result.NO_CHANGE)
return '=';
return ' ';
}
}