Skip to content

Commit e22baf2

Browse files
authored
Merge pull request #13 from BHFock/unstage
Add unstage command and improve flag consistency
2 parents a496ea8 + 270986b commit e22baf2

File tree

2 files changed

+118
-21
lines changed

2 files changed

+118
-21
lines changed

docs/tutorial.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ git-cl: A Git subcommand to manage changelists in Git. Group files by intent, ma
2121
- [Git Status codes](#git-status-codes)
2222
- [Color Key](#color-key)
2323
- [2.3 Diff a changelist](#23-diff-a-changelist)
24-
- [2.4 Stage a changelist](#24-stage-a-changelist)
24+
- [2.4 Stage and unstage a changelist](#24-stage-and-unstage-a-changelist)
2525
- [2.5 Commit a changelist](#25-commit-a-changelist)
2626
- [2.6 Remove files from changelists](#26-remove-files-from-changelists)
2727
- [2.7 Delete changelists](#27-delete-changelists)
@@ -229,15 +229,17 @@ git cl diff docs tests # Show combined diff for both 'docs' and 'tests'
229229
git cl diff docs --staged # Show staged changes for 'docs' changelist
230230
```
231231

232-
### 2.4 Stage a changelist
232+
### 2.4 Stage and Unstage a Changelist
233+
234+
#### Stage a changelist
233235

234236
```
235237
git cl stage <changelist-name>
236238
```
237239

238-
- Stages all tracked files from the changelist.
239-
- Only files already tracked by Git will be staged. Untracked files ([??]) in the changelist are safely ignored and remain untracked unless you add them with `git add` first.
240-
- Changelist is deleted after staging.
240+
- Stages all tracked files in the changelist.
241+
- Untracked files ([??]) are ignored unless added with git add.
242+
- The changelist is deleted after staging, unless --keep is used.
241243

242244
#### Example
243245

@@ -248,6 +250,24 @@ git commit -m "Refactor docs"
248250

249251
Tip: Run `git cl diff` first if you want to review the changes before staging.
250252

253+
#### Unstage a changelist
254+
255+
```
256+
git cl unstage <changelist-name>
257+
```
258+
259+
- Unstages files from the changelist (i.e. removes them from the index).
260+
- Only applies to staged files — unchanged or unstaged files are ignored.
261+
- Files remain in the changelist and your working directory.
262+
263+
#### Example
264+
265+
```
266+
git cl unstage docs
267+
```
268+
269+
This is useful when you've staged something too early and want to pull it back without losing the changelist group.
270+
251271
### 2.5 Commit a changelist
252272

253273
```
@@ -412,7 +432,8 @@ This will show all files, including those with status codes like `[UU]` (unmerge
412432
| View grouped status | `git cl status` / `git cl st` | `git cl st` |
413433
| View all statuses, no color | `git cl status --all --no-color` | |
414434
| Show diff for changelist(s) | `git cl diff <name1> [<name2> ...] [--staged]` | |
415-
| Stage a changelist | `git cl stage <name> [--keep]` | |
435+
| Stage a changelist | `git cl stage <name> [--delete]` | |
436+
| Unstage a changelist | `git cl unstage <name> [--delete]` | |
416437
| Commit with inline message | `git cl commit <name> -m "Message" [--keep]` | `git cl ci` |
417438
| Commit using message from file | `git cl commit <name> -F message.txt [--keep]` | |
418439
| Remove files from changelists | `git cl remove <file1> <file2> ...` | `git cl rm` |

git-cl

Lines changed: 91 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,7 @@ def cl_stage(args: argparse.Namespace) -> None:
585585
deletes the changelist.
586586
587587
Args:
588-
args: argparse.Namespace with 'name' and 'keep' attributes.
588+
args: argparse.Namespace with 'name' and 'delete' attributes.
589589
"""
590590
changelists = clutil_load()
591591
name = args.name
@@ -624,13 +624,71 @@ def cl_stage(args: argparse.Namespace) -> None:
624624
print(f"Error staging files: {error}")
625625
return
626626

627-
# Only delete if --keep flag is not set
628-
if not args.keep:
627+
# Only delete if --delete flag is set
628+
if args.delete:
629+
del changelists[name]
630+
clutil_save(changelists)
631+
print(f"Deleted changelist '{name}'")
632+
633+
634+
def cl_unstage(args: argparse.Namespace) -> None:
635+
"""
636+
Unstages all staged files in the specified changelist, moving them back
637+
to the working directory as unstaged changes.
638+
639+
Args:
640+
args: argparse.Namespace with 'name' and 'keep' attributes.
641+
"""
642+
changelists = clutil_load()
643+
name = args.name
644+
if name not in changelists:
645+
print(f"Changelist '{name}' not found.")
646+
return
647+
648+
git_root = clutil_get_git_root()
649+
to_unstage = []
650+
651+
# Get current status to identify staged files
652+
status_map = clutil_get_file_status_map(show_all=True)
653+
654+
for stored_path in changelists[name]:
655+
# Convert stored path (relative to git root) to absolute path
656+
abs_path = (git_root / stored_path).resolve()
657+
658+
if not abs_path.exists():
659+
print(f"Warning: '{stored_path}' does not exist.")
660+
continue
661+
662+
# Convert to path relative to current working directory for Git command
663+
rel_to_cwd = os.path.relpath(abs_path, Path.cwd())
664+
665+
# Check if this file has staged changes
666+
rel_to_git_root = abs_path.relative_to(git_root).as_posix()
667+
status = status_map.get(rel_to_git_root, " ")
668+
669+
# File has staged changes if first character is not space
670+
if status[0] != " ":
671+
to_unstage.append(rel_to_cwd)
672+
673+
if not to_unstage:
674+
print(f"No staged files to unstage in changelist '{name}'.")
675+
return
676+
677+
try:
678+
# Use git reset HEAD to unstage files
679+
subprocess.run(["git", "reset", "HEAD", "--"] + to_unstage, check=True)
680+
print(f"Unstaged files from changelist '{name}':")
681+
for file in to_unstage:
682+
print(f" {file}")
683+
except subprocess.CalledProcessError as error:
684+
print(f"Error unstaging files: {error}")
685+
return
686+
687+
# Only delete if --delete flag is set
688+
if args.delete:
629689
del changelists[name]
630690
clutil_save(changelists)
631691
print(f"Deleted changelist '{name}'")
632-
else:
633-
print(f"Kept changelist '{name}' (use --keep flag to preserve)")
634692

635693

636694
def cl_status(args: argparse.Namespace) -> None:
@@ -995,22 +1053,40 @@ def main() -> None:
9951053
# stage
9961054
stage_parser = subparsers.add_parser('stage',
9971055
help=("Stage tracked files from a "
998-
"changelist (ignores "
999-
"untracked)"),
1056+
"changelist"),
10001057
description=(
10011058
"Stage all tracked files from "
1002-
"the specified changelist. "
1003-
"Untracked files in the "
1004-
"changelist are ignored and "
1005-
"remain untracked. By default, "
1006-
"the changelist is deleted after "
1007-
"staging unless --keep is used."))
1059+
"the specified changelist. Only "
1060+
"files already tracked by Git "
1061+
"will be staged. Untracked files "
1062+
"in the changelist are safely "
1063+
"ignored and remain untracked. "
1064+
"By default, the changelist is "
1065+
"kept unless --delete is used."))
10081066
stage_parser.add_argument('name', metavar='CHANGELIST',
10091067
help='Name of the changelist to stage')
1010-
stage_parser.add_argument('--keep', action='store_true',
1011-
help='Keep the changelist after staging')
1068+
stage_parser.add_argument('--delete', action='store_true',
1069+
help='Delete the changelist after staging')
10121070
stage_parser.set_defaults(func=cl_stage)
10131071

1072+
# unstage
1073+
unstage_parser = subparsers.add_parser('unstage',
1074+
help=("Unstage tracked files from "
1075+
"a changelist"),
1076+
description=(
1077+
"Unstage all staged files from "
1078+
"the specified changelist, "
1079+
"moving them back to unstaged "
1080+
"state in the working "
1081+
"directory. By default, the "
1082+
"changelist is kept unless "
1083+
"--delete is used."))
1084+
unstage_parser.add_argument('name', metavar='CHANGELIST',
1085+
help='Name of the changelist to unstage')
1086+
unstage_parser.add_argument('--delete', action='store_true',
1087+
help='Delete the changelist after unstaging')
1088+
unstage_parser.set_defaults(func=cl_unstage)
1089+
10141090
# commit / ci
10151091
commit_parser = subparsers.add_parser('commit', aliases=['ci'],
10161092
help=("Commit tracked files from a "

0 commit comments

Comments
 (0)