Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7317981
merge octopus: add test cases for simple, complex, and failure octopu…
jar-of-salt Sep 14, 2025
e0a9950
merge octopus: add test repos for octopus merge
jar-of-salt Sep 14, 2025
9cddb4d
merge octopus: update repo path in path in test resources
jar-of-salt Sep 14, 2025
abdced1
merge octopus: add octopus base computation
jar-of-salt Sep 14, 2025
7b0c053
merge octopus: add 'merge_annotated_commits_octopus'
jar-of-salt Sep 14, 2025
88fc025
merge octopus: add octopinitial octopus merge logic
jar-of-salt Sep 14, 2025
e8904ef
merge octopus: fix some issues with obtaining trees; fix seg fault; p…
jar-of-salt Sep 14, 2025
2006f8d
merge octopus: fix merge base calculation
jar-of-salt Sep 15, 2025
996adca
merge octopus: remove references to aggressive merge strategy
jar-of-salt Sep 15, 2025
caf28d4
merge octopus: fix compiler warnings
jar-of-salt Sep 15, 2025
5b3a805
merge octopus: use in-memory indexes only for octopus steps
jar-of-salt Sep 15, 2025
f28c71a
merge octopus: break octopus logic into fastforward and simple merge …
jar-of-salt Sep 16, 2025
a57a084
merge octopus: remove spaces
jar-of-salt Sep 16, 2025
fb092c1
merge octopus: update git repo for test case complex
jar-of-salt Sep 16, 2025
b6c7611
merge octopus: add credit to Junio C Hamano; remove TODOs
jar-of-salt Sep 16, 2025
6668068
merge octopus: remove TODOs related to octopus
jar-of-salt Sep 16, 2025
7378706
merge octopus: resolve further octopus-related todos
jar-of-salt Sep 16, 2025
e8f925a
merge octopus: convert simple test to use same repo as complex test
jar-of-salt Sep 16, 2025
ed768bc
merge octopus: rename merge-octopus-complex resource to merge-octopus
jar-of-salt Sep 16, 2025
f9f8da0
merge octopus: remove unused simple octopus repo
jar-of-salt Sep 16, 2025
cbe3c21
merge octopus: remove merge octopus fail repo in favor of common repo
jar-of-salt Sep 16, 2025
530ad94
merge octopus: add standard test repo
jar-of-salt Sep 16, 2025
5548ba6
merge octopus: convert tests to use common octopus test repo; change …
jar-of-salt Sep 16, 2025
ef572d3
merge octopus: remove unnecessary hooks files from test repo
jar-of-salt Sep 16, 2025
97dc1a6
merge octopus: remove unnecessary exclude file from test repo
jar-of-salt Sep 16, 2025
8061171
merge octopus: update 'git_merge' documentation to reflect octopus fu…
jar-of-salt Sep 16, 2025
057a322
merge octopus: add todo for automatic merge
jar-of-salt Sep 16, 2025
05c663f
merge octopus: add fastforwardable branch
jar-of-salt Sep 16, 2025
7cfe5e4
merge octopus: fix fastforward logic to work with an in-memory index
jar-of-salt Sep 16, 2025
b545bd5
merge octopus: fix conflict and ff/skip test cases
jar-of-salt Sep 16, 2025
722359b
merge octopus: add fastforward-able branch and skippable branch
jar-of-salt Sep 16, 2025
cad0f32
merge octopus: cleanup formatting;\nclarify comments on octopus behavior
jar-of-salt Sep 16, 2025
493f96a
merge octopus: add explanatory git log graph for complex test case
jar-of-salt Sep 16, 2025
7405407
merge octopus: update comment RE automatic merge on octopus
jar-of-salt Sep 21, 2025
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
4 changes: 4 additions & 0 deletions include/git2/merge.h
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,10 @@ GIT_EXTERN(int) git_merge_commits(
* to the index. Callers should inspect the repository's index after this
* completes, resolve any conflicts and prepare a commit.
*
* If multiple commits are provided, they are merged using the octopus strategy.
* Merges with this strategy admit no conflicts, and the merge will fail at the first
* conflict encountered.
*
* For compatibility with git, the repository is put into a merging
* state. Once the commit is done (or if the user wishes to abort),
* you should clear this state by calling
Expand Down
276 changes: 267 additions & 9 deletions src/libgit2/merge.c
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,49 @@ static int compute_base(
return error;
}

static int compute_base_octopus(
git_annotated_commit **out,
git_repository *repo,
const git_annotated_commit *one,
const git_annotated_commit **twos,
size_t twos_len,
const git_merge_options *given_opts)
{
git_array_oid_t head_ids = GIT_ARRAY_INIT;
git_oid base_id;
git_annotated_commit *base = NULL;
git_merge_options opts = GIT_MERGE_OPTIONS_INIT;
size_t i;
int error;

*out = NULL;

if (given_opts)
memcpy(&opts, given_opts, sizeof(git_merge_options));

for (i = 0; i < twos_len; i++) {
if ((error = insert_head_ids(&head_ids, twos[i])) < 0)
goto done;

}
if ((error = insert_head_ids(&head_ids, one)) < 0 ||
(error = git_merge_base_octopus(&base_id, repo,
head_ids.size, head_ids.ptr)) < 0)
goto done;

if ((error = git_annotated_commit_lookup(&base, repo, &base_id)) < 0)
goto done;

done:
if (error == 0)
*out = base;
else
git_annotated_commit_free(base);

git_array_clear(head_ids);
return error;
}

static int iterator_for_annotated_commit(
git_iterator **out,
git_annotated_commit *commit)
Expand Down Expand Up @@ -2456,6 +2499,221 @@ static int merge_annotated_commits(
return error;
}

static int git_merge__octopus_fastforward(
git_tree **tree_out,
git_repository *repo,
git_array_oid_t *reference_commits,
git_annotated_commit *their_commit)
{
git_index *index;
git_oid result_oid;
int error;

git_index_new(&index);

if (!their_commit->tree &&
(error = git_commit_tree(&their_commit->tree, their_commit->commit)) < 0)
goto done;

/* Fastforward to the new tree */
if ((error = git_index_read_tree(index, their_commit->tree)) < 0 ||
(error = git_tree__write_index(&result_oid, index, repo)) < 0 ||
(error = git_tree_lookup(tree_out, repo, &result_oid)) < 0)
goto done;

/* Fastforward base reference commit */
reference_commits->ptr[1] = their_commit->commit->object.cached.oid;
reference_commits->size--;

done:
git_index_free(index);
return error;
}

static int git_merge__octopus_simple(
git_tree **tree_out,
git_array_oid_t *reference_commits_out,
git_repository *repo,
git_oidarray *bases,
git_tree *reference_tree,
git_annotated_commit *their_commit,
const git_merge_options *opts)
{
git_iterator_options iter_opts = GIT_ITERATOR_OPTIONS_INIT;
git_iterator *base_iter = NULL, *reference_iter = NULL, *their_iter = NULL;
git_oid result_oid, *id = NULL;
git_index *index;
git_tree *base_tree;
git_commit *commit;
int error;

GIT_ASSERT_ARG(tree_out);
GIT_ASSERT_ARG(reference_commits_out);

*tree_out = NULL;

git_index_new(&index);

if ((error = git_commit_lookup(&commit, repo,
(const git_oid*)&bases->ids[0])) < 0 ||
(error = git_commit_tree(&base_tree, commit)) < 0 ||
(error = git_iterator_for_tree(&base_iter, base_tree, &iter_opts)) < 0 ||
(error = git_iterator_for_tree(&reference_iter, reference_tree,
&iter_opts)) < 0 ||
(error = iterator_for_annotated_commit(&their_iter, their_commit)) < 0 ||
(error = git_merge__iterators(&index, repo, base_iter, reference_iter,
their_iter, opts)) < 0 ||
(error = git_tree__write_index(&result_oid, index, repo)) < 0 ||
(error = git_tree_lookup(tree_out, repo, &result_oid)) < 0)
goto done;

id = git_array_alloc(*reference_commits_out);
if (id == NULL) {
error = -1;
goto done;
}

git_oid_cpy(id, git_commit_id(their_commit->commit));

done:
git_index_free(index);
git_tree_free(base_tree);
git_commit_free(commit);
git_iterator_free(base_iter);
git_iterator_free(reference_iter);
git_iterator_free(their_iter);

return error;
}

/* Ported from Junio C Hamano's original octopus implmentation in bash. */
static int merge_annotated_commits_octopus(
git_index **index_out,
git_annotated_commit **base_out,
git_repository *repo,
git_index *repo_index,
git_annotated_commit *ours,
git_annotated_commit **their_commits,
size_t their_commits_len,
const git_merge_options *given_opts)
{
git_annotated_commit *base = NULL, *their_commit = NULL;
git_iterator *base_iter = NULL, *our_iter = NULL, *their_iter = NULL;
git_oid result_oid, *id = NULL, *their_commit_id = NULL;
git_index *index;
git_array_oid_t reference_commits = GIT_ARRAY_INIT;
git_merge_options opts;
int error;
git_oidarray bases = {0};
git_tree *reference_tree, *temp_tree;
size_t i, fastforward = 0;

GIT_ASSERT_ARG(index_out);
GIT_ASSERT_ARG(base_out);

/* The octopus strategy does not permit merge conflicts */
if ((error = merge_normalize_opts(repo, &opts, given_opts)) < 0)
goto done;

opts.flags |= GIT_MERGE_FAIL_ON_CONFLICT;

if ((error = compute_base_octopus(&base, repo, ours,
(const git_annotated_commit**)their_commits,
their_commits_len, &opts)) < 0) {
if (error != GIT_ENOTFOUND)
goto done;

git_error_clear();
}

/*
* Allocate space for the ID of the current commit.
* The current commit must always be at the front of the reference list,
* since it must be the first argument to `git_merge_bases_many` to match
* git's standard behavior
*/
their_commit_id = git_array_alloc(reference_commits);

/* Set initial reference commit */
id = git_array_alloc(reference_commits);
GIT_ERROR_CHECK_ARRAY(reference_commits);

if (id == NULL) {
error = -1;
goto done;
}

git_oid_cpy(id, git_commit_id(ours->commit));

/* Set reference tree */
if ((error = git_index_write_tree(&result_oid, repo_index)) < 0 ||
(error = git_tree_lookup(&reference_tree, repo, &result_oid)) < 0)
goto done;


for (i = 0; i < their_commits_len; i++) {
their_commit = their_commits[i];
their_commit_id = &reference_commits.ptr[0];

git_oid_cpy(their_commit_id, git_commit_id(their_commit->commit));

if ((error = git_merge_bases_many(&bases, repo, reference_commits.size,
reference_commits.ptr)) < 0)
goto done;

if (git_oid_cmp(&bases.ids[0], git_commit_id(their_commit->commit)) == 0) {
continue;
}

/* Octopus admits fastforward on the first commit only */
if (!fastforward) {
if (git_oid_cmp(&reference_commits.ptr[1], &bases.ids[0]) == 0 &&
(error = git_merge__octopus_fastforward(&temp_tree, repo,
&reference_commits, their_commit)) < 0)
goto done;

fastforward = 1;
}

/*
* This does not implement automatic merge.
* right now this fails at the first conflict, instead of running all
* branches through and returning failures after last one.
*/
if ((error = git_merge__octopus_simple(&temp_tree, &reference_commits, repo,
&bases, reference_tree, their_commit, &opts)) < 0)
goto done;

git_tree_free(reference_tree);

reference_tree = temp_tree;
temp_tree = NULL;

git_oidarray_dispose(&bases);
}

git_index_new(&index);

git_index_read_tree(index, reference_tree);

*index_out = index;

if (base_out) {
*base_out = base;
base = NULL;
}

done:
if (!given_opts || !given_opts->metric)
git__free(opts.metric);
git_annotated_commit_free(base);
git_array_dispose(reference_commits);
git_tree_free(reference_tree);
git_iterator_free(base_iter);
git_iterator_free(our_iter);
git_iterator_free(their_iter);
return error;
}

int git_merge_commits(
git_index **out,
Expand Down Expand Up @@ -3343,15 +3601,11 @@ int git_merge(
git_indexwriter indexwriter = GIT_INDEXWRITER_INIT;
unsigned int checkout_strategy;
int error = 0;
size_t octopus = their_heads_len > 1;

GIT_ASSERT_ARG(repo);
GIT_ASSERT_ARG(their_heads && their_heads_len > 0);

if (their_heads_len != 1) {
git_error_set(GIT_ERROR_MERGE, "can only merge a single branch");
return -1;
}

if ((error = git_repository__ensure_not_bare(repo, "merge")) < 0)
goto done;

Expand All @@ -3372,11 +3626,15 @@ int git_merge(
their_heads_len)) < 0)
goto done;

/* TODO: octopus */
if (octopus) {
if ((error = merge_annotated_commits_octopus(&index, &base, repo, repo_index,
our_head, (git_annotated_commit**)their_heads, their_heads_len, merge_opts)) < 0)
goto done;
} else if ((error = merge_annotated_commits(&index, &base, repo, our_head,
(git_annotated_commit *)their_heads[0], 0, merge_opts)) < 0)
goto done;

if ((error = merge_annotated_commits(&index, &base, repo, our_head,
(git_annotated_commit *)their_heads[0], 0, merge_opts)) < 0 ||
(error = git_merge__check_result(repo, index)) < 0 ||
if ((error = git_merge__check_result(repo, index)) < 0 ||
(error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0)
goto done;

Expand Down
Loading