Skip to content

Commit 2e86494

Browse files
committed
list branches sync action
1 parent e79f278 commit 2e86494

3 files changed

Lines changed: 88 additions & 60 deletions

File tree

component_config/configSchema.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,17 @@
9999
},
100100
"branch": {
101101
"type": "string",
102+
"enum": [],
102103
"title": "Branch Name",
103-
"propertyOrder": 70
104+
"propertyOrder": 70,
105+
"options": {
106+
"async": {
107+
"cache": false,
108+
"label": "List Branches",
109+
"action": "listBranches",
110+
"autoload": []
111+
}
112+
}
104113
},
105114
"filename": {
106115
"type": "string",

src/component.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
from traceback import TracebackException
1313

1414
import dacite
15-
from keboola.component.base import ComponentBase
15+
from keboola.component.base import ComponentBase, sync_action
1616
from keboola.component.exceptions import UserException
1717

1818
import source_file
1919
import source_git
20-
from configuration import Configuration, SourceEnum, encrypted_keys
20+
from configuration import AuthEnum, Configuration, SourceEnum, encrypted_keys
2121

2222

2323
class Component(ComponentBase):
@@ -37,7 +37,7 @@ def __init__(self):
3737
self.parameters = dacite.from_dict(
3838
Configuration,
3939
self.configuration.parameters,
40-
config=dacite.Config(cast=[SourceEnum], convert_key=encrypted_keys),
40+
config=dacite.Config(cast=[AuthEnum, SourceEnum], convert_key=encrypted_keys),
4141
)
4242

4343
def run(self):
@@ -122,6 +122,15 @@ def _merge_user_parameters(self):
122122
with open(os.path.join(self.data_folder_path, "config.json"), "w+") as inp:
123123
json.dump(config_data, inp)
124124

125+
@sync_action("listBranches")
126+
def get_repository_branches(self):
127+
"""
128+
Returns a list of branches in the git repository.
129+
This method is used to populate the branches dropdown in the UI.
130+
"""
131+
git_handler = source_git.GitHandler(self.parameters.git)
132+
return git_handler.get_repository_branches()
133+
125134

126135
"""
127136
Main entrypoint

src/source_git.py

Lines changed: 66 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from keboola.component.exceptions import UserException
88

9-
from configuration import GitConfiguration
9+
from configuration import AuthEnum, GitConfiguration
1010

1111

1212
class GitHandler:
@@ -16,7 +16,65 @@ def __init__(self, git_cfg: GitConfiguration):
1616
# add path for absolute imports to start at the cloned repository root level
1717
sys.path.append(os.path.join(pathlib.Path(__file__).parent.parent, self.REPO_PATH))
1818

19+
self.env = os.environ.copy()
1920
self.git_cfg = git_cfg
21+
self.repo_auth_url = None # ‼️ NEVER EVER INCLUDE THIS VARIABLE IN LOGGING OUTPUT ‼️
22+
23+
if not self.git_cfg.url:
24+
raise UserException("Git repository URL is required")
25+
26+
if self.git_cfg.auth == AuthEnum.PAT:
27+
self._set_up_token_auth()
28+
else:
29+
self._set_up_ssh_command()
30+
31+
# do not ask for credentials when git authentication fails
32+
self.env["GIT_TERMINAL_PROMPT"] = "0"
33+
34+
def _set_up_token_auth(self) -> None:
35+
if not self.git_cfg.encrypted_token:
36+
raise UserException("No personal access token provided")
37+
38+
if not self.git_cfg.url.startswith("https://"):
39+
raise UserException("PAT authentication is only supported for HTTPS URLs")
40+
41+
self.repo_auth_url = self.git_cfg.url.replace(
42+
"https://", f"https://x-token-auth:{self.git_cfg.encrypted_token}@"
43+
)
44+
logging.info("Git token authentication set up for HTTPS URL.")
45+
46+
def _set_up_ssh_command(self) -> None:
47+
if self.git_cfg.auth == AuthEnum.SSH and not self.git_cfg.ssh_keys.keys.encrypted_private:
48+
raise UserException("SSH key is required for SSH authentication")
49+
50+
repo_url = self.git_cfg.url
51+
if repo_url.startswith("git@") or repo_url.startswith("ssh://"):
52+
logging.warning("SSH URL detected but no ssh_key_path provided. Trying default SSH configuration.")
53+
54+
ssh_command = [
55+
"ssh",
56+
# the following lines could be used to disable strict host key checking, but it is better
57+
# for security reasons to use the known_hosts file prepared in Dockerfile
58+
# "-o",
59+
# "StrictHostKeyChecking=no",
60+
"-o",
61+
"BatchMode=yes", # do not ask for credentials when SSH auth fails
62+
"-o",
63+
"ConnectTimeout=30",
64+
"-o",
65+
"ServerAliveInterval=60",
66+
]
67+
68+
if self.git_cfg.ssh_keys.keys.encrypted_private:
69+
ssh_key_path = os.path.expanduser("~/.ssh/github_private_key")
70+
with open(ssh_key_path, "wb") as f:
71+
for line in self.git_cfg.ssh_keys.keys.encrypted_private.splitlines():
72+
f.write(line.encode() + b"\n")
73+
# ensure SSH key has correct permissions
74+
os.chmod(ssh_key_path, 0o600)
75+
ssh_command.extend(["-i", ssh_key_path])
76+
77+
self.env["GIT_SSH_COMMAND"] = " ".join(ssh_command)
2078

2179
def clone_repository(self):
2280
"""
@@ -25,62 +83,24 @@ def clone_repository(self):
2583
Returns:
2684
Path to the main script file to execute
2785
"""
28-
repo_url = self.git_cfg.url
29-
if not repo_url:
30-
raise UserException("Git repository URL is required")
3186

3287
branch = self.git_cfg.branch or "main"
3388

34-
logging.info("Cloning git repository: %s", repo_url)
89+
logging.info("Cloning git repository: %s", self.git_cfg.url)
3590

3691
try:
3792
clone_args = ["git", "clone"]
3893

3994
if branch:
4095
clone_args.extend(["--branch", branch])
4196

42-
if self.git_cfg.ssh_keys.keys.encrypted_private and self.git_cfg.encrypted_token:
43-
self.git_cfg.encrypted_token = None
44-
45-
if self.git_cfg.encrypted_token and repo_url.startswith("https://"):
46-
repo_url = repo_url.replace("https://", f"https://x-token-auth:{self.git_cfg.encrypted_token}@")
47-
48-
clone_args.extend([repo_url, self.REPO_PATH])
49-
50-
env = os.environ.copy()
51-
52-
ssh_command = [
53-
"ssh",
54-
# the following lines could be used to disable strict host key checking, but it is better
55-
# for security reasons to use the known_hosts file prepared in Dockerfile
56-
# "-o",
57-
# "StrictHostKeyChecking=no",
58-
"-o",
59-
"BatchMode=yes", # do not ask for credentials when SSH auth fails
60-
"-o",
61-
"ConnectTimeout=30",
62-
"-o",
63-
"ServerAliveInterval=60",
64-
]
65-
66-
if self.git_cfg.ssh_keys.keys.encrypted_private:
67-
ssh_key_path = os.path.expanduser("~/.ssh/github_private_key")
68-
with open(ssh_key_path, "wb") as f:
69-
for line in self.git_cfg.ssh_keys.keys.encrypted_private.splitlines():
70-
f.write(line.encode() + b"\n")
71-
# ensure SSH key has correct permissions
72-
os.chmod(ssh_key_path, 0o600)
73-
ssh_command.extend(["-i", ssh_key_path])
74-
elif repo_url.startswith("git@") or repo_url.startswith("ssh://"):
75-
logging.warning("SSH URL detected but no ssh_key_path provided. Trying default SSH configuration.")
76-
77-
env["GIT_SSH_COMMAND"] = " ".join(ssh_command)
97+
clone_args.extend([self.repo_auth_url or self.git_cfg.url, self.REPO_PATH])
7898

7999
process = subprocess.Popen(
80100
clone_args,
81101
stdout=subprocess.PIPE,
82102
stderr=subprocess.PIPE,
83-
env=env,
103+
env=self.env,
84104
)
85105
_, stderr = process.communicate()
86106

@@ -110,33 +130,23 @@ def get_repository_branches(self):
110130
List of branch names
111131
"""
112132
try:
113-
repo_url = self.git_cfg.url
114-
if not repo_url:
115-
raise UserException("Git repository URL is required")
116-
117133
branches_args = ["git", "ls-remote", "--heads"]
118134

119-
if self.git_cfg.ssh_keys.keys.encrypted_private and self.git_cfg.encrypted_token:
120-
self.git_cfg.encrypted_token = None
121-
122-
if self.git_cfg.encrypted_token and repo_url.startswith("https://"):
123-
repo_url = repo_url.replace("https://", f"https://x-token-auth:{self.git_cfg.encrypted_token}@")
124-
125-
branches_args.append(repo_url)
135+
branches_args.append(self.repo_auth_url or self.git_cfg.url)
126136

127137
process = subprocess.Popen(
128138
branches_args,
129139
stdout=subprocess.PIPE,
130140
stderr=subprocess.PIPE,
131-
cwd=self.REPO_PATH,
141+
env=self.env,
132142
)
133143
stdout, stderr = process.communicate()
134144

135145
if process.returncode != 0:
136146
raise UserException(f"Failed to get branches: {stderr.decode()}")
137147

138-
branches = [line.strip().split("refs/heads")[-1] for line in stdout.decode().splitlines() if line.strip()]
139-
return branches
148+
branches = [line.strip().split("refs/heads/")[-1] for line in stdout.decode().splitlines() if line.strip()]
149+
return [{"value": b, "label": b} for b in branches]
140150

141151
except Exception as e:
142152
raise UserException(f"Error getting repository branches: {str(e)}") from e

0 commit comments

Comments
 (0)