Browse Source

Merge branch 'git-credential-store' of git+ssh://github.com/manueljacob/dulwich

Jelmer Vernooij 4 years ago
parent
commit
0da4174a89
4 changed files with 82 additions and 7 deletions
  1. 26 0
      dulwich/client.py
  2. 8 5
      dulwich/config.py
  3. 3 2
      dulwich/ignore.py
  4. 45 0
      dulwich/tests/test_client.py

+ 26 - 0
dulwich/client.py

@@ -40,6 +40,8 @@ Known capabilities that are not supported:
 
 from contextlib import closing
 from io import BytesIO, BufferedReader
+import errno
+import os
 import select
 import socket
 import subprocess
@@ -58,6 +60,7 @@ except ImportError:
     import urllib.parse as urlparse
 
 import dulwich
+from dulwich.config import get_xdg_config_home_path
 from dulwich.errors import (
     GitProtocolError,
     NotGitRepository,
@@ -1873,3 +1876,26 @@ def get_transport_and_path(location, **kwargs):
         return default_local_git_client_cls(**kwargs), location
     else:
         return SSHGitClient(hostname, username=username, **kwargs), path
+
+
+DEFAULT_GIT_CREDENTIALS_PATHS = [
+    os.path.expanduser('~/.git-credentials'),
+    get_xdg_config_home_path('git', 'credentials')]
+
+def get_credentials_from_store(scheme, hostname, username=None,
+                               fnames=DEFAULT_GIT_CREDENTIALS_PATHS):
+    for fname in fnames:
+        try:
+            with open(fname, 'rb') as f:
+                for line in f:
+                    parsed_line = urlparse.urlparse(line)
+                    if (parsed_line.scheme == scheme and
+                            parsed_line.hostname == hostname and
+                            (username is None or
+                                parsed_line.username == username)):
+                        return parsed_line.username, parsed_line.password
+        except OSError as e:
+            if e.errno != errno.ENOENT:
+                raise
+            # If the file doesn't exist, try the next one.
+            continue

+ 8 - 5
dulwich/config.py

@@ -487,6 +487,13 @@ class ConfigFile(ConfigDict):
                 f.write(b"\t" + key + b" = " + value + b"\n")
 
 
+def get_xdg_config_home_path(*path_segments):
+    xdg_config_home = os.environ.get(
+        "XDG_CONFIG_HOME", os.path.expanduser("~/.config/"),
+    )
+    return os.path.join(xdg_config_home, *path_segments)
+
+
 class StackedConfig(Config):
     """Configuration which reads from multiple config files.."""
 
@@ -509,11 +516,7 @@ class StackedConfig(Config):
         """
         paths = []
         paths.append(os.path.expanduser("~/.gitconfig"))
-
-        xdg_config_home = os.environ.get(
-            "XDG_CONFIG_HOME", os.path.expanduser("~/.config/"),
-        )
-        paths.append(os.path.join(xdg_config_home, "git", "config"))
+        paths.append(get_xdg_config_home_path("git", "config"))
 
         if "GIT_CONFIG_NOSYSTEM" not in os.environ:
             paths.append("/etc/gitconfig")

+ 3 - 2
dulwich/ignore.py

@@ -26,6 +26,8 @@ import os.path
 import re
 import sys
 
+from dulwich.config import get_xdg_config_home_path
+
 
 def _translate_segment(segment):
     if segment == b"*":
@@ -271,8 +273,7 @@ def default_user_ignore_filter_path(config):
     except KeyError:
         pass
 
-    xdg_config_home = os.environ.get("XDG_CONFIG_HOME", "~/.config/")
-    return os.path.join(xdg_config_home, 'git', 'ignore')
+    return get_xdg_config_home_path('git', 'ignore')
 
 
 class IgnoreFilterManager(object):

+ 45 - 0
dulwich/tests/test_client.py

@@ -20,6 +20,7 @@
 
 from io import BytesIO
 import base64
+import os
 import sys
 import shutil
 import tempfile
@@ -57,6 +58,7 @@ from dulwich.client import (
     UpdateRefsError,
     check_wants,
     default_urllib3_manager,
+    get_credentials_from_store,
     get_transport_and_path,
     get_transport_and_path_from_url,
     parse_rsync_url,
@@ -1312,3 +1314,46 @@ class FetchPackResultTests(TestCase):
                 {b'refs/heads/master':
                  b'2f3dc7a53fb752a6961d3a56683df46d4d3bf262'}, {},
                 b'user/agent'))
+
+
+class GitCredentialStoreTests(TestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        with tempfile.NamedTemporaryFile(delete=False) as f:
+            f.write(b'https://user:pass@example.org')
+        cls.fname = f.name
+
+    @classmethod
+    def tearDownClass(cls):
+        os.unlink(cls.fname)
+
+    def test_nonmatching_scheme(self):
+        self.assertEqual(
+            get_credentials_from_store(
+                b'http', b'example.org', fnames=[self.fname]),
+            None)
+
+    def test_nonmatching_hostname(self):
+        self.assertEqual(
+            get_credentials_from_store(
+                b'https', b'noentry.org', fnames=[self.fname]),
+            None)
+
+    def test_match_without_username(self):
+        self.assertEqual(
+            get_credentials_from_store(
+                b'https', b'example.org', fnames=[self.fname]),
+            (b'user', b'pass'))
+
+    def test_match_with_matching_username(self):
+        self.assertEqual(
+            get_credentials_from_store(
+                b'https', b'example.org', b'user', fnames=[self.fname]),
+            (b'user', b'pass'))
+
+    def test_no_match_with_nonmatching_username(self):
+        self.assertEqual(
+            get_credentials_from_store(
+                b'https', b'example.org', b'otheruser', fnames=[self.fname]),
+            None)