Skip to content

Commit

Permalink
feat: extend git rebase to support drop
Browse files Browse the repository at this point in the history
  • Loading branch information
SuperBo committed Nov 24, 2024
1 parent 38df90f commit 16318d3
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 138 deletions.
31 changes: 4 additions & 27 deletions lua/fugit2/core/git_gpg.lua
Original file line number Diff line number Diff line change
Expand Up @@ -119,38 +119,15 @@ end
---@return integer err
---@return string err_msg
local function create_commit_with_sign(repo, commit_content, gpg_sign, msg)
local commit_id, head, head_direct, err
local commit_id, err
commit_id, err = repo:create_commit_with_signature(commit_content, gpg_sign, nil)
if not commit_id then
return nil, err, "Failed to create commit with sign"
end

head, err = repo:reference_lookup "HEAD"
if not head then
return nil, err, "Failed to lookup HEAD"
end

head_direct, err = head:resolve()
if head_direct then
-- normal branch
_, err = head_direct:set_target(commit_id, "commit: " .. utils.lines_head(msg))
if err ~= 0 then
return nil, err, "Failed to set head to new commit"
end
elseif err == git2.GIT_ERROR.GIT_ENOTFOUND then
-- initial branch
local head_ref_name = head:symbolic_target()
if not head_ref_name then
return nil, err, "Failed to get HEAD sympolic target"
end

head, err = repo:create_reference(head_ref_name, commit_id, false, "commit (initial): " .. utils.lines_head(msg))
if err ~= 0 then
return nil, err, "Failed to create initial reference " .. head_ref_name
end
else
-- error
return nil, err, "Failed to get HEAD target"
err = repo:update_head_for_commit(commit_id, utils.lines_head(msg))
if err ~= 0 then
return nil, err, "Failed to update HEAD to new commit"
end

return commit_id, 0, ""
Expand Down
32 changes: 0 additions & 32 deletions lua/fugit2/core/git_rebase.lua

This file was deleted.

208 changes: 208 additions & 0 deletions lua/fugit2/core/git_rebase_helper.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---Module contains reimplementation of git rebase
---Extends libgit2 git rebase with support for
---SQUASH AND FIXUP

local DynamicArray = require "fugit2.util.dynamic_array"
local git2 = require "fugit2.git2"
local libgit2 = require "fugit2.libgit2"

---@module "fugit2.core.git_rebase"
local M = {}

---@enum FUGIT2_GIT_REBASE_OPERATION
M.GIT_REBASE_OPERATION = {
PICK = 0,
REWORD = 1,
EDIT = 2,
SQUASH = 3,
FIXUP = 4,
EXEC = 5,
DROP = 6,
BREAK = 7,
BASE = 8,
}

-- =================================
-- | Rewrite libgit2 rebase in lua |
-- =================================

-- Fugit2 Git In-memory rebase. DEPRECATED
---@class Fugit2GitRebase
---@field repo GitRepository
---@field last_commit GitCommit Last commit
---@field operations Fugit2DynamicArray
---@field onto_id GitObjectId
---@field onto_name string
local GitRebase = {}
GitRebase.__index = GitRebase

---@param repo ffi.cdata* git_repository pointer
---@param branch ffi.cdata*? git_annotated_commit pointer
---@param upstream ffi.cdata*? git_annotated_commit pointer
---@param onto ffi.cdata*? git_annotated_commit pointer
---@return Fugit2DynamicArray?
---@return GIT_ERROR
local function rebase_init_operations(repo, branch, upstream, onto)
local revwalk = libgit2.git_revwalk_double_pointer(0LL)
local commit = libgit2.git_commit_double_pointer(0LL)
local merge = false
local id = libgit2.git_oid()
local operations, err

if not upstream then
upstream = onto
end

err = libgit2.C.git_revwalk_new(revwalk, repo)
if err ~= 0 then
return nil, err
end

