bitp 1.3.1__tar.gz → 1.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {bitp-1.3.1 → bitp-1.3.2}/PKG-INFO +1 -1
  2. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/explore.py +61 -1
  3. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/projects.py +62 -5
  4. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/query.py +39 -3
  5. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/dashboard.js +7 -1
  6. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/explore.js +59 -6
  7. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/PKG-INFO +1 -1
  8. {bitp-1.3.1 → bitp-1.3.2}/.gitignore +0 -0
  9. {bitp-1.3.1 → bitp-1.3.2}/.gitlab-ci.yml +0 -0
  10. {bitp-1.3.1 → bitp-1.3.2}/COPYING +0 -0
  11. {bitp-1.3.1 → bitp-1.3.2}/INSTALL.md +0 -0
  12. {bitp-1.3.1 → bitp-1.3.2}/README.md +0 -0
  13. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/__init__.py +0 -0
  14. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/__main__.py +0 -0
  15. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/cli.py +0 -0
  16. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/__init__.py +0 -0
  17. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/b4.py +0 -0
  18. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/branch.py +0 -0
  19. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/common.py +0 -0
  20. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/config.py +0 -0
  21. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/confjson.py +0 -0
  22. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/daemon_cmd.py +0 -0
  23. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/deps.py +0 -0
  24. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/export.py +0 -0
  25. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/fragment.py +0 -0
  26. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/info.py +0 -0
  27. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/patches.py +0 -0
  28. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/recipe.py +0 -0
  29. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/repos.py +0 -0
  30. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/search.py +0 -0
  31. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/setup.py +0 -0
  32. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/ssh_remote.py +0 -0
  33. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/commands/update.py +0 -0
  34. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/core.py +0 -0
  35. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/daemon.py +0 -0
  36. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/fzf_bindings.py +0 -0
  37. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/persona.py +0 -0
  38. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/__init__.py +0 -0
  39. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/generic/__init__.py +0 -0
  40. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/yocto/__init__.py +0 -0
  41. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/yocto/bblayers.py +0 -0
  42. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/yocto/commands/__init__.py +0 -0
  43. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/personas/yocto/layers.py +0 -0
  44. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tags.py +0 -0
  45. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tmux.py +0 -0
  46. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/__init__.py +0 -0
  47. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/base.py +0 -0
  48. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/demo_dialogs.py +0 -0
  49. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/demo_inline_prompt.py +0 -0
  50. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/dialogs.py +0 -0
  51. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/explore_dialogs.py +0 -0
  52. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/factory.py +0 -0
  53. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/fzf_backend.py +0 -0
  54. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/fzf_inline_prompt.py +0 -0
  55. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/inline_options.py +0 -0
  56. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/tui/textual_backend.py +0 -0
  57. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/__init__.py +0 -0
  58. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/api.py +0 -0
  59. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/api_commands.py +0 -0
  60. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/server.py +0 -0
  61. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/sse.py +0 -0
  62. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/favicon.svg +0 -0
  63. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/index.html +0 -0
  64. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/api.js +0 -0
  65. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/app.js +0 -0
  66. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/column-resizer.js +0 -0
  67. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/sortable.js +0 -0
  68. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/terminal-ws.js +0 -0
  69. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/utils.js +0 -0
  70. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/b4.js +0 -0
  71. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/branch.js +0 -0
  72. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/commits.js +0 -0
  73. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/config.js +0 -0
  74. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/deps.js +0 -0
  75. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/export.js +0 -0
  76. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/fragments.js +0 -0
  77. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/hosts.js +0 -0
  78. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/https.js +0 -0
  79. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/info.js +0 -0
  80. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/layer-index.js +0 -0
  81. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/messages.js +0 -0
  82. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/patches.js +0 -0
  83. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/recipes.js +0 -0
  84. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/setup.js +0 -0
  85. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/terminal.js +0 -0
  86. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/js/views/update.js +0 -0
  87. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/style.css +0 -0
  88. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/terminal.html +0 -0
  89. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/Sortable.min.js +0 -0
  90. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/diff2html-ui.min.js +0 -0
  91. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/diff2html.min.css +0 -0
  92. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/github-dark.min.css +0 -0
  93. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/highlight.min.js +0 -0
  94. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/xterm-addon-fit.min.js +0 -0
  95. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/xterm.css +0 -0
  96. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/vendor/xterm.min.js +0 -0
  97. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/static/ws-test.html +0 -0
  98. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/terminal.py +0 -0
  99. {bitp-1.3.1 → bitp-1.3.2}/bitbake_project/web/websocket.py +0 -0
  100. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/SOURCES.txt +0 -0
  101. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/dependency_links.txt +0 -0
  102. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/entry_points.txt +0 -0
  103. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/requires.txt +0 -0
  104. {bitp-1.3.1 → bitp-1.3.2}/bitp.egg-info/top_level.txt +0 -0
  105. {bitp-1.3.1 → bitp-1.3.2}/docs/images/commits.png +0 -0
  106. {bitp-1.3.1 → bitp-1.3.2}/docs/images/dashboard.png +0 -0
  107. {bitp-1.3.1 → bitp-1.3.2}/docs/images/explore-expanded.png +0 -0
  108. {bitp-1.3.1 → bitp-1.3.2}/docs/images/explore.png +0 -0
  109. {bitp-1.3.1 → bitp-1.3.2}/docs/images/recipes-deps.png +0 -0
  110. {bitp-1.3.1 → bitp-1.3.2}/docs/images/recipes.png +0 -0
  111. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-b4.png +0 -0
  112. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-branch.png +0 -0
  113. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-commits.png +0 -0
  114. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-config.png +0 -0
  115. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-dashboard.png +0 -0
  116. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-explore.png +0 -0
  117. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-export.png +0 -0
  118. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-hosts.png +0 -0
  119. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-patches.png +0 -0
  120. {bitp-1.3.1 → bitp-1.3.2}/docs/images/web-terminal.png +0 -0
  121. {bitp-1.3.1 → bitp-1.3.2}/docs/render_screenshot.py +0 -0
  122. {bitp-1.3.1 → bitp-1.3.2}/pyproject.toml +0 -0
  123. {bitp-1.3.1 → bitp-1.3.2}/scripts/build-standalone.py +0 -0
  124. {bitp-1.3.1 → bitp-1.3.2}/setup.cfg +0 -0
  125. {bitp-1.3.1 → bitp-1.3.2}/tests/README.md +0 -0
  126. {bitp-1.3.1 → bitp-1.3.2}/tests/__init__.py +0 -0
  127. {bitp-1.3.1 → bitp-1.3.2}/tests/conftest.py +0 -0
  128. {bitp-1.3.1 → bitp-1.3.2}/tests/data/bblayers/simple.conf +0 -0
  129. {bitp-1.3.1 → bitp-1.3.2}/tests/data/bblayers/with_variables.conf +0 -0
  130. {bitp-1.3.1 → bitp-1.3.2}/tests/integration/__init__.py +0 -0
  131. {bitp-1.3.1 → bitp-1.3.2}/tests/integration/test_cli_dispatch.py +0 -0
  132. {bitp-1.3.1 → bitp-1.3.2}/tests/integration/test_projects_command.py +0 -0
  133. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/__init__.py +0 -0
  134. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_bblayers_parser.py +0 -0
  135. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_cli_parser.py +0 -0
  136. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_colors.py +0 -0
  137. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_confjson.py +0 -0
  138. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_custom_update.py +0 -0
  139. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_daemon.py +0 -0
  140. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_explore.py +0 -0
  141. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_fzf_bindings.py +0 -0
  142. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_fzf_menu.py +0 -0
  143. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_gitrepo.py +0 -0
  144. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_helpers.py +0 -0
  145. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_info.py +0 -0
  146. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_setup.py +0 -0
  147. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_ssh_remote.py +0 -0
  148. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_state.py +0 -0
  149. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_tags.py +0 -0
  150. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_tmux.py +0 -0
  151. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_update.py +0 -0
  152. {bitp-1.3.1 → bitp-1.3.2}/tests/unit/test_web_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitp
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Tools for BitBake/Yocto projects
5
5
  Author-email: Bruce Ashfield <bruce.ashfield@gmail.com>
