@@ -232,30 +232,66 @@ async def unstage(root: str, files: list[str]) -> None:
232232
233233
234234async def discard (root : str , files : list [str ]) -> None :
235- """Discard changes in files. Tracked files are restored via git checkout;
236- untracked files are deleted from disk."""
235+ """Fully discard all changes for files — both staged and unstaged.
236+
237+ Tracked modified/deleted files are unstaged then restored via checkout.
238+ Newly added (staged) files are unstaged then deleted from disk.
239+ Untracked files are deleted from disk.
240+ """
237241 if not files :
238242 return
239243
240- # Determine which files are untracked
244+ requested = set (files )
245+
241246 _ , st_out , _ = await _run (
242247 "status" ,
243248 "--porcelain=v2" ,
244249 "--untracked-files=all" ,
245250 cwd = root ,
246251 check = False ,
247252 )
248- untracked = set ()
249- for line in st_out .splitlines ():
250- if line .startswith ("? " ):
251- untracked .add (line [2 :])
252253
253- tracked = [f for f in files if f not in untracked ]
254- to_delete = [f for f in files if f in untracked ]
255-
256- if tracked :
257- await _run ("checkout" , "--" , * tracked , cwd = root )
254+ to_unstage : list [str ] = [] # staged changes that need unstaging first
255+ to_checkout : list [str ] = [] # working-tree changes to restore from HEAD
256+ to_delete : list [str ] = [] # untracked / newly-added files to remove
258257
258+ for line in st_out .splitlines ():
259+ if line .startswith ("? " ):
260+ path = line [2 :]
261+ if path in requested :
262+ to_delete .append (path )
263+ elif line .startswith ("1 " ) or line .startswith ("2 " ):
264+ parts = line .split (" " , 8 )
265+ xy = parts [1 ]
266+ path = parts [- 1 ]
267+ if line .startswith ("2 " ):
268+ path = path .split ("\t " )[0 ]
269+ if path not in requested :
270+ continue
271+ staged_code = xy [0 ]
272+ unstaged_code = xy [1 ]
273+ if staged_code == "A" :
274+ # Newly added file: unstage → becomes untracked → delete
275+ to_unstage .append (path )
276+ to_delete .append (path )
277+ elif staged_code != "." :
278+ # Staged modification/deletion: unstage then checkout
279+ to_unstage .append (path )
280+ to_checkout .append (path )
281+ if unstaged_code != "." and staged_code != "A" :
282+ # Working-tree change: checkout (deduplicated)
283+ if path not in to_checkout :
284+ to_checkout .append (path )
285+
286+ # 1. Unstage any staged changes (reverts index to HEAD)
287+ if to_unstage :
288+ await _run ("restore" , "--staged" , "--" , * to_unstage , cwd = root )
289+
290+ # 2. Restore working-tree files from HEAD
291+ if to_checkout :
292+ await _run ("checkout" , "--" , * to_checkout , cwd = root )
293+
294+ # 3. Remove untracked / newly-added files
259295 for f in to_delete :
260296 full = os .path .join (root , f )
261297 if os .path .isfile (full ):
0 commit comments