class Au::Commit

Attributes

created_at[R]
doc_diff_ids[R]
id[R]
message[R]
parent_id_1[R]
parent_id_2[R]

Public Class Methods

create(staged_file_paths, commit_message, parent_commit_id = nil) click to toggle source
# File lib/au/models/commit.rb, line 46
def self.create(staged_file_paths, commit_message, parent_commit_id = nil)
  # only support one parent
  # file_paths: changed / added / removed files_paths
  parent_commit = find(parent_commit_id)
  new_doc_diff_ids = {}

  # Check if this commit exists somewhere in the repository already
  if Commit.list.any?
    Commit.list.each do |commit|
      same = true
      other_commit = find(commit)
      if other_commit.doc_diff_ids.length == staged_file_paths.length
        Document.find(staged_file_paths).each do |doc|
          # pass in the diff id for this doc from parent commit, if present
          same = ((doc.diff(other_commit ? other_commit.doc_diff_ids[doc.path] : nil) == '') ? same : false)
        end
        raise ('Error: Duplicate commit with ' + commit) if same
      end
    end
  end

  Document.find(staged_file_paths).each do |doc|
    # pass in the diff id for this doc from parent commit, if present
    diff_id = doc.create_diff(parent_commit ? parent_commit.doc_diff_ids[doc.path] : nil)
    new_doc_diff_ids[doc.path] = diff_id ? diff_id : -1
  end

  if parent_commit
    docs_from_parent_commit = Document.find(parent_commit.doc_diff_ids.keys)
    docs_from_parent_commit.each do |doc|
      new_doc_diff_ids[doc.path] ||= parent_commit.doc_diff_ids[doc.path]
    end
  end

  new_commit_id = build_commit_id
  db.transaction do
    db[new_commit_id] = {
      message: commit_message,
      parent_id_1: parent_commit_id,
      doc_diff_ids: new_doc_diff_ids.delete_if{ |key, val| val == -1 },
      created_at: Time.now.to_f
    }
  end
  new_commit_id
end
create_commit_resolve_conflict(staged_file_paths, parent_commit_id_1, parent_commit_id_2) click to toggle source
# File lib/au/models/commit.rb, line 158
def self.create_commit_resolve_conflict(staged_file_paths, parent_commit_id_1, parent_commit_id_2)
  parent_commit_1 = find(parent_commit_id_1)
  new_doc_diff_ids = {}

  Document.find(staged_file_paths).each do |doc|
    # pass in the diff id for this doc from parent commit, if present
    diff_id = doc.create_diff(parent_commit_1 ? parent_commit_1.doc_diff_ids[doc.path] : nil)
    new_doc_diff_ids[doc.path] = diff_id ? diff_id : -1
  end

  if parent_commit_1
    docs_from_parent_commit_1 = Document.find(parent_commit_1.doc_diff_ids.keys)
    docs_from_parent_commit_1.each do |doc|
      new_doc_diff_ids[doc.path] ||= parent_commit_1.doc_diff_ids[doc.path]
    end
  end

  new_commit_id = build_commit_id
  db.transaction do
    db[new_commit_id] = {
        message: "Resolved conflicts for #{parent_commit_id_1} and #{parent_commit_id_2}",
        parent_id_1: parent_commit_id_1,
        parent_id_2: parent_commit_id_2,
        doc_diff_ids: new_doc_diff_ids.delete_if{ |key, val| val == -1 },
        created_at: Time.now.to_f
    }
  end
  new_commit_id
end
find(commit_id) click to toggle source

find a commit which has been created

# File lib/au/models/commit.rb, line 29
def self.find(commit_id)
  return unless commit_id # used for initial commit
  db.transaction(true) do
    init_attrs = %i(message doc_diff_ids parent_id_1 parent_id_2 created_at)
    new(commit_id, *db[commit_id].values_at(*init_attrs))
  end
end
find_remote(commit_id, repo_path) click to toggle source

find a remote commit which has been created

