Module: Amp::Graphs::CopyCalculator

Defined in:
lib/amp/graphs/copies.rb

Overview

CopyCalculator

This module manages finding what files have been copied between two changesets, using a base, ancestor changeset. Closely related to merging. We need this class because Mercurial, by default, allows us to copy files and move files, and be smart enough to follow these copies throughout the version history. Other VCS’s just treat the moved file as a brand-new file. Thus, when we update from one changeset to another, we need to follow these copies.

Class Method Summary collapse

Class Method Details

.find_copies(repo, changeset_local, changeset_remote, changeset_ancestor, check_dirs = false) ⇒ [Hash, Hash]

TODO:

Add tracking of directory renames!

Calculates the copies between 2 changesets, using a pre-calculated common ancestor node. This is used during updates as part of Mercurial’s ability to track renames without git-style guessing. Unfortunately it does require some amount of calculation. This method returns two hashes in an array: [renames, divergent]. “Renames” are moves from one file to another, and “divergent” are two moves of the same file, only with different end-names. See @example for how divergent works.

if the local changeset renamed “foo” to “bar”, and the remote changeset renamed “foo” to “baz”, then the “divergent” hash would be:

{"foo" => ["bar", "baz"]}

Parameters:

  • repo (Repository)

    The repo for which we are calculating changes. Typically a LocalRepository.

  • changeset_local (Changeset)

    The local (or just 1 of the bases) changeset.

  • changeset_remote (Changeset)

    The remote (or the second base) changeset

  • changeset_ancestor (Changeset)

    The common ancestor between changeset_local and changeset_remote

  • check_dirs (Boolean) (defaults to: false)

    (false) Whether or not to analyze for directory renames. this is an expensive operation, so it defaults to false.

Returns:

  • ([Hash, Hash])

    This method returns two hashes in an array, where the first is a list of normal file-moves (“foo” renamed to “bar” returns => “bar”) and the second is a list of divergent file-moves (see @example)



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/amp/graphs/copies.rb', line 42

def self.find_copies(repo, changeset_local, changeset_remote, changeset_ancestor, check_dirs=false)
  # are we udpating from an empty directory? quite easy.
  if changeset_local.nil? || changeset_remote.nil? || 
     changeset_remote == changeset_local
    return {}, {}
  end
  # avoid silly behavior for parent -> working directory
  if changeset_remote.node == nil && c1.node == repo.dirstate.parents.first
    return repo.dirstate.copies, {}
  end
  
  limit = find_limit(repo, changeset_local.revision, changeset_remote.revision)
  man_local    = changeset_local.manifest
  man_remote   = changeset_remote.manifest
  man_ancestor = changeset_ancestor.manifest
  
  # gets the versioned_file for a given file and node ID
  easy_file_lookup = proc do |file, node|
    if node.size == 20
      return repo.versioned_file(file, :file_id => node) 
    end
    cs = (changeset_local.revision == nil) ? changeset_local : changeset_remote
    return cs.get_file(file)
  end
  
  ctx = easy_file_lookup
  copy = {}
  full_copy = {}
  diverge = {}
  
  # check for copies from manifest1 to manifest2
  check_copies = proc do |file, man1, man2|
    vf1 = easy_file_lookup[file, man1[file]]
    find_old_names(vf1, limit) do |old_name|
      full_copy[file] = old_name # remember for dir rename detection
      if man2[old_name] # original file in other manifest?
        # if the original file is unchanged on the other branch,
        # no merge needed
        if man2[old_name] != man_ancestor[old_name]
          vf2 = easy_file_lookup[old_file, man2[old_file]]
          vfa = vf1.ancestor(vf2)
          # related and name changed on only one side?
          if vfa && (vfa.path == file || vfa.path == vf2.path) && (vf1 == vfa || vf2 == vfa)
            copy[file] = old_file
          end
        end
      elsif man_ancestor[old_file]
        (diverge[old_file] ||= []) << file
      end
    end
  end
  
  UI.debug("   searching for copies back to rev #{limit}")
  
  unmatched_1 = double_intersection(man_local, man_remote, man_ancestor)
  unmatched_2 = double_intersection(man_remote, man_local, man_ancestor)
  
  UI.debug("   unmatched files in local:\n #{unmatched_1.join("\n")}") if unmatched_1.any?
  UI.debug("   unmatched files in other:\n #{unmatched_2.join("\n")}") if unmatched_2.any?
  
  unmatched_1.each {|file| check_copies[file, man_local, man_remote] }
  unmatched_2.each {|file| check_copies[file, man_remote, man_local] }
  
  diverge_2 = {}
  diverge.each do |old_file, file_list|
    if file_list.size == 1
      diverge.delete old_file 
    else
      file_list.each {|file| diverge_2[file] = true}
    end
  end
  
  if !(full_copy.any?) || !check_dirs
    return copy, diverge
  end
  
  # CHECK FOR DIRECTORY RENAMES
  # TODO TODO TODO
end