Skip to content

Commit 300c3b0

Browse files
authored
Merge pull request #60 from mxstack/fix/35-parallel-credential-prompts
Fix credential prompts overlapping in parallel VCS operations
2 parents 241db6e + c662826 commit 300c3b0

File tree

6 files changed

+140
-5
lines changed

6 files changed

+140
-5
lines changed

CHANGES.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 4.1.2 (unreleased)
44

5+
- Fix #35: Add `smart-threading` configuration option to prevent overlapping credential prompts when using HTTPS URLs. When enabled (default), HTTPS packages are processed serially first to ensure clean credential prompts, then other packages are processed in parallel for speed. Can be disabled with `smart-threading = false` if you have credential helpers configured.
6+
[jensens]
7+
58
- Fix #34: The `offline` configuration setting and `--offline` CLI flag are now properly respected to prevent VCS fetch/update operations. Previously, setting `offline = true` in mx.ini or using the `--offline` CLI flag was ignored, and VCS operations still occurred.
69
[jensens]
710

CLAUDE.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -599,13 +599,22 @@ Quick summary:
599599
- Create unreleased section if it doesn't exist
600600
- Include issue number when applicable
601601

602-
3. **Run relevant tests locally**
602+
3. **Always check and update documentation**
603+
- **README.md**: Update configuration tables, usage examples, or feature descriptions
604+
- **EXTENDING.md**: Update if hooks API changed
605+
- **RELEASE.md**: Update if release process changed
606+
- Check if new configuration options need documentation
607+
- Check if new features need usage examples
608+
- Update any affected sections (don't just append)
609+
- **MANDATORY**: After any code change that adds/modifies features or configuration, verify documentation is updated
610+
611+
4. **Run relevant tests locally**
603612
```bash
604613
source .venv/bin/activate
605614
pytest tests/test_*.py -v
606615
```
607616

608-
4. **Check CI status before marking PR ready**
617+
5. **Check CI status before marking PR ready**
609618
```bash
610619
gh pr checks <PR_NUMBER>
611620
```

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,23 @@ The **main section** must be called `[settings]`, even if kept empty.
8484
|--------|-------------|---------|
8585
| `default-target` | Target directory for VCS checkouts | `./sources` |
8686
| `threads` | Number of parallel threads for fetching sources | `4` |
87+
| `smart-threading` | Process HTTPS packages serially to avoid overlapping credential prompts (see below) | `True` |
8788
| `offline` | Skip all VCS fetch operations (handy for offline work) | `False` |
8889
| `default-install-mode` | Default `install-mode` for packages: `direct` or `skip` | `direct` |
8990
| `default-update` | Default update behavior: `yes` or `no` | `yes` |
9091
| `default-use` | Default use behavior (when false, sources not checked out) | `True` |
9192

93+
##### Smart Threading
94+
95+
When `smart-threading` is enabled (default), mxdev uses a two-phase approach to prevent credential prompts from overlapping:
96+
97+
1. **Phase 1**: HTTPS packages are processed serially (one at a time) to ensure clean, visible credential prompts
98+
2. **Phase 2**: Remaining packages (SSH, local) are processed in parallel for speed
99+
100+
This solves the problem where parallel git operations would cause multiple credential prompts to overlap, making it confusing which package needs credentials.
101+
102+
**When to disable**: Set `smart-threading = false` if you have git credential helpers configured (e.g., credential cache, credential store) and never see prompts.
103+
92104
#### Package Overrides
93105

94106
##### `version-overrides`

src/mxdev/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ def __init__(
4646
else:
4747
settings["threads"] = "4"
4848

49+
# Set default for smart-threading (process HTTPS packages serially to avoid
50+
# overlapping credential prompts)
51+
settings.setdefault("smart-threading", "true")
52+
4953
mode = settings.get("default-install-mode", "direct")
5054
if mode not in ["direct", "skip"]:
5155
raise ValueError("default-install-mode must be one of 'direct' or 'skip'")

src/mxdev/processing.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,11 @@ def fetch(state: State) -> None:
191191
return
192192

193193
logger.info("# Fetch sources from VCS")
194+
smart_threading = to_bool(state.configuration.settings.get("smart-threading", True))
194195
workingcopies = WorkingCopies(
195-
packages, threads=int(state.configuration.settings["threads"])
196+
packages,
197+
threads=int(state.configuration.settings["threads"]),
198+
smart_threading=smart_threading,
196199
)
197200
# Pass offline setting from configuration instead of hardcoding False
198201
offline = to_bool(state.configuration.settings.get("offline", False))

src/mxdev/vcs/common.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,12 +163,41 @@ def get_workingcopytypes() -> typing.Dict[str, typing.Type[BaseWorkingCopy]]:
163163

164164

165165
class WorkingCopies:
166-
def __init__(self, sources: typing.Dict[str, typing.Dict], threads=5):
166+
def __init__(
167+
self,
168+
sources: typing.Dict[str, typing.Dict],
169+
threads=5,
170+
smart_threading=True,
171+
):
167172
self.sources = sources
168173
self.threads = threads
174+
self.smart_threading = smart_threading
169175
self.errors = False
170176
self.workingcopytypes = get_workingcopytypes()
171177

178+
def _separate_https_packages(
179+
self, packages: typing.List[str]
180+
) -> typing.Tuple[typing.List[str], typing.List[str]]:
181+
"""Separate HTTPS packages from others for smart threading.
182+
183+
Returns (https_packages, other_packages)
184+
"""
185+
https_packages = []
186+
other_packages = []
187+
188+
for name in packages:
189+
if name not in self.sources:
190+
other_packages.append(name)
191+
continue
192+
source = self.sources[name]
193+
url = source.get("url", "")
194+
if url.startswith("https://"):
195+
https_packages.append(name)
196+
else:
197+
other_packages.append(name)
198+
199+
return https_packages, other_packages
200+
172201
def process(self, the_queue: queue.Queue) -> None:
173202
if self.threads < 2:
174203
worker(self, the_queue)
@@ -187,6 +216,43 @@ def process(self, the_queue: queue.Queue) -> None:
187216
sys.exit(1)
188217

189218
def checkout(self, packages: typing.Iterable[str], **kwargs) -> None:
219+
# Smart threading: process HTTPS packages serially to avoid overlapping prompts
220+
packages_list = list(packages)
221+
if self.smart_threading and self.threads > 1:
222+
https_pkgs, other_pkgs = self._separate_https_packages(packages_list)
223+
if https_pkgs and other_pkgs:
224+
logger.info(
225+
"Smart threading: processing %d HTTPS package(s) serially...",
226+
len(https_pkgs),
227+
)
228+
# Save original thread count and process HTTPS packages serially
229+
original_threads = self.threads
230+
self.threads = 1
231+
self._checkout_impl(https_pkgs, **kwargs)
232+
self.threads = original_threads
233+
# Process remaining packages in parallel
234+
logger.info(
235+
"Smart threading: processing %d other package(s) in parallel...",
236+
len(other_pkgs),
237+
)
238+
self._checkout_impl(other_pkgs, **kwargs)
239+
return
240+
elif https_pkgs:
241+
logger.info(
242+
"Smart threading: processing %d HTTPS package(s) serially...",
243+
len(https_pkgs),
244+
)
245+
original_threads = self.threads
246+
self.threads = 1
247+
self._checkout_impl(packages_list, **kwargs)
248+
self.threads = original_threads
249+
return
250+
251+
# Normal processing (smart_threading disabled or threads=1)
252+
self._checkout_impl(packages_list, **kwargs)
253+
254+
def _checkout_impl(self, packages: typing.List[str], **kwargs) -> None:
255+
"""Internal implementation of checkout logic."""
190256
the_queue: queue.Queue = queue.Queue()
191257
if "update" in kwargs and not isinstance(kwargs["update"], bool):
192258
if kwargs["update"].lower() in ("true", "yes", "on", "force"):
@@ -287,12 +353,50 @@ def status(
287353
sys.exit(1)
288354

289355
def update(self, packages: typing.Iterable[str], **kwargs) -> None:
290-
the_queue: queue.Queue = queue.Queue()
291356
# Check for offline mode early - skip all updates if offline
292357
offline = kwargs.get("offline", False)
293358
if offline:
294359
logger.info("Skipped updates (offline mode)")
295360
return
361+
362+
# Smart threading: process HTTPS packages serially to avoid overlapping prompts
363+
packages_list = list(packages)
364+
if self.smart_threading and self.threads > 1:
365+
https_pkgs, other_pkgs = self._separate_https_packages(packages_list)
366+
if https_pkgs and other_pkgs:
367+
logger.info(
368+
"Smart threading: updating %d HTTPS package(s) serially...",
369+
len(https_pkgs),
370+
)
371+
# Save original thread count and process HTTPS packages serially
372+
original_threads = self.threads
373+
self.threads = 1
374+
self._update_impl(https_pkgs, **kwargs)
375+
self.threads = original_threads
376+
# Process remaining packages in parallel
377+
logger.info(
378+
"Smart threading: updating %d other package(s) in parallel...",
379+
len(other_pkgs),
380+
)
381+
self._update_impl(other_pkgs, **kwargs)
382+
return
383+
elif https_pkgs:
384+
logger.info(
385+
"Smart threading: updating %d HTTPS package(s) serially...",
386+
len(https_pkgs),
387+
)
388+
original_threads = self.threads
389+
self.threads = 1
390+
self._update_impl(packages_list, **kwargs)
391+
self.threads = original_threads
392+
return
393+
394+
# Normal processing (smart_threading disabled or threads=1)
395+
self._update_impl(packages_list, **kwargs)
396+
397+
def _update_impl(self, packages: typing.List[str], **kwargs) -> None:
398+
"""Internal implementation of update logic."""
399+
the_queue: queue.Queue = queue.Queue()
296400
for name in packages:
297401
kw = kwargs.copy()
298402
if name not in self.sources:

0 commit comments

Comments
 (0)