# File lib/au/models/commit.rb, line 38
def self.find_remote(commit_id, repo_path)
  return unless commit_id # used for initial commit
  db(repo_path).transaction(true) do
    init_attrs = %i(message doc_diff_ids parent_id_1 parent_id_2 created_at)
    new(commit_id, *db(repo_path)[commit_id].values_at(*init_attrs))
  end
end
list() click to toggle source
# File lib/au/models/commit.rb, line 22
def self.list
  db.transaction(true) do
    db.roots
  end
end
merge(parent_commit_id_1, parent_commit_id_2) click to toggle source
# File lib/au/models/commit.rb, line 92
def self.merge(parent_commit_id_1, parent_commit_id_2)
  # If commit 2 is commit 1's ancestor, don't merge
  if gen_ancestor_set(parent_commit_id_1, Set.new).include?(parent_commit_id_2)
    puts 'Stop merging. Commit to be merged is ancestor of current commit'
    return nil
  end

  # Load commit objects
  parent1_commit = find(parent_commit_id_1)
  parent2_commit = find(parent_commit_id_2)
  ancestor_commit_id = lowest_common_ancestor(parent_commit_id_1, parent_commit_id_2)
  ancestor_commit = find(ancestor_commit_id)

  # Initialize local variables
  new_doc_diff_ids = {}
  conflict_file_paths = []

  # Go through files in commit 1
  parent1_commit.doc_diff_ids.each do |doc_path, diff1_id|
    if parent2_commit.doc_diff_ids[doc_path].nil?
      # If this file only exists in commit 1, add it to the file table
      new_doc_diff_ids[doc_path] = diff1_id
      next
    end

    doc = Document.find(doc_path)
    begin
      # If this file also exists in commit 2, run diff3 to merge files and check for conflict.
      doc.diff_3(ancestor_diff_id: ancestor_commit.doc_diff_ids[doc_path], other_diff_id: parent2_commit.doc_diff_ids[doc_path])
      new_doc_diff_ids[doc_path] = doc.create_diff(diff1_id) if conflict_file_paths.empty?
    rescue Document::HasMergeConflict
      conflict_file_paths << doc.path
    end
  end

  unless conflict_file_paths.empty?
    # add a file to indicate user is resolving conflicts
    File.open(Repository.resolve_conflict_fname, 'w') do |f|
      f.truncate(0)
      f.write(parent_commit_id_1.to_s + ',' + parent_commit_id_2.to_s)
    end

    puts "Merge failed due to conflicts. Fix conflicts in files: \n#{conflict_file_paths.join('\n')}"
    return nil
  end

  # Check if there is any file in commit 2 and not in commit 1,
  # if so, add them to file table
  parent2_commit.doc_diff_ids.each do |doc_path, diff2_id|
    new_doc_diff_ids[doc_path] = diff2_id unless parent1_commit.doc_diff_ids.key?(doc_path)
  end

  # Generate commit id and Write commit
  new_commit_id = build_commit_id
  db.transaction do
    db[new_commit_id] = {
      message: "Merged #{parent_commit_id_1} and #{parent_commit_id_2}",
      parent_id_1: parent_commit_id_1,
      parent_id_2: parent_commit_id_2,
      doc_diff_ids: new_doc_diff_ids,
      created_at: Time.now.to_f
    }
  end
  new_commit_id
end
new(id, message, doc_diff_ids, parent_id_1 = nil, parent_id_2 = nil, created_at = 0) click to toggle source
# File lib/au/models/commit.rb, line 13
def initialize(id, message, doc_diff_ids, parent_id_1 = nil, parent_id_2 = nil, created_at = 0)
  @id = id
  @message = message
  @doc_diff_ids = doc_diff_ids
  @parent_id_1 = parent_id_1
  @parent_id_2 = parent_id_2
  @created_at = created_at
end

Private Class Methods

