code: package org.rug.data.characteristics.comps;
  
  import org.apache.tinkerpop.gremlin.structure.Edge;
  import org.apache.tinkerpop.gremlin.structure.Vertex;
  import org.eclipse.jgit.diff.DiffEntry;
  import org.eclipse.jgit.diff.DiffFormatter;
  import org.eclipse.jgit.diff.Edit;
  import org.eclipse.jgit.lib.ObjectId;
  import org.eclipse.jgit.lib.Repository;
  import org.eclipse.jgit.patch.FileHeader;
  import org.eclipse.jgit.util.io.DisabledOutputStream;
  import org.rug.data.labels.EdgeLabel;
  import org.rug.data.labels.VertexLabel;
  import org.rug.data.project.GitVersion;
  import org.rug.data.project.IVersion;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  import java.io.IOException;
  import java.util.*;
  
  /**
   * Calculates metrics related to file changes:
   * - PCCC: Percentage of Commits a Class has Changed
   * - PCPC: Percentage of Commits a Package has Changed
   * - TACH: Total Amount of Changes
   * - FRCH: Frequency of Changes
   * - CHO: Change has Occurred
   */
  public class ChangeMetrics extends AbstractComponentCharacteristic {
  
      public static final String NAME = "freqOfChanges";
  
      private final static Logger logger = LoggerFactory.getLogger(ChangeMetrics.class);
      /**
       * The similarity measured as a percentage of the bytes between two files to count them as a rename.
       * Default value used by git is 60.
       */
      protected static final int RENAME_SCORE = 50;
      /**
       * The maximum number of files to compare within a rename to not reduce performance.
       * Default value used by git is 1000.
       */
      protected static final int RENAME_LIMIT = 500;
  
  
      private SourceCodeRetriever retriever;
      private Map<String, Long> changeHistory;
      private GitVersion previousVersion;
      private GitVersion currentVersion;
  
      private DiffFormatter diffFormatter;
      private List<DiffEntry> entries;
  
      public ChangeMetrics(String name) {
          super(name,
                  VertexLabel.allFiles(),
                  EnumSet.noneOf(EdgeLabel.class));
          this.changeHistory = new HashMap<>(1000);
      }
  
      /**
       * Triggers the calculation of this characteristic given that the given version is
       * a {@link GitVersion} object.
       * @param version the version from which to retrieve the components (must be of type {@link GitVersion}).
       */
      @Override
      public void calculate(IVersion version) {
          if (version instanceof GitVersion) {
              currentVersion = (GitVersion)version;
              retriever = version.getSourceCodeRetriever();
              if (previousVersion != null) {
                  initDiff(currentVersion.getRepository(),
                          previousVersion.getCommitObjectId(),
                          currentVersion.getCommitObjectId());
                  super.calculate(version);
              }
              previousVersion = currentVersion;
          }
      }
  
      @Override
      protected void calculate(Vertex vertex) {
          var pathFile = retriever.relativize(retriever.getPathOf(vertex));
          if (pathFile.isEmpty()){
              vertex.property(this.name, 0L);
              vertex.property(TACHMetricPackage.NAME, 0L);
              vertex.property(CHOMetricPackage.NAME, false);
              return;
          }
          var pathFileStr = pathFile.get().toString();
          var changeOpt = getDiffOf(pathFileStr);
  
          var hasChanged = false;
          String key;
          if (changeOpt.isPresent()){
              var change = changeOpt.get();
              Long oldValue;
              key = String.format("%s#%s", change.getNewPath(), vertex.value("name"));
              switch (change.getChangeType()) {
                  case ADD:
                  case MODIFY:
                      hasChanged = true;
                      oldValue = changeHistory.getOrDefault(key, 0L);
                      changeHistory.put(key, oldValue + 1);
                      break;
                  case COPY:
                  case RENAME:
                      hasChanged = true;
                      oldValue = changeHistory.remove(String.format("%s#%s", change.getOldPath(), vertex.value("name")));
                      oldValue = oldValue == null ? 0L : oldValue;
                      changeHistory.put(key, oldValue + 1);
                      break;
                  case DELETE:
                  default:
                      break;
              }
          }else {
              key = String.format("%s#%s", pathFileStr, vertex.value("name"));
          }
          vertex.property(TACHMetricPackage.NAME, countTotalAmountOfChanges(changeOpt));
          vertex.property(CHOMetricPackage.NAME, hasChanged);
          vertex.property(this.name, changeHistory.getOrDefault(key, 0L));
      }
  
      @Override
      protected void calculate(Edge edge) {
  
      }
  
      /**
       * Computes the list of DiffEntry that contains all the differences between the 2 commits.
       * Note that this implementation only returns the differences between the two given commit, ignoring any
       * commit in between.
       * @param repo The repository in which the commits are.
       * @param parent The parent commit to which needs to be compared.
       * @param child The child commit that needs to be compared to the parent commit.
       */
      private void initDiff(Repository repo, ObjectId parent, ObjectId child) {
          if (diffFormatter == null) {
              diffFormatter = new DiffFormatter(DisabledOutputStream.INSTANCE);
              diffFormatter.setRepository(repo);
              diffFormatter.setDetectRenames(true);
          }
          var renameDetector = diffFormatter.getRenameDetector();
          renameDetector.setRenameScore(RENAME_SCORE);
          renameDetector.setRenameLimit(RENAME_LIMIT);
          try {
              entries = diffFormatter.scan(parent, child);
          } catch (IOException e) {
              logger.error("Could not perform diff between parent commit {} and child {}.", parent.getName(), child.getName());
              entries = new ArrayList<>();
          }
          diffFormatter.close();
      }
  
      private long countTotalAmountOfChanges(Optional<DiffEntry> entry) {
          int linesDeleted = 0, linesAdded = 0, linesModified = 0;
          if (entry.isPresent()){
              try {
                  FileHeader fileHeader = diffFormatter.toFileHeader(entry.get());
                  for (Edit edit : fileHeader.toEditList()) {
                      switch (edit.getType()) {
                          case INSERT:
                              linesAdded += edit.getEndB() - edit.getBeginB();
                              break;
                          case DELETE:
                              linesDeleted += edit.getEndA() - edit.getBeginA();
                              break;
                          case REPLACE:
                              linesModified += edit.getEndA() - edit.getBeginA();
                              break;
                          case EMPTY:
                              break;
                      }
                  }
              } catch (IOException e) {
                  logger.error("Cannot convert to file header: {}", entry.get().getNewPath());
              }
          }else {
              return 0;
          }
          return linesAdded + linesDeleted + 2 * linesModified;
      }
  
      /**
       * Looks in the diff entry of between the current commits to find the given path.
       * @param pathSuffix the path to use as a suffix.
       * @return an optional containing a DiffEntry if any path was matched.
       */
      private Optional<DiffEntry> getDiffOf(String pathSuffix){
          return entries.stream().filter(e -> e.getNewPath().endsWith(pathSuffix)).findFirst();
      }
  }
  