Skip to content

Commit ec2eb76

Browse files
committed
Add function to derive repository name from Git URL and refactor ExtensionsWindow for improved extension management. Update UI logic for custom section handling and refresh extension list after installation.
1 parent f99b456 commit ec2eb76

1 file changed

Lines changed: 153 additions & 99 deletions

File tree

  • extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton

extensions/pyRevitCore.extension/pyRevit.tab/pyRevit.panel/Extensions.smartbutton/script.py

Lines changed: 153 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@
1919
logger = script.get_logger()
2020

2121

22+
def _repo_name_from_git_url(git_url):
23+
"""Derive repo/folder name from a Git URL (e.g. .../owner/repo.git -> repo)."""
24+
if not git_url:
25+
return ""
26+
# Strip .git suffix
27+
url = git_url.rstrip("/")
28+
if url.lower().endswith(".git"):
29+
url = url[:-4]
30+
# Last path segment: after last / or, for git@host:path, after last /
31+
if "/" in url:
32+
url = url.rsplit("/", 1)[-1]
33+
elif url.startswith("git@"):
34+
# git@host:owner/repo -> owner/repo -> repo
35+
if ":" in url:
36+
url = url.split(":", 1)[1]
37+
if "/" in url:
38+
url = url.rsplit("/", 1)[-1]
39+
return url.strip() or ""
40+
41+
2242
class ExtensionPackageListItem:
2343
"""Extension object that is used in Extensions list ui.
2444
@@ -88,32 +108,18 @@ def searchable_values(self):
88108
)
89109

90110

91-
class InstallPackageMenuItem(framework.Controls.MenuItem):
92-
"""Context menu item for package installation destinations
93-
94-
Instances of this class will be set to the possible directory paths
95-
that are appropriate to install extensions in. This includes pyRevit
96-
default extension folder and all other extension folders set by the
97-
user. When installing an extension, user can select the destination
98-
from the destinations menu which contains instances of this class.
99-
100-
Attributes:
101-
InstallPackageMenuItem.install_path (str): Destination address
102-
103-
"""
104-
105-
install_path = ""
106-
107-
108111
class ExtensionsWindow(forms.WPFWindow):
109112
"""Extension window managing installation and removal of extensions"""
110113

111114
def __init__(self, xaml_file_name):
112115
forms.WPFWindow.__init__(self, xaml_file_name)
113-
self._setup_ext_dirs_ui(user_config.get_thirdparty_ext_root_dirs())
114116
self._setup_ext_pkg_ui(extpkgs.get_ext_packages())
115117
default_path = user_config.get_thirdparty_ext_root_dirs(include_default=True)[0]
116118
self.custom_ext_install_path_tb.Text = default_path
119+
if self.selected_pkg:
120+
self._update_add_custom_section_for_selection(self.selected_pkg)
121+
else:
122+
self._update_add_custom_section_for_new()
117123

118124
@property
119125
def selected_pkg(self):
@@ -138,24 +144,6 @@ def selected_pkgs(self):
138144
"""
139145
return self.extpkgs_lb.SelectedItems
140146

141-
def _setup_ext_dirs_ui(self, ext_dirs_list):
142-
"""Creates the installation destination context menu. Creates a menu
143-
item for each directory address provided in ext_dirs_list
144-
145-
Args:
146-
ext_dirs_list (list): List of destination directories
147-
148-
"""
149-
150-
for ext_dir in ext_dirs_list:
151-
ext_dir_install_menu_item = InstallPackageMenuItem()
152-
ext_dir_install_menu_item.install_path = ext_dir
153-
ext_dir_install_menu_item.Header = self.get_locale_string(
154-
"Extension.InstallPath"
155-
).format(ext_dir)
156-
ext_dir_install_menu_item.Click += self.install_ext_pkg
157-
self.ext_install_b.ContextMenu.AddChild(ext_dir_install_menu_item)
158-
159147
def _setup_ext_pkg_ui(self, ext_pkgs_list):
160148
"""Creates a list of initialized ExtensionPackageListItem objects,
161149
one for each extension package object in ext_pkgs_list
@@ -172,7 +160,13 @@ def _setup_ext_pkg_ui(self, ext_pkgs_list):
172160
self.extpkgs_lb.ItemsSource = sorted(
173161
self._exts_list, key=lambda x: x.Builtin, reverse=True
174162
)
175-
self.extpkgs_lb.SelectedIndex = 0
163+
self.extpkgs_lb.SelectedIndex = -1
164+
165+
def _refresh_extension_list(self):
166+
"""Reload extension packages and refresh the grid (e.g. after installing)."""
167+
ext_pkgs_list = extpkgs.get_ext_packages()
168+
self._setup_ext_pkg_ui(ext_pkgs_list)
169+
self._update_add_custom_section_for_new()
176170

177171
def _update_ext_info_panel(self, ext_pkg_item):
178172
"""Updated the extension information panel based on the info
@@ -217,14 +211,15 @@ def _update_ext_info_panel(self, ext_pkg_item):
217211
else:
218212
self.ext_repolink_t.Text = ""
219213