6
6
  License: GPL-2.0-only
@@ -471,6 +471,7 @@ def _build_explore_menu_lines(
471
471
  branch_priorities: Optional[List[str]] = None,
472
472
  branch_excludes: Optional[List[str]] = None,
473
473
  branch_stale_days: int = 365,
474
+ all_layers_repos: Optional[Set[str]] = None,
474
475
  ) -> str:
475
476
  """Build fzf menu input for explore repo/layers list. Returns newline-joined string."""
476
477
  menu_lines = []
@@ -479,6 +480,7 @@ def _build_explore_menu_lines(
479
480
  expanded_repos = expanded_repos or set()
480
481
  configured_layers = configured_layers or set()
481
482
  branch_expanded_repos = branch_expanded_repos or set()
483
+ all_layers_repos = all_layers_repos or set()
482
484
 
483
485
  # Layers mode: show individual layers instead of repos
484
486
  if layers_mode:
@@ -999,7 +1001,7 @@ def fzf_explore_repo_list(
999
1001
  prompt = "Select layer: "
1000
1002
  else:
1001
1003
  prefix = get_project_header_prefix()
1002
- header = f"{prefix}{Colors.cyan('REPOS VIEW')} | Enter=explore | q=quit | L=layers view\n{Colors.cyan('Update:')} u=single | U=all | m=merge | r=refresh | R=refresh all | b=branches | B=branch all\n{Colors.cyan('View:')} v=verbose | Enter/←/→/\\=fold | t=history | S=shell | M=messages\n{Colors.cyan('Manage:')} a/d=add/remove | +/-=track/hide | c=config | A=discover | P=pull patches | X=prune backups"
1004
+ header = f"{prefix}{Colors.cyan('REPOS VIEW')} | Enter=explore | q=quit | L=layers view\n{Colors.cyan('Update:')} u=single | U=all | m=merge | r=refresh | R=refresh all | b=branches | B=branch all\n{Colors.cyan('View:')} v=verbose | Enter/←/→/\\=fold | t=history | S=shell | M=messages | *=all layers\n{Colors.cyan('Manage:')} a/d=add/remove | +/-=track/hide | c=config | A=discover | P=pull patches | X=prune backups"
1003
1005
  if is_yocto:
1004
1006
  header += f"\n{Colors.cyan('Yocto:')} i=info | ctrl-r=recipes | D=deps | f=fragments"
1005
1007
  prompt = "Select repo: "
@@ -1124,6 +1126,7 @@ def fzf_explore_repo_list(
1124
1126
  fzf_cmd.extend(get_action_binding("L", "LAYERS_TOGGLE"))
1125
1127
  fzf_cmd.extend(get_action_binding("c", "CONFIG {1}"))
1126
1128
  fzf_cmd.extend(get_action_binding("A", "DISCOVER_TOGGLE"))
1129
+ fzf_cmd.extend(get_action_binding("*", "ALL_LAYERS {1}")) # per-repo all layers toggle
1127
1130
  fzf_cmd.extend(get_action_binding("U", "UPDATE_ALL"))
1128
1131
  fzf_cmd.extend(get_action_binding("b", "BRANCH_TOGGLE {1}"))
1129
1132
  fzf_cmd.extend(get_action_binding("B", "BRANCH_ALL"))
@@ -1392,6 +1395,16 @@ def fzf_explore_repo_list(
1392
1395
  return "layers_toggle", None, query
1393
1396
  elif output == "DISCOVER_TOGGLE":
1394
1397
  return "discover_toggle", None, query
1398
+ elif output.startswith("ALL_LAYERS "):
1399
+ repo_path = output[11:].strip()
1400
+ if repo_path.startswith("LAYER:"):
1401
+ # Resolve parent repo from layer path
1402
+ layer_path = repo_path[6:]
1403
+ from .common import git_toplevel
1404
+ repo_path = git_toplevel(layer_path) or layer_path
1405
+ if repo_path in ("HEADER", "PROJECT") or repo_path.startswith("SEPARATOR") or repo_path.startswith("BRANCH:"):
1406
+ return "cancelled", None, query
1407
+ return "all_layers_toggle", repo_path, query
1395
1408
  elif output == "UPDATE_ALL":
1396
1409
  return "update_all", None, query
1397
1410
  elif output.startswith("BRANCH_TOGGLE "):
@@ -4492,6 +4505,8 @@ def run_explore(args) -> int:
4492
4505
  explore_verbose_mode = False
4493
4506
  explore_layers_mode = False
4494
4507
  expanded_repos: Set[str] = set()
4508
+ all_layers_repos: Set[str] = set() # repos showing all layers (not just tracked)
4509
+ all_layers_saved: Dict[str, List[str]] = {} # original repo_layers before all-layers merge
4495
4510
  branch_expanded_repos: Set[str] = set()
4496
4511
  branch_priorities: List[str] = defaults.get("__branch_priorities__", [])
4497
4512
  branch_excludes: List[str] = defaults.get("__branch_excludes__", [])
@@ -5247,6 +5262,51 @@ def run_explore(args) -> int:
5247
5262
  if selected_repo:
5248
5263
  next_selection = selected_repo
5249
5264
  continue
5265
+ elif list_action == "all_layers_toggle" and selected_repo:
5266
+ # Toggle showing all layers (including untracked) for a repo
5267
+ if selected_repo in ("HEADER", "PROJECT", "SETTINGS") or selected_repo.startswith("SEPARATOR") or selected_repo.startswith("BRANCH:"):
5268
+ continue
5269
+ if selected_repo in all_layers_repos:
5270
+ # Toggle off: restore original tracked layers
5271
+ all_layers_repos.discard(selected_repo)
5272
+ if selected_repo in all_layers_saved:
5273
+ repo_layers[selected_repo] = all_layers_saved.pop(selected_repo)
5274
+ # Recompute layer upstream counts for tracked layers only
5275
+ info = repo_info[selected_repo]
5276
+ branch = info["branch"]
5277
+ layers = repo_layers.get(selected_repo, [])
5278
+ if len(layers) > 1 and branch != "(detached)":
5279
+ try:
5280
+ info["layer_upstream_counts"] = get_upstream_layer_counts(selected_repo, branch, layers)
5281
+ except Exception:
5282
+ info["layer_upstream_counts"] = {}
5283
+ else:
5284
+ info["layer_upstream_counts"] = {}
5285
+ else:
5286
+ # Toggle on: scan repo for all layers, merge with tracked
5287
+ from .query import _scan_repo_layers
5288
+ all_layers_saved[selected_repo] = list(repo_layers.get(selected_repo, []))
5289
+ scanned = _scan_repo_layers(selected_repo)
5290
+ tracked = set(repo_layers.get(selected_repo, []))
5291
+ # Merge: tracked first, then untracked
5292
+ merged = list(repo_layers.get(selected_repo, []))
5293
+ for lp in scanned:
5294
+ if lp not in tracked:
5295
+ merged.append(lp)
5296
+ repo_layers[selected_repo] = merged
5297
+ all_layers_repos.add(selected_repo)
5298
+ # Auto-expand to show the layers
5299
+ expanded_repos.add(selected_repo)
5300
+ # Compute upstream counts for all layers
5301
+ info = repo_info[selected_repo]
5302
+ branch = info["branch"]
5303
+ if branch != "(detached)" and len(merged) > 1:
5304
+ try:
5305
+ info["layer_upstream_counts"] = get_upstream_layer_counts(selected_repo, branch, merged)
5306
+ except Exception:
5307
+ info["layer_upstream_counts"] = {}
5308
+ next_selection = selected_repo
5309
+ continue
5250
5310
  elif list_action == "prune_backups" and selected_repo:
5251
5311
  is_special = (selected_repo in ("HEADER", "PROJECT", "PROJECT_CONFIG",
5252
5312
  "PROJECT_INFO", "PROJECT_DEPS", "SETTINGS", "INFO")
@@ -1105,6 +1105,15 @@ def _background_fetch_remote_summaries(remote_projects: list) -> None:
1105
1105
  log_message(f"Remote status updated ({', '.join(sorted(hosts))})")
1106
1106
 
1107
1107
 
1108
+ def _count_subdirs(repo: str) -> int:
1109
+ """Count immediate subdirectories in a repo (quick layer-count heuristic)."""
1110
+ try:
1111
+ return sum(1 for e in os.scandir(repo)
1112
+ if e.is_dir(follow_symlinks=False) and not e.name.startswith("."))
1113
+ except OSError:
1114
+ return 0
1115
+
1116
+
1108
1117
  def _get_project_git_repos(path: str) -> List[str]:
1109
1118
  """Find all git repos in a project directory, up to 2 levels deep."""
1110
1119
  git_repos: list = []
@@ -1155,6 +1164,7 @@ def _get_dashboard_summary(path: str, project_info: Optional[dict] = None) -> di
1155
1164
  "dirty_count": 0,
1156
1165
  "ahead": 0,
1157
1166
  "behind": 0,
1167
+ "tracked_behind": None, # None = no layer info, int = filtered count
1158
1168
  "branch": "",
1159
1169
  "needs_setup": False,
1160
1170
  }
@@ -1179,18 +1189,20 @@ def _get_dashboard_summary(path: str, project_info: Optional[dict] = None) -> di
1179
1189
 
1180
1190
  # Gather per-repo stats
1181
1191
  branches: dict = {} # branch_name -> count
1192
+ repos_behind: dict = {} # repo_path -> (behind_count, branch)
1182
1193
 
1183
1194
  for repo in git_repos:
1184
1195
  # Branch
1196
+ repo_branch = ""
1185
1197
  try:
1186
1198
  r = subprocess.run(
1187
1199
  ["git", "-C", repo, "rev-parse", "--abbrev-ref", "HEAD"],
1188
1200
  capture_output=True, text=True, timeout=3,
1189
1201
  )
1190
1202
  if r.returncode == 0:
1191
- b = r.stdout.strip()
1192
- if b:
1193
- branches[b] = branches.get(b, 0) + 1
1203
+ repo_branch = r.stdout.strip()
1204
+ if repo_branch:
1205
+ branches[repo_branch] = branches.get(repo_branch, 0) + 1
1194
1206
  except Exception:
1195
1207
  pass
1196
1208
 
@@ -1214,8 +1226,12 @@ def _get_dashboard_summary(path: str, project_info: Optional[dict] = None) -> di
1214
1226
  if r.returncode == 0:
1215
1227
  parts = r.stdout.strip().split()
1216
1228
  if len(parts) == 2:
1217
- summary["ahead"] += int(parts[0])
1218
- summary["behind"] += int(parts[1])
1229
+ ahead = int(parts[0])
1230
+ behind = int(parts[1])
1231
+ summary["ahead"] += ahead
1232
+ summary["behind"] += behind
1233
+ if behind > 0:
1234
+ repos_behind[repo] = (behind, repo_branch)
1219
1235
  except Exception:
1220
1236
  pass
1221
1237
 
@@ -1223,6 +1239,47 @@ def _get_dashboard_summary(path: str, project_info: Optional[dict] = None) -> di
1223
1239
  if branches:
1224
1240
  summary["branch"] = max(branches, key=branches.get)
1225
1241
 
1242
+ # Compute tracked-layer behind count for multi-layer repos
1243
+ if repos_behind:
1244
+ try:
1245
+ from ..personas.yocto.bblayers import extract_layer_paths
1246
+ from ..personas.yocto.layers import git_toplevel
1247
+ # Find bblayers.conf
1248
+ bblayers_path = None
1249
+ for cand in ["conf/bblayers.conf", "build/conf/bblayers.conf"]:
1250
+ full = os.path.join(path, cand)
1251
+ if os.path.isfile(full):
1252
+ bblayers_path = full
1253
+ break
1254
+ if bblayers_path:
1255
+ layers = extract_layer_paths(bblayers_path)
1256
+ # Map repo -> list of tracked layer paths
1257
+ repo_layers: dict = {}
1258
+ for layer in layers:
1259
+ layer = os.path.realpath(layer)
1260
+ repo = git_toplevel(layer)
1261
+ if repo:
1262
+ repo_layers.setdefault(repo, []).append(layer)
1263
+ # For repos that are behind and have tracked layers,
1264
+ # compute per-layer filtered counts
1265
+ tracked_total = 0
1266
+ has_layer_info = False
1267
+ for repo, (behind, rbranch) in repos_behind.items():
1268
+ tracked = repo_layers.get(repo)
1269
+ if tracked and len(tracked) < _count_subdirs(repo):
1270
+ # Multi-layer repo with partial tracking
1271
+ has_layer_info = True
1272
+ from ..personas.yocto.layers import get_upstream_layer_counts
1273
+ lcounts = get_upstream_layer_counts(repo, rbranch, tracked)
1274
+ tracked_total += sum(lcounts.values())
1275
+ else:
1276
+ # Single-layer or all layers tracked — full count applies
1277
+ tracked_total += behind
1278
+ if has_layer_info:
1279
+ summary["tracked_behind"] = tracked_total
1280
+ except Exception:
1281
+ pass
1282
+
1226
1283
  # Last updated — most recent HEAD reflog mtime across repos
1227
1284
  latest_reflog = 0.0
1228
1285
  for repo in git_repos:
@@ -138,6 +138,22 @@ def _find_repo(repos, repo_name, defaults):
138
138
  return find_repo_by_identifier(repos, repo_name, defaults)
139
139
 
140
140
 
141
+ def _scan_repo_layers(repo: str) -> list:
142
+ """Scan a repo for all directories containing conf/layer.conf."""
143
+ layers = []
144
+ try:
145
+ for entry in os.scandir(repo):
146
+ if entry.is_dir(follow_symlinks=False) and not entry.name.startswith("."):
147
+ if os.path.isfile(os.path.join(entry.path, "conf", "layer.conf")):
148
+ layers.append(entry.path)
149
+ except OSError:
150
+ pass
151
+ # Also check the repo root itself
152
+ if os.path.isfile(os.path.join(repo, "conf", "layer.conf")):
153
+ layers.insert(0, repo)
154
+ return layers
155
+
156
+
141
157
  def query_repos(project: str, discover: bool = False,
142
158
  include_hidden: bool = False) -> dict:
143
159
  """Return list of repo dicts (same shape as /api/repos)."""
@@ -1223,19 +1239,39 @@ def query_prune_backups(repo_name: str, project: str) -> dict:
1223
1239
 
1224
1240
 
1225
1241
  def query_repo_layers(repo_name: str, project: str) -> dict:
1226
- """Return layers for a repo."""
1242
+ """Return layers for a repo (including untracked layers with upstream counts)."""
1227
1243
  repos, defaults, repo_layers_map, configured, *_ = _resolve_repos(project)
1228
1244
  repo = _find_repo(repos, repo_name, defaults)
1229
1245
  if not repo:
1230
1246
  return {"error": f"Repo not found: {repo_name}"}
1231
1247
 
1232
- layers = repo_layers_map.get(repo, [])
1248
+ # Merge tracked and scanned layers
1249
+ tracked = repo_layers_map.get(repo, [])
1250
+ all_layers = _scan_repo_layers(repo) if repo else []
1251
+ tracked_set = set(os.path.realpath(l) for l in tracked)
1252
+ merged = list(tracked)
1253
+ for l in all_layers:
1254
+ if os.path.realpath(l) not in tracked_set:
1255
+ merged.append(l)
1256
+
1257
+ # Compute per-layer upstream counts
1258
+ from .explore import current_branch
1259
+ from ..personas.yocto.layers import get_upstream_layer_counts
1260
+ branch = current_branch(repo) if repo else ""
1261
+ layer_counts = {}
1262
+ if branch and branch != "(detached)" and merged:
1263
+ try:
1264
+ layer_counts = get_upstream_layer_counts(repo, branch, merged)
1265
+ except Exception:
1266
+ pass
1267
+
1233
1268
  result = []
1234
- for lp in layers:
1269
+ for lp in merged:
1235
1270
  result.append({
1236
1271
  "name": os.path.basename(lp),
1237
1272
  "path": lp,
1238
1273
  "configured": lp in configured,
1274
+ "upstream_count": layer_counts.get(lp, 0),
1239
1275
  })
1240
1276
  return {"layers": result}
1241
1277
 
@@ -583,7 +583,13 @@ const DashboardView = {
583
583
  const parts = [`${s.repo_count} repos`];
584
584
  if (s.dirty_count) parts.push(`<span style="color:var(--red)">\u270e${s.dirty_count}</span>`);
585
585
  if (s.ahead) parts.push(`<span style="color:var(--accent)">\u2191${s.ahead}</span>`);
586
- if (s.behind) parts.push(`<span style="color:var(--yellow)">\u2193${s.behind}</span>`);
586
+ if (s.behind) {
587
+ let behindStr = '\u2193' + s.behind;
588
+ if (s.tracked_behind != null && s.tracked_behind < s.behind) {
589
+ behindStr += ' <span style="color:var(--text-muted);font-size:var(--font-size-xs)">(' + s.tracked_behind + ' tracked)</span>';
590
+ }
591
+ parts.push('<span style="color:var(--yellow)">' + behindStr + '</span>');
592
+ }
587
593
  if (!s.dirty_count && !s.ahead && !s.behind) parts.push(`<span style="color:var(--green)">\u2713</span>`);
588
594
  return parts.join(' ');
589
595
  },
@@ -19,6 +19,8 @@ const ExploreView = {
19
19
  _discoverMode: false,
20
20
  _hiddenMode: false,
21
21
  _configuredLayers: new Set(),
22
+ _allLayersRepos: new Set(), // repos toggled to show all layers
23
+ _allLayersCache: {}, // repo name -> [{name, path, configured}]
22
24
 
23
25
  keyBindings: [
24
26
  {key: 'j/k', desc: 'Navigate rows'},
@@ -246,10 +248,35 @@ const ExploreView = {
246
248
  // Layer child rows (if expanded and has layers)
247
249
  const hasBranchChildren = this._branchesMode && isExpanded && this._branchCache[name] && this._branchCache[name].length > 0;
248
250
  if (hasLayers && isExpanded) {
249
- const layerCounts = repo.layer_upstream_counts || {};
250
- layers.forEach((layer, li) => {
251
+ const layerCounts = Object.assign({}, repo.layer_upstream_counts || {});
252
+ const showingAll = this._allLayersRepos.has(name);
253
+ const allLayers = showingAll ? (this._allLayersCache[name] || []) : null;
254
+
255
+ // Merge upstream counts from all-layers fetch
256
+ if (allLayers) {
257
+ allLayers.forEach(al => {
258
+ if (al.upstream_count > 0 && !layerCounts[al.path]) {
259
+ layerCounts[al.path] = al.upstream_count;
260
+ }
261
+ });
262
+ }
263
+
264
+ // Build display list: tracked layers, then untracked if toggled
265
+ const displayLayers = [];
266
+ layers.forEach(l => displayLayers.push({ path: l, fromAll: false }));
267
+ if (allLayers) {
268
+ const trackedSet = new Set(layers);
269
+ allLayers.forEach(al => {
270
+ if (!trackedSet.has(al.path)) {
271
+ displayLayers.push({ path: al.path, fromAll: true });
272
+ }
273
+ });
274
+ }
275
+
276
+ displayLayers.forEach((entry, li) => {
277
+ const layer = entry.path;
251
278
  const layerName = layer.split('/').pop();
252
- const isLast = li === layers.length - 1 && !hasBranchChildren;
279
+ const isLast = li === displayLayers.length - 1 && !hasBranchChildren;
253
280
  const lcount = layerCounts[layer] || 0;
254
281
  const lcountHtml = lcount > 0
255
282
  ? `<span style="color:var(--yellow)">\u2193${lcount}</span>`
@@ -259,8 +286,8 @@ const ExploreView = {
259
286
  ? `<button class="btn-icon" data-action="remove-layer" data-layer-path="${esc(layer)}" title="Remove layer" style="color:var(--text-muted)">&minus;</button>`
260
287
  : `<button class="btn-icon" data-action="add-layer" data-layer-path="${esc(layer)}" title="Add layer" style="color:var(--green)">+</button>`;
261
288
  const configDot = isConfigured
262
- ? '<span style="color:var(--green)" title="Configured">\u25cf</span>'
263
- : '<span style="color:var(--text-muted)" title="Not configured">\u25cb</span>';
289
+ ? '<span style="color:var(--green)" title="In bblayers.conf">\u25cf</span>'
290
+ : '<span style="color:var(--text-muted)" title="Not in bblayers.conf">\u25cb</span>';
264
291
  const layerTr = document.createElement('tr');
265
292
  layerTr.className = 'explore-layer-row' + (isLast ? ' tree-last' : '');
266
293
  layerTr.dataset.parentRepo = name;
@@ -277,7 +304,6 @@ const ExploreView = {
277
304
  layerTr.addEventListener('click', (e) => {
278
305
  if (e.target.closest('[data-action]')) return;
279
306
  App.state.selectedRepo = name;
280
- // Store relative layer path (e.g. "meta-core") for the API
281
307
  const repoPath = repo.path || '';
282
308
  App.state.selectedLayer = (repoPath && layer.startsWith(repoPath + '/'))
283
309
  ? layer.substring(repoPath.length + 1)
@@ -307,6 +333,33 @@ const ExploreView = {
307
333
  });
308
334
  }
309
335
  });
336
+
337
+ // "Show all layers" / "Show tracked only" toggle row
338
+ if (!this._discoverMode) {
339
+ const toggleTr = document.createElement('tr');
340
+ toggleTr.className = 'explore-layer-row tree-last';
341
+ toggleTr.style.cursor = 'pointer';
342
+ const toggleLabel = showingAll ? 'Show tracked only' : 'Show all layers';
343
+ const toggleIcon = showingAll ? '\u25b4' : '\u25be';
344
+ const vEmpty = this._verboseMode ? '<td></td>' : '';
345
+ toggleTr.innerHTML = `<td class="tree-line"></td><td colspan="1"><span class="explore-all-layers-toggle" data-repo="${esc(name)}" style="color:var(--accent);font-size:var(--font-size-sm)">${toggleIcon} ${toggleLabel}</span></td>${vEmpty}<td></td><td></td><td></td><td></td>`;
346
+ tbody.appendChild(toggleTr);
347
+ toggleTr.querySelector('.explore-all-layers-toggle').addEventListener('click', (e) => {
348
+ e.stopPropagation();
349
+ const rn = e.target.dataset.repo;
350
+ if (this._allLayersRepos.has(rn)) {
351
+ this._allLayersRepos.delete(rn);
352
+ this._renderRepos(this._repoData);
353
+ } else {
354
+ // Fetch all layers for this repo
355
+ API.getRepoLayers(rn, this._project()).then(data => {
356
+ this._allLayersCache[rn] = data.layers || [];
357
+ this._allLayersRepos.add(rn);
358
+ this._renderRepos(this._repoData);
359
+ }).catch(err => App.showToast(err.message, 'error'));
360
+ }
361
+ });
362
+ }
310
363
  }
311
364
 
312
365
  // Branch child rows (if expanded in branches mode)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bitp
3
- Version: 1.3.1
3
+ Version: 1.3.2
4
4
  Summary: Tools for BitBake/Yocto projects
5
5
  Author-email: Bruce Ashfield <bruce.ashfield@gmail.com>
6
6
  License: GPL-2.0-only
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes