Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Implementing merge with default options #295

Merged
merged 9 commits into from

4 participants

@victorgp

This pull request implements the merge operation based on the default options libgit2 defines.

The merge receives an Oid and returns a MergeResult object.

This MergeResult object is quite similar than the git_merge_result libgit2 implements. it has:

->is_fastforward: boolean field that indicates the merge was fastforward
->is_uptodate: boolean field that indicates the merge was already up to date
->fastforward_oid: python Oid object with the new fastforward Oid to be the new head (only if the merge was fastforward, None otherwise)
->status: dict with the repository status after the merge (if the merge was not fastforward, either there were conflicts or not, an empty dict otherwise)

Some notes:
-The merge, as it name says, only does the merge, it does not perform any commit nor updates the references (ex. in the fastforward case)
-This method does not allow customized parameters for the merge operation, i would like to leave that for a second iteration over this method and let's it work for the moment with the default options defined in the libgit2 constant GIT_MERGE_OPTS_INIT.

The tests i implemented would help to understand this behaviour easily.

Extra: there is also a minor change in the Repository_status method, i removed the args parameter because it wasn't being used anywhere within the method.

@jdavid
Collaborator

If there is an error here, I understand you don't have any merge result to free.

I think it depends on when the error was raised inside the git_merge function, but taking a look to that funcion, the merge_result value is set just before returning, therefore it can't fail after assigning the content to merge_result, so you are right, i should remove that git_merge_result_free.

But, anyway, i would prefer to initialize this pointer to NULL so i can check before it is freed if they got anything or not, so i don't need to know anything about the insides of the functions called, although i see you don't initialize any pointer to NULL in this library.

So, it's up to you, i don't really care, i can remove that line or initialize the pointer to NULL, which one do you prefer?

Collaborator

Yes, pygit2 assumes libgit2 does not set results when there is an error. Maybe some libgit2 developer, @carlosmn?, can say whether this is a safe assumption or not.

But yes if you prefer, initialize to NULL, and maybe just goto error for cleanup?

Owner

In case it encounters an error, a libgit2 function will free any resources it has allocated, but you cannot depend on the output parameters pointing to anything valid. They're either left alone or set to NULL (this depends a bit on who wrote the code), but what you definitely never need to do is call a free function on an output parameter in case of error.

Collaborator

Okey, so we are doing it right.

That said, error handling in pygit2 would be simplified if libgit2 did set return values to NULL on error systematically. Maybe I should open an issue with examples...

Ok, so if i understand correctly i need to remove that line that frees the variable.
But then, the same would happen with the git_merge_head_free(oid_merge_head) line used in error as we cannot be sure oid_merge_head is already set, therefore i think the correct solution is:
-remove the git_merge_head_free(oid_merge_head); in the error: section
-change git_merge_result_free(merge_result); for git_merge_head_free(oid_merge_head);

Do you agree?

Collaborator

yes it is right in new code

@jdavid
Collaborator

tabs, trailing whitespace and one blank line to remove (the trailing whitespace is in the blank line)

You are right, i'll fix that

@jdavid
Collaborator

No error check here. PyObject_New may return NULL.

You are right, i'll fix that

@jdavid
Collaborator

Hello Victor, thanks for contributing.

We have the general policy to wrap git structs, so it should be like:

typedef struct {
    PyObject_HEAD
    git_merge_result *result;
} MergeResult;

Note that Repository_status is "expensive" and may fail. Do not keep it.

And please update the documentation docs/merge.rst

@jdavid
Collaborator

As for PEP 7 brace position is not good. See http://www.python.org/dev/peps/pep-0007/#code-lay-out

You are right, i'll fix that

@victorgp

Thanks @jdavid for your comments.

About the MergeResult struct, you are right, i'll implement it in that way.

@victorgp

Fixes done, please take a look and tell me if you like them.
Thanks

@jdavid
Collaborator

Why do you need MergeResult.index?

merge = repo.merge(...)
merge.index

