Skip to content

Commit e59b8e2

Browse files
authored
Merge pull request #506 from fcollonval/498-fix-loading-config
Fix multiline Git options interpretation
2 parents f7584be + 07220a5 commit e59b8e2

File tree

2 files changed

+111
-5
lines changed

2 files changed

+111
-5
lines changed

jupyterlab_git/git.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Module for executing git commands, sending results back to the handlers
33
"""
44
import os
5+
import re
56
import subprocess
67
from subprocess import Popen, PIPE, CalledProcessError
78

@@ -10,7 +11,11 @@
1011
from tornado.web import HTTPError
1112

1213

14+
# Git configuration options exposed through the REST API
1315
ALLOWED_OPTIONS = ['user.name', 'user.email']
16+
# Regex pattern to capture (key, value) of Git configuration options.
17+
# See https://git-scm.com/docs/git-config#_syntax for git var syntax
18+
CONFIG_PATTERN = re.compile(r"(?:^|\n)([\w\-\.]+)\=")
1419

1520

1621
class GitAuthInputWrapper:
@@ -102,11 +107,8 @@ def config(self, top_repo_path, **kwargs):
102107
response["message"] = error.decode("utf-8").strip()
103108
else:
104109
raw = output.decode("utf-8").strip()
105-
response["options"] = dict()
106-
for l in raw.split("\n"):
107-
k, v = l.split("=", maxsplit=1)
108-
if k in ALLOWED_OPTIONS:
109-
response["options"][k] = v
110+
s = CONFIG_PATTERN.split(raw)
111+
response["options"] = {k:v for k, v in zip(s[1::2], s[2::2]) if k in ALLOWED_OPTIONS}
110112

111113
return response
112114

jupyterlab_git/tests/test_config.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,110 @@ def test_git_get_config_success(self, popen):
5050
},
5151
}
5252

53+
@patch("subprocess.Popen")
54+
def test_git_get_config_multiline(self, popen):
55+
# Given
56+
process_mock = Mock()
57+
attrs = {
58+
"communicate": Mock(
59+
return_value=(
60+
b"user.name=John Snow\n"
61+
62+
b'alias.summary=!f() { printf "Summary of this branch...\n'
63+
b'"; printf "%s\n'
64+
b'" $(git rev-parse --abbrev-ref HEAD); printf "\n'
65+
b"Most-active files, with churn count\n"
66+
b'"; git churn | head -7; }; f\n'
67+
b'alias.topic-base-branch-name=!f(){ printf "master\n'
68+
b'"; };f\n'
69+
b'alias.topic-start=!f(){ topic_branch="$1"; git topic-create "$topic_branch"; git topic-push; };f',
70+
b"",
71+
)
72+
),
73+
"returncode": 0,
74+
}
75+
process_mock.configure_mock(**attrs)
76+
popen.return_value = process_mock
77+
78+
# When
79+
body = {"path": "test_path"}
80+
response = self.tester.post(["config"], body=body)
81+
82+
# Then
83+
popen.assert_called_once_with(
84+
["git", "config", "--list"],
85+
stdout=subprocess.PIPE,
86+
stderr=subprocess.PIPE,
87+
cwd="test_path",
88+
)
89+
process_mock.communicate.assert_called_once_with()
90+
91+
assert response.status_code == 201
92+
payload = response.json()
93+
assert payload == {
94+
"code": 0,
95+
"options": {
96+
"user.name": "John Snow",
97+
"user.email": "[email protected]",
98+
},
99+
}
100+
101+
@patch("subprocess.Popen")
102+
@patch(
103+
"jupyterlab_git.git.ALLOWED_OPTIONS",
104+
["alias.summary", "alias.topic-base-branch-name"],
105+
)
106+
def test_git_get_config_accepted_multiline(self, popen):
107+
# Given
108+
process_mock = Mock()
109+
attrs = {
110+
"communicate": Mock(
111+
return_value=(
112+
b"user.name=John Snow\n"
113+
114+
b'alias.summary=!f() { printf "Summary of this branch...\n'
115+
b'"; printf "%s\n'
116+
b'" $(git rev-parse --abbrev-ref HEAD); printf "\n'
117+
b"Most-active files, with churn count\n"
118+
b'"; git churn | head -7; }; f\n'
119+
b'alias.topic-base-branch-name=!f(){ printf "master\n'
120+
b'"; };f\n'
121+
b'alias.topic-start=!f(){ topic_branch="$1"; git topic-create "$topic_branch"; git topic-push; };f',
122+
b"",
123+
)
124+
),
125+
"returncode": 0,
126+
}
127+
process_mock.configure_mock(**attrs)
128+
popen.return_value = process_mock
129+
130+
# When
131+
body = {"path": "test_path"}
132+
response = self.tester.post(["config"], body=body)
133+
134+
# Then
135+
popen.assert_called_once_with(
136+
["git", "config", "--list"],
137+
stdout=subprocess.PIPE,
138+
stderr=subprocess.PIPE,
139+
cwd="test_path",
140+
)
141+
process_mock.communicate.assert_called_once_with()
142+
143+
assert response.status_code == 201
144+
payload = response.json()
145+
assert payload == {
146+
"code": 0,
147+
"options": {
148+
"alias.summary": '!f() { printf "Summary of this branch...\n'
149+
'"; printf "%s\n'
150+
'" $(git rev-parse --abbrev-ref HEAD); printf "\n'
151+
"Most-active files, with churn count\n"
152+
'"; git churn | head -7; }; f',
153+
"alias.topic-base-branch-name": '!f(){ printf "master\n"; };f',
154+
},
155+
}
156+
53157
@patch("subprocess.Popen")
54158
def test_git_set_config_success(self, popen):
55159
# Given

0 commit comments

Comments
 (0)