commitId,
commitId);
+ Map<String, String> attributes = builder.getAttributes();
+ if (!attributes.isEmpty())
+ result.setAttributes(attributes);
+
if (myCurrentCommit.getParentCount() > 0) {
for (RevCommit parent : myCurrentCommit.getParents()) {
parseBody(parent);
private final String currentVersion;
private final String parentVersion;
private final List<VcsChange> changes = new ArrayList<VcsChange>();
+ private final Map<String, String> myAttributes = new HashMap<>();
private final String repositoryDebugInfo = myGitRoot.debugInfo();
private final IgnoreSubmoduleErrorsTreeFilter filter = new IgnoreSubmoduleErrorsTreeFilter(myGitRoot);
private final Map<String, RevCommit> commitsWithFix = new HashMap<String, RevCommit>();
return changes;
}
+ @NotNull
+ public Map<String, String> getAttributes() {
+ return myAttributes;
+ }
+
/**
* collect changes for the commit
*/
tw.setFilter(filter);
tw.setRecursive(true);
myContext.addTree(myGitRoot, tw, myRepository, commit, shouldIgnoreSubmodulesErrors());
- for (RevCommit parentCommit : commit.getParents()) {
+ RevCommit[] parents = commit.getParents();
+ boolean reportPerParentChangedFiles = myConfig.reportPerParentChangedFiles() && parents.length > 1; // report only for merge commits
+ for (RevCommit parentCommit : parents) {
myContext.addTree(myGitRoot, tw, myRepository, parentCommit, true);
+ if (reportPerParentChangedFiles) {
+ tw.reportChangedFilesForParentCommit(parentCommit);
+ }
}
new VcsChangesTreeWalker(tw).walk();
+
+ if (reportPerParentChangedFiles) {
+ Map<String, String> changedFilesAttributes = tw.buildChangedFilesAttributes();
+ if (!changedFilesAttributes.isEmpty()) {
+ myAttributes.putAll(changedFilesAttributes);
+ }
+ }
} finally {
tw.release();
}
return TeamCityProperties.getBooleanOrTrue("teamcity.git.treatMissingCommitAsRecoverableError");
}
+ @Override
+ public boolean reportPerParentChangedFiles() {
+ return TeamCityProperties.getBoolean("teamcity.git.reportPerParentChangedFiles");
+ }
+
@NotNull
@Override
public List<String> getRecoverableFetchErrorMessages() {
boolean treatMissingBranchTipAsRecoverableError();
+ boolean reportPerParentChangedFiles();
+
@NotNull
List<String> getRecoverableFetchErrorMessages();
}
import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.buildTriggers.vcs.git.submodules.IgnoreSubmoduleErrorsTreeFilter;
+import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.vcs.VcsChange;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
+import java.util.*;
+
/**
* @author dmitry.neverov
*/
private final String myRepositoryDebugInfo;
private final boolean myVerboseTreeWalkLog;
+ private final List<String> myParentCommits = new ArrayList<>(0);
+ private final Map<String, List<String>> myPerParentChangedFiles = new HashMap<>();
+
public VcsChangeTreeWalk(@NotNull ObjectReader repo,
@NotNull String repositoryDebugInfo,
boolean verboseTreeWalkLog) {
}
+ public void reportChangedFilesForParentCommit(@NotNull RevCommit parentCommit) {
+ myParentCommits.add(parentCommit.name());
+ myPerParentChangedFiles.put(parentCommit.name(), new ArrayList<>());
+ }
+
+
+ @NotNull
+ public Map<String, String> buildChangedFilesAttributes() {
+ if (myPerParentChangedFiles.isEmpty())
+ return Collections.emptyMap();
+ Map<String, String> result = new HashMap<>();
+ for (Map.Entry<String, List<String>> entry : myPerParentChangedFiles.entrySet()) {
+ String parentCommit = entry.getKey();
+ List<String> files = entry.getValue();
+ result.put("teamcity.transient.changedFiles." + parentCommit, StringUtil.join("\n", files));
+ }
+ return result;
+ }
+
+
@Nullable
VcsChange getVcsChange(String currentVersion, String parentVersion) {
final String path = getPathString();
final ChangeType gitChangeType = classifyChange();
+ fillPerParentChangedFiles(path);
if (isExtraDebug())
LOG.debug("Processing change " + treeWalkInfo(path) + " as " + gitChangeType + " " + myRepositoryDebugInfo);
}
+ private void fillPerParentChangedFiles(@NotNull String path) {
+ int treeCount = getTreeCount();
+ if (!myParentCommits.isEmpty() && myParentCommits.size() == treeCount - 1) {
+ for (int i = 1; i < treeCount; i++) {
+ if (!idEqual(0, i)) {
+ String parentCommit = myParentCommits.get(i - 1);
+ List<String> changedFiles = myPerParentChangedFiles.get(parentCommit);
+ if (changedFiles != null) {
+ changedFiles.add(path);
+ }
+ }
+ }
+ }
+ }
+
+
/**
* Classify change in tree walker. The first tree is assumed to be a current commit and other
* trees are assumed to be parent commits. In the case of multiple changes, the changes that
import jetbrains.buildServer.util.cache.ResetCacheHandler;
import jetbrains.buildServer.vcs.*;
import org.apache.log4j.Level;
+import org.assertj.core.data.MapEntry;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.MergeResult;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryBuilder;
+import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.URIish;
import org.jetbrains.annotations.NotNull;
}
+ public void report_per_parent_changed_files() throws Exception {
+ // 4 f1=2, f2=2
+ // |\
+ // | 3 f1=1, f2=2
+ // 2 | f1=2, f2=1
+ // |/
+ // 1 f1=1, f2=1
+
+ // setup repo
+ File repoDir = myTempFiles.createTempDir();
+ Git git = Git.init().setDirectory(repoDir).call();
+
+ File f1 = new File(repoDir, "f1");
+ File f2 = new File(repoDir, "f2");
+ FileUtil.writeFileAndReportErrors(f1, "1");
+ FileUtil.writeFileAndReportErrors(f2, "1");
+ git.add().addFilepattern(".").call();
+ RevCommit c1 = git.commit().setAll(true).setMessage("1").call();
+
+ FileUtil.writeFileAndReportErrors(f1, "2");
+ RevCommit c2 = git.commit().setAll(true).setMessage("2").call();
+
+ git.branchCreate().setName("branch1").setStartPoint(c1).call();
+ git.checkout().setName("branch1").call();
+ FileUtil.writeFileAndReportErrors(f2, "2");
+ RevCommit c3 = git.commit().setAll(true).setMessage("3").call();
+
+ git.checkout().setName("master").call();
+ MergeResult result = git.merge().include(c3).setCommit(true).setMessage("4").call();
+ String c4 = result.getNewHead().name();
+
+ myConfig.setReportPerParentChangedFiles(true);
+ ServerPluginConfig config = myConfig.build();
+ GitVcsSupport vcs = gitSupport().withPluginConfig(config).build();
+
+ // collect changes
+ VcsRoot root = vcsRoot().withFetchUrl(repoDir).build();
+
+ RepositoryStateData s1 = RepositoryStateData.createVersionState("refs/heads/master", map("refs/heads/master", c1.name()));
+ RepositoryStateData s2 = RepositoryStateData.createVersionState("refs/heads/master", map("refs/heads/master", c2.name()));
+ RepositoryStateData s3 = RepositoryStateData.createVersionState("refs/heads/master", map("refs/heads/master", c3.name()));
+ RepositoryStateData s23 = RepositoryStateData.createVersionState("refs/heads/master", map(
+ "refs/heads/master", c2.name(),
+ "refs/heads/branch1", c3.name()
+ ));
+ RepositoryStateData s4 = RepositoryStateData.createVersionState("refs/heads/master", map("refs/heads/master", c4));
+
+ ModificationData m2 = vcs.getCollectChangesPolicy().collectChanges(root, s1, s2, CheckoutRules.DEFAULT).get(0);
+ then(m2.getAttributes()).isEmpty();
+
+ ModificationData m3 = vcs.getCollectChangesPolicy().collectChanges(root, s1, s3, CheckoutRules.DEFAULT).get(0);
+ then(m3.getAttributes()).isEmpty();
+
+ ModificationData m4 = vcs.getCollectChangesPolicy().collectChanges(root, s23, s4, CheckoutRules.DEFAULT).get(0);
+ then(m4.getAttributes()).containsOnly(
+ MapEntry.entry("teamcity.transient.changedFiles." + c2.name(), "f2"),
+ MapEntry.entry("teamcity.transient.changedFiles." + c3.name(), "f1"));
+ }
+
+
private GitVcsSupport git() {
return gitSupport().withPluginConfig(myConfig).build();
}
private Boolean myIgnoreMissingRemoteRef;
private Integer myMergeRetryAttempts;
private Boolean myRunInPlaceGc;
+ private Boolean myReportPerParentChangedFiles;
public static PluginConfigBuilder pluginConfig() {
return new PluginConfigBuilder();
public List<String> getRecoverableFetchErrorMessages() {
return myDelegate.getRecoverableFetchErrorMessages();
}
+
+ @Override
+ public boolean reportPerParentChangedFiles() {
+ return myReportPerParentChangedFiles != null ? myReportPerParentChangedFiles : myDelegate.reportPerParentChangedFiles();
+ }
};
}
myRunInPlaceGc = runInPlaceGc;
return this;
}
+
+ PluginConfigBuilder setReportPerParentChangedFiles(boolean report) {
+ myReportPerParentChangedFiles = report;
+ return this;
+ }
}