mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 17:56:21 +00:00
ff84423891
(cherry picked from commit 453798dcc7
)
270 lines
7.5 KiB
Ruby
270 lines
7.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, nil]
|
|
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, message = I18n.t('sites.fetch.merge.message'))
|
|
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: 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
|
|
|
|
# Trae todos los archivos desde LFS
|
|
#
|
|
# @return [Boolean]
|
|
def git_lfs_checkout
|
|
git_sh('git', 'lfs', 'fetch', 'origin', default_branch)
|
|
git_sh('git', 'lfs', 'checkout')
|
|
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
|
|
|
|
# Detecta si hay que hacer un pull o no
|
|
#
|
|
# @return [Boolean]
|
|
def up_to_date?
|
|
rugged.merge_analysis(remote_head_commit).include?(:up_to_date)
|
|
end
|
|
|
|
# Detecta si es posible adelantar la historia local a la remota o
|
|
# necesitamos un merge
|
|
#
|
|
# @return [Boolean]
|
|
def fast_forward?
|
|
rugged.merge_analysis(remote_head_commit).include?(:fastforward)
|
|
end
|
|
|
|
# Mueve la historia local a la remota
|
|
#
|
|
# @see {https://stackoverflow.com/a/27077322}
|
|
# @return [nil]
|
|
def fast_forward!
|
|
rugged.checkout_tree(remote_head_commit)
|
|
rugged.references.update(rugged.head.resolve, remote_head_commit.oid)
|
|
|
|
nil
|
|
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
|
|
git_sh("git", "gc")
|
|
end
|
|
|
|
# Pushea cambios al repositorio remoto
|
|
#
|
|
# @param :remote [Rugged::Remote]
|
|
# @return [Boolean, nil]
|
|
def push(remote = origin)
|
|
remote.push(rugged.head.canonical_name, credentials: credentials_for(remote))
|
|
git_sh('git', 'lfs', 'push', remote.name, default_branch)
|
|
end
|
|
|
|
# Hace limpieza de LFS
|
|
def lfs_cleanup
|
|
git_sh("git", "lfs", "prune")
|
|
git_sh("git", "lfs", "dedup")
|
|
end
|
|
|
|
private
|
|
|
|
# @deprecated
|
|
def credentials
|
|
@credentials ||= credentials_for(origin)
|
|
end
|
|
|
|
# Si Sutty tiene una llave privada de tipo ED25519, devuelve las
|
|
# credenciales necesarias para trabajar con repositorios remotos.
|
|
#
|
|
# @param :remote [Rugged::Remote]
|
|
# @return [Nil, Rugged::Credentials::SshKey]
|
|
def credentials_for(remote)
|
|
return unless File.exist? private_key
|
|
|
|
Rugged::Credentials::SshKey.new username: username_for(remote), publickey: public_key, privatekey: private_key
|
|
end
|
|
|
|
# Obtiene el nombre de usuario para el repositorio remoto, por
|
|
# defecto git
|
|
#
|
|
# @param :remote [Rugged::Remote]
|
|
# @return [String]
|
|
def username_for(remote)
|
|
username = parse_url(remote.url)&.user if remote.respond_to? :url
|
|
|
|
username || 'git'
|
|
end
|
|
|
|
# @param :url [String]
|
|
# @return [URI, nil]
|
|
def parse_url(url)
|
|
GitCloneUrl.parse(url)
|
|
rescue URI::Error => e
|
|
ExceptionNotifier.notify_exception(e, data: { path: path, url: url })
|
|
nil
|
|
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
|
|
|
|
# Ejecuta un comando de git
|
|
#
|
|
# @param :args [Array]
|
|
# @return [Boolean]
|
|
def git_sh(*args)
|
|
env = { 'PATH' => '/usr/bin', 'LANG' => ENV['LANG'], 'HOME' => path }
|
|
|
|
r = nil
|
|
Open3.popen2e(env, *args, unsetenv_others: true, chdir: path) do |_, _, t|
|
|
r = t.value
|
|
end
|
|
|
|
r&.success?
|
|
end
|
|
end
|
|
end
|