How that is better than:

merge = repo.merge(...)
repo.index

If MergeResult does not keep a reference to the repository, code would be simpler.

@victorgp

Actually i don't need it but since the git_merge_result struct has an index field, i supposed you also wanted it here.

I'll remove it so it's simpler

@jdavid
Collaborator

Okey, I missed that. Anyway if you don't need it just remove.

@victorgp

Index removed!

@jdavid jdavid merged commit 1938147 into from
@alexband

@victorgp does merge_result has any attributes to indicate this merge is viable? like will it produce conflicts?

there is 'is_fastforward', 'is_up_to_date', but no other identification for whether the merge is viable, (is it can be merged but not fastforward merge?)

@alexband

sorry I found that merge_result has an index object, that explains

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
32 docs/merge.rst
@@ -2,4 +2,36 @@
Merge
**********************************************************************
+.. contents::
+
.. automethod:: pygit2.Repository.merge_base
+.. automethod:: pygit2.Repository.merge
+
+The merge method
+=================
+
+The method does a merge over the current working copy.
+It gets an Oid object as a parameter and returns a MergeResult object.
+
+As its name says, it only does the merge, does not commit nor update the
+branch reference in the case of a fastforward.
+
+For the moment, the merge does not support options, it will perform the
+merge with the default ones defined in GIT_MERGE_OPTS_INIT libgit2 constant.
+
+Example::
+
+ >>> branch_head_hex = '5ebeeebb320790caf276b9fc8b24546d63316533'
+ >>> branch_oid = self.repo.get(branch_head_hex).oid
+ >>> merge_result = self.repo.merge(branch_oid)
+
+The MergeResult object
+======================
+
+Represents the result of a merge and contains these fields:
+
+- is_uptodate: bool, if there wasn't any merge because the repo was already
+ up to date
+- is_fastforward: bool, whether the merge was fastforward or not
+- fastforward_oid: Oid, in the case it was a fastforward, this is the
+ forwarded Oid.
View
138 src/mergeresult.c
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2010-2013 The pygit2 contributors
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version 2,
+ * as published by the Free Software Foundation.
+ *
+ * In addition to the permissions in the GNU General Public License,
+ * the authors give you unlimited permission to link the compiled
+ * version of this file into combinations with other programs,
+ * and to distribute those combinations without any restriction
+ * coming from the use of this file. (The General Public License
+ * restrictions do apply in other respects; for example, they cover
+ * modification of the file, and distribution when not linked into
+ * a combined executable.)
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include "utils.h"
+#include "types.h"
+#include "oid.h"
+#include "repository.h"
+#include "mergeresult.h"
+
+extern PyTypeObject MergeResultType;
+extern PyTypeObject IndexType;
+
+PyObject *
+git_merge_result_to_python(git_merge_result *merge_result, Repository *repo)
+{
+ MergeResult *py_merge_result;
+
+ py_merge_result = PyObject_New(MergeResult, &MergeResultType);
+ if (!py_merge_result)
+ return NULL;
+
+ py_merge_result->result = merge_result;
+ py_merge_result->repo = repo;
+
+ return (PyObject*) py_merge_result;
+}
+
+PyDoc_STRVAR(MergeResult_is_uptodate__doc__, "Is up to date");
+
+PyObject *
+MergeResult_is_uptodate__get__(MergeResult *self)
+{
+ if (git_merge_result_is_uptodate(self->result))
+ Py_RETURN_TRUE;
+ else
+ Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(MergeResult_is_fastforward__doc__, "Is fastforward");
+
+PyObject *
+MergeResult_is_fastforward__get__(MergeResult *self)
+{
+ if (git_merge_result_is_fastforward(self->result))
+ Py_RETURN_TRUE;
+ else
+ Py_RETURN_FALSE;
+}
+
+PyDoc_STRVAR(MergeResult_fastforward_oid__doc__, "Fastforward Oid");
+
+PyObject *
+MergeResult_fastforward_oid__get__(MergeResult *self)
+{
+ if (git_merge_result_is_fastforward(self->result)) {
+ git_oid fastforward_oid;
+ git_merge_result_fastforward_oid(&fastforward_oid, self->result);
+ return git_oid_to_python((const git_oid *)&fastforward_oid);
+ }
+ else Py_RETURN_NONE;
+}
+
+PyGetSetDef MergeResult_getseters[] = {
+ GETTER(MergeResult, is_uptodate),
+ GETTER(MergeResult, is_fastforward),
+ GETTER(MergeResult, fastforward_oid),
+ {NULL},
+};
+
+PyDoc_STRVAR(MergeResult__doc__, "MergeResult object.");
+
+PyTypeObject MergeResultType = {
+ PyVarObject_HEAD_INIT(NULL, 0)
+ "_pygit2.MergeResult", /* tp_name */
+ sizeof(MergeResult), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ 0, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_compare */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT, /* tp_flags */
+ MergeResult__doc__, /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ 0, /* tp_methods */
+ 0, /* tp_members */
+ MergeResult_getseters, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ 0, /* tp_init */
+ 0, /* tp_alloc */
+ 0, /* tp_new */
+};
+
View
37 src/mergeresult.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2010-2013 The pygit2 contributors
+ *
+ * This file is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License, version 2,
+ * as published by the Free Software Foundation.
+ *
+ * In addition to the permissions in the GNU General Public License,
+ * the authors give you unlimited permission to link the compiled
+ * version of this file into combinations with other programs,
+ * and to distribute those combinations without any restriction
+ * coming from the use of this file. (The General Public License
+ * restrictions do apply in other respects; for example, they cover
+ * modification of the file, and distribution when not linked into
+ * a combined executable.)
+ *
+ * This file is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; see the file COPYING. If not, write to
+ * the Free Software Foundation, 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#ifndef INCLUDE_pygit2_merge_result_h
+#define INCLUDE_pygit2_merge_result_h
+
+#define PY_SSIZE_T_CLEAN
+#include <Python.h>
+#include <git2.h>
+
+PyObject* git_merge_result_to_python(git_merge_result *merge_result, Repository *repo);
+
+#endif
View
5 src/pygit2.c
@@ -67,6 +67,7 @@ extern PyTypeObject NoteIterType;
extern PyTypeObject BlameType;
extern PyTypeObject BlameIterType;
extern PyTypeObject BlameHunkType;
+extern PyTypeObject MergeResultType;
@@ -428,6 +429,10 @@ moduleinit(PyObject* m)
ADD_CONSTANT_INT(m, GIT_BLAME_TRACK_COPIES_SAME_COMMIT_COPIES)
ADD_CONSTANT_INT(m, GIT_BLAME_TRACK_COPIES_ANY_COMMIT_COPIES)
+ /* Merge */
+ INIT_TYPE(MergeResultType, NULL, NULL)
+ ADD_TYPE(m, MergeResult)
+
/* Global initialization of libgit2 */
git_threads_init();
View
53 src/repository.c
@@ -38,6 +38,7 @@
#include "remote.h"
#include "branch.h"
#include "blame.h"
+#include "mergeresult.h"
#include <git2/odb_backend.h>
extern PyObject *GitError;
@@ -578,6 +579,55 @@ Repository_merge_base(Repository *self, PyObject *args)
return git_oid_to_python(&oid);
}
+PyDoc_STRVAR(Repository_merge__doc__,
+ "merge(oid) -> MergeResult\n"
+ "\n"
+ "Merges the given oid and returns the MergeResult.\n"
+ "\n"
+ "If the merge is fastforward the MergeResult will contain the new\n"
+ "fastforward oid.\n"
+ "If the branch is uptodate, nothing to merge, the MergeResult will\n"
+ "have the fastforward oid as None.\n"
+ "If the merge is not fastforward the MergeResult will have the status\n"
+ "produced by the merge, even if there are conflicts.");
+
+PyObject *
+Repository_merge(Repository *self, PyObject *py_oid)
+{
+ git_merge_result *merge_result;
+ git_merge_head *oid_merge_head;
+ git_oid oid;
+ const git_merge_opts default_opts = GIT_MERGE_OPTS_INIT;
+ int err;
+ size_t len;
+ PyObject *py_merge_result;
+
+ len = py_oid_to_git_oid(py_oid, &oid);
+ if (len == 0)
+ return NULL;
+
+ err = git_merge_head_from_oid(&oid_merge_head, self->repo, &oid);
+ if (err < 0)
+ goto error;
+
+ err = git_merge(&merge_result, self->repo,
+ (const git_merge_head **)&oid_merge_head, 1,
+ &default_opts);
+ if (err < 0) {
+ git_merge_head_free(oid_merge_head);
+ goto error;
+ }
+
+ py_merge_result = git_merge_result_to_python(merge_result, self);
+
+ git_merge_head_free(oid_merge_head);
+
+ return py_merge_result;
+
+error:
+ return Error_set(err);
+}
+
PyDoc_STRVAR(Repository_walk__doc__,
"walk(oid, sort_mode) -> iterator\n"
"\n"
@@ -1093,7 +1143,7 @@ PyDoc_STRVAR(Repository_status__doc__,
"paths as keys and status flags as values. See pygit2.GIT_STATUS_*.");
PyObject *
-Repository_status(Repository *self, PyObject *args)
+Repository_status(Repository *self)
{
PyObject *dict;
int err;
@@ -1551,6 +1601,7 @@ PyMethodDef Repository_methods[] = {
METHOD(Repository, TreeBuilder, METH_VARARGS),
METHOD(Repository, walk, METH_VARARGS),
METHOD(Repository, merge_base, METH_VARARGS),
+ METHOD(Repository, merge, METH_O),
METHOD(Repository, read, METH_O),
METHOD(Repository, write, METH_VARARGS),
METHOD(Repository, create_reference_direct, METH_VARARGS),
View
4 src/repository.h
@@ -63,10 +63,12 @@ PyObject*
Repository_create_reference(Repository *self, PyObject *args, PyObject* kw);
PyObject* Repository_packall_references(Repository *self, PyObject *args);
-PyObject* Repository_status(Repository *self, PyObject *args);
+PyObject* Repository_status(Repository *self);
PyObject* Repository_status_file(Repository *self, PyObject *value);
PyObject* Repository_TreeBuilder(Repository *self, PyObject *args);
PyObject* Repository_blame(Repository *self, PyObject *args, PyObject *kwds);
+PyObject* Repository_merge(Repository *self, PyObject *py_oid);
+
#endif
View
2  src/types.h
@@ -217,5 +217,7 @@ typedef struct {
char boundary;
} BlameHunk;
+/* git_merge */
+SIMPLE_TYPE(MergeResult, git_merge_result, result)
#endif
View
BIN  test/data/testrepoformerging.tar
Binary file not shown
View
73 test/test_repository.py
@@ -302,6 +302,76 @@ def test_reset_mixed(self):
self.assertTrue("bonjour le monde\n" in diff.patch)
+class RepositoryTest_III(utils.RepoTestCaseForMerging):
+
+ def test_merge_none(self):
+ self.assertRaises(TypeError, self.repo.merge, None)
+
+ def test_merge_uptodate(self):
+ branch_head_hex = '5ebeeebb320790caf276b9fc8b24546d63316533'
+ branch_oid = self.repo.get(branch_head_hex).oid
+ merge_result = self.repo.merge(branch_oid)
+ self.assertTrue(merge_result.is_uptodate)
+ self.assertFalse(merge_result.is_fastforward)
+ self.assertEquals(None, merge_result.fastforward_oid)
+ self.assertEquals({}, self.repo.status())
+
+ def test_merge_fastforward(self):
+ branch_head_hex = 'e97b4cfd5db0fb4ebabf4f203979ca4e5d1c7c87'
+ branch_oid = self.repo.get(branch_head_hex).oid
+ merge_result = self.repo.merge(branch_oid)
+ self.assertFalse(merge_result.is_uptodate)
+ self.assertTrue(merge_result.is_fastforward)
+ # Asking twice to assure the reference counting is correct
+ self.assertEquals(branch_head_hex, merge_result.fastforward_oid.hex)
+ self.assertEquals(branch_head_hex, merge_result.fastforward_oid.hex)
+ self.assertEquals({}, self.repo.status())
+
+ def test_merge_no_fastforward_no_conflicts(self):
+ branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
+ branch_oid = self.repo.get(branch_head_hex).oid
+ merge_result = self.repo.merge(branch_oid)
+ self.assertFalse(merge_result.is_uptodate)
+ self.assertFalse(merge_result.is_fastforward)
+ # Asking twice to assure the reference counting is correct
+ self.assertEquals(None, merge_result.fastforward_oid)
+ self.assertEquals(None, merge_result.fastforward_oid)
+ self.assertEquals({'bye.txt': 1}, self.repo.status())
+ self.assertEquals({'bye.txt': 1}, self.repo.status())
+ # Checking the index works as expected
+ self.repo.index.remove('bye.txt')
+ self.repo.index.write()
+ self.assertEquals({'bye.txt': 128}, self.repo.status())
+
+ def test_merge_no_fastforward_conflicts(self):
+ branch_head_hex = '1b2bae55ac95a4be3f8983b86cd579226d0eb247'
+ branch_oid = self.repo.get(branch_head_hex).oid
+ merge_result = self.repo.merge(branch_oid)
+ self.assertFalse(merge_result.is_uptodate)
+ self.assertFalse(merge_result.is_fastforward)
+ # Asking twice to assure the reference counting is correct
+ self.assertEquals(None, merge_result.fastforward_oid)
+ self.assertEquals(None, merge_result.fastforward_oid)
+ self.assertEquals({'.gitignore': 132}, self.repo.status())
+ self.assertEquals({'.gitignore': 132}, self.repo.status())
+ # Checking the index works as expected
+ self.repo.index.add('.gitignore')
+ self.repo.index.write()
+ self.assertEquals({'.gitignore': 2}, self.repo.status())
+
+ def test_merge_invalid_hex(self):
+ branch_head_hex = '12345678'
+ self.assertRaises(KeyError, self.repo.merge, branch_head_hex)
+
+ def test_merge_already_something_in_index(self):
+ branch_head_hex = '03490f16b15a09913edb3a067a3dc67fbb8d41f1'
+ branch_oid = self.repo.get(branch_head_hex).oid
+ with open(os.path.join(self.repo.workdir, 'inindex.txt'), 'w') as f:
+ f.write('new content')
+ self.repo.index.add('inindex.txt')
+ self.assertRaises(pygit2.GitError, self.repo.merge, branch_oid)
+
+
class NewRepositoryTest(utils.NoRepoTestCase):
def test_new_repo(self):
@@ -376,8 +446,7 @@ def test_clone_bare_repository(self):
def test_clone_remote_name(self):
repo_path = "./test/data/testrepo.git/"
repo = clone_repository(
- repo_path, self._temp_dir, remote_name="custom_remote"
- )
+ repo_path, self._temp_dir, remote_name="custom_remote")
self.assertFalse(repo.is_empty)
self.assertEqual(repo.remotes[0].name, "custom_remote")
View
5 test/utils.py
@@ -146,6 +146,11 @@ class RepoTestCase(AutoRepoTestCase):
repo_spec = 'tar', 'testrepo'
+class RepoTestCaseForMerging(AutoRepoTestCase):
+
+ repo_spec = 'tar', 'testrepoformerging'
+
+
class DirtyRepoTestCase(AutoRepoTestCase):
repo_spec = 'tar', 'dirtyrepo'
Something went wrong with that request. Please try again.