diff --git a/docs/merge.rst b/docs/merge.rst index 55e411ac1..82c243a7d 100644 --- a/docs/merge.rst +++ b/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. diff --git a/src/mergeresult.c b/src/mergeresult.c new file mode 100644 index 000000000..1ce4497e0 --- /dev/null +++ b/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 +#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 */ +}; + diff --git a/src/mergeresult.h b/src/mergeresult.h new file mode 100644 index 000000000..7aadac4aa --- /dev/null +++ b/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 +#include + +PyObject* git_merge_result_to_python(git_merge_result *merge_result, Repository *repo); + +#endif diff --git a/src/pygit2.c b/src/pygit2.c index 4bd37c1df..e1766d26e 100644 --- a/src/pygit2.c +++ b/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(); diff --git a/src/repository.c b/src/repository.c index 20e2a7c50..f590507d8 100644 --- a/src/repository.c +++ b/src/repository.c @@ -38,6 +38,7 @@ #include "remote.h" #include "branch.h" #include "blame.h" +#include "mergeresult.h" #include 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), diff --git a/src/repository.h b/src/repository.h index 3c6094872..735f77480 100644 --- a/src/repository.h +++ b/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 diff --git a/src/types.h b/src/types.h index 46062a456..e69226b73 100644 --- a/src/types.h +++ b/src/types.h @@ -217,5 +217,7 @@ typedef struct { char boundary; } BlameHunk; +/* git_merge */ +SIMPLE_TYPE(MergeResult, git_merge_result, result) #endif diff --git a/test/data/testrepoformerging.tar b/test/data/testrepoformerging.tar new file mode 100644 index 000000000..c2b145803 Binary files /dev/null and b/test/data/testrepoformerging.tar differ diff --git a/test/test_repository.py b/test/test_repository.py index 43e196be8..11ebc4de3 100644 --- a/test/test_repository.py +++ b/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") diff --git a/test/utils.py b/test/utils.py index fb7ca58f9..4d941e5c3 100644 --- a/test/utils.py +++ b/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'