err = libgit2.C.git_revwalk_push(revwalk[0], libgit2.C.git_annotated_commit_id(branch))
if err ~= 0 then
libgit2.C.git_revwalk_free(revwalk[0])
return nil, err
end

err = libgit2.C.git_revwalk_hide(revwalk[0], libgit2.C.git_annotated_commit_id(upstream))
if err ~= 0 then
libgit2.C.git_revwalk_free(revwalk[0])
return nil, err
end

libgit2.C.git_revwalk_sorting(revwalk[0], libgit2.GIT_SORT.REVERSE)

local init_operation = M.GIT_REBASE_OPERATION.PICK
operations = DynamicArray.new(16, 16, libgit2.git_rebase_operation_array)
while true do
err = libgit2.C.git_revwalk_next(id, revwalk[0])
if err ~= 0 then
break
end

err = libgit2.C.git_commit_lookup(commit, repo, id)
if err ~= 0 then
goto rebase_init_operations_cleanup
end

merge = (libgit2.C.git_commit_parentcount(commit[0]) > 1)
libgit2.C.git_commit_free(commit[0])

if not merge then
local position = operations:append()
local op = operations[position]
op["type"] = init_operation
libgit2.C.git_oid_cpy(op["id"], id)
op["exec"] = 0LL
end
end

err = 0

::rebase_init_operations_cleanup::
libgit2.C.git_revwalk_free(revwalk[0])
return operations, err
end

---@param repo GitRepository git_repository pointer
---@param onto GitAnnotatedCommit git_annotated_commit pointer
---@param operations Fugit2DynamicArray
---@return Fugit2GitRebase?
---@return GIT_ERROR
local function rebase_init_inmemory(repo, onto, operations)
local last_commit = libgit2.git_commit_double_pointer()
local err = libgit2.C.git_commit_lookup(repo.repo, libgit2.C.git_annotated_commit_id(onto.commit))
if err ~= 0 then
return nil, err
end

local onto_id = onto:id()

local onto_ref = onto:ref()
local onto_name
if onto_ref then
onto_name = vim.startswith(onto_ref, "refs/heads/") and onto_ref:sub(12) or onto_ref
else
onto_name = onto_id:tostring(8)
end

---@type Fugit2GitRebase
local rebase = {
repo = repo,
operations = operations,
last_commit = git2.Commit.new(last_commit[0]),
onto_id = onto_id,
onto_name = onto_name,
}
setmetatable(rebase, GitRebase)
return rebase, 0
end

-- Init rebase
---@param repo GitRepository
---@param branch GitAnnotatedCommit?
---@param upstream GitAnnotatedCommit?
---@param onto GitAnnotatedCommit?
---@return Fugit2GitRebase?
---@return GIT_ERROR
function GitRebase.init(repo, branch, upstream, onto)
local rebase, operations, err

if not onto then
onto = upstream
end
assert(onto ~= nil, "Upstream or onto must be set")

if not branch then
local head, head_branch

head, err = repo:head()
if not head then
return nil, err
end

head_branch, err = repo:annotated_commit_from_ref(head)
if not head_branch then
return nil, err
end

branch = head_branch
end

operations, err =
rebase_init_operations(repo.repo, branch and branch.commit, upstream and upstream.commit, onto and onto.commit)
if not operations then
goto fugit2_git_rebase_init_cleanup
end

rebase, err = rebase_init_inmemory(repo, onto, operations)

::fugit2_git_rebase_init_cleanup::
return rebase, err
end

-- ========================================
-- | Libgit2 override for inmemory rebase |
-- ========================================

---@param upstream GitAnnotatedCommit?
---@param onto GitAnnotatedCommit?
---@return string rebase_onto_name
function M.git_rebase_onto_name(upstream, onto)
if not onto then
onto = upstream
end
assert(onto ~= nil, "Upstream or onto must be set")

local onto_ref = onto:ref()
local onto_name
if onto_ref then
onto_name = vim.startswith(onto_ref, "refs/heads/") and onto_ref:sub(12) or onto_ref
else
onto_name = onto:id():tostring(8)
end