220-
# Update Installed folder info
214+
# Update install path (own line, like Developed by)
221215
if ext_pkg_item.ext_pkg.is_installed:
222-
self.show_element(self.ext_installed_l)
223-
self.ext_installed_l.Content = self.get_locale_string(
224-
"Extension.InstalledPath"
225-
).format(ext_pkg_item.ext_pkg.is_installed)
216+
self.show_element(self.ext_installpath_tb)
217+
self.ext_installpath_tb.Text = (
218+
self.get_locale_string("ExtensionInfo.InstallPathLink")
219+
+ ext_pkg_item.ext_pkg.is_installed
220+
)
226221
else:
227-
self.hide_element(self.ext_installed_l)
222+
self.hide_element(self.ext_installpath_tb)
228223

229224
# Update dependencies
230225
if ext_pkg_item.ext_pkg.dependencies:
@@ -267,9 +262,6 @@ def _update_ext_action_buttons(self, ext_pkg_items):
267262
if len(ext_pkg_items) == 1:
268263
ext_pkg_item = ext_pkg_items[0]
269264
if ext_pkg_item.ext_pkg.is_installed:
270-
# Action Button: Install
271-
self.hide_element(self.ext_install_b)
272-
273265
# Action Button: Remove
274266
if ext_pkg_item.ext_pkg.builtin:
275267
self.hide_element(self.ext_remove_b)
@@ -279,10 +271,8 @@ def _update_ext_action_buttons(self, ext_pkg_items):
279271
# Action Button: Toggle (Enable / Disable)
280272
self._update_toggle_button(enable=ext_pkg_item.ext_pkg.config.disabled)
281273
else:
282-
self.show_element(self.ext_install_b)
283274
self.hide_element(self.ext_toggle_b, self.ext_remove_b)
284275
elif len(ext_pkg_items) > 1:
285-
self.hide_element(self.ext_install_b)
286276
self.hide_element(self.ext_remove_b)
287277
# hide the button if includes any cli extensions
288278
if any([not x.ext_pkg.is_installed for x in ext_pkg_items]):
@@ -330,55 +320,123 @@ def update_ext_info(self, sender, args):
330320
self.show_element(self.ext_infopanel)
331321
self._update_ext_info_panel(self.selected_pkg)
332322
self._update_ext_action_buttons([self.selected_pkg])
323+
self._update_add_custom_section_for_selection(self.selected_pkg)
333324
elif self.selected_pkgs:
334325
self.hide_element(self.ext_infostack)
335326
self._update_ext_action_buttons(self.selected_pkgs)
327+
self._update_add_custom_section_for_new()
336328
else:
337-
self.hide_element(self.ext_infopanel)
338-
339-
def handle_install_button_popup(self, sender, args):
340-
"""Callback for Install package destination context menu
341-
342-
This callback method will popup a menu with a list of install
343-
destinations, when the install button is clicked.
344-
"""
345-
sender.ContextMenu.IsEnabled = True
346-
sender.ContextMenu.PlacementTarget = sender
347-
sender.ContextMenu.Placement = (
348-
framework.Controls.Primitives.PlacementMode.Bottom
349-
)
350-
sender.ContextMenu.IsOpen = True
351-
352-
def install_ext_pkg(self, sender, args):
353-
"""Installs the selected extension, then reloads pyRevit"""
329+
self.show_element(self.ext_infopanel)
330+
self.hide_element(self.ext_infostack)
331+
self.hide_element(self.ext_toggle_b, self.ext_remove_b)
332+
self._update_add_custom_section_for_new()
333+
334+
def _update_add_custom_section_for_selection(self, ext_pkg_item):
335+
"""Populate Add Custom section from selected extension; disable Pick, show Install only if not installed."""
336+
self.custom_git_url_tb.Text = ext_pkg_item.GitURL or ""
337+
if getattr(self, "custom_ext_name_tb", None):
338+
self.custom_ext_name_tb.Text = ext_pkg_item.Name or ""
339+
self.custom_git_url_tb.IsReadOnly = True
340+
if getattr(self, "custom_ext_name_tb", None):
341+
self.custom_ext_name_tb.IsReadOnly = True
342+
self.path_custom_ext_b.IsEnabled = False
343+
if ext_pkg_item.ext_pkg.is_installed:
344+
self.custom_ext_install_path_tb.Text = ext_pkg_item.ext_pkg.is_installed
345+
else:
346+
default_path = user_config.get_thirdparty_ext_root_dirs(include_default=True)[0]
347+
self.custom_ext_install_path_tb.Text = default_path
348+
if ext_pkg_item.ext_pkg.is_installed:
349+
self.hide_element(self.install_custom_ext_b)
350+
else:
351+
self.show_element(self.install_custom_ext_b)
352+
self.install_custom_ext_b.Content = self.get_locale_string("Buttons.InstallExtension")
353+
354+
def _update_add_custom_section_for_new(self):
355+
"""Reset Add Custom section for adding a new extension; enable Pick, show Add and install."""
356+
self.custom_git_url_tb.Text = ""
357+
if getattr(self, "custom_ext_name_tb", None):
358+
self.custom_ext_name_tb.Text = ""
359+
self.custom_git_url_tb.IsReadOnly = False
360+
if getattr(self, "custom_ext_name_tb", None):
361+
self.custom_ext_name_tb.IsReadOnly = False
362+
self.path_custom_ext_b.IsEnabled = True
363+
default_path = user_config.get_thirdparty_ext_root_dirs(include_default=True)[0]
364+
self.custom_ext_install_path_tb.Text = default_path
365+
self.show_element(self.install_custom_ext_b)
366+
self._update_ext_info_from_git_fields()
367+
368+
def _update_ext_info_from_git_fields(self):
369+
"""Update extension details panel from Git information fields when in add-new mode."""
370+
if self.custom_git_url_tb.IsReadOnly:
371+
return
372+
name = ""
373+
if getattr(self, "custom_ext_name_tb", None):
374+
name = self.custom_ext_name_tb.Text.strip()
375+
url = self.custom_git_url_tb.Text.strip()
376+
path = self.custom_ext_install_path_tb.Text.strip()
377+
if name or url or path:
378+
self.show_element(self.ext_infostack)
379+
display_name = name or (_repo_name_from_git_url(url) if url else "") or "(enter Git URL)"
380+
self.ext_name_l.Content = display_name
381+
self.ext_desc_l.Text = (url + " ") if url else ""
382+
if url and (url.startswith("http://") or url.startswith("https://")):
383+
self.ext_gitlink_t.Text = "(" + url + ")"
384+
self.ext_gitlink_hl.NavigateUri = framework.Uri(url)
385+
else:
386+
self.ext_gitlink_t.Text = ""
387+
self.ext_author_t.Text = ""
388+
self.ext_author_nolink_t.Text = ""
389+
self.ext_repolink_t.Text = ""
390+
if path:
391+
self.show_element(self.ext_installpath_tb)
392+
self.ext_installpath_tb.Text = (
393+
self.get_locale_string("ExtensionInfo.InstallPathLink") + path
394+
)
395+
else:
396+
self.hide_element(self.ext_installpath_tb)
397+
self.hide_element(self.ext_dependencies_l)
398+
else:
399+
self.hide_element(self.ext_infostack)
354400

