55"""
66
77import yaml
8- from typing import Dict , Optional
8+ from typing import Dict , List , Optional , Tuple
99from ...common .context import Context
1010from ...common .module import CommandModule , ValidationError
1111from ..extract .utils import get_commit_changed_files
1212from ...common .utils import log_info , log_error , log_success , log_warning
13+ from .validation import validate_description , validate_feature_name , VALID_PREFIXES
1314
1415
15- def add_feature (ctx : Context , feature_name : str , commit : str , description : Optional [str ] = None ) -> bool :
16- """Add files from a commit to a feature
16+ def add_or_update_feature (
17+ ctx : Context ,
18+ feature_name : str ,
19+ commit : str ,
20+ description : str ,
21+ ) -> Tuple [bool , str ]:
22+ """Add or update a feature with files from a commit.
23+
24+ If feature exists, merges files (appends new ones).
25+ If feature is new, creates it.
26+
27+ Args:
28+ ctx: Build context
29+ feature_name: Feature key name (e.g., 'llm-chat')
30+ commit: Git commit reference
31+ description: Feature description with prefix (e.g., 'feat: LLM chat')
1732
18- Examples:
19- dev feature add my-feature HEAD
20- dev feature add llm-chat HEAD~3 --description "LLM chat integration"
33+ Returns:
34+ Tuple of (success, error_message)
2135 """
36+ # Validate inputs
37+ valid , error = validate_feature_name (feature_name )
38+ if not valid :
39+ return False , error
40+
41+ valid , error = validate_description (description )
42+ if not valid :
43+ return False , error
44+
2245 features_file = ctx .get_features_yaml_path ()
2346
2447 # Get changed files from commit
2548 changed_files = get_commit_changed_files (commit , ctx .chromium_src )
2649 if not changed_files :
27- log_error (f"No changed files found in commit { commit } " )
28- return False
50+ return False , f"No changed files found in commit { commit } "
2951
3052 # Load existing features
31- features : Dict = {"features" : {}}
53+ features : Dict = {"version" : "1.0" , " features" : {}}
3254 if features_file .exists ():
3355 with open (features_file , "r" ) as f :
3456 content = yaml .safe_load (f )
35- if content and "features" in content :
57+ if content :
3658 features = content
59+ if "features" not in features :
60+ features ["features" ] = {}
61+
62+ existing_feature = features ["features" ].get (feature_name )
63+
64+ if existing_feature :
65+ # Update existing feature - merge files
66+ existing_files = set (existing_feature .get ("files" , []))
67+ new_files = set (changed_files )
68+
69+ added_files = new_files - existing_files
70+ already_present = new_files & existing_files
71+ merged_files = existing_files | new_files
72+
73+ log_info (f"Updating existing feature '{ feature_name } '" )
74+ log_info (f" Current files: { len (existing_files )} " )
75+ log_info (f" Files from commit: { len (new_files )} " )
76+
77+ if added_files :
78+ log_success (f" Adding { len (added_files )} new file(s):" )
79+ for f in sorted (added_files )[:10 ]:
80+ log_info (f" + { f } " )
81+ if len (added_files ) > 10 :
82+ log_info (f" ... and { len (added_files ) - 10 } more" )
3783
38- # Add or update feature
39- features ["features" ][feature_name ] = {
40- "description" : description or f"Feature: { feature_name } " ,
41- "files" : sorted (changed_files ),
42- "commit" : commit ,
43- }
84+ if already_present :
85+ log_warning (f" Skipping { len (already_present )} file(s) already in feature" )
86+
87+ features ["features" ][feature_name ]["files" ] = sorted (merged_files )
88+ # Update description if provided (allows updating description)
89+ features ["features" ][feature_name ]["description" ] = description
90+
91+ else :
92+ # Create new feature
93+ log_info (f"Creating new feature '{ feature_name } '" )
94+ log_info (f" Files from commit: { len (changed_files )} " )
95+
96+ features ["features" ][feature_name ] = {
97+ "description" : description ,
98+ "files" : sorted (changed_files ),
99+ }
44100
45101 # Save to file
46102 with open (features_file , "w" ) as f :
47103 yaml .safe_dump (features , f , sort_keys = False , default_flow_style = False )
48104
49- log_success (f"✓ Added feature '{ feature_name } ' with { len (changed_files )} files" )
50- return True
105+ total_files = len (features ["features" ][feature_name ]["files" ])
106+ if existing_feature :
107+ log_success (f"✓ Updated feature '{ feature_name } ' - now has { total_files } files" )
108+ else :
109+ log_success (f"✓ Created feature '{ feature_name } ' with { total_files } files" )
110+
111+ return True , ""
112+
113+
114+ # Keep old function name for backwards compatibility but mark deprecated
115+ def add_feature (ctx : Context , feature_name : str , commit : str , description : Optional [str ] = None ) -> bool :
116+ """Deprecated: Use add_or_update_feature instead."""
117+ if description is None :
118+ log_error ("Description is required and must have a valid prefix" )
119+ return False
120+ success , error = add_or_update_feature (ctx , feature_name , commit , description )
121+ if not success :
122+ log_error (error )
123+ return success
51124
52125
53126def list_features (ctx : Context ):
@@ -134,11 +207,11 @@ def execute(self, ctx: Context, feature_name: str, **kwargs) -> None:
134207 show_feature (ctx , feature_name )
135208
136209
137- class AddFeatureModule (CommandModule ):
138- """Add files from a commit to a feature """
210+ class AddUpdateFeatureModule (CommandModule ):
211+ """Add or update a feature with files from a commit """
139212 produces = []
140213 requires = []
141- description = "Add files from a commit to a feature "
214+ description = "Add or update a feature with files from a commit "
142215
143216 def validate (self , ctx : Context ) -> None :
144217 """Validate git is available"""
@@ -148,10 +221,21 @@ def validate(self, ctx: Context) -> None:
148221 if not ctx .chromium_src .exists ():
149222 raise ValidationError (f"Chromium source not found: { ctx .chromium_src } " )
150223
151- def execute (self , ctx : Context , feature_name : str , commit : str , description : Optional [str ] = None , ** kwargs ) -> None :
152- success = add_feature (ctx , feature_name , commit , description )
224+ def execute (
225+ self ,
226+ ctx : Context ,
227+ name : str ,
228+ commit : str ,
229+ description : str ,
230+ ** kwargs ,
231+ ) -> None :
232+ success , error = add_or_update_feature (ctx , name , commit , description )
153233 if not success :
154- raise RuntimeError (f"Failed to add feature '{ feature_name } '" )
234+ raise RuntimeError (error )
235+
236+
237+ # Backwards compatibility alias
238+ AddFeatureModule = AddUpdateFeatureModule
155239
156240
157241class ClassifyFeaturesModule (CommandModule ):
0 commit comments