Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@

- Update wheels to libgit2 1.9.6

- Wrap libgit2's rebase API: add `Repository.rebase_init(...)` and
`Repository.rebase_open(...)` returning a `Rebase` object, plus
`RebaseOperation` and `enums.RebaseOperationType`
[#1483](https://github.com/libgit2/pygit2/pull/1483)

- Fix `enums.CheckoutStrategy.CONFLICT_STYLE_ZDIFF3`, which was mistakenly
bound to the `DIFF3` constant
[#1483](https://github.com/libgit2/pygit2/pull/1483)

Breaking changes:

- Remove deprecated `pygit2.legacyenums` module and `GIT_*` constants,
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Table of Contents
objects
oid
packing
rebase
references
transactions
remotes
Expand Down
83 changes: 83 additions & 0 deletions docs/rebase.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
**********************************************************************
Rebase
**********************************************************************

.. contents::

.. automethod:: pygit2.Repository.rebase_init
.. automethod:: pygit2.Repository.rebase_open

The Rebase type
====================

.. autoclass:: pygit2.Rebase
:members:
:special-members: __len__, __getitem__, __next__

.. autoclass:: pygit2.RebaseOperation
:members:

Example
=======

Rebase the current branch onto its upstream::

>>> committer = repo.default_signature
>>> rebase = repo.rebase_init(upstream=repo.branches['origin/master'])
>>> for operation in rebase:
... # If repo.index.conflicts is not None at this point, the
... # operation left conflicts in the index and conflict markers
... # in the working directory. Resolve them, stage each
... # resolution with repo.index.add(path), and only then commit.
... rebase.commit(committer=committer)
>>> rebase.finish(committer)

Use ``abort()`` instead of ``finish()`` to reset the repository and the
working directory to their state before the rebase began.

``commit()`` returns ``None`` for a patch that turns out to be already
present upstream; like ``git rebase``, simply move on to the next
operation.

With ``inmemory=True`` the rebase does not touch HEAD, the repository
state, or the working directory; each step's result is available as
``rebase.inmemory_index`` and updating the branch reference afterwards is
the caller's responsibility.

Working with rebase operations
==============================

Iterating over a ``Rebase`` yields a :py:class:`~pygit2.RebaseOperation`
describing each step. ``len()`` and indexing expose the same operations
up front, without advancing the rebase, so the plan can be inspected
before applying it::

>>> rebase = repo.rebase_init(upstream=repo.branches['origin/master'])
>>> for i in range(len(rebase)):
... print(rebase[i])
<pygit2.RebaseOperation PICK 4a3fe06...>
<pygit2.RebaseOperation PICK 8ae4a25...>

A rebase started with ``rebase_init()`` replays the non-merge commits
in ``upstream..branch``; merge commits are skipped, linearizing the
history, just like plain ``git rebase`` (libgit2 has no equivalent of
``--rebase-merges``). Every operation's ``type`` is therefore
``RebaseOperationType.PICK``, ``id`` names the original commit being
replayed, and ``exec`` is ``None``. The remaining
``RebaseOperationType`` values mirror the verbs of git's interactive
rebase, which libgit2 does not implement (as of 1.9): they are declared
for completeness but never produced. Looking the original commit up is
useful for progress reporting or for reusing its metadata::

>>> from pygit2.enums import RebaseOperationType
>>> committer = repo.default_signature
>>> for operation in rebase:
... assert operation.type == RebaseOperationType.PICK
... original = repo[operation.id]
... step, total = rebase.current_index + 1, len(rebase)
... print(f'[{step}/{total}] picking {original.short_id}:',
... original.message.strip())
... rebase.commit(committer=committer)
[1/2] picking 4a3fe06: Add feature
[2/2] picking 8ae4a25: Fix tests
>>> rebase.finish(committer)
1 change: 1 addition & 0 deletions pygit2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@
option,
)
from .packbuilder import PackBuilder
from .rebase import Rebase, RebaseOperation
from .remotes import Remote
from .repository import Repository
from .settings import Settings
Expand Down
26 changes: 26 additions & 0 deletions pygit2/_libgit2/ffi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ class GitBufC:
class GitCheckoutOptionsC:
# incomplete
checkout_strategy: int
ancestor_label: ArrayC[char]
our_label: ArrayC[char]
their_label: ArrayC[char]

class GitCommitC:
pass
Expand Down Expand Up @@ -271,6 +274,22 @@ class GitProxyOptionsC:
# certificate_check
# payload

class GitRebaseC:
pass

class GitRebaseOperationC:
type: int
id: GitOidC
exec: char_pointer

class GitRebaseOptionsC:
version: int
quiet: int
inmemory: int
rewrite_notes_ref: ArrayC[char]
merge_options: GitMergeOptionsC
checkout_options: GitCheckoutOptionsC

class GitRemoteC:
pass

Expand Down Expand Up @@ -382,6 +401,12 @@ def new(a: Literal['git_stash_save_options *']) -> GitStashSaveOptionsC: ...
@overload
def new(a: Literal['git_strarray *']) -> GitStrrayC: ...
@overload
def new(a: Literal['git_rebase **']) -> _Pointer[GitRebaseC]: ...
@overload
def new(a: Literal['git_rebase_options *']) -> GitRebaseOptionsC: ...
@overload
def new(a: Literal['git_rebase_operation **']) -> _Pointer[GitRebaseOperationC]: ...
@overload
def new(a: Literal['git_tree **']) -> _Pointer[GitTreeC]: ...
@overload
def new(a: Literal['git_buf *'], b: tuple[NULL_TYPE, Literal[0]]) -> GitBufC: ...
Expand All @@ -401,6 +426,7 @@ def new(
) -> ArrayC[char_pointer]: ... # For string arrays
def addressof(a: object, attribute: str) -> _Pointer[object]: ...
def new_handle(a: T) -> _Pointer[T]: ...
def gc(cdata: T, destructor: Any, size: int = ...) -> T: ...

class buffer(bytes):
def __init__(self, a: object) -> None: ...
Expand Down
1 change: 1 addition & 0 deletions pygit2/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
'graph.h',
'index.h',
'merge.h',
'rebase.h',
'net.h',
'refspec.h',
'repository.h',
Expand Down
80 changes: 80 additions & 0 deletions pygit2/decl/rebase.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
typedef struct git_rebase git_rebase;

#define GIT_REBASE_OPTIONS_VERSION ...
#define GIT_REBASE_NO_OPERATION ...

typedef enum {
GIT_REBASE_OPERATION_PICK = 0,
GIT_REBASE_OPERATION_REWORD,
GIT_REBASE_OPERATION_EDIT,
GIT_REBASE_OPERATION_SQUASH,
GIT_REBASE_OPERATION_FIXUP,
GIT_REBASE_OPERATION_EXEC
} git_rebase_operation_t;

typedef struct {
unsigned int version;
int quiet;
int inmemory;
const char *rewrite_notes_ref;
git_merge_options merge_options;
git_checkout_options checkout_options;
...;
} git_rebase_options;

typedef struct {
git_rebase_operation_t type;
const git_oid id;
const char *exec;
...;
} git_rebase_operation;

int git_rebase_options_init(git_rebase_options *opts, unsigned int version);

int git_rebase_init(
git_rebase **out,
git_repository *repo,
const git_annotated_commit *branch,
const git_annotated_commit *upstream,
const git_annotated_commit *onto,
const git_rebase_options *opts);

int git_rebase_open(
git_rebase **out,
git_repository *repo,
const git_rebase_options *opts);

const char *git_rebase_orig_head_name(git_rebase *rebase);
const git_oid *git_rebase_orig_head_id(git_rebase *rebase);
const char *git_rebase_onto_name(git_rebase *rebase);
const git_oid *git_rebase_onto_id(git_rebase *rebase);

size_t git_rebase_operation_entrycount(git_rebase *rebase);
size_t git_rebase_operation_current(git_rebase *rebase);
git_rebase_operation *git_rebase_operation_byindex(
git_rebase *rebase,
size_t idx);

int git_rebase_next(
git_rebase_operation **operation,
git_rebase *rebase);

int git_rebase_inmemory_index(
git_index **index,
git_rebase *rebase);

int git_rebase_commit(
git_oid *id,
git_rebase *rebase,
const git_signature *author,
const git_signature *committer,
const char *message_encoding,
const char *message);

int git_rebase_abort(git_rebase *rebase);

int git_rebase_finish(
git_rebase *rebase,
const git_signature *signature);

void git_rebase_free(git_rebase *rebase);
30 changes: 29 additions & 1 deletion pygit2/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ class CheckoutStrategy(IntFlag):
notifications; don't update the working directory or index.
"""

CONFLICT_STYLE_ZDIFF3 = _pygit2.GIT_CHECKOUT_CONFLICT_STYLE_DIFF3
CONFLICT_STYLE_ZDIFF3 = _pygit2.GIT_CHECKOUT_CONFLICT_STYLE_ZDIFF3
""" Include common ancestor data in zdiff3 format for conflicts """


Expand Down Expand Up @@ -998,6 +998,34 @@ class Option(IntEnum):
ADD_SSL_X509_CERT = options.GIT_OPT_ADD_SSL_X509_CERT


class RebaseOperationType(IntEnum):
"""Type of rebase operation in-progress after calling Rebase.next()."""

PICK = C.GIT_REBASE_OPERATION_PICK
"""The given commit is to be cherry-picked. The client should commit
the changes and continue if there are no conflicts."""

REWORD = C.GIT_REBASE_OPERATION_REWORD
"""The given commit is to be cherry-picked, but the client should prompt
the user to provide an updated commit message."""

EDIT = C.GIT_REBASE_OPERATION_EDIT
"""The given commit is to be cherry-picked, but the client should stop
to allow the user to edit the changes before committing them."""

SQUASH = C.GIT_REBASE_OPERATION_SQUASH
"""The given commit is to be squashed into the previous commit. The
commit message will be merged with the previous message."""

FIXUP = C.GIT_REBASE_OPERATION_FIXUP
"""The given commit is to be squashed into the previous commit. The
commit message from this commit will be discarded."""

EXEC = C.GIT_REBASE_OPERATION_EXEC
"""No commit will be cherry-picked. The client should run the given
command and (if successful) continue."""


class ReferenceFilter(IntEnum):
"""Filters for References.iterator()."""

Expand Down
Loading
Loading