return onto_name
end

return M
74 changes: 69 additions & 5 deletions lua/fugit2/git2.lua
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,13 @@ function AnnotatedCommit:id()
return ObjectId.borrow(git_oid)
end

-- Get the refname that the given git_annotated_commit refers to.
---@return string?
function AnnotatedCommit:ref()
local c_char = libgit2.C.git_annotated_commit_ref(self.commit)
return c_char ~= nil and ffi.string(c_char) or nil
end

-- =================
-- | Blame methods |
-- =================
Expand Down Expand Up @@ -1869,6 +1876,25 @@ function Rebase:next()
return RebaseOperation.borrow(operation[0]), 0
end

-- Skip the next rebase operation and returns the information about it.
---@return GitRebaseOperation?
---@return GIT_ERROR
function Rebase:skip()
-- rebase_movenext
local c_rebase = self.rebase
local next = c_rebase["started"] and c_rebase["current"] + 1 or 0
if next == c_rebase["operations"]["size"] then
return nil, libgit2.GIT_ERROR.GIT_ITEROVER
end

c_rebase["started"] = 1
c_rebase["current"] = next

-- rebase_next_inmemory
local operation = libgit2.git_rebase_operation_pointer()
operation = c_rebase["operations"]["ptr"] + next
return RebaseOperation.borrow(operation), 0
end

---Gets the count of rebase operations that are to be applied.
---@return integer
Expand Down Expand Up @@ -1958,15 +1984,13 @@ end
---@param message string? The message for this commit, or NULL to use the message from the original commit.
---@return GitObjectId?
---@return GIT_ERROR err Zero on success, GIT_EUNMERGED if there are unmerged changes in the index, GIT_EAPPLIED if the current commit has already been applied to the upstream and there is nothing to commit, -1 on failure.
function Rebase:commit_amend_inmemory(author, committer, message)

end
function Rebase:commit_amend_inmemory(author, committer, message) end

---Finishes a rebase that is currently in progress once all patches have been applied.
---@param signature GitSignature
---@param signature GitSignature?
---@return GIT_ERROR err Zero on success; -1 on error
function Rebase:finish(signature)
return libgit2.C.git_rebase_finish(self.rebase, signature.sign)
return libgit2.C.git_rebase_finish(self.rebase, signature and signature.sign)
end

---Gets the index produced by the last operation,
Expand Down Expand Up @@ -2290,6 +2314,45 @@ function Repository:set_head_detached(oid)
return err
end

-- Set head to commit id. Useful for git commit created with gpg
-- or git rebase inmemory
---@param commit_id GitObjectId
---@param commit_head_line string
---@param log_prefix string? default: commit
---@return GIT_ERROR
function Repository:update_head_for_commit(commit_id, commit_head_line, log_prefix)
local head, err = self:reference_lookup "HEAD"
if not head then
return err
end

local head_direct
head_direct, err = head:resolve()
if head_direct then
-- normal branch
_, err = head_direct:set_target(commit_id, (log_prefix or "commit: ") .. commit_head_line)
if err ~= 0 then
return err
end
elseif err == libgit2.GIT_ERROR.GIT_ENOTFOUND then
-- initial branch
local head_ref_name = head:symbolic_target()
if not head_ref_name then
return err
end

head, err = self:create_reference(head_ref_name, commit_id, false, "commit (initial): " .. commit_head_line)
if err ~= 0 then
return err
end
else
-- error
return err
end

return 0
end

-- Gets GitCommit signature
---@param oid GitObjectId
---@param field string? GPG sign field
Expand Down Expand Up @@ -3875,6 +3938,7 @@ M.IndexEntry = IndexEntry
M.ObjectId = ObjectId
M.Reference = Reference
M.Repository = Repository
M.Commit = Commit

M.GIT_BRANCH = libgit2.GIT_BRANCH
M.GIT_CHECKOUT = libgit2.GIT_CHECKOUT
Expand Down
Loading

0 comments on commit 16318d3

Please sign in to comment.