build_commit_id() click to toggle source
# File lib/au/models/commit.rb, line 241
def self.build_commit_id
  Digest::MD5.hexdigest(Time.now.to_f.to_s)
end
db(repo_path = Repository.path) click to toggle source
# File lib/au/models/commit.rb, line 235
def self.db(repo_path = Repository.path)
  # in this pstore file, each key is a commit id
  @db ||= {}
  @db[repo_path] ||= PStore.new(File.join(repo_path, 'commits.pstore'))
end
gen_ancestor_set(commit_id, commit_id_set, skip_first=false) click to toggle source
# File lib/au/models/commit.rb, line 245
def self.gen_ancestor_set(commit_id, commit_id_set, skip_first=false)
  commit_id_set.add(commit_id) unless skip_first
  commit = find(commit_id)

  if commit.parent_id_1
    commit_id_set = self.gen_ancestor_set(commit.parent_id_1, commit_id_set)
  end
  if commit.parent_id_2
    commit_id_set = self.gen_ancestor_set(commit.parent_id_2, commit_id_set)
  end
  commit_id_set
end
lowest_common_ancestor(parent_commit_1, parent_commit_2) click to toggle source
# File lib/au/models/commit.rb, line 258
def self.lowest_common_ancestor(parent_commit_1, parent_commit_2)
  # keys are commit id, values are Array of number of hops
  all_ancestors_of_parent_1 = self.gen_ancestor_set(parent_commit_1, Set.new, skip_first=true)
  all_ancestors_of_parent_2 = self.gen_ancestor_set(parent_commit_2, Set.new, skip_first=true)

  common_ancestors = all_ancestors_of_parent_1 & all_ancestors_of_parent_2
  raise 'no common ancestors' if common_ancestors.size.zero?

  while common_ancestors.size != 1
    picked_ancestor = common_ancestors.first
    ancestors_to_be_removed_set = gen_ancestor_set(picked_ancestor, Set.new, skip_first=true)
    common_ancestors -= ancestors_to_be_removed_set
  end

  common_ancestors.first
end

Public Instance Methods

cat(file_path) click to toggle source

returns a tempfile with the content of the document at the recorded diff of commit.

# File lib/au/models/commit.rb, line 200
def cat(file_path)
  diff_id = doc_diff_ids[file_path]
  return unless diff_id
  Document.find(file_path).content_from(diff_id)
end
checkout(current_commit_id) click to toggle source
# File lib/au/models/commit.rb, line 188
def checkout(current_commit_id)
  current_commit = Commit.find(current_commit_id)
  @doc_diff_ids.each{ |doc_path, diff_id| Document.find(doc_path).checkout(diff_id) }
  current_commit.doc_diff_ids.each_key do |doc_path|
    abs_doc_path = File.join(Repository.root, doc_path)
    abs_dir_path = File.dirname(abs_doc_path)
    File.delete(abs_doc_path) if not @doc_diff_ids.include?(doc_path) and File.exist?(abs_doc_path)
    Dir.delete(abs_dir_path) if Dir.empty?(abs_dir_path)
  end
end
has_parent_1?() click to toggle source
# File lib/au/models/commit.rb, line 218
def has_parent_1?
  !!parent_id_1
end
log() click to toggle source

Returns an array of ids, starting from self.id and through all left parents (parent 1).

# File lib/au/models/commit.rb, line 208
def log
  commits = [self]
  this_commit = self
  while this_commit.has_parent_1?
    commits << this_commit.parent_1
    this_commit = this_commit.parent_1
  end
  commits
end
parent_1() click to toggle source
# File lib/au/models/commit.rb, line 222
def parent_1
  return nil unless parent_id_1
  @parent ||= self.class.find(parent_id_1)
end
tracked_docs_md5() click to toggle source
# File lib/au/models/commit.rb, line 227
def tracked_docs_md5
  Document.find(doc_diff_ids.keys).each_with_object({}) do |doc, accum|
    accum[doc.path] = doc.md5_at(doc_diff_ids[doc.path])
  end
end