gitflash 0.7.0.beta2 → 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 +37 -0
- data/README.md +15 -11
- data/command_descriptions.yml +21 -7
- data/lib/gitflash/atomic_file.rb +40 -0
- data/lib/gitflash/cli.rb +27 -4
- data/lib/gitflash/command_runner.rb +12 -1
- data/lib/gitflash/commands/clean.rb +29 -1
- data/lib/gitflash/commands/gc.rb +19 -4
- data/lib/gitflash/commands/hook_install.rb +66 -12
- data/lib/gitflash/commands/mark.rb +9 -3
- data/lib/gitflash/commands/snapshot_create.rb +6 -4
- data/lib/gitflash/commands/undo.rb +18 -8
- data/lib/gitflash/commands/worktree_command.rb +17 -3
- data/lib/gitflash/commands/worktree_remove.rb +14 -5
- data/lib/gitflash/config.rb +63 -13
- data/lib/gitflash/config_keys.rb +52 -4
- data/lib/gitflash/confirmation_required.rb +6 -2
- data/lib/gitflash/hook/claude.rb +17 -12
- data/lib/gitflash/hook/command_parser.rb +91 -68
- 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 +28 -2
- data/lib/gitflash/hook/paths.rb +47 -0
- data/lib/gitflash/hook/rules.rb +31 -9
- 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 +27 -4
- data/lib/gitflash/protection.rb +12 -4
- 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 +18 -21
- data/lib/gitflash/ui.rb +31 -3
- data/lib/gitflash/untracked_files.rb +19 -0
- data/lib/gitflash/usage_report.rb +66 -0
- data/lib/gitflash/version.rb +1 -1
- data/schema/v1.json +39 -2
- metadata +14 -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
|
@@ -109,3 +109,40 @@ Second beta of the parallel agent work release: the `pick` branch list, a quiete
|
|
|
109
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
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
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
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, the latest
|
|
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; both files add up
|
|
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
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
|
|
|
@@ -142,8 +153,11 @@ mark:
|
|
|
142
153
|
created. The mark is the git config key `branch.<name>.gitflash-owner` and disappears with
|
|
143
154
|
the branch.
|
|
144
155
|
|
|
145
|
-
Without BRANCH the current branch is marked.
|
|
146
|
-
|
|
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.
|
|
147
161
|
|
|
148
162
|
`gitflash wt add` marks the branches it creates. Use `gitflash mark` for branches made with
|
|
149
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
|
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,8 +109,15 @@ 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
|
|
@@ -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
|
|
@@ -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] },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
+
require 'shellwords'
|
|
4
5
|
|
|
5
6
|
module Gitflash
|
|
6
7
|
module Commands
|
|
@@ -11,10 +12,14 @@ module Gitflash
|
|
|
11
12
|
SCOPES = Hook::Settings::SCOPES
|
|
12
13
|
MARKING_COMMAND = 'gitflash hook claude'
|
|
13
14
|
|
|
15
|
+
# From the weakest protection to the strongest
|
|
16
|
+
STRENGTH = %w[snapshot ask deny].freeze
|
|
17
|
+
|
|
14
18
|
def call
|
|
15
19
|
plan = build_plan
|
|
16
20
|
data = settings.read(plan[:file])
|
|
17
21
|
entry = settings.entry(data)
|
|
22
|
+
add_mode(plan, entry)
|
|
18
23
|
marking = settings.entry(data, Hook::Claude::POST_EVENT)
|
|
19
24
|
return already_installed(plan) if up_to_date?(entry, marking, plan)
|
|
20
25
|
return planned(plan, "Would add the gitflash hook to #{plan[:file]}") if ui.dry_run?
|
|
@@ -32,8 +37,46 @@ module Gitflash
|
|
|
32
37
|
scope = choice(:scope, SCOPES, 'local')
|
|
33
38
|
file = settings.path(scope)
|
|
34
39
|
refuse_symlink!(file) unless scope == 'user'
|
|
35
|
-
{ scope: scope, file: file,
|
|
36
|
-
|
|
40
|
+
{ scope: scope, file: file, marking_command: MARKING_COMMAND,
|
|
41
|
+
applies_to: settings.applies_to(scope) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Without --mode the installed mode is kept, so running install again to add the
|
|
45
|
+
# PostToolUse hook never turns ask or deny off. A weaker --mode needs --downgrade.
|
|
46
|
+
def add_mode(plan, entry)
|
|
47
|
+
previous = entry && installed_mode(entry['command'])
|
|
48
|
+
mode = options[:mode] ? choice(:mode, Hook::Claude::MODES, 'snapshot') : previous
|
|
49
|
+
mode ||= 'snapshot'
|
|
50
|
+
refuse_downgrade!(previous, mode) if previous
|
|
51
|
+
plan[:mode] = mode
|
|
52
|
+
plan[:previous_mode] = previous if previous && previous != mode
|
|
53
|
+
plan[:command] = hook_command(mode)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# The mode the installed command runs in, read the way a shell splits it: `snapshot` when
|
|
57
|
+
# it has no --mode; a --mode gitflash does not know is refused, never read as snapshot,
|
|
58
|
+
# so a reinstall cannot quietly weaken it
|
|
59
|
+
def installed_mode(command)
|
|
60
|
+
words = Shellwords.split(command.to_s)
|
|
61
|
+
index = words.index { |word| word == '--mode' || word.start_with?('--mode=') }
|
|
62
|
+
return 'snapshot' unless index
|
|
63
|
+
|
|
64
|
+
mode = words[index] == '--mode' ? words[index + 1] : words[index].delete_prefix('--mode=')
|
|
65
|
+
return mode if Hook::Claude::MODES.include?(mode)
|
|
66
|
+
|
|
67
|
+
raise Error.new("The installed hook command `#{command}` has a mode gitflash cannot " \
|
|
68
|
+
'read; fix or remove it, then install again', code: 'invalid_settings')
|
|
69
|
+
rescue ArgumentError
|
|
70
|
+
raise Error.new("The installed hook command `#{command}` cannot be parsed",
|
|
71
|
+
code: 'invalid_settings')
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def refuse_downgrade!(previous, mode)
|
|
75
|
+
return if options[:downgrade] || STRENGTH.index(mode).to_i >= STRENGTH.index(previous).to_i
|
|
76
|
+
|
|
77
|
+
usage_error!('invalid_options',
|
|
78
|
+
"The installed hook runs in #{previous} mode; --mode #{mode} would protect " \
|
|
79
|
+
'less. Pass --downgrade to change it anyway.')
|
|
37
80
|
end
|
|
38
81
|
|
|
39
82
|
# The repository decides what .claude holds, and a link there could point at the user's own
|
|
@@ -54,10 +97,9 @@ module Gitflash
|
|
|
54
97
|
def install(data, entry, marking, plan)
|
|
55
98
|
entry ? entry['command'] = plan[:command] : add_entry(data, 'PreToolUse', plan[:command])
|
|
56
99
|
add_entry(data, Hook::Claude::POST_EVENT, MARKING_COMMAND) unless marking
|
|
57
|
-
|
|
58
|
-
File.write(plan[:file], "#{JSON.pretty_generate(data)}\n")
|
|
100
|
+
write_atomically(plan[:file], "#{JSON.pretty_generate(data)}\n")
|
|
59
101
|
ui.report(status: 'done', plan: plan, result: plan,
|
|
60
|
-
text: "Installed the gitflash hook in #{plan[:file]}.\n" \
|
|
102
|
+
text: "Installed the gitflash hook in #{plan[:file]} (#{mode_text(plan)}).\n" \
|
|
61
103
|
"It applies to #{plan[:applies_to]}, including running ones.")
|
|
62
104
|
end
|
|
63
105
|
|
|
@@ -68,22 +110,34 @@ module Gitflash
|
|
|
68
110
|
usage_error!('invalid_options', "Unknown #{name} '#{value}' (use #{allowed.join(', ')})")
|
|
69
111
|
end
|
|
70
112
|
|
|
71
|
-
def hook_command
|
|
72
|
-
mode = choice(:mode, Hook::Claude::MODES, 'snapshot')
|
|
113
|
+
def hook_command(mode)
|
|
73
114
|
mode == 'snapshot' ? 'gitflash hook claude' : "gitflash hook claude --mode #{mode}"
|
|
74
115
|
end
|
|
75
116
|
|
|
117
|
+
def mode_text(plan)
|
|
118
|
+
changed = plan[:previous_mode] ? ", was #{plan[:previous_mode]}" : ''
|
|
119
|
+
"#{plan[:mode]} mode#{changed}"
|
|
120
|
+
end
|
|
121
|
+
|
|
76
122
|
def already_installed(plan)
|
|
77
123
|
ui.report(status: 'noop', plan: plan, result: plan,
|
|
78
124
|
text: "Already installed in #{plan[:file]}.\nIt applies to #{plan[:applies_to]}.")
|
|
79
125
|
end
|
|
80
126
|
|
|
127
|
+
# Claude Code ignores a settings file it cannot parse, and with it the user's own
|
|
128
|
+
# permission rules, so the file is replaced in one step (AtomicFile)
|
|
129
|
+
def write_atomically(path, text)
|
|
130
|
+
AtomicFile.write(path, text)
|
|
131
|
+
rescue SystemCallError, IOError => e
|
|
132
|
+
raise Error.new("Could not write #{path}: #{e.message}", code: 'invalid_settings')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Settings#read has checked that `hooks` and each of its events have the shape Claude Code
|
|
136
|
+
# reads, so a missing event is the only one to create
|
|
81
137
|
def add_entry(data, event, command)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
'hooks' => [{ 'type' => 'command', 'command' => command, 'timeout' => 30 }]
|
|
86
|
-
}
|
|
138
|
+
groups = ((data['hooks'] ||= {})[event] ||= [])
|
|
139
|
+
hook = { 'type' => 'command', 'command' => command, 'timeout' => 30 }
|
|
140
|
+
groups << { 'matcher' => 'Bash', 'hooks' => [hook] }
|
|
87
141
|
end
|
|
88
142
|
end
|
|
89
143
|
end
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Gitflash
|
|
4
4
|
module Commands
|
|
5
|
-
# Marks branches as agent or human work (`--owner
|
|
6
|
-
#
|
|
5
|
+
# Marks branches as agent or human work (`--owner`) or removes the mark (`--clear`), so
|
|
6
|
+
# `gitflash clean --agent` knows which branches an agent created. Without --owner a person
|
|
7
|
+
# in a terminal marks the branch as theirs (human) and an agent or script as agent work:
|
|
8
|
+
# `agent` is the side `clean --agent` deletes, so a person never claims it by accident.
|
|
7
9
|
# Without branch names the current branch is marked. Needs no confirmation: only a git
|
|
8
10
|
# config key changes, and `--clear` or another `mark` reverses it.
|
|
9
11
|
class Mark < Base
|
|
@@ -32,12 +34,16 @@ module Gitflash
|
|
|
32
34
|
usage_error!('invalid_options', 'Use either --owner or --clear, not both') if given
|
|
33
35
|
return nil
|
|
34
36
|
end
|
|
35
|
-
return
|
|
37
|
+
return default_owner if given.nil?
|
|
36
38
|
return given if Ownership::OWNERS.include?(given)
|
|
37
39
|
|
|
38
40
|
usage_error!('invalid_options', "Unknown owner '#{given}' (use agent or human)")
|
|
39
41
|
end
|
|
40
42
|
|
|
43
|
+
def default_owner
|
|
44
|
+
$stdin.tty? ? 'human' : 'agent'
|
|
45
|
+
end
|
|
46
|
+
|
|
41
47
|
def verb
|
|
42
48
|
options[:clear] ? 'clear the mark of' : "mark as #{owner}"
|
|
43
49
|
end
|
|
@@ -20,10 +20,12 @@ module Gitflash
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def text(snapshot)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
23
|
+
lines = ["Saved snapshot #{snapshot.id}"]
|
|
24
|
+
lines << "Skipped large untracked files: #{snapshot.skipped_files.join(', ')}" if
|
|
25
|
+
snapshot.skipped_files.any?
|
|
26
|
+
lines << "Left out by snapshot_exclude: #{snapshot.excluded_files.join(', ')}" if
|
|
27
|
+
snapshot.excluded_files.any?
|
|
28
|
+
lines.join("\n")
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|