gitflash 0.3.0 → 0.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37431db670588b8025d973c045532de047b33a667fc1c86152e1180d2d0a01a6
4
- data.tar.gz: 307fe7991415b2bedce69a08f7b0e09e591be3cbcb6be692c1d46c87c79019b1
3
+ metadata.gz: f754910573873164cbdb81ddd5191e9c472db9b482df20072e5ea4e7c5473f95
4
+ data.tar.gz: 764369b4f40ff33aaf2576721d58fafe54a7bda777e00e84387b008a3a9d006a
5
5
  SHA512:
6
- metadata.gz: dcc8b7a6a6c740c46c78d29766bc78b99fa9949217482a71e47070a077a51759903c74a82adbcbe24590e27dbc04f5db5501175efda1a2a624f7b84211e72579
7
- data.tar.gz: 541587b9f335e4ccd47f4cb0e7ec9806d1e4a2abb06d8811e81e8c483c2eba2126c26f7cbaebe5074d07d968d4e156b6ea751ccfbff08b6128a6239af0cd4272
6
+ metadata.gz: f0fa13de63f9418c5acec24af7e645927ee502e63401e36288b4d478e8d17d5d41bfe762e4055dc9fe67915c6b466d755a61ceb885a9f0c9e31d4d8a6abe4f23
7
+ data.tar.gz: a4c00a408fced68c41d503711ee5204df1bd978d14fe0b5b9a02a6d53e3b54a1a8381a930b87c86c5cd67ce239711a7b002e8931bcb184edab2f2fb1b1849e3d
data/CHANGELOG.md ADDED
@@ -0,0 +1,55 @@
1
+ ## [0.1.0.alpha] - 2023-06-17
2
+
3
+ - Initial release
4
+
5
+ ## [0.1.1.alpha] - 2023-10-28
6
+
7
+ - Fix delete method
8
+ - Added and updated test
9
+ - bug fixes
10
+
11
+ ## [0.1.2.alpha] - 2023-11-4
12
+
13
+ - Refactors and code improvements
14
+ - Zeitwerk warning suppress
15
+ - Update README
16
+ - Bug fixes
17
+
18
+ ## [0.2.0.alpha] - 2023-12-9
19
+
20
+ - Add reset command
21
+ - Code improvements
22
+
23
+ ## [0.3.0] - 2025-10-10
24
+
25
+ - Fix reset method
26
+ - Update to Ruby version 3.3.2
27
+ - Bug fixes
28
+
29
+ ## [0.4.0] - 2026-09-24
30
+
31
+ - Run git commands without a shell and raise on failure
32
+ - List branches with `git for-each-ref`; detached HEAD no longer shows as a branch
33
+ - List the latest 100 commits for `reset`, unaffected by `log.decorate`
34
+ - Exit with an error when a git command fails or outside a git repository
35
+ - `delete` does nothing when no branches are available or selected
36
+ - `reset --hard` prints `Exited` when declined; fix single-commit message
37
+ - Correct `reset` help text
38
+ - Portable `bin/gitflash` shebang
39
+ - Require thor >= 1.4 (CVE-2025-54314) and Ruby >= 3.3
40
+ - Add `version` command (`--version`, `-v`)
41
+ - CI: pin third-party action to a commit SHA, restrict token permissions, test Ruby 3.3–3.4
42
+
43
+ ## [0.5.0] - 2026-10-03
44
+
45
+ - Add `branches` command with last commit, upstream (ahead, behind, gone) and merge status, filters `--merged`, `--gone`, `--stale DAYS`
46
+ - Commands accept arguments: `checkout BRANCH`, `delete BRANCH...`, `reset COMMIT`
47
+ - Add `--json`, `--yes` and `--dry-run` options to every command
48
+ - Never wait for input without a terminal: exit with code 2 and explain what to pass
49
+ - `delete` keeps unmerged branches unless `--force` is given (breaking: it used to always force-delete) and also protects the default branch
50
+ - Detect the default branch from `origin/HEAD`, falling back to `main` or `master`
51
+ - Add `reset --soft`
52
+ - JSON contract version 1: every `--json` run prints one envelope (`schema`, `command`, `ok`, `status`, `dry_run`, `plan`, `result`, `error`) defined in `schema/v1.json`; stable error codes
53
+ - Results include what is needed to revert: deleted branch SHAs, the previous branch after `checkout`, the previous commit after `reset`
54
+ - Add `schema` command that prints the JSON Schema
55
+ - Internal: new `Repo` service layer with `Branch` and `Commit` records; integration specs run against real temporary git repositories
data/README.md CHANGED
@@ -2,9 +2,11 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/gitflash.svg)](https://badge.fury.io/rb/gitflash)
4
4
 
5
- ⚠️ **This project is stil under development**: Use at your own risk!
5
+ ⚠️ **This project is still under development**: Use at your own risk!
6
6
 
7
- This gem allows you to use some of the most common git commands in a more user intuitive way.
7
+ A safety net and cleanup tool for git repositories that AI coding agents work in. It works on plain git, with nothing new to adopt, and every command can be run by a person in a terminal or by an agent with a stable JSON contract.
8
+
9
+ See [docs/ROADMAP.md](docs/ROADMAP.md) for where the project is going.
8
10
 
9
11
  ## Installation
10
12
 
@@ -24,10 +26,67 @@ Or install it yourself as:
24
26
 
25
27
  ## Requirements
26
28
 
27
- This gem requires Ruby 3.3.2+.
29
+ This gem requires Ruby 3.3+ and git.
30
+
28
31
  ## Usage
29
32
 
30
- Run `gitflash` to get a list with the available commands and their description.
33
+ Run `gitflash` inside a git repository to get a list with the available commands and their description.
34
+
35
+ | Command | Description |
36
+ | --- | --- |
37
+ | `gitflash branches` | List local branches with last commit, upstream status and merge status. Filter with `--merged`, `--gone` or `--stale DAYS` |
38
+ | `gitflash checkout [BRANCH]` | Check out a branch, or pick one from a list |
39
+ | `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
+ | `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 |
41
+ | `gitflash schema` | Print the JSON Schema of the `--json` output |
42
+ | `gitflash version` | Print the installed version (also `--version`, `-v`) |
43
+
44
+ Without an argument, a command shows an interactive list. With arguments it runs directly, which also works in scripts.
45
+
46
+ Options for every command:
47
+
48
+ | Option | Effect |
49
+ | --- | --- |
50
+ | `--json` | Print JSON instead of text. Never shows lists or prompts |
51
+ | `--yes`, `-y` | Skip confirmation prompts |
52
+ | `--dry-run` | Show what would happen without changing anything |
53
+
54
+ Run `gitflash help <command>` for details on a command.
55
+
56
+ ### Scripts and AI agents
57
+
58
+ Without a terminal (for example in a script, CI or an AI coding agent), gitflash never waits for input:
59
+
60
+ - A command that needs a list selection fails with exit code 2 and says which argument to pass.
61
+ - A destructive action (`delete`, `reset --hard`) fails with exit code 2 and returns the plan unless you pass `--yes`.
62
+
63
+ With `--json`, every command prints one object in the same envelope, defined in [schema/v1.json](schema/v1.json) (also printed by `gitflash schema`):
64
+
65
+ ```json
66
+ {
67
+ "schema": 1,
68
+ "command": "delete",
69
+ "ok": true,
70
+ "status": "done",
71
+ "dry_run": false,
72
+ "plan": { "branches": ["old-feature"], "force": false },
73
+ "result": { "deleted": [{ "branch": "old-feature", "sha": "3551cba" }], "failed": [] }
74
+ }
75
+ ```
76
+
77
+ - `status`: `done`, `planned` (`--dry-run`), `noop`, `cancelled`, `failed`, `confirmation_required` or `error`. `ok` is true for the first four.
78
+ - `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`.
79
+ - `error.code` is a stable identifier such as `unknown_branch`, `protected_branch` or `confirmation_required`.
80
+
81
+ ```bash
82
+ gitflash branches --merged --json
83
+ ```
84
+
85
+ ```bash
86
+ gitflash delete old-feature another-branch --yes --json
87
+ ```
88
+
89
+ Exit codes: `0` success, `1` a git command failed (for example a branch was not fully merged), `2` invalid usage or confirmation required.
31
90
 
32
91
  ## Development
33
92
 
data/bin/gitflash CHANGED
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env ruby -wU
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'gitflash'
4
5
  require 'gitflash/cli'
@@ -1,33 +1,48 @@
1
+ branches:
2
+ short: 'List local branches with merge and upstream status'
3
+ long: |
4
+ Lists local branches with their last commit, upstream status (ahead, behind, gone)
5
+ and whether they are merged into the default branch.
6
+
7
+ The default branch is the one origin/HEAD points to, else main or master.
8
+
9
+ Filters (a branch is listed when it matches any of them):
10
+ --merged merged into the default branch
11
+ --gone upstream branch was deleted on the remote
12
+ --stale DAYS no commits for DAYS days
13
+
14
+ Pass --json for machine-readable output.
15
+
1
16
  checkout:
2
- short: 'Checkout the selected branch'
17
+ short: 'Checkout a branch'
3
18
  long: |
4
- Presents a list with all the available local branches.
19
+ Checks out BRANCH when given.
5
20
 
6
- You can use the arrow keys to navigate through the branches.
7
- Type for quick searching a specific branch.
8
- Press `enter` to choose and checkout the branch.
9
- Press `ctrl+c` to cancel the operation.
21
+ Without BRANCH, presents a list with all the available local branches:
22
+ use the arrow keys to navigate, type to search, press `enter` to checkout
23
+ and `ctrl+c` to cancel. Without a terminal, BRANCH is required.
10
24
 
11
25
  delete:
12
- short: 'Delete the selected branches'
26
+ short: 'Delete branches'
13
27
  long: |
14
- Presents a list with all the available local branches (without current or main/master).
28
+ Deletes the given BRANCHES. Without them, presents a list with the local branches
29
+ (without the current, default, main or master branch): press `space` to choose
30
+ branches and `enter` to confirm. Without a terminal, BRANCHES are required.
31
+
32
+ Branches with unmerged changes are kept unless --force is given.
15
33
 
16
- You can use the arrow keys to navigate through the branches.
17
- Type for quick searching a specific branch.
18
- Press `space` to choose multiple branches.
19
- Press `enter` to confirm selection and delete branches.
20
- Press `ctrl+c` to cancel the operation.
34
+ Asks for confirmation before deleting. Pass --yes to skip it, which is required
35
+ without a terminal, or --dry-run to only show what would be deleted.
21
36
 
22
37
  reset:
23
- short: 'Reset your code to the selected commit'
38
+ short: 'Reset your code to a commit'
24
39
  long: |
25
- Accepts an optional parameter (hard) if you want to hard reset.
40
+ Resets the current branch to COMMIT when given.
26
41
 
27
- Presents a list with all the available commits.
42
+ Without COMMIT, presents a list with the latest 100 commits of the current branch:
43
+ use the arrow keys to navigate, type to search, press `enter` to reset to that commit
44
+ and `ctrl+c` to cancel. Without a terminal, COMMIT is required.
28
45
 
29
- You can use the arrow keys to navigate through the commits.
30
- Type for quick searching a specific commit.
31
- Press `space` to choose multiple commits.
32
- Press `enter` to confirm selection and reset to that commit.
33
- Press `ctrl+c` to cancel the operation.
46
+ By default performs a mixed reset: your changes are kept as unstaged changes.
47
+ Pass --soft to keep them staged, or --hard to discard them. A hard reset asks for
48
+ confirmation; pass --yes to skip it, which is required without a terminal.
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Gitflash
6
+ # A local branch as reported by `git for-each-ref`
7
+ Branch = Data.define(
8
+ :name, :sha, :current, :default, :upstream, :upstream_gone, :ahead, :behind,
9
+ :merged, :last_commit_at, :last_commit_author, :last_commit_subject
10
+ ) do
11
+ # Parses one line produced with Repo::BRANCH_FORMAT.
12
+ # `merged_names` is nil when merge status is unknown.
13
+ def self.parse(line, default_branch: nil, merged_names: nil)
14
+ name, sha, head, upstream, track, date, author, subject = line.split(Repo::SEPARATOR, 8)
15
+
16
+ new(
17
+ name: name,
18
+ sha: sha,
19
+ current: head == '*',
20
+ default: name == default_branch,
21
+ upstream: upstream.to_s.empty? ? nil : upstream,
22
+ upstream_gone: track == 'gone',
23
+ ahead: track.to_s[/ahead (\d+)/, 1].to_i,
24
+ behind: track.to_s[/behind (\d+)/, 1].to_i,
25
+ merged: merged_names&.include?(name),
26
+ last_commit_at: Time.iso8601(date),
27
+ last_commit_author: author,
28
+ last_commit_subject: subject.to_s
29
+ )
30
+ end
31
+
32
+ alias_method :current?, :current
33
+ alias_method :default?, :default
34
+ alias_method :upstream_gone?, :upstream_gone
35
+
36
+ def stale?(days, now: Time.now)
37
+ last_commit_at < now - (days * 86_400)
38
+ end
39
+
40
+ def to_h
41
+ super.merge(last_commit_at: last_commit_at.iso8601)
42
+ end
43
+ end
44
+ end
data/lib/gitflash/cli.rb CHANGED
@@ -6,113 +6,77 @@ module Gitflash
6
6
  class Cli < Thor
7
7
  extend Configuration::Descriptions
8
8
 
9
+ SCHEMA_PATH = File.expand_path('../../schema/v1.json', __dir__)
10
+
9
11
  def self.exit_on_failure?
10
12
  true
11
13
  end
12
14
 
13
- desc 'checkout', descriptions.checkout.short
14
- long_desc descriptions.checkout.long
15
-
16
- def checkout
17
- branches? ? checkout_branch : prompt.ok('You only have one branch!')
18
- end
19
-
20
- desc 'delete', descriptions.delete.short
21
- long_desc descriptions.delete.long
22
-
23
- def delete
24
- branches? ? delete_branch : prompt.ok('You only have one branch!')
25
- end
26
-
27
- desc 'reset', descriptions.reset.short
28
- long_desc descriptions.reset.long
29
-
30
- option :hard, type: :boolean, default: false, desc: 'Perform a hard reset'
31
- def reset
32
- commits? ? reset_to_commit : prompt.ok('You only have one branch!')
33
- end
34
-
35
- private
36
-
37
- def checkout_branch
38
- selection = prompt.select(
39
- 'Select a branch to checkout',
40
- branches,
41
- **checkout_options
42
- )
15
+ class_option :json, type: :boolean, default: false,
16
+ desc: 'Print JSON instead of text; never prompts'
17
+ class_option :yes, type: :boolean, aliases: '-y', default: false,
18
+ desc: 'Skip confirmation prompts'
19
+ class_option :dry_run, type: :boolean, default: false,
20
+ desc: 'Show what would happen without changing anything'
43
21
 
44
- git.checkout(selection)
22
+ desc 'branches', descriptions.branches.short
23
+ long_desc descriptions.branches.long
24
+ option :merged, type: :boolean, desc: 'Branches merged into the default branch'
25
+ option :gone, type: :boolean, desc: 'Branches whose upstream branch was deleted'
26
+ option :stale, type: :numeric, banner: 'DAYS', desc: 'Branches without commits for DAYS days'
27
+ def branches
28
+ run_command(Commands::Branches)
45
29
  end
46
30
 
47
- def delete_branch
48
- selection = prompt.multi_select(
49
- 'Select branches to delete',
50
- branches(options: { master: false, current: false })
51
- )
52
-
53
- warning_message = <<~TEXT
54
- You are about to permanently delete the following branches even if they have unmerged changes:
55
-
56
- #{selection.map { |br| "* #{br}" }.join("\n")}
57
- TEXT
58
-
59
- prompt_proceed_warning(warning_message) { git.delete(selection) }
60
- end
61
-
62
- def reset_to_commit
63
- selection = prompt.select('Select a commit to reset to', branch_commits)
64
- reset_options = { commit_hash: selection, hard: options[:hard] }
65
-
66
- if options[:hard]
67
- warning_message = 'You are about to reset your branch and lose all your current changes'
68
- prompt.proceed_with_warning(warning_message) { git.reset(**reset_options) }
69
- else
70
- git.reset(**reset_options)
71
- end
72
- end
73
-
74
- def branches?
75
- all_branches && all_branches.size > 1
76
- end
77
-
78
- def commits?
79
- branch_commits && branch_commits.size > 1
31
+ desc 'checkout [BRANCH]', descriptions.checkout.short
32
+ long_desc descriptions.checkout.long
33
+ def checkout(branch = nil)
34
+ run_command(Commands::Checkout, *[branch].compact)
80
35
  end
81
36
 
82
- def prompt
83
- @prompt ||= Prompt.create
37
+ desc 'delete [BRANCH...]', descriptions.delete.short
38
+ long_desc descriptions.delete.long
39
+ option :force, type: :boolean, default: false,
40
+ desc: 'Delete branches even if they have unmerged changes'
41
+ def delete(*branches)
42
+ run_command(Commands::Delete, *branches)
84
43
  end
85
44
 
86
- def prompt_proceed_warning(message)
87
- prompt.warn(message)
88
- answer = prompt.yes?('Do you want to proceed?')
89
- puts answer ? yield : 'Exited'
45
+ desc 'reset [COMMIT]', descriptions.reset.short
46
+ long_desc descriptions.reset.long
47
+ option :hard, type: :boolean, default: false, desc: 'Discard all current changes'
48
+ option :soft, type: :boolean, default: false, desc: 'Keep changes staged'
49
+ def reset(commit = nil)
50
+ run_command(Commands::Reset, *[commit].compact)
90
51
  end
91
52
 
92
- def checkout_options
93
- {}.tap do |opt|
94
- opt[:default] = current unless current.nil?
95
- end
53
+ desc 'schema', 'Print the JSON Schema of the --json output'
54
+ def schema
55
+ puts File.read(SCHEMA_PATH)
96
56
  end
97
57
 
98
- def branches(options: {})
99
- git.local_branches(**options)
100
- end
58
+ desc 'version', 'Print the gitflash version'
59
+ map %w[--version -v] => :version
101
60
 
102
- def all_branches
103
- @all_branches ||= git.all_local_branches
61
+ def version
62
+ puts VERSION
104
63
  end
105
64
 
106
- def current
107
- @current ||= git.current_branch
108
- end
65
+ private
109
66
 
110
- def branch_commits
111
- @branch_commits = git.branch_commits
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)
112
76
  end
