1module Gitlab 2 module Git 3 class Commit 4 include Gitlab::EncodingHelper 5 6 attr_accessor :raw_commit, :head 7 8 MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes 9 MIN_SHA_LENGTH = 7 10 SERIALIZE_KEYS = %i[ 11 id message parent_ids 12 authored_date author_name author_email 13 committed_date committer_name committer_email trailers 14 ].freeze 15 16 attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator 17 18 def ==(other) 19 return false unless other.is_a?(Gitlab::Git::Commit) 20 21 id && id == other.id 22 end 23 24 class << self 25 # Get single commit 26 # 27 # Ex. 28 # Commit.find(repo, '29eda46b') 29 # 30 # Commit.find(repo, 'master') 31 # 32 def find(repo, commit_id = "HEAD") 33 # Already a commit? 34 return commit_id if commit_id.is_a?(Gitlab::Git::Commit) 35 36 # A rugged reference? 37 commit_id = Gitlab::Git::Ref.dereference_object(commit_id) 38 return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit) 39 40 # Some weird thing? 41 return nil unless commit_id.is_a?(String) 42 43 # This saves us an RPC round trip. 44 return nil if commit_id.include?(':') 45 46 commit = rugged_find(repo, commit_id) 47 48 decorate(repo, commit) if commit 49 rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, 50 Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, 51 Rugged::OdbError, Rugged::TreeError, ArgumentError 52 nil 53 end 54 55 def rugged_find(repo, commit_id) 56 obj = repo.rev_parse_target(commit_id) 57 58 obj.is_a?(Rugged::Commit) ? obj : nil 59 end 60 61 def decorate(repository, commit, ref = nil) 62 Gitlab::Git::Commit.new(repository, commit, ref) 63 end 64 65 def shas_with_signatures(repository, shas) 66 shas.select do |sha| 67 begin 68 Rugged::Commit.extract_signature(repository.rugged, sha) 69 rescue Rugged::OdbError 70 false 71 end 72 end 73 end 74 end 75 76 def initialize(repository, raw_commit, head = nil) 77 raise "Nil as raw commit passed" unless raw_commit 78 79 @repository = repository 80 @head = head 81 82 case raw_commit 83 when Hash 84 init_from_hash(raw_commit) 85 when Rugged::Commit 86 init_from_rugged(raw_commit) 87 when Gitaly::GitCommit 88 init_from_gitaly(raw_commit) 89 else 90 raise "Invalid raw commit type: #{raw_commit.class}" 91 end 92 end 93 94 def sha 95 id 96 end 97 98 def short_id(length = 10) 99 id.to_s[0..length] 100 end 101 102 def safe_message 103 @safe_message ||= message 104 end 105 106 def no_commit_message 107 "--no commit message" 108 end 109 110 def to_hash 111 serialize_keys.map.with_object({}) do |key, hash| 112 hash[key] = send(key) 113 end 114 end 115 116 def date 117 committed_date 118 end 119 120 def parents 121 parent_ids.map { |oid| self.class.find(@repository, oid) }.compact 122 end 123 124 def message 125 encode! @message 126 end 127 128 def author_name 129 encode! @author_name 130 end 131 132 def author_email 133 encode! @author_email 134 end 135 136 def committer_name 137 encode! @committer_name 138 end 139 140 def committer_email 141 encode! @committer_email 142 end 143 144 def rugged_commit 145 @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit) 146 raw_commit 147 else 148 @repository.rev_parse_target(id) 149 end 150 end 151 152 def merge_commit? 153 parent_ids.size > 1 154 end 155 156 def to_gitaly_commit 157 return raw_commit if raw_commit.is_a?(Gitaly::GitCommit) 158 159 message_split = raw_commit.message.split("\n", 2) 160 Gitaly::GitCommit.new( 161 id: raw_commit.oid, 162 subject: message_split[0] ? message_split[0].chomp.b : "", 163 body: raw_commit.message.b, 164 parent_ids: raw_commit.parent_ids, 165 author: gitaly_commit_author_from_rugged(raw_commit.author), 166 committer: gitaly_commit_author_from_rugged(raw_commit.committer), 167 trailers: gitaly_trailers_from_rugged(raw_commit) 168 ) 169 end 170 171 private 172 173 def init_from_hash(hash) 174 raw_commit = hash.symbolize_keys 175 176 serialize_keys.each do |key| 177 send("#{key}=", raw_commit[key]) 178 end 179 end 180 181 def init_from_rugged(commit) 182 author = commit.author 183 committer = commit.committer 184 185 @raw_commit = commit 186 @id = commit.oid 187 @message = commit.message 188 @authored_date = author[:time] 189 @committed_date = committer[:time] 190 @author_name = author[:name] 191 @author_email = author[:email] 192 @committer_name = committer[:name] 193 @committer_email = committer[:email] 194 @parent_ids = commit.parents.map(&:oid) 195 @trailers = Hash[commit.trailers] 196 end 197 198 def init_from_gitaly(commit) 199 @raw_commit = commit 200 @id = commit.id 201 # TODO: Once gitaly "takes over" Rugged consider separating the 202 # subject from the message to make it clearer when there's one 203 # available but not the other. 204 @message = message_from_gitaly_body 205 @authored_date = init_date_from_gitaly(commit.author) 206 @author_name = commit.author.name.dup 207 @author_email = commit.author.email.dup 208 @committed_date = init_date_from_gitaly(commit.committer) 209 @committer_name = commit.committer.name.dup 210 @committer_email = commit.committer.email.dup 211 @parent_ids = Array(commit.parent_ids) 212 @trailers = Hash[commit.trailers.map { |t| [t.key, t.value] }] 213 end 214 215 # Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone 216 # offset in author.timezone. If the latter isn't present, assume UTC. 217 def init_date_from_gitaly(author) 218 if author.timezone.present? 219 Time.strptime("#{author.date.seconds} #{author.timezone}", '%s %z') 220 else 221 Time.at(author.date.seconds).utc 222 end 223 end 224 225 def serialize_keys 226 SERIALIZE_KEYS 227 end 228 229 def gitaly_commit_author_from_rugged(author_or_committer) 230 Gitaly::CommitAuthor.new( 231 name: author_or_committer[:name].b, 232 email: author_or_committer[:email].b, 233 date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i) 234 ) 235 end 236 237 def gitaly_trailers_from_rugged(rugged_commit) 238 rugged_commit.trailers.map do |(key, value)| 239 Gitaly::CommitTrailer.new(key: key, value: value) 240 end 241 end 242 243 def message_from_gitaly_body 244 return @raw_commit.subject.dup if @raw_commit.body_size.zero? 245 246 @raw_commit.body.dup 247 end 248 end 249 end 250end 251