5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-29 16:06:23 +00:00
panel/app/models/site/repository.rb
f 6ff0a36b44 BREAKING CHANGE: Site::Repository#commit
poder añadir y eliminar archivos en el mismo commit
2023-07-03 13:23:54 -03:00

193 lines
5.5 KiB
Ruby

# frozen_string_literal: true
class Site
# Acciones para el repositorio Git de un sitio. Por ahora hacemos un
# uso muy básico de Git, con lo que asumimos varias cosas, por ejemplo
# que un sitio tiene un solo origen, que siempre se trabaja con la
# rama master, etc.
class Repository
attr_reader :rugged, :path
# @param [String] la ruta del repositorio
def initialize(path)
@path = path
@rugged = Rugged::Repository.new(path)
end
# Obtiene la rama por defecto a partir de la referencia actual
#
# Por ejemplo "refs/heads/no-master" => "no-master"
#
# XXX: No memoizamos para obtener siempre el nombre de la rama
# actual, aunque por ahora asumimos que siempre estamos en la misma,
# internamente (ej. vía shell) podríamos cambiarla.
#
# @return [String]
def default_branch
rugged.head.canonical_name.split('/', 3).last
end
# Obtiene el origin
#
# @return [Rugged::Remote]
def origin
@origin ||= rugged.remotes.find do |remote|
remote.name == 'origin'
end
end
# Trae los cambios del repositorio de origen sin aplicarlos y
# devuelve la cantidad de commits pendientes.
#
# XXX: Prestar atención a la velocidad de respuesta cuando tengamos
# repositorios remotos.
#
# @return [Integer]
def fetch
if origin.check_connection(:fetch, credentials: credentials)
rugged.fetch(origin, credentials: credentials)[:received_objects]
else
0
end
end
# Incorpora los cambios en el repositorio actual
#
# @return [Rugged::Commit]
def merge(usuarie)
merge = rugged.merge_commits(head_commit, remote_head_commit)
# No hacemos nada si hay conflictos, pero notificarnos
begin
raise MergeConflictsException if merge.conflicts?
rescue MergeConflictsException => e
ExceptionNotifier.notify_exception(e, data: { path: path, merge: merge })
return # No hacer nada
end
commit = Rugged::Commit
.create(rugged, update_ref: 'HEAD',
parents: [head_commit, remote_head_commit],
tree: merge.write_tree(rugged),
message: I18n.t('sites.fetch.merge.message'),
author: author(usuarie), committer: committer)
# Forzamos el checkout para mover el HEAD al último commit y
# escribir los cambios
rugged.checkout 'HEAD', strategy: :force
commit
end
# El último commit
#
# @return [Rugged::Commit]
def head_commit
rugged.branches[default_branch].target
end
# El último commit del repositorio remoto
#
# XXX: Realmente no recuerdo por qué esto era necesario ~f
#
# @return [Rugged::Commit]
def remote_head_commit
rugged.branches["origin/#{default_branch}"].target
end
# Compara los commits entre el repositorio remoto y el actual para
# que luego los podamos mostrar.
def commits
walker = Rugged::Walker.new rugged
# Obtenemos todos los commits que existen en origin/master que no
# están en la rama master local
walker.push "refs/remotes/origin/#{default_branch}"
walker.hide "refs/heads/#{default_branch}"
walker.each.to_a
end
# Hay commits sin aplicar?
def needs_pull?
fetch
!commits.empty?
end
# Guarda los cambios en git
#
# @param :add [Array] Archivos a agregar
# @param :rm [Array] Archivos a eliminar
# @param :usuarie [Usuarie] Quién hace el commit
# @param :message [String] Mensaje
def commit(add: [], rm: [], usuarie:, message:)
# Cargar el árbol actual
rugged.index.read_tree rugged.head.target.tree
add.each do |file|
rugged.index.add(relativize(file))
end
rm.each do |file|
rugged.index.remove(relativize(file))
end
# Escribir los cambios para que el repositorio se vea tal cual
rugged.index.write
Rugged::Commit.create(rugged, message: message, update_ref: 'HEAD',
parents: [rugged.head.target],
tree: rugged.index.write_tree,
author: author(usuarie),
committer: committer)
end
def author(author)
{ name: author.name, email: author.email, time: Time.now }
end
def committer
{ name: 'Sutty', email: "sutty@#{Site.domain}", time: Time.now }
end
# Garbage collection
#
# @return [Boolean]
def gc
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
cmd = 'git gc'
r = nil
Open3.popen2e(env, cmd, unsetenv_others: true, chdir: path) do |_, _, t|
r = t.value
end
r&.success?
end
private
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
# credenciales necesarias para trabajar con repositorios remotos.
#
# @return [Nil, Rugged::Credentials::SshKey]
def credentials
return unless File.exist? private_key
@credentials ||= Rugged::Credentials::SshKey.new username: 'git', publickey: public_key, privatekey: private_key
end
# @return [String]
def public_key
@public_key ||= Rails.root.join('.ssh', 'id_ed25519.pub').to_s
end
# @return [String]
def private_key
@private_key ||= Rails.root.join('.ssh', 'id_ed25519').to_s
end
def relativize(file)
Pathname.new(file).relative_path_from(Pathname.new(path)).to_s
end
end
end