bitp 1.0.6__py3-none-any.whl
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.
- bitbake_project/__init__.py +88 -0
- bitbake_project/__main__.py +14 -0
- bitbake_project/cli.py +1580 -0
- bitbake_project/commands/__init__.py +60 -0
- bitbake_project/commands/branch.py +889 -0
- bitbake_project/commands/common.py +2372 -0
- bitbake_project/commands/config.py +1515 -0
- bitbake_project/commands/deps.py +903 -0
- bitbake_project/commands/explore.py +2269 -0
- bitbake_project/commands/export.py +1030 -0
- bitbake_project/commands/fragment.py +884 -0
- bitbake_project/commands/init.py +515 -0
- bitbake_project/commands/projects.py +1505 -0
- bitbake_project/commands/recipe.py +1374 -0
- bitbake_project/commands/repos.py +154 -0
- bitbake_project/commands/search.py +313 -0
- bitbake_project/commands/update.py +181 -0
- bitbake_project/core.py +1811 -0
- bitp-1.0.6.dist-info/METADATA +401 -0
- bitp-1.0.6.dist-info/RECORD +24 -0
- bitp-1.0.6.dist-info/WHEEL +5 -0
- bitp-1.0.6.dist-info/entry_points.txt +3 -0
- bitp-1.0.6.dist-info/licenses/COPYING +338 -0
- bitp-1.0.6.dist-info/top_level.txt +1 -0
bitbake_project/cli.py
ADDED
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# PYTHON_ARGCOMPLETE_OK
|
|
3
|
+
#
|
|
4
|
+
# Copyright (C) 2025 Bruce Ashfield <bruce.ashfield@gmail.com>
|
|
5
|
+
#
|
|
6
|
+
# SPDX-License-Identifier: GPL-2.0-only
|
|
7
|
+
#
|
|
8
|
+
"""
|
|
9
|
+
CLI entry point for bit.
|
|
10
|
+
|
|
11
|
+
Handles argument parsing and command dispatch.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import signal
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
from typing import Dict, List, Optional, Set, Tuple
|
|
21
|
+
|
|
22
|
+
from .core import Colors, fzf_expandable_menu, parse_help_options
|
|
23
|
+
from .commands import fzf_available
|
|
24
|
+
|
|
25
|
+
# Try to import argcomplete for tab completion
|
|
26
|
+
try:
|
|
27
|
+
import argcomplete
|
|
28
|
+
from argcomplete.completers import SuppressCompleter
|
|
29
|
+
HAS_ARGCOMPLETE = True
|
|
30
|
+
|
|
31
|
+
class RepoCompleter:
|
|
32
|
+
"""Complete repo names from bblayers.conf."""
|
|
33
|
+
def __init__(self, include_edit=False):
|
|
34
|
+
self.include_edit = include_edit
|
|
35
|
+
|
|
36
|
+
def __call__(self, prefix, parsed_args, **kwargs):
|
|
37
|
+
try:
|
|
38
|
+
completions = []
|
|
39
|
+
# Add 'edit' as a valid option for config command
|
|
40
|
+
if self.include_edit and "edit".startswith(prefix):
|
|
41
|
+
completions.append("edit")
|
|
42
|
+
|
|
43
|
+
# Try to get repos for completion
|
|
44
|
+
bblayers = getattr(parsed_args, 'bblayers', None)
|
|
45
|
+
path = bblayers or "conf/bblayers.conf"
|
|
46
|
+
if not os.path.exists(path):
|
|
47
|
+
path = "build/conf/bblayers.conf"
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
return completions
|
|
50
|
+
# Quick parse for completion
|
|
51
|
+
with open(path) as f:
|
|
52
|
+
content = f.read()
|
|
53
|
+
import re
|
|
54
|
+
# Match both quoted and unquoted paths
|
|
55
|
+
paths = re.findall(r'"([^"]+)"', content)
|
|
56
|
+
paths += re.findall(r'^\s*(/[^\s\\]+)', content, re.MULTILINE)
|
|
57
|
+
repos = set()
|
|
58
|
+
for p in paths:
|
|
59
|
+
if '/' in p and '${' not in p and os.path.isdir(p):
|
|
60
|
+
try:
|
|
61
|
+
toplevel = subprocess.check_output(
|
|
62
|
+
["git", "-C", p, "rev-parse", "--show-toplevel"],
|
|
63
|
+
stderr=subprocess.DEVNULL, text=True
|
|
64
|
+
).strip()
|
|
65
|
+
# Try to get custom display name first
|
|
66
|
+
try:
|
|
67
|
+
display = subprocess.check_output(
|
|
68
|
+
["git", "-C", toplevel, "config", "--get", "bit.display-name"],
|
|
69
|
+
stderr=subprocess.DEVNULL, text=True
|
|
70
|
+
).strip()
|
|
71
|
+
except:
|
|
72
|
+
# Fall back to deriving from origin URL or basename
|
|
73
|
+
try:
|
|
74
|
+
url = subprocess.check_output(
|
|
75
|
+
["git", "-C", toplevel, "config", "--get", "remote.origin.url"],
|
|
76
|
+
stderr=subprocess.DEVNULL, text=True
|
|
77
|
+
).strip()
|
|
78
|
+
display = os.path.basename(url.rstrip('/'))
|
|
79
|
+
if display.endswith('.git'):
|
|
80
|
+
display = display[:-4]
|
|
81
|
+
except:
|
|
82
|
+
display = os.path.basename(toplevel)
|
|
83
|
+
repos.add(display)
|
|
84
|
+
except:
|
|
85
|
+
pass
|
|
86
|
+
completions.extend([r for r in repos if r.lower().startswith(prefix.lower())])
|
|
87
|
+
return completions
|
|
88
|
+
except:
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
class LayerCompleter:
|
|
92
|
+
"""Complete layer names from bblayers.conf."""
|
|
93
|
+
def __call__(self, prefix, parsed_args, **kwargs):
|
|
94
|
+
try:
|
|
95
|
+
bblayers = getattr(parsed_args, 'bblayers', None)
|
|
96
|
+
path = bblayers or "conf/bblayers.conf"
|
|
97
|
+
if not os.path.exists(path):
|
|
98
|
+
path = "build/conf/bblayers.conf"
|
|
99
|
+
if not os.path.exists(path):
|
|
100
|
+
return []
|
|
101
|
+
with open(path) as f:
|
|
102
|
+
content = f.read()
|
|
103
|
+
import re
|
|
104
|
+
# Match both quoted and unquoted paths
|
|
105
|
+
paths = re.findall(r'"([^"]+)"', content)
|
|
106
|
+
paths += re.findall(r'^\s*(/[^\s\\]+)', content, re.MULTILINE)
|
|
107
|
+
layers = []
|
|
108
|
+
for p in paths:
|
|
109
|
+
if '/' in p and '${' not in p and os.path.isdir(p):
|
|
110
|
+
layers.append(os.path.basename(p))
|
|
111
|
+
return [l for l in layers if l.lower().startswith(prefix.lower())]
|
|
112
|
+
except:
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
except ImportError:
|
|
116
|
+
HAS_ARGCOMPLETE = False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# SINGLE SOURCE OF TRUTH FOR ALL COMMANDS
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# Add new commands/subcommands here. Format:
|
|
123
|
+
# (command, description, [subcommands])
|
|
124
|
+
# where subcommands is a list of (subcommand, description) tuples.
|
|
125
|
+
#
|
|
126
|
+
# This list is used by:
|
|
127
|
+
# - Interactive command menu (bit with no args)
|
|
128
|
+
# - Help browser (bit help)
|
|
129
|
+
# - Bash completion
|
|
130
|
+
# =============================================================================
|
|
131
|
+
COMMAND_TREE = [
|
|
132
|
+
("branch", "View and switch branches across repos", []),
|
|
133
|
+
("config", "View and configure repo/layer settings", []),
|
|
134
|
+
("deps", "Show layer and recipe dependencies", [
|
|
135
|
+
("deps layers", "Show layer dependency tree"),
|
|
136
|
+
("deps recipe", "Show recipe dependency tree"),
|
|
137
|
+
]),
|
|
138
|
+
("export", "Export patches from layer repos", [
|
|
139
|
+
("export prep", "Prepare commits for export (reorder/group)"),
|
|
140
|
+
]),
|
|
141
|
+
("explore", "Interactively explore commits in layer repos", []),
|
|
142
|
+
("fragments", "Browse and manage OE configuration fragments", [
|
|
143
|
+
("fragments list", "List all available fragments"),
|
|
144
|
+
("fragments enable", "Enable a fragment"),
|
|
145
|
+
("fragments disable", "Disable a fragment"),
|
|
146
|
+
("fragments show", "Show fragment content"),
|
|
147
|
+
]),
|
|
148
|
+
("help", "Browse help for all commands", []),
|
|
149
|
+
("init", "Initialize/setup OE/Yocto build environment", [
|
|
150
|
+
("init shell", "Start a shell with build environment pre-sourced"),
|
|
151
|
+
("init clone", "Show or clone core Yocto/OE repositories"),
|
|
152
|
+
]),
|
|
153
|
+
("projects", "Manage multiple project directories", [
|
|
154
|
+
("projects add", "Add a project directory"),
|
|
155
|
+
("projects remove", "Remove a project from list"),
|
|
156
|
+
("projects list", "List all registered projects"),
|
|
157
|
+
]),
|
|
158
|
+
("recipes", "Search and browse BitBake recipes", []),
|
|
159
|
+
("repos", "List layer repos", [
|
|
160
|
+
("repos status", "Show one-liner status for each repo"),
|
|
161
|
+
]),
|
|
162
|
+
("search", "Search OpenEmbedded Layer Index for layers", []),
|
|
163
|
+
("status", "Show local commit summary for layer repos", []),
|
|
164
|
+
("update", "Update git repos referenced by layers in bblayers.conf", []),
|
|
165
|
+
]
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# COMMAND CATEGORIES FOR MENU GROUPING
|
|
169
|
+
# =============================================================================
|
|
170
|
+
# Define categories and which commands belong to each.
|
|
171
|
+
# CATEGORY_ORDER determines display order in the menu.
|
|
172
|
+
# =============================================================================
|
|
173
|
+
COMMAND_CATEGORIES = {
|
|
174
|
+
"git": ("Git/Repository", ["explore", "branch", "status", "update", "repos", "export"]),
|
|
175
|
+
"config": ("Configuration", ["config", "fragments", "init", "projects"]),
|
|
176
|
+
"discovery": ("Discovery", ["recipes", "deps", "search"]),
|
|
177
|
+
"help": ("Help", ["help"]),
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
CATEGORY_ORDER = ["git", "config", "discovery", "help"]
|
|
181
|
+
|
|
182
|
+
# Commands grouped by interaction style (alternative grouping)
|
|
183
|
+
INTERACTION_CATEGORIES = {
|
|
184
|
+
"interactive": ("Interactive (fzf browsers)", ["explore", "branch", "recipes", "fragments", "deps", "search", "projects", "help"]),
|
|
185
|
+
"output": ("Output & Exit", ["status", "update", "repos", "config", "init", "export"]),
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
INTERACTION_ORDER = ["interactive", "output"]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_menu_sort_mode(defaults_file: str = ".bit.defaults") -> str:
|
|
192
|
+
"""
|
|
193
|
+
Get the menu sort mode from defaults file.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Sort mode: "category" (default), "alpha", or "interactive"
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
if os.path.exists(defaults_file):
|
|
200
|
+
with open(defaults_file, encoding="utf-8") as f:
|
|
201
|
+
data = json.load(f)
|
|
202
|
+
mode = data.get("menu_sort", "category")
|
|
203
|
+
if mode in ("category", "alpha", "interactive"):
|
|
204
|
+
return mode
|
|
205
|
+
except (json.JSONDecodeError, OSError):
|
|
206
|
+
pass
|
|
207
|
+
return "category"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def sort_commands_by_mode(
|
|
211
|
+
commands: List[Tuple[str, str, List[Tuple[str, str]]]],
|
|
212
|
+
mode: str,
|
|
213
|
+
) -> Tuple[List[Tuple[str, str, List[Tuple[str, str]]]], Optional[Dict[str, str]]]:
|
|
214
|
+
"""
|
|
215
|
+
Sort/group commands based on the specified mode.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
commands: List of (cmd, description, subcommands) tuples
|
|
219
|
+
mode: "alpha", "category", or "interactive"
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Tuple of (sorted_commands, categories_dict) where categories_dict maps
|
|
223
|
+
command names to their category headers (or None for alpha mode).
|
|
224
|
+
"""
|
|
225
|
+
if mode == "alpha":
|
|
226
|
+
return sorted(commands, key=lambda x: x[0]), None
|
|
227
|
+
|
|
228
|
+
# Build command lookup
|
|
229
|
+
cmd_lookup = {cmd: (cmd, desc, subs) for cmd, desc, subs in commands}
|
|
230
|
+
|
|
231
|
+
if mode == "interactive":
|
|
232
|
+
categories = INTERACTION_CATEGORIES
|
|
233
|
+
order = INTERACTION_ORDER
|
|
234
|
+
else: # category (default)
|
|
235
|
+
categories = COMMAND_CATEGORIES
|
|
236
|
+
order = CATEGORY_ORDER
|
|
237
|
+
|
|
238
|
+
sorted_commands: List[Tuple[str, str, List[Tuple[str, str]]]] = []
|
|
239
|
+
cmd_to_category: Dict[str, str] = {}
|
|
240
|
+
|
|
241
|
+
for cat_key in order:
|
|
242
|
+
if cat_key not in categories:
|
|
243
|
+
continue
|
|
244
|
+
cat_label, cat_cmds = categories[cat_key]
|
|
245
|
+
first_in_category = True
|
|
246
|
+
for cmd_name in cat_cmds:
|
|
247
|
+
if cmd_name in cmd_lookup:
|
|
248
|
+
if first_in_category:
|
|
249
|
+
cmd_to_category[cmd_name] = cat_label
|
|
250
|
+
first_in_category = False
|
|
251
|
+
sorted_commands.append(cmd_lookup[cmd_name])
|
|
252
|
+
|
|
253
|
+
# Add any commands not in any category at the end
|
|
254
|
+
categorized = set()
|
|
255
|
+
for cat_key in order:
|
|
256
|
+
if cat_key in categories:
|
|
257
|
+
categorized.update(categories[cat_key][1])
|
|
258
|
+
for cmd, desc, subs in commands:
|
|
259
|
+
if cmd not in categorized:
|
|
260
|
+
sorted_commands.append((cmd, desc, subs))
|
|
261
|
+
|
|
262
|
+
return sorted_commands, cmd_to_category
|
|
263
|
+
|
|
264
|
+
# Flatten for legacy COMMANDS list (used by some completers)
|
|
265
|
+
COMMANDS = [(cmd, desc) for cmd, desc, _ in COMMAND_TREE]
|
|
266
|
+
for cmd, desc, subs in COMMAND_TREE:
|
|
267
|
+
for subcmd, subdesc in subs:
|
|
268
|
+
COMMANDS.append((subcmd, subdesc))
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def build_parser() -> Tuple[argparse.ArgumentParser, Dict]:
|
|
272
|
+
"""Build the argument parser with all subcommands."""
|
|
273
|
+
parser = argparse.ArgumentParser(
|
|
274
|
+
description="Tools for BitBake projects",
|
|
275
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
276
|
+
)
|
|
277
|
+
parser.add_argument(
|
|
278
|
+
"--completion",
|
|
279
|
+
action="store_true",
|
|
280
|
+
help="Show bash completion setup instructions",
|
|
281
|
+
)
|
|
282
|
+
# Global options available to all subcommands
|
|
283
|
+
parser.add_argument(
|
|
284
|
+
"--bblayers",
|
|
285
|
+
default=None,
|
|
286
|
+
help="Path to bblayers.conf (auto-detects conf/bblayers.conf or build/conf/bblayers.conf if omitted)",
|
|
287
|
+
)
|
|
288
|
+
parser.add_argument(
|
|
289
|
+
"--defaults-file",
|
|
290
|
+
default=".bit.defaults",
|
|
291
|
+
help="Path to per-repo default actions file",
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--all", "-a",
|
|
295
|
+
action="store_true",
|
|
296
|
+
help="Discover all layers/repos (slower, finds unconfigured layers)",
|
|
297
|
+
)
|
|
298
|
+
subparsers = parser.add_subparsers(dest="command", metavar="action", title="commands")
|
|
299
|
+
|
|
300
|
+
# ---------- update ----------
|
|
301
|
+
update = subparsers.add_parser(
|
|
302
|
+
"update",
|
|
303
|
+
aliases=["u"],
|
|
304
|
+
help="Update git repos referenced by layers in bblayers.conf",
|
|
305
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
306
|
+
)
|
|
307
|
+
update.add_argument("repo", nargs="?", help="Specific repo to update (by name, index, or path)")
|
|
308
|
+
update.add_argument("--dry-run", action="store_true", help="Show the commands that would run without executing them")
|
|
309
|
+
update.add_argument("--resume", action="store_true", help="Resume from previous run using the resume file")
|
|
310
|
+
update.add_argument(
|
|
311
|
+
"--resume-file",
|
|
312
|
+
default=".bitbake-layers-update.resume",
|
|
313
|
+
help="Path to resume state file",
|
|
314
|
+
)
|
|
315
|
+
update.add_argument(
|
|
316
|
+
"--plain",
|
|
317
|
+
action="store_true",
|
|
318
|
+
help="Use text-based prompts instead of fzf",
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
# ---------- status ----------
|
|
322
|
+
status = subparsers.add_parser(
|
|
323
|
+
"status",
|
|
324
|
+
help="Show local commit summary for layer repos (alias for 'explore --status')",
|
|
325
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
326
|
+
)
|
|
327
|
+
status.add_argument("--max-commits", type=int, default=10, help="Max commits to show with -v")
|
|
328
|
+
status.add_argument(
|
|
329
|
+
"-v", "--verbose",
|
|
330
|
+
action="count",
|
|
331
|
+
default=0,
|
|
332
|
+
help="Increase verbosity: -v shows commits (limited), -vv shows all commits",
|
|
333
|
+
)
|
|
334
|
+
status.add_argument(
|
|
335
|
+
"--fetch",
|
|
336
|
+
action="store_true",
|
|
337
|
+
help="Fetch from origin before checking status (shows accurate upstream changes)",
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
# ---------- repos ----------
|
|
341
|
+
repos = subparsers.add_parser(
|
|
342
|
+
"repos",
|
|
343
|
+
help="List layer repos",
|
|
344
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
345
|
+
)
|
|
346
|
+
repos_sub = repos.add_subparsers(dest="repos_command", metavar="command")
|
|
347
|
+
|
|
348
|
+
repos_status = repos_sub.add_parser(
|
|
349
|
+
"status",
|
|
350
|
+
help="Show one-liner status: commit counts, branch, clean/dirty",
|
|
351
|
+
)
|
|
352
|
+
repos_status.add_argument(
|
|
353
|
+
"--fetch",
|
|
354
|
+
action="store_true",
|
|
355
|
+
help="Fetch from origin before checking",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# ---------- init ----------
|
|
359
|
+
init = subparsers.add_parser(
|
|
360
|
+
"init",
|
|
361
|
+
help="Initialize/setup OE/Yocto build environment",
|
|
362
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
363
|
+
epilog="""
|
|
364
|
+
Subcommands:
|
|
365
|
+
(none) Show the source command for oe-init-build-env
|
|
366
|
+
shell Start a new shell with build environment pre-sourced
|
|
367
|
+
clone Clone core Yocto/OE repositories for a new project
|
|
368
|
+
|
|
369
|
+
Examples:
|
|
370
|
+
# Show the source command to set up build environment
|
|
371
|
+
bit init
|
|
372
|
+
|
|
373
|
+
# Start a shell ready to run bitbake commands
|
|
374
|
+
bit init shell
|
|
375
|
+
|
|
376
|
+
# Clone core repos then show setup command
|
|
377
|
+
bit init clone --execute
|
|
378
|
+
bit init clone -b scarthgap --execute
|
|
379
|
+
""",
|
|
380
|
+
)
|
|
381
|
+
init_subparsers = init.add_subparsers(dest="init_command")
|
|
382
|
+
|
|
383
|
+
# Default behavior (no subcommand): show source command
|
|
384
|
+
init.add_argument(
|
|
385
|
+
"--layers-dir",
|
|
386
|
+
default="layers",
|
|
387
|
+
help="Directory containing layers (relative to current dir)",
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# init shell subcommand
|
|
391
|
+
init_shell = init_subparsers.add_parser(
|
|
392
|
+
"shell",
|
|
393
|
+
help="Start a shell with build environment pre-sourced",
|
|
394
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
395
|
+
epilog="""
|
|
396
|
+
Start a new interactive shell with the OE/Yocto build environment already
|
|
397
|
+
sourced. You can immediately run bitbake commands without manual setup.
|
|
398
|
+
|
|
399
|
+
The shell prompt will show "(oe)" to indicate the environment is active.
|
|
400
|
+
|
|
401
|
+
Examples:
|
|
402
|
+
# Start a shell ready for bitbake
|
|
403
|
+
bit init shell
|
|
404
|
+
|
|
405
|
+
# With custom layers directory
|
|
406
|
+
bit init shell --layers-dir my-layers
|
|
407
|
+
""",
|
|
408
|
+
)
|
|
409
|
+
init_shell.add_argument(
|
|
410
|
+
"--layers-dir",
|
|
411
|
+
default="layers",
|
|
412
|
+
help="Directory containing layers (relative to current dir)",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# init clone subcommand
|
|
416
|
+
init_clone = init_subparsers.add_parser(
|
|
417
|
+
"clone",
|
|
418
|
+
help="Show or clone core Yocto/OE repositories",
|
|
419
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
420
|
+
epilog="""
|
|
421
|
+
Clone the three core repositories needed for a Yocto/OE build:
|
|
422
|
+
- bitbake: The build engine
|
|
423
|
+
- openembedded-core: Core metadata and recipes
|
|
424
|
+
- meta-yocto: Yocto Project reference distribution (poky)
|
|
425
|
+
|
|
426
|
+
Examples:
|
|
427
|
+
# Show clone commands without executing
|
|
428
|
+
bit init clone
|
|
429
|
+
|
|
430
|
+
# Clone repos into layers/ directory
|
|
431
|
+
bit init clone --execute
|
|
432
|
+
|
|
433
|
+
# Clone a specific branch (e.g., scarthgap)
|
|
434
|
+
bit init clone -b scarthgap --execute
|
|
435
|
+
|
|
436
|
+
# Clone to a custom layers directory
|
|
437
|
+
bit init clone --execute --layers-dir my-layers
|
|
438
|
+
""",
|
|
439
|
+
)
|
|
440
|
+
init_clone.add_argument(
|
|
441
|
+
"--execute",
|
|
442
|
+
action="store_true",
|
|
443
|
+
help="Actually clone the repos (default: just show commands)",
|
|
444
|
+
)
|
|
445
|
+
init_clone.add_argument(
|
|
446
|
+
"--branch", "-b",
|
|
447
|
+
default="master",
|
|
448
|
+
help="Branch to clone (master, scarthgap, kirkstone, etc.)",
|
|
449
|
+
)
|
|
450
|
+
init_clone.add_argument(
|
|
451
|
+
"--layers-dir",
|
|
452
|
+
default="layers",
|
|
453
|
+
help="Directory to clone repos into",
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Keep bootstrap as an alias for backwards compatibility
|
|
457
|
+
bootstrap = subparsers.add_parser(
|
|
458
|
+
"bootstrap",
|
|
459
|
+
help="(alias for 'init clone') Clone core Yocto/OE repositories",
|
|
460
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
461
|
+
epilog="""
|
|
462
|
+
NOTE: 'bootstrap' is an alias for 'init clone'. Consider using 'init clone' instead.
|
|
463
|
+
|
|
464
|
+
Examples:
|
|
465
|
+
bit init clone --execute
|
|
466
|
+
bit init clone -b scarthgap --execute
|
|
467
|
+
""",
|
|
468
|
+
)
|
|
469
|
+
bootstrap.add_argument(
|
|
470
|
+
"--clone",
|
|
471
|
+
action="store_true",
|
|
472
|
+
help="Actually clone the repos (default: just show commands)",
|
|
473
|
+
)
|
|
474
|
+
bootstrap.add_argument(
|
|
475
|
+
"--branch", "-b",
|
|
476
|
+
default="master",
|
|
477
|
+
help="Branch to clone (master, scarthgap, kirkstone, etc.)",
|
|
478
|
+
)
|
|
479
|
+
bootstrap.add_argument(
|
|
480
|
+
"--layers-dir",
|
|
481
|
+
default="layers",
|
|
482
|
+
help="Directory to clone repos into",
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# ---------- search ----------
|
|
486
|
+
search = subparsers.add_parser(
|
|
487
|
+
"search",
|
|
488
|
+
help="Search OpenEmbedded Layer Index for layers",
|
|
489
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
490
|
+
epilog="""
|
|
491
|
+
Examples:
|
|
492
|
+
# Browse all layers interactively (fzf)
|
|
493
|
+
bit search
|
|
494
|
+
|
|
495
|
+
# Search for layers matching a term
|
|
496
|
+
bit search security
|
|
497
|
+
|
|
498
|
+
# Clone a layer directly
|
|
499
|
+
bit search -c meta-virtualization
|
|
500
|
+
|
|
501
|
+
# Get layer info for scripting (machine-readable key=value output)
|
|
502
|
+
bit search -i meta-virtualization
|
|
503
|
+
# Output: name=..., url=..., depends=..., optional=...
|
|
504
|
+
|
|
505
|
+
# Use in scripts:
|
|
506
|
+
url=$(bit search -i meta-virt | grep '^url=' | cut -d= -f2)
|
|
507
|
+
""",
|
|
508
|
+
)
|
|
509
|
+
search.add_argument(
|
|
510
|
+
"query",
|
|
511
|
+
nargs="?",
|
|
512
|
+
help="Search term (searches layer names and descriptions)",
|
|
513
|
+
)
|
|
514
|
+
search.add_argument(
|
|
515
|
+
"--branch", "-b",
|
|
516
|
+
default="master",
|
|
517
|
+
help="OE branch to search (master, scarthgap, kirkstone, etc.)",
|
|
518
|
+
)
|
|
519
|
+
search.add_argument(
|
|
520
|
+
"--force", "-f",
|
|
521
|
+
action="store_true",
|
|
522
|
+
help="Force refresh from layer index (ignore cache)",
|
|
523
|
+
)
|
|
524
|
+
search.add_argument(
|
|
525
|
+
"--clone", "-c",
|
|
526
|
+
action="store_true",
|
|
527
|
+
help="Clone the matching layer (requires exact match or single result)",
|
|
528
|
+
)
|
|
529
|
+
search.add_argument(
|
|
530
|
+
"--target", "-t",
|
|
531
|
+
metavar="DIR",
|
|
532
|
+
help="Target directory for clone (default: layers/<name>)",
|
|
533
|
+
)
|
|
534
|
+
search.add_argument(
|
|
535
|
+
"--info", "-i",
|
|
536
|
+
action="store_true",
|
|
537
|
+
help="Show layer info (URL, subdir, dependencies) for scripting",
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# ---------- export ----------
|
|
541
|
+
export = subparsers.add_parser(
|
|
542
|
+
"export",
|
|
543
|
+
help="Export patches from layer repos",
|
|
544
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
545
|
+
epilog="""
|
|
546
|
+
Examples:
|
|
547
|
+
# Export all local commits into one directory with aggregate cover letter
|
|
548
|
+
bit export --target-dir ~/patches
|
|
549
|
+
|
|
550
|
+
# Interactively select commits using fzf (or manual input if fzf unavailable)
|
|
551
|
+
bit export --target-dir ~/patches --pick
|
|
552
|
+
|
|
553
|
+
# Export to per-repo subdirectories, each with its own cover letter
|
|
554
|
+
bit export --target-dir ~/patches --layout per-repo
|
|
555
|
+
|
|
556
|
+
# Combine: interactive selection + per-repo subdirectories
|
|
557
|
+
bit export --target-dir ~/patches --layout per-repo --pick
|
|
558
|
+
|
|
559
|
+
# Force overwrite existing export directory
|
|
560
|
+
bit export --target-dir ~/patches --force
|
|
561
|
+
|
|
562
|
+
# Export as version 2 of a patch series
|
|
563
|
+
bit export --target-dir ~/patches -v 2
|
|
564
|
+
# Produces: [OE-core][PATCH v2 01/05] commit title
|
|
565
|
+
|
|
566
|
+
# Create pull branches in each repo (for git pull requests)
|
|
567
|
+
bit export --target-dir ~/patches --branch feature-xyz
|
|
568
|
+
# Creates 'feature-xyz' branch in each repo, cover letter includes pull URLs
|
|
569
|
+
|
|
570
|
+
Options explained:
|
|
571
|
+
--layout flat (default):
|
|
572
|
+
All patches written to --target-dir with one aggregate cover letter.
|
|
573
|
+
|
|
574
|
+
--layout per-repo:
|
|
575
|
+
Each repo gets its own subdirectory with its own cover letter.
|
|
576
|
+
|
|
577
|
+
--pick:
|
|
578
|
+
Interactive commit selection with fzf (Tab to mark, Enter to confirm).
|
|
579
|
+
Without --pick, exports all commits from origin/<branch>..HEAD.
|
|
580
|
+
|
|
581
|
+
-v N, --series-version N:
|
|
582
|
+
Add version to patch subjects: [PATCH v2 01/05] instead of [PATCH 01/05].
|
|
583
|
+
|
|
584
|
+
-b NAME, --branch NAME:
|
|
585
|
+
Create a pull branch in each repo with the selected commits.
|
|
586
|
+
Branch is based on origin/<branch> with commits cherry-picked.
|
|
587
|
+
Cover letter includes 'git pull <url> <branch>' commands.
|
|
588
|
+
Use --force to overwrite existing branches.
|
|
589
|
+
""",
|
|
590
|
+
)
|
|
591
|
+
export.add_argument(
|
|
592
|
+
"--layout",
|
|
593
|
+
choices=["flat", "per-repo"],
|
|
594
|
+
default="flat",
|
|
595
|
+
help="'flat': all patches in one directory with aggregate cover letter. "
|
|
596
|
+
"'per-repo': each repo gets its own subdirectory with own cover letter",
|
|
597
|
+
)
|
|
598
|
+
export.add_argument(
|
|
599
|
+
"--target-dir",
|
|
600
|
+
help="Directory to place exported patches (created if needed)",
|
|
601
|
+
)
|
|
602
|
+
export.add_argument(
|
|
603
|
+
"--force",
|
|
604
|
+
action="store_true",
|
|
605
|
+
help="Remove existing contents of target directory before export",
|
|
606
|
+
)
|
|
607
|
+
export.add_argument(
|
|
608
|
+
"--pick",
|
|
609
|
+
action="store_true",
|
|
610
|
+
help="Interactively select commits using fzf (or manual input). "
|
|
611
|
+
"Without this flag, exports all commits from origin/<branch>..HEAD",
|
|
612
|
+
)
|
|
613
|
+
export.add_argument(
|
|
614
|
+
"-v", "--series-version",
|
|
615
|
+
type=int,
|
|
616
|
+
metavar="N",
|
|
617
|
+
help="Version number for patch series (e.g., -v 2 produces [PATCH v2 1/5])",
|
|
618
|
+
)
|
|
619
|
+
export.add_argument(
|
|
620
|
+
"-b", "--branch",
|
|
621
|
+
metavar="NAME",
|
|
622
|
+
help="Create a branch with selected commits in each repo for pulling. "
|
|
623
|
+
"Branch is based on origin/<current-branch> with selected commits cherry-picked. "
|
|
624
|
+
"Use --force to overwrite existing branches. "
|
|
625
|
+
"Cover letter will include 'git pull' URLs.",
|
|
626
|
+
)
|
|
627
|
+
export.add_argument(
|
|
628
|
+
"--from-branch",
|
|
629
|
+
metavar="NAME",
|
|
630
|
+
help="Export commits from specified branch instead of HEAD. "
|
|
631
|
+
"Useful after 'export prep --branch' to export from the prep branch.",
|
|
632
|
+
)
|
|
633
|
+
export.add_argument(
|
|
634
|
+
"--export-state-file",
|
|
635
|
+
default=".bit.export-state.json",
|
|
636
|
+
help="JSON file to remember previous export choices per repo. "
|
|
637
|
+
"Stores HEAD SHA, include/skip decision, and range for each repo. "
|
|
638
|
+
"On next export, if HEAD matches, previous choices become defaults",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Export subcommands
|
|
642
|
+
export_sub = export.add_subparsers(dest="export_command", metavar="command")
|
|
643
|
+
|
|
644
|
+
export_prep = export_sub.add_parser(
|
|
645
|
+
"prep",
|
|
646
|
+
help="Group commits for upstream via rebase",
|
|
647
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
648
|
+
epilog="""
|
|
649
|
+
Examples:
|
|
650
|
+
# Interactively select commits to group for upstream in each repo
|
|
651
|
+
bit export prep
|
|
652
|
+
|
|
653
|
+
# Create backup branches before rebasing
|
|
654
|
+
bit export prep --backup
|
|
655
|
+
|
|
656
|
+
# Create a branch for PR submission at the cut point
|
|
657
|
+
bit export prep --branch zedd/kernel
|
|
658
|
+
|
|
659
|
+
# Preview what would happen without making changes
|
|
660
|
+
bit export prep --dry-run
|
|
661
|
+
|
|
662
|
+
Workflow:
|
|
663
|
+
For each repo with local commits:
|
|
664
|
+
1. Shows commits between origin/<branch> and HEAD
|
|
665
|
+
2. User selects which commits are destined for upstream (Tab=toggle, Space=range)
|
|
666
|
+
3. User selects insertion point showing selected commit count
|
|
667
|
+
4. Reorders: commits before insertion -> upstream commits -> remaining
|
|
668
|
+
5. Optionally creates a branch at the cut point (via --branch or b/B keys)
|
|
669
|
+
|
|
670
|
+
After all repos are processed, prompts to proceed with export.
|
|
671
|
+
Saves prep state for automatic use by subsequent 'export' command.
|
|
672
|
+
|
|
673
|
+
This is useful before running 'export' when you have scattered commits
|
|
674
|
+
that need to be grouped together for upstream submission.
|
|
675
|
+
""",
|
|
676
|
+
)
|
|
677
|
+
export_prep.add_argument(
|
|
678
|
+
"--backup",
|
|
679
|
+
action="store_true",
|
|
680
|
+
help="Create backup branch (e.g., <branch>-backup-<timestamp>) before rebasing",
|
|
681
|
+
)
|
|
682
|
+
export_prep.add_argument(
|
|
683
|
+
"--dry-run",
|
|
684
|
+
action="store_true",
|
|
685
|
+
help="Show what would happen without making changes",
|
|
686
|
+
)
|
|
687
|
+
export_prep.add_argument(
|
|
688
|
+
"--plain",
|
|
689
|
+
action="store_true",
|
|
690
|
+
help="Use text-based prompts instead of fzf",
|
|
691
|
+
)
|
|
692
|
+
export_prep.add_argument(
|
|
693
|
+
"--branch",
|
|
694
|
+
metavar="NAME",
|
|
695
|
+
help="Create branch at last upstream commit for PR submission",
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
# ---------- explore ----------
|
|
699
|
+
explore = subparsers.add_parser(
|
|
700
|
+
"explore",
|
|
701
|
+
aliases=["x"],
|
|
702
|
+
help="Interactively explore commits in layer repos",
|
|
703
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
704
|
+
epilog="""
|
|
705
|
+
Examples:
|
|
706
|
+
# Interactive exploration (fzf)
|
|
707
|
+
bit explore
|
|
708
|
+
bit explore OE-core
|
|
709
|
+
|
|
710
|
+
# Text status output (like 'status' command)
|
|
711
|
+
bit explore --status
|
|
712
|
+
bit explore --status -v
|
|
713
|
+
bit explore --status --refresh
|
|
714
|
+
|
|
715
|
+
Repo list navigation:
|
|
716
|
+
Enter/-> Explore commits in selected repo
|
|
717
|
+
u Update (pull --rebase)
|
|
718
|
+
m Merge (pull)
|
|
719
|
+
r Refresh (fetch from origin)
|
|
720
|
+
v Toggle verbose display
|
|
721
|
+
s Show detailed status
|
|
722
|
+
q Quit
|
|
723
|
+
|
|
724
|
+
Commit browser navigation:
|
|
725
|
+
Tab Toggle single commit selection
|
|
726
|
+
Space Mark range endpoints (select all between)
|
|
727
|
+
? Toggle preview pane
|
|
728
|
+
d Toggle diff mode (stat vs full patch)
|
|
729
|
+
c Copy commit hash to clipboard
|
|
730
|
+
e Export selected commit(s) as .patch
|
|
731
|
+
Esc/b/<- Back to repo list
|
|
732
|
+
q Quit entirely
|
|
733
|
+
""",
|
|
734
|
+
)
|
|
735
|
+
explore_repo_arg = explore.add_argument(
|
|
736
|
+
"repo",
|
|
737
|
+
nargs="?",
|
|
738
|
+
help="Jump directly to specific repo (by index, name, or path)",
|
|
739
|
+
)
|
|
740
|
+
explore.add_argument(
|
|
741
|
+
"--upstream-count",
|
|
742
|
+
type=int,
|
|
743
|
+
default=20,
|
|
744
|
+
help="Number of upstream commits to show for context",
|
|
745
|
+
)
|
|
746
|
+
explore.add_argument(
|
|
747
|
+
"--status",
|
|
748
|
+
action="store_true",
|
|
749
|
+
help="Print text status summary instead of interactive fzf",
|
|
750
|
+
)
|
|
751
|
+
explore.add_argument(
|
|
752
|
+
"--refresh",
|
|
753
|
+
action="store_true",
|
|
754
|
+
help="Fetch from origin before showing status",
|
|
755
|
+
)
|
|
756
|
+
explore.add_argument(
|
|
757
|
+
"-v", "--verbose",
|
|
758
|
+
action="count",
|
|
759
|
+
default=0,
|
|
760
|
+
help="Increase verbosity: -v shows commits (limited), -vv shows all commits",
|
|
761
|
+
)
|
|
762
|
+
explore.add_argument(
|
|
763
|
+
"--max-commits",
|
|
764
|
+
type=int,
|
|
765
|
+
default=10,
|
|
766
|
+
help="Max commits to show with -v",
|
|
767
|
+
)
|
|
768
|
+
if HAS_ARGCOMPLETE:
|
|
769
|
+
explore_repo_arg.completer = RepoCompleter()
|
|
770
|
+
|
|
771
|
+
# ---------- config ----------
|
|
772
|
+
config = subparsers.add_parser(
|
|
773
|
+
"config",
|
|
774
|
+
aliases=["c"],
|
|
775
|
+
help="View and configure repo/layer settings",
|
|
776
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
777
|
+
epilog="""
|
|
778
|
+
Examples:
|
|
779
|
+
# Interactive config (fzf interface)
|
|
780
|
+
bit config
|
|
781
|
+
|
|
782
|
+
# CLI: View config for a specific repo
|
|
783
|
+
bit config 1
|
|
784
|
+
bit config OE-core
|
|
785
|
+
|
|
786
|
+
# CLI: Set display name
|
|
787
|
+
bit config 1 --display-name "OE-core"
|
|
788
|
+
bit config 1 --display-name "" # clear
|
|
789
|
+
|
|
790
|
+
# CLI: Set update default
|
|
791
|
+
bit config 1 --update-default skip
|
|
792
|
+
|
|
793
|
+
# CLI: Edit layer.conf
|
|
794
|
+
bit config edit meta-mylayer
|
|
795
|
+
|
|
796
|
+
Interactive keybindings:
|
|
797
|
+
Enter/-> Configure repo (submenu with all options)
|
|
798
|
+
d Set display name (prompts for input)
|
|
799
|
+
r Set default to rebase
|
|
800
|
+
m Set default to merge
|
|
801
|
+
s Set default to skip
|
|
802
|
+
e Edit layer.conf in $EDITOR
|
|
803
|
+
q Quit
|
|
804
|
+
""",
|
|
805
|
+
)
|
|
806
|
+
config_repo_arg = config.add_argument(
|
|
807
|
+
"repo",
|
|
808
|
+
nargs="?",
|
|
809
|
+
help="Repo index (from list), name, or path to configure. Omit to list all repos. Use 'edit <layer>' to edit layer.conf.",
|
|
810
|
+
)
|
|
811
|
+
config_extra_arg = config.add_argument(
|
|
812
|
+
"extra_arg",
|
|
813
|
+
nargs="?",
|
|
814
|
+
help=argparse.SUPPRESS, # Hidden - used for 'edit <layer>' syntax
|
|
815
|
+
)
|
|
816
|
+
if HAS_ARGCOMPLETE:
|
|
817
|
+
config_repo_arg.completer = RepoCompleter(include_edit=True)
|
|
818
|
+
config_extra_arg.completer = LayerCompleter() # For 'edit <layer>'
|
|
819
|
+
config.add_argument(
|
|
820
|
+
"--display-name",
|
|
821
|
+
metavar="NAME",
|
|
822
|
+
help="Set custom display name for patch subjects (empty string to clear)",
|
|
823
|
+
)
|
|
824
|
+
config.add_argument(
|
|
825
|
+
"--update-default",
|
|
826
|
+
metavar="ACTION",
|
|
827
|
+
choices=["rebase", "merge", "skip"],
|
|
828
|
+
help="Set update default action for the repo (rebase, merge, or skip)",
|
|
829
|
+
)
|
|
830
|
+
config.add_argument(
|
|
831
|
+
"-e", "--edit",
|
|
832
|
+
action="store_true",
|
|
833
|
+
help="Open interactive config menu for the specified repo",
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
# ---------- deps ----------
|
|
837
|
+
deps = subparsers.add_parser(
|
|
838
|
+
"deps",
|
|
839
|
+
aliases=["d"],
|
|
840
|
+
help="Show layer and recipe dependencies",
|
|
841
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
842
|
+
epilog="""
|
|
843
|
+
Examples:
|
|
844
|
+
# Interactive layer dependency browser
|
|
845
|
+
bit deps
|
|
846
|
+
bit deps layers
|
|
847
|
+
|
|
848
|
+
# Show dependency tree for a specific layer
|
|
849
|
+
bit deps layers meta-oe
|
|
850
|
+
bit deps layers meta-networking
|
|
851
|
+
|
|
852
|
+
# Show reverse dependencies (what depends on this layer)
|
|
853
|
+
bit deps layers --reverse meta-oe
|
|
854
|
+
bit deps layers -r core
|
|
855
|
+
|
|
856
|
+
# Output formats
|
|
857
|
+
bit deps layers meta-oe --format=tree # ASCII tree (default)
|
|
858
|
+
bit deps layers meta-oe --format=dot # DOT graph (for graphviz)
|
|
859
|
+
bit deps layers meta-oe --format=list # Simple list
|
|
860
|
+
|
|
861
|
+
# Text list mode (no fzf)
|
|
862
|
+
bit deps layers --list
|
|
863
|
+
bit deps layers --list -v
|
|
864
|
+
|
|
865
|
+
Key bindings (in fzf browser):
|
|
866
|
+
Enter Show dependency tree
|
|
867
|
+
ctrl-r Show reverse dependencies
|
|
868
|
+
ctrl-d Output DOT format
|
|
869
|
+
ctrl-a Full graph DOT output
|
|
870
|
+
? Toggle preview
|
|
871
|
+
q Quit
|
|
872
|
+
""",
|
|
873
|
+
)
|
|
874
|
+
deps.add_argument(
|
|
875
|
+
"--list", "-l",
|
|
876
|
+
action="store_true",
|
|
877
|
+
help="List all layers (text output, no fzf)",
|
|
878
|
+
)
|
|
879
|
+
deps.add_argument(
|
|
880
|
+
"-v", "--verbose",
|
|
881
|
+
action="store_true",
|
|
882
|
+
help="Verbose output (with --list)",
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
deps_sub = deps.add_subparsers(dest="deps_command", metavar="command")
|
|
886
|
+
|
|
887
|
+
# deps layers
|
|
888
|
+
deps_layers = deps_sub.add_parser("layers", help="Show layer dependency tree")
|
|
889
|
+
deps_layers.add_argument(
|
|
890
|
+
"layer",
|
|
891
|
+
nargs="?",
|
|
892
|
+
help="Specific layer name (optional, for tree view)",
|
|
893
|
+
)
|
|
894
|
+
deps_layers.add_argument(
|
|
895
|
+
"-r", "--reverse",
|
|
896
|
+
action="store_true",
|
|
897
|
+
help="Show reverse dependencies (what depends on this layer)",
|
|
898
|
+
)
|
|
899
|
+
deps_layers.add_argument(
|
|
900
|
+
"--format",
|
|
901
|
+
choices=["tree", "dot", "list"],
|
|
902
|
+
default="tree",
|
|
903
|
+
help="Output format (default: tree)",
|
|
904
|
+
)
|
|
905
|
+
deps_layers.add_argument(
|
|
906
|
+
"--list", "-l",
|
|
907
|
+
action="store_true",
|
|
908
|
+
help="List all layers (text output, no fzf)",
|
|
909
|
+
)
|
|
910
|
+
deps_layers.add_argument(
|
|
911
|
+
"-v", "--verbose",
|
|
912
|
+
action="store_true",
|
|
913
|
+
help="Verbose output (with --list)",
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
# deps recipe (placeholder for future)
|
|
917
|
+
deps_recipe = deps_sub.add_parser("recipe", help="Show recipe dependency tree")
|
|
918
|
+
deps_recipe.add_argument(
|
|
919
|
+
"recipe",
|
|
920
|
+
help="Recipe name",
|
|
921
|
+
)
|
|
922
|
+
deps_recipe.add_argument(
|
|
923
|
+
"-r", "--rdepends",
|
|
924
|
+
action="store_true",
|
|
925
|
+
help="Include runtime dependencies (RDEPENDS)",
|
|
926
|
+
)
|
|
927
|
+
deps_recipe.add_argument(
|
|
928
|
+
"--format",
|
|
929
|
+
choices=["tree", "dot", "list"],
|
|
930
|
+
default="tree",
|
|
931
|
+
help="Output format (default: tree)",
|
|
932
|
+
)
|
|
933
|
+
|
|
934
|
+
# ---------- branch ----------
|
|
935
|
+
branch = subparsers.add_parser(
|
|
936
|
+
"branch",
|
|
937
|
+
aliases=["b"],
|
|
938
|
+
help="View and switch branches across repos",
|
|
939
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
940
|
+
epilog="""
|
|
941
|
+
Examples:
|
|
942
|
+
# Interactive branch management (fzf interface)
|
|
943
|
+
bit branch
|
|
944
|
+
|
|
945
|
+
# CLI: View current branch for a repo
|
|
946
|
+
bit branch 1
|
|
947
|
+
bit branch OE-core
|
|
948
|
+
|
|
949
|
+
# CLI: Switch a single repo to a branch
|
|
950
|
+
bit branch 1 kirkstone
|
|
951
|
+
bit branch OE-core kirkstone
|
|
952
|
+
|
|
953
|
+
# CLI: Switch all repos to the same branch
|
|
954
|
+
bit branch --all kirkstone
|
|
955
|
+
|
|
956
|
+
Interactive keybindings:
|
|
957
|
+
Enter/-> Select repo and pick branch
|
|
958
|
+
<- Back to repo list
|
|
959
|
+
q Quit
|
|
960
|
+
|
|
961
|
+
Only clean repos can be switched. Dirty repos are skipped with a warning.
|
|
962
|
+
""",
|
|
963
|
+
)
|
|
964
|
+
branch_repo_arg = branch.add_argument(
|
|
965
|
+
"repo",
|
|
966
|
+
nargs="?",
|
|
967
|
+
help="Repo index (from list) or path. Omit to list all repos.",
|
|
968
|
+
)
|
|
969
|
+
branch.add_argument(
|
|
970
|
+
"target_branch",
|
|
971
|
+
nargs="?",
|
|
972
|
+
metavar="BRANCH",
|
|
973
|
+
help="Branch to checkout",
|
|
974
|
+
)
|
|
975
|
+
branch.add_argument(
|
|
976
|
+
"--all",
|
|
977
|
+
dest="all_repos",
|
|
978
|
+
action="store_true",
|
|
979
|
+
help="Switch all repos to the specified branch",
|
|
980
|
+
)
|
|
981
|
+
if HAS_ARGCOMPLETE:
|
|
982
|
+
branch_repo_arg.completer = RepoCompleter()
|
|
983
|
+
|
|
984
|
+
# ---------- help ----------
|
|
985
|
+
help_cmd = subparsers.add_parser(
|
|
986
|
+
"help",
|
|
987
|
+
aliases=["h"],
|
|
988
|
+
help="Browse help for all commands (interactive)",
|
|
989
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
990
|
+
description="""\
|
|
991
|
+
Interactive help browser with preview pane.
|
|
992
|
+
|
|
993
|
+
Browse all commands with live preview of their help text.
|
|
994
|
+
Select a command and press Enter to run it.
|
|
995
|
+
|
|
996
|
+
Keybindings:
|
|
997
|
+
Enter Run selected command
|
|
998
|
+
\\ Expand/collapse subcommands
|
|
999
|
+
? Toggle preview pane
|
|
1000
|
+
q Quit
|
|
1001
|
+
""",
|
|
1002
|
+
)
|
|
1003
|
+
|
|
1004
|
+
# ---------- projects ----------
|
|
1005
|
+
projects = subparsers.add_parser(
|
|
1006
|
+
"projects",
|
|
1007
|
+
aliases=["p"],
|
|
1008
|
+
help="Manage multiple bit working directories",
|
|
1009
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1010
|
+
description="""\
|
|
1011
|
+
Manage and switch between multiple Yocto/OE project directories.
|
|
1012
|
+
|
|
1013
|
+
Run without subcommands for an interactive project picker.
|
|
1014
|
+
|
|
1015
|
+
Projects are stored in ~/.config/bit/projects.json.
|
|
1016
|
+
""",
|
|
1017
|
+
)
|
|
1018
|
+
projects_sub = projects.add_subparsers(dest="projects_command", metavar="command")
|
|
1019
|
+
|
|
1020
|
+
# projects add
|
|
1021
|
+
projects_add = projects_sub.add_parser(
|
|
1022
|
+
"add",
|
|
1023
|
+
help="Add a project directory",
|
|
1024
|
+
)
|
|
1025
|
+
projects_add.add_argument(
|
|
1026
|
+
"path",
|
|
1027
|
+
nargs="?",
|
|
1028
|
+
help="Path to project directory (default: current directory)",
|
|
1029
|
+
)
|
|
1030
|
+
projects_add.add_argument(
|
|
1031
|
+
"-n", "--name",
|
|
1032
|
+
help="Display name for the project",
|
|
1033
|
+
)
|
|
1034
|
+
projects_add.add_argument(
|
|
1035
|
+
"-d", "--description",
|
|
1036
|
+
help="Optional description",
|
|
1037
|
+
)
|
|
1038
|
+
|
|
1039
|
+
# projects remove
|
|
1040
|
+
projects_remove = projects_sub.add_parser(
|
|
1041
|
+
"remove",
|
|
1042
|
+
help="Remove a project from the list",
|
|
1043
|
+
)
|
|
1044
|
+
projects_remove.add_argument(
|
|
1045
|
+
"path",
|
|
1046
|
+
nargs="?",
|
|
1047
|
+
help="Path to project directory (interactive if omitted)",
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# projects list
|
|
1051
|
+
projects_sub.add_parser(
|
|
1052
|
+
"list",
|
|
1053
|
+
help="List all registered projects",
|
|
1054
|
+
)
|
|
1055
|
+
|
|
1056
|
+
# projects shell (alias for init shell)
|
|
1057
|
+
projects_shell = projects_sub.add_parser(
|
|
1058
|
+
"shell",
|
|
1059
|
+
help="Start a shell with build environment (alias for 'init shell')",
|
|
1060
|
+
)
|
|
1061
|
+
projects_shell.add_argument(
|
|
1062
|
+
"--layers-dir",
|
|
1063
|
+
default="layers",
|
|
1064
|
+
help="Directory containing layers (relative to current dir)",
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
# ---------- recipes ----------
|
|
1068
|
+
recipes = subparsers.add_parser(
|
|
1069
|
+
"recipes",
|
|
1070
|
+
aliases=["r"],
|
|
1071
|
+
help="Search and browse BitBake recipes",
|
|
1072
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1073
|
+
epilog="""
|
|
1074
|
+
Examples:
|
|
1075
|
+
# Source selection menu, then search/browser
|
|
1076
|
+
bit recipes
|
|
1077
|
+
|
|
1078
|
+
# Browse ALL local recipes (full list, navigate with fzf search)
|
|
1079
|
+
bit recipes --browse
|
|
1080
|
+
|
|
1081
|
+
# Search with a query
|
|
1082
|
+
bit recipes linux
|
|
1083
|
+
|
|
1084
|
+
# Skip menu, search configured layers only
|
|
1085
|
+
bit recipes --configured linux
|
|
1086
|
+
|
|
1087
|
+
# Skip menu, search all local (configured + discovered)
|
|
1088
|
+
bit recipes --local linux
|
|
1089
|
+
|
|
1090
|
+
# Skip menu, search layer index API
|
|
1091
|
+
bit recipes --index linux
|
|
1092
|
+
|
|
1093
|
+
# Filter to specific layer
|
|
1094
|
+
bit recipes --layer meta-oe
|
|
1095
|
+
|
|
1096
|
+
# Filter by SECTION
|
|
1097
|
+
bit recipes --section kernel
|
|
1098
|
+
|
|
1099
|
+
# Text output (no fzf)
|
|
1100
|
+
bit recipes --list linux
|
|
1101
|
+
|
|
1102
|
+
# Rebuild cache
|
|
1103
|
+
bit recipes --force
|
|
1104
|
+
|
|
1105
|
+
Key bindings (in fzf browser):
|
|
1106
|
+
Enter View recipe in $PAGER or $EDITOR
|
|
1107
|
+
ctrl-e Edit recipe in $EDITOR (local only)
|
|
1108
|
+
alt-c Copy path to clipboard
|
|
1109
|
+
alt-d Show recipe dependencies
|
|
1110
|
+
alt-s Switch source (Configured → Local → Index)
|
|
1111
|
+
? Toggle preview
|
|
1112
|
+
q Quit
|
|
1113
|
+
""",
|
|
1114
|
+
)
|
|
1115
|
+
recipes.add_argument(
|
|
1116
|
+
"query",
|
|
1117
|
+
nargs="?",
|
|
1118
|
+
help="Search term (searches recipe names, summaries, descriptions)",
|
|
1119
|
+
)
|
|
1120
|
+
recipes.add_argument(
|
|
1121
|
+
"--browse",
|
|
1122
|
+
action="store_true",
|
|
1123
|
+
help="Browse ALL local recipes (no query filter, full list)",
|
|
1124
|
+
)
|
|
1125
|
+
recipes.add_argument(
|
|
1126
|
+
"--configured",
|
|
1127
|
+
action="store_true",
|
|
1128
|
+
help="Skip menu, search configured layers only",
|
|
1129
|
+
)
|
|
1130
|
+
recipes.add_argument(
|
|
1131
|
+
"--local",
|
|
1132
|
+
action="store_true",
|
|
1133
|
+
help="Skip menu, search all local (configured + discovered)",
|
|
1134
|
+
)
|
|
1135
|
+
recipes.add_argument(
|
|
1136
|
+
"--index",
|
|
1137
|
+
action="store_true",
|
|
1138
|
+
help="Skip menu, search layer index API",
|
|
1139
|
+
)
|
|
1140
|
+
recipes.add_argument(
|
|
1141
|
+
"--layer",
|
|
1142
|
+
metavar="NAME",
|
|
1143
|
+
help="Filter to specific layer",
|
|
1144
|
+
)
|
|
1145
|
+
recipes.add_argument(
|
|
1146
|
+
"--section",
|
|
1147
|
+
metavar="SEC",
|
|
1148
|
+
help="Filter by SECTION (base, kernel, multimedia, etc.)",
|
|
1149
|
+
)
|
|
1150
|
+
recipes.add_argument(
|
|
1151
|
+
"--list",
|
|
1152
|
+
action="store_true",
|
|
1153
|
+
help="Text output (no fzf)",
|
|
1154
|
+
)
|
|
1155
|
+
recipes.add_argument(
|
|
1156
|
+
"--force",
|
|
1157
|
+
action="store_true",
|
|
1158
|
+
help="Rebuild cache",
|
|
1159
|
+
)
|
|
1160
|
+
recipes.add_argument(
|
|
1161
|
+
"--branch", "-b",
|
|
1162
|
+
default="master",
|
|
1163
|
+
help="Branch for layer index API (master, scarthgap, kirkstone, etc.)",
|
|
1164
|
+
)
|
|
1165
|
+
# Search field options
|
|
1166
|
+
recipes.add_argument(
|
|
1167
|
+
"--name", "-n",
|
|
1168
|
+
action="store_true",
|
|
1169
|
+
help="Search recipe names (default if no field specified)",
|
|
1170
|
+
)
|
|
1171
|
+
recipes.add_argument(
|
|
1172
|
+
"--summary", "-s",
|
|
1173
|
+
action="store_true",
|
|
1174
|
+
help="Search SUMMARY field",
|
|
1175
|
+
)
|
|
1176
|
+
recipes.add_argument(
|
|
1177
|
+
"--description", "-d",
|
|
1178
|
+
action="store_true",
|
|
1179
|
+
help="Search DESCRIPTION field",
|
|
1180
|
+
)
|
|
1181
|
+
recipes.add_argument(
|
|
1182
|
+
"--sort",
|
|
1183
|
+
choices=["name", "layer"],
|
|
1184
|
+
default=None,
|
|
1185
|
+
help="Sort results by name (default) or layer",
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
# ---------- fragments ----------
|
|
1189
|
+
fragments = subparsers.add_parser(
|
|
1190
|
+
"fragments",
|
|
1191
|
+
aliases=["f", "frags"],
|
|
1192
|
+
help="Browse and manage OE configuration fragments",
|
|
1193
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
1194
|
+
epilog="""
|
|
1195
|
+
Examples:
|
|
1196
|
+
# Interactive fragment browser with fzf
|
|
1197
|
+
bit fragments
|
|
1198
|
+
|
|
1199
|
+
# List all available fragments
|
|
1200
|
+
bit fragments list
|
|
1201
|
+
bit fragments list -v
|
|
1202
|
+
|
|
1203
|
+
# Enable a fragment
|
|
1204
|
+
bit fragments enable meta/yocto/sstate-mirror-cdn
|
|
1205
|
+
bit fragments enable machine/qemuarm64
|
|
1206
|
+
|
|
1207
|
+
# Disable a fragment
|
|
1208
|
+
bit fragments disable meta/yocto/sstate-mirror-cdn
|
|
1209
|
+
|
|
1210
|
+
# Show fragment content
|
|
1211
|
+
bit fragments show meta/yocto/sstate-mirror-cdn
|
|
1212
|
+
|
|
1213
|
+
Key bindings (in fzf browser):
|
|
1214
|
+
Enter Toggle enable/disable
|
|
1215
|
+
Tab Multi-select fragments
|
|
1216
|
+
e Enable all selected
|
|
1217
|
+
d Disable all selected
|
|
1218
|
+
v View fragment file
|
|
1219
|
+
c Edit toolcfg.conf
|
|
1220
|
+
? Toggle preview
|
|
1221
|
+
q Quit
|
|
1222
|
+
""",
|
|
1223
|
+
)
|
|
1224
|
+
fragments.add_argument(
|
|
1225
|
+
"--confpath",
|
|
1226
|
+
default=None,
|
|
1227
|
+
help="Path to toolcfg.conf (default: conf/toolcfg.conf)",
|
|
1228
|
+
)
|
|
1229
|
+
fragments.add_argument(
|
|
1230
|
+
"--list", "-l",
|
|
1231
|
+
action="store_true",
|
|
1232
|
+
help="List all fragments (text output, no fzf)",
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
fragments_sub = fragments.add_subparsers(dest="fragment_command", metavar="command")
|
|
1236
|
+
|
|
1237
|
+
# fragments list
|
|
1238
|
+
fragments_list = fragments_sub.add_parser("list", help="List all available fragments")
|
|
1239
|
+
fragments_list.add_argument("-v", "--verbose", action="store_true", help="Show descriptions")
|
|
1240
|
+
|
|
1241
|
+
# fragments enable
|
|
1242
|
+
fragments_enable = fragments_sub.add_parser("enable", help="Enable a fragment")
|
|
1243
|
+
fragments_enable.add_argument("fragmentname", nargs="+", help="Fragment name(s) to enable")
|
|
1244
|
+
|
|
1245
|
+
# fragments disable
|
|
1246
|
+
fragments_disable = fragments_sub.add_parser("disable", help="Disable a fragment")
|
|
1247
|
+
fragments_disable.add_argument("fragmentname", nargs="+", help="Fragment name(s) to disable")
|
|
1248
|
+
|
|
1249
|
+
# fragments show
|
|
1250
|
+
fragments_show = fragments_sub.add_parser("show", help="Show fragment content")
|
|
1251
|
+
fragments_show.add_argument("fragmentname", help="Fragment name to show")
|
|
1252
|
+
|
|
1253
|
+
return parser, {"export": export}
|
|
1254
|
+
|
|
1255
|
+
|
|
1256
|
+
def _has_valid_project_context() -> bool:
|
|
1257
|
+
"""
|
|
1258
|
+
Check if we're in a valid bitbake project directory.
|
|
1259
|
+
Returns True if bblayers.conf exists or layers can be discovered.
|
|
1260
|
+
"""
|
|
1261
|
+
# Check for bblayers.conf
|
|
1262
|
+
candidates = ["conf/bblayers.conf", "build/conf/bblayers.conf"]
|
|
1263
|
+
for cand in candidates:
|
|
1264
|
+
if os.path.exists(cand):
|
|
1265
|
+
return True
|
|
1266
|
+
|
|
1267
|
+
# Try to discover layers (look for conf/layer.conf files)
|
|
1268
|
+
try:
|
|
1269
|
+
for root, dirs, files in os.walk(".", topdown=True):
|
|
1270
|
+
# Skip hidden dirs and common non-layer dirs
|
|
1271
|
+
dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("build", "downloads", "sstate-cache", "tmp")]
|
|
1272
|
+
if "conf" in dirs:
|
|
1273
|
+
layer_conf = os.path.join(root, "conf", "layer.conf")
|
|
1274
|
+
if os.path.exists(layer_conf):
|
|
1275
|
+
return True
|
|
1276
|
+
# Don't go too deep
|
|
1277
|
+
if root.count(os.sep) > 3:
|
|
1278
|
+
dirs.clear()
|
|
1279
|
+
except (OSError, PermissionError):
|
|
1280
|
+
pass
|
|
1281
|
+
|
|
1282
|
+
return False
|
|
1283
|
+
|
|
1284
|
+
|
|
1285
|
+
SORT_MODES = ["category", "alpha", "interactive"]
|
|
1286
|
+
SORT_MODE_LABELS = {
|
|
1287
|
+
"category": "Category",
|
|
1288
|
+
"alpha": "A-Z",
|
|
1289
|
+
"interactive": "Interactive",
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
|
|
1293
|
+
def set_menu_sort_mode(mode: str, defaults_file: str = ".bit.defaults") -> bool:
|
|
1294
|
+
"""
|
|
1295
|
+
Save menu sort mode to defaults file.
|
|
1296
|
+
|
|
1297
|
+
Args:
|
|
1298
|
+
mode: Sort mode ("category", "alpha", or "interactive")
|
|
1299
|
+
defaults_file: Path to defaults file
|
|
1300
|
+
|
|
1301
|
+
Returns:
|
|
1302
|
+
True if saved successfully
|
|
1303
|
+
"""
|
|
1304
|
+
if mode not in SORT_MODES:
|
|
1305
|
+
return False
|
|
1306
|
+
|
|
1307
|
+
try:
|
|
1308
|
+
data = {}
|
|
1309
|
+
if os.path.exists(defaults_file):
|
|
1310
|
+
with open(defaults_file, encoding="utf-8") as f:
|
|
1311
|
+
data = json.load(f)
|
|
1312
|
+
|
|
1313
|
+
data["menu_sort"] = mode
|
|
1314
|
+
|
|
1315
|
+
with open(defaults_file, "w", encoding="utf-8") as f:
|
|
1316
|
+
json.dump(data, f, indent=2)
|
|
1317
|
+
return True
|
|
1318
|
+
except (json.JSONDecodeError, OSError):
|
|
1319
|
+
return False
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def fzf_command_menu(defaults_file: str = ".bit.defaults") -> Optional[str]:
|
|
1323
|
+
"""Show fzf menu to select a subcommand. Returns command name or None if cancelled."""
|
|
1324
|
+
# Get sort mode from defaults
|
|
1325
|
+
sort_mode = get_menu_sort_mode(defaults_file)
|
|
1326
|
+
current_selection: Optional[str] = None
|
|
1327
|
+
|
|
1328
|
+
while True:
|
|
1329
|
+
# Sort/group commands according to mode
|
|
1330
|
+
sorted_commands, categories = sort_commands_by_mode(COMMAND_TREE, sort_mode)
|
|
1331
|
+
|
|
1332
|
+
# Build header with current sort mode indicator
|
|
1333
|
+
mode_label = SORT_MODE_LABELS.get(sort_mode, sort_mode)
|
|
1334
|
+
header = f"Enter=run | \\=expand | ctrl-s=sort ({mode_label}) | q=quit"
|
|
1335
|
+
|
|
1336
|
+
result = fzf_expandable_menu(
|
|
1337
|
+
sorted_commands,
|
|
1338
|
+
header=header,
|
|
1339
|
+
prompt="bit ",
|
|
1340
|
+
height="~80%",
|
|
1341
|
+
categories=categories,
|
|
1342
|
+
sort_key="ctrl-s",
|
|
1343
|
+
initial_selection=current_selection,
|
|
1344
|
+
)
|
|
1345
|
+
|
|
1346
|
+
# Handle sort mode cycling
|
|
1347
|
+
if isinstance(result, tuple) and result[0] == "SORT":
|
|
1348
|
+
# Preserve cursor position if we have a valid command
|
|
1349
|
+
if result[1]:
|
|
1350
|
+
current_selection = result[1]
|
|
1351
|
+
# Cycle to next sort mode
|
|
1352
|
+
current_idx = SORT_MODES.index(sort_mode) if sort_mode in SORT_MODES else 0
|
|
1353
|
+
sort_mode = SORT_MODES[(current_idx + 1) % len(SORT_MODES)]
|
|
1354
|
+
# Save the new mode
|
|
1355
|
+
set_menu_sort_mode(sort_mode, defaults_file)
|
|
1356
|
+
continue
|
|
1357
|
+
|
|
1358
|
+
# Regular selection or cancel
|
|
1359
|
+
return result
|
|
1360
|
+
|
|
1361
|
+
|
|
1362
|
+
def fzf_help_browser() -> Optional[str]:
|
|
1363
|
+
"""
|
|
1364
|
+
Interactive help browser with preview pane.
|
|
1365
|
+
Returns command name to run, or None to exit.
|
|
1366
|
+
"""
|
|
1367
|
+
# Get the script path for preview commands
|
|
1368
|
+
script_path = os.path.abspath(sys.argv[0])
|
|
1369
|
+
|
|
1370
|
+
# Add (general) to the command tree for help browser
|
|
1371
|
+
commands = [("(general)", "Overview and global options", [])] + COMMAND_TREE
|
|
1372
|
+
|
|
1373
|
+
# Build preview command dynamically from COMMAND_TREE
|
|
1374
|
+
# Collect all subcommands that need special handling (have spaces)
|
|
1375
|
+
subcommand_cases = []
|
|
1376
|
+
subcommand_to_parent = {}
|
|
1377
|
+
for cmd, _, subs in COMMAND_TREE:
|
|
1378
|
+
for subcmd, _ in subs:
|
|
1379
|
+
# Subcommands like "init clone" need explicit elif
|
|
1380
|
+
subcommand_cases.append(
|
|
1381
|
+
f'elif [ "$cmd" = "{subcmd}" ]; then "{script_path}" {subcmd} --help 2>&1; '
|
|
1382
|
+
)
|
|
1383
|
+
# Map subcommand to parent for return value
|
|
1384
|
+
subcommand_to_parent[subcmd] = cmd
|
|
1385
|
+
|
|
1386
|
+
preview_cmd = (
|
|
1387
|
+
f'cmd={{1}}; '
|
|
1388
|
+
f'if [ "$cmd" = "(general)" ]; then "{script_path}" --help 2>&1; '
|
|
1389
|
+
+ "".join(subcommand_cases)
|
|
1390
|
+
+ f'else "{script_path}" "$cmd" --help 2>&1; fi'
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
# Options provider: parse --help output for command options
|
|
1394
|
+
def get_options(cmd: str) -> List[Tuple[str, str]]:
|
|
1395
|
+
if cmd == "(general)":
|
|
1396
|
+
return parse_help_options(script_path, "")
|
|
1397
|
+
return parse_help_options(script_path, cmd)
|
|
1398
|
+
|
|
1399
|
+
selected = fzf_expandable_menu(
|
|
1400
|
+
commands,
|
|
1401
|
+
header="Enter=run | \\=expand | v=options | ?=preview | q=quit",
|
|
1402
|
+
prompt="Select command: ",
|
|
1403
|
+
height="100%",
|
|
1404
|
+
preview_cmd=preview_cmd,
|
|
1405
|
+
preview_window="right,60%,wrap",
|
|
1406
|
+
options_provider=get_options,
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
if not selected:
|
|
1410
|
+
return None
|
|
1411
|
+
|
|
1412
|
+
# Don't try to "run" general help
|
|
1413
|
+
if selected == "(general)":
|
|
1414
|
+
return None
|
|
1415
|
+
|
|
1416
|
+
# For subcommands, return the parent command
|
|
1417
|
+
if selected in subcommand_to_parent:
|
|
1418
|
+
return subcommand_to_parent[selected]
|
|
1419
|
+
|
|
1420
|
+
return selected
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
def main(argv=None) -> int:
|
|
1424
|
+
"""Main entry point."""
|
|
1425
|
+
def handle_sigint(signum, frame):
|
|
1426
|
+
print("\nInterrupted, exiting.")
|
|
1427
|
+
sys.exit(1)
|
|
1428
|
+
|
|
1429
|
+
signal.signal(signal.SIGINT, handle_sigint)
|
|
1430
|
+
|
|
1431
|
+
parser, subparsers = build_parser()
|
|
1432
|
+
if HAS_ARGCOMPLETE:
|
|
1433
|
+
argcomplete.autocomplete(parser)
|
|
1434
|
+
args = parser.parse_args(argv)
|
|
1435
|
+
|
|
1436
|
+
# Import commands module here to avoid circular imports
|
|
1437
|
+
from . import commands
|
|
1438
|
+
|
|
1439
|
+
if args.completion:
|
|
1440
|
+
print("Bash completion setup for bit")
|
|
1441
|
+
print("=" * 42)
|
|
1442
|
+
print()
|
|
1443
|
+
print("1. Install argcomplete:")
|
|
1444
|
+
print(" pip install argcomplete")
|
|
1445
|
+
print()
|
|
1446
|
+
print("2. Add to your ~/.bashrc:")
|
|
1447
|
+
print(' eval "$(register-python-argcomplete bit)"')
|
|
1448
|
+
print()
|
|
1449
|
+
print("3. Reload your shell:")
|
|
1450
|
+
print(" source ~/.bashrc")
|
|
1451
|
+
print()
|
|
1452
|
+
print("Usage:")
|
|
1453
|
+
print(" bit config m<TAB> # complete repo names")
|
|
1454
|
+
print(" bit config e<TAB> # complete to 'edit'")
|
|
1455
|
+
print(" bit -<TAB> # complete options")
|
|
1456
|
+
print()
|
|
1457
|
+
if HAS_ARGCOMPLETE:
|
|
1458
|
+
print("Status: argcomplete is installed")
|
|
1459
|
+
else:
|
|
1460
|
+
print("Status: argcomplete is NOT installed")
|
|
1461
|
+
return 0
|
|
1462
|
+
|
|
1463
|
+
# Check for current project and change to it if set
|
|
1464
|
+
# (skip for 'projects' command itself to avoid confusion)
|
|
1465
|
+
if args.command not in ("projects", "p"):
|
|
1466
|
+
current_project = commands.get_current_project()
|
|
1467
|
+
if current_project and os.path.isdir(current_project):
|
|
1468
|
+
# Only change if we're not already in the project or a subdirectory
|
|
1469
|
+
cwd = os.path.abspath(os.getcwd())
|
|
1470
|
+
proj = os.path.abspath(current_project)
|
|
1471
|
+
if not cwd.startswith(proj):
|
|
1472
|
+
os.chdir(current_project)
|
|
1473
|
+
|
|
1474
|
+
if not args.command:
|
|
1475
|
+
# Check if we're in a valid project context
|
|
1476
|
+
# If not, offer to select/add a project
|
|
1477
|
+
if not _has_valid_project_context():
|
|
1478
|
+
# No project context - show projects picker
|
|
1479
|
+
print("No bblayers.conf found and no layers discovered.")
|
|
1480
|
+
print("Use 'projects' to select or add a project directory.\n")
|
|
1481
|
+
if fzf_available():
|
|
1482
|
+
result = commands.run_projects(args, from_auto_prompt=True)
|
|
1483
|
+
if result == 2:
|
|
1484
|
+
# User selected a project and wants to see command menu
|
|
1485
|
+
# Change to the selected project directory
|
|
1486
|
+
current_project = commands.get_current_project()
|
|
1487
|
+
if current_project and os.path.isdir(current_project):
|
|
1488
|
+
os.chdir(current_project)
|
|
1489
|
+
print() # Blank line before menu
|
|
1490
|
+
# Fall through to show command menu
|
|
1491
|
+
else:
|
|
1492
|
+
return result
|
|
1493
|
+
else:
|
|
1494
|
+
print("Run: bit projects add /path/to/project")
|
|
1495
|
+
return 1
|
|
1496
|
+
|
|
1497
|
+
# Show fzf menu if available, otherwise print help
|
|
1498
|
+
if fzf_available():
|
|
1499
|
+
selected = fzf_command_menu()
|
|
1500
|
+
if selected == "help":
|
|
1501
|
+
# Show interactive help browser
|
|
1502
|
+
cmd_to_run = fzf_help_browser()
|
|
1503
|
+
if cmd_to_run:
|
|
1504
|
+
# Split in case of subcommands like "init clone", "export prep"
|
|
1505
|
+
return main(cmd_to_run.split())
|
|
1506
|
+
return 0
|
|
1507
|
+
elif selected:
|
|
1508
|
+
# Re-invoke main with the selected command to get proper arg defaults
|
|
1509
|
+
# Split in case of subcommands like "init clone"
|
|
1510
|
+
return main(selected.split())
|
|
1511
|
+
else:
|
|
1512
|
+
return 0 # Cancelled
|
|
1513
|
+
else:
|
|
1514
|
+
print("Note: Install 'fzf' for interactive menus. Showing help instead.\n")
|
|
1515
|
+
parser.print_help()
|
|
1516
|
+
return 0
|
|
1517
|
+
|
|
1518
|
+
if args.command == "export":
|
|
1519
|
+
export_cmd = getattr(args, "export_command", None)
|
|
1520
|
+
if export_cmd == "prep":
|
|
1521
|
+
return commands.run_prepare_export(args)
|
|
1522
|
+
if not args.target_dir:
|
|
1523
|
+
subparsers["export"].print_help()
|
|
1524
|
+
return 1
|
|
1525
|
+
return commands.run_export(args)
|
|
1526
|
+
|
|
1527
|
+
if args.command in ("update", "u"):
|
|
1528
|
+
return commands.run_update(args)
|
|
1529
|
+
if args.command == "status":
|
|
1530
|
+
# status is an alias for explore --status
|
|
1531
|
+
args.status = True
|
|
1532
|
+
args.refresh = getattr(args, 'fetch', False) # map old --fetch to --refresh
|
|
1533
|
+
args.repo = None
|
|
1534
|
+
args.upstream_count = 20
|
|
1535
|
+
return commands.run_explore(args)
|
|
1536
|
+
if args.command == "repos":
|
|
1537
|
+
return commands.run_repos(args)
|
|
1538
|
+
if args.command == "init":
|
|
1539
|
+
if getattr(args, "init_command", None) == "clone":
|
|
1540
|
+
# Map init clone args to bootstrap args
|
|
1541
|
+
args.clone = getattr(args, "execute", False)
|
|
1542
|
+
return commands.run_bootstrap(args)
|
|
1543
|
+
if getattr(args, "init_command", None) == "shell":
|
|
1544
|
+
return commands.run_init_shell(args)
|
|
1545
|
+
return commands.run_init(args)
|
|
1546
|
+
if args.command == "bootstrap":
|
|
1547
|
+
# Legacy alias - bootstrap uses --clone, init clone uses --execute
|
|
1548
|
+
return commands.run_bootstrap(args)
|
|
1549
|
+
if args.command == "search":
|
|
1550
|
+
return commands.run_search(args)
|
|
1551
|
+
if args.command in ("config", "c"):
|
|
1552
|
+
return commands.run_config(args)
|
|
1553
|
+
if args.command in ("deps", "d"):
|
|
1554
|
+
return commands.run_deps(args)
|
|
1555
|
+
if args.command in ("branch", "b"):
|
|
1556
|
+
return commands.run_branch(args)
|
|
1557
|
+
if args.command in ("explore", "x"):
|
|
1558
|
+
return commands.run_explore(args)
|
|
1559
|
+
if args.command in ("help", "h"):
|
|
1560
|
+
if fzf_available():
|
|
1561
|
+
cmd_to_run = fzf_help_browser()
|
|
1562
|
+
if cmd_to_run:
|
|
1563
|
+
return main(cmd_to_run.split())
|
|
1564
|
+
return 0
|
|
1565
|
+
else:
|
|
1566
|
+
parser.print_help()
|
|
1567
|
+
return 0
|
|
1568
|
+
if args.command in ("projects", "p"):
|
|
1569
|
+
return commands.run_projects(args)
|
|
1570
|
+
if args.command in ("recipes", "r"):
|
|
1571
|
+
return commands.run_recipe(args)
|
|
1572
|
+
if args.command in ("fragments", "f", "frags"):
|
|
1573
|
+
return commands.run_fragment(args)
|
|
1574
|
+
|
|
1575
|
+
parser.error(f"Unknown command: {args.command}")
|
|
1576
|
+
return 1
|
|
1577
|
+
|
|
1578
|
+
|
|
1579
|
+
if __name__ == "__main__":
|
|
1580
|
+
sys.exit(main())
|