gitflash 0.5.1 → 0.7.0.beta1
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 +25 -0
- data/README.md +69 -2
- data/bin/gitflash +10 -0
- data/command_descriptions.yml +99 -0
- data/lib/gitflash/branch.rb +14 -2
- data/lib/gitflash/cherry_pick.rb +128 -0
- data/lib/gitflash/cleanup.rb +70 -0
- data/lib/gitflash/cli.rb +72 -21
- data/lib/gitflash/command_runner.rb +23 -0
- data/lib/gitflash/commands/base.rb +15 -0
- data/lib/gitflash/commands/branches.rb +1 -0
- data/lib/gitflash/commands/checkout.rb +2 -1
- data/lib/gitflash/commands/clean.rb +51 -0
- data/lib/gitflash/commands/clean_criteria.rb +29 -0
- data/lib/gitflash/commands/delete.rb +23 -8
- data/lib/gitflash/commands/gc.rb +41 -0
- data/lib/gitflash/commands/hook_install.rb +77 -0
- data/lib/gitflash/commands/hook_status.rb +80 -0
- data/lib/gitflash/commands/mark.rb +85 -0
- data/lib/gitflash/commands/pick.rb +131 -0
- data/lib/gitflash/commands/pick_control.rb +67 -0
- data/lib/gitflash/commands/pick_outcome.rb +52 -0
- data/lib/gitflash/commands/removal_report.rb +65 -0
- data/lib/gitflash/commands/reset.rb +7 -1
- data/lib/gitflash/commands/snapshot_create.rb +30 -0
- data/lib/gitflash/commands/snapshot_list.rb +20 -0
- data/lib/gitflash/commands/undo.rb +131 -0
- data/lib/gitflash/commands/worktree_add.rb +98 -0
- data/lib/gitflash/commands/worktree_clean.rb +49 -0
- data/lib/gitflash/commands/worktree_command.rb +28 -0
- data/lib/gitflash/commands/worktree_list.rb +48 -0
- data/lib/gitflash/commands/worktree_lock.rb +54 -0
- data/lib/gitflash/commands/worktree_move.rb +47 -0
- data/lib/gitflash/commands/worktree_prune.rb +41 -0
- data/lib/gitflash/commands/worktree_remove.rb +119 -0
- data/lib/gitflash/commands/worktree_unlock.rb +12 -0
- data/lib/gitflash/config.rb +95 -0
- data/lib/gitflash/config_keys.rb +19 -0
- data/lib/gitflash/git/bash_command.rb +5 -4
- data/lib/gitflash/git/in_directory.rb +29 -0
- data/lib/gitflash/hook/branch_marker.rb +52 -0
- data/lib/gitflash/hook/claude.rb +126 -0
- data/lib/gitflash/hook/command_parser.rb +121 -0
- data/lib/gitflash/hook/creations.rb +85 -0
- data/lib/gitflash/hook/rules.rb +108 -0
- data/lib/gitflash/hook/settings.rb +73 -0
- data/lib/gitflash/hook_cli.rb +77 -0
- data/lib/gitflash/ownership.rb +59 -0
- data/lib/gitflash/protection.rb +19 -0
- data/lib/gitflash/repo.rb +12 -3
- data/lib/gitflash/restore.rb +141 -0
- data/lib/gitflash/snapshot.rb +47 -0
- data/lib/gitflash/snapshots.rb +70 -0
- data/lib/gitflash/state_capture.rb +129 -0
- data/lib/gitflash/ui.rb +7 -5
- data/lib/gitflash/version.rb +1 -1
- data/lib/gitflash/worktree.rb +32 -0
- data/lib/gitflash/worktree_cli.rb +115 -0
- data/lib/gitflash/worktree_revival.rb +55 -0
- data/lib/gitflash/worktrees.rb +147 -0
- data/schema/v1.json +1885 -102
- metadata +46 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e8466fec203a9fdd7ac08e721fb0bb624fb8f486e68f9b369660e5d369e717ad
|
|
4
|
+
data.tar.gz: 53243a2e0dc1d5877bd5541999fec636e89ac882b9af78d84790ef32be3e12cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27b78d4baac19b15a9f6bef731cc0ec3622ee5c51a2ee82ab9f20a63a5f50d6ecedb3c4f1651d0849eae970f5a66c20480272d0bf00b0284e8434f1893805901
|
|
7
|
+
data.tar.gz: 1c1f6e76de4e2f67fb7c1c591e9569d8578a96cad8cc9e58ff75fd9448abaa8ead4e7b96cef519d6a4a991e75299e07eaafcc8d060896b8742b9c7f4ec4b93f5
|
data/CHANGELOG.md
CHANGED
|
@@ -62,3 +62,28 @@ Security fixes for names and text that come from the repository:
|
|
|
62
62
|
- Branch and default-branch detection use full ref names. A tag named like `origin/<default>` could change the short name of `origin/HEAD`, so the default branch lost its deletion protection and its merge status; `origin/HEAD` must now point inside `refs/remotes/origin/`
|
|
63
63
|
- `delete` passes `--` before the branch name; `resolve_commit` refuses references that start with a dash
|
|
64
64
|
- Text output shows terminal control characters of repository-controlled text (commit subjects, git errors) as escapes such as `\x1B`, so a crafted commit subject cannot move the cursor, rewrite output or set the clipboard (OSC 52). JSON output keeps the original text
|
|
65
|
+
|
|
66
|
+
## [0.6.0] - 2026-10-05
|
|
67
|
+
|
|
68
|
+
- Snapshots: gitflash saves branches, HEAD, tracked, staged and untracked files and stash entries before every change it makes, as git objects under `refs/gitflash/snapshots/` (the working tree is never touched; untracked files over 50 MB are skipped)
|
|
69
|
+
- Add `undo [SNAPSHOT]`: restores the parts a snapshot saved (the latest by default), after saving the current state so the undo can be undone
|
|
70
|
+
- Add `snapshot` (save on request, `--scope`, `--message`), `snapshots` (list) and `gc --older-than DAYS`
|
|
71
|
+
- Every `done` result of `checkout`, `delete` and `reset` includes `undo` with the snapshot id; human output ends with `Undo with: gitflash undo ID`
|
|
72
|
+
- Add `hook claude`: a Claude Code `PreToolUse` hook that saves a snapshot before destructive git commands an agent runs itself (`reset`, `checkout -- .`, `restore`, `clean -f`, `branch -D`, `stash drop`, `rebase`, `worktree remove --force`, ...). Modes: `snapshot` (default), `ask`, `deny`. It never blocks a command because of its own errors
|
|
73
|
+
- Add `hook install [--scope local|project|user] [--mode ...]` to register the hook in Claude Code settings; it reports which sessions the file applies to, and the local scope uses the main checkout's root inside a worktree, as Claude Code does
|
|
74
|
+
- Add `hook status` to show whether the hook protects Claude Code sessions in the current directory
|
|
75
|
+
- JSON schema: new `undo` field, commands `undo`, `snapshots`, `snapshot`, `gc`, `hook`, error codes `unknown_snapshot`, `wrong_worktree`, `invalid_settings`
|
|
76
|
+
|
|
77
|
+
## [0.7.0.beta1] - 2026-10-07
|
|
78
|
+
|
|
79
|
+
Beta of the parallel agent work release. Install with `gem install gitflash --pre`. The JSON contract of the new commands may still change before 0.7.0. Needs git 2.36 or newer.
|
|
80
|
+
|
|
81
|
+
- Settings: `.gitflash.yml` in the repository and `~/.config/gitflash.yml` (the repository file wins) with `protected` branch patterns, `stale_days` and `worktree_dir`. Unknown keys, wrong types, duplicate keys and unreadable files fail with the new error code `invalid_config`
|
|
82
|
+
- Ownership marks: `branch.<name>.gitflash-owner` (`agent` or `human`) in git config, shown as `owner` in `branches` and `worktree list`. Add `mark [BRANCH...] [--owner agent|human] [--clear]` to set it
|
|
83
|
+
- Add `worktree` (alias `wt`): `list`, `add`, `remove`, `clean`, `prune`, `lock`, `unlock` and `move`. `wt list --json` gives absolute paths, dirty state, ahead and behind, merge status, lock state and owner. `wt add` marks the branches it creates as agent work. `wt remove` and `wt clean` save a snapshot first and refuse the main checkout, the current worktree, and locked or dirty worktrees unless `--force`. `undo` adds a removed worktree back with its branch and uncommitted files. Ignored files are not saved: the plan lists them before they are deleted. A worktree with an untracked file over 50 MB is not removed
|
|
84
|
+
- Add `clean`: deletes merged branches and branches whose upstream is gone, and with `--stale [DAYS]` and `--agent` stale and agent-created ones, with a snapshot first. Each chosen branch lists why. Unmerged branches need `--force`
|
|
85
|
+
- `clean` and `delete` never touch the current and default branch, `main`, `master`, `protected` branches or branches checked out in a worktree
|
|
86
|
+
- Add `pick SOURCE [SHA...]`: `--list` shows the commits of SOURCE that are not on the current branch (already applied patches are marked), `SHA...` applies them oldest first with `git cherry-pick -x` after a snapshot, and on a conflict it stops and lists the files. `pick --continue`, `--skip` and `--abort` wrap git's own controls
|
|
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
|
+
- 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
|
+
- Author names from other branches are shown with control characters escaped in `pick` output
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitFlash
|
|
2
2
|
|
|
3
|
-
[](https://badge.fury.io/gh/Kleanthismits%2Fgit_flash)
|
|
4
4
|
|
|
5
5
|
⚠️ **This project is still under development**: Use at your own risk!
|
|
6
6
|
|
|
@@ -24,9 +24,11 @@ Or install it yourself as:
|
|
|
24
24
|
|
|
25
25
|
$ gem install gitflash
|
|
26
26
|
|
|
27
|
+
Prerelease versions such as `0.7.0.beta1` are only installed when you ask for them: `gem install gitflash --pre`, or `gem 'gitflash', '0.7.0.beta1'` in a Gemfile.
|
|
28
|
+
|
|
27
29
|
## Requirements
|
|
28
30
|
|
|
29
|
-
This gem requires Ruby 3.3+ and git.
|
|
31
|
+
This gem requires Ruby 3.3+ and git 2.36+ (the worktree commands read `git worktree list -z`).
|
|
30
32
|
|
|
31
33
|
## Usage
|
|
32
34
|
|
|
@@ -38,9 +40,73 @@ Run `gitflash` inside a git repository to get a list with the available commands
|
|
|
38
40
|
| `gitflash checkout [BRANCH]` | Check out a branch, or pick one from a list |
|
|
39
41
|
| `gitflash delete [BRANCH...]` | Delete branches, or pick them from a list. The current, default, `main` and `master` branches are protected. Unmerged branches are kept unless you pass `--force` |
|
|
40
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) or human work, 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`, `protected` in `.gitflash.yml`, branches checked out in a worktree |
|
|
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 by default: deleted branches, moved branches, HEAD, uncommitted and untracked files, dropped stashes |
|
|
48
|
+
| `gitflash snapshot` | Save the current state on request (`--scope`, `--message`) |
|
|
49
|
+
| `gitflash snapshots` | List snapshots, newest first |
|
|
50
|
+
| `gitflash gc` | Delete snapshots older than 30 days (`--older-than DAYS`) |
|
|
51
|
+
| `gitflash hook install` | Protect plain git commands run by Claude Code and mark the branches it creates (see below) |
|
|
52
|
+
| `gitflash hook status` | Show whether the hook protects Claude Code sessions in the current directory |
|
|
41
53
|
| `gitflash schema` | Print the JSON Schema of the `--json` output |
|
|
42
54
|
| `gitflash version` | Print the installed version (also `--version`, `-v`) |
|
|
43
55
|
|
|
56
|
+
### Parallel agent work
|
|
57
|
+
|
|
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
|
+
|
|
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. A worktree with an untracked file over 50 MB is not removed.
|
|
61
|
+
|
|
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
|
+
|
|
64
|
+
Settings live in `.gitflash.yml` in the repository and `~/.config/gitflash.yml` (the repository file wins):
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
protected: ["release/*"] # branch patterns clean and delete never touch
|
|
68
|
+
stale_days: 30 # used by --stale without a number
|
|
69
|
+
worktree_dir: "../%<repo>s.worktrees/%<branch>s"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Undo
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
gitflash delete old-feature --yes
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gitflash undo --yes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`gitflash undo` restores only what the snapshot saved and what differs, and saves the current state first, so an undo can itself be undone.
|
|
85
|
+
|
|
86
|
+
### Protect plain git commands run by AI agents
|
|
87
|
+
|
|
88
|
+
Agents often run git directly. Claude Code's own checkpoints do not cover changes made by shell commands, so a `git reset --hard` or `git clean -fd` run by an agent cannot be rewound there. Install the gitflash hook once per repository:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
gitflash hook install
|
|
92
|
+
```
|
|
93
|
+
|
|
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.
|
|
95
|
+
|
|
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.
|
|
97
|
+
|
|
98
|
+
Where the hook goes, following Claude Code's own rules:
|
|
99
|
+
|
|
100
|
+
| Scope | File | Applies to |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `local` (default) | `.claude/settings.local.json` at the repository root (the main checkout's root inside a worktree) | Sessions anywhere in the repository and its worktrees |
|
|
103
|
+
| `project` | `.claude/settings.json` at the repository top level, to share with your team | Sessions started in that directory |
|
|
104
|
+
| `user` | `~/.claude/settings.json` | Every session |
|
|
105
|
+
|
|
106
|
+
Claude Code reloads settings files, so running sessions pick the hook up without a restart. Run `gitflash hook status` in the directory where you start Claude Code to check which files apply and whether the `gitflash` executable is found.
|
|
107
|
+
|
|
108
|
+
### Scripts
|
|
109
|
+
|
|
44
110
|
Without an argument, a command shows an interactive list. With arguments it runs directly, which also works in scripts.
|
|
45
111
|
|
|
46
112
|
Options for every command:
|
|
@@ -76,6 +142,7 @@ With `--json`, every command prints one object in the same envelope, defined in
|
|
|
76
142
|
|
|
77
143
|
- `status`: `done`, `planned` (`--dry-run`), `noop`, `cancelled`, `failed`, `confirmation_required` or `error`. `ok` is true for the first four.
|
|
78
144
|
- `result` includes what you need to revert: the SHA of each deleted branch (`git branch NAME SHA`), the previous branch after `checkout`, the previous commit after `reset`.
|
|
145
|
+
- `undo` names the snapshot saved before the change and the command that restores it (`gitflash undo ID`).
|
|
79
146
|
- `error.code` is a stable identifier such as `unknown_branch`, `protected_branch` or `confirmation_required`.
|
|
80
147
|
|
|
81
148
|
```bash
|
data/bin/gitflash
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
# Fast path for the agent hook, which runs before every shell command: when the command does
|
|
5
|
+
# not mention git there is nothing to do, so exit before loading the rest of gitflash.
|
|
6
|
+
if ARGV.first(2) == %w[hook claude]
|
|
7
|
+
input = $stdin.read
|
|
8
|
+
exit 0 unless input.include?('git')
|
|
9
|
+
|
|
10
|
+
require 'stringio'
|
|
11
|
+
$stdin = StringIO.new(input)
|
|
12
|
+
end
|
|
13
|
+
|
|
4
14
|
require 'gitflash'
|
|
5
15
|
require 'gitflash/cli'
|
|
6
16
|
|
data/command_descriptions.yml
CHANGED
|
@@ -46,3 +46,102 @@ reset:
|
|
|
46
46
|
By default performs a mixed reset: your changes are kept as unstaged changes.
|
|
47
47
|
Pass --soft to keep them staged, or --hard to discard them. A hard reset asks for
|
|
48
48
|
confirmation; pass --yes to skip it, which is required without a terminal.
|
|
49
|
+
|
|
50
|
+
undo:
|
|
51
|
+
short: 'Restore a snapshot (the latest by default)'
|
|
52
|
+
long: |
|
|
53
|
+
Restores the parts of the repository a snapshot saved: deleted or moved branches,
|
|
54
|
+
HEAD, tracked, staged and untracked files, and dropped stash entries. Only parts that
|
|
55
|
+
differ are changed; branches and files created after the snapshot are left alone.
|
|
56
|
+
|
|
57
|
+
gitflash saves a snapshot before every change it makes, and the agent hook saves one
|
|
58
|
+
before destructive git commands. List them with `gitflash snapshots`.
|
|
59
|
+
|
|
60
|
+
The current state is saved as a new snapshot first, so an undo can itself be undone.
|
|
61
|
+
Asks for confirmation; pass --yes to skip it (required without a terminal) or
|
|
62
|
+
--dry-run to only show the plan.
|
|
63
|
+
|
|
64
|
+
snapshot:
|
|
65
|
+
short: 'Save the current state so it can be restored with gitflash undo'
|
|
66
|
+
long: |
|
|
67
|
+
Saves branches, HEAD, tracked, staged and untracked files (not ignored ones) and stash
|
|
68
|
+
entries as git objects under refs/gitflash/snapshots/. The working tree is not changed.
|
|
69
|
+
Untracked files larger than 50 MB are skipped and listed.
|
|
70
|
+
|
|
71
|
+
Use --scope to save only some parts, for example --scope worktree.
|
|
72
|
+
|
|
73
|
+
clean:
|
|
74
|
+
short: 'Delete merged, gone, stale or agent-created branches'
|
|
75
|
+
long: |
|
|
76
|
+
Deletes branches that are no longer needed, with a snapshot first so `gitflash undo`
|
|
77
|
+
brings them back.
|
|
78
|
+
|
|
79
|
+
Without options it selects branches merged into the default branch and branches whose
|
|
80
|
+
upstream was deleted. Add criteria to select more (a branch is chosen when it matches any):
|
|
81
|
+
--merged merged into the default branch
|
|
82
|
+
--gone upstream branch was deleted on the remote
|
|
83
|
+
--stale [DAYS] no commits for DAYS days (default: `stale_days` setting, 30)
|
|
84
|
+
--agent created by an agent (marked by `gitflash wt add`)
|
|
85
|
+
|
|
86
|
+
Never selected: the current and default branch, main, master, `protected` branches from
|
|
87
|
+
.gitflash.yml, and branches checked out in any worktree. Unmerged branches are kept unless
|
|
88
|
+
--force is given. Every chosen branch lists why it was chosen.
|
|
89
|
+
|
|
90
|
+
Asks for confirmation; pass --yes to skip it (required without a terminal), or --dry-run.
|
|
91
|
+
|
|
92
|
+
worktree_remove:
|
|
93
|
+
long: |
|
|
94
|
+
Removes the given worktrees, given as a path or the branch checked out in them. Without
|
|
95
|
+
arguments, shows a menu in a terminal. The main checkout and the current worktree are
|
|
96
|
+
never removed; locked worktrees and worktrees with uncommitted changes are refused
|
|
97
|
+
unless --force is given.
|
|
98
|
+
|
|
99
|
+
Each removal saves a snapshot first, including uncommitted and untracked files. Ignored
|
|
100
|
+
files (build output, dependencies, .env) are not saved: the plan and the confirmation list
|
|
101
|
+
them, since removal deletes them for good. A worktree with an untracked file over 50 MB
|
|
102
|
+
is not removed, because the snapshot cannot hold it. `gitflash undo ID` brings the
|
|
103
|
+
directory, its branch and the saved files back.
|
|
104
|
+
|
|
105
|
+
Asks for confirmation; pass --yes to skip it (required without a terminal), or --dry-run.
|
|
106
|
+
Accepts the global options after the subcommand: gitflash wt remove feature --yes --json
|
|
107
|
+
|
|
108
|
+
pick:
|
|
109
|
+
short: 'Cherry-pick commits from another branch'
|
|
110
|
+
long: |
|
|
111
|
+
Moves fixes between branches, for example between agent branches.
|
|
112
|
+
|
|
113
|
+
`gitflash pick SOURCE --list` lists the commits of SOURCE that are not on the current branch
|
|
114
|
+
yet, oldest first, with SHA, subject, author, date and a diff summary. A commit whose change
|
|
115
|
+
is already here (the same patch under another SHA) is marked `=` (`applied` in JSON) and is
|
|
116
|
+
never picked. Merge commits are left out.
|
|
117
|
+
|
|
118
|
+
`gitflash pick SOURCE SHA...` applies the given commits, oldest first whatever the order
|
|
119
|
+
given, with `git cherry-pick -x` (the message records the origin). Without SHAs, a menu in a
|
|
120
|
+
terminal lets you choose. Add --no-commit to only stage the combined changes.
|
|
121
|
+
|
|
122
|
+
A snapshot is saved first and the plan asks for confirmation (pass --yes, required without a
|
|
123
|
+
terminal, or --dry-run). `gitflash undo` returns to the starting point.
|
|
124
|
+
|
|
125
|
+
On a conflict the pick stops and the result lists the conflicted files, the commits already
|
|
126
|
+
applied and the ones still waiting. Resolve the files and stage them, then run
|
|
127
|
+
`gitflash pick --continue`. `gitflash pick --skip` drops the commit that conflicts and
|
|
128
|
+
`gitflash pick --abort` returns to the state before the pick (both ask for confirmation and
|
|
129
|
+
save a snapshot first).
|
|
130
|
+
|
|
131
|
+
For agents: `gitflash pick SOURCE --list --json` and
|
|
132
|
+
`gitflash pick SOURCE SHA... --yes --json`.
|
|
133
|
+
|
|
134
|
+
mark:
|
|
135
|
+
short: 'Mark branches as agent or human work'
|
|
136
|
+
long: |
|
|
137
|
+
Records who created a branch, so `gitflash clean --agent` only touches branches an agent
|
|
138
|
+
created. The mark is the git config key `branch.<name>.gitflash-owner` and disappears with
|
|
139
|
+
the branch.
|
|
140
|
+
|
|
141
|
+
Without BRANCH the current branch is marked. The owner is `agent` unless --owner human is
|
|
142
|
+
given; --clear removes the mark. A branch without a mark counts as human work.
|
|
143
|
+
|
|
144
|
+
`gitflash wt add` marks the branches it creates. Use `gitflash mark` for branches made with
|
|
145
|
+
plain git, for example at the start of an agent session: `gitflash mark`.
|
|
146
|
+
|
|
147
|
+
Needs no confirmation. Pass --dry-run to see what would change, --json for the result.
|
data/lib/gitflash/branch.rb
CHANGED
|
@@ -6,10 +6,15 @@ module Gitflash
|
|
|
6
6
|
# A local branch as reported by `git for-each-ref`
|
|
7
7
|
Branch = Data.define(
|
|
8
8
|
:name, :sha, :current, :default, :upstream, :upstream_gone, :ahead, :behind,
|
|
9
|
-
:merged, :last_commit_at, :last_commit_author, :last_commit_subject
|
|
9
|
+
:merged, :last_commit_at, :last_commit_author, :last_commit_subject, :owner
|
|
10
10
|
) do
|
|
11
|
+
# Records are built without an owner; Ownership#annotate adds it
|
|
12
|
+
def initialize(owner: nil, **fields)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
# Parses one line produced with Repo::BRANCH_FORMAT.
|
|
12
|
-
# `merged_names` is nil when merge status is unknown.
|
|
17
|
+
# `merged_names` is nil when merge status is unknown. `owner` is set by the caller.
|
|
13
18
|
def self.parse(line, default_branch: nil, merged_names: nil)
|
|
14
19
|
name, sha, head, upstream, track, date, author, subject = fields(line)
|
|
15
20
|
|
|
@@ -29,6 +34,12 @@ module Gitflash
|
|
|
29
34
|
)
|
|
30
35
|
end
|
|
31
36
|
|
|
37
|
+
# { branch name => sha } from `git for-each-ref --format=TIPS_FORMAT refs/heads/`
|
|
38
|
+
def self.parse_tips(output)
|
|
39
|
+
output.to_s.each_line(chomp: true).to_h { |line| line.split(' ', 2) }
|
|
40
|
+
.transform_keys { |ref| ref.delete_prefix(Repo::HEADS) }
|
|
41
|
+
end
|
|
42
|
+
|
|
32
43
|
# The record's fields; the full ref name is shortened by removing refs/heads/ only, because
|
|
33
44
|
# `refname:short` gives another name when a tag has the same name.
|
|
34
45
|
def self.fields(line)
|
|
@@ -50,4 +61,5 @@ module Gitflash
|
|
|
50
61
|
super.merge(last_commit_at: last_commit_at.iso8601)
|
|
51
62
|
end
|
|
52
63
|
end
|
|
64
|
+
Branch::TIPS_FORMAT = '%(refname) %(objectname)'
|
|
53
65
|
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitflash
|
|
4
|
+
# Cherry-picking between branches: finds the commits of a source branch whose change is not on
|
|
5
|
+
# the current branch yet, applies them oldest first, and wraps git's own sequence controls.
|
|
6
|
+
# All git access for `gitflash pick` is here; it never prompts or prints.
|
|
7
|
+
class CherryPick
|
|
8
|
+
# `applied` is true when an equivalent change is already on the current branch
|
|
9
|
+
Candidate = Data.define(:sha, :subject, :author, :date, :applied, :files, :insertions,
|
|
10
|
+
:deletions) do
|
|
11
|
+
def short_sha = sha[0, 7]
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
super.merge(short_sha: short_sha, status: applied ? 'applied' : 'new')
|
|
15
|
+
.except(:applied)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The state of an unfinished pick: files with conflicts and the commits still to apply
|
|
20
|
+
Progress = Data.define(:conflicted, :remaining, :current)
|
|
21
|
+
|
|
22
|
+
RECORD = "\x1e"
|
|
23
|
+
SEPARATOR = "\x1f"
|
|
24
|
+
FORMAT = ['%m', '%H', '%s', '%an', '%cI'].join(SEPARATOR)
|
|
25
|
+
ENV_NO_EDITOR = { 'GIT_EDITOR' => 'true', 'GIT_SEQUENCE_EDITOR' => 'true' }.freeze
|
|
26
|
+
|
|
27
|
+
def initialize(bash: Git::BashCommand)
|
|
28
|
+
@bash = bash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# The full ref of a local (or remote-tracking) branch, nil when there is none
|
|
32
|
+
def source_ref(name)
|
|
33
|
+
return nil if name.to_s.empty? || name.start_with?('-')
|
|
34
|
+
|
|
35
|
+
["#{Repo::HEADS}#{name}", "refs/remotes/#{name}"].find { |ref| ref?(ref) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Commits of `source_ref` that are not merges, oldest first, each marked as applied when an
|
|
39
|
+
# equivalent change (same patch) is already on HEAD
|
|
40
|
+
def candidates(source_ref)
|
|
41
|
+
output = @bash.exec('git', 'log', '--left-right', '--cherry-mark', '--right-only',
|
|
42
|
+
'--no-merges', '--reverse', '--shortstat', "--format=#{RECORD}#{FORMAT}",
|
|
43
|
+
"HEAD...#{source_ref}", '--')
|
|
44
|
+
output.split(RECORD).reject { |record| record.strip.empty? }.map { |record| parse(record) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Applies the commits (full shas, in the given order) with `git cherry-pick`.
|
|
48
|
+
# `record_origin` adds "(cherry picked from ...)" to each message; `commit: false` only stages.
|
|
49
|
+
def apply(shas, record_origin: true, commit: true)
|
|
50
|
+
args = ['cherry-pick']
|
|
51
|
+
args << '-x' if record_origin
|
|
52
|
+
args << '--no-commit' unless commit
|
|
53
|
+
run(*args, *shas)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def continue = run('cherry-pick', '--continue')
|
|
57
|
+
|
|
58
|
+
def abort = run('cherry-pick', '--abort')
|
|
59
|
+
|
|
60
|
+
def skip = run('cherry-pick', '--skip')
|
|
61
|
+
|
|
62
|
+
# True while a cherry-pick waits for the user (conflicts or a stopped sequence)
|
|
63
|
+
def in_progress?
|
|
64
|
+
ref?('CHERRY_PICK_HEAD') || File.exist?(todo_path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def progress
|
|
68
|
+
Progress.new(conflicted: conflicted_files, remaining: remaining, current: current)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def parse(record)
|
|
74
|
+
line, *rest = record.split("\n", 2)
|
|
75
|
+
mark, sha, subject, author, date = line.split(SEPARATOR, 5)
|
|
76
|
+
files, insertions, deletions = stat(rest.first.to_s)
|
|
77
|
+
Candidate.new(sha: sha, subject: subject.to_s, author: author, date: date,
|
|
78
|
+
applied: mark == '=', files: files, insertions: insertions,
|
|
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)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def conflicted_files
|
|
88
|
+
stdout, _stderr, success = @bash.capture('git', 'diff', '--name-only', '--diff-filter=U',
|
|
89
|
+
'-z')
|
|
90
|
+
success ? stdout.split("\0") : []
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Commits still to apply after the one that stopped, as full shas
|
|
94
|
+
def remaining
|
|
95
|
+
return [] unless File.exist?(todo_path)
|
|
96
|
+
|
|
97
|
+
shas = File.readlines(todo_path, chomp: true).filter_map { |line| line[/\Apick (\h+)/, 1] }
|
|
98
|
+
shas.filter_map { |sha| full_sha(sha) }.reject { |sha| sha == current }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def current
|
|
102
|
+
full_sha('CHERRY_PICK_HEAD')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def full_sha(ref)
|
|
106
|
+
stdout, _stderr, success = @bash.capture('git', 'rev-parse', '--verify', '--quiet',
|
|
107
|
+
"#{ref}^{commit}")
|
|
108
|
+
success ? stdout.strip : nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ref?(ref)
|
|
112
|
+
_stdout, _stderr, success = @bash.capture('git', 'rev-parse', '--verify', '--quiet', ref)
|
|
113
|
+
success
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def todo_path
|
|
117
|
+
stdout, _stderr, success = @bash.capture('git', 'rev-parse', '--path-format=absolute',
|
|
118
|
+
'--git-path', 'sequencer/todo')
|
|
119
|
+
success ? stdout.strip : ''
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def run(*)
|
|
123
|
+
stdout, stderr, success = @bash.capture('git', *, env: ENV_NO_EDITOR)
|
|
124
|
+
Result.new(success: success,
|
|
125
|
+
output: [stdout, stderr].map(&:strip).reject(&:empty?).join("\n"))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitflash
|
|
4
|
+
# Decides what `clean` and `wt clean` would remove. Pure: it reads records and settings, runs
|
|
5
|
+
# no git command, so every rule can be tested without a repository.
|
|
6
|
+
module Cleanup
|
|
7
|
+
CRITERIA = %i[merged gone stale agent].freeze
|
|
8
|
+
DEFAULT_CRITERIA = %i[merged gone].freeze
|
|
9
|
+
|
|
10
|
+
# A record chosen for removal and why. `item` is a Branch or a Worktree.
|
|
11
|
+
Candidate = Data.define(:item, :reasons)
|
|
12
|
+
|
|
13
|
+
# Which criteria are active. `stale_days` applies to :stale.
|
|
14
|
+
Rules = Data.define(:criteria, :stale_days, :now) do
|
|
15
|
+
def reasons(branch)
|
|
16
|
+
criteria.filter_map { |criterion| label(criterion) if match?(criterion, branch) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def match?(criterion, branch)
|
|
22
|
+
case criterion
|
|
23
|
+
when :merged then branch.merged && !branch.default?
|
|
24
|
+
when :gone then branch.upstream_gone?
|
|
25
|
+
when :stale then branch.stale?(stale_days, now: now)
|
|
26
|
+
when :agent then branch.owner == 'agent'
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def label(criterion)
|
|
31
|
+
{ merged: 'merged', gone: 'upstream gone', agent: 'agent-created',
|
|
32
|
+
stale: "no commits for #{stale_days} days" }.fetch(criterion)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
module_function
|
|
37
|
+
|
|
38
|
+
# Branches that match at least one criterion and are not protected, each with its reasons
|
|
39
|
+
def branches(branches, rules:, protection:)
|
|
40
|
+
branches.reject { |branch| protection.protected?(branch) }.filter_map do |branch|
|
|
41
|
+
reasons = rules.reasons(branch)
|
|
42
|
+
Candidate.new(item: branch, reasons: reasons) if reasons.any?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Worktrees whose unprotected branch matches, as [chosen, skipped]. `blocker` is called with
|
|
47
|
+
# a worktree and returns why it may not be removed, or nil. A blocked match is skipped with
|
|
48
|
+
# that reason instead of being chosen.
|
|
49
|
+
def worktrees(worktrees, branches:, rules:, protection:, blocker:)
|
|
50
|
+
matched = matching(worktrees, branches, rules, protection)
|
|
51
|
+
chosen, skipped = matched.partition { |candidate| blocker.call(candidate.item).nil? }
|
|
52
|
+
[chosen, skipped.map { |candidate| skip(candidate.item, blocker.call(candidate.item)) }]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def matching(worktrees, branches, rules, protection)
|
|
56
|
+
by_name = branches.to_h { |branch| [branch.name, branch] }
|
|
57
|
+
worktrees.filter_map do |worktree|
|
|
58
|
+
branch = by_name[worktree.branch]
|
|
59
|
+
next if branch.nil? || protection.protected?(branch)
|
|
60
|
+
|
|
61
|
+
reasons = rules.reasons(branch)
|
|
62
|
+
Candidate.new(item: worktree, reasons: reasons) if reasons.any?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def skip(worktree, reason)
|
|
67
|
+
{ path: worktree.path, branch: worktree.branch, reason: reason }
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/gitflash/cli.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'thor'
|
|
|
5
5
|
module Gitflash
|
|
6
6
|
class Cli < Thor
|
|
7
7
|
extend Configuration::Descriptions
|
|
8
|
+
include CommandRunner
|
|
8
9
|
|
|
9
10
|
SCHEMA_PATH = File.expand_path('../../schema/v1.json', __dir__)
|
|
10
11
|
|
|
@@ -25,13 +26,13 @@ module Gitflash
|
|
|
25
26
|
option :gone, type: :boolean, desc: 'Branches whose upstream branch was deleted'
|
|
26
27
|
option :stale, type: :numeric, banner: 'DAYS', desc: 'Branches without commits for DAYS days'
|
|
27
28
|
def branches
|
|
28
|
-
run_command(Commands::Branches)
|
|
29
|
+
run_command('branches', Commands::Branches)
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
desc 'checkout [BRANCH]', descriptions.checkout.short
|
|
32
33
|
long_desc descriptions.checkout.long
|
|
33
34
|
def checkout(branch = nil)
|
|
34
|
-
run_command(Commands::Checkout, *[branch].compact)
|
|
35
|
+
run_command('checkout', Commands::Checkout, *[branch].compact)
|
|
35
36
|
end
|
|
36
37
|
|
|
37
38
|
desc 'delete [BRANCH...]', descriptions.delete.short
|
|
@@ -39,7 +40,28 @@ module Gitflash
|
|
|
39
40
|
option :force, type: :boolean, default: false,
|
|
40
41
|
desc: 'Delete branches even if they have unmerged changes'
|
|
41
42
|
def delete(*branches)
|
|
42
|
-
run_command(Commands::Delete, *branches)
|
|
43
|
+
run_command('delete', Commands::Delete, *branches)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
desc 'mark [BRANCH...]', descriptions.mark.short
|
|
47
|
+
long_desc descriptions.mark.long
|
|
48
|
+
option :owner, type: :string, enum: Ownership::OWNERS, desc: 'agent (default) or human'
|
|
49
|
+
option :clear, type: :boolean, default: false, desc: 'Remove the mark'
|
|
50
|
+
def mark(*branches)
|
|
51
|
+
run_command('mark', Commands::Mark, *branches)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc 'clean', descriptions.clean.short
|
|
55
|
+
long_desc descriptions.clean.long
|
|
56
|
+
option :merged, type: :boolean, desc: 'Branches merged into the default branch'
|
|
57
|
+
option :gone, type: :boolean, desc: 'Branches whose upstream branch was deleted'
|
|
58
|
+
option :stale, type: :string, banner: 'DAYS', lazy_default: '',
|
|
59
|
+
desc: 'Branches without commits for DAYS days (default: stale_days setting)'
|
|
60
|
+
option :agent, type: :boolean, desc: 'Branches created by an agent'
|
|
61
|
+
option :force, type: :boolean, default: false,
|
|
62
|
+
desc: 'Also delete selected branches that have unmerged changes'
|
|
63
|
+
def clean
|
|
64
|
+
run_command('clean', Commands::Clean)
|
|
43
65
|
end
|
|
44
66
|
|
|
45
67
|
desc 'reset [COMMIT]', descriptions.reset.short
|
|
@@ -47,9 +69,55 @@ module Gitflash
|
|
|
47
69
|
option :hard, type: :boolean, default: false, desc: 'Discard all current changes'
|
|
48
70
|
option :soft, type: :boolean, default: false, desc: 'Keep changes staged'
|
|
49
71
|
def reset(commit = nil)
|
|
50
|
-
run_command(Commands::Reset, *[commit].compact)
|
|
72
|
+
run_command('reset', Commands::Reset, *[commit].compact)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
desc 'undo [SNAPSHOT]', descriptions.undo.short
|
|
76
|
+
long_desc descriptions.undo.long
|
|
77
|
+
def undo(snapshot = nil)
|
|
78
|
+
run_command('undo', Commands::Undo, *[snapshot].compact)
|
|
51
79
|
end
|
|
52
80
|
|
|
81
|
+
desc 'snapshots', 'List snapshots, newest first'
|
|
82
|
+
def snapshots
|
|
83
|
+
run_command('snapshots', Commands::SnapshotList)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc 'snapshot', descriptions.snapshot.short
|
|
87
|
+
long_desc descriptions.snapshot.long
|
|
88
|
+
option :message, type: :string, desc: 'Why the snapshot was taken'
|
|
89
|
+
option :scope, type: :array, banner: 'PARTS',
|
|
90
|
+
desc: 'Parts to save: branches head worktree stashes (default: all)'
|
|
91
|
+
def snapshot
|
|
92
|
+
run_command('snapshot', Commands::SnapshotCreate)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
desc 'gc', 'Delete old snapshots'
|
|
96
|
+
option :older_than, type: :numeric, banner: 'DAYS',
|
|
97
|
+
desc: 'Delete snapshots older than DAYS days (default 30)'
|
|
98
|
+
def gc
|
|
99
|
+
run_command('gc', Commands::Gc)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
desc 'hook SUBCOMMAND', 'Agent hooks: claude (run the hook), install (register it)'
|
|
103
|
+
subcommand 'hook', HookCli
|
|
104
|
+
|
|
105
|
+
desc 'pick [SOURCE] [SHA...]', descriptions.pick.short
|
|
106
|
+
long_desc descriptions.pick.long
|
|
107
|
+
option :list, type: :boolean, desc: 'List the commits of SOURCE that are not on this branch yet'
|
|
108
|
+
option :commit, type: :boolean, default: true,
|
|
109
|
+
desc: 'Create a commit per pick; --no-commit only stages the changes'
|
|
110
|
+
option :continue, type: :boolean, desc: 'Continue after resolving conflicts'
|
|
111
|
+
option :skip, type: :boolean, desc: 'Skip the commit that conflicts'
|
|
112
|
+
option :abort, type: :boolean, desc: 'Give up and return to the state before the pick'
|
|
113
|
+
def pick(source = nil, *shas)
|
|
114
|
+
run_command('pick', Commands::Pick.command_for(options), *[source, *shas].compact)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
desc 'worktree SUBCOMMAND', 'Worktrees for parallel agent work (alias: wt)'
|
|
118
|
+
subcommand 'worktree', WorktreeCli
|
|
119
|
+
map 'wt' => :worktree
|
|
120
|
+
|
|
53
121
|
desc 'schema', 'Print the JSON Schema of the --json output'
|
|
54
122
|
def schema
|
|
55
123
|
puts File.read(SCHEMA_PATH)
|
|
@@ -61,22 +129,5 @@ module Gitflash
|
|
|
61
129
|
def version
|
|
62
130
|
puts VERSION
|
|
63
131
|
end
|
|
64
|
-
|
|
65
|
-
private
|
|
66
|
-
|
|
67
|
-
def run_command(command_class, *)
|
|
68
|
-
ui = build_ui(command_class.name.split('::').last.downcase)
|
|
69
|
-
repo = Repo.new
|
|
70
|
-
repo.ensure_work_tree!
|
|
71
|
-
status = command_class.new(repo: repo, ui: ui, options: options).call(*)
|
|
72
|
-
exit(status) unless status.zero?
|
|
73
|
-
rescue Error => e
|
|
74
|
-
ui.error(e)
|
|
75
|
-
exit(e.exit_code)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def build_ui(command)
|
|
79
|
-
Ui.new(command: command, json: options[:json], yes: options[:yes], dry_run: options[:dry_run])
|
|
80
|
-
end
|
|
81
132
|
end
|
|
82
133
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gitflash
|
|
4
|
+
# Runs a command class with the parsed Thor options and turns errors into output and exit codes
|
|
5
|
+
module CommandRunner
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_command(name, command_class, *, repo_required: true)
|
|
9
|
+
ui = build_ui(name)
|
|
10
|
+
repo = Repo.new
|
|
11
|
+
repo.ensure_work_tree! if repo_required
|
|
12
|
+
status = command_class.new(repo: repo, ui: ui, options: options).call(*)
|
|
13
|
+
exit(status) unless status.zero?
|
|
14
|
+
rescue Error => e
|
|
15
|
+
ui.error(e)
|
|
16
|
+
exit(e.exit_code)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def build_ui(name)
|
|
20
|
+
Ui.new(command: name, json: options[:json], yes: options[:yes], dry_run: options[:dry_run])
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|