gitt 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0299e137ff3816d95cdc4a92ccc4af9a47bacb19756e49ce4c3ad046d7dfe701'
4
+ data.tar.gz: 39829d79861e88f5e6d7af24f34202d518dbbc8faffe212503e340766a891342
5
+ SHA512:
6
+ metadata.gz: 932f1cd9202c325642f1e829ec7a7baffe5d3d1d364db1f7c3e807f0f4081998dbe0f0e9413d97d87241646595ed838cb72738164c89391be439ad83f1b647d2
7
+ data.tar.gz: f00e865926dbc62c1776fb6ecdbf62e31037b20259964c7c27985642d6693da1f7c16d0457be1ff605d7c51e2c0db3699b9c44626e95b1b334736a4370ffe9ff
checksums.yaml.gz.sig ADDED
Binary file
data/LICENSE.adoc ADDED
@@ -0,0 +1,134 @@
1
+ = Hippocratic License
2
+
3
+ Version: 2.1.0.
4
+
5
+ Purpose. The purpose of this License is for the Licensor named above to
6
+ permit the Licensee (as defined below) broad permission, if consistent
7
+ with Human Rights Laws and Human Rights Principles (as each is defined
8
+ below), to use and work with the Software (as defined below) within the
9
+ full scope of Licensor’s copyright and patent rights, if any, in the
10
+ Software, while ensuring attribution and protecting the Licensor from
11
+ liability.
12
+
13
+ Permission and Conditions. The Licensor grants permission by this
14
+ license ("License"), free of charge, to the extent of Licensor’s
15
+ rights under applicable copyright and patent law, to any person or
16
+ entity (the "Licensee") obtaining a copy of this software and
17
+ associated documentation files (the "Software"), to do everything with
18
+ the Software that would otherwise infringe (i) the Licensor’s copyright
19
+ in the Software or (ii) any patent claims to the Software that the
20
+ Licensor can license or becomes able to license, subject to all of the
21
+ following terms and conditions:
22
+
23
+ * Acceptance. This License is automatically offered to every person and
24
+ entity subject to its terms and conditions. Licensee accepts this
25
+ License and agrees to its terms and conditions by taking any action with
26
+ the Software that, absent this License, would infringe any intellectual
27
+ property right held by Licensor.
28
+ * Notice. Licensee must ensure that everyone who gets a copy of any part
29
+ of this Software from Licensee, with or without changes, also receives
30
+ the License and the above copyright notice (and if included by the
31
+ Licensor, patent, trademark and attribution notice). Licensee must cause
32
+ any modified versions of the Software to carry prominent notices stating
33
+ that Licensee changed the Software. For clarity, although Licensee is
34
+ free to create modifications of the Software and distribute only the
35
+ modified portion created by Licensee with additional or different terms,
36
+ the portion of the Software not modified must be distributed pursuant to
37
+ this License. If anyone notifies Licensee in writing that Licensee has
38
+ not complied with this Notice section, Licensee can keep this License by
39
+ taking all practical steps to comply within 30 days after the notice. If
40
+ Licensee does not do so, Licensee’s License (and all rights licensed
41
+ hereunder) shall end immediately.
42
+ * Compliance with Human Rights Principles and Human Rights Laws.
43
+ [arabic]
44
+ . Human Rights Principles.
45
+ [loweralpha]
46
+ .. Licensee is advised to consult the articles of the United Nations
47
+ Universal Declaration of Human Rights and the United Nations Global
48
+ Compact that define recognized principles of international human rights
49
+ (the "Human Rights Principles"). Licensee shall use the Software in a
50
+ manner consistent with Human Rights Principles.
51
+ .. Unless the Licensor and Licensee agree otherwise, any dispute,
52
+ controversy, or claim arising out of or relating to (i) Section 1(a)
53
+ regarding Human Rights Principles, including the breach of Section 1(a),
54
+ termination of this License for breach of the Human Rights Principles,
55
+ or invalidity of Section 1(a) or (ii) a determination of whether any Law
56
+ is consistent or in conflict with Human Rights Principles pursuant to
57
+ Section 2, below, shall be settled by arbitration in accordance with the
58
+ Hague Rules on Business and Human Rights Arbitration (the "Rules");
59
+ provided, however, that Licensee may elect not to participate in such
60
+ arbitration, in which event this License (and all rights licensed
61
+ hereunder) shall end immediately. The number of arbitrators shall be one
62
+ unless the Rules require otherwise.
63
+ +
64
+ Unless both the Licensor and Licensee agree to the contrary: (1) All
65
+ documents and information concerning the arbitration shall be public and
66
+ may be disclosed by any party; (2) The repository referred to under
67
+ Article 43 of the Rules shall make available to the public in a timely
68
+ manner all documents concerning the arbitration which are communicated
69
+ to it, including all submissions of the parties, all evidence admitted
70
+ into the record of the proceedings, all transcripts or other recordings
71
+ of hearings and all orders, decisions and awards of the arbitral
72
+ tribunal, subject only to the arbitral tribunal’s powers to take such
73
+ measures as may be necessary to safeguard the integrity of the arbitral
74
+ process pursuant to Articles 18, 33, 41 and 42 of the Rules; and (3)
75
+ Article 26(6) of the Rules shall not apply.
76
+ . Human Rights Laws. The Software shall not be used by any person or
77
+ entity for any systems, activities, or other uses that violate any Human
78
+ Rights Laws. "Human Rights Laws" means any applicable laws,
79
+ regulations, or rules (collectively, "Laws") that protect human,
80
+ civil, labor, privacy, political, environmental, security, economic, due
81
+ process, or similar rights; provided, however, that such Laws are
82
+ consistent and not in conflict with Human Rights Principles (a dispute
83
+ over the consistency or a conflict between Laws and Human Rights
84
+ Principles shall be determined by arbitration as stated above). Where
85
+ the Human Rights Laws of more than one jurisdiction are applicable or in
86
+ conflict with respect to the use of the Software, the Human Rights Laws
87
+ that are most protective of the individuals or groups harmed shall
88
+ apply.
89
+ . Indemnity. Licensee shall hold harmless and indemnify Licensor (and
90
+ any other contributor) against all losses, damages, liabilities,
91
+ deficiencies, claims, actions, judgments, settlements, interest, awards,
92
+ penalties, fines, costs, or expenses of whatever kind, including
93
+ Licensor’s reasonable attorneys’ fees, arising out of or relating to
94
+ Licensee’s use of the Software in violation of Human Rights Laws or
95
+ Human Rights Principles.
96
+ * Failure to Comply. Any failure of Licensee to act according to the
97
+ terms and conditions of this License is both a breach of the License and
98
+ an infringement of the intellectual property rights of the Licensor
99
+ (subject to exceptions under Laws, e.g., fair use). In the event of a
100
+ breach or infringement, the terms and conditions of this License may be
101
+ enforced by Licensor under the Laws of any jurisdiction to which
102
+ Licensee is subject. Licensee also agrees that the Licensor may enforce
103
+ the terms and conditions of this License against Licensee through
104
+ specific performance (or similar remedy under Laws) to the extent
105
+ permitted by Laws. For clarity, except in the event of a breach of this
106
+ License, infringement, or as otherwise stated in this License, Licensor
107
+ may not terminate this License with Licensee.
108
+ * Enforceability and Interpretation. If any term or provision of this
109
+ License is determined to be invalid, illegal, or unenforceable by a
110
+ court of competent jurisdiction, then such invalidity, illegality, or
111
+ unenforceability shall not affect any other term or provision of this
112
+ License or invalidate or render unenforceable such term or provision in
113
+ any other jurisdiction; provided, however, subject to a court
114
+ modification pursuant to the immediately following sentence, if any term
115
+ or provision of this License pertaining to Human Rights Laws or Human
116
+ Rights Principles is deemed invalid, illegal, or unenforceable against
117
+ Licensee by a court of competent jurisdiction, all rights in the
118
+ Software granted to Licensee shall be deemed null and void as between
119
+ Licensor and Licensee. Upon a determination that any term or provision
120
+ is invalid, illegal, or unenforceable, to the extent permitted by Laws,
121
+ the court may modify this License to affect the original purpose that
122
+ the Software be used in compliance with Human Rights Principles and
123
+ Human Rights Laws as closely as possible. The language in this License
124
+ shall be interpreted as to its fair meaning and not strictly for or
125
+ against any party.
126
+ * Disclaimer. TO THE FULL EXTENT ALLOWED BY LAW, THIS SOFTWARE COMES
127
+ "AS IS," WITHOUT ANY WARRANTY, EXPRESS OR IMPLIED, AND LICENSOR AND
128
+ ANY OTHER CONTRIBUTOR SHALL NOT BE LIABLE TO ANYONE FOR ANY DAMAGES OR
129
+ OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
130
+ OR THIS LICENSE, UNDER ANY KIND OF LEGAL CLAIM.
131
+
132
+ This Hippocratic License is an link:https://ethicalsource.dev[Ethical Source license] and is offered
133
+ for use by licensors and licensees at their own risk, on an "AS IS" basis, and with no warranties
134
+ express or implied, to the maximum extent permitted by Laws.
data/README.adoc ADDED
@@ -0,0 +1,299 @@
1
+ :toc: macro
2
+ :toclevels: 5
3
+ :figure-caption!:
4
+
5
+ :git_link: link:https://git-scm.com[Git]
6
+ :struct_link: link:https://www.alchemists.io/articles/ruby_structs[Struct]
7
+
8
+ = Gitt
9
+
10
+ Provides a monadic Object API to the {git_link} CLI. This project is also an extraction of work originally implemented within the following projects:
11
+
12
+ * link:https://www.alchemists.io/projects/git-lint[Git Lint]
13
+ * link:https://www.alchemists.io/projects/milestoner[Milestoner]
14
+ * link:https://www.alchemists.io/projects/rubysmith[Rubysmith]
15
+
16
+ If you are looking for alternatives within this space, you might find the following gems of benefit too:
17
+
18
+ * link:https://github.com/ruby-git/ruby-git[Ruby Git]
19
+ * link:https://github.com/libgit2/rugged[Rugged]
20
+
21
+ toc::[]
22
+
23
+ == Features
24
+
25
+ * Wraps all Git commands with additional enhancements to improve your working experience.
26
+ * Answers link:https://dry-rb.org/gems/dry-monads[monads] so you can pipe commands together for more powerful and complex workflows.
27
+
28
+ == Requirements
29
+
30
+ . {git_link}
31
+ . link:https://www.ruby-lang.org[Ruby]
32
+
33
+ == Setup
34
+
35
+ To set up the project, run:
36
+
37
+ [source,bash]
38
+ ----
39
+ bin/setup
40
+ ----
41
+
42
+ == Usage
43
+
44
+ At a high level, this project provides a centralized Object API via a single object: `Repository`. Example:
45
+
46
+ [source,ruby]
47
+ ----
48
+ git = Gitt::Repository.new
49
+
50
+ git.branch # Equivalent to `git branch <arguments>`.
51
+ git.branch_default # Answers default branch.
52
+ git.branch_name # Answers current branch.
53
+ git.call # Allows you to run any Git command.
54
+ git.commits # Answers commit records.
55
+ git.config # Equivalent to `git config <arguments>`.
56
+ git.exist? # Answers if current directory is a Git repository or not.
57
+ git.get # Equivalent to `git config get`.
58
+ git.log # Equivalent to `git log <arguments>`.
59
+ git.origin? # Answers if repository has an origin or not.
60
+ git.set # Equivalent to `get config set`.
61
+ git.tag # Equivalent to `git tag <arguments>`.
62
+ git.tag? # Answers if local or remote tag exists.
63
+ git.tag_create # Create a new tag.
64
+ git.tag_last # Anwswers last tag created.
65
+ git.tag_local? # Answers if local tag exists?
66
+ git.tag_parse # Parses and answers a tag record.
67
+ git.tag_remote? # Answers if remote tag exists?
68
+ git.tagged? # Answers if the repository has any tags.
69
+ git.tags_push # Pushes local tags to remote git.
70
+ git.uncommitted # Parses a file and answers an unsaved commit message.
71
+ ----
72
+
73
+ === Commands
74
+
75
+ Should you want to use individual commands instead of interacting with the `Repository` object, you
76
+ can leverage any of the objects in the `Commands` namespace which -- at a minimum -- use the link:https://www.alchemists.io/articles/interactor_pattern[Command Pattern]. Here are the _select_ commands which are enhanced further:
77
+
78
+ ==== link:https://git-scm.com/docs/git-branch[Branch]
79
+
80
+ Handles branches.
81
+
82
+ [source,ruby]
83
+ ----
84
+ branch = Gitt::Commands::Branch.new
85
+
86
+ # Answers branch default (via Git `init.defaultBranch` configuration).
87
+ branch.default # Success "main"
88
+
89
+ # Accepts any argument you'd send to `git branch`. Example:
90
+ branch.call "--list" # Success " main\n"
91
+
92
+ # Answers current branch
93
+ branch.name # Success "major"
94
+ ----
95
+
96
+ ==== link:https://git-scm.com/docs/git-config[Config]
97
+
98
+ Handles global and local configurations.
99
+
100
+ [source,ruby]
101
+ ----
102
+ config = Gitt::Commands::Config.new
103
+
104
+ # Accepts any argument you'd send to `git config`. Example:
105
+ config.call "--get", "rebase.abbreviateCommands" # Success "true\n"
106
+
107
+ # Answers value for key with support for fallback value or block manipulation.
108
+ config.get "user.name" # Success "Brooke Kuhlmann"
109
+ config.get "user.unknown", "fallback" # Success "fallback"
110
+ config.get("user.unknown") { |value| value + "fallback" } # "fallback"
111
+
112
+ # Answers true or false if origin is defined.
113
+ config.origin? # true
114
+
115
+ # Sets configuration key and value.
116
+ config.set "user.demo", "test" # Success "test"
117
+ ----
118
+
119
+ ==== link:https://git-scm.com/docs/git-log[Log]
120
+
121
+ Handles commit history.
122
+
123
+ [source,ruby]
124
+ ----
125
+ log = Gitt::Commands::Log.new
126
+
127
+ log.call "--oneline", "-1" # Success "5e21a9866827 Added documentation\n"
128
+ ----
129
+
130
+ The `Log` class provides two other methods but they require a more detailed explanation. The first is `Log#all` which answers an array of commits (records) upon success and accepts the same arguments as given to `#call`.
131
+
132
+ [source,ruby]
133
+ ----
134
+ commit = log.all
135
+ ----
136
+
137
+ The second, is:
138
+
139
+ [source,ruby]
140
+ ----
141
+ commit log.uncommitted ".git/COMMIT_EDITMSG"
142
+ ----
143
+
144
+ The above will answer a single commit record. This is great for building a commit object from an unsaved commit message. The only disadvantage of this approach is that you will get template commits which are always stripped out by Git when processing a _saved_ commit.
145
+
146
+ ==== link:https://git-scm.com/docs/git-tag[Tag]
147
+
148
+ Handles the tagging/versioning of commits.
149
+
150
+ [source,ruby]
151
+ ----
152
+ tag = Gitt::Commands::Tag.new
153
+
154
+ # Accepts any argument you'd send to `git tag`.
155
+ # Example: tag.call "--list"
156
+ stdout, stderr, status = tag.call
157
+
158
+ # Answers true or false base on whether local and remote tag exist.
159
+ tag.exist? "0.1.0"
160
+
161
+ # Answers last tag for git.
162
+ tag.last
163
+
164
+ # Answers if local tag exists.
165
+ tag.local? "0.1.0"
166
+
167
+ # Pushes tags to remote git.
168
+ tag.push
169
+
170
+ # Answers if remote tag exists.
171
+ tag.remote? "0.1.0"
172
+
173
+ # Answers true or false based on whether repository is tagged.
174
+ tag.tagged?
175
+ ----
176
+
177
+ === Models
178
+
179
+ In order to have access to rich data from the Git client, there are several models available to you.
180
+
181
+ ==== Commit
182
+
183
+ An instance of `Gitt::Commits::Model` is what is answered back to when using `Gitt::Repository` via the `#commits` or `#uncommitted` methods. In each case, you'll either get an array of records, a single record, or a failure depending on the result. Here's an example of a single record:
184
+
185
+ [source,ruby]
186
+ ----
187
+ # #<struct Gitt::Commits::Model
188
+ # author_date_relative="2 days ago",
189
+ # author_email="demo@example.com",
190
+ # author_name="Demo User",
191
+ # body="Necessary to explain recent changes.\n",
192
+ # body_lines=["Necessary to explain recent changes."],
193
+ # body_paragraphs=["Necessary to explain recent changes."],
194
+ # message="Updated documentation with new functionality\n\nNecessary to explain recent changes.\n",
195
+ # sha="5e21a9866827bf5c68bd445ea01b3837a3936b45",
196
+ # subject="Updated documentation with new functionality",
197
+ # trailers=[],
198
+ # trailers_index=nil>
199
+ ----
200
+
201
+ You get a {struct_link} with the following attributes:
202
+
203
+ * `author_date_relative`: Stores the relative date of when the commit was made.
204
+ * `author_email`: Stores the author email.
205
+ * `author_name`: Stores the author name.
206
+ * `body`: Stores the commit body which excludes the subject and leading space.
207
+ * `body_lines`: Stores each line of the body in an array.
208
+ * `body_paragraphs`: Stores each paragraph of the body as an array (i.e. broken by double new lines).
209
+ * `message`: Stores the entire, raw, commit message (i.e. subject and body).
210
+ * `sha`: Stores the commit SHA.
211
+ * `subject`: Stores the commit subject.
212
+ * `trailers`: Stores any commit trailers as an array of `GtiPlus::Trailers::Model` records.
213
+ * `trailers_index`: Stores the starting index of trailers within the commit message.
214
+
215
+ ==== Tag
216
+
217
+ An instance of `Gitt::Tags::Model` is what is answered back to when using `Gitt::Repository` via the `#tag_parse` method. Here's an example:
218
+
219
+ [source,ruby]
220
+ ----
221
+ # #<struct Gitt::Tags::Model
222
+ # author_date="Tue Dec 29 17:33:01 2020 -0700",
223
+ # author_email="demo@example.com",
224
+ # author_name="Demo User",
225
+ # body="- Added gem skeleton\n- Added RSpec dependnecy",
226
+ # sha="d041d07c29f97b5b06b3b2fd05fa1dd018c7da7c",
227
+ # subject="Version 0.1.0",
228
+ # version="0.1.0">
229
+ ----
230
+
231
+ You get a {struct_link} with the following attributes:
232
+
233
+ * `author_date`: Stores author creation date.
234
+ * `author_email`: Stores author email.
235
+ * `author_name`: Store author name.
236
+ * `body`: Stores body of tag which can be sentences, multiple paragraphs, and/or signature information.
237
+ * `sha`: Stores the commit SHA for which this tag labels
238
+ * `subject`: Stores the subject.
239
+ * `version`: Stores the version.
240
+
241
+ ==== Trailer
242
+
243
+ A trailer is nested within a commit record when trailer information exists. Example:
244
+
245
+ [source,ruby]
246
+ ----
247
+ #<struct Gitt::Commits::Trailers::Model key="Issue", delimiter=":", space=" ", value="123">
248
+ ----
249
+
250
+ The attributes break down as follows:
251
+
252
+ * `key`: Answers the key.
253
+ * `delimiter`: Answers the delimiter which must be a colon but can be missing if invalid.
254
+ * `space`: Answers either a space or an empty string with the former being invalid.
255
+ * `value`: Answers the value associated with the key.
256
+
257
+ == Development
258
+
259
+ To contribute, run:
260
+
261
+ [source,bash]
262
+ ----
263
+ git clone https://github.com/bkuhlmann/gitt
264
+ cd gitt
265
+ bin/setup
266
+ ----
267
+
268
+ You can also use the IRB console for direct access to all objects:
269
+
270
+ [source,bash]
271
+ ----
272
+ bin/console
273
+ ----
274
+
275
+ == Tests
276
+
277
+ To test, run:
278
+
279
+ [source,bash]
280
+ ----
281
+ bundle exec rake
282
+ ----
283
+
284
+ == link:https://www.alchemists.io/policies/license[License]
285
+
286
+ == link:https://www.alchemists.io/policies/security[Security]
287
+
288
+ == link:https://www.alchemists.io/policies/code_of_conduct[Code of Conduct]
289
+
290
+ == link:https://www.alchemists.io/policies/contributions[Contributions]
291
+
292
+ == link:https://www.alchemists.io/projects/gitt/versions[Versions]
293
+
294
+ == link:https://www.alchemists.io/community[Community]
295
+
296
+ == Credits
297
+
298
+ * Built with link:https://www.alchemists.io/projects/gemsmith[Gemsmith].
299
+ * Engineered by link:https://www.alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].
data/gitt.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "gitt"
5
+ spec.version = "1.0.0"
6
+ spec.authors = ["Brooke Kuhlmann"]
7
+ spec.email = ["brooke@alchemists.io"]
8
+ spec.homepage = "https://www.alchemists.io/projects/gitt"
9
+ spec.summary = "Provides a monadic Object API wrapper around the Git CLI."
10
+ spec.license = "Hippocratic-2.1"
11
+
12
+ spec.metadata = {
13
+ "bug_tracker_uri" => "https://github.com/bkuhlmann/gitt/issues",
14
+ "changelog_uri" => "https://www.alchemists.io/projects/gitt/versions",
15
+ "documentation_uri" => "https://www.alchemists.io/projects/gitt",
16
+ "funding_uri" => "https://github.com/sponsors/bkuhlmann",
17
+ "label" => "Gitt",
18
+ "rubygems_mfa_required" => "true",
19
+ "source_code_uri" => "https://github.com/bkuhlmann/gitt"
20
+ }
21
+
22
+ spec.signing_key = Gem.default_key_path
23
+ spec.cert_chain = [Gem.default_cert_path]
24
+
25
+ spec.required_ruby_version = "~> 3.2"
26
+ spec.add_dependency "dry-monads", "~> 1.6"
27
+ spec.add_dependency "refinements", "~> 10.0"
28
+ spec.add_dependency "zeitwerk", "~> 2.6"
29
+
30
+ spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
31
+ spec.files = Dir["*.gemspec", "lib/**/*"]
32
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gitt
4
+ module Commands
5
+ # A Git branch command wrapper.
6
+ class Branch
7
+ def initialize shell: SHELL
8
+ @shell = shell
9
+ end
10
+
11
+ def default
12
+ shell.call("config", "init.defaultBranch")
13
+ .fmap(&:chomp)
14
+ .fmap { |name| name.empty? ? "main" : name }
15
+ end
16
+
17
+ def call(*arguments) = shell.call "branch", *arguments
18
+
19
+ def name = shell.call("rev-parse", "--abbrev-ref", "HEAD").fmap(&:chomp)
20
+
21
+ private
22
+
23
+ attr_reader :shell
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+
5
+ module Gitt
6
+ module Commands
7
+ # A Git config command wrapper.
8
+ class Config
9
+ include Dry::Monads[:result]
10
+
11
+ def initialize shell: SHELL
12
+ @shell = shell
13
+ end
14
+
15
+ def call(*arguments) = shell.call "config", *arguments
16
+
17
+ def get key, fallback = "", *arguments
18
+ call(*arguments, "--get", key).fmap(&:chomp)
19
+ .or do |error|
20
+ block_given? ? yield(error) : Success(fallback)
21
+ end
22
+ end
23
+
24
+ def origin? = !get("remote.origin.url").value_or(EMPTY_STRING).empty?
25
+
26
+ def set(key, value, *arguments) = call(*arguments, "--add", key, value).fmap { value }
27
+
28
+ private
29
+
30
+ attr_reader :shell
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads"
4
+
5
+ module Gitt
6
+ module Commands
7
+ # A Git log command wrapper.
8
+ class Log
9
+ include Dry::Monads[:result]
10
+
11
+ KEY_MAP = {
12
+ author_email: "%ae",
13
+ author_name: "%an",
14
+ authored_at: "%at",
15
+ authored_relative_at: "%ar",
16
+ body: "%b",
17
+ committed_at: "%ct",
18
+ committed_relative_at: "%cr",
19
+ committer_email: "%ce",
20
+ committer_name: "%cn",
21
+ raw: "%B",
22
+ sha: "%H",
23
+ signature: "%G?",
24
+ subject: "%s",
25
+ trailers: "%(trailers)"
26
+ }.freeze
27
+
28
+ def initialize shell: SHELL, key_map: KEY_MAP, parser: Parsers::Commit
29
+ @shell = shell
30
+ @key_map = key_map
31
+ @parser = parser
32
+ end
33
+
34
+ def call(*arguments) = shell.call "log", *arguments
35
+
36
+ def index *arguments
37
+ arguments.prepend(pretty_format)
38
+ .then { |pretty_format| call(*pretty_format) }
39
+ .fmap { |content| String(content).scrub("?").split %("\n") }
40
+ .fmap { |entries| build_records entries }
41
+ end
42
+
43
+ def uncommitted path
44
+ return Failure %(Invalid commit message path: "#{path}".) unless path.exist?
45
+
46
+ shell.call("mktree")
47
+ .bind { |raw_sha| shell.call "commit-tree", "-F", path.to_s, raw_sha.chomp }
48
+ .bind { |sha| index "-1", sha.chomp }
49
+ .fmap(&:first)
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :shell, :key_map, :parser
55
+
56
+ def pretty_format
57
+ key_map.reduce("") { |string, (key, value)| string + "<#{key}>#{value}</#{key}>%n" }
58
+ .then { |structure| %(--pretty=format:"#{structure}") }
59
+ end
60
+
61
+ def build_records(entries) = entries.map { |entry| parser.call(entry) }
62
+ end
63
+ end
64
+ end