git 4.3.2 → 5.0.0.beta.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 +4 -4
- data/.github/copilot-instructions.md +67 -2705
- data/.github/pull_request_template.md +3 -1
- data/.github/skills/breaking-change-analysis/SKILL.md +102 -0
- data/.github/skills/ci-cd-troubleshooting/SKILL.md +264 -0
- data/.github/skills/command-implementation/REFERENCE.md +993 -0
- data/.github/skills/command-implementation/SKILL.md +229 -0
- data/.github/skills/command-test-conventions/SKILL.md +660 -0
- data/.github/skills/command-yard-documentation/SKILL.md +426 -0
- data/.github/skills/dependency-management/SKILL.md +72 -0
- data/.github/skills/development-workflow/SKILL.md +506 -0
- data/.github/skills/extract-command-from-lib/SKILL.md +487 -0
- data/.github/skills/extract-facade-from-base-lib/SKILL.md +586 -0
- data/.github/skills/facade-implementation/REFERENCE.md +840 -0
- data/.github/skills/facade-implementation/SKILL.md +260 -0
- data/.github/skills/facade-test-conventions/SKILL.md +380 -0
- data/.github/skills/facade-yard-documentation/SKILL.md +429 -0
- data/.github/skills/make-skill-template/SKILL.md +176 -0
- data/.github/skills/pr-readiness-review/SKILL.md +185 -0
- data/.github/skills/project-context/SKILL.md +313 -0
- data/.github/skills/pull-request-review/SKILL.md +168 -0
- data/.github/skills/refactor-command-to-commandlineresult/SKILL.md +131 -0
- data/.github/skills/release-management/SKILL.md +125 -0
- data/.github/skills/review-arguments-dsl/CHECKLIST.md +788 -0
- data/.github/skills/review-arguments-dsl/SKILL.md +214 -0
- data/.github/skills/review-backward-compatibility/SKILL.md +275 -0
- data/.github/skills/review-cross-command-consistency/SKILL.md +139 -0
- data/.github/skills/reviewing-skills/SKILL.md +189 -0
- data/.github/skills/rspec-unit-testing-standards/SKILL.md +639 -0
- data/.github/skills/tdd-refactor-step/SKILL.md +236 -0
- data/.github/skills/test-debugging/SKILL.md +160 -0
- data/.github/skills/yard-documentation/SKILL.md +793 -0
- data/.github/workflows/continuous_integration.yml +3 -2
- data/.github/workflows/enforce_conventional_commits.yml +1 -1
- data/.github/workflows/experimental_continuous_integration.yml +2 -2
- data/.github/workflows/release.yml +3 -4
- data/.gitignore +8 -0
- data/.husky/pre-commit +13 -0
- data/.release-please-manifest.json +1 -1
- data/.rspec +3 -0
- data/.rubocop.yml +7 -3
- data/.rubocop_todo.yml +23 -5
- data/.yardopts +1 -0
- data/CHANGELOG.md +0 -40
- data/CONTRIBUTING.md +694 -53
- data/README.md +17 -5
- data/Rakefile +61 -9
- data/commitlint.test +4 -0
- data/git.gemspec +14 -8
- data/lib/git/args_builder.rb +0 -8
- data/lib/git/base.rb +486 -410
- data/lib/git/branch.rb +380 -43
- data/lib/git/branch_delete_failure.rb +31 -0
- data/lib/git/branch_delete_result.rb +63 -0
- data/lib/git/branch_info.rb +178 -0
- data/lib/git/branches.rb +130 -24
- data/lib/git/command_line/base.rb +245 -0
- data/lib/git/command_line/capturing.rb +249 -0
- data/lib/git/command_line/result.rb +96 -0
- data/lib/git/command_line/streaming.rb +194 -0
- data/lib/git/command_line.rb +43 -322
- data/lib/git/command_line_result.rb +4 -88
- data/lib/git/commands/add.rb +131 -0
- data/lib/git/commands/am/abort.rb +43 -0
- data/lib/git/commands/am/apply.rb +252 -0
- data/lib/git/commands/am/continue.rb +43 -0
- data/lib/git/commands/am/quit.rb +43 -0
- data/lib/git/commands/am/retry.rb +47 -0
- data/lib/git/commands/am/show_current_patch.rb +64 -0
- data/lib/git/commands/am/skip.rb +42 -0
- data/lib/git/commands/am.rb +33 -0
- data/lib/git/commands/apply.rb +237 -0
- data/lib/git/commands/archive/list_formats.rb +46 -0
- data/lib/git/commands/archive.rb +140 -0
- data/lib/git/commands/arguments.rb +3510 -0
- data/lib/git/commands/base.rb +403 -0
- data/lib/git/commands/branch/copy.rb +94 -0
- data/lib/git/commands/branch/create.rb +173 -0
- data/lib/git/commands/branch/delete.rb +80 -0
- data/lib/git/commands/branch/list.rb +162 -0
- data/lib/git/commands/branch/move.rb +94 -0
- data/lib/git/commands/branch/set_upstream.rb +86 -0
- data/lib/git/commands/branch/show_current.rb +49 -0
- data/lib/git/commands/branch/unset_upstream.rb +57 -0
- data/lib/git/commands/branch.rb +34 -0
- data/lib/git/commands/cat_file/batch.rb +364 -0
- data/lib/git/commands/cat_file/filtered.rb +105 -0
- data/lib/git/commands/cat_file/raw.rb +210 -0
- data/lib/git/commands/cat_file.rb +49 -0
- data/lib/git/commands/checkout/branch.rb +151 -0
- data/lib/git/commands/checkout/files.rb +115 -0
- data/lib/git/commands/checkout.rb +38 -0
- data/lib/git/commands/checkout_index.rb +105 -0
- data/lib/git/commands/clean.rb +100 -0
- data/lib/git/commands/clone.rb +240 -0
- data/lib/git/commands/commit.rb +272 -0
- data/lib/git/commands/commit_tree.rb +100 -0
- data/lib/git/commands/config_option_syntax/add.rb +83 -0
- data/lib/git/commands/config_option_syntax/get.rb +117 -0
- data/lib/git/commands/config_option_syntax/get_all.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_color.rb +91 -0
- data/lib/git/commands/config_option_syntax/get_color_bool.rb +93 -0
- data/lib/git/commands/config_option_syntax/get_regexp.rb +115 -0
- data/lib/git/commands/config_option_syntax/get_urlmatch.rb +102 -0
- data/lib/git/commands/config_option_syntax/list.rb +107 -0
- data/lib/git/commands/config_option_syntax/remove_section.rb +74 -0
- data/lib/git/commands/config_option_syntax/rename_section.rb +78 -0
- data/lib/git/commands/config_option_syntax/replace_all.rb +104 -0
- data/lib/git/commands/config_option_syntax/set.rb +114 -0
- data/lib/git/commands/config_option_syntax/unset.rb +89 -0
- data/lib/git/commands/config_option_syntax/unset_all.rb +89 -0
- data/lib/git/commands/config_option_syntax.rb +56 -0
- data/lib/git/commands/describe.rb +155 -0
- data/lib/git/commands/diff.rb +656 -0
- data/lib/git/commands/diff_files.rb +518 -0
- data/lib/git/commands/diff_index.rb +496 -0
- data/lib/git/commands/fetch.rb +352 -0
- data/lib/git/commands/fsck.rb +136 -0
- data/lib/git/commands/gc.rb +132 -0
- data/lib/git/commands/grep.rb +338 -0
- data/lib/git/commands/init.rb +99 -0
- data/lib/git/commands/log.rb +632 -0
- data/lib/git/commands/ls_files.rb +191 -0
- data/lib/git/commands/ls_remote.rb +155 -0
- data/lib/git/commands/ls_tree.rb +131 -0
- data/lib/git/commands/maintenance/register.rb +75 -0
- data/lib/git/commands/maintenance/run.rb +104 -0
- data/lib/git/commands/maintenance/start.rb +66 -0
- data/lib/git/commands/maintenance/stop.rb +55 -0
- data/lib/git/commands/maintenance/unregister.rb +79 -0
- data/lib/git/commands/maintenance.rb +31 -0
- data/lib/git/commands/merge/abort.rb +44 -0
- data/lib/git/commands/merge/continue.rb +44 -0
- data/lib/git/commands/merge/quit.rb +46 -0
- data/lib/git/commands/merge/start.rb +245 -0
- data/lib/git/commands/merge.rb +28 -0
- data/lib/git/commands/merge_base.rb +86 -0
- data/lib/git/commands/mv.rb +77 -0
- data/lib/git/commands/name_rev.rb +114 -0
- data/lib/git/commands/pull.rb +377 -0
- data/lib/git/commands/push.rb +246 -0
- data/lib/git/commands/read_tree.rb +149 -0
- data/lib/git/commands/remote/add.rb +91 -0
- data/lib/git/commands/remote/get_url.rb +66 -0
- data/lib/git/commands/remote/list.rb +54 -0
- data/lib/git/commands/remote/prune.rb +61 -0
- data/lib/git/commands/remote/remove.rb +52 -0
- data/lib/git/commands/remote/rename.rb +69 -0
- data/lib/git/commands/remote/set_branches.rb +63 -0
- data/lib/git/commands/remote/set_head.rb +82 -0
- data/lib/git/commands/remote/set_url.rb +71 -0
- data/lib/git/commands/remote/set_url_add.rb +61 -0
- data/lib/git/commands/remote/set_url_delete.rb +64 -0
- data/lib/git/commands/remote/show.rb +71 -0
- data/lib/git/commands/remote/update.rb +72 -0
- data/lib/git/commands/remote.rb +42 -0
- data/lib/git/commands/repack.rb +277 -0
- data/lib/git/commands/reset.rb +147 -0
- data/lib/git/commands/rev_parse.rb +297 -0
- data/lib/git/commands/revert/abort.rb +45 -0
- data/lib/git/commands/revert/continue.rb +57 -0
- data/lib/git/commands/revert/quit.rb +47 -0
- data/lib/git/commands/revert/skip.rb +44 -0
- data/lib/git/commands/revert/start.rb +153 -0
- data/lib/git/commands/revert.rb +29 -0
- data/lib/git/commands/rm.rb +114 -0
- data/lib/git/commands/show.rb +632 -0
- data/lib/git/commands/show_ref/exclude_existing.rb +120 -0
- data/lib/git/commands/show_ref/exists.rb +78 -0
- data/lib/git/commands/show_ref/list.rb +145 -0
- data/lib/git/commands/show_ref/verify.rb +120 -0
- data/lib/git/commands/show_ref.rb +42 -0
- data/lib/git/commands/stash/apply.rb +75 -0
- data/lib/git/commands/stash/branch.rb +65 -0
- data/lib/git/commands/stash/clear.rb +41 -0
- data/lib/git/commands/stash/create.rb +58 -0
- data/lib/git/commands/stash/drop.rb +67 -0
- data/lib/git/commands/stash/list.rb +39 -0
- data/lib/git/commands/stash/pop.rb +78 -0
- data/lib/git/commands/stash/push.rb +103 -0
- data/lib/git/commands/stash/show.rb +149 -0
- data/lib/git/commands/stash/store.rb +63 -0
- data/lib/git/commands/stash.rb +38 -0
- data/lib/git/commands/status.rb +169 -0
- data/lib/git/commands/symbolic_ref/delete.rb +68 -0
- data/lib/git/commands/symbolic_ref/read.rb +95 -0
- data/lib/git/commands/symbolic_ref/update.rb +76 -0
- data/lib/git/commands/symbolic_ref.rb +38 -0
- data/lib/git/commands/tag/create.rb +139 -0
- data/lib/git/commands/tag/delete.rb +55 -0
- data/lib/git/commands/tag/list.rb +143 -0
- data/lib/git/commands/tag/verify.rb +71 -0
- data/lib/git/commands/tag.rb +26 -0
- data/lib/git/commands/update_ref/batch.rb +140 -0
- data/lib/git/commands/update_ref/delete.rb +92 -0
- data/lib/git/commands/update_ref/update.rb +106 -0
- data/lib/git/commands/update_ref.rb +42 -0
- data/lib/git/commands/version.rb +52 -0
- data/lib/git/commands/worktree/add.rb +140 -0
- data/lib/git/commands/worktree/list.rb +64 -0
- data/lib/git/commands/worktree/lock.rb +58 -0
- data/lib/git/commands/worktree/management_base.rb +51 -0
- data/lib/git/commands/worktree/move.rb +66 -0
- data/lib/git/commands/worktree/prune.rb +67 -0
- data/lib/git/commands/worktree/remove.rb +63 -0
- data/lib/git/commands/worktree/repair.rb +76 -0
- data/lib/git/commands/worktree/unlock.rb +47 -0
- data/lib/git/commands/worktree.rb +43 -0
- data/lib/git/commands/write_tree.rb +68 -0
- data/lib/git/commands.rb +89 -0
- data/lib/git/detached_head_info.rb +54 -0
- data/lib/git/diff.rb +297 -7
- data/lib/git/diff_file_numstat_info.rb +29 -0
- data/lib/git/diff_file_patch_info.rb +134 -0
- data/lib/git/diff_file_raw_info.rb +127 -0
- data/lib/git/diff_info.rb +169 -0
- data/lib/git/diff_path_status.rb +78 -19
- data/lib/git/diff_result.rb +32 -0
- data/lib/git/diff_stats.rb +59 -14
- data/lib/git/dirstat_info.rb +86 -0
- data/lib/git/errors.rb +65 -2
- data/lib/git/execution_context/global.rb +56 -0
- data/lib/git/execution_context/repository.rb +147 -0
- data/lib/git/execution_context.rb +482 -0
- data/lib/git/file_ref.rb +74 -0
- data/lib/git/fsck_object.rb +9 -9
- data/lib/git/fsck_result.rb +1 -1
- data/lib/git/lib.rb +1606 -1028
- data/lib/git/log.rb +15 -2
- data/lib/git/object.rb +92 -22
- data/lib/git/parsers/branch.rb +224 -0
- data/lib/git/parsers/cat_file.rb +111 -0
- data/lib/git/parsers/diff.rb +585 -0
- data/lib/git/parsers/fsck.rb +133 -0
- data/lib/git/parsers/grep.rb +42 -0
- data/lib/git/parsers/ls_tree.rb +58 -0
- data/lib/git/parsers/stash.rb +208 -0
- data/lib/git/parsers/tag.rb +257 -0
- data/lib/git/remote.rb +133 -9
- data/lib/git/repository/branching.rb +572 -0
- data/lib/git/repository/committing.rb +191 -0
- data/lib/git/repository/configuring.rb +156 -0
- data/lib/git/repository/diffing.rb +775 -0
- data/lib/git/repository/inspecting.rb +153 -0
- data/lib/git/repository/logging.rb +247 -0
- data/lib/git/repository/merging.rb +295 -0
- data/lib/git/repository/object_operations.rb +1101 -0
- data/lib/git/repository/path_resolver.rb +207 -0
- data/lib/git/repository/remote_operations.rb +753 -0
- data/lib/git/repository/shared_private.rb +51 -0
- data/lib/git/repository/staging.rb +390 -0
- data/lib/git/repository/stashing.rb +107 -0
- data/lib/git/repository/status_operations.rb +180 -0
- data/lib/git/repository/worktree_operations.rb +159 -0
- data/lib/git/repository.rb +264 -1
- data/lib/git/stash.rb +85 -4
- data/lib/git/stash_info.rb +104 -0
- data/lib/git/stashes.rb +130 -13
- data/lib/git/status.rb +224 -18
- data/lib/git/tag_delete_failure.rb +31 -0
- data/lib/git/tag_delete_result.rb +63 -0
- data/lib/git/tag_info.rb +105 -0
- data/lib/git/version.rb +109 -2
- data/lib/git/version_constraint.rb +81 -0
- data/lib/git/worktree.rb +120 -5
- data/lib/git/worktrees.rb +107 -7
- data/lib/git.rb +114 -18
- data/redesign/1_architecture_existing.md +54 -18
- data/redesign/2_architecture_redesign.md +365 -46
- data/redesign/3_architecture_implementation.md +1451 -54
- data/tasks/gem_tasks.rake +4 -0
- data/tasks/npm_tasks.rake +7 -0
- data/tasks/rspec.rake +48 -0
- data/tasks/test.rake +13 -1
- data/tasks/yard.rake +34 -7
- metadata +349 -20
- data/lib/git/index.rb +0 -6
- data/lib/git/path.rb +0 -38
- data/lib/git/working_directory.rb +0 -6
- /data/{release-please-config.json → .release-please-config.json} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/commands/ls_files'
|
|
4
|
+
require 'git/commands/rev_parse'
|
|
5
|
+
require 'git/escaped_path'
|
|
6
|
+
require 'git/status'
|
|
7
|
+
|
|
8
|
+
module Git
|
|
9
|
+
class Repository
|
|
10
|
+
# Facade methods for repository-status operations
|
|
11
|
+
#
|
|
12
|
+
# Provides methods for querying the state of the repository: checking
|
|
13
|
+
# whether any commits exist, listing untracked working-tree files, and
|
|
14
|
+
# listing files tracked in the index.
|
|
15
|
+
#
|
|
16
|
+
# Included by {Git::Repository}.
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
#
|
|
20
|
+
module StatusOperations
|
|
21
|
+
# Returns `true` if the repository has no commits yet
|
|
22
|
+
#
|
|
23
|
+
# Checks whether `HEAD` can be resolved to a commit object. A brand-new
|
|
24
|
+
# repository (or one created with `git checkout --orphan`) where no commit
|
|
25
|
+
# has been made yet will have no commits.
|
|
26
|
+
#
|
|
27
|
+
# @example Check whether a repository is empty
|
|
28
|
+
# repo.no_commits? #=> true # freshly initialized, no commits yet
|
|
29
|
+
# repo.no_commits? #=> false # at least one commit exists
|
|
30
|
+
#
|
|
31
|
+
# @return [Boolean] `true` when the repository has no commits, `false` otherwise
|
|
32
|
+
#
|
|
33
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status other
|
|
34
|
+
# than when the repository has no commits
|
|
35
|
+
#
|
|
36
|
+
def no_commits?
|
|
37
|
+
Git::Commands::RevParse.new(@execution_context).call('HEAD', verify: true)
|
|
38
|
+
false
|
|
39
|
+
rescue Git::FailedError => e
|
|
40
|
+
raise unless e.result.status.exitstatus == 128 &&
|
|
41
|
+
e.result.stderr == 'fatal: Needed a single revision'
|
|
42
|
+
|
|
43
|
+
true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# List all files in the working tree that are not tracked by git
|
|
47
|
+
#
|
|
48
|
+
# Runs `git ls-files --others --exclude-standard` from the working tree
|
|
49
|
+
# root and returns an array of repository-relative file paths. Files that
|
|
50
|
+
# match `.gitignore` or other standard exclusion rules are omitted.
|
|
51
|
+
#
|
|
52
|
+
# @example Get untracked files
|
|
53
|
+
# repo.untracked_files #=> ["new_feature.rb", "tmp/debug.log"]
|
|
54
|
+
#
|
|
55
|
+
# @example No untracked files
|
|
56
|
+
# repo.untracked_files #=> []
|
|
57
|
+
#
|
|
58
|
+
# @return [Array<String>] repository-relative paths of untracked,
|
|
59
|
+
# non-ignored files; empty when there are none
|
|
60
|
+
#
|
|
61
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
62
|
+
#
|
|
63
|
+
def untracked_files
|
|
64
|
+
Git::Commands::LsFiles.new(@execution_context).call(
|
|
65
|
+
others: true, exclude_standard: true, chdir: @execution_context.git_work_dir
|
|
66
|
+
).stdout.split("\n").map { |f| Private.unescape_quoted_path(f) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns a {Git::Status} object describing the working tree and index state
|
|
70
|
+
#
|
|
71
|
+
# Constructs a {Git::Status} for this repository by collecting information from
|
|
72
|
+
# `git ls-files --stage`, `git ls-files --others`, `git diff-files`, and
|
|
73
|
+
# `git diff-index HEAD` (the last only when at least one commit exists). The
|
|
74
|
+
# result identifies which files have been modified, added, deleted, or are
|
|
75
|
+
# untracked.
|
|
76
|
+
#
|
|
77
|
+
# @example Check which files are modified
|
|
78
|
+
# repo.status.changed #=> { "lib/foo.rb" => <Git::Status::StatusFile ...> }
|
|
79
|
+
#
|
|
80
|
+
# @example Check for untracked files
|
|
81
|
+
# repo.status.untracked #=> { "new_file.rb" => <Git::Status::StatusFile ...> }
|
|
82
|
+
#
|
|
83
|
+
# @example Iterate over all status files
|
|
84
|
+
# repo.status.each { |file| puts "#{file.path}: #{file.type}" }
|
|
85
|
+
#
|
|
86
|
+
# @return [Git::Status] the status of the repository
|
|
87
|
+
#
|
|
88
|
+
# @raise [Git::FailedError] if any underlying git command exits with a
|
|
89
|
+
# non-zero exit status
|
|
90
|
+
#
|
|
91
|
+
def status
|
|
92
|
+
Git::Status.new(self)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# List all files tracked in the index
|
|
96
|
+
#
|
|
97
|
+
# Runs `git ls-files --stage` under the given `location` and returns a
|
|
98
|
+
# hash keyed by file path with per-file index metadata.
|
|
99
|
+
#
|
|
100
|
+
# @example List all indexed files in the working tree
|
|
101
|
+
# repo.ls_files
|
|
102
|
+
# #=> { "README.md" => { path: "README.md", mode_index: "100644",
|
|
103
|
+
# #=> sha_index: "abc123...", stage: "0" }, ... }
|
|
104
|
+
#
|
|
105
|
+
# @example List indexed files under a specific directory
|
|
106
|
+
# repo.ls_files('lib/')
|
|
107
|
+
# #=> { "lib/git.rb" => { path: "lib/git.rb", ... }, ... }
|
|
108
|
+
#
|
|
109
|
+
# @param location [String, nil] the path to restrict the listing to;
|
|
110
|
+
# defaults to `'.'` (all tracked files) when `nil`
|
|
111
|
+
#
|
|
112
|
+
# @return [Hash{String => Hash}] a hash of index entries keyed by file path
|
|
113
|
+
#
|
|
114
|
+
# Each value is a Hash with the following keys:
|
|
115
|
+
# * `:path` [String] the file path
|
|
116
|
+
# * `:mode_index` [String] the file's index mode (e.g. `"100644"`)
|
|
117
|
+
# * `:sha_index` [String] the file's index SHA
|
|
118
|
+
# * `:stage` [String] the merge stage (`"0"` for normal entries)
|
|
119
|
+
#
|
|
120
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
121
|
+
#
|
|
122
|
+
def ls_files(location = nil)
|
|
123
|
+
location ||= '.'
|
|
124
|
+
{}.tap do |files|
|
|
125
|
+
Git::Commands::LsFiles.new(@execution_context).call(location, stage: true).stdout.split("\n").each do |line|
|
|
126
|
+
info, file = Private.split_status_line(line)
|
|
127
|
+
mode, sha, stage = info.split
|
|
128
|
+
files[file] = { path: file, mode_index: mode, sha_index: sha, stage: stage }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Private helpers local to {Git::Repository::StatusOperations}
|
|
134
|
+
#
|
|
135
|
+
# @api private
|
|
136
|
+
#
|
|
137
|
+
module Private
|
|
138
|
+
module_function
|
|
139
|
+
|
|
140
|
+
# Split a tab-delimited status line from `git ls-files --stage` output
|
|
141
|
+
#
|
|
142
|
+
# The output format is `<mode> <sha> <stage>\t<file>`. Quoted file paths
|
|
143
|
+
# (which git uses when the path contains non-ASCII or special characters)
|
|
144
|
+
# are unescaped before being returned. `line` is assumed to be non-empty
|
|
145
|
+
# because `git ls-files --stage` never emits blank lines.
|
|
146
|
+
#
|
|
147
|
+
# @param line [String] a single line of git ls-files output
|
|
148
|
+
#
|
|
149
|
+
# @return [Array<String>] the tab-delimited parts with the last part
|
|
150
|
+
# unescaped when it was git-quoted
|
|
151
|
+
#
|
|
152
|
+
def split_status_line(line)
|
|
153
|
+
parts = line.split("\t")
|
|
154
|
+
parts[-1] = unescape_quoted_path(parts[-1])
|
|
155
|
+
parts
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Unescape a git-quoted path
|
|
159
|
+
#
|
|
160
|
+
# Git wraps paths containing non-ASCII or special characters in
|
|
161
|
+
# double-quotes and octal-escapes each byte. This method strips the
|
|
162
|
+
# surrounding quotes and delegates unescaping to {Git::EscapedPath}.
|
|
163
|
+
#
|
|
164
|
+
# @param path [String] the path as it appears in git output
|
|
165
|
+
#
|
|
166
|
+
# @return [String] the unescaped path
|
|
167
|
+
#
|
|
168
|
+
def unescape_quoted_path(path)
|
|
169
|
+
if path.start_with?('"') && path.end_with?('"')
|
|
170
|
+
Git::EscapedPath.new(path[1..-2]).unescape
|
|
171
|
+
else
|
|
172
|
+
path
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private_constant :Private
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'git/commands/worktree'
|
|
4
|
+
require 'git/worktree'
|
|
5
|
+
require 'git/worktrees'
|
|
6
|
+
|
|
7
|
+
module Git
|
|
8
|
+
class Repository
|
|
9
|
+
# Facade methods for worktree operations
|
|
10
|
+
#
|
|
11
|
+
# Included by {Git::Repository}.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
#
|
|
15
|
+
module WorktreeOperations
|
|
16
|
+
# Returns all worktrees as an array of directory and SHA pairs
|
|
17
|
+
#
|
|
18
|
+
# Lists all worktrees attached to the repository, including the main
|
|
19
|
+
# worktree and all linked worktrees.
|
|
20
|
+
#
|
|
21
|
+
# @example List all worktrees
|
|
22
|
+
# repo.worktrees_all
|
|
23
|
+
# #=> [["/path/to/main", "4bef5ab..."], ["/tmp/worktree-1", "b8c6320..."]]
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Array(String, String)>] array of `[directory, sha]` pairs
|
|
26
|
+
#
|
|
27
|
+
# `directory` is the worktree path reported by git (absolute or relative,
|
|
28
|
+
# depending on repository configuration); `sha` is the full SHA of the
|
|
29
|
+
# checked-out HEAD commit
|
|
30
|
+
#
|
|
31
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
32
|
+
#
|
|
33
|
+
# @see https://git-scm.com/docs/git-worktree git-worktree documentation
|
|
34
|
+
#
|
|
35
|
+
def worktrees_all
|
|
36
|
+
worktree_entries = []
|
|
37
|
+
current_directory = ''
|
|
38
|
+
command_output = Git::Commands::Worktree::List.new(@execution_context).call(porcelain: true).stdout
|
|
39
|
+
|
|
40
|
+
command_output.each_line(chomp: true) do |line|
|
|
41
|
+
key, value = line.split(' ', 2)
|
|
42
|
+
current_directory = value if key == 'worktree'
|
|
43
|
+
worktree_entries << [current_directory, value] if key == 'HEAD'
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
worktree_entries
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Create a new linked worktree at the given directory
|
|
50
|
+
#
|
|
51
|
+
# @example Create a worktree at a path (auto-creates a branch)
|
|
52
|
+
# repo.worktree_add('/tmp/feature')
|
|
53
|
+
#
|
|
54
|
+
# @example Create a worktree and check out an existing commitish
|
|
55
|
+
# repo.worktree_add('/tmp/hotfix', 'main')
|
|
56
|
+
#
|
|
57
|
+
# @param dir [String] filesystem path for the new worktree
|
|
58
|
+
#
|
|
59
|
+
# @param commitish [String, nil] branch, tag, or commit to check out
|
|
60
|
+
#
|
|
61
|
+
# When `nil`, git creates a new branch named after the final path component
|
|
62
|
+
#
|
|
63
|
+
# @return [String] the output from the git worktree add command
|
|
64
|
+
#
|
|
65
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
66
|
+
#
|
|
67
|
+
# @see https://git-scm.com/docs/git-worktree git-worktree documentation
|
|
68
|
+
#
|
|
69
|
+
def worktree_add(dir, commitish = nil)
|
|
70
|
+
args = [dir]
|
|
71
|
+
args << commitish unless commitish.nil?
|
|
72
|
+
|
|
73
|
+
Git::Commands::Worktree::Add.new(@execution_context).call(*args).stdout
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Remove a linked worktree
|
|
77
|
+
#
|
|
78
|
+
# @example Remove a worktree
|
|
79
|
+
# repo.worktree_remove('/tmp/feature')
|
|
80
|
+
#
|
|
81
|
+
# @param dir [String] filesystem path of the worktree to remove
|
|
82
|
+
#
|
|
83
|
+
# @return [String] the output from the git worktree remove command
|
|
84
|
+
# (typically empty)
|
|
85
|
+
#
|
|
86
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
87
|
+
#
|
|
88
|
+
# @see https://git-scm.com/docs/git-worktree git-worktree documentation
|
|
89
|
+
#
|
|
90
|
+
def worktree_remove(dir)
|
|
91
|
+
Git::Commands::Worktree::Remove.new(@execution_context).call(dir).stdout
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Prune stale worktree administrative files
|
|
95
|
+
#
|
|
96
|
+
# Removes stale administrative files from `$GIT_DIR/worktrees`. A
|
|
97
|
+
# worktree becomes stale when its directory no longer exists on disk.
|
|
98
|
+
#
|
|
99
|
+
# @example Prune stale worktrees
|
|
100
|
+
# repo.worktree_prune
|
|
101
|
+
#
|
|
102
|
+
# @return [String] the output from the git worktree prune command
|
|
103
|
+
# (typically empty)
|
|
104
|
+
#
|
|
105
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
106
|
+
#
|
|
107
|
+
# @see https://git-scm.com/docs/git-worktree git-worktree documentation
|
|
108
|
+
#
|
|
109
|
+
def worktree_prune
|
|
110
|
+
Git::Commands::Worktree::Prune.new(@execution_context).call.stdout
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Return a {Git::Worktree} object for the given directory and optional commitish
|
|
114
|
+
#
|
|
115
|
+
# This is a factory method — it constructs the domain object but does not
|
|
116
|
+
# immediately execute any git commands.
|
|
117
|
+
#
|
|
118
|
+
# @example Get a worktree object for a new path
|
|
119
|
+
# wt = repo.worktree('/tmp/feature')
|
|
120
|
+
#
|
|
121
|
+
# @example Get a worktree object for a specific branch or commit
|
|
122
|
+
# wt = repo.worktree('/tmp/hotfix', 'main')
|
|
123
|
+
#
|
|
124
|
+
# @param dir [String] filesystem path for the worktree
|
|
125
|
+
#
|
|
126
|
+
# @param commitish [String, nil] branch, tag, or commit to associate with
|
|
127
|
+
# the worktree; `nil` means no commitish is specified
|
|
128
|
+
#
|
|
129
|
+
# @return [Git::Worktree] a worktree domain object for the given path
|
|
130
|
+
#
|
|
131
|
+
def worktree(dir, commitish = nil)
|
|
132
|
+
Git::Worktree.new(self, dir, commitish)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Return a {Git::Worktrees} collection of all worktrees (main and linked)
|
|
136
|
+
#
|
|
137
|
+
# The collection is populated eagerly when this method is called (git runs
|
|
138
|
+
# at construction time). It is enumerable and supports indexed access by
|
|
139
|
+
# worktree path.
|
|
140
|
+
#
|
|
141
|
+
# @example Iterate over all worktrees
|
|
142
|
+
# repo.worktrees.each { |wt| puts wt.dir }
|
|
143
|
+
#
|
|
144
|
+
# @example Count worktrees
|
|
145
|
+
# repo.worktrees.size
|
|
146
|
+
#
|
|
147
|
+
# @example Access a specific worktree by path
|
|
148
|
+
# repo.worktrees['/tmp/feature']
|
|
149
|
+
#
|
|
150
|
+
# @return [Git::Worktrees] an enumerable collection of all worktrees
|
|
151
|
+
#
|
|
152
|
+
# @raise [Git::FailedError] if git exits with a non-zero exit status
|
|
153
|
+
#
|
|
154
|
+
def worktrees
|
|
155
|
+
Git::Worktrees.new(self)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
data/lib/git/repository.rb
CHANGED
|
@@ -1,6 +1,269 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'find'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
require 'git/execution_context/repository'
|
|
7
|
+
require 'git/repository/branching'
|
|
8
|
+
require 'git/repository/committing'
|
|
9
|
+
require 'git/repository/configuring'
|
|
10
|
+
require 'git/repository/diffing'
|
|
11
|
+
require 'git/repository/inspecting'
|
|
12
|
+
require 'git/repository/logging'
|
|
13
|
+
require 'git/repository/merging'
|
|
14
|
+
require 'git/repository/object_operations'
|
|
15
|
+
require 'git/repository/path_resolver'
|
|
16
|
+
require 'git/repository/remote_operations'
|
|
17
|
+
require 'git/repository/staging'
|
|
18
|
+
require 'git/repository/stashing'
|
|
19
|
+
require 'git/repository/status_operations'
|
|
20
|
+
require 'git/repository/worktree_operations'
|
|
21
|
+
|
|
3
22
|
module Git
|
|
4
|
-
|
|
23
|
+
# The main public interface for interacting with a Git repository
|
|
24
|
+
#
|
|
25
|
+
# `Git::Repository` is the **orchestration layer** for all git operations. It acts
|
|
26
|
+
# as the glue between the user-facing API and the underlying components, but
|
|
27
|
+
# contains minimal domain logic itself. For each operation it:
|
|
28
|
+
#
|
|
29
|
+
# 1. **Pre-processes arguments** — transforms user-provided values into forms
|
|
30
|
+
# suitable for the command layer (e.g. path expansion, option normalization,
|
|
31
|
+
# Ruby-idiomatic defaults, deprecation handling, input validation).
|
|
32
|
+
# 2. **Calls commands** — invokes one or more `Git::Commands::*` classes via the
|
|
33
|
+
# injected `Git::ExecutionContext::Repository`.
|
|
34
|
+
# 3. **Builds rich return values** — passes raw command output through
|
|
35
|
+
# `Git::Parsers::*` classes and result-class factory methods to assemble the
|
|
36
|
+
# meaningful Ruby objects the caller expects.
|
|
37
|
+
#
|
|
38
|
+
# Some operations are genuinely one-line delegators when no pre/post-processing is
|
|
39
|
+
# needed (e.g. `add`, `reset`), but many are short orchestration sequences that
|
|
40
|
+
# coordinate argument preparation, one or more command calls, and result assembly.
|
|
41
|
+
#
|
|
42
|
+
# Facade methods are organized into focused modules under `lib/git/repository/`
|
|
43
|
+
# (e.g. {Git::Repository::Staging}) and included into this class.
|
|
44
|
+
#
|
|
45
|
+
# @api public
|
|
46
|
+
#
|
|
47
|
+
class Repository
|
|
48
|
+
include Git::Repository::Branching
|
|
49
|
+
include Git::Repository::Committing
|
|
50
|
+
include Git::Repository::Configuring
|
|
51
|
+
include Git::Repository::Diffing
|
|
52
|
+
include Git::Repository::Inspecting
|
|
53
|
+
include Git::Repository::Logging
|
|
54
|
+
include Git::Repository::Merging
|
|
55
|
+
include Git::Repository::ObjectOperations
|
|
56
|
+
include Git::Repository::RemoteOperations
|
|
57
|
+
include Git::Repository::Staging
|
|
58
|
+
include Git::Repository::Stashing
|
|
59
|
+
include Git::Repository::StatusOperations
|
|
60
|
+
include Git::Repository::WorktreeOperations
|
|
61
|
+
|
|
62
|
+
# Open a working copy at an existing path
|
|
63
|
+
#
|
|
64
|
+
# The new repository factories are additive scaffolding introduced by the
|
|
65
|
+
# architectural redesign. The top-level {Git.open} entry point still returns a
|
|
66
|
+
# {Git::Base} object; this method exists so future work can route construction
|
|
67
|
+
# through {Git::Repository} without changing the public entry points.
|
|
68
|
+
#
|
|
69
|
+
# Note: this method opens working copies only. To open a bare repository, use
|
|
70
|
+
# {Git::Repository.bare}.
|
|
71
|
+
#
|
|
72
|
+
# @example Open the working copy in the current directory
|
|
73
|
+
# repository = Git::Repository.open('.')
|
|
74
|
+
#
|
|
75
|
+
# @param working_dir [String] the path to the root of the working copy
|
|
76
|
+
#
|
|
77
|
+
# May be any path inside the working tree when `:repository` is not given.
|
|
78
|
+
#
|
|
79
|
+
# @param options [Hash] options that control how the repository is located
|
|
80
|
+
#
|
|
81
|
+
# @option options [String] :repository a non-standard path to the `.git`
|
|
82
|
+
# directory
|
|
83
|
+
#
|
|
84
|
+
# When given, `working_dir` is used as-is (the working tree root is not
|
|
85
|
+
# auto-detected).
|
|
86
|
+
#
|
|
87
|
+
# @option options [String] :index a non-standard path to the index file
|
|
88
|
+
#
|
|
89
|
+
# @option options [Logger] :log a logger forwarded to the command layer
|
|
90
|
+
#
|
|
91
|
+
# @option options [String, nil, :use_global_config] :git_ssh path to a custom SSH executable;
|
|
92
|
+
# pass `:use_global_config` (the default) to use `Git::Base.config.git_ssh`
|
|
93
|
+
#
|
|
94
|
+
# @option options [String, :use_global_config] :binary_path path to the git binary;
|
|
95
|
+
# pass `:use_global_config` (the default) to use `Git::Base.config.binary_path`
|
|
96
|
+
#
|
|
97
|
+
# @return [Git::Repository] a repository bound to the resolved paths
|
|
98
|
+
#
|
|
99
|
+
# @raise [ArgumentError] if `working_dir` is not a directory or is not inside
|
|
100
|
+
# a git working tree
|
|
101
|
+
#
|
|
102
|
+
def self.open(working_dir, options = {})
|
|
103
|
+
raise ArgumentError, "'#{working_dir}' is not a directory" unless Dir.exist?(working_dir)
|
|
104
|
+
|
|
105
|
+
working_dir = resolve_open_working_dir(working_dir, options) unless options[:repository]
|
|
106
|
+
|
|
107
|
+
paths = PathResolver.resolve_paths(
|
|
108
|
+
working_directory: working_dir,
|
|
109
|
+
repository: options[:repository],
|
|
110
|
+
index: options[:index]
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
from_paths(options, paths)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Open an existing bare repository at `git_dir`
|
|
117
|
+
#
|
|
118
|
+
# The new repository factories are additive scaffolding introduced by the
|
|
119
|
+
# architectural redesign. The top-level {Git.bare} entry point still returns a
|
|
120
|
+
# {Git::Base} object; this method exists so future work can route construction
|
|
121
|
+
# through {Git::Repository} without changing the public entry points.
|
|
122
|
+
#
|
|
123
|
+
# @example Open a bare repository
|
|
124
|
+
# repository = Git::Repository.bare('/path/to/repo.git')
|
|
125
|
+
#
|
|
126
|
+
# @param git_dir [String] the path to the bare repository directory
|
|
127
|
+
#
|
|
128
|
+
# @param options [Hash] options forwarded to the constructed repository
|
|
129
|
+
#
|
|
130
|
+
# @option options [Logger] :log a logger forwarded to the command layer
|
|
131
|
+
#
|
|
132
|
+
# @option options [String, nil, :use_global_config] :git_ssh path to a custom SSH executable;
|
|
133
|
+
# pass `:use_global_config` (the default) to use `Git::Base.config.git_ssh`
|
|
134
|
+
#
|
|
135
|
+
# @option options [String, :use_global_config] :binary_path path to the git binary;
|
|
136
|
+
# pass `:use_global_config` (the default) to use `Git::Base.config.binary_path`
|
|
137
|
+
#
|
|
138
|
+
# @return [Git::Repository] a repository bound to the bare repository directory
|
|
139
|
+
#
|
|
140
|
+
def self.bare(git_dir, options = {})
|
|
141
|
+
paths = PathResolver.resolve_paths(repository: git_dir, bare: true)
|
|
142
|
+
|
|
143
|
+
from_paths(options, paths)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Resolve the worktree root to use as the working directory for `.open`
|
|
147
|
+
#
|
|
148
|
+
# Delegates to {PathResolver.root_of_worktree}, forwarding `:binary_path`
|
|
149
|
+
# and `:git_ssh` from `options`.
|
|
150
|
+
#
|
|
151
|
+
# @param working_dir [String] a path inside the working tree
|
|
152
|
+
#
|
|
153
|
+
# @param options [Hash] the caller-supplied options hash from `.open`
|
|
154
|
+
#
|
|
155
|
+
# @return [String] the absolute path to the root of the working tree
|
|
156
|
+
#
|
|
157
|
+
# @raise [ArgumentError] if `working_dir` is not inside a git working tree
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
#
|
|
161
|
+
def self.resolve_open_working_dir(working_dir, options)
|
|
162
|
+
PathResolver.root_of_worktree(
|
|
163
|
+
working_dir,
|
|
164
|
+
binary_path: options.fetch(:binary_path, :use_global_config),
|
|
165
|
+
git_ssh: options.fetch(:git_ssh, :use_global_config)
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
private_class_method :resolve_open_working_dir
|
|
169
|
+
|
|
170
|
+
# Build a repository from caller options and resolved paths
|
|
171
|
+
#
|
|
172
|
+
# @param options [Hash] the caller-supplied options (`:git_ssh`,
|
|
173
|
+
# `:binary_path`, `:log`)
|
|
174
|
+
#
|
|
175
|
+
# @param paths [Hash{Symbol => (String, nil)}] the resolved
|
|
176
|
+
# `:working_directory`, `:repository`, and `:index` paths
|
|
177
|
+
#
|
|
178
|
+
# @return [Git::Repository] the constructed repository
|
|
179
|
+
#
|
|
180
|
+
# @api private
|
|
181
|
+
#
|
|
182
|
+
def self.from_paths(options, paths)
|
|
183
|
+
execution_context = Git::ExecutionContext::Repository.from_hash(
|
|
184
|
+
options.merge(paths), logger: options[:log]
|
|
185
|
+
)
|
|
186
|
+
new(execution_context: execution_context)
|
|
187
|
+
end
|
|
188
|
+
private_class_method :from_paths
|
|
189
|
+
|
|
190
|
+
# @return [Git::ExecutionContext::Repository] the execution context used to run
|
|
191
|
+
# git commands for this repository
|
|
192
|
+
# @api private
|
|
193
|
+
attr_reader :execution_context
|
|
194
|
+
|
|
195
|
+
# @param execution_context [Git::ExecutionContext::Repository] the context used
|
|
196
|
+
# to run git commands for this repository; must not be nil
|
|
197
|
+
#
|
|
198
|
+
# @raise [ArgumentError] if `execution_context` is nil
|
|
199
|
+
#
|
|
200
|
+
def initialize(execution_context:)
|
|
201
|
+
raise ArgumentError, 'execution_context must not be nil' if execution_context.nil?
|
|
202
|
+
|
|
203
|
+
@execution_context = execution_context
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Returns the root of the working tree, or `nil` for a bare repository
|
|
207
|
+
#
|
|
208
|
+
# @example Get the working directory path
|
|
209
|
+
# repository.dir #=> #<Pathname:/path/to/repo>
|
|
210
|
+
#
|
|
211
|
+
# @return [Pathname, nil] the working directory path, or `nil` when bare
|
|
212
|
+
#
|
|
213
|
+
def dir
|
|
214
|
+
working_dir = execution_context.git_work_dir
|
|
215
|
+
working_dir && Pathname.new(working_dir)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns the repository (`.git`) directory
|
|
219
|
+
#
|
|
220
|
+
# @example Get the repository directory path
|
|
221
|
+
# repository.repo #=> #<Pathname:/path/to/repo/.git>
|
|
222
|
+
#
|
|
223
|
+
# @return [Pathname, nil] the repository directory path
|
|
224
|
+
#
|
|
225
|
+
def repo
|
|
226
|
+
repository = execution_context.git_dir
|
|
227
|
+
repository && Pathname.new(repository)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Returns the git index file
|
|
231
|
+
#
|
|
232
|
+
# @example Get the index file path
|
|
233
|
+
# repository.index #=> #<Pathname:/path/to/repo/.git/index>
|
|
234
|
+
#
|
|
235
|
+
# @return [Pathname, nil] the index file path
|
|
236
|
+
#
|
|
237
|
+
def index
|
|
238
|
+
index_file = execution_context.git_index_file
|
|
239
|
+
index_file && Pathname.new(index_file)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Returns the size of the repository directory in bytes
|
|
243
|
+
#
|
|
244
|
+
# Sums the sizes of every regular file under the repository (`.git`)
|
|
245
|
+
# directory in a single traversal. Symbolic links are not followed, so files
|
|
246
|
+
# that physically live outside the repository (reached through a symlinked
|
|
247
|
+
# directory) are never counted. Files that disappear mid-traversal are
|
|
248
|
+
# silently skipped.
|
|
249
|
+
#
|
|
250
|
+
# @example Get the repository size in bytes
|
|
251
|
+
# repository.repo_size #=> 12345
|
|
252
|
+
#
|
|
253
|
+
# @return [Integer] the total size in bytes of the repository directory
|
|
254
|
+
#
|
|
255
|
+
def repo_size
|
|
256
|
+
repository = repo
|
|
257
|
+
return 0 unless repository&.directory?
|
|
258
|
+
|
|
259
|
+
total = 0
|
|
260
|
+
Find.find(repository.to_s) do |path|
|
|
261
|
+
stat = File.lstat(path)
|
|
262
|
+
total += stat.size if stat.file?
|
|
263
|
+
rescue Errno::ENOENT
|
|
264
|
+
next
|
|
265
|
+
end
|
|
266
|
+
total
|
|
267
|
+
end
|
|
5
268
|
end
|
|
6
269
|
end
|