Repository: python-gitlab/python-gitlab

Issue:
Add "submodules" API
## Description of the problem, including code/CLI snippet
Since GitLab 11.5 there is an API for handling Git submodules.
https://docs.gitlab.com/ee/api/repository_submodules.html

I would love to help here, but i don't get how to add new API elements. python-gitlab looks like having a nice pluggable structure, but currently i do not understand what i need to do exactly. A short introduction for developers in the documentation would be nice.

## Expected Behavior
Having submodules API available in python-gitlab

## Actual Behavior
Submodules API not implemented

## Specifications

  - python-gitlab version: 1.11
  - API version you are using (v3/v4): v4
  - Gitlab server version (or gitlab.com): 12.2.4



Discussion:

Resolving patch:
diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py
--- a/gitlab/v4/objects.py
+++ b/gitlab/v4/objects.py
@@ -3885,6 +3885,29 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject):
         ("wikis", "ProjectWikiManager"),
     )
 
+    @cli.register_custom_action("Project", ("submodule", "branch", "commit_sha"))
+    @exc.on_http_error(exc.GitlabUpdateError)
+    def update_submodule(self, submodule, branch, commit_sha, **kwargs):
+        """Update a project submodule
+
+        Args:
+            submodule (str): Full path to the submodule
+            branch (str): Name of the branch to commit into
+            commit_sha (str): Full commit SHA to update the submodule to
+            commit_message (str): Commit message. If no message is provided, a default one will be set (optional)
+
+        Raises:
+            GitlabAuthenticationError: If authentication is not correct
+            GitlabPutError: If the submodule could not be updated
+        """
+
+        submodule = submodule.replace("/", "%2F")  # .replace('.', '%2E')
+        path = "/projects/%s/repository/submodules/%s" % (self.get_id(), submodule)
+        data = {"branch": branch, "commit_sha": commit_sha}
+        if "commit_message" in kwargs:
+            data["commit_message"] = kwargs["commit_message"]
+        return self.manager.gitlab.http_put(path, post_data=data)
+
     @cli.register_custom_action("Project", tuple(), ("path", "ref", "recursive"))
     @exc.on_http_error(exc.GitlabGetError)
     def repository_tree(self, path="", ref="", recursive=False, **kwargs):


Test patch:
diff --git a/gitlab/tests/test_gitlab.py b/gitlab/tests/test_gitlab.py
--- a/gitlab/tests/test_gitlab.py
+++ b/gitlab/tests/test_gitlab.py
@@ -717,6 +717,56 @@ def resp_mark_all_as_done(url, request):
         with HTTMock(resp_mark_all_as_done):
             self.gl.todos.mark_all_as_done()
 
+    def test_update_submodule(self):
+        @urlmatch(
+            scheme="http", netloc="localhost", path="/api/v4/projects/1$", method="get"
+        )
+        def resp_get_project(url, request):
+            headers = {"content-type": "application/json"}
+            content = '{"name": "name", "id": 1}'.encode("utf-8")
+            return response(200, content, headers, None, 5, request)
+
+        @urlmatch(
+            scheme="http",
+            netloc="localhost",
+            path="/api/v4/projects/1/repository/submodules/foo%2Fbar",
+            method="put",
+        )
+        def resp_update_submodule(url, request):
+            headers = {"content-type": "application/json"}
+            content = """{
+            "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+            "short_id": "ed899a2f4b5",
+            "title": "Message",
+            "author_name": "Author",
+            "author_email": "author@example.com",
+            "committer_name": "Author",
+            "committer_email": "author@example.com",
+            "created_at": "2018-09-20T09:26:24.000-07:00",
+            "message": "Message", 
+            "parent_ids": [ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba" ], 
+            "committed_date": "2018-09-20T09:26:24.000-07:00",
+            "authored_date": "2018-09-20T09:26:24.000-07:00", 
+            "status": null}"""
+            content = content.encode("utf-8")
+            return response(200, content, headers, None, 5, request)
+
+        with HTTMock(resp_get_project):
+            project = self.gl.projects.get(1)
+            self.assertIsInstance(project, Project)
+            self.assertEqual(project.name, "name")
+            self.assertEqual(project.id, 1)
+        with HTTMock(resp_update_submodule):
+            ret = project.update_submodule(
+                submodule="foo/bar",
+                branch="master",
+                commit_sha="4c3674f66071e30b3311dac9b9ccc90502a72664",
+                commit_message="Message",
+            )
+            self.assertIsInstance(ret, dict)
+            self.assertEqual(ret["message"], "Message")
+            self.assertEqual(ret["id"], "ed899a2f4b50b4370feeea94676502b42383c746")
+
     def _default_config(self):
         fd, temp_path = tempfile.mkstemp()
         os.write(fd, valid_config)