113
77
 
114
- def git
115
- Gitflash::Git::Wrapper
78
+ def build_ui(command)
79
+ Ui.new(command: command, json: options[:json], yes: options[:yes], dry_run: options[:dry_run])
116
80
  end
117
81
  end
118
82
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitflash
4
+ module Commands
5
+ # Shared setup for commands. `call` returns the process exit status.
6
+ class Base
7
+ def initialize(repo:, ui:, options: {})
8
+ @repo = repo
9
+ @ui = ui
10
+ @options = options
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :repo, :ui, :options
16
+
17
+ def require_interactive!(message)
18
+ usage_error!('input_required', message) unless ui.interactive?
19
+ end
20
+
21
+ def usage_error!(code, message)
22
+ raise UsageError.new(message, code: code)
23
+ end
24
+
25
+ def git_error!(command, result)
26
+ raise Error.new("git #{command} failed:\n#{result.output}", code: 'git_failed')
27
+ end
28
+
29
+ def planned(plan, text)
30
+ ui.report(status: 'planned', plan: plan, text: text)
31
+ end
32
+
33
+ def cancelled(plan)
34
+ ui.report(status: 'cancelled', plan: plan, text: 'Exited')
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitflash
4
+ module Commands
5
+ # Lists local branches, optionally filtered to merged, gone or stale ones
6
+ class Branches < Base
7
+ def call
8
+ branches = filter(repo.branches(merged_status: true))
9
+ result = { default_branch: repo.default_branch, branches: branches.map(&:to_h) }
10
+ ui.report(status: 'done', result: result, text: table(branches))
11
+ end
12
+
13
+ private
14
+
15
+ # A branch is listed when it matches any of the given filters
16
+ def filter(branches)
17
+ filters = active_filters
18
+ return branches if filters.empty?
19
+
20
+ branches.select { |branch| filters.any? { |matches| matches.call(branch) } }
21
+ end
22
+
23
+ def active_filters
24
+ filters = {
25
+ merged: ->(branch) { branch.merged && !branch.default? },
26
+ gone: lambda(&:upstream_gone?),
27
+ stale: ->(branch) { branch.stale?(options[:stale]) }
28
+ }
29
+ filters.select { |name, _filter| options[name] }.values
30
+ end
31
+
32
+ def table(branches)
33
+ return 'No branches match.' if branches.empty?
34
+
35
+ width = branches.map { |branch| branch.name.length }.max
36
+ branches.map { |branch| row(branch, width) }.join("\n")
37
+ end
38
+
39
+ def row(branch, width)
40
+ marker = branch.current? ? '*' : ' '
41
+ columns = [
42
+ "#{marker} #{branch.name.ljust(width)}",
43
+ age(branch.last_commit_at).ljust(12),
44
+ status(branch).ljust(22),
45
+ branch.last_commit_subject
46
+ ]
47
+ columns.join(' ').rstrip
48
+ end
49
+
50
+ def status(branch)
51
+ {
52
+ 'default' => branch.default?,
53
+ 'merged' => branch.merged && !branch.default?,
54
+ 'gone' => branch.upstream_gone?,
55
+ "ahead #{branch.ahead}" => branch.ahead.positive?,
56
+ "behind #{branch.behind}" => branch.behind.positive?
57
+ }.select { |_label, shown| shown }.keys.join(', ')
58
+ end
59
+
60
+ def age(time)
61
+ days = ((Time.now - time) / 86_400).floor
62
+ return 'today' if days < 1
63
+
64
+ days == 1 ? '1 day ago' : "#{days} days ago"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitflash
4
+ module Commands
5
+ # Checks out a local branch given as an argument or picked from a menu
6
+ class Checkout < Base
7
+ def call(name = nil)
8
+ branches = repo.branches
9
+ current = branches.find(&:current?)&.name
10
+
11
+ if name.nil?
12
+ require_interactive!('Pass the branch to check out: gitflash checkout BRANCH')
13
+ return noop(current, 'You only have one branch!') if branches.size < 2
14
+
15
+ name = pick(branches, current)
16
+ end
17
+
18
+ checkout(name, branches.map(&:name), current)
19
+ end
20
+
21
+ private
22
+
23
+ def pick(branches, current)
24
+ select_options = current.nil? ? {} : { default: current }
25
+ ui.select('Select a branch to checkout', branches.map(&:name), **select_options)
26
+ end
27
+
28
+ def checkout(name, names, current)
29
+ usage_error!('unknown_branch', "Unknown branch '#{name}'") unless names.include?(name)
30
+ return noop(name, "Already on '#{name}'") if name == current
31
+
32
+ plan = { branch: name }
33
+ return planned(plan, "Would check out '#{name}'") if ui.dry_run?
34
+
35
+ result = repo.checkout(name)
36
+ git_error!('checkout', result) unless result.success?
37
+
38
+ ui.report(status: 'done', plan: plan, result: { branch: name, previous_branch: current },
39
+ text: "Switched to branch '#{name}'")
40
+ end
41
+
42
+ def noop(branch, text)
43
+ ui.report(status: 'noop', result: { branch: branch, previous_branch: branch }, text: text)
44
+ end
45
+ end
46
+ end
47
+ end