cherry-files-toolkit 0.1.0__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.
- cherry_files_toolkit-0.1.0/.gitignore +4 -0
- cherry_files_toolkit-0.1.0/LICENSE +21 -0
- cherry_files_toolkit-0.1.0/PKG-INFO +103 -0
- cherry_files_toolkit-0.1.0/README.md +71 -0
- cherry_files_toolkit-0.1.0/TODO.md +15 -0
- cherry_files_toolkit-0.1.0/cherry-files-diff.sh +275 -0
- cherry_files_toolkit-0.1.0/cherry-files-toolkit.md +176 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/__init__.py +0 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/main.py +39 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/modules/__init__.py +2 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/modules/cherry_files_diff.py +244 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/modules/cherry_files_picker.py +227 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/utilities/__init__.py +2 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/utilities/git_functions.py +67 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/widgets/__init__.py +2 -0
- cherry_files_toolkit-0.1.0/cherry_files_toolkit/widgets/filterable_option_picker.py +74 -0
- cherry_files_toolkit-0.1.0/notes/cherry-files-toolkit.md +177 -0
- cherry_files_toolkit-0.1.0/notes/git-path-specific-checkout.md +35 -0
- cherry_files_toolkit-0.1.0/notes/learning-bash.md +199 -0
- cherry_files_toolkit-0.1.0/notes/learning_tree_structures.py +54 -0
- cherry_files_toolkit-0.1.0/pyproject.toml +19 -0
- cherry_files_toolkit-0.1.0/references/cherry-files-diff.sh +275 -0
- cherry_files_toolkit-0.1.0/references/ref.py +102 -0
- cherry_files_toolkit-0.1.0/ui-ux/cherry-files-picker-search-selected-right.md +25 -0
- cherry_files_toolkit-0.1.0/ui-ux/cherry-files-toolkit.excalidraw +5458 -0
- cherry_files_toolkit-0.1.0/ui-ux/cherry-toolkit-design-01.png +0 -0
- cherry_files_toolkit-0.1.0/ui-ux/cherry-toolkit-design-02.png +0 -0
- cherry_files_toolkit-0.1.0/ui-ux/lazygit-reference.png +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nelson Urrutia
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cherry-files-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TUI toolkit for cherry-picking files between git branches
|
|
5
|
+
Project-URL: Repository, https://github.com/NelsonUrrutia/cherry-files-toolkit
|
|
6
|
+
Author-email: Nelson Urrutia <nelurrutia15@gmail.com>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Nelson Urrutia
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Python: >=3.10
|
|
30
|
+
Requires-Dist: textual
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Cherry Files Toolkit
|
|
34
|
+
|
|
35
|
+
> A terminal UI (built with [Textual](https://textual.textualize.io/)) for moving
|
|
36
|
+
> work between git branches at the **file** level — pick the final state of the
|
|
37
|
+
> files you want and apply them to another branch as one clean commit, no
|
|
38
|
+
> commit-by-commit cherry-picking.
|
|
39
|
+
|
|
40
|
+
It ships two tools side by side:
|
|
41
|
+
|
|
42
|
+
- **Cherry Files Picker** — select files from your current branch and commit
|
|
43
|
+
their current content onto a target branch.
|
|
44
|
+
- **Cherry Files Diff** — see every file added, modified, or deleted on a
|
|
45
|
+
branch since it diverged from its base, grouped as a scannable tree.
|
|
46
|
+
|
|
47
|
+
## The idea
|
|
48
|
+
|
|
49
|
+
This project is a new iteration of my previous
|
|
50
|
+
[Cherry Files Picker](https://github.com/NelsonUrrutia/cherry-files-picker).
|
|
51
|
+
The goal is the same — move the final state of selected files between branches
|
|
52
|
+
as one clean commit — but this version upgrades the UI to a full TUI and adds
|
|
53
|
+
a second module, **Cherry Files Diff**, for understanding branch divergence at
|
|
54
|
+
a glance.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install cherry-files-toolkit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.10+ and `git` on your PATH.
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
Run it from inside any git repository:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cherry-files-toolkit
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Press `Ctrl+Q` to quit.
|
|
73
|
+
|
|
74
|
+
### Cherry Files Picker (right panel)
|
|
75
|
+
|
|
76
|
+
Transfers the final approved state of selected files from your working branch
|
|
77
|
+
to another branch:
|
|
78
|
+
|
|
79
|
+
1. **Source branch** is pre-filled with your current branch.
|
|
80
|
+
2. Pick a **target branch** — where the work should land.
|
|
81
|
+
3. Search and select the **files** to move. Selections persist while you
|
|
82
|
+
filter.
|
|
83
|
+
4. Write a **commit title and description**, review the selected files, and
|
|
84
|
+
hit **Cherry Pick Files**. The toolkit commits those files' current
|
|
85
|
+
content onto the target branch in a single commit.
|
|
86
|
+
|
|
87
|
+
Use it when you know which files are done and want them on another branch
|
|
88
|
+
without replaying the messy commit history that produced them.
|
|
89
|
+
|
|
90
|
+
### Cherry Files Diff (left panel)
|
|
91
|
+
|
|
92
|
+
Explains branch divergence at the file level, so you can recover context on a
|
|
93
|
+
branch you don't know well:
|
|
94
|
+
|
|
95
|
+
1. Pick the **divergent branch** (the one with the work) and its **base
|
|
96
|
+
branch**.
|
|
97
|
+
2. **Start Diff Checker** finds the point where they diverged and lists every
|
|
98
|
+
file created, modified, or deleted since — with per-category counts and a
|
|
99
|
+
directory tree for fast scanning.
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Cherry Files Toolkit
|
|
2
|
+
|
|
3
|
+
> A terminal UI (built with [Textual](https://textual.textualize.io/)) for moving
|
|
4
|
+
> work between git branches at the **file** level — pick the final state of the
|
|
5
|
+
> files you want and apply them to another branch as one clean commit, no
|
|
6
|
+
> commit-by-commit cherry-picking.
|
|
7
|
+
|
|
8
|
+
It ships two tools side by side:
|
|
9
|
+
|
|
10
|
+
- **Cherry Files Picker** — select files from your current branch and commit
|
|
11
|
+
their current content onto a target branch.
|
|
12
|
+
- **Cherry Files Diff** — see every file added, modified, or deleted on a
|
|
13
|
+
branch since it diverged from its base, grouped as a scannable tree.
|
|
14
|
+
|
|
15
|
+
## The idea
|
|
16
|
+
|
|
17
|
+
This project is a new iteration of my previous
|
|
18
|
+
[Cherry Files Picker](https://github.com/NelsonUrrutia/cherry-files-picker).
|
|
19
|
+
The goal is the same — move the final state of selected files between branches
|
|
20
|
+
as one clean commit — but this version upgrades the UI to a full TUI and adds
|
|
21
|
+
a second module, **Cherry Files Diff**, for understanding branch divergence at
|
|
22
|
+
a glance.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install cherry-files-toolkit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Requires Python 3.10+ and `git` on your PATH.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Run it from inside any git repository:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cherry-files-toolkit
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Press `Ctrl+Q` to quit.
|
|
41
|
+
|
|
42
|
+
### Cherry Files Picker (right panel)
|
|
43
|
+
|
|
44
|
+
Transfers the final approved state of selected files from your working branch
|
|
45
|
+
to another branch:
|
|
46
|
+
|
|
47
|
+
1. **Source branch** is pre-filled with your current branch.
|
|
48
|
+
2. Pick a **target branch** — where the work should land.
|
|
49
|
+
3. Search and select the **files** to move. Selections persist while you
|
|
50
|
+
filter.
|
|
51
|
+
4. Write a **commit title and description**, review the selected files, and
|
|
52
|
+
hit **Cherry Pick Files**. The toolkit commits those files' current
|
|
53
|
+
content onto the target branch in a single commit.
|
|
54
|
+
|
|
55
|
+
Use it when you know which files are done and want them on another branch
|
|
56
|
+
without replaying the messy commit history that produced them.
|
|
57
|
+
|
|
58
|
+
### Cherry Files Diff (left panel)
|
|
59
|
+
|
|
60
|
+
Explains branch divergence at the file level, so you can recover context on a
|
|
61
|
+
branch you don't know well:
|
|
62
|
+
|
|
63
|
+
1. Pick the **divergent branch** (the one with the work) and its **base
|
|
64
|
+
branch**.
|
|
65
|
+
2. **Start Diff Checker** finds the point where they diverged and lists every
|
|
66
|
+
file created, modified, or deleted since — with per-category counts and a
|
|
67
|
+
directory tree for fast scanning.
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# TODO
|
|
2
|
+
|
|
3
|
+
## Cherry Files Diff
|
|
4
|
+
|
|
5
|
+
- Preview created and modified files content
|
|
6
|
+
- Navigate with arrow keys
|
|
7
|
+
- Press enter or click the file name
|
|
8
|
+
- Show file diff content in a popup
|
|
9
|
+
- Search for refactor opportunities
|
|
10
|
+
|
|
11
|
+
## Cherry Pick Files
|
|
12
|
+
|
|
13
|
+
- Improve UI
|
|
14
|
+
- Get data from the commit content
|
|
15
|
+
- Start git steps
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Show all files created, modified, or deleted on a branch relative to a base branch.
|
|
3
|
+
# Usage: cherry-files-diff [branch-name] [--base <base-branch>] [--tree] [--no-color] [--help]
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
# ── Help ───────────────────────────────────────────────────────────────────────
|
|
8
|
+
for arg in "$@"; do
|
|
9
|
+
if [[ "$arg" == "--help" || "$arg" == "-h" ]]; then
|
|
10
|
+
cat <<'EOF'
|
|
11
|
+
cherry-files-diff — Show all files changed on a branch, grouped by folder.
|
|
12
|
+
|
|
13
|
+
USAGE
|
|
14
|
+
cherry-files-diff [branch] [--base <base-branch>] [--tree] [--no-color] [--help]
|
|
15
|
+
|
|
16
|
+
ARGUMENTS
|
|
17
|
+
branch Branch to inspect. Defaults to the current branch.
|
|
18
|
+
|
|
19
|
+
FLAGS
|
|
20
|
+
--base <branch> Branch to compare against. Defaults to "main".
|
|
21
|
+
--tree Output as a file tree (ideal for saving to a file).
|
|
22
|
+
--no-color Disable color output (also auto-disabled when piping).
|
|
23
|
+
-h, --help Show this help message.
|
|
24
|
+
|
|
25
|
+
EXAMPLES
|
|
26
|
+
cherry-files-diff
|
|
27
|
+
Compare the current branch against main.
|
|
28
|
+
|
|
29
|
+
cherry-files-diff my-feature --base develop
|
|
30
|
+
Compare my-feature against develop.
|
|
31
|
+
|
|
32
|
+
cherry-files-diff --tree > changes.txt
|
|
33
|
+
Save a tree-formatted plain-text report to a file.
|
|
34
|
+
|
|
35
|
+
OUTPUT
|
|
36
|
+
Files are grouped by top-level folder and categorized as:
|
|
37
|
+
+ CREATED — files added on the branch
|
|
38
|
+
~ MODIFIED — files changed on the branch
|
|
39
|
+
- DELETED — files removed on the branch
|
|
40
|
+
EOF
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
# ── Color support ──────────────────────────────────────────────────────────────
|
|
46
|
+
USE_COLOR=true
|
|
47
|
+
for arg in "$@"; do
|
|
48
|
+
[[ "$arg" == "--no-color" ]] && USE_COLOR=false
|
|
49
|
+
done
|
|
50
|
+
[[ "${NO_COLOR:-}" != "" ]] && USE_COLOR=false
|
|
51
|
+
[ -t 1 ] || USE_COLOR=false
|
|
52
|
+
if $USE_COLOR && ! tput colors &>/dev/null 2>&1; then
|
|
53
|
+
USE_COLOR=false
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
if $USE_COLOR; then
|
|
57
|
+
GREEN="\033[0;32m"
|
|
58
|
+
YELLOW="\033[0;33m"
|
|
59
|
+
RED="\033[0;31m"
|
|
60
|
+
BOLD="\033[1m"
|
|
61
|
+
DIM="\033[2m"
|
|
62
|
+
RESET="\033[0m"
|
|
63
|
+
else
|
|
64
|
+
GREEN="" YELLOW="" RED="" BOLD="" DIM="" RESET=""
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
err() { printf "${RED}ERROR:${RESET} %s\n" "$*" >&2; }
|
|
68
|
+
|
|
69
|
+
# ── Guard: must be inside a git repo ──────────────────────────────────────────
|
|
70
|
+
if ! git rev-parse --git-dir &>/dev/null; then
|
|
71
|
+
err "Not inside a git repository."
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# ── Parse arguments ───────────────────────────────────────────────────────────
|
|
76
|
+
BRANCH=""
|
|
77
|
+
BASE="main"
|
|
78
|
+
TREE=false
|
|
79
|
+
skip_next=false
|
|
80
|
+
|
|
81
|
+
for arg in "$@"; do
|
|
82
|
+
if $skip_next; then
|
|
83
|
+
BASE="$arg"
|
|
84
|
+
skip_next=false
|
|
85
|
+
continue
|
|
86
|
+
fi
|
|
87
|
+
case "$arg" in
|
|
88
|
+
--no-color | --help | -h) ;;
|
|
89
|
+
--tree) TREE=true ;;
|
|
90
|
+
--base) skip_next=true ;;
|
|
91
|
+
*) [[ -z "$BRANCH" ]] && BRANCH="$arg" ;;
|
|
92
|
+
esac
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [[ -z "$BRANCH" ]]; then
|
|
96
|
+
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
97
|
+
printf "${DIM}No branch specified. Using current branch: %s${RESET}\n" "$BRANCH"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# ── Guard: branches must exist ────────────────────────────────────────────────
|
|
101
|
+
branch_exists() {
|
|
102
|
+
git show-ref --verify --quiet "refs/heads/$1" ||
|
|
103
|
+
git show-ref --verify --quiet "refs/remotes/origin/$1"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ! branch_exists "$BRANCH"; then
|
|
107
|
+
err "Branch '$BRANCH' not found locally or on origin."
|
|
108
|
+
echo ""
|
|
109
|
+
echo "Available local branches:"
|
|
110
|
+
git branch --list | sed 's/^/ /'
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if ! branch_exists "$BASE"; then
|
|
115
|
+
err "Base branch '$BASE' not found locally or on origin."
|
|
116
|
+
echo ""
|
|
117
|
+
echo "Available local branches:"
|
|
118
|
+
git branch --list | sed 's/^/ /'
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
# ── Guard: branch and base must differ ────────────────────────────────────────
|
|
123
|
+
if [[ "$BRANCH" == "$BASE" ]]; then
|
|
124
|
+
err "Branch and base are both '$BRANCH'. Select a different branch to compare."
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
# ── Find divergence point ─────────────────────────────────────────────────────
|
|
129
|
+
MERGE_BASE=$(git merge-base "$BASE" "$BRANCH" 2>/dev/null) || {
|
|
130
|
+
err "Could not compute merge-base between '$BASE' and '$BRANCH'."
|
|
131
|
+
exit 1
|
|
132
|
+
}
|
|
133
|
+
MERGE_BASE_SHORT="${MERGE_BASE:0:8}"
|
|
134
|
+
|
|
135
|
+
echo ""
|
|
136
|
+
printf "${BOLD}Branch diff: %s${RESET}\n" "$BRANCH"
|
|
137
|
+
printf "${DIM}Comparing from merge-base %s (where %s diverged from %s)${RESET}\n" "$MERGE_BASE_SHORT" "$BRANCH" "$BASE"
|
|
138
|
+
echo ""
|
|
139
|
+
|
|
140
|
+
# ── Get all changed files ─────────────────────────────────────────────────────
|
|
141
|
+
DIFF_OUTPUT=$(git diff --name-status "$MERGE_BASE".."$BRANCH" 2>/dev/null)
|
|
142
|
+
|
|
143
|
+
if [[ -z "$DIFF_OUTPUT" ]]; then
|
|
144
|
+
printf "${DIM}No file changes found between '%s' and '%s'.${RESET}\n" "$BASE" "$BRANCH"
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
|
|
148
|
+
# ── Parse into buckets ────────────────────────────────────────────────────────
|
|
149
|
+
ADDED=$(echo "$DIFF_OUTPUT" | awk '$1 == "A" {print $2}')
|
|
150
|
+
MODIFIED=$(echo "$DIFF_OUTPUT" | awk '$1 == "M" {print $2}')
|
|
151
|
+
DELETED=$(echo "$DIFF_OUTPUT" | awk '$1 == "D" {print $2}')
|
|
152
|
+
|
|
153
|
+
count() { [[ -z "$1" ]] && echo 0 || echo "$1" | grep -c .; }
|
|
154
|
+
COUNT_A=$(count "$ADDED")
|
|
155
|
+
COUNT_M=$(count "$MODIFIED")
|
|
156
|
+
COUNT_D=$(count "$DELETED")
|
|
157
|
+
COUNT_TOTAL=$((COUNT_A + COUNT_M + COUNT_D))
|
|
158
|
+
|
|
159
|
+
# ── Summary header ────────────────────────────────────────────────────────────
|
|
160
|
+
printf "${BOLD}Summary:${RESET} "
|
|
161
|
+
printf "${GREEN}+%d created${RESET} " "$COUNT_A"
|
|
162
|
+
printf "${YELLOW}~%d modified${RESET} " "$COUNT_M"
|
|
163
|
+
printf "${RED}%s deleted${RESET}\n" "-${COUNT_D}"
|
|
164
|
+
echo ""
|
|
165
|
+
|
|
166
|
+
# ── Tree output ───────────────────────────────────────────────────────────────
|
|
167
|
+
print_as_tree() {
|
|
168
|
+
local files="$1" label="$2" cat_conn="$3" cat_pipe="$4"
|
|
169
|
+
|
|
170
|
+
printf "%s %s\n" "$cat_conn" "$label"
|
|
171
|
+
|
|
172
|
+
local folder_arr=()
|
|
173
|
+
while IFS= read -r line; do
|
|
174
|
+
folder_arr+=("$line")
|
|
175
|
+
done < <(echo "$files" | awk -F'/' 'NF > 1 {print $1} NF == 1 {print "(root)"}' | sort -u)
|
|
176
|
+
|
|
177
|
+
local nfolders=${#folder_arr[@]}
|
|
178
|
+
local fi
|
|
179
|
+
for ((fi = 0; fi < nfolders; fi++)); do
|
|
180
|
+
local folder="${folder_arr[$fi]}"
|
|
181
|
+
local folder_conn="├──" folder_pipe="│ "
|
|
182
|
+
if [[ $fi -eq $((nfolders - 1)) ]]; then
|
|
183
|
+
folder_conn="└──"
|
|
184
|
+
folder_pipe=" "
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
local folder_files
|
|
188
|
+
if [[ "$folder" == "(root)" ]]; then
|
|
189
|
+
folder_files=$(echo "$files" | awk -F'/' 'NF == 1')
|
|
190
|
+
else
|
|
191
|
+
folder_files=$(echo "$files" | awk -v f="$folder/" 'index($0, f) == 1 {print substr($0, length(f)+1)}')
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
local file_arr=()
|
|
195
|
+
while IFS= read -r line; do
|
|
196
|
+
file_arr+=("$line")
|
|
197
|
+
done <<<"$folder_files"
|
|
198
|
+
|
|
199
|
+
local nfiles=${#file_arr[@]}
|
|
200
|
+
printf "%s %s %s/ (%d)\n" "$cat_pipe" "$folder_conn" "$folder" "$nfiles"
|
|
201
|
+
|
|
202
|
+
local fj
|
|
203
|
+
for ((fj = 0; fj < nfiles; fj++)); do
|
|
204
|
+
local file_conn="├──"
|
|
205
|
+
if [[ $fj -eq $((nfiles - 1)) ]]; then file_conn="└──"; fi
|
|
206
|
+
printf "%s %s %s %s\n" "$cat_pipe" "$folder_pipe" "$file_conn" "${file_arr[$fj]}"
|
|
207
|
+
done
|
|
208
|
+
done
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# ── Grouped output ────────────────────────────────────────────────────────────
|
|
212
|
+
print_by_folder() {
|
|
213
|
+
local files="$1" prefix="$2" color="$3"
|
|
214
|
+
|
|
215
|
+
local folders
|
|
216
|
+
folders=$(echo "$files" | awk -F'/' 'NF > 1 {print $1} NF == 1 {print "(root)"}' | sort -u)
|
|
217
|
+
|
|
218
|
+
while IFS= read -r folder; do
|
|
219
|
+
local folder_files folder_count
|
|
220
|
+
if [[ "$folder" == "(root)" ]]; then
|
|
221
|
+
folder_files=$(echo "$files" | awk -F'/' 'NF == 1')
|
|
222
|
+
else
|
|
223
|
+
folder_files=$(echo "$files" | awk -v f="$folder/" 'index($0, f) == 1 {print substr($0, length(f)+1)}')
|
|
224
|
+
fi
|
|
225
|
+
folder_count=$(echo "$folder_files" | grep -c .)
|
|
226
|
+
printf " ${BOLD}%s/${RESET} ${DIM}(%d)${RESET}\n" "$folder" "$folder_count"
|
|
227
|
+
echo "$folder_files" | while IFS= read -r fname; do
|
|
228
|
+
printf " ${color}%s${RESET} %s\n" "$prefix" "$fname"
|
|
229
|
+
done
|
|
230
|
+
done <<<"$folders"
|
|
231
|
+
echo ""
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if $TREE; then
|
|
235
|
+
printf "%s (vs %s)\n" "$BRANCH" "$BASE"
|
|
236
|
+
|
|
237
|
+
cat_arr=()
|
|
238
|
+
if [[ $COUNT_A -gt 0 ]]; then cat_arr+=("A"); fi
|
|
239
|
+
if [[ $COUNT_M -gt 0 ]]; then cat_arr+=("M"); fi
|
|
240
|
+
if [[ $COUNT_D -gt 0 ]]; then cat_arr+=("D"); fi
|
|
241
|
+
|
|
242
|
+
ncats=${#cat_arr[@]}
|
|
243
|
+
for ((ci = 0; ci < ncats; ci++)); do
|
|
244
|
+
cat_conn="├──"
|
|
245
|
+
cat_pipe="│ "
|
|
246
|
+
if [[ $ci -eq $((ncats - 1)) ]]; then
|
|
247
|
+
cat_conn="└──"
|
|
248
|
+
cat_pipe=" "
|
|
249
|
+
fi
|
|
250
|
+
case "${cat_arr[$ci]}" in
|
|
251
|
+
A) print_as_tree "$ADDED" "CREATED ($COUNT_A)" "$cat_conn" "$cat_pipe" ;;
|
|
252
|
+
M) print_as_tree "$MODIFIED" "MODIFIED ($COUNT_M)" "$cat_conn" "$cat_pipe" ;;
|
|
253
|
+
D) print_as_tree "$DELETED" "DELETED ($COUNT_D)" "$cat_conn" "$cat_pipe" ;;
|
|
254
|
+
esac
|
|
255
|
+
done
|
|
256
|
+
echo ""
|
|
257
|
+
else
|
|
258
|
+
if [[ $COUNT_A -gt 0 ]]; then
|
|
259
|
+
printf "${GREEN}${BOLD}CREATED (%d)${RESET}\n" "$COUNT_A"
|
|
260
|
+
print_by_folder "$ADDED" "+" "$GREEN"
|
|
261
|
+
fi
|
|
262
|
+
if [[ $COUNT_M -gt 0 ]]; then
|
|
263
|
+
printf "${YELLOW}${BOLD}MODIFIED (%d)${RESET}\n" "$COUNT_M"
|
|
264
|
+
print_by_folder "$MODIFIED" "~" "$YELLOW"
|
|
265
|
+
fi
|
|
266
|
+
if [[ $COUNT_D -gt 0 ]]; then
|
|
267
|
+
printf "${RED}${BOLD}DELETED (%d)${RESET}\n" "$COUNT_D"
|
|
268
|
+
print_by_folder "$DELETED" "-" "$RED"
|
|
269
|
+
fi
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
# ── Footer ────────────────────────────────────────────────────────────────────
|
|
273
|
+
printf "${BOLD}Total: %d files touched on '%s'${RESET}\n" "$COUNT_TOTAL" "$BRANCH"
|
|
274
|
+
printf "${DIM}(merge-base: %s)${RESET}\n" "$MERGE_BASE_SHORT"
|
|
275
|
+
echo ""
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Cherry Files Toolkit — Requirements & Implementation Checklist
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Two separate bash CLI tools that together form a git file workflow toolkit:
|
|
6
|
+
|
|
7
|
+
| Tool | Command | Purpose |
|
|
8
|
+
| --------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `cherry-files-diff` | `cherry-files-diff [branch] [--base <branch>] [--tree] [--no-color]` | Show all files changed on a branch vs a base, grouped by folder |
|
|
10
|
+
| `cherry-files-picker` | `cherry-files-picker` | Interactive TUI to cherry-pick specific files from a source branch into a target branch as a single clean commit |
|
|
11
|
+
|
|
12
|
+
1. Now — Python: Enhance cherry-files-picker with the diff integration (show changed files as a selectable list, manual entry below). Publish to PyPI when ready.
|
|
13
|
+
2. Later — Bash: Rewrite as a learning exercise, zero deps, distribution-friendly single file.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Tool 1 — cherry-files-diff
|
|
18
|
+
|
|
19
|
+
### What it is
|
|
20
|
+
|
|
21
|
+
Already built. Currently installed at `~/.local/bin/branch-diff`. Needs to be renamed.
|
|
22
|
+
|
|
23
|
+
### Requirements
|
|
24
|
+
|
|
25
|
+
- [ ] Rename `~/.local/bin/branch-diff` → `~/.local/bin/cherry-files-diff`
|
|
26
|
+
- [ ] Update the name in the script's header comment and usage line
|
|
27
|
+
|
|
28
|
+
### Flags (already implemented)
|
|
29
|
+
|
|
30
|
+
- `[branch]` — branch to inspect (default: current branch)
|
|
31
|
+
- `--base <branch>` — branch to compare against (default: `main`)
|
|
32
|
+
- `--tree` — output as a file tree
|
|
33
|
+
- `--no-color` — disable color (auto-disabled when piping)
|
|
34
|
+
- `-h, --help` — show help
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Tool 2 — cherry-files-picker
|
|
39
|
+
|
|
40
|
+
### What it is
|
|
41
|
+
|
|
42
|
+
A bash rewrite of the existing Python `cherry-files-picker` tool, enhanced with a `gum` TUI and integrated diff-based file selection.
|
|
43
|
+
|
|
44
|
+
### Dependencies
|
|
45
|
+
|
|
46
|
+
- `gum` by Charmbracelet — `brew install gum`
|
|
47
|
+
- Script must check for `gum` on startup and print install instructions if missing
|
|
48
|
+
|
|
49
|
+
### Install location
|
|
50
|
+
|
|
51
|
+
`~/.local/bin/cherry-files-picker`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## cherry-files-picker — Detailed Requirements
|
|
56
|
+
|
|
57
|
+
### Startup
|
|
58
|
+
|
|
59
|
+
- [ ] Check `gum` is installed; print `brew install gum` and exit 1 if not
|
|
60
|
+
- [ ] Check we're inside a git repo; styled error + exit 1 if not
|
|
61
|
+
- [ ] Display branded header using `gum style --border rounded`
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### Step 1 — Branch Settings
|
|
66
|
+
|
|
67
|
+
- [ ] Show section header: `── 1. BRANCH SETTINGS ──`
|
|
68
|
+
- [ ] **Source branch** — `gum choose` from `git branch --list` output
|
|
69
|
+
- [ ] **Base branch** — `gum choose` with `main` pre-highlighted (used for diff computation)
|
|
70
|
+
- [ ] **Target branch** — `gum choose` from branch list
|
|
71
|
+
- [ ] Guard: source branch ≠ target branch; show error and exit 1 if equal
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### Step 2 — File Selection
|
|
76
|
+
|
|
77
|
+
- [ ] Show section header: `── 2. FILE SELECTION ──`
|
|
78
|
+
|
|
79
|
+
**Part A — From diff (primary)**
|
|
80
|
+
|
|
81
|
+
- [ ] Compute merge base: `git merge-base "$BASE" "$SOURCE"`
|
|
82
|
+
- [ ] Get changed file list: `git diff --name-only "$MERGE_BASE".."$SOURCE"`
|
|
83
|
+
- [ ] If files exist: present with `gum choose --no-limit` (SPACE to toggle, ENTER to confirm)
|
|
84
|
+
- [ ] If no files in diff: show a notice and skip to Part B
|
|
85
|
+
|
|
86
|
+
**Part B — Manual entry (secondary, optional)**
|
|
87
|
+
|
|
88
|
+
- [ ] Show prompt explaining optional manual additions
|
|
89
|
+
- [ ] Loop: `gum input --placeholder "File path (leave blank to finish)"`
|
|
90
|
+
- [ ] Stop loop on empty input
|
|
91
|
+
- [ ] Append manual entries to selected list; skip duplicates
|
|
92
|
+
|
|
93
|
+
**Validation**
|
|
94
|
+
|
|
95
|
+
- [ ] Guard: at least 1 file must be selected total; show error and exit 1 if none
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
### Step 3 — Commit Settings
|
|
100
|
+
|
|
101
|
+
- [ ] Show section header: `── 3. COMMIT SETTINGS ──`
|
|
102
|
+
- [ ] **Commit title** — `gum input`, re-prompt if blank (required)
|
|
103
|
+
- [ ] **Commit description** — `gum input`, re-prompt if blank (required)
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Step 4 — Summary
|
|
108
|
+
|
|
109
|
+
- [ ] Show section header: `── SUMMARY ──`
|
|
110
|
+
- [ ] Display source branch, base branch, target branch
|
|
111
|
+
- [ ] List all selected files (bulleted)
|
|
112
|
+
- [ ] Display commit title and description
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
### Step 5 — Confirm
|
|
117
|
+
|
|
118
|
+
- [ ] `gum confirm "Continue with merge?"`
|
|
119
|
+
- [ ] On cancel: print "Merge cancelled." and exit 0
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### Step 6 — Execute
|
|
124
|
+
|
|
125
|
+
- [ ] Create a temp directory (`mktemp -d`) for staging file contents
|
|
126
|
+
- [ ] For each selected file: `git show "$SOURCE:$file" > "$TMPDIR/<index>"` (reads from source branch without switching)
|
|
127
|
+
- [ ] `git checkout "$TARGET"`
|
|
128
|
+
- [ ] For each file: `mkdir -p "$(dirname "$file")"` + copy from temp
|
|
129
|
+
- [ ] `git add -A`
|
|
130
|
+
- [ ] `git commit -m "$TITLE" -m "$DESC" -m "Source branch: $SOURCE"`
|
|
131
|
+
- [ ] Clean up temp directory
|
|
132
|
+
- [ ] Display styled success message
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Edge Cases
|
|
137
|
+
|
|
138
|
+
| Scenario | Expected behavior |
|
|
139
|
+
| ----------------------------------- | --------------------------------------------------------- |
|
|
140
|
+
| `gum` not installed | Error with `brew install gum` instructions, exit 1 |
|
|
141
|
+
| Not in a git repo | Styled error, exit 1 |
|
|
142
|
+
| Source == Target branch | Error "Source and target must be different", exit 1 |
|
|
143
|
+
| No files in diff | Skip Part A, go straight to manual entry |
|
|
144
|
+
| No files selected at all | Error "No files selected", exit 1 |
|
|
145
|
+
| Blank commit title or description | Re-prompt until non-empty |
|
|
146
|
+
| User cancels at confirm | "Merge cancelled." exit 0 |
|
|
147
|
+
| File doesn't exist on source branch | `git show` will error; catch and report which file failed |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Verification
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# Tool 1
|
|
155
|
+
cherry-files-diff --help # check renamed correctly
|
|
156
|
+
cherry-files-diff --base main # verify output unchanged
|
|
157
|
+
|
|
158
|
+
# Tool 2 — happy path
|
|
159
|
+
cd ~/Documents/GitHub/Retheme-Half-Magic
|
|
160
|
+
cherry-files-picker
|
|
161
|
+
# 1. Select source: euphoria-collab-launch
|
|
162
|
+
# 2. Select base: main
|
|
163
|
+
# 3. Select target: stage
|
|
164
|
+
# 4. Pick files from diff checkboxes
|
|
165
|
+
# 5. Add one manual file path
|
|
166
|
+
# 6. Enter commit title + description
|
|
167
|
+
# 7. Confirm → check git log on stage shows new commit
|
|
168
|
+
|
|
169
|
+
# Tool 2 — cancel
|
|
170
|
+
cherry-files-picker # proceed to confirm step → select No → exits cleanly
|
|
171
|
+
|
|
172
|
+
# Tool 2 — no gum
|
|
173
|
+
# Temporarily: mv $(which gum) /tmp/gum_bak
|
|
174
|
+
cherry-files-picker # should print install instructions
|
|
175
|
+
# mv /tmp/gum_bak back
|
|
176
|
+
```
|
|
File without changes
|