Repository: PyGithub/PyGithub

Issue:
InputGitTreeElement should allow passing "null" for sha
Github's [Tree creation api](https://developer.github.com/v3/git/trees/#create-a-tree) allows us to pass `sha = null` to indicate that the specified blob needs to be deleted.

However, I don't have a way to pass this info to my `InputGitTreeElement`. I can either give it a str or a `github.GithubObject.NotSet`. This means I have no way of deleting files from a tree using PyGithub (I'd like to delete multiple files in a single commit so tree creation is the ideal choice for me).

The current design is to only pass the `sha` if it is actually set:
https://github.com/PyGithub/PyGithub/blob/540a085001/github/InputGitTreeElement.py#L81

I can understand that passing a `None` goes against the design. I think something like `github.GithubObject.Null` could be introduced to explicitly say that this field is `null`. It can be used everywhere the GH API accepts a null value.

Example
```python
new_tree = repo.create_git_tree(
    [
        InputGitTreeElement(
            path="my/dir/my_file.txt", mode="100644", type="blob", sha=github.GithubObject.Null
        ),
   ],
    base_tree=head_commit.tree
)
```
This will delete `my/dir/my_file.txt`

---

My current workaround is to directly hit the api to create tree (using requests, setting `sha=None`), get the tree sha & use it with pygithub for my remaining workflow (committing, etc).

Please let me know in case I misunderstood some aspect or if anything needs to be elaborated upon.


Discussion:
Actually NotSet is an class in it's own right that has nothing to do with None, so if you pass in None, it *should* work fine.
Oh, wait, I see. We assert it's a str! Right, okay, so I think loosening to allow None is perfectly fine then, nice catch.
I'm having a lot of trouble with this change. I've made the necessary changes, but the create_git_tree() API either returns 422 GitRPC::BadObjectState or a generic 404 Not Found error.  Are you able to provide a working example against a public repo so I can grab the replay data for the unit test?
My code is for a private repo, so I just created a stripped-down version of it for you here:

```python
import json
import requests
from http import HTTPStatus
from github import Github

TOKEN = "<GITHUB TOKEN>"
gh_client = Github(TOKEN)

repo = gh_client.get_repo("duaraghav8/test")
my_branch = "tree-test"

# paths is list of files to delete
paths = ["pods/raghav/network/resources.tf", "abc"]

sha = repo.get_branch(my_branch).commit.sha
head_commit = repo.get_git_commit(sha)

tree_objects = [
    {"path": path, "mode": "100644", "type": "blob", "sha": None}
    for path in paths
]
payload = {
    "base_tree": head_commit.tree.sha,
    "tree": tree_objects,
}
headers = {"Authorization": f"token {TOKEN}"}
url = f"{repo.url}/git/trees"

response = requests.post(url, headers=headers, data=json.dumps(payload))
if response.status_code != HTTPStatus.CREATED:
    raise RuntimeError("Failed")

new_tree_sha = response.json()["sha"]
tree = repo.get_git_tree(new_tree_sha)
```

1 reason I too received `422 GitRPC::BadObjectState` was because I was supplying filenames in `paths` that didn't exist in the base tree. For eg, try corrupting 1 of the filenames above.

I can't make my test repo public, but here is the structure, just setup dummy files in your own repo

```
abc
pods/
  raghav/
    network/
      resources.tf
```
Thanks so much! I'll give this a try in the next day or so. Maybe without the f-strings. :-)