355-
try:
356-
extpkgs.install(self.selected_pkg.ext_pkg, sender.install_path)
357-
self.Close()
358-
call_reload()
359-
except Exception as pkg_install_err:
360-
logger.error("Error installing package." " | {}".format(pkg_install_err))
361-
self.Close()
401+
def git_info_text_changed(self, sender, args):
402+
"""When Git information fields change, update the details panel if in add-new mode."""
403+
if self.custom_git_url_tb.IsReadOnly:
404+
return
405+
self._update_ext_info_from_git_fields()
406+
self.install_custom_ext_b.Content = self.get_locale_string("AddCustomExtension.AddAndInstall")
362407

363408
def custom_extension_path(self, sender, args):
364409
"Picks a folder to install to"
365410
custom_path = forms.pick_folder(owner=self)
366411
if custom_path:
367412
custom_path = os.path.normpath(custom_path)
368413
self.custom_ext_install_path_tb.Text = custom_path if custom_path else ""
414+
self._update_ext_info_from_git_fields()
369415

370416
def install_custom_extension(self, sender, args):
371-
"""Installs a custom extension from a Git URL
372-
373-
This mimics the behavior of:
374-
pyrevit extend ui ExtName https://github.com/user/repo.git
375-
--dest="path" --token=token
376-
"""
417+
"""Installs a custom extension from a Git URL or the selected catalog extension."""
377418

