summaryrefslogtreecommitdiff
path: root/lib/gitlab/graph_commit.rb
blob: b1ac1a0223bd78fe9a2763a179ace29047d10c2a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
require "grit"

module Gitlab
  class GraphCommit
    attr_accessor :time, :space, :refs

    include ActionView::Helpers::TagHelper

    def self.to_graph(project)
      @repo = project.repo

      commits = collect_commits(@repo).dup

      ref_cache = {}

      commits.map! { |commit| GraphCommit.new(Commit.new(commit))}
      commits.each { |commit| commit.add_refs(ref_cache, @repo) }

      days = GraphCommit.index_commits(commits)
      @days_json = days.compact.collect{|d| [d.day, d.strftime("%b")] }.to_json
      @commits_json = commits.map(&:to_graph_hash).to_json

      return @days_json, @commits_json
    end

    # Get commits from repository
    #
    def self.collect_commits repo
      Grit::Commit.find_all(repo, nil, {max_count: self.max_count})
    end

    def self.max_count
      @max_count ||= 650
    end

    # Method is adding time and space on the
    # list of commits. As well as returns date list
    # corelated with time set on commits.
    #
    # @param [Array<GraphCommit>] comits to index
    #
    # @return [Array<TimeDate>] list of commit dates corelated with time on commits
    def self.index_commits(commits)
      days, heads = [], []
      map = {}

      commits.reverse.each_with_index do |c,i|
        c.time = i
        days[i] = c.committed_date
        map[c.id] = c
        heads += c.refs unless c.refs.nil?
      end

      heads.select!{|h| h.is_a? Grit::Head or h.is_a? Grit::Remote}
      # sort heads so the master is top and current branches are closer
      heads.sort! do |a,b|
        if a.name == "master"
          -1
        elsif b.name == "master"
          1
        else
          b.commit.committed_date <=> a.commit.committed_date
        end
      end

      @_reserved = {}
      days.each_index do |i|
        @_reserved[i] = []
      end

      heads.each do |h|
        if map.include? h.commit.id then
          place_chain(map[h.commit.id], map)
        end
      end
      days
    end

    # Add space mark on commit and its parents
    #
    # @param [GraphCommit] the commit object.
    # @param [Hash<String,GraphCommit>] map of commits
    def self.place_chain(commit, map, parent_time = nil)
      leaves = take_left_leaves(commit, map)
      if leaves.empty? then
        return
      end
      space = find_free_space(leaves.last.time..leaves.first.time)
      leaves.each{|l| l.space = space}
      # and mark it as reserved
      min_time = leaves.last.time
      parents = leaves.last.parents.collect
      parents.each do |p|
        if map.include? p.id then
          parent = map[p.id]
          if parent.time < min_time then
            min_time = parent.time
          end
        end
      end
      if parent_time.nil? then
        max_time = leaves.first.time
      else
        max_time = parent_time - 1
      end
      mark_reserved(min_time..max_time, space)
      # Visit branching chains
      leaves.each do |l|
        parents = l.parents.collect
          .select{|p| map.include? p.id and map[p.id].space == 0}
        for p in parents
          place_chain(map[p.id], map, l.time)
        end
      end
    end

    def self.mark_reserved(time_range, space)
      for day in time_range
        @_reserved[day].push(space)
      end
    end

    def self.find_free_space(time_range)
      reserved = []
      for day in time_range
          reserved += @_reserved[day]
      end
      space = 1
      while reserved.include? space do
        space += 1
      end
      space
    end

    # Takes most left subtree branch of commits
    # which don't have space mark yet.
    #
    # @param [GraphCommit] the commit object.
    # @param [Hash<String,GraphCommit>] map of commits
    #
    # @return [Array<GraphCommit>] list of branch commits
    def self.take_left_leaves(commit, map)
      leaves = []
      leaves.push(commit)  if commit.space == 0
      while true
        parent = commit.parents.collect
          .select{|p| map.include? p.id and map[p.id].space == 0}
        if parent.count == 0 then
          return leaves
        else
          commit = map[parent.first.id]
          leaves.push(commit)
        end
      end
    end


    def initialize(commit)
      @_commit = commit
      @time = -1
      @space = 0
    end

    def method_missing(m, *args, &block)
      @_commit.send(m, *args, &block)
    end

    def to_graph_hash
      h = {}
      h[:parents] = self.parents.collect do |p|
        [p.id,0,0]
      end
      h[:author]  = Gitlab::Encode.utf8(author.name)
      h[:time]    = time
      h[:space]   = space
      h[:refs]    = refs.collect{|r|r.name}.join(" ") unless refs.nil?
      h[:id]      = sha
      h[:date]    = date
      h[:message] = escape_once(Gitlab::Encode.utf8(message))
      h[:login]   = author.email
      h
    end

    def add_refs(ref_cache, repo)
      if ref_cache.empty?
        repo.refs.each do |ref|
          ref_cache[ref.commit.id] ||= []
          ref_cache[ref.commit.id] << ref
        end
      end
      @refs = ref_cache[@_commit.id] if ref_cache.include?(@_commit.id)
      @refs ||= []
    end
  end
end