Resolving patch:
diff --git a/github/InputGitTreeElement.py b/github/InputGitTreeElement.py
--- a/github/InputGitTreeElement.py
+++ b/github/InputGitTreeElement.py
@@ -53,7 +53,7 @@ def __init__(
         :param mode: string
         :param type: string
         :param content: string
-        :param sha: string
+        :param sha: string or None
         """
 
         assert isinstance(path, (str, six.text_type)), path
@@ -62,8 +62,10 @@ def __init__(
         assert content is github.GithubObject.NotSet or isinstance(
             content, (str, six.text_type)
         ), content
-        assert sha is github.GithubObject.NotSet or isinstance(
-            sha, (str, six.text_type)
+        assert (
+            sha is github.GithubObject.NotSet
+            or sha is None
+            or isinstance(sha, (str, six.text_type))
         ), sha
         self.__path = path
         self.__mode = mode


Test patch:
diff --git a/tests/ReplayData/Repository.testCreateGitTreeWithNullSha.txt b/tests/ReplayData/Repository.testCreateGitTreeWithNullSha.txt
new file mode 100644
--- /dev/null
+++ b/tests/ReplayData/Repository.testCreateGitTreeWithNullSha.txt
@@ -0,0 +1,10 @@
+https
+POST
+api.github.com
+None
+/repos/jacquev6/PyGithub/git/trees
+{'Content-Type': 'application/json', 'Authorization': 'Basic login_and_password_removed', 'User-Agent': 'PyGithub/Python'}
+{"tree": [{"path": "Baz.bar", "type": "blob", "mode": "100644", "sha": null}]}
+201
+[('date', 'Fri, 27 Dec 2019 00:51:47 GMT'), ('content-type', 'application/json; charset=utf-8'), ('content-length', '401'), ('server', 'GitHub.com'), ('status', '201 Created'), ('x-ratelimit-limit', '5000'), ('x-ratelimit-remaining', '4996'), ('x-ratelimit-reset', '1577411349'), ('cache-control', 'private, max-age=60, s-maxage=60'), ('vary', 'Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding'), ('etag', '"92c715dcb38b27913fafb5f0b0033832"'), ('x-oauth-scopes', 'public_repo, repo:status'), ('x-accepted-oauth-scopes', ''), ('location', 'https://api.github.com/repos/jacquev6/PyGithub/git/trees/9b8166fc80d0f0fe9192d4bf1dbaa87f194e012f'), ('x-github-media-type', 'github.v3; format=json'), ('access-control-expose-headers', 'ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type'), ('access-control-allow-origin', '*'), ('strict-transport-security', 'max-age=31536000; includeSubdomains; preload'), ('x-frame-options', 'deny'), ('x-content-type-options', 'nosniff'), ('x-xss-protection', '1; mode=block'), ('referrer-policy', 'origin-when-cross-origin, strict-origin-when-cross-origin'), ('content-security-policy', "default-src 'none'"), ('x-github-request-id', 'CDCA:2D1A:67EE9D:75F15E:5E0555A3')]
+{"sha":"9b8166fc80d0f0fe9192d4bf1dbaa87f194e012f","url":"https://api.github.com/repos/jacquev6/PyGithub/git/trees/9b8166fc80d0f0fe9192d4bf1dbaa87f194e012f","tree":[{"path":"README.md","mode":"100644","type":"blob","sha":"54f3a4bf0a60f29d7c4798b590f92ffd56dd6d21","size":12,"url":"https://api.github.com/repos/jacquev6/PyGithub/git/blobs/54f3a4bf0a60f29d7c4798b590f92ffd56dd6d21"}],"truncated":false}
diff --git a/tests/Repository.py b/tests/Repository.py
--- a/tests/Repository.py
+++ b/tests/Repository.py
@@ -283,6 +283,19 @@ def testCreateGitTreeWithSha(self):
         )
         self.assertEqual(tree.sha, "fae707821159639589bf94f3fb0a7154ec5d441b")
 
+    def testCreateGitTreeWithNullSha(self):
+        tree = self.repo.create_git_tree(
+            [
+                github.InputGitTreeElement(
+                    "Baz.bar",
+                    "100644",
+                    "blob",
+                    sha=None,
+                )
+            ]
+        )
+        self.assertEqual(tree.sha, "9b8166fc80d0f0fe9192d4bf1dbaa87f194e012f")
+
     def testCreateGitCommit(self):
         tree = self.repo.get_git_tree("107139a922f33bab6fbeb9f9eb8787e7f19e0528")
         commit = self.repo.create_git_commit("Commit created by PyGithub", tree, [])