ruby_git 0.2.0 → 0.3.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.commitlintrc.yml +16 -0
  3. data/.github/CODEOWNERS +4 -0
  4. data/.github/workflows/continuous_integration.yml +87 -0
  5. data/.github/workflows/enforce_conventional_commits.yml +21 -0
  6. data/.github/workflows/experimental_ruby_builds.yml +65 -0
  7. data/.gitignore +3 -0
  8. data/.husky/commit-msg +1 -0
  9. data/.markdownlint.yml +25 -0
  10. data/.rubocop.yml +13 -15
  11. data/.yardopts +6 -1
  12. data/CHANGELOG.md +50 -0
  13. data/CONTRIBUTING.md +5 -5
  14. data/{LICENSE.md → LICENSE.txt} +1 -1
  15. data/PLAN.md +67 -0
  16. data/README.md +58 -6
  17. data/RELEASING.md +5 -54
  18. data/Rakefile +31 -38
  19. data/RubyGit Class Diagram.svg +1 -0
  20. data/bin/command-line-test +189 -0
  21. data/bin/console +2 -0
  22. data/bin/setup +13 -2
  23. data/lib/ruby_git/command_line/options.rb +61 -0
  24. data/lib/ruby_git/command_line/result.rb +155 -0
  25. data/lib/ruby_git/command_line/runner.rb +296 -0
  26. data/lib/ruby_git/command_line.rb +95 -0
  27. data/lib/ruby_git/encoding_normalizer.rb +49 -0
  28. data/lib/ruby_git/errors.rb +169 -0
  29. data/lib/ruby_git/repository.rb +33 -0
  30. data/lib/ruby_git/status/branch.rb +92 -0
  31. data/lib/ruby_git/status/entry.rb +162 -0
  32. data/lib/ruby_git/status/ignored_entry.rb +44 -0
  33. data/lib/ruby_git/status/ordinary_entry.rb +207 -0
  34. data/lib/ruby_git/status/parser.rb +203 -0
  35. data/lib/ruby_git/status/renamed_entry.rb +257 -0
  36. data/lib/ruby_git/status/report.rb +143 -0
  37. data/lib/ruby_git/status/stash.rb +33 -0
  38. data/lib/ruby_git/status/submodule_status.rb +85 -0
  39. data/lib/ruby_git/status/unmerged_entry.rb +248 -0
  40. data/lib/ruby_git/status/untracked_entry.rb +52 -0
  41. data/lib/ruby_git/status.rb +33 -0
  42. data/lib/ruby_git/version.rb +1 -1
  43. data/lib/ruby_git/worktree.rb +175 -33
  44. data/lib/ruby_git.rb +91 -28
  45. data/package.json +11 -0
  46. data/ruby_git.gemspec +32 -20
  47. metadata +145 -47
  48. data/.travis.yml +0 -26
  49. data/CODEOWNERS +0 -3
  50. data/MAINTAINERS.md +0 -8
  51. data/lib/ruby_git/error.rb +0 -8
  52. data/lib/ruby_git/file_helpers.rb +0 -42
  53. data/lib/ruby_git/git_binary.rb +0 -106
  54. /data/{ISSUE_TEMPLATE.md → .github/ISSUE_TEMPLATE.md} +0 -0
  55. /data/{PULL_REQUEST_TEMPLATE.md → .github/PULL_REQUEST_TEMPLATE.md} +0 -0
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyGit
4
+ module Status
5
+ # Represents git branch information
6
+ #
7
+ # @api public
8
+ class Branch
9
+ # @attribute [rw] name
10
+ #
11
+ # The name of the current branch
12
+ #
13
+ # @example
14
+ # branch.name #=> 'main'
15
+ #
16
+ # @return [String, nil] branch name or nil if detached HEAD
17
+ #
18
+ # @api public
19
+ attr_accessor :name
20
+
21
+ # @attribute [rw] oid
22
+ #
23
+ # The object ID (hash) of the current commit
24
+ #
25
+ # @example
26
+ # branch.oid #=> 'abcdef1234567890'
27
+ #
28
+ # @return [String] commit hash
29
+ #
30
+ # @api public
31
+ attr_accessor :oid
32
+
33
+ # @attribute [rw] upstream
34
+ #
35
+ # The name of the upstream branch
36
+ #
37
+ # @example
38
+ # branch.upstream #=> 'origin/main'
39
+ #
40
+ # @return [String, nil] upstream branch name or nil if no upstream
41
+ #
42
+ # @api public
43
+ attr_accessor :upstream
44
+
45
+ # @attribute [rw] ahead
46
+ #
47
+ # Number of commits ahead of upstream
48
+ #
49
+ # @example
50
+ # branch.ahead #=> 2
51
+ #
52
+ # @return [Integer] number of commits ahead
53
+ #
54
+ # @api public
55
+ attr_accessor :ahead
56
+
57
+ # @attribute [rw] behind
58
+ #
59
+ # Number of commits behind upstream
60
+ #
61
+ # @example
62
+ # branch.behind #=> 3
63
+ #
64
+ # @return [Integer] number of commits behind
65
+ #
66
+ # @api public
67
+ attr_accessor :behind
68
+
69
+ # Check if the branch has an upstream configured
70
+ #
71
+ # @example
72
+ # branch.upstream? #=> true
73
+ #
74
+ # @return [Boolean] true if upstream is configured
75
+ #
76
+ def upstream?
77
+ !@upstream.nil?
78
+ end
79
+
80
+ # Check if HEAD is detached
81
+ #
82
+ # @example
83
+ # branch.detached? #=> true
84
+ #
85
+ # @return [Boolean] true if HEAD is detached
86
+ #
87
+ def detached?
88
+ @name.nil?
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyGit
4
+ module Status
5
+ # Base class for git status entries
6
+ #
7
+ # @api public
8
+ class Entry
9
+ # Status code mapping to symbols
10
+ STATUS_CODES = {
11
+ '.': :unmodified,
12
+ M: :modified,
13
+ T: :type_changed,
14
+ A: :added,
15
+ D: :deleted,
16
+ R: :renamed,
17
+ C: :copied,
18
+ U: :updated_but_unmerged,
19
+ '?': :untracked,
20
+ '!': :ignored
21
+ }.freeze
22
+
23
+ # Rename operation mapping to symbols
24
+ RENAME_OPERATIONS = {
25
+ 'R' => :rename
26
+ # git status doesn't actually try to detect copies
27
+ # 'C' => :copy
28
+ }.freeze
29
+
30
+ # @attribute [r] path
31
+ #
32
+ # The path of the file
33
+ #
34
+ # @example
35
+ # entry.path #=> 'lib/example.rb'
36
+ #
37
+ # @return [String] file path
38
+ #
39
+ attr_reader :path
40
+
41
+ # Initialize a new entry
42
+ #
43
+ # @example
44
+ # Entry.new('lib/example.rb')
45
+ #
46
+ # @param path [String] file path
47
+ #
48
+ def initialize(path)
49
+ @path = path
50
+ end
51
+
52
+ # Convert a status code to a symbol
53
+ #
54
+ # @example
55
+ # Entry.status_to_symbol('M') #=> :modified
56
+ #
57
+ # @param code [String] status code
58
+ # @return [Symbol] status as symbol
59
+ #
60
+ def self.status_to_symbol(code)
61
+ STATUS_CODES[code.to_sym] || :unknown
62
+ end
63
+
64
+ # Convert a rename operation to a symbol
65
+ #
66
+ # @example
67
+ # Entry.rename_operation_to_symbol('R') #=> :rename
68
+ #
69
+ # @param code [String] the operation code
70
+ # @return [Symbol] operation as symbol
71
+ #
72
+ def self.rename_operation_to_symbol(code)
73
+ RENAME_OPERATIONS[code] || :unknown
74
+ end
75
+
76
+ # Get the staging status
77
+ #
78
+ # @example
79
+ # entry.staging_status #=> :modified
80
+ #
81
+ # @return [Symbol, nil] staging status symbol or nil if not applicable
82
+ #
83
+ def index_status = nil
84
+
85
+ # Get the worktree status
86
+ #
87
+ # @example
88
+ # entry.worktree_status #=> :unmodified
89
+ #
90
+ # @return [Symbol, nil] worktree status symbol or nil if not applicable
91
+ #
92
+ def worktree_status = nil
93
+
94
+ # Is the entry an ignored file?
95
+ #
96
+ # * Ignored entries are not considered untracked
97
+ #
98
+ # @example
99
+ # entry.ignored? #=> false
100
+ #
101
+ # @return [Boolean]
102
+ #
103
+ def ignored? = false
104
+
105
+ # Is the entry an untracked file?
106
+ #
107
+ # * Ignored entries are not considered untracked
108
+ #
109
+ # @example
110
+ # entry.ignored? #=> false
111
+ #
112
+ # @return [Boolean]
113
+ #
114
+ def untracked? = false
115
+
116
+ # Does the entry have unstaged changes in the worktree?
117
+ #
118
+ # * An entry can have both staged and unstaged changes
119
+ # * All untracked entries are considered unstaged
120
+ #
121
+ # @example
122
+ # entry.ignored? #=> false
123
+ #
124
+ # @return [Boolean]
125
+ #
126
+ def unstaged? = false
127
+
128
+ # Does the entry have staged changes in the index?
129
+ #
130
+ # * An entry can have both staged and unstaged changes
131
+ #
132
+ # @example
133
+ # entry.ignored? #=> false
134
+ #
135
+ # @return [Boolean]
136
+ #
137
+ def staged? = false
138
+
139
+ # Does the entry have staged changes in the index with no unstaged changes?
140
+ #
141
+ # * An entry can have both staged and unstaged changes
142
+ #
143
+ # @example
144
+ # entry.fully_staged? #=> false
145
+ #
146
+ # @return [Boolean]
147
+ #
148
+ def fully_staged? = staged? && !unstaged?
149
+
150
+ # Does the entry represent a merge conflict?
151
+ #
152
+ # * Merge conflicts are not considered untracked, staged or unstaged
153
+ #
154
+ # @example
155
+ # entry.conflict? #=> false
156
+ #
157
+ # @return [Boolean]
158
+ #
159
+ def unmerged? = false
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entry'
4
+
5
+ module RubyGit
6
+ module Status
7
+ # Represents an ignored file in git status
8
+ #
9
+ # @api public
10
+ class IgnoredEntry < Entry
11
+ # Parse a git status line to create an ignored entry
12
+ #
13
+ # @example
14
+ # IgnoredEntry.parse('!! lib/example.rb') #=> #<RubyGit::Status::IgnoredEntry:0x00000001046bd488 ...>
15
+ #
16
+ # @param line [String] line from git status
17
+ # @return [RubyGit::Status::IgnoredEntry] parsed entry
18
+ #
19
+ def self.parse(line)
20
+ tokens = line.split(' ', 2)
21
+ new(path: tokens[1])
22
+ end
23
+
24
+ # Initialize with the path
25
+ #
26
+ # @example
27
+ # IgnoredEntry.new(path: 'lib/example.rb')
28
+ #
29
+ # @param path [String] file path
30
+ #
31
+ def initialize(path:)
32
+ super(path)
33
+ end
34
+
35
+ # Is the entry an ignored file?
36
+ # @example
37
+ # entry.ignored? #=> false
38
+ # @return [Boolean]
39
+ def ignored?
40
+ true
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entry'
4
+ require_relative 'submodule_status'
5
+
6
+ module RubyGit
7
+ module Status
8
+ # Represents an ordinary changed file in git status
9
+ #
10
+ # @api public
11
+ class OrdinaryEntry < Entry
12
+ # @attribute [r] index_status
13
+ #
14
+ # The status in the staging area
15
+ #
16
+ # @example
17
+ # entry.index_status #=> :modified
18
+ #
19
+ # @return [Symbol] staging status
20
+ #
21
+ # @api public
22
+ attr_reader :index_status
23
+
24
+ # @attribute [r] worktree_status
25
+ #
26
+ # The status in the worktree
27
+ #
28
+ # @example
29
+ # entry.worktree_status #=> :modified
30
+ #
31
+ # @return [Symbol] worktree status
32
+ #
33
+ # @api public
34
+ attr_reader :worktree_status
35
+
36
+ # @attribute [r] submodule_status
37
+ #
38
+ # The submodule status if the entry is a submodule
39
+ #
40
+ # @example
41
+ # entry.submodule #=> 'N...'
42
+ #
43
+ # @return [SubmoduleStatus, nil] submodule status or nil if not a submodule
44
+ #
45
+ # @api public
46
+ attr_reader :submodule_status
47
+
48
+ # @attribute [r] head_sha
49
+ #
50
+ # The SHA of this object in HEAD
51
+ #
52
+ # @example
53
+ # entry.head_sha #=> 'd670460b4b4aece5915caf5c68d12f560a9fe3e4'
54
+ #
55
+ # @return [String] SHA of this object in HEAD
56
+ #
57
+ # @api public
58
+ attr_reader :head_sha
59
+
60
+ # @attribute [r] index_sha
61
+ #
62
+ # The SHA of this object in the index
63
+ #
64
+ # @example
65
+ # entry.index_sha #=> 'd670460b4b4aece5915caf5c68d12f560a9fe3e4'
66
+ #
67
+ # @return [String] SHA of this object in the index
68
+ #
69
+ # @api public
70
+ attr_reader :index_sha
71
+
72
+ # @attribute [r] head_mode
73
+ #
74
+ # The file mode in HEAD
75
+ #
76
+ # @example
77
+ # entry.head_mode #=> 0o100644
78
+ #
79
+ # @return [Integer] file mode in HEAD
80
+ #
81
+ # @api private
82
+ attr_reader :head_mode
83
+
84
+ # @attribute [r] index_mode
85
+ #
86
+ # The file mode in the index
87
+ #
88
+ # @example
89
+ # entry.index_mode #=> 0o100644
90
+ #
91
+ # @return [Integer] file mode in the index
92
+ #
93
+ # @api private
94
+ attr_reader :index_mode
95
+
96
+ # @attribute [r] worktree_mode
97
+ #
98
+ # The file mode in the worktree
99
+ #
100
+ # @example
101
+ # entry.worktree_mode #=> 0o100644
102
+ #
103
+ # @return [Integer] file mode in the worktree
104
+ #
105
+ # @api private
106
+ attr_reader :worktree_mode
107
+
108
+ # Parse an ordinary change line of git status output
109
+ #
110
+ # The line is expected to be in porcelain v2 format with NUL terminators.
111
+ #
112
+ # The format is as follows:
113
+ # 1 <XY> <sub> <mH> <mI> <mW> <hH> <hI> <path>
114
+ #
115
+ # @example
116
+ # line = '1 M N... 100644 100644 100644 d670460b4b4aece5915caf5c68d12f560a9fe3e4 ' \
117
+ # \d670460b4b4aece5915caf5c68d12f560a9fe3e4 lib/example.rb'
118
+ # OrdinaryEntry.parse(line) #=> #<RubyGit::Status::OrdinaryEntry:0x00000001046bd488 ...>
119
+ #
120
+ # @param line [String] line from git status
121
+ #
122
+ # @return [RubyGit::Status::OrdinaryEntry] parsed entry
123
+ #
124
+ def self.parse(line) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
125
+ tokens = line.split
126
+
127
+ new(
128
+ index_status: Entry.status_to_symbol(tokens[1][0]),
129
+ worktree_status: Entry.status_to_symbol(tokens[1][1]),
130
+ submodule_status: SubmoduleStatus.parse(tokens[2]),
131
+ head_mode: Integer(tokens[3], 8),
132
+ index_mode: Integer(tokens[4], 8),
133
+ worktree_mode: Integer(tokens[5], 8),
134
+ head_sha: tokens[6],
135
+ index_sha: tokens[7],
136
+ path: tokens[8]
137
+ )
138
+ end
139
+
140
+ # Initialize a new ordinary entry
141
+ #
142
+ # @example
143
+ # path = 'lib/example.rb'
144
+ # index_status = :modified
145
+ # worktree_status = :modified
146
+ # submodule_status = nil
147
+ # worktree_mode = 0o100644
148
+ # index_mode = 0o100644
149
+ # head_mode = 0o100644
150
+ # head_sha = 'd670460b4b4aece5915caf5c68d12f560a9fe3e4'
151
+ # index_sha = 'd670460b4b4aece5915caf5c68d12f560a9fe3e4'
152
+ # OrdinaryEntry.new(
153
+ # path:, index_status:, worktree_status:, submodule_status:,
154
+ # worktree_mode:, index_mode:, head_mode:, head_sha:, index_sha:
155
+ # )
156
+ #
157
+ # @param path [String] file path
158
+ # @param index_status [Symbol] status in staging area
159
+ # @param worktree_status [Symbol] status in worktree
160
+ # @param submodule_status [SubmoduleStatus, nil] submodule status if applicable
161
+ # @param worktree_mode [Integer] file mode in worktree
162
+ # @param index_mode [Integer] file mode in staging area
163
+ # @param head_mode [Integer] file mode in HEAD
164
+ #
165
+ def initialize( # rubocop:disable Metrics/ParameterLists
166
+ path:,
167
+ index_status:, worktree_status:,
168
+ submodule_status:,
169
+ head_mode:, index_mode:, worktree_mode:,
170
+ head_sha:, index_sha:
171
+ )
172
+ super(path)
173
+ @index_status = index_status
174
+ @worktree_status = worktree_status
175
+ @submodule_status = submodule_status
176
+ @worktree_mode = worktree_mode
177
+ @index_mode = index_mode
178
+ @head_mode = head_mode
179
+ @head_sha = head_sha
180
+ @index_sha = index_sha
181
+ end
182
+
183
+ # Does the entry have unstaged changes in the worktree?
184
+ #
185
+ # * An entry can have both staged and unstaged changes
186
+ # * All untracked entries are considered unstaged
187
+ #
188
+ # @example
189
+ # entry.ignored? #=> false
190
+ # @return [Boolean]
191
+ def unstaged?
192
+ worktree_status != :unmodified
193
+ end
194
+
195
+ # Does the entry have staged changes in the index?
196
+ #
197
+ # * An entry can have both staged and unstaged changes
198
+ #
199
+ # @example
200
+ # entry.ignored? #=> false
201
+ # @return [Boolean]
202
+ def staged?
203
+ index_status != :unmodified
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'report'
4
+ require_relative 'branch'
5
+ require_relative 'stash'
6
+ require_relative 'entry'
7
+ require_relative 'ordinary_entry'
8
+ require_relative 'renamed_entry'
9
+ require_relative 'unmerged_entry'
10
+ require_relative 'ignored_entry'
11
+ require_relative 'untracked_entry'
12
+
13
+ module RubyGit
14
+ module Status
15
+ # Parses the git status porcelain v2 format output
16
+ #
17
+ # git status --porcelain=v2 -z \
18
+ # --untracked-files --ignored-files --renames \
19
+ # --ahead-behind --branch --show-stash
20
+ #
21
+ # @api public
22
+ class Parser
23
+ # Parse the git status output and return a report object
24
+ #
25
+ # @example
26
+ # status_output = `git status -u --porcelain=v2 --renames --branch --show-stash -z`
27
+ # report = RubyGit::Status::Parser.parse(status_output) #=> #<RubyGit::Status::Report>
28
+ #
29
+ # @param status_output [String] raw git status output
30
+ #
31
+ # @return [RubyGit::Status::Report] parsed status report
32
+ #
33
+ def self.parse(status_output)
34
+ new(status_output).parse
35
+ end
36
+
37
+ # Create a new status output parser
38
+ #
39
+ # @example
40
+ # status_output = `git status -u --porcelain=v2 --renames --branch --show-stash -z`
41
+ # parser = RubyGit::Status::Parser.new(status_output)
42
+ #
43
+ # @param status_output [String] raw git status output
44
+ #
45
+ def initialize(status_output)
46
+ @status_output = status_output
47
+ @entries = []
48
+ @branch = nil
49
+ @stash = Stash.new(0)
50
+ end
51
+
52
+ # Parse the git status output
53
+ #
54
+ # @example
55
+ # status_output = `git status -u --porcelain=v2 --renames --branch --show-stash -z`
56
+ # parser = RubyGit::Status::Parser.new(status_output)
57
+ # parser.parse #=> #<RubyGit::Status::Report>
58
+ #
59
+ # @return [RubyGit::Status::Report] parsed status report
60
+ #
61
+ def parse
62
+ process_lines(status_output_lines)
63
+
64
+ Report.new(@branch, @stash, @entries)
65
+ end
66
+
67
+ # Define the parser for each line type
68
+ LINE_PARSER_FACTORY = {
69
+ '1' => ->(line) { OrdinaryEntry.parse(line) },
70
+ '2' => ->(line) { RenamedEntry.parse(line) },
71
+ 'u' => ->(line) { UnmergedEntry.parse(line) },
72
+ '!' => ->(line) { IgnoredEntry.parse(line) },
73
+ '?' => ->(line) { UntrackedEntry.parse(line) }
74
+ }.freeze
75
+
76
+ private
77
+
78
+ # Process the split lines of git status output
79
+ #
80
+ # @param lines [Array<String>] lines from git status output
81
+ # @return [void]
82
+ #
83
+ # @api private
84
+ def process_lines(lines)
85
+ # Use the LINE_PARSER_FACTORY to parse each line
86
+ # based on the first character like the code below
87
+ lines.each do |line|
88
+ next if line.empty?
89
+
90
+ if line.start_with?('#')
91
+ parse_header_line(line)
92
+ else
93
+ @entries << LINE_PARSER_FACTORY[line[0]].call(line)
94
+ end
95
+ end
96
+ end
97
+
98
+ # Parse a header line (starts with #)
99
+ #
100
+ # @param line [String] header line from git status
101
+ #
102
+ # @return [void]
103
+ #
104
+ # @api private
105
+ def parse_header_line(line)
106
+ tokens = line.split
107
+ header_type = tokens[1]
108
+
109
+ process_header(header_type, tokens)
110
+ end
111
+
112
+ # Split the status output into lines
113
+ #
114
+ # This is more complicated than a simple split because
115
+ # the lines are NUL terminated but some entries have
116
+ # also use NUL as a field separator (e.g. renamed entries).
117
+ #
118
+ # @return [Array<String>] split lines
119
+ #
120
+ # @api private
121
+ #
122
+ def status_output_lines
123
+ parts = @status_output.split("\u0000")
124
+ [].tap do |lines|
125
+ until parts.empty?
126
+ next_entry_type = parts.first[0]
127
+ lines << parts.shift
128
+ lines[-1] = "#{lines[-1]}\u0000#{parts.shift}" if next_entry_type == '2'
129
+ end
130
+ end
131
+ end
132
+
133
+ # Info from the status_output about the branch
134
+ # @return [Branch]
135
+ # @api private
136
+ def branch = @branch ||= Branch.new
137
+
138
+ # Info about the stashes about stashes
139
+ # @return [Stash]
140
+ # @api private
141
+ attr_reader :stash
142
+
143
+ # Branch name setter that handles detached HEAD
144
+ # @param name [String] branch name
145
+ # @return [void]
146
+ # @api private
147
+ def branch_name=(name)
148
+ branch.name = name unless name == '(detached)'
149
+ end
150
+
151
+ # Branch oid setter that handles initial commit
152
+ # @param oid [String] branch oid
153
+ # @return [void]
154
+ # @api private
155
+ def branch_oid=(oid)
156
+ branch.oid = oid unless oid == '(initial)'
157
+ end
158
+
159
+ # Branch upstream setter
160
+ # @param upstream [String] branch upstream
161
+ # @return [void]
162
+ # @api private
163
+ def branch_upstream=(upstream)
164
+ branch.upstream = upstream
165
+ end
166
+
167
+ # Branch ahead setter that converts from string to integer
168
+ # @param ahead [String] branch ahead
169
+ # @return [void]
170
+ # @api private
171
+ def branch_ahead=(ahead)
172
+ branch.ahead = ahead.sub('+', '').to_i
173
+ end
174
+
175
+ # Branch behind setter that converts from string to integer
176
+ # @param behind [String] branch behind
177
+ # @return [void]
178
+ # @api private
179
+ def branch_behind=(behind)
180
+ branch.behind = behind.sub('-', '').to_i
181
+ end
182
+
183
+ # Process a specific header type with its tokens
184
+ #
185
+ # @param header_type [String] type of header
186
+ # @param tokens [Array<String>] tokens from header line
187
+ # @return [void]
188
+ #
189
+ # @api private
190
+ def process_header(header_type, tokens)
191
+ case header_type
192
+ when 'branch.head' then self.branch_name = tokens[2]
193
+ when 'branch.oid' then self.branch_oid = tokens[2]
194
+ when 'branch.upstream' then self.branch_upstream = tokens[2]
195
+ when 'branch.ab'
196
+ self.branch_ahead = tokens[2]
197
+ self.branch_behind = tokens[3]
198
+ when 'stash' then stash.count = tokens[2].to_i
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end