Report per-parent changed files via attributes
authorDmitry Neverov <dmitry.neverov@gmail.com>
Sat, 20 Jan 2018 21:44:29 +0000 (22:44 +0100)
committerDmitry Neverov <dmitry.neverov@gmail.com>
Sat, 20 Jan 2018 21:44:29 +0000 (22:44 +0100)
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/ModificationDataRevWalk.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/PluginConfigImpl.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/ServerPluginConfig.java
git-server/src/jetbrains/buildServer/buildTriggers/vcs/git/VcsChangeTreeWalk.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/CollectChangesTest.java
git-tests/src/jetbrains/buildServer/buildTriggers/vcs/git/tests/PluginConfigBuilder.java

index 493face79cc252a26885b558a442f6d92b995882..2636a75035ba007f2cfc7d404e721d6677dc3371 100644 (file)
@@ -108,6 +108,10 @@ class ModificationDataRevWalk extends RevWalk {
       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);
@@ -147,6 +151,7 @@ class ModificationDataRevWalk extends RevWalk {
     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>();
@@ -169,6 +174,11 @@ class ModificationDataRevWalk extends RevWalk {
       return changes;
     }
 
+    @NotNull
+    public Map<String, String> getAttributes() {
+      return myAttributes;
+    }
+
     /**
      * collect changes for the commit
      */
@@ -178,11 +188,23 @@ class ModificationDataRevWalk extends RevWalk {
         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();
       }
index 61c92806ded4a88a2ca21d667ce34cbc2cde47d0..7c7876b8bcc56f6c5720e469e2d23445f6f14ef7 100644 (file)
@@ -560,6 +560,11 @@ public class PluginConfigImpl implements ServerPluginConfig {
     return TeamCityProperties.getBooleanOrTrue("teamcity.git.treatMissingCommitAsRecoverableError");
   }
 
+  @Override
+  public boolean reportPerParentChangedFiles() {
+    return TeamCityProperties.getBoolean("teamcity.git.reportPerParentChangedFiles");
+  }
+
   @NotNull
   @Override
   public List<String> getRecoverableFetchErrorMessages() {
index 2540d08b08e19cf0d0a7450a8a9e6a52eaff52da..490893f775a2e18604cae9e4d1d6e9f8e175a311 100644 (file)
@@ -161,6 +161,8 @@ public interface ServerPluginConfig extends PluginConfig {
 
   boolean treatMissingBranchTipAsRecoverableError();
 
+  boolean reportPerParentChangedFiles();
+
   @NotNull
   List<String> getRecoverableFetchErrorMessages();
 }
index 15a78b9f9ec8ceefcc1925fd9644c78a8175b2e0..63f88d2c60a1230342756d517d3dbee2c02f39b1 100644 (file)
@@ -18,15 +18,19 @@ package jetbrains.buildServer.buildTriggers.vcs.git;
 
 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
  */
@@ -37,6 +41,9 @@ public class VcsChangeTreeWalk extends TreeWalk {
   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) {
@@ -54,10 +61,31 @@ public class VcsChangeTreeWalk extends TreeWalk {
   }
 
 
+  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);
@@ -72,6 +100,22 @@ public class VcsChangeTreeWalk extends TreeWalk {
   }
 
 
+  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
index 6032aaddd57046689bf76dc6f5c4a9dece8437ab..e49f9d0b3032f1314d243502114b997d9a199af4 100644 (file)
@@ -26,10 +26,14 @@ import jetbrains.buildServer.util.TestFor;
 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;
@@ -628,6 +632,66 @@ public class CollectChangesTest extends BaseRemoteRepositoryTest {
   }
 
 
+  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();
   }
index 899915a82fa7eac89153ceb2df51e0f4818d8c13..499357c190c7153283c391dd06847c59319fe534 100644 (file)
@@ -71,6 +71,7 @@ public class PluginConfigBuilder {
   private Boolean myIgnoreMissingRemoteRef;
   private Integer myMergeRetryAttempts;
   private Boolean myRunInPlaceGc;
+  private Boolean myReportPerParentChangedFiles;
 
   public static PluginConfigBuilder pluginConfig() {
     return new PluginConfigBuilder();
@@ -368,6 +369,11 @@ public class PluginConfigBuilder {
       public List<String> getRecoverableFetchErrorMessages() {
         return myDelegate.getRecoverableFetchErrorMessages();
       }
+
+      @Override
+      public boolean reportPerParentChangedFiles() {
+        return myReportPerParentChangedFiles != null ? myReportPerParentChangedFiles : myDelegate.reportPerParentChangedFiles();
+      }
     };
   }
 
@@ -550,4 +556,9 @@ public class PluginConfigBuilder {
     myRunInPlaceGc = runInPlaceGc;
     return this;
   }
+
+  PluginConfigBuilder setReportPerParentChangedFiles(boolean report) {
+    myReportPerParentChangedFiles = report;
+    return this;
+  }
 }