378419
try:
379-
# Get values from UI
420+
# Catalog install: selected extension from list, not yet installed
421+
if self.selected_pkg and not self.selected_pkg.ext_pkg.is_installed:
422+
dest_path = self.custom_ext_install_path_tb.Text
423+
if not dest_path:
424+
ext_dirs = user_config.get_thirdparty_ext_root_dirs(include_default=True)
425+
dest_path = ext_dirs[0]
426+
token = self.custom_token_pb.Password.strip()
427+
if token:
428+
self.selected_pkg.ext_pkg.config.private_repo = True
429+
self.selected_pkg.ext_pkg.config.token = token
430+
extpkgs.install(self.selected_pkg.ext_pkg, dest_path)
431+
self._refresh_extension_list()
432+
self.Close()
433+
call_reload()
434+
return
435+
436+
# Add new extension from Git URL
380437
git_url = self.custom_git_url_tb.Text.strip()
381-
ext_name = self.custom_ext_name_tb.Text.strip()
438+
_name_tb = getattr(self, "custom_ext_name_tb", None)
439+
ext_name = (_name_tb.Text.strip() if _name_tb else "") or _repo_name_from_git_url(git_url)
382440
token = self.custom_token_pb.Password.strip()
383441

384442
# Validation
@@ -387,7 +445,7 @@ def install_custom_extension(self, sender, args):
387445
return
388446

389447
if not ext_name:
390-
forms.alert("Please enter an extension name.", exitscript=False)
448+
forms.alert("Could not derive extension name from URL. Please enter a Git URL with a repo path (e.g. .../owner/repo.git).", exitscript=False)
391449
return
392450

393451
# Check if URL is valid git URL
@@ -402,24 +460,15 @@ def install_custom_extension(self, sender, args):
402460
)
403461
return
404462

405-
# If token is provided, inject it into the URL
406-
if token:
407-
# For HTTPS URLs, inject token
408-
if git_url.startswith("https://") or git_url.startswith("http://"):
409-
# Parse URL to inject token
410-
# Format: https://oauth2:TOKEN@github.com/user/repo.git
411-
url_parts = git_url.split("://", 1)
412-
if len(url_parts) == 2:
413-
protocol = url_parts[0]
414-
rest = url_parts[1]
415-
416-
# Remove any existing credentials
417-
if "@" in rest:
418-
# Already has credentials, replace them
419-
rest = rest.split("@", 1)[1]
420-
421-
# Inject token (use 'oauth2' as username for GitLab compatibility)
422-
git_url = "{0}://oauth2:{1}@{2}".format(protocol, token, rest)
463+
# Use a clean URL; token is passed via config and used by git_clone
464+
# (embedding credentials in the URL can cause "too many redirects or
465+
# authentication replays" with libgit2)
466+
if git_url.startswith("https://") or git_url.startswith("http://"):
467+
if "@" in git_url.split("://", 1)[1].split("/")[0]:
468+
# Strip existing credentials from URL
469+
protocol, rest = git_url.split("://", 1)
470+
rest = rest.split("@", 1)[-1]
471+
git_url = protocol + "://" + rest
423472

424473
# Get default extension directory
425474
dest_path = self.custom_ext_install_path_tb.Text
@@ -451,6 +500,7 @@ def install_custom_extension(self, sender, args):
451500
temp_pkg.config.token = token
452501

453502
extpkgs.install(temp_pkg, dest_path)
503+
self._refresh_extension_list()
454504

455505
forms.alert(
456506
'Extension "{}" installed successfully! \n'
@@ -489,6 +539,10 @@ def remove_ext_pkg(self, sender, args):
489539
call_reload()
490540
except Exception as pkg_remove_err:
491541
logger.error("Error removing package. | {}".format(pkg_remove_err))
542+
forms.alert(
543+
"Error removing extension:\n{}".format(str(pkg_remove_err)),
544+
exitscript=False,
545+
)
492546

493547

494548
def open_ext_dirs_in_explorer(ext_dirs_list):

0 commit comments

Comments
 (0)