gitflash 0.5.0 → 0.5.1

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: f754910573873164cbdb81ddd5191e9c472db9b482df20072e5ea4e7c5473f95
4
- data.tar.gz: 764369b4f40ff33aaf2576721d58fafe54a7bda777e00e84387b008a3a9d006a
3
+ metadata.gz: 86ac16bd9cd3c64cd98305a6423e67c05187040d8caba798d132a4f985ee28df
4
+ data.tar.gz: 8da24e90611e20296f70ca3cadab5a77f697c97ff43cf535b9f51b1bd40a66be
5
5
  SHA512:
6
- metadata.gz: f0fa13de63f9418c5acec24af7e645927ee502e63401e36288b4d478e8d17d5d41bfe762e4055dc9fe67915c6b466d755a61ceb885a9f0c9e31d4d8a6abe4f23
7
- data.tar.gz: a4c00a408fced68c41d503711ee5204df1bd978d14fe0b5b9a02a6d53e3b54a1a8381a930b87c86c5cd67ce239711a7b002e8931bcb184edab2f2fb1b1849e3d
6
+ metadata.gz: c573a9265e4558a9d502840cb7b65b0c07c6e6f640c74bde499ed1b4ff160fd8aeb101a9ecd95b692a2170c812d0a6afd14d8abe8629d46b8e65732a7622384c
7
+ data.tar.gz: 53e55b8afcc5f32cd1b0fa238e0f7db356b4e919d57f606ebec37ee3a86539fff2dd0afaef3b5bf6f295f235e9f4452ec1b5bb3dee5dc7fd1df964cb3e6f3d85
data/CHANGELOG.md CHANGED
@@ -53,3 +53,12 @@
53
53
  - Results include what is needed to revert: deleted branch SHAs, the previous branch after `checkout`, the previous commit after `reset`
54
54
  - Add `schema` command that prints the JSON Schema
55
55
  - Internal: new `Repo` service layer with `Branch` and `Commit` records; integration specs run against real temporary git repositories
56
+
57
+ ## [0.5.1] - 2026-10-04
58
+
59
+ Security fixes for names and text that come from the repository:
60
+
61
+ - `checkout` uses `git switch --` and refuses a branch name that starts with a dash. A local ref named like `--force` (for example from a cloned repository) used to become a `git checkout --force` option and discard uncommitted changes
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
+ - `delete` passes `--` before the branch name; `resolve_commit` refuses references that start with a dash
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
@@ -11,7 +11,7 @@ module Gitflash
11
11
  # Parses one line produced with Repo::BRANCH_FORMAT.
12
12
  # `merged_names` is nil when merge status is unknown.
13
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)
14
+ name, sha, head, upstream, track, date, author, subject = fields(line)
15
15
 
16
16
  new(
17
17
  name: name,
@@ -29,6 +29,15 @@ module Gitflash
29
29
  )
30
30
  end
31
31
 
32
+ # The record's fields; the full ref name is shortened by removing refs/heads/ only, because
33
+ # `refname:short` gives another name when a tag has the same name.
34
+ def self.fields(line)
35
+ fields = line.split(Repo::SEPARATOR, 8)
36
+ fields[0] = fields[0].delete_prefix(Repo::HEADS)
37
+ fields
38
+ end
39
+ private_class_method :fields
40
+
32
41
  alias_method :current?, :current
33
42
  alias_method :default?, :default
34
43
  alias_method :upstream_gone?, :upstream_gone
@@ -12,7 +12,7 @@ module Gitflash
12
12
  end
13
13
 
14
14
  def label
15
- "#{sha} - #{subject}"
15
+ "#{sha} - #{TerminalText.safe(subject)}"
16
16
  end
17
17
 
18
18
  def to_h
data/lib/gitflash/repo.rb CHANGED
@@ -6,12 +6,14 @@ module Gitflash
6
6
  class Repo
7
7
  SEPARATOR = "\x1f"
8
8
  BRANCH_FORMAT = %w[
9
- refname:short objectname:short HEAD upstream:short upstream:track,nobracket
9
+ refname objectname:short HEAD upstream:short upstream:track,nobracket
10
10
  committerdate:iso-strict authorname subject
11
11
  ].map { |field| "%(#{field})" }.join('%1f')
12
12
  COMMIT_FORMAT = %w[%h %s %an %cI].join('%x1f')
13
13
  COMMITS_LIMIT = 100
14
14
  FALLBACK_DEFAULT_BRANCHES = %w[main master].freeze
15
+ HEADS = 'refs/heads/'
16
+ REMOTE_HEAD = 'refs/remotes/origin/'
15
17
 
16
18
  def initialize(bash: Git::BashCommand)
17
19
  @bash = bash
@@ -41,7 +43,7 @@ module Gitflash
41
43
  def default_branch
42
44
  return @default_branch if defined?(@default_branch)
43
45
 
44
- @default_branch = default_ref&.delete_prefix('origin/')
46
+ @default_branch = branch_name(default_ref)
45
47
  end
46
48
 
47
49
  # Commits of the current branch, newest first. Empty for a branch without commits.
@@ -54,17 +56,22 @@ module Gitflash
54
56
 
55
57
  # Full sha of a commit reference, or nil when it does not name a commit
56
58
  def resolve_commit(ref)
59
+ return nil if ref.start_with?('-')
60
+
57
61
  stdout, _stderr, success = @bash.capture('git', 'rev-parse', '--verify', '--quiet',
58
62
  "#{ref}^{commit}")
59
63
  success ? stdout.strip : nil
60
64
  end
61
65
 
66
+ # Uses `git switch`, which never discards changes or treats the name as a file path.
67
+ # A name that starts with a dash is refused: a ref named like an option must not become one.
62
68
  def checkout(branch)
