/*
documentr - Edit, maintain, and present software documentation on the web.
Copyright (C) 2012-2013 Maik Schreiber
This program 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.
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.blizzy.documentr.page;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.SortedMap;
import java.util.regex.Pattern;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.CherryPickResult;
import org.eclipse.jgit.api.CherryPickResult.CherryPickStatus;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.ResetCommand;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.PersonIdent;
import org.gitective.core.CommitUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.eventbus.EventBus;
import de.blizzy.documentr.DocumentrConstants;
import de.blizzy.documentr.access.User;
import de.blizzy.documentr.repository.IGlobalRepositoryManager;
import de.blizzy.documentr.repository.ILockedRepository;
import de.blizzy.documentr.repository.RepositoryUtil;
import de.blizzy.documentr.util.Util;
@Component
class CherryPicker implements ICherryPicker {
private static final Pattern CONFLICT_MARKERS_RE = Pattern.compile(
"^.*?[\\r\\n]<<<<<<< .*?[\\r\\n]=======.*?[\\r\\n]>>>>>>> .*$", Pattern.DOTALL + Pattern.MULTILINE); //$NON-NLS-1$
@Autowired
private IGlobalRepositoryManager globalRepositoryManager;
@Autowired
private EventBus eventBus;
@Autowired
private IPageStore pageStore;
@Autowired
private MessageSource messageSource;
@Override
public SortedMap<String, List<CommitCherryPickResult>> cherryPick(String projectName, String branchName, String path,
List<String> commits, Set<String> targetBranches, Set<CommitCherryPickConflictResolve> conflictResolves,
boolean dryRun, User user, Locale locale) throws IOException {
Assert.hasLength(projectName);
Assert.hasLength(path);
Assert.notEmpty(commits);
Assert.notEmpty(targetBranches);
Assert.notNull(conflictResolves);
Assert.notNull(user);
// always do a dry run first and return early if it fails
if (!dryRun) {
SortedMap<String, List<CommitCherryPickResult>> results = cherryPick(
projectName, branchName, path, commits, targetBranches, conflictResolves, true, user, locale);
for (List<CommitCherryPickResult> branchResults : results.values()) {
for (CommitCherryPickResult result : branchResults) {
if (result.getStatus() != CommitCherryPickResult.Status.OK) {
return results;
}
}
}
}
try {
SortedMap<String, List<CommitCherryPickResult>> results = Maps.newTreeMap();
for (String targetBranch : targetBranches) {
List<CommitCherryPickResult> branchResults = cherryPick(
projectName, branchName, path, commits, targetBranch, conflictResolves, dryRun, user, locale);
results.put(targetBranch, branchResults);
}
return results;
} catch (GitAPIException e) {
throw new IOException(e);
}
}
private List<CommitCherryPickResult> cherryPick(String projectName, String branchName, String path, List<String> commits,
String targetBranch, Set<CommitCherryPickConflictResolve> conflictResolves, boolean dryRun, User user,
Locale locale) throws IOException, GitAPIException {
ILockedRepository repo = null;
List<CommitCherryPickResult> cherryPickResults = Lists.newArrayList();
boolean hadConflicts = false;
boolean failed = false;
try {
repo = globalRepositoryManager.getProjectBranchRepository(projectName, targetBranch);
String tempBranchName = "_temp_" + String.valueOf((long) (Math.random() * Long.MAX_VALUE)); //$NON-NLS-1$
Git git = Git.wrap(repo.r());
git.branchCreate()
.setName(tempBranchName)
.setStartPoint(targetBranch)
.call();
git.checkout()
.setName(tempBranchName)
.call();
for (String commit : commits) {
PageVersion pageVersion = PageUtil.toPageVersion(CommitUtils.getCommit(repo.r(), commit));
if (!hadConflicts) {
CommitCherryPickResult singleCherryPickResult =
cherryPick(repo, branchName, path, pageVersion, targetBranch, conflictResolves, user, locale);
if (singleCherryPickResult != null) {
cherryPickResults.add(singleCherryPickResult);
if (singleCherryPickResult.getStatus() == CommitCherryPickResult.Status.CONFLICT) {
hadConflicts = true;
}
} else {
failed = true;
break;
}
} else {
cherryPickResults.add(new CommitCherryPickResult(pageVersion, CommitCherryPickResult.Status.UNKNOWN));
}
}
if (hadConflicts || failed) {
git.reset()
.setMode(ResetCommand.ResetType.HARD)
.call();
}
git.checkout()
.setName(targetBranch)
.call();
if (!dryRun && !hadConflicts && !failed) {
git.merge()
.include(repo.r().resolve(tempBranchName))
.call();
}
git.branchDelete()
.setBranchNames(tempBranchName)
.setForce(true)
.call();
if (failed) {
throw new IOException("cherry-picking failed"); //$NON-NLS-1$
}
} finally {
Util.closeQuietly(repo);
}
if (!dryRun && !hadConflicts && !failed) {
eventBus.post(new PageChangedEvent(projectName, targetBranch, path));
}
return cherryPickResults;
}
private CommitCherryPickResult cherryPick(ILockedRepository repo, String branchName, String path, PageVersion pageVersion,
String targetBranch, Set<CommitCherryPickConflictResolve> conflictResolves, User user, Locale locale)
throws IOException, GitAPIException {
CommitCherryPickResult cherryPickResult;
CherryPickResult result = Git.wrap(repo.r()).cherryPick()
.include(repo.r().resolve(pageVersion.getCommitName()))
.call();
CherryPickStatus status = result.getStatus();
switch (status) {
case OK:
cherryPickResult = new CommitCherryPickResult(pageVersion, CommitCherryPickResult.Status.OK);
break;
case CONFLICTING:
cherryPickResult = tryResolveConflict(repo, branchName, path, pageVersion, targetBranch, conflictResolves,
user, locale);
break;
default:
cherryPickResult = null;
break;
}
return cherryPickResult;
}
private CommitCherryPickResult tryResolveConflict(ILockedRepository repo, String branchName, String path,
PageVersion pageVersion, String targetBranch, Set<CommitCherryPickConflictResolve> conflictResolves,
User user, Locale locale) throws IOException, GitAPIException {
File workingDir = RepositoryUtil.getWorkingDir(repo.r());
File pagesDir = new File(workingDir, DocumentrConstants.PAGES_DIR_NAME);
File workingFile = Util.toFile(pagesDir, path + DocumentrConstants.PAGE_SUFFIX);
String resolveText = getCherryPickConflictResolveText(conflictResolves, targetBranch, pageVersion.getCommitName());
CommitCherryPickResult result;
if (resolveText != null) {
if (!CONFLICT_MARKERS_RE.matcher("\n" + resolveText).matches()) { //$NON-NLS-1$
FileUtils.writeStringToFile(workingFile, resolveText, Charsets.UTF_8.name());
Git git = Git.wrap(repo.r());
git.add()
.addFilepattern(DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX) //$NON-NLS-1$
.call();
PersonIdent ident = new PersonIdent(user.getLoginName(), user.getEmail());
git.commit()
.setAuthor(ident)
.setCommitter(ident)
.setMessage(DocumentrConstants.PAGES_DIR_NAME + "/" + path + DocumentrConstants.PAGE_SUFFIX) //$NON-NLS-1$
.call();
result = new CommitCherryPickResult(pageVersion, CommitCherryPickResult.Status.OK);
} else {
result = new CommitCherryPickResult(pageVersion, resolveText);
}
} else {
String text = FileUtils.readFileToString(workingFile, Charsets.UTF_8);
text = StringUtils.replace(text, "<<<<<<< OURS", //$NON-NLS-1$
"<<<<<<< " + messageSource.getMessage("targetBranchX", new Object[] { targetBranch }, locale)); //$NON-NLS-1$ //$NON-NLS-2$
text = StringUtils.replace(text, ">>>>>>> THEIRS", //$NON-NLS-1$
">>>>>>> " + messageSource.getMessage("sourceBranchX", new Object[] { branchName }, locale)); //$NON-NLS-1$ //$NON-NLS-2$
result = new CommitCherryPickResult(pageVersion, text);
}
return result;
}
private String getCherryPickConflictResolveText(Set<CommitCherryPickConflictResolve> conflictResolves,
String targetBranch, String commit) {
for (CommitCherryPickConflictResolve resolve : conflictResolves) {
if (resolve.isApplicable(targetBranch, commit)) {
return resolve.getText();
}
}
return null;
}
@Override
public List<String> getCommitsList(String projectName, String branchName, String path,
String version1, String version2) throws IOException {
List<PageVersion> pageVersions = Lists.newArrayList(pageStore.listPageVersions(projectName, branchName, path));
boolean foundVersion1 = false;
boolean foundVersion2 = false;
for (PageVersion pageVersion : pageVersions) {
String commit = pageVersion.getCommitName();
if (!foundVersion1 && commit.equals(version1)) {
foundVersion1 = true;
}
if (!foundVersion2 && commit.equals(version2)) {
foundVersion2 = true;
}
if (foundVersion1 && foundVersion2) {
break;
}
}
if (!foundVersion1 || !foundVersion2) {
throw new IllegalArgumentException("one of version1 or version2 not found in version history of page"); //$NON-NLS-1$
}
Collections.reverse(pageVersions);
boolean include = false;
for (Iterator<PageVersion> iter = pageVersions.iterator(); iter.hasNext();) {
PageVersion version = iter.next();
if (!include) {
iter.remove();
}
String commit = version.getCommitName();
if (commit.equals(version1)) {
include = true;
} else if (commit.equals(version2)) {
include = false;
}
}
Function<PageVersion, String> function = new Function<PageVersion, String>() {
@Override
public String apply(PageVersion version) {
return version.getCommitName();
}
};
return Lists.transform(pageVersions, function);
}
}