gitflash 0.7.0.beta1 → 0.7.0.beta3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +59 -0
- data/README.md +17 -13
- data/command_descriptions.yml +25 -7
- data/lib/gitflash/atomic_file.rb +40 -0
- data/lib/gitflash/cherry_pick/log_parser.rb +63 -0
- data/lib/gitflash/cherry_pick.rb +10 -18
- data/lib/gitflash/cli.rb +28 -5
- data/lib/gitflash/command_runner.rb +12 -1
- data/lib/gitflash/commands/base.rb +3 -0
- data/lib/gitflash/commands/clean.rb +29 -1
- data/lib/gitflash/commands/gc.rb +19 -4
- data/lib/gitflash/commands/hook_install.rb +79 -12
- data/lib/gitflash/commands/mark.rb +9 -3
- data/lib/gitflash/commands/pick.rb +13 -1
- data/lib/gitflash/commands/pick_control.rb +1 -1
- data/lib/gitflash/commands/removal_report.rb +8 -3
- data/lib/gitflash/commands/snapshot_create.rb +6 -4
- data/lib/gitflash/commands/undo.rb +24 -11
- data/lib/gitflash/commands/worktree_add.rb +6 -4
- data/lib/gitflash/commands/worktree_command.rb +17 -3
- data/lib/gitflash/commands/worktree_list.rb +3 -3
- data/lib/gitflash/commands/worktree_lock.rb +7 -3
- data/lib/gitflash/commands/worktree_move.rb +9 -4
- data/lib/gitflash/commands/worktree_prune.rb +1 -1
- data/lib/gitflash/commands/worktree_remove.rb +23 -7
- data/lib/gitflash/config.rb +69 -16
- data/lib/gitflash/config_keys.rb +64 -4
- data/lib/gitflash/confirmation_required.rb +6 -2
- data/lib/gitflash/git/bash_command.rb +5 -2
- data/lib/gitflash/hook/branch_marker.rb +109 -6
- data/lib/gitflash/hook/claude.rb +45 -24
- data/lib/gitflash/hook/command_parser.rb +100 -59
- data/lib/gitflash/hook/command_words.rb +73 -0
- data/lib/gitflash/hook/failure.rb +29 -0
- data/lib/gitflash/hook/git_invocation.rb +119 -0
- data/lib/gitflash/hook/history_rules.rb +81 -0
- data/lib/gitflash/hook/losses.rb +51 -0
- data/lib/gitflash/hook/messages.rb +64 -0
- data/lib/gitflash/hook/paths.rb +47 -0
- data/lib/gitflash/hook/rules.rb +36 -12
- data/lib/gitflash/hook/settings.rb +19 -2
- data/lib/gitflash/hook/shell_quotes.rb +64 -0
- data/lib/gitflash/hook/shell_text.rb +101 -0
- data/lib/gitflash/hook/wrappers.rb +67 -0
- data/lib/gitflash/hook_cli.rb +34 -4
- data/lib/gitflash/protection.rb +12 -4
- data/lib/gitflash/repo.rb +3 -2
- data/lib/gitflash/snapshot.rb +9 -2
- data/lib/gitflash/snapshot_check.rb +60 -0
- data/lib/gitflash/snapshots.rb +26 -3
- data/lib/gitflash/state_capture.rb +26 -24
- data/lib/gitflash/terminal_text.rb +9 -0
- data/lib/gitflash/ui.rb +32 -4
- data/lib/gitflash/untracked_files.rb +19 -0
- data/lib/gitflash/usage_report.rb +66 -0
- data/lib/gitflash/version.rb +1 -1
- data/lib/gitflash/worktree.rb +4 -4
- data/schema/v1.json +39 -2
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf03db9e16e9f006dae9721b8ba1b16faf97218d98ea53768ff27b1b3c017069
|
|
4
|
+
data.tar.gz: a369a61204dd224d254b53b00488f5dbab9f344b1dfad49e17762caf575e8d59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66c9f53118048f0071f77952c9d59d761ced1af71bbb19f829e08b84e6fbfbf7f09f13ce440fe6cadfae97ffd9f377a540153d0caa19e1ede8aaa40d9ecfc161
|
|
7
|
+
data.tar.gz: e93052d61297e1e36c15a601a5bc8f6d0053145ceb3f613d45a418457321691999f400a48715cf1a7654b313cb68902854015e23ef9142a345d419b9591667e9
|
data/CHANGELOG.md
CHANGED
|
@@ -87,3 +87,62 @@ Beta of the parallel agent work release. Install with `gem install gitflash --pr
|
|
|
87
87
|
- The Claude Code hook also runs after Bash commands (`PostToolUse`): branches created with plain git (`checkout -b`, `switch -c`, `branch NAME`, `worktree add`) are marked as agent work. `hook install` adds it, so run it again after upgrading; `hook status` reports whether marking is on. A gitflash hook under a matcher that does not cover Bash no longer counts as installed
|
|
88
88
|
- JSON schema: new commands `clean`, `mark`, `pick` and `worktree ...`, error codes `invalid_config`, `unknown_worktree`, `protected_worktree` and `conflict`, the `owner` field, and `recreate_worktree` in the `undo` plan
|
|
89
89
|
- Author names from other branches are shown with control characters escaped in `pick` output
|
|
90
|
+
|
|
91
|
+
## [0.7.0.beta2] - 2026-10-08
|
|
92
|
+
|
|
93
|
+
Second beta of the parallel agent work release: the `pick` branch list, a quieter `hook claude` and a round of security fixes on top of beta1. Install with `gem install gitflash --pre`. If you installed the hook before, run `gitflash hook install` again. Needs git 2.36 or newer.
|
|
94
|
+
|
|
95
|
+
- `pick` without a source branch shows a list of the other local branches in a terminal, as `checkout` does; without a terminal the source is still required
|
|
96
|
+
- `hook claude` is no longer listed in `gitflash hook` help (it is run by Claude Code, not by hand; `gitflash hook help claude` still describes it) and prints a short note instead of waiting for input when run in a terminal. The `hook` line in `gitflash help` now says it installs and checks the Claude Code hooks
|
|
97
|
+
- Security hardening, from a CodeRabbit scan of the repository:
|
|
98
|
+
- Snapshots save untracked files whose names look like Git pathspec magic (`:(exclude)*`) or start or end with a space; before, such a name could drop other files from the snapshot. `Repo#toplevel` and the git readers keep whitespace at the end of a path
|
|
99
|
+
- Confirmation prompts show control characters in paths as escapes, as other output does
|
|
100
|
+
- `protected` branch patterns of the user file and the repository file add up; a repository can protect more branches but can no longer remove the ones the user protects
|
|
101
|
+
- `worktree_dir` may only use `%<repo>s` and `%<branch>s` and is limited to 512 characters (anything else is `invalid_config`); a format width such as `%<repo>999999999s` could exhaust memory
|
|
102
|
+
- `pick --continue --dry-run` only shows the plan
|
|
103
|
+
- `pick` reads `git log -z` with one field per line, so a commit message cannot forge entries in the list, and only commits found by `git rev-list` are offered
|
|
104
|
+
- The git runner never hands a lone string to a shell
|
|
105
|
+
- The Claude hook in `ask` mode asks also when no snapshot could be saved, notices bundled branch flags (`git branch -qD x`), `git clean -i`, and commands inside `bash -c`, `sh -c` and `eval`, and marks a branch as agent work only if it did not exist before the command
|
|
106
|
+
- `hook install` refuses to write `.claude` settings through a symbolic link in the repository (the user scope may be a link)
|
|
107
|
+
- `docs/map/_meta/check.rb` and `sync-twins.sh` do not follow links, read files over 2 MB, or print raw file names
|
|
108
|
+
- Output keeps a path from the repository on one line: a newline or tab in a worktree path is shown as an escape, so it cannot forge a row in the confirmation text, the list or the menus. `Repo#toplevel` and the git readers remove only git's final newline, not a carriage return that ends the path
|
|
109
|
+
- When a command touches several directories the hook tells Claude one `gitflash undo` command per snapshot; a failure to record branches never stops the snapshot; the record directory is refused unless it is a real directory only the user can use
|
|
110
|
+
- A literal backslash in a path is shown doubled, so a path with a newline and a path with the characters backslash and n stay two different choices in the `wt remove` menu
|
|
111
|
+
- The hook marks a branch the command deleted and created again (`git branch -D x && git switch -c x`) as agent work: the record keeps a branch's creation stamp, not only its name
|
|
112
|
+
|
|
113
|
+
## [0.7.0.beta3] - 2026-10-25
|
|
114
|
+
|
|
115
|
+
Third beta of the parallel agent work release: the fixes of a security review of the repository (issues #34 to #54), each reviewed before merging. Install with `gem install gitflash --pre`. Needs git 2.36 or newer.
|
|
116
|
+
|
|
117
|
+
Some behaviour changes, so check scripts and agent instructions that call gitflash:
|
|
118
|
+
|
|
119
|
+
- `gc`, and `wt remove` / `wt clean` of a worktree with ignored files, are no longer confirmed by `--yes`: pass `--confirm TOKEN` with the token the refusal names
|
|
120
|
+
- `gc --older-than` takes a whole number of days, 1 or more
|
|
121
|
+
- `mark` without `--owner` marks the branch as `human` when stdin is a terminal
|
|
122
|
+
- `clean` and `delete` also protect `develop`, `development`, `trunk` and `release/*`
|
|
123
|
+
- A repository's `worktree_dir` must stay next to the repository; set other places in `~/.config/gitflash.yml`
|
|
124
|
+
- Usage errors exit with 2 and, with `--json`, print the JSON envelope
|
|
125
|
+
|
|
126
|
+
The changes:
|
|
127
|
+
|
|
128
|
+
- The Claude hook no longer lets a command through when it fails in `ask` or `deny` mode: an error now asks the user or blocks the command, with the reason. A snapshot that fails no longer stops `ask` mode from asking, and `cd ~unknownuser` or `git -C ~unknownuser` no longer makes the hook fail (#34)
|
|
129
|
+
- The Claude hook snapshots the directory Claude runs in when a `cd` or `git -C` path is decided by the shell (`cd "$(git rev-parse --show-toplevel)"`, `cd "$REPO"`, `cd -`); before, it looked for a directory with that literal name and saved nothing. It says that directory was a guess and may not be the repository the command changes, and it tells the agent about every risky command it could not snapshot, in snapshot mode too, instead of staying silent (#35)
|
|
130
|
+
- The Claude hook sees git commands inside shell syntax: after `if`, `then`, `do`, `while`, `!` and the like, inside `$(...)`, `<(...)` and backticks, and after a quote escaped with a backslash (`echo "a\"b"; git reset --hard`). Quotes are followed as the shell does, and a command nested deeper than three levels counts as risky instead of being skipped. Before, `deny` mode let these through and snapshot mode saved nothing (#36)
|
|
131
|
+
- The Claude hook sees git behind wrappers with options (`sudo -u me`, `env -i`, `/usr/bin/env`, `timeout 5`, `nice -n 5`, `xargs`, `find -exec`), expands git aliases (`-c alias.x=...` and the repository's `alias.*`, including `!` aliases), snapshots the repository that `--work-tree`, `--git-dir`, `GIT_WORK_TREE` or `GIT_DIR` names, follows `pushd`, and no longer takes a redirection such as `> out` for a branch name. When a command deletes branches it does not name, the snapshot keeps every branch instead of none (#37)
|
|
132
|
+
- The Claude hook snapshots before more commands that lose work: `git rm -f`, `git checkout-index -f`, `git read-tree -u --reset`, `git reflog expire` and `delete`, `git gc --prune=now`, `git prune`, `git push . :branch`, `git update-ref --stdin`, `git worktree remove` without `--force` (it still deletes ignored files), and `git clean` without `-f` when `clean.requireForce` is false (#38)
|
|
133
|
+
- `gc --older-than` takes a whole number of days, 1 or more. Before, `0` or a negative value selected every snapshot, including the newest, and a fraction such as `0.5` selected snapshots older than that part of a day (#39)
|
|
134
|
+
- `hook install` keeps the installed mode when run without `--mode`; before, running it again (as the upgrade note says) silently turned `ask` or `deny` back into `snapshot`. A weaker `--mode` now needs `--downgrade`, and the result reports `mode` and `previous_mode` (#40)
|
|
135
|
+
- The Claude hook no longer suggests that `gitflash undo` brings everything back: for `git clean -x`/`-X` and `git worktree remove` it names the ignored files that will be deleted (no snapshot saves them), and it names untracked files over 50 MB that a snapshot left out (#41)
|
|
136
|
+
- `--yes` no longer confirms a change `gitflash undo` cannot revert. `gc` (which deletes snapshots) now fails with `confirmation_required` and a `confirm_token` in the error; re-run with `--confirm TOKEN` to make that exact change, or type the token in a terminal. The token is made from the plan, so it never confirms a different change (#42)
|
|
137
|
+
- `wt remove` and `wt clean` no longer delete ignored files (`.env`, local databases) on `--yes` alone: when a chosen worktree has ignored files, which no snapshot saves, confirming needs `--confirm TOKEN`, as for `gc` (#43)
|
|
138
|
+
- `undo` without an ID restores the latest snapshot taken in the current worktree, not the newest one in the repository, which can belong to another agent working in a parallel worktree. The plan shows the snapshot's `reason` (#44)
|
|
139
|
+
- `clean` and `delete` also protect `develop`, `development`, `trunk` and `release/*`, so a git-flow `develop` merged into `main` is no longer deleted. When the repository has a remote but no `origin/HEAD`, `clean` warns that the default branch was guessed (#45)
|
|
140
|
+
- `--json` prints the envelope for every error of the commands that use it (all but `schema` and `version`, which print their own output). An unexpected error is reported as `failed` (exit 1) instead of a stack trace. A bad option or unknown command is `invalid_usage` with exit 2, as the README says, instead of Thor's text and exit 1. A damaged snapshot ref is skipped with a warning instead of breaking every command that takes a snapshot. Settings whose root is not an object, or that cannot be read, are `invalid_settings`. A path with an unknown `~user` is a usage error (#46)
|
|
141
|
+
- Snapshots read from refs are checked before use: one whose ref name does not match its id, or that holds a commit id that is not a full SHA, an invalid branch name or a relative worktree path, is skipped with a warning. Snapshots are ordered by the time they were taken, not by ref name, so a planted ref cannot become the default for `undo` (#48)
|
|
142
|
+
- A worktree argument that is one worktree's path and another worktree's branch is refused as ambiguous instead of picking whichever is listed first (#52)
|
|
143
|
+
- `mark` without `--owner` marks the branch as `human` when stdin is a terminal and as `agent` otherwise (agents and scripts without a terminal; a script run from a terminal should pass `--owner agent`). Before, a person running `gitflash mark` to claim a branch marked it as agent work, which `clean --agent --force` deletes (#50)
|
|
144
|
+
- A repository's `.gitflash.yml` can no longer place worktrees anywhere on disk: its `worktree_dir` must be a relative path that stays next to the repository (no absolute path, `~` or climbing above the repository's parent), and `worktree_dir` in `~/.config/gitflash.yml` now wins over the repository's (#47)
|
|
145
|
+
- New `snapshot_exclude` setting: untracked files whose path or name matches (for example `*.pem`, `.env*`) are never copied into snapshots, and are listed as `excluded_files`. The README now explains that snapshots copy every untracked file that is not ignored into git objects (#49)
|
|
146
|
+
- `protected` patterns accept `{a,b}` alternatives, and a pattern that could never match a branch (a space at either end, a control character, `~`, `^`, `:` or `\`, `..` outside a `{a,b}` alternative, or an empty one) is `invalid_config` instead of silently protecting nothing (#51)
|
|
147
|
+
- `hook install` writes the settings file in one step (a temporary file renamed into place), keeps its permissions and a dotfiles link, and refuses a `hooks` value it cannot add to. A crash while writing can no longer leave a broken settings file that Claude Code would ignore, together with your permission rules (#53)
|
|
148
|
+
- CI: every GitHub Action is pinned to a commit SHA, checkout no longer keeps the token in the git config, and Dependabot proposes updates to the pins (#54)
|
data/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Or install it yourself as:
|
|
|
24
24
|
|
|
25
25
|
$ gem install gitflash
|
|
26
26
|
|
|
27
|
-
Prerelease versions such as `0.7.0.
|
|
27
|
+
Prerelease versions such as `0.7.0.beta3` are only installed when you ask for them: `gem install gitflash --pre`, or `gem 'gitflash', '0.7.0.beta3'` in a Gemfile.
|
|
28
28
|
|
|
29
29
|
## Requirements
|
|
30
30
|
|
|
@@ -38,16 +38,16 @@ Run `gitflash` inside a git repository to get a list with the available commands
|
|
|
38
38
|
| --- | --- |
|
|
39
39
|
| `gitflash branches` | List local branches with last commit, upstream status and merge status. Filter with `--merged`, `--gone` or `--stale DAYS` |
|
|
40
40
|
| `gitflash checkout [BRANCH]` | Check out a branch, or pick one from a list |
|
|
41
|
-
| `gitflash delete [BRANCH...]` | Delete branches, or pick them from a list. The current
|
|
41
|
+
| `gitflash delete [BRANCH...]` | Delete branches, or pick them from a list. The current and default branch, `main`, `master`, `develop`, `development`, `trunk` and `release/*` are protected. Unmerged branches are kept unless you pass `--force` |
|
|
42
42
|
| `gitflash reset [COMMIT]` | Reset to a commit, or pick one of the latest 100. Mixed by default; `--soft` keeps changes staged, `--hard` discards them after confirmation |
|
|
43
|
-
| `gitflash mark [BRANCH...]` | Mark branches as agent (default
|
|
44
|
-
| `gitflash clean` | Delete merged branches and branches whose upstream is gone, with a snapshot first. `--stale [DAYS]` and `--agent` add stale and agent-created branches; unmerged ones are kept unless `--force`. Protected: current, default, `main`, `master`, `protected` in `.gitflash.yml`, branches checked out in a worktree |
|
|
43
|
+
| `gitflash mark [BRANCH...]` | Mark branches as agent or human work (default: human when stdin is a terminal, agent otherwise; a script run from a terminal should pass `--owner agent`), or `--clear` the mark. Without a branch it marks the current one. Use it for branches made with plain git so `clean --agent` can find them |
|
|
44
|
+
| `gitflash clean` | Delete merged branches and branches whose upstream is gone, with a snapshot first. `--stale [DAYS]` and `--agent` add stale and agent-created branches; unmerged ones are kept unless `--force`. Protected: current, default, `main`, `master`, `develop`, `development`, `trunk`, `release/*`, `protected` in `.gitflash.yml`, branches checked out in a worktree |
|
|
45
45
|
| `gitflash worktree` (`wt`) `list`, `add`, `remove`, `clean`, `prune`, `lock`, `unlock`, `move` | Worktrees for parallel agent work, with absolute paths in `--json` (see below) |
|
|
46
|
-
| `gitflash pick SOURCE [SHA...]` | Cherry-pick commits from another branch: `--list` shows what is missing here, `--continue`, `--skip` and `--abort` handle conflicts |
|
|
47
|
-
| `gitflash undo [SNAPSHOT]` | Restore a snapshot, the latest
|
|
46
|
+
| `gitflash pick [SOURCE] [SHA...]` | Cherry-pick commits from another branch (pick SOURCE from a list when you omit it): `--list` shows what is missing here, `--continue`, `--skip` and `--abort` handle conflicts |
|
|
47
|
+
| `gitflash undo [SNAPSHOT]` | Restore a snapshot, by default the latest one taken in this worktree: deleted branches, moved branches, HEAD, uncommitted and untracked files, dropped stashes |
|
|
48
48
|
| `gitflash snapshot` | Save the current state on request (`--scope`, `--message`) |
|
|
49
49
|
| `gitflash snapshots` | List snapshots, newest first |
|
|
50
|
-
| `gitflash gc` | Delete snapshots older than 30 days (`--older-than DAYS
|
|
50
|
+
| `gitflash gc` | Delete snapshots older than 30 days (`--older-than DAYS`, at least 1) |
|
|
51
51
|
| `gitflash hook install` | Protect plain git commands run by Claude Code and mark the branches it creates (see below) |
|
|
52
52
|
| `gitflash hook status` | Show whether the hook protects Claude Code sessions in the current directory |
|
|
53
53
|
| `gitflash schema` | Print the JSON Schema of the `--json` output |
|
|
@@ -57,18 +57,21 @@ Run `gitflash` inside a git repository to get a list with the available commands
|
|
|
57
57
|
|
|
58
58
|
`gitflash wt add feature` creates a worktree (default `../<repo>.worktrees/feature`) and marks a new branch as agent work. For a branch made with plain git, run `gitflash mark` on it (no argument marks the current branch). `gitflash wt list --json` gives every worktree's absolute path, branch, dirty state, ahead/behind, merge status, lock state and owner, so an agent never has to switch directories.
|
|
59
59
|
|
|
60
|
-
`gitflash wt remove` and `gitflash wt clean` save a snapshot first, and `gitflash undo` adds the worktree back with its branch and uncommitted files. Ignored files (`.env`, `node_modules`) are not saved: the plan lists them before they are deleted
|
|
60
|
+
`gitflash wt remove` and `gitflash wt clean` save a snapshot first, and `gitflash undo` adds the worktree back with its branch and uncommitted files. Ignored files (`.env`, `node_modules`) are not saved: the plan lists them before they are deleted, and removing a worktree that has some needs `--confirm TOKEN` (the token the refusal names) instead of `--yes`. A worktree with an untracked file over 50 MB is not removed.
|
|
61
61
|
|
|
62
62
|
`gitflash pick other-branch --list` lists the commits missing here (a commit whose patch is already here is marked `=`). `gitflash pick other-branch SHA... --yes` applies them oldest first with `-x`. On a conflict it stops and lists the files; resolve them and run `gitflash pick --continue`, or `--skip` / `--abort`.
|
|
63
63
|
|
|
64
|
-
Settings live in `.gitflash.yml` in the repository and `~/.config/gitflash.yml` (the repository file wins):
|
|
64
|
+
Settings live in `.gitflash.yml` in the repository and `~/.config/gitflash.yml` (the repository file wins, except for `worktree_dir`, where your own file wins; a repository's `worktree_dir` must be a relative path that stays next to the repository):
|
|
65
65
|
|
|
66
66
|
```yaml
|
|
67
|
-
protected: ["release/*"] # branch patterns clean and delete never touch
|
|
67
|
+
protected: ["release/*", "hotfix/{a,b}"] # branch patterns clean and delete never touch; both files add up
|
|
68
68
|
stale_days: 30 # used by --stale without a number
|
|
69
|
-
worktree_dir: "../%<repo>s.worktrees/%<branch>s"
|
|
69
|
+
worktree_dir: "../%<repo>s.worktrees/%<branch>s" # only these two placeholders, up to 512 characters
|
|
70
|
+
snapshot_exclude: ["*.pem", ".env*", "secrets"] # untracked files snapshots never copy (a directory covers all below it); both files add up
|
|
70
71
|
```
|
|
71
72
|
|
|
73
|
+
Snapshots copy every untracked file that is not ignored (up to 50 MB) into git objects under `refs/gitflash/snapshots/`. A stray secret that is not in `.gitignore` is copied too, and a `git push --mirror` or a `refs/*` refspec would publish it. Ignore such files, or list them in `snapshot_exclude`; files left out this way cannot be restored by `gitflash undo`.
|
|
74
|
+
|
|
72
75
|
### Undo
|
|
73
76
|
|
|
74
77
|
Before every change it makes, gitflash saves a snapshot: branches, HEAD, tracked, staged and untracked files, and stash entries. Snapshots are plain git objects under `refs/gitflash/snapshots/`; the working tree is never touched while saving. Git's own `revert` and `reflog` cover committed work; snapshots also cover what git never recorded: uncommitted and untracked files, deleted branches (git deletes their reflog) and dropped stashes.
|
|
@@ -91,9 +94,9 @@ Agents often run git directly. Claude Code's own checkpoints do not cover change
|
|
|
91
94
|
gitflash hook install
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
Before Claude Code runs a command that can discard work git cannot restore (`reset`, `checkout -- .`, `restore`, `clean -f`, `branch -D`, `stash drop`, `rebase`, `worktree remove --force`, ...), the hook saves a snapshot and tells the agent how to undo it. Other commands pass through; the hook adds about 0.1 s to commands that do not mention git. `--mode ask` also asks you to approve such commands, `--mode deny` blocks them.
|
|
97
|
+
Before Claude Code runs a command that can discard work git cannot restore (`reset`, `checkout -- .`, `restore`, `clean -f`, `branch -D`, `stash drop`, `rebase`, `worktree remove --force`, ...), the hook saves a snapshot and tells the agent how to undo it. Other commands pass through; the hook adds about 0.1 s to commands that do not mention git. `--mode ask` also asks you to approve such commands, `--mode deny` blocks them. If the hook itself fails, `ask` mode still asks and `deny` mode blocks the command.
|
|
95
98
|
|
|
96
|
-
The same install adds a second hook that runs after a command: when the agent creates a branch with plain git (`git checkout -b`, `git switch -c`, `git branch NAME`, `git worktree add`), gitflash marks it as agent work, so `gitflash clean --agent` can find it. Only new, unmarked branches are marked, and `gitflash mark BRANCH --owner human` changes it. If you installed the hook with an earlier version, run `gitflash hook install` again to add it; `gitflash hook status` says whether branch marking is on.
|
|
99
|
+
The same install adds a second hook that runs after a command: when the agent creates a branch with plain git (`git checkout -b`, `git switch -c`, `git branch NAME`, `git worktree add`), gitflash marks it as agent work, so `gitflash clean --agent` can find it. Only new, unmarked branches are marked, and `gitflash mark BRANCH --owner human` changes it. If you installed the hook with an earlier version, run `gitflash hook install` again to add it (the installed `--mode` is kept; a weaker mode needs `--downgrade`); `gitflash hook status` says whether branch marking is on.
|
|
97
100
|
|
|
98
101
|
Where the hook goes, following Claude Code's own rules:
|
|
99
102
|
|
|
@@ -125,6 +128,7 @@ Without a terminal (for example in a script, CI or an AI coding agent), gitflash
|
|
|
125
128
|
|
|
126
129
|
- A command that needs a list selection fails with exit code 2 and says which argument to pass.
|
|
127
130
|
- A destructive action (`delete`, `reset --hard`) fails with exit code 2 and returns the plan unless you pass `--yes`.
|
|
131
|
+
- A change `gitflash undo` cannot revert (`gc`, which deletes snapshots, and `wt remove` or `wt clean` of a worktree with ignored files) is not covered by `--yes`: it fails with exit code 2 and names a token (with `--json`, the plan and `error.confirm_token`; `--dry-run` shows the plan as text). Re-run with `--confirm TOKEN` to make that exact change; in a terminal you type the token instead.
|
|
128
132
|
|
|
129
133
|
With `--json`, every command prints one object in the same envelope, defined in [schema/v1.json](schema/v1.json) (also printed by `gitflash schema`):
|
|
130
134
|
|
data/command_descriptions.yml
CHANGED
|
@@ -26,7 +26,8 @@ delete:
|
|
|
26
26
|
short: 'Delete branches'
|
|
27
27
|
long: |
|
|
28
28
|
Deletes the given BRANCHES. Without them, presents a list with the local branches
|
|
29
|
-
(without the current
|
|
29
|
+
(without the current and default branch or main, master, develop, development, trunk and
|
|
30
|
+
release/*): press `space` to choose
|
|
30
31
|
branches and `enter` to confirm. Without a terminal, BRANCHES are required.
|
|
31
32
|
|
|
32
33
|
Branches with unmerged changes are kept unless --force is given.
|
|
@@ -48,7 +49,7 @@ reset:
|
|
|
48
49
|
confirmation; pass --yes to skip it, which is required without a terminal.
|
|
49
50
|
|
|
50
51
|
undo:
|
|
51
|
-
short: 'Restore a snapshot (the latest by default)'
|
|
52
|
+
short: 'Restore a snapshot (the latest of this worktree by default)'
|
|
52
53
|
long: |
|
|
53
54
|
Restores the parts of the repository a snapshot saved: deleted or moved branches,
|
|
54
55
|
HEAD, tracked, staged and untracked files, and dropped stash entries. Only parts that
|
|
@@ -57,6 +58,12 @@ undo:
|
|
|
57
58
|
gitflash saves a snapshot before every change it makes, and the agent hook saves one
|
|
58
59
|
before destructive git commands. List them with `gitflash snapshots`.
|
|
59
60
|
|
|
61
|
+
Without SNAPSHOT it restores the latest snapshot taken in this worktree. All worktrees of
|
|
62
|
+
a repository share the snapshots, so the newest one may belong to another agent's
|
|
63
|
+
worktree. To restore one of those, run `gitflash undo ID` in the worktree it was taken in;
|
|
64
|
+
here an ID from elsewhere works only for a snapshot that saved branches alone. The plan
|
|
65
|
+
shows the snapshot's reason.
|
|
66
|
+
|
|
60
67
|
The current state is saved as a new snapshot first, so an undo can itself be undone.
|
|
61
68
|
Asks for confirmation; pass --yes to skip it (required without a terminal) or
|
|
62
69
|
--dry-run to only show the plan.
|
|
@@ -83,8 +90,10 @@ clean:
|
|
|
83
90
|
--stale [DAYS] no commits for DAYS days (default: `stale_days` setting, 30)
|
|
84
91
|
--agent created by an agent (marked by `gitflash wt add`)
|
|
85
92
|
|
|
86
|
-
Never selected: the current and default branch, main, master,
|
|
87
|
-
.gitflash.yml, and branches checked out in any
|
|
93
|
+
Never selected: the current and default branch, main, master, develop, development, trunk,
|
|
94
|
+
release/*, `protected` branches from .gitflash.yml, and branches checked out in any
|
|
95
|
+
worktree. With an origin remote but no origin/HEAD the default branch is guessed, and
|
|
96
|
+
the plan warns about it. Unmerged branches are kept unless
|
|
88
97
|
--force is given. Every chosen branch lists why it was chosen.
|
|
89
98
|
|
|
90
99
|
Asks for confirmation; pass --yes to skip it (required without a terminal), or --dry-run.
|
|
@@ -98,7 +107,9 @@ worktree_remove:
|
|
|
98
107
|
|
|
99
108
|
Each removal saves a snapshot first, including uncommitted and untracked files. Ignored
|
|
100
109
|
files (build output, dependencies, .env) are not saved: the plan and the confirmation list
|
|
101
|
-
them, since removal deletes them for good
|
|
110
|
+
them, since removal deletes them for good, and --yes is not enough to confirm that: pass
|
|
111
|
+
--confirm TOKEN with the token the refusal names (error.confirm_token in --json), or
|
|
112
|
+
type it in a terminal. A worktree with an untracked file over 50 MB
|
|
102
113
|
is not removed, because the snapshot cannot hold it. `gitflash undo ID` brings the
|
|
103
114
|
directory, its branch and the saved files back.
|
|
104
115
|
|
|
@@ -110,6 +121,10 @@ pick:
|
|
|
110
121
|
long: |
|
|
111
122
|
Moves fixes between branches, for example between agent branches.
|
|
112
123
|
|
|
124
|
+
Without SOURCE, a terminal shows a list of the other local branches to choose from (use the
|
|
125
|
+
arrow keys, type to search, `enter` to choose and `ctrl+c` to cancel). Without a terminal,
|
|
126
|
+
SOURCE is required.
|
|
127
|
+
|
|
113
128
|
`gitflash pick SOURCE --list` lists the commits of SOURCE that are not on the current branch
|
|
114
129
|
yet, oldest first, with SHA, subject, author, date and a diff summary. A commit whose change
|
|
115
130
|
is already here (the same patch under another SHA) is marked `=` (`applied` in JSON) and is
|
|
@@ -138,8 +153,11 @@ mark:
|
|
|
138
153
|
created. The mark is the git config key `branch.<name>.gitflash-owner` and disappears with
|
|
139
154
|
the branch.
|
|
140
155
|
|
|
141
|
-
Without BRANCH the current branch is marked.
|
|
142
|
-
|
|
156
|
+
Without BRANCH the current branch is marked. Without --owner the owner is `human` when you
|
|
157
|
+
run it in a terminal and `agent` when stdin is not a terminal (agents, CI), so a person
|
|
158
|
+
never marks their own branch as one `clean --agent` deletes; a script run from a terminal
|
|
159
|
+
should pass --owner agent; --clear removes the mark. A
|
|
160
|
+
branch without a mark counts as human work.
|
|
143
161
|
|
|
144
162
|
`gitflash wt add` marks the branches it creates. Use `gitflash mark` for branches made with
|
|
145
163
|
plain git, for example at the start of an agent session: `gitflash mark`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Gitflash
|
|
7
|
+
# Replaces a file in one step: the text goes to a temporary file next to it, which is then
|
|
8
|
+
# renamed into place, so a crash never leaves half a file. The file keeps its permissions,
|
|
9
|
+
# and a symbolic link (dotfiles) is followed, so the link stays and its target changes.
|
|
10
|
+
module AtomicFile
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def write(path, text)
|
|
14
|
+
path = link_target(path) if File.symlink?(path)
|
|
15
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
16
|
+
file = Tempfile.create(['.gitflash', File.extname(path)], File.dirname(path))
|
|
17
|
+
replace(file, path, text)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Where a link points, also when that file does not exist yet: the link's directory and the
|
|
21
|
+
# target's directory are resolved, the file name is kept
|
|
22
|
+
def link_target(path)
|
|
23
|
+
target = File.expand_path(File.readlink(path), File.dirname(path))
|
|
24
|
+
return File.realpath(target) if File.exist?(target)
|
|
25
|
+
|
|
26
|
+
File.join(File.realpath(File.dirname(target)), File.basename(target))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def replace(file, path, text)
|
|
30
|
+
file.write(text)
|
|
31
|
+
file.flush
|
|
32
|
+
file.fsync
|
|
33
|
+
file.close
|
|
34
|
+
File.chmod(File.stat(path).mode & 0o7777, file.path) if File.exist?(path)
|
|
35
|
+
File.rename(file.path, path)
|
|
36
|
+
ensure
|
|
37
|
+
FileUtils.rm_f(file.path) if file && File.exist?(file.path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitflash
|
|
4
|
+
class CherryPick
|
|
5
|
+
# Reads the output of `git log -z --shortstat --format=FORMAT` into Candidate records.
|
|
6
|
+
#
|
|
7
|
+
# One field per line: a subject or an author name cannot hold a newline, and `-z` ends each
|
|
8
|
+
# commit with a NUL, which text cannot hold either, so the framing is unambiguous whatever a
|
|
9
|
+
# commit message says. Git prints the fields of a commit, a NUL, the stat of that commit and
|
|
10
|
+
# the fields of the next one. So chunk 0 holds the fields of commit 0, chunk i the stat of
|
|
11
|
+
# commit i-1 and the fields of commit i, and the last chunk only a stat.
|
|
12
|
+
module LogParser
|
|
13
|
+
FORMAT = '%m%n%H%n%s%n%an%n%cI'
|
|
14
|
+
FIELDS = 5
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def parse(output)
|
|
19
|
+
chunks = output.split("\0", -1)
|
|
20
|
+
stats = stats_of(chunks)
|
|
21
|
+
fields = chunks[0...-1].map { |chunk| chunk.lines(chomp: true).last(FIELDS) }
|
|
22
|
+
fields.each_with_index.filter_map do |row, index|
|
|
23
|
+
candidate(row, stats[index + 1]) if row.size == FIELDS
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def stats_of(chunks)
|
|
28
|
+
last = chunks.size - 1
|
|
29
|
+
chunks.each_with_index.map { |chunk, index| stat_part(chunk, index == last) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The part of a chunk before the fields of the next commit; the last chunk is all stat
|
|
33
|
+
def stat_part(chunk, last)
|
|
34
|
+
last ? chunk : chunk.lines(chomp: true)[0...-FIELDS].to_a.join("\n")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def candidate(row, stat_text)
|
|
38
|
+
mark, sha, subject, author, date = row
|
|
39
|
+
files, insertions, deletions = stat(stat_text)
|
|
40
|
+
Candidate.new(sha: sha, subject: subject, author: author, date: date,
|
|
41
|
+
applied: mark == '=', files: files, insertions: insertions,
|
|
42
|
+
deletions: deletions)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# [files, insertions, deletions] from a line such as
|
|
46
|
+
# " 2 files changed, 3 insertions(+), 1 deletion(-)". Split on commas and spaces, with no
|
|
47
|
+
# regular expression, so a long run of digits in the text costs only its own length.
|
|
48
|
+
def stat(text)
|
|
49
|
+
line = text.to_s.lines.map(&:strip).reject(&:empty?).last.to_s
|
|
50
|
+
counts = { 'file' => 0, 'insertion' => 0, 'deletion' => 0 }
|
|
51
|
+
line.split(',').each { |part| tally(counts, part) }
|
|
52
|
+
counts.values
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def tally(counts, part)
|
|
56
|
+
count, label = part.strip.split(' ', 2)
|
|
57
|
+
number = Integer(count, 10, exception: false)
|
|
58
|
+
kind = counts.keys.find { |name| label.to_s.start_with?(name) }
|
|
59
|
+
counts[kind] = number if number && kind
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/gitflash/cherry_pick.rb
CHANGED
|
@@ -19,9 +19,6 @@ module Gitflash
|
|
|
19
19
|
# The state of an unfinished pick: files with conflicts and the commits still to apply
|
|
20
20
|
Progress = Data.define(:conflicted, :remaining, :current)
|
|
21
21
|
|
|
22
|
-
RECORD = "\x1e"
|
|
23
|
-
SEPARATOR = "\x1f"
|
|
24
|
-
FORMAT = ['%m', '%H', '%s', '%an', '%cI'].join(SEPARATOR)
|
|
25
22
|
ENV_NO_EDITOR = { 'GIT_EDITOR' => 'true', 'GIT_SEQUENCE_EDITOR' => 'true' }.freeze
|
|
26
23
|
|
|
27
24
|
def initialize(bash: Git::BashCommand)
|
|
@@ -38,10 +35,11 @@ module Gitflash
|
|
|
38
35
|
# Commits of `source_ref` that are not merges, oldest first, each marked as applied when an
|
|
39
36
|
# equivalent change (same patch) is already on HEAD
|
|
40
37
|
def candidates(source_ref)
|
|
41
|
-
output = @bash.exec('git', 'log', '--left-right', '--cherry-mark', '--right-only',
|
|
42
|
-
'--no-merges', '--reverse', '--shortstat', "--format=#{
|
|
38
|
+
output = @bash.exec('git', 'log', '-z', '--left-right', '--cherry-mark', '--right-only',
|
|
39
|
+
'--no-merges', '--reverse', '--shortstat', "--format=#{LogParser::FORMAT}",
|
|
43
40
|
"HEAD...#{source_ref}", '--')
|
|
44
|
-
|
|
41
|
+
eligible = eligible_shas(source_ref)
|
|
42
|
+
LogParser.parse(output).select { |candidate| eligible.include?(candidate.sha) }.uniq(&:sha)
|
|
45
43
|
end
|
|
46
44
|
|
|
47
45
|
# Applies the commits (full shas, in the given order) with `git cherry-pick`.
|
|
@@ -70,18 +68,12 @@ module Gitflash
|
|
|
70
68
|
|
|
71
69
|
private
|
|
72
70
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
deletions: deletions)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def stat(text)
|
|
83
|
-
[text[/(\d+) files? changed/, 1], text[/(\d+) insertions?\(\+\)/, 1],
|
|
84
|
-
text[/(\d+) deletions?\(-\)/, 1]].map(&:to_i)
|
|
71
|
+
# The commits of the source that are not merges and not on HEAD, listed by git without any
|
|
72
|
+
# text a commit author controls. A commit message holding the separators of the log format
|
|
73
|
+
# could forge extra records; only SHAs found here become candidates.
|
|
74
|
+
def eligible_shas(source_ref)
|
|
75
|
+
@bash.exec('git', 'rev-list', '--right-only', '--no-merges', "HEAD...#{source_ref}", '--')
|
|
76
|
+
.lines(chomp: true).to_set
|
|
85
77
|
end
|
|
86
78
|
|
|
87
79
|
def conflicted_files
|
data/lib/gitflash/cli.rb
CHANGED
|
@@ -9,6 +9,18 @@ module Gitflash
|
|
|
9
9
|
|
|
10
10
|
SCHEMA_PATH = File.expand_path('../../schema/v1.json', __dir__)
|
|
11
11
|
|
|
12
|
+
# Thor reports a bad option or argument as text with exit 1. It is a usage error, so it
|
|
13
|
+
# exits with 2, and with --json it is printed in the JSON envelope like every other error.
|
|
14
|
+
def self.start(given_args = ARGV, config = {})
|
|
15
|
+
config[:shell] ||= Thor::Base.shell.new
|
|
16
|
+
dispatch(nil, given_args.dup, nil, config)
|
|
17
|
+
rescue Thor::Error => e
|
|
18
|
+
UsageReport.new(given_args).print(e)
|
|
19
|
+
exit(2)
|
|
20
|
+
rescue Errno::EPIPE
|
|
21
|
+
nil
|
|
22
|
+
end
|
|
23
|
+
|
|
12
24
|
def self.exit_on_failure?
|
|
13
25
|
true
|
|
14
26
|
end
|
|
@@ -16,7 +28,10 @@ module Gitflash
|
|
|
16
28
|
class_option :json, type: :boolean, default: false,
|
|
17
29
|
desc: 'Print JSON instead of text; never prompts'
|
|
18
30
|
class_option :yes, type: :boolean, aliases: '-y', default: false,
|
|
19
|
-
desc: 'Skip confirmation prompts'
|
|
31
|
+
desc: 'Skip confirmation prompts for changes gitflash undo can revert'
|
|
32
|
+
class_option :confirm, type: :string, banner: 'TOKEN',
|
|
33
|
+
desc: 'Confirm a change gitflash undo cannot revert, with the token ' \
|
|
34
|
+
'from its plan'
|
|
20
35
|
class_option :dry_run, type: :boolean, default: false,
|
|
21
36
|
desc: 'Show what would happen without changing anything'
|
|
22
37
|
|
|
@@ -45,7 +60,8 @@ module Gitflash
|
|
|
45
60
|
|
|
46
61
|
desc 'mark [BRANCH...]', descriptions.mark.short
|
|
47
62
|
long_desc descriptions.mark.long
|
|
48
|
-
option :owner, type: :string, enum: Ownership::OWNERS,
|
|
63
|
+
option :owner, type: :string, enum: Ownership::OWNERS,
|
|
64
|
+
desc: 'agent or human (default: human in a terminal, agent otherwise)'
|
|
49
65
|
option :clear, type: :boolean, default: false, desc: 'Remove the mark'
|
|
50
66
|
def mark(*branches)
|
|
51
67
|
run_command('mark', Commands::Mark, *branches)
|
|
@@ -93,13 +109,20 @@ module Gitflash
|
|
|
93
109
|
end
|
|
94
110
|
|
|
95
111
|
desc 'gc', 'Delete old snapshots'
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
long_desc <<~TEXT
|
|
113
|
+
Deletes snapshots older than --older-than DAYS (default 30). gitflash undo cannot bring a
|
|
114
|
+
deleted snapshot back, so --yes is not enough: without a terminal gc stops and names a
|
|
115
|
+
token (with --json also the plan and error.confirm_token); re-run with --confirm TOKEN to
|
|
116
|
+
delete exactly those snapshots. In a terminal you type the token. Pass --dry-run to see the
|
|
117
|
+
snapshots it would delete.
|
|
118
|
+
TEXT
|
|
119
|
+
option :older_than, type: :string, banner: 'DAYS',
|
|
120
|
+
desc: 'Delete snapshots older than DAYS days, at least 1 (default 30)'
|
|
98
121
|
def gc
|
|
99
122
|
run_command('gc', Commands::Gc)
|
|
100
123
|
end
|
|
101
124
|
|
|
102
|
-
desc 'hook SUBCOMMAND', '
|
|
125
|
+
desc 'hook SUBCOMMAND', 'Claude Code hooks: install them and check they are active'
|
|
103
126
|
subcommand 'hook', HookCli
|
|
104
127
|
|
|
105
128
|
desc 'pick [SOURCE] [SHA...]', descriptions.pick.short
|
|
@@ -14,10 +14,21 @@ module Gitflash
|
|
|
14
14
|
rescue Error => e
|
|
15
15
|
ui.error(e)
|
|
16
16
|
exit(e.exit_code)
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
report_unexpected(ui, name, e)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Anything else still gets the envelope, so a script reading --json is never left with a
|
|
22
|
+
# stack trace
|
|
23
|
+
def report_unexpected(ui, name, error)
|
|
24
|
+
unexpected = Error.new("#{name} failed unexpectedly: #{error.message} (#{error.class})")
|
|
25
|
+
ui.error(unexpected)
|
|
26
|
+
exit(unexpected.exit_code)
|
|
17
27
|
end
|
|
18
28
|
|
|
19
29
|
def build_ui(name)
|
|
20
|
-
Ui.new(command: name, json: options[:json], yes: options[:yes], dry_run: options[:dry_run]
|
|
30
|
+
Ui.new(command: name, json: options[:json], yes: options[:yes], dry_run: options[:dry_run],
|
|
31
|
+
confirm: options[:confirm])
|
|
21
32
|
end
|
|
22
33
|
end
|
|
23
34
|
end
|
|
@@ -14,6 +14,9 @@ module Gitflash
|
|
|
14
14
|
|
|
15
15
|
attr_reader :repo, :ui, :options
|
|
16
16
|
|
|
17
|
+
# A path from the repository, kept on one line of the output
|
|
18
|
+
def shown(path) = TerminalText.line(path)
|
|
19
|
+
|
|
17
20
|
def require_interactive!(message)
|
|
18
21
|
usage_error!('input_required', message) unless ui.interactive?
|
|
19
22
|
end
|
|
@@ -29,7 +29,34 @@ module Gitflash
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def build_plan(names)
|
|
32
|
-
super.merge(reasons: @reasons.slice(*names), skipped: @skipped)
|
|
32
|
+
plan = super.merge(reasons: @reasons.slice(*names), skipped: @skipped)
|
|
33
|
+
warning ? plan.merge(warning: warning) : plan
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def summary(names)
|
|
37
|
+
warning ? "#{super}\n\nNote: #{warning}" : super
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Without origin/HEAD, "merged" means merged into a local main or master, which may not be
|
|
41
|
+
# the branch the team ships from
|
|
42
|
+
def warning
|
|
43
|
+
return @warning if defined?(@warning)
|
|
44
|
+
|
|
45
|
+
@warning = if default_branch_guessed?
|
|
46
|
+
'origin/HEAD is not set, so gitflash guessed the default branch ' \
|
|
47
|
+
"(#{repo.default_branch || 'none'}). Run `git remote set-head origin " \
|
|
48
|
+
'--auto` so merged branches are judged against the right one.'
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# An origin remote but no origin/HEAD: the default branch came from a local main or master
|
|
53
|
+
def default_branch_guessed?
|
|
54
|
+
_out, _err, set = Git::BashCommand.capture('git', 'symbolic-ref', '--quiet',
|
|
55
|
+
"#{Repo::REMOTE_HEAD}HEAD")
|
|
56
|
+
return false if set
|
|
57
|
+
|
|
58
|
+
remotes, _err, = Git::BashCommand.capture('git', 'remote')
|
|
59
|
+
remotes.split.include?('origin')
|
|
33
60
|
end
|
|
34
61
|
|
|
35
62
|
def skipped_row(candidate)
|
|
@@ -38,6 +65,7 @@ module Gitflash
|
|
|
38
65
|
|
|
39
66
|
def nothing_to_clean
|
|
40
67
|
plan = { branches: [], force: force?, reasons: {}, skipped: @skipped }
|
|
68
|
+
plan[:warning] = warning if warning
|
|
41
69
|
ui.report(status: 'noop', plan: plan, result: { deleted: [], failed: [] },
|
|
42
70
|
text: @skipped.empty? ? 'Nothing to clean' : skipped_text)
|
|
43
71
|
end
|
data/lib/gitflash/commands/gc.rb
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Gitflash
|
|
4
4
|
module Commands
|
|
5
|
-
# Deletes snapshots older than --older-than DAYS (default 30). Git frees their
|
|
6
|
-
# its next garbage collection.
|
|
5
|
+
# Deletes snapshots older than --older-than DAYS (default 30, at least 1). Git frees their
|
|
6
|
+
# objects at its next garbage collection. A whole number of days is required: 0 or a
|
|
7
|
+
# negative number would select every snapshot, including the one just taken.
|
|
7
8
|
class Gc < Base
|
|
8
9
|
DEFAULT_DAYS = 30
|
|
9
10
|
|
|
10
11
|
def call
|
|
11
|
-
days =
|
|
12
|
+
days = older_than_days
|
|
12
13
|
old = older_than(days)
|
|
13
14
|
plan = { older_than_days: days, snapshots: old.map(&:id) }
|
|
14
15
|
return nothing(plan) if old.empty?
|
|
@@ -18,9 +19,23 @@ module Gitflash
|
|
|
18
19
|
|
|
19
20
|
private
|
|
20
21
|
|
|
22
|
+
def older_than_days
|
|
23
|
+
value = options[:older_than]
|
|
24
|
+
return DEFAULT_DAYS if value.nil?
|
|
25
|
+
|
|
26
|
+
days = Integer(value.to_s, 10, exception: false)
|
|
27
|
+
return days if days&.positive?
|
|
28
|
+
|
|
29
|
+
usage_error!('invalid_options',
|
|
30
|
+
"--older-than needs a whole number of days, 1 or more, got '#{value}'")
|
|
31
|
+
end
|
|
32
|
+
|
|
21
33
|
def delete(plan, text)
|
|
22
34
|
return planned(plan, "Would delete #{text}") if ui.dry_run?
|
|
23
|
-
|
|
35
|
+
|
|
36
|
+
# Deleted snapshots are gone for good: no snapshot of the snapshots is kept
|
|
37
|
+
summary = "You are about to delete #{text}"
|
|
38
|
+
return cancelled(plan) unless ui.confirm?(summary, plan: plan, irreversible: true)
|
|
24
39
|
|
|
25
40
|
plan[:snapshots].each { |id| snapshots.delete(id) }
|
|
26
41
|
ui.report(status: 'done', plan: plan, result: { deleted: plan[:snapshots] },
|