63
- run('git', 'checkout', branch, '--')
69
+ refuse_option_like!(branch)
70
+ run('git', 'switch', '--', branch)
64
71
  end
65
72
 
66
73
  def delete_branch(branch, force: false)
67
- run('git', 'branch', force ? '-D' : '-d', branch)
74
+ run('git', 'branch', force ? '-D' : '-d', '--', branch)
68
75
  end
69
76
 
70
77
  def reset(commit, mode:)
@@ -73,13 +80,27 @@ module Gitflash
73
80
 
74
81
  private
75
82
 
83
+ # Removes only the prefix the ref starts with, so a branch named `refs/heads/x` keeps its name
84
+ def branch_name(ref)
85
+ return nil unless ref
86
+
87
+ ref.delete_prefix(ref.start_with?(REMOTE_HEAD) ? REMOTE_HEAD : HEADS)
88
+ end
89
+
90
+ def refuse_option_like!(name)
91
+ return unless name.start_with?('-')
92
+
93
+ raise UsageError.new("Invalid branch name '#{name}'", code: 'invalid_usage')
94
+ end
95
+
76
96
  def branch_lines
77
97
  @branch_lines ||= @bash.exec('git', 'for-each-ref', "--format=#{BRANCH_FORMAT}",
78
- 'refs/heads/')
98
+ HEADS)
79
99
  .each_line(chomp: true).reject(&:empty?)
80
100
  end
81
101
 
82
- # Ref used to check merge status: origin/HEAD's target, else a local main/master
102
+ # Full ref used to check merge status: origin/HEAD's target, else a local main/master.
103
+ # Full names are used throughout because short names change when a tag has the same name.
83
104
  def default_ref
84
105
  return @default_ref if defined?(@default_ref)
85
106
 
@@ -87,24 +108,23 @@ module Gitflash
87
108
  end
88
109
 
89
110
  def remote_default_ref
90
- ref = @bash.exec('git', 'symbolic-ref', '--quiet', '--short',
91
- 'refs/remotes/origin/HEAD').strip
92
- ref.empty? ? nil : ref
111
+ ref = @bash.exec('git', 'symbolic-ref', '--quiet', "#{REMOTE_HEAD}HEAD").strip
112
+ ref.start_with?(REMOTE_HEAD) ? ref : nil
93
113
  rescue Git::CommandError
94
114
  nil
95
115
  end
96
116
 
97
117
  def local_default_branch
98
- names = branch_lines.map { |line| line.split(SEPARATOR, 2).first }
99
- FALLBACK_DEFAULT_BRANCHES.find { |name| names.include?(name) }
118
+ names = branch_lines.map { |line| line.split(SEPARATOR, 2).first.delete_prefix(HEADS) }
119
+ name = FALLBACK_DEFAULT_BRANCHES.find { |candidate| names.include?(candidate) }
120
+ name && "#{HEADS}#{name}"
100
121
  end
101
122
 
102
123
  def merged_branch_names
103
124
  return nil unless default_ref
104
125
 
105
- @bash.exec('git', 'for-each-ref', "--merged=#{default_ref}", '--format=%(refname:short)',
106
- 'refs/heads/')
107
- .each_line(chomp: true).to_set
126
+ @bash.exec('git', 'for-each-ref', "--merged=#{default_ref}", '--format=%(refname)', HEADS)
127
+ .each_line(chomp: true).to_set { |ref| ref.delete_prefix(HEADS) }
108
128
  end
109
129
 
110
130
  def run(*)
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitflash
4
+ # Makes text from the repository safe to print in a terminal. Commit subjects, author names
5
+ # and git messages are written by other people; control characters in them could move the
6
+ # cursor, rewrite earlier output or set the clipboard (OSC 52). They are shown as escapes.
7
+ # Newlines and tabs stay.
8
+ module TerminalText
9
+ UNSAFE = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/
10
+
11
+ module_function
12
+
13
+ def safe(text)
14
+ text.to_s.scrub.gsub(UNSAFE) do |char|
15
+ format(char.ord > 0xFF ? '\\u%04X' : '\\x%02X', char.ord)
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/gitflash/ui.rb CHANGED
@@ -38,7 +38,7 @@ module Gitflash
38
38
  document = envelope(status: status, plan: plan, result: result, error: error)
39
39
  $stdout.puts JSON.generate(document)
40
40
  else
41
- $stdout.puts text
41
+ $stdout.puts TerminalText.safe(text)
42
42
  end
43
43
  OK_STATUSES.include?(status) ? 0 : 1
44
44
  end
@@ -48,7 +48,7 @@ module Gitflash
48
48
  details = { code: error.code, message: error.message, exit_code: error.exit_code }
49
49
  $stdout.puts JSON.generate(envelope(status: error.status, plan: error.plan, error: details))
50
50
  else
51
- warn error.message
51
+ warn TerminalText.safe(error.message)
52
52
  end
53
53
  end
54
54
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Gitflash
4
- VERSION = '0.5.0'
4
+ VERSION = '0.5.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitflash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kleanthis Mitsioulis
@@ -89,6 +89,7 @@ files:
89
89
  - lib/gitflash/prompt.rb
90
90
  - lib/gitflash/repo.rb
91
91
  - lib/gitflash/result.rb
92
+ - lib/gitflash/terminal_text.rb
92
93
  - lib/gitflash/ui.rb
93
94
  - lib/gitflash/usage_error.rb
94
95
  - lib/gitflash/version.rb
@@ -114,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
114
115
  - !ruby/object:Gem::Version
115
116
  version: '0'
116
117
  requirements: []
117
- rubygems_version: 3.7.2
118
+ rubygems_version: 4.0.22
118
119
  specification_version: 4
119
120
  summary: Simplified usage of some git cli commands
120
121
  test_files: []