braid 1.1.7 → 1.1.9
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/lib/braid/check_gem.rb +7 -0
- data/lib/braid/command.rb +6 -0
- data/lib/braid/commands/add.rb +41 -1
- data/lib/braid/commands/diff.rb +2 -1
- data/lib/braid/commands/push.rb +7 -5
- data/lib/braid/commands/remove.rb +1 -0
- data/lib/braid/commands/setup.rb +1 -0
- data/lib/braid/commands/status.rb +1 -0
- data/lib/braid/commands/update.rb +1 -0
- data/lib/braid/commands/upgrade_config.rb +1 -0
- data/lib/braid/main.rb +17 -3
- data/lib/braid/mirror.rb +31 -16
- data/lib/braid/operations.rb +241 -80
- data/lib/braid/sorbet/fake_runtime.rb +10 -0
- data/lib/braid/version.rb +4 -1
- data/lib/braid.rb +2 -3
- metadata +20 -6
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 0ddcc3421c5a2dffcf7634e71118d7392d9f506ab49064f25eb2c38f7f0d67b1
         | 
| 4 | 
            +
              data.tar.gz: d3ad647f242d287ece52de41be7edb56cfdc3c469336ee84bb35c15257ca4d36
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: f002d88fa8bb752ab878d746c8b1ead6c73b80518d37a06f261eba2b99540c6081c7991a9f404ec0ca26c39165b6e9345725e4b4f49f52ad132ca1e359465e39
         | 
| 7 | 
            +
              data.tar.gz: 4da0947c03c1846b073b331f4118f8b07e42b4138c3450b3f2ce6ba967e3a8451edf843622e3e6b6bbe4f5c70951866d7c6abbcc7e1211d10501b75fefa8e095
         | 
    
        data/lib/braid/check_gem.rb
    CHANGED
    
    | @@ -1,3 +1,10 @@ | |
| 1 | 
            +
            # Since this file can't safely depend on the Sorbet runtime, it isn't prudent to
         | 
| 2 | 
            +
            # try to commit to `typed: true` even if the file currently passes type checking
         | 
| 3 | 
            +
            # without needing any references to `T`.  Like `exe/braid`, this file doesn't
         | 
| 4 | 
            +
            # have much code worth type checking.
         | 
| 5 | 
            +
            #
         | 
| 6 | 
            +
            # typed: false
         | 
| 7 | 
            +
             | 
| 1 8 | 
             
            # Braid has several entry points that run code from Ruby gems (either Braid
         | 
| 2 9 | 
             
            # itself or dependencies such as Sorbet) and expect to get the correct versions
         | 
| 3 10 | 
             
            # (either the same copy of Braid or versions of dependencies consistent with the
         | 
    
        data/lib/braid/command.rb
    CHANGED
    
    | @@ -17,6 +17,11 @@ module Braid | |
| 17 17 | 
             
                  klass.new.run(*args)
         | 
| 18 18 |  | 
| 19 19 | 
             
                rescue BraidError => error
         | 
| 20 | 
            +
                  handle_error(error)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                sig {params(error: BraidError).returns(T.noreturn)}
         | 
| 24 | 
            +
                def self.handle_error(error)
         | 
| 20 25 | 
             
                  case error
         | 
| 21 26 | 
             
                    when Operations::ShellExecutionError
         | 
| 22 27 | 
             
                      msg "Shell error: #{error.message}"
         | 
| @@ -36,6 +41,7 @@ module Braid | |
| 36 41 | 
             
                  self.class.msg(str)
         | 
| 37 42 | 
             
                end
         | 
| 38 43 |  | 
| 44 | 
            +
                sig {returns(Config)}
         | 
| 39 45 | 
             
                def config
         | 
| 40 46 | 
             
                  @config ||= Config.new({'mode' => config_mode})
         | 
| 41 47 | 
             
                end
         | 
    
        data/lib/braid/commands/add.rb
    CHANGED
    
    | @@ -1,15 +1,55 @@ | |
| 1 | 
            +
            # typed: true
         | 
| 1 2 | 
             
            module Braid
         | 
| 2 3 | 
             
              module Commands
         | 
| 3 4 | 
             
                class Add < Command
         | 
| 5 | 
            +
                  # Returns the default branch name of the repository at the given URL, or
         | 
| 6 | 
            +
                  # nil if it couldn't be determined.
         | 
| 7 | 
            +
                  #
         | 
| 8 | 
            +
                  # We won't be able to determine a default branch in certain cases that we
         | 
| 9 | 
            +
                  # expect to be unusual in the context of Braid, such as if the HEAD is
         | 
| 10 | 
            +
                  # detached or points to a ref outside of `refs/heads`.  (Presumably, the
         | 
| 11 | 
            +
                  # same thing happens if the server is too old to report symrefs to us.)
         | 
| 12 | 
            +
                  # In those cases, a plausible alternative behavior would be to just lock
         | 
| 13 | 
            +
                  # the mirror to the remote HEAD revision, but that's probably not what the
         | 
| 14 | 
            +
                  # user wants.  It's much more likely that something is wrong and Braid
         | 
| 15 | 
            +
                  # should report an error.
         | 
| 16 | 
            +
                  def get_default_branch_name(url)
         | 
| 17 | 
            +
                    head_targets = []
         | 
| 18 | 
            +
                    # The `HEAD` parameter here doesn't appear to do an exact match (it
         | 
| 19 | 
            +
                    # appears to match any ref with `HEAD` as the last path component, such
         | 
| 20 | 
            +
                    # as `refs/remotes/origin/HEAD` in the unusual case where the repository
         | 
| 21 | 
            +
                    # contains its own remote-tracking branches), but it reduces the data we
         | 
| 22 | 
            +
                    # have to scan a bit.
         | 
| 23 | 
            +
                    git.ls_remote(['--symref', url, 'HEAD']).split("\n").each do |line|
         | 
| 24 | 
            +
                      m = /^ref: (.*)\tHEAD$/.match(line)
         | 
| 25 | 
            +
                      head_targets.push(m[1]) if m
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
                    return nil unless head_targets.size == 1
         | 
| 28 | 
            +
                    m = /^refs\/heads\/(.*)$/.match(head_targets[0])
         | 
| 29 | 
            +
                    return nil unless m
         | 
| 30 | 
            +
                    m[1]
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 4 33 | 
             
                  def run(url, options = {})
         | 
| 5 34 | 
             
                    with_reset_on_error do
         | 
| 35 | 
            +
                      if options['branch'].nil? && options['tag'].nil? && options['revision'].nil?
         | 
| 36 | 
            +
                        default_branch = get_default_branch_name(url)
         | 
| 37 | 
            +
                        if default_branch.nil?
         | 
| 38 | 
            +
                          raise BraidError, <<-MSG
         | 
| 39 | 
            +
            Failed to detect the default branch of the remote repository.  Please specify
         | 
| 40 | 
            +
            the branch you want to use via the --branch option.
         | 
| 41 | 
            +
            MSG
         | 
| 42 | 
            +
                        end
         | 
| 43 | 
            +
                        options['branch'] = default_branch
         | 
| 44 | 
            +
                      end
         | 
| 45 | 
            +
             | 
| 6 46 | 
             
                      mirror           = config.add_from_options(url, options)
         | 
| 7 47 | 
             
                      add_config_file
         | 
| 8 48 |  | 
| 9 49 | 
             
                      mirror.branch = nil if options['revision']
         | 
| 10 50 | 
             
                      raise BraidError, 'Can not add mirror specifying both a revision and a tag' if options['revision'] && mirror.tag
         | 
| 11 51 |  | 
| 12 | 
            -
                      branch_message   =  | 
| 52 | 
            +
                      branch_message   = mirror.branch.nil? ? '' : " branch '#{mirror.branch}'"
         | 
| 13 53 | 
             
                      tag_message      = mirror.tag.nil? ? '' : " tag '#{mirror.tag}'"
         | 
| 14 54 | 
             
                      revision_message = options['revision'] ? " at #{display_revision(mirror, options['revision'])}" : ''
         | 
| 15 55 | 
             
                      msg "Adding mirror of '#{mirror.url}'#{branch_message}#{tag_message}#{revision_message}."
         | 
    
        data/lib/braid/commands/diff.rb
    CHANGED
    
    | @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            # typed: true
         | 
| 1 2 | 
             
            module Braid
         | 
| 2 3 | 
             
              module Commands
         | 
| 3 4 | 
             
                class Diff < Command
         | 
| @@ -35,7 +36,7 @@ module Braid | |
| 35 36 |  | 
| 36 37 | 
             
                    # XXX: Warn if the user specifies file paths that are outside the
         | 
| 37 38 | 
             
                    # mirror?  Currently, they just won't match anything.
         | 
| 38 | 
            -
                    git.diff_to_stdout( | 
| 39 | 
            +
                    git.diff_to_stdout(mirror.diff_args(options['git_diff_args']))
         | 
| 39 40 |  | 
| 40 41 | 
             
                    clear_remote(mirror, options)
         | 
| 41 42 | 
             
                  end
         | 
    
        data/lib/braid/commands/push.rb
    CHANGED
    
    | @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            # typed: true
         | 
| 1 2 | 
             
            require 'fileutils'
         | 
| 2 3 | 
             
            require 'tmpdir'
         | 
| 3 4 |  | 
| @@ -43,7 +44,8 @@ module Braid | |
| 43 44 | 
             
                    end
         | 
| 44 45 | 
             
                    clone_dir = Dir.tmpdir + "/braid_push.#{$$}"
         | 
| 45 46 | 
             
                    Dir.mkdir(clone_dir)
         | 
| 46 | 
            -
                     | 
| 47 | 
            +
                    # TODO (typing): Remove this `T.must` somehow?
         | 
| 48 | 
            +
                    remote_url = T.must(git.remote_url(mirror.remote))
         | 
| 47 49 | 
             
                    if remote_url == mirror.cached_url
         | 
| 48 50 | 
             
                      remote_url = mirror.url
         | 
| 49 51 | 
             
                    elsif File.directory?(remote_url)
         | 
| @@ -67,7 +69,7 @@ module Braid | |
| 67 69 | 
             
                      File.open('.git/objects/info/alternates', 'wb') { |f|
         | 
| 68 70 | 
             
                        f.puts(odb_paths)
         | 
| 69 71 | 
             
                      }
         | 
| 70 | 
            -
                      git.fetch(remote_url, mirror.remote_ref)
         | 
| 72 | 
            +
                      git.fetch(remote_url, [mirror.remote_ref])
         | 
| 71 73 | 
             
                      new_tree = git.make_tree_with_item(base_revision,
         | 
| 72 74 | 
             
                        mirror.remote_path || '', local_mirror_item)
         | 
| 73 75 | 
             
                      if git.require_version('2.27')
         | 
| @@ -105,11 +107,11 @@ module Braid | |
| 105 107 | 
             
                      # Update HEAD the same way git.checkout(base_revision) would, but
         | 
| 106 108 | 
             
                      # don't populate the index or working tree (to save us the trouble of
         | 
| 107 109 | 
             
                      # emptying them again before the git.read_tree).
         | 
| 108 | 
            -
                      git.update_ref('--no-deref', 'HEAD', base_revision)
         | 
| 109 | 
            -
                      git. | 
| 110 | 
            +
                      git.update_ref(['--no-deref', 'HEAD', base_revision])
         | 
| 111 | 
            +
                      git.read_tree_um(new_tree)
         | 
| 110 112 | 
             
                      system('git commit -v')
         | 
| 111 113 | 
             
                      msg "Pushing changes to remote branch #{branch}."
         | 
| 112 | 
            -
                      git.push(remote_url, "HEAD:refs/heads/#{branch}")
         | 
| 114 | 
            +
                      git.push([remote_url, "HEAD:refs/heads/#{branch}"])
         | 
| 113 115 | 
             
                    end
         | 
| 114 116 | 
             
                    FileUtils.rm_r(clone_dir)
         | 
| 115 117 |  | 
    
        data/lib/braid/commands/setup.rb
    CHANGED
    
    
    
        data/lib/braid/main.rb
    CHANGED
    
    | @@ -1,13 +1,24 @@ | |
| 1 | 
            +
            # typed: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'braid'
         | 
| 2 4 |  | 
| 3 5 | 
             
            require 'rubygems'
         | 
| 4 6 | 
             
            require 'main'
         | 
| 5 7 |  | 
| 8 | 
            +
            # This is needed for `T` below to resolve to `Braid::T` when using the fake
         | 
| 9 | 
            +
            # Sorbet runtime.  TODO: Indent the contents and accept the large diff?
         | 
| 10 | 
            +
            module Braid
         | 
| 11 | 
            +
             | 
| 6 12 | 
             
            Home = File.expand_path(ENV['HOME'] || '~')
         | 
| 7 13 |  | 
| 8 14 | 
             
            # mostly blantantly stolen from ara's punch script
         | 
| 9 15 | 
             
            # main kicks ass!
         | 
| 10 | 
            -
            Main {
         | 
| 16 | 
            +
            T.unsafe(Main).run {
         | 
| 17 | 
            +
              # `Main` is somewhat mind-bending and I'm unsure what the type of `self`
         | 
| 18 | 
            +
              # actually is here, but whatever it is, we don't have a type declaration for
         | 
| 19 | 
            +
              # it.
         | 
| 20 | 
            +
              T.bind(self, T.untyped)
         | 
| 21 | 
            +
             | 
| 11 22 | 
             
              description <<-TXT
         | 
| 12 23 | 
             
                braid is a simple tool to help track git repositories inside a git repository.
         | 
| 13 24 |  | 
| @@ -20,7 +31,8 @@ Main { | |
| 20 31 | 
             
              # The "main" library doesn't provide a way to do this??
         | 
| 21 32 | 
             
              def check_no_extra_args!
         | 
| 22 33 | 
             
                if @argv.length > 0
         | 
| 23 | 
            -
                   | 
| 34 | 
            +
                  Braid::Command.handle_error(
         | 
| 35 | 
            +
                    Braid::BraidError.new('Extra argument(s) passed to command.'))
         | 
| 24 36 | 
             
                end
         | 
| 25 37 | 
             
              end
         | 
| 26 38 |  | 
| @@ -123,7 +135,7 @@ Main { | |
| 123 135 |  | 
| 124 136 | 
             
                mixin :optional_local_path, :option_verbose, :option_keep_remote
         | 
| 125 137 |  | 
| 126 | 
            -
                synopsis(Main::Usage.default_synopsis(self) + ' [-- git_diff_arg*]')
         | 
| 138 | 
            +
                synopsis(T.unsafe(Main::Usage).default_synopsis(self) + ' [-- git_diff_arg*]')
         | 
| 127 139 |  | 
| 128 140 | 
             
                run {
         | 
| 129 141 | 
             
                  if @argv.length > 0 && @argv[0] == '--'
         | 
| @@ -318,3 +330,5 @@ Main { | |
| 318 330 |  | 
| 319 331 | 
             
              run { help! }
         | 
| 320 332 | 
             
            }
         | 
| 333 | 
            +
             | 
| 334 | 
            +
            end
         | 
    
        data/lib/braid/mirror.rb
    CHANGED
    
    | @@ -86,7 +86,7 @@ DESC | |
| 86 86 | 
             
                  raise NoTagAndBranch if options['tag'] && options['branch']
         | 
| 87 87 |  | 
| 88 88 | 
             
                  tag = options['tag']
         | 
| 89 | 
            -
                  branch = options['branch'] | 
| 89 | 
            +
                  branch = options['branch']
         | 
| 90 90 |  | 
| 91 91 | 
             
                  path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
         | 
| 92 92 | 
             
                  raise PathRequired unless path
         | 
| @@ -107,7 +107,7 @@ DESC | |
| 107 107 | 
             
                  branch.nil? && tag.nil?
         | 
| 108 108 | 
             
                end
         | 
| 109 109 |  | 
| 110 | 
            -
                sig {params(commit:  | 
| 110 | 
            +
                sig {params(commit: Operations::Git::ObjectExpr).returns(T::Boolean)}
         | 
| 111 111 | 
             
                def merged?(commit)
         | 
| 112 112 | 
             
                  # tip from spearce in #git:
         | 
| 113 113 | 
             
                  # `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
         | 
| @@ -115,9 +115,7 @@ DESC | |
| 115 115 | 
             
                  !!base_revision && git.merge_base(commit, base_revision) == commit
         | 
| 116 116 | 
             
                end
         | 
| 117 117 |  | 
| 118 | 
            -
                 | 
| 119 | 
            -
                # Braid::Operations::Git::TreeItem.
         | 
| 120 | 
            -
                sig {params(revision: String).returns(T.untyped)}
         | 
| 118 | 
            +
                sig {params(revision: String).returns(Operations::Git::TreeItem)}
         | 
| 121 119 | 
             
                def upstream_item_for_revision(revision)
         | 
| 122 120 | 
             
                  git.get_tree_item(revision, self.remote_path)
         | 
| 123 121 | 
             
                end
         | 
| @@ -211,16 +209,28 @@ DESC | |
| 211 209 | 
             
                  git.remote_url(remote) == cached_url
         | 
| 212 210 | 
             
                end
         | 
| 213 211 |  | 
| 214 | 
            -
                sig {returns( | 
| 212 | 
            +
                sig {returns(Operations::Git::ObjectID)}
         | 
| 215 213 | 
             
                def base_revision
         | 
| 216 | 
            -
                  #  | 
| 217 | 
            -
                  #  | 
| 218 | 
            -
                  #  | 
| 219 | 
            -
                   | 
| 220 | 
            -
                   | 
| 221 | 
            -
             | 
| 214 | 
            +
                  # TODO (typing): We think `revision` should always be non-nil here these
         | 
| 215 | 
            +
                  # days and we can completely drop the `inferred_revision` code, but we're
         | 
| 216 | 
            +
                  # waiting for a better time to actually make this runtime behavior change
         | 
| 217 | 
            +
                  # and accept any risk of breakage
         | 
| 218 | 
            +
                  # (https://github.com/cristibalan/braid/pull/105/files#r857150464).
         | 
| 219 | 
            +
                  #
         | 
| 220 | 
            +
                  # Temporary variable
         | 
| 221 | 
            +
                  # (https://sorbet.org/docs/flow-sensitive#limitations-of-flow-sensitivity)
         | 
| 222 | 
            +
                  revision1 = revision
         | 
| 223 | 
            +
                  if revision1
         | 
| 224 | 
            +
                    git.rev_parse(revision1)
         | 
| 222 225 | 
             
                  else
         | 
| 223 | 
            -
                    inferred_revision
         | 
| 226 | 
            +
                    # NOTE: Given that `inferred_revision` does appear to return nil on one
         | 
| 227 | 
            +
                    # code path, using this `T.must` and giving `base_revision` a
         | 
| 228 | 
            +
                    # non-nilable return type presents a theoretical risk of leading us to
         | 
| 229 | 
            +
                    # make changes to callers that break things at runtime.  But we judge
         | 
| 230 | 
            +
                    # this a lesser evil than making the return type nilable and changing
         | 
| 231 | 
            +
                    # all callers to type-check successfully with that when we hope to
         | 
| 232 | 
            +
                    # revert the change soon anyway.
         | 
| 233 | 
            +
                    T.must(inferred_revision)
         | 
| 224 234 | 
             
                  end
         | 
| 225 235 | 
             
                end
         | 
| 226 236 |  | 
| @@ -310,7 +320,12 @@ DESC | |
| 310 320 |  | 
| 311 321 | 
             
                sig {returns(String)}
         | 
| 312 322 | 
             
                def remote
         | 
| 313 | 
            -
                   | 
| 323 | 
            +
                  # Ensure that we replace any characters in the mirror path that might be
         | 
| 324 | 
            +
                  # problematic in a Git ref name.  Theoretically, this may introduce
         | 
| 325 | 
            +
                  # collisions between mirrors, but we don't expect that to be much of a
         | 
| 326 | 
            +
                  # problem because Braid doesn't keep remotes by default after a command
         | 
| 327 | 
            +
                  # exits.
         | 
| 328 | 
            +
                  "#{branch || tag || 'revision'}_braid_#{path}".gsub(/[^-A-Za-z0-9]/, '_')
         | 
| 314 329 | 
             
                end
         | 
| 315 330 |  | 
| 316 331 | 
             
                private
         | 
| @@ -322,8 +337,8 @@ DESC | |
| 322 337 |  | 
| 323 338 | 
             
                sig {returns(T.nilable(String))}
         | 
| 324 339 | 
             
                def inferred_revision
         | 
| 325 | 
            -
                  local_commits = git.rev_list('HEAD', "-- #{path}").split("\n")
         | 
| 326 | 
            -
                  remote_hashes = git.rev_list("--pretty=format | 
| 340 | 
            +
                  local_commits = git.rev_list(['HEAD', "-- #{path}"]).split("\n")
         | 
| 341 | 
            +
                  remote_hashes = git.rev_list(["--pretty=format:%T", remote]).split('commit ').map do |chunk|
         | 
| 327 342 | 
             
                    chunk.split("\n", 2).map { |value| value.strip }
         | 
| 328 343 | 
             
                  end
         | 
| 329 344 | 
             
                  hash          = T.let(nil, T.nilable(String))
         | 
    
        data/lib/braid/operations.rb
    CHANGED
    
    | @@ -1,5 +1,8 @@ | |
| 1 | 
            +
            # typed: strict
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require 'singleton'
         | 
| 2 4 | 
             
            require 'rubygems'
         | 
| 5 | 
            +
            require 'shellwords'
         | 
| 3 6 | 
             
            require 'tempfile'
         | 
| 4 7 |  | 
| 5 8 | 
             
            module Braid
         | 
| @@ -7,45 +10,64 @@ module Braid | |
| 7 10 |  | 
| 8 11 | 
             
              module Operations
         | 
| 9 12 | 
             
                class ShellExecutionError < BraidError
         | 
| 13 | 
            +
                  # TODO (typing): Should this be nilable?
         | 
| 14 | 
            +
                  sig {returns(T.nilable(String))}
         | 
| 10 15 | 
             
                  attr_reader :err, :out
         | 
| 11 16 |  | 
| 17 | 
            +
                  sig {params(err: T.nilable(String), out: T.nilable(String)).void}
         | 
| 12 18 | 
             
                  def initialize(err = nil, out = nil)
         | 
| 13 19 | 
             
                    @err = err
         | 
| 14 20 | 
             
                    @out = out
         | 
| 15 21 | 
             
                  end
         | 
| 16 22 |  | 
| 23 | 
            +
                  sig {returns(String)}
         | 
| 17 24 | 
             
                  def message
         | 
| 18 | 
            -
                    @err.to_s.split("\n").first
         | 
| 25 | 
            +
                    first_line = @err.to_s.split("\n").first
         | 
| 26 | 
            +
                    # Currently, first_line can be nil if @err was empty, but Sorbet thinks
         | 
| 27 | 
            +
                    # that the `message` method of an Exception should always return non-nil
         | 
| 28 | 
            +
                    # (although override checking isn't enforced as of this writing), so
         | 
| 29 | 
            +
                    # handle nil here.  This seems ad-hoc but better than putting in a
         | 
| 30 | 
            +
                    # `T.must` that we know has a risk of being wrong.  Hopefully this will
         | 
| 31 | 
            +
                    # be fixed better in https://github.com/cristibalan/braid/issues/90.
         | 
| 32 | 
            +
                    first_line.nil? ? '' : first_line
         | 
| 19 33 | 
             
                  end
         | 
| 20 34 | 
             
                end
         | 
| 21 35 | 
             
                class VersionTooLow < BraidError
         | 
| 36 | 
            +
                  sig {params(command: String, version: String, required: String).void}
         | 
| 22 37 | 
             
                  def initialize(command, version, required)
         | 
| 23 38 | 
             
                    @command  = command
         | 
| 24 | 
            -
                     | 
| 39 | 
            +
                    # TODO (typing): Probably should not be nilable
         | 
| 40 | 
            +
                    @version  = T.let(version.to_s.split("\n").first, T.nilable(String))
         | 
| 25 41 | 
             
                    @required = required
         | 
| 26 42 | 
             
                  end
         | 
| 27 43 |  | 
| 44 | 
            +
                  sig {returns(String)}
         | 
| 28 45 | 
             
                  def message
         | 
| 29 46 | 
             
                    "#{@command} version too low: #{@version}. #{@required} needed."
         | 
| 30 47 | 
             
                  end
         | 
| 31 48 | 
             
                end
         | 
| 32 49 | 
             
                class UnknownRevision < BraidError
         | 
| 50 | 
            +
                  sig {returns(String)}
         | 
| 33 51 | 
             
                  def message
         | 
| 34 52 | 
             
                    "unknown revision: #{super}"
         | 
| 35 53 | 
             
                  end
         | 
| 36 54 | 
             
                end
         | 
| 37 55 | 
             
                class LocalChangesPresent < BraidError
         | 
| 56 | 
            +
                  sig {returns(String)}
         | 
| 38 57 | 
             
                  def message
         | 
| 39 58 | 
             
                    'local changes are present'
         | 
| 40 59 | 
             
                  end
         | 
| 41 60 | 
             
                end
         | 
| 42 61 | 
             
                class MergeError < BraidError
         | 
| 62 | 
            +
                  sig {returns(String)}
         | 
| 43 63 | 
             
                  attr_reader :conflicts_text
         | 
| 44 64 |  | 
| 65 | 
            +
                  sig {params(conflicts_text: String).void}
         | 
| 45 66 | 
             
                  def initialize(conflicts_text)
         | 
| 46 67 | 
             
                    @conflicts_text = conflicts_text
         | 
| 47 68 | 
             
                  end
         | 
| 48 69 |  | 
| 70 | 
            +
                  sig {returns(String)}
         | 
| 49 71 | 
             
                  def message
         | 
| 50 72 | 
             
                    'could not merge'
         | 
| 51 73 | 
             
                  end
         | 
| @@ -53,18 +75,24 @@ module Braid | |
| 53 75 |  | 
| 54 76 | 
             
                # The command proxy is meant to encapsulate commands such as git, that work with subcommands.
         | 
| 55 77 | 
             
                class Proxy
         | 
| 78 | 
            +
                  extend T::Sig
         | 
| 56 79 | 
             
                  include Singleton
         | 
| 57 80 |  | 
| 58 | 
            -
                   | 
| 59 | 
            -
             | 
| 81 | 
            +
                  # TODO (typing): We could make this method abstract if our fake Sorbet
         | 
| 82 | 
            +
                  # runtime supported abstract methods.
         | 
| 83 | 
            +
                  sig {returns(String)}
         | 
| 84 | 
            +
                  def self.command
         | 
| 85 | 
            +
                    raise InternalError, 'Proxy.command not overridden'
         | 
| 60 86 | 
             
                  end
         | 
| 61 87 |  | 
| 62 88 | 
             
                  # hax!
         | 
| 89 | 
            +
                  sig {returns(String)}
         | 
| 63 90 | 
             
                  def version
         | 
| 64 | 
            -
                    _, out, _ = exec!( | 
| 91 | 
            +
                    _, out, _ = exec!([self.class.command, '--version'])
         | 
| 65 92 | 
             
                    out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
         | 
| 66 93 | 
             
                  end
         | 
| 67 94 |  | 
| 95 | 
            +
                  sig {params(required: String).returns(T::Boolean)}
         | 
| 68 96 | 
             
                  def require_version(required)
         | 
| 69 97 | 
             
                    # Gem::Version is intended for Ruby gem versions, but various web sites
         | 
| 70 98 | 
             
                    # suggest it as a convenient way of comparing version strings in
         | 
| @@ -73,77 +101,115 @@ module Braid | |
| 73 101 | 
             
                    Gem::Version.new(version) >= Gem::Version.new(required)
         | 
| 74 102 | 
             
                  end
         | 
| 75 103 |  | 
| 104 | 
            +
                  sig {params(required: String).void}
         | 
| 76 105 | 
             
                  def require_version!(required)
         | 
| 77 106 | 
             
                    require_version(required) || raise(VersionTooLow.new(self.class.command, version, required))
         | 
| 78 107 | 
             
                  end
         | 
| 79 108 |  | 
| 80 109 | 
             
                  private
         | 
| 81 110 |  | 
| 111 | 
            +
                  sig {params(name: String).returns(T::Array[String])}
         | 
| 82 112 | 
             
                  def command(name)
         | 
| 83 113 | 
             
                    # stub
         | 
| 84 | 
            -
                    name
         | 
| 114 | 
            +
                    [name]
         | 
| 85 115 | 
             
                  end
         | 
| 86 116 |  | 
| 87 | 
            -
                   | 
| 88 | 
            -
             | 
| 117 | 
            +
                  sig {params(arg: String, args: T::Array[String]).returns(String)}
         | 
| 118 | 
            +
                  def invoke(arg, args)
         | 
| 119 | 
            +
                    exec!(command(arg) + args)[1].strip # return stdout
         | 
| 89 120 | 
             
                  end
         | 
| 90 121 |  | 
| 91 | 
            -
                   | 
| 92 | 
            -
             | 
| 93 | 
            -
                   | 
| 122 | 
            +
                  # Some of the unit tests want to mock out `exec`, but they have no way to
         | 
| 123 | 
            +
                  # construct a real Process::Status and thus use an integer instead.  We
         | 
| 124 | 
            +
                  # have to accommodate this in the type annotation to avoid runtime type
         | 
| 125 | 
            +
                  # check failures during the tests.  In normal use of Braid, this will
         | 
| 126 | 
            +
                  # always be a real Process::Status.  Fortunately, allowing Integer doesn't
         | 
| 127 | 
            +
                  # seem to cause any other problems right now.
         | 
| 128 | 
            +
                  ProcessStatusOrInteger = T.type_alias { T.any(Process::Status, Integer) }
         | 
| 94 129 |  | 
| 130 | 
            +
                  sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
         | 
| 95 131 | 
             
                  def exec(cmd)
         | 
| 96 | 
            -
                    cmd.strip!
         | 
| 97 | 
            -
             | 
| 98 132 | 
             
                    Operations::with_modified_environment({'LANG' => 'C'}) do
         | 
| 99 133 | 
             
                      log(cmd)
         | 
| 100 | 
            -
                       | 
| 134 | 
            +
                      # The special `[cmd[0], cmd[0]]` syntax ensures that `cmd[0]` is
         | 
| 135 | 
            +
                      # interpreted as the path of the executable and not a shell command
         | 
| 136 | 
            +
                      # even if `cmd` has only one element. See the documentation:
         | 
| 137 | 
            +
                      # https://ruby-doc.org/core-3.1.2/Process.html#method-c-spawn.
         | 
| 138 | 
            +
                      # Granted, this shouldn't matter for Braid for two reasons: (1)
         | 
| 139 | 
            +
                      # `cmd[0]` is always "git", which doesn't contain any shell special
         | 
| 140 | 
            +
                      # characters, and (2) `cmd` always has at least one additional
         | 
| 141 | 
            +
                      # argument (the Git subcommand). However, it's still nice to make our
         | 
| 142 | 
            +
                      # intent clear.
         | 
| 143 | 
            +
                      out, err, status = T.unsafe(Open3).capture3([cmd[0], cmd[0]], *cmd[1..])
         | 
| 101 144 | 
             
                      [status, out, err]
         | 
| 102 145 | 
             
                    end
         | 
| 103 146 | 
             
                  end
         | 
| 104 147 |  | 
| 148 | 
            +
                  sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
         | 
| 105 149 | 
             
                  def exec!(cmd)
         | 
| 106 150 | 
             
                    status, out, err = exec(cmd)
         | 
| 107 151 | 
             
                    raise ShellExecutionError.new(err, out) unless status == 0
         | 
| 108 152 | 
             
                    [status, out, err]
         | 
| 109 153 | 
             
                  end
         | 
| 110 154 |  | 
| 155 | 
            +
                  sig {params(cmd: T::Array[String]).returns(ProcessStatusOrInteger)}
         | 
| 111 156 | 
             
                  def system(cmd)
         | 
| 112 | 
            -
                    cmd.strip!
         | 
| 113 | 
            -
             | 
| 114 157 | 
             
                    # Without this, "braid diff" output came out in the wrong order on Windows.
         | 
| 115 158 | 
             
                    $stdout.flush
         | 
| 116 159 | 
             
                    $stderr.flush
         | 
| 117 160 | 
             
                    Operations::with_modified_environment({'LANG' => 'C'}) do
         | 
| 118 | 
            -
                       | 
| 161 | 
            +
                      # See the comment in `exec` about the `[cmd[0], cmd[0]]` syntax.
         | 
| 162 | 
            +
                      T.unsafe(Kernel).system([cmd[0], cmd[0]], *cmd[1..])
         | 
| 119 163 | 
             
                      return $?
         | 
| 120 164 | 
             
                    end
         | 
| 121 165 | 
             
                  end
         | 
| 122 166 |  | 
| 167 | 
            +
                  sig {params(str: String).void}
         | 
| 123 168 | 
             
                  def msg(str)
         | 
| 124 169 | 
             
                    puts "Braid: #{str}"
         | 
| 125 170 | 
             
                  end
         | 
| 126 171 |  | 
| 172 | 
            +
                  sig {params(cmd: T::Array[String]).void}
         | 
| 127 173 | 
             
                  def log(cmd)
         | 
| 128 | 
            -
                     | 
| 174 | 
            +
                    # Note: `Shellwords.shelljoin` follows Bourne shell quoting rules, as
         | 
| 175 | 
            +
                    # its documentation states.  This may not be what a Windows user
         | 
| 176 | 
            +
                    # expects, but it's not worth the trouble to try to find a library that
         | 
| 177 | 
            +
                    # produces something better on Windows, especially because it's unclear
         | 
| 178 | 
            +
                    # which of Windows's several different quoted formats we would use
         | 
| 179 | 
            +
                    # (e.g., CommandLineToArgvW, cmd.exe, or PowerShell).  The most
         | 
| 180 | 
            +
                    # important thing is to use _some_ unambiguous representation.
         | 
| 181 | 
            +
                    msg "Executing `#{Shellwords.shelljoin(cmd)}` in #{Dir.pwd}" if verbose?
         | 
| 129 182 | 
             
                  end
         | 
| 130 183 |  | 
| 184 | 
            +
                  sig {returns(T::Boolean)}
         | 
| 131 185 | 
             
                  def verbose?
         | 
| 132 186 | 
             
                    Braid.verbose
         | 
| 133 187 | 
             
                  end
         | 
| 134 188 | 
             
                end
         | 
| 135 189 |  | 
| 136 190 | 
             
                class Git < Proxy
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                  sig {returns(String)}
         | 
| 193 | 
            +
                  def self.command
         | 
| 194 | 
            +
                    'git'
         | 
| 195 | 
            +
                  end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                  # A string representing a Git object ID (i.e., hash).  This type alias is
         | 
| 198 | 
            +
                  # used as documentation and is not enforced, so there's a risk that we
         | 
| 199 | 
            +
                  # mistakenly mark something as an ObjectID when it can actually be a
         | 
| 200 | 
            +
                  # String that is not an ObjectID.
         | 
| 201 | 
            +
                  ObjectID = T.type_alias { String }
         | 
| 202 | 
            +
             | 
| 203 | 
            +
                  # A string containing an expression that can be evaluated to an object ID
         | 
| 204 | 
            +
                  # by `git rev-parse`.  Ditto the remark about lack of enforcement.
         | 
| 205 | 
            +
                  ObjectExpr = T.type_alias { String } 
         | 
| 206 | 
            +
             | 
| 137 207 | 
             
                  # Get the physical path to a file in the git repository (e.g.,
         | 
| 138 208 | 
             
                  # 'MERGE_MSG'), taking into account worktree configuration.  The returned
         | 
| 139 209 | 
             
                  # path may be absolute or relative to the current working directory.
         | 
| 210 | 
            +
                  sig {params(path: String).returns(String)}
         | 
| 140 211 | 
             
                  def repo_file_path(path)
         | 
| 141 | 
            -
                     | 
| 142 | 
            -
                      invoke(:rev_parse, '--git-path', path)
         | 
| 143 | 
            -
                    else
         | 
| 144 | 
            -
                      # Git < 2.5 doesn't support linked worktrees anyway.
         | 
| 145 | 
            -
                      File.join(invoke(:rev_parse, '--git-dir'), path)
         | 
| 146 | 
            -
                    end
         | 
| 212 | 
            +
                    invoke('rev-parse', ['--git-path', path])
         | 
| 147 213 | 
             
                  end
         | 
| 148 214 |  | 
| 149 215 | 
             
                  # If the current directory is not inside a git repository at all, this
         | 
| @@ -151,8 +217,9 @@ module Braid | |
| 151 217 | 
             
                  # propagated as a ShellExecutionError.  is_inside_worktree can return
         | 
| 152 218 | 
             
                  # false when inside a bare repository and in certain other rare cases such
         | 
| 153 219 | 
             
                  # as when the GIT_WORK_TREE environment variable is set.
         | 
| 220 | 
            +
                  sig {returns(T::Boolean)}
         | 
| 154 221 | 
             
                  def is_inside_worktree
         | 
| 155 | 
            -
                    invoke( | 
| 222 | 
            +
                    invoke('rev-parse', ['--is-inside-work-tree']) == 'true'
         | 
| 156 223 | 
             
                  end
         | 
| 157 224 |  | 
| 158 225 | 
             
                  # Get the prefix of the current directory relative to the worktree.  Empty
         | 
| @@ -160,21 +227,23 @@ module Braid | |
| 160 227 | 
             
                  # In some cases in which the current directory is not inside a worktree at
         | 
| 161 228 | 
             
                  # all, this will successfully return an empty string, so it may be
         | 
| 162 229 | 
             
                  # desirable to check is_inside_worktree first.
         | 
| 230 | 
            +
                  sig {returns(String)}
         | 
| 163 231 | 
             
                  def relative_working_dir
         | 
| 164 | 
            -
                    invoke( | 
| 232 | 
            +
                    invoke('rev-parse', ['--show-prefix'])
         | 
| 165 233 | 
             
                  end
         | 
| 166 234 |  | 
| 167 | 
            -
                   | 
| 168 | 
            -
             | 
| 235 | 
            +
                  sig {params(message: T.nilable(String), args: T::Array[String]).returns(T::Boolean)}
         | 
| 236 | 
            +
                  def commit(message, args = [])
         | 
| 237 | 
            +
                    cmd = ['git', 'commit', '--no-verify']
         | 
| 169 238 | 
             
                    message_file = nil
         | 
| 170 239 | 
             
                    if message # allow nil
         | 
| 171 240 | 
             
                      message_file = Tempfile.new('braid_commit')
         | 
| 172 241 | 
             
                      message_file.print("Braid: #{message}")
         | 
| 173 242 | 
             
                      message_file.flush
         | 
| 174 243 | 
             
                      message_file.close
         | 
| 175 | 
            -
                      cmd  | 
| 244 | 
            +
                      cmd += ['-F', T.must(message_file.path)]
         | 
| 176 245 | 
             
                    end
         | 
| 177 | 
            -
                    cmd  | 
| 246 | 
            +
                    cmd += args
         | 
| 178 247 | 
             
                    status, out, err = exec(cmd)
         | 
| 179 248 | 
             
                    message_file.unlink if message_file
         | 
| 180 249 |  | 
| @@ -187,50 +256,55 @@ module Braid | |
| 187 256 | 
             
                    end
         | 
| 188 257 | 
             
                  end
         | 
| 189 258 |  | 
| 190 | 
            -
                   | 
| 191 | 
            -
             | 
| 192 | 
            -
                     | 
| 193 | 
            -
             | 
| 194 | 
            -
             | 
| 195 | 
            -
                  def checkout(treeish)
         | 
| 196 | 
            -
                    invoke(:checkout, treeish)
         | 
| 197 | 
            -
                    true
         | 
| 259 | 
            +
                  sig {params(remote: T.nilable(String), args: T::Array[String]).void}
         | 
| 260 | 
            +
                  def fetch(remote = nil, args = [])
         | 
| 261 | 
            +
                    args = ['-n', remote] + args if remote
         | 
| 262 | 
            +
                    exec!(['git', 'fetch'] + args)
         | 
| 198 263 | 
             
                  end
         | 
| 199 264 |  | 
| 200 265 | 
             
                  # Returns the base commit or nil.
         | 
| 266 | 
            +
                  sig {params(target: ObjectExpr, source: ObjectExpr).returns(T.nilable(ObjectID))}
         | 
| 201 267 | 
             
                  def merge_base(target, source)
         | 
| 202 | 
            -
                    invoke( | 
| 268 | 
            +
                    invoke('merge-base', [target, source])
         | 
| 203 269 | 
             
                  rescue ShellExecutionError
         | 
| 204 270 | 
             
                    nil
         | 
| 205 271 | 
             
                  end
         | 
| 206 272 |  | 
| 207 | 
            -
                   | 
| 208 | 
            -
             | 
| 273 | 
            +
                  sig {params(expr: ObjectExpr).returns(ObjectID)}
         | 
| 274 | 
            +
                  def rev_parse(expr)
         | 
| 275 | 
            +
                    invoke('rev-parse', [expr])
         | 
| 209 276 | 
             
                  rescue ShellExecutionError
         | 
| 210 | 
            -
                    raise UnknownRevision,  | 
| 277 | 
            +
                    raise UnknownRevision, expr
         | 
| 211 278 | 
             
                  end
         | 
| 212 279 |  | 
| 213 280 | 
             
                  # Implies tracking.
         | 
| 281 | 
            +
                  #
         | 
| 282 | 
            +
                  # TODO (typing): Remove the return value if we're confident that nothing
         | 
| 283 | 
            +
                  # uses it, here and in similar cases.
         | 
| 284 | 
            +
                  sig {params(remote: String, path: String).returns(TrueClass)}
         | 
| 214 285 | 
             
                  def remote_add(remote, path)
         | 
| 215 | 
            -
                    invoke( | 
| 286 | 
            +
                    invoke('remote', ['add', remote, path])
         | 
| 216 287 | 
             
                    true
         | 
| 217 288 | 
             
                  end
         | 
| 218 289 |  | 
| 290 | 
            +
                  sig {params(remote: String).returns(TrueClass)}
         | 
| 219 291 | 
             
                  def remote_rm(remote)
         | 
| 220 | 
            -
                    invoke( | 
| 292 | 
            +
                    invoke('remote', ['rm', remote])
         | 
| 221 293 | 
             
                    true
         | 
| 222 294 | 
             
                  end
         | 
| 223 295 |  | 
| 224 296 | 
             
                  # Checks git remotes.
         | 
| 297 | 
            +
                  sig {params(remote: String).returns(T.nilable(String))}
         | 
| 225 298 | 
             
                  def remote_url(remote)
         | 
| 226 299 | 
             
                    key = "remote.#{remote}.url"
         | 
| 227 | 
            -
                    invoke( | 
| 300 | 
            +
                    invoke('config', [key])
         | 
| 228 301 | 
             
                  rescue ShellExecutionError
         | 
| 229 302 | 
             
                    nil
         | 
| 230 303 | 
             
                  end
         | 
| 231 304 |  | 
| 305 | 
            +
                  sig {params(target: ObjectExpr).returns(TrueClass)}
         | 
| 232 306 | 
             
                  def reset_hard(target)
         | 
| 233 | 
            -
                    invoke( | 
| 307 | 
            +
                    invoke('reset', ['--hard', target])
         | 
| 234 308 | 
             
                    true
         | 
| 235 309 | 
             
                  end
         | 
| 236 310 |  | 
| @@ -242,41 +316,59 @@ module Braid | |
| 242 316 | 
             
                  # 'recursive' part (i.e., merge of bases) does not come into play and only
         | 
| 243 317 | 
             
                  # the trees matter.  But for some reason, Git's smartest tree merge
         | 
| 244 318 | 
             
                  # algorithm is only available via the 'recursive' strategy.
         | 
| 319 | 
            +
                  sig {params(base_treeish: ObjectExpr, local_treeish: ObjectExpr, remote_treeish: ObjectExpr).returns(TrueClass)}
         | 
| 245 320 | 
             
                  def merge_trees(base_treeish, local_treeish, remote_treeish)
         | 
| 246 | 
            -
                    invoke( | 
| 321 | 
            +
                    invoke('merge-recursive', [base_treeish, '--', local_treeish, remote_treeish])
         | 
| 247 322 | 
             
                    true
         | 
| 248 323 | 
             
                  rescue ShellExecutionError => error
         | 
| 249 324 | 
             
                    # 'CONFLICT' messages go to stdout.
         | 
| 250 325 | 
             
                    raise MergeError, error.out
         | 
| 251 326 | 
             
                  end
         | 
| 252 327 |  | 
| 328 | 
            +
                  sig {params(prefix: String).returns(String)}
         | 
| 253 329 | 
             
                  def read_ls_files(prefix)
         | 
| 254 | 
            -
                    invoke('ls-files', prefix)
         | 
| 330 | 
            +
                    invoke('ls-files', [prefix])
         | 
| 255 331 | 
             
                  end
         | 
| 256 332 |  | 
| 257 333 | 
             
                  class BlobWithMode
         | 
| 334 | 
            +
                    extend T::Sig
         | 
| 335 | 
            +
                    sig {params(hash: ObjectID, mode: String).void}
         | 
| 258 336 | 
             
                    def initialize(hash, mode)
         | 
| 259 337 | 
             
                      @hash = hash
         | 
| 260 338 | 
             
                      @mode = mode
         | 
| 261 339 | 
             
                    end
         | 
| 262 | 
            -
                     | 
| 340 | 
            +
                    sig {returns(ObjectID)}
         | 
| 341 | 
            +
                    attr_reader :hash
         | 
| 342 | 
            +
                    sig {returns(String)}
         | 
| 343 | 
            +
                    attr_reader :mode
         | 
| 263 344 | 
             
                  end
         | 
| 264 345 | 
             
                  # Allow the class to be referenced as `git.BlobWithMode`.
         | 
| 346 | 
            +
                  sig {returns(T.class_of(BlobWithMode))}
         | 
| 265 347 | 
             
                  def BlobWithMode
         | 
| 266 348 | 
             
                    Git::BlobWithMode
         | 
| 267 349 | 
             
                  end
         | 
| 350 | 
            +
                  # An ObjectID used as a TreeItem represents a tree.
         | 
| 351 | 
            +
                  TreeItem = T.type_alias { T.any(ObjectID, BlobWithMode) }
         | 
| 268 352 |  | 
| 269 353 | 
             
                  # Get the item at the given path in the given tree.  If it's a tree, just
         | 
| 270 354 | 
             
                  # return its hash; if it's a blob, return a BlobWithMode object.  (This is
         | 
| 271 355 | 
             
                  # how we remember the mode for single-file mirrors.)
         | 
| 356 | 
            +
                  # TODO (typing): Should `path` be nilable?
         | 
| 357 | 
            +
                  sig {params(tree: ObjectExpr, path: T.nilable(String)).returns(TreeItem)}
         | 
| 272 358 | 
             
                  def get_tree_item(tree, path)
         | 
| 273 359 | 
             
                    if path.nil? || path == ''
         | 
| 274 360 | 
             
                      tree
         | 
| 275 361 | 
             
                    else
         | 
| 276 | 
            -
                      m = /^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke( | 
| 277 | 
            -
                       | 
| 278 | 
            -
             | 
| 279 | 
            -
             | 
| 362 | 
            +
                      m = /^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke('ls-tree', [tree, path]))
         | 
| 363 | 
            +
                      if m.nil?
         | 
| 364 | 
            +
                        # This can happen if the user runs `braid add` with a `--path` that
         | 
| 365 | 
            +
                        # doesn't exist.  TODO: Make the error message more user-friendly in
         | 
| 366 | 
            +
                        # that case.
         | 
| 367 | 
            +
                        raise ShellExecutionError, 'No tree item exists at the given path'
         | 
| 368 | 
            +
                      end
         | 
| 369 | 
            +
                      mode = T.must(m[1])
         | 
| 370 | 
            +
                      type = T.must(m[2])
         | 
| 371 | 
            +
                      hash = T.must(m[3])
         | 
| 280 372 | 
             
                      if type == 'tree'
         | 
| 281 373 | 
             
                        hash
         | 
| 282 374 | 
             
                      elsif type == 'blob'
         | 
| @@ -291,37 +383,47 @@ module Braid | |
| 291 383 | 
             
                  # path.  If update_worktree is true, then update the worktree, otherwise
         | 
| 292 384 | 
             
                  # disregard the state of the worktree (most useful with a temporary index
         | 
| 293 385 | 
             
                  # file).
         | 
| 386 | 
            +
                  sig {params(item: TreeItem, path: String, update_worktree: T::Boolean).void}
         | 
| 294 387 | 
             
                  def add_item_to_index(item, path, update_worktree)
         | 
| 295 388 | 
             
                    if item.is_a?(BlobWithMode)
         | 
| 296 | 
            -
                       | 
| 297 | 
            -
                      # wasn't added until 2.0.0.
         | 
| 298 | 
            -
                      invoke(:update_index, '--add', '--cacheinfo', item.mode, item.hash, path)
         | 
| 389 | 
            +
                      invoke('update-index', ['--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}"])
         | 
| 299 390 | 
             
                      if update_worktree
         | 
| 300 391 | 
             
                        # XXX If this fails, we've already updated the index.
         | 
| 301 | 
            -
                        invoke( | 
| 392 | 
            +
                        invoke('checkout-index', [path])
         | 
| 302 393 | 
             
                      end
         | 
| 303 394 | 
             
                    else
         | 
| 304 395 | 
             
                      # According to
         | 
| 305 396 | 
             
                      # https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
         | 
| 306 397 | 
             
                      # `--prefix=` is valid if the path is empty.
         | 
| 307 | 
            -
                      invoke( | 
| 398 | 
            +
                      invoke('read-tree', ["--prefix=#{path}", update_worktree ? '-u' : '-i', item])
         | 
| 308 399 | 
             
                    end
         | 
| 309 400 | 
             
                  end
         | 
| 310 401 |  | 
| 311 402 | 
             
                  # Read tree into the root of the index.  This may not be the preferred way
         | 
| 312 403 | 
             
                  # to do it, but it seems to work.
         | 
| 404 | 
            +
                  sig {params(treeish: ObjectExpr).void}
         | 
| 313 405 | 
             
                  def read_tree_im(treeish)
         | 
| 314 | 
            -
                    invoke( | 
| 315 | 
            -
             | 
| 406 | 
            +
                    invoke('read-tree', ['-im', treeish])
         | 
| 407 | 
            +
                  end
         | 
| 408 | 
            +
             | 
| 409 | 
            +
                  sig {params(treeish: ObjectExpr).void}
         | 
| 410 | 
            +
                  def read_tree_um(treeish)
         | 
| 411 | 
            +
                    invoke('read-tree', ['-um', treeish])
         | 
| 316 412 | 
             
                  end
         | 
| 317 413 |  | 
| 318 414 | 
             
                  # Write a tree object for the current index and return its ID.
         | 
| 415 | 
            +
                  sig {returns(ObjectID)}
         | 
| 319 416 | 
             
                  def write_tree
         | 
| 320 | 
            -
                    invoke( | 
| 417 | 
            +
                    invoke('write-tree', [])
         | 
| 321 418 | 
             
                  end
         | 
| 322 419 |  | 
| 323 420 | 
             
                  # Execute a block using a temporary git index file, initially empty.
         | 
| 324 | 
            -
                   | 
| 421 | 
            +
                  sig {
         | 
| 422 | 
            +
                    type_parameters(:R).params(
         | 
| 423 | 
            +
                      blk: T.proc.returns(T.type_parameter(:R))
         | 
| 424 | 
            +
                    ).returns(T.type_parameter(:R))
         | 
| 425 | 
            +
                  }
         | 
| 426 | 
            +
                  def with_temporary_index(&blk)
         | 
| 325 427 | 
             
                    Dir.mktmpdir('braid_index') do |dir|
         | 
| 326 428 | 
             
                      Operations::with_modified_environment(
         | 
| 327 429 | 
             
                        {'GIT_INDEX_FILE' => File.join(dir, 'index')}) do
         | 
| @@ -330,6 +432,7 @@ module Braid | |
| 330 432 | 
             
                    end
         | 
| 331 433 | 
             
                  end
         | 
| 332 434 |  | 
| 435 | 
            +
                  sig {params(main_content: T.nilable(ObjectExpr), item_path: String, item: TreeItem).returns(ObjectID)}
         | 
| 333 436 | 
             
                  def make_tree_with_item(main_content, item_path, item)
         | 
| 334 437 | 
             
                    with_temporary_index do
         | 
| 335 438 | 
             
                      # If item_path is '', then rm_r_cached will fail.  But in that case,
         | 
| @@ -344,103 +447,161 @@ module Braid | |
| 344 447 | 
             
                    end
         | 
| 345 448 | 
             
                  end
         | 
| 346 449 |  | 
| 450 | 
            +
                  sig {params(args: T::Array[String]).returns(T.nilable(String))}
         | 
| 347 451 | 
             
                  def config(args)
         | 
| 348 | 
            -
                    invoke( | 
| 452 | 
            +
                    invoke('config', args) rescue nil
         | 
| 349 453 | 
             
                  end
         | 
| 350 454 |  | 
| 455 | 
            +
                  sig {params(path: String).void}
         | 
| 456 | 
            +
                  def add(path)
         | 
| 457 | 
            +
                    invoke('add', [path])
         | 
| 458 | 
            +
                  end
         | 
| 459 | 
            +
             | 
| 460 | 
            +
                  sig {params(path: String).void}
         | 
| 461 | 
            +
                  def rm(path)
         | 
| 462 | 
            +
                    invoke('rm', [path])
         | 
| 463 | 
            +
                  end
         | 
| 464 | 
            +
             | 
| 465 | 
            +
                  sig {params(path: String).returns(TrueClass)}
         | 
| 351 466 | 
             
                  def rm_r(path)
         | 
| 352 | 
            -
                    invoke( | 
| 467 | 
            +
                    invoke('rm', ['-r', path])
         | 
| 353 468 | 
             
                    true
         | 
| 354 469 | 
             
                  end
         | 
| 355 470 |  | 
| 356 471 | 
             
                  # Remove from index only.
         | 
| 472 | 
            +
                  sig {params(path: String).returns(TrueClass)}
         | 
| 357 473 | 
             
                  def rm_r_cached(path)
         | 
| 358 | 
            -
                    invoke( | 
| 474 | 
            +
                    invoke('rm', ['-r', '--cached', path])
         | 
| 359 475 | 
             
                    true
         | 
| 360 476 | 
             
                  end
         | 
| 361 477 |  | 
| 478 | 
            +
                  sig {params(path: String, treeish: ObjectExpr).returns(ObjectID)}
         | 
| 362 479 | 
             
                  def tree_hash(path, treeish = 'HEAD')
         | 
| 363 | 
            -
                    out = invoke( | 
| 364 | 
            -
                    out.split[2]
         | 
| 480 | 
            +
                    out = invoke('ls-tree', [treeish, '-d', path])
         | 
| 481 | 
            +
                    T.must(out.split[2])
         | 
| 482 | 
            +
                  end
         | 
| 483 | 
            +
             | 
| 484 | 
            +
                  sig {params(args: T::Array[String]).returns(String)}
         | 
| 485 | 
            +
                  def diff(args)
         | 
| 486 | 
            +
                    invoke('diff', args)
         | 
| 365 487 | 
             
                  end
         | 
| 366 488 |  | 
| 367 | 
            -
                   | 
| 489 | 
            +
                  sig {params(args: T::Array[String]).returns(ProcessStatusOrInteger)}
         | 
| 490 | 
            +
                  def diff_to_stdout(args)
         | 
| 368 491 | 
             
                    # For now, ignore the exit code.  It can be 141 (SIGPIPE) if the user
         | 
| 369 492 | 
             
                    # quits the pager before reading all the output.
         | 
| 370 | 
            -
                    system( | 
| 493 | 
            +
                    system(['git', 'diff'] + args)
         | 
| 371 494 | 
             
                  end
         | 
| 372 495 |  | 
| 496 | 
            +
                  sig {returns(T::Boolean)}
         | 
| 373 497 | 
             
                  def status_clean?
         | 
| 374 | 
            -
                    _, out, _ = exec('git status')
         | 
| 498 | 
            +
                    _, out, _ = exec(['git', 'status'])
         | 
| 375 499 | 
             
                    !out.split("\n").grep(/nothing to commit/).empty?
         | 
| 376 500 | 
             
                  end
         | 
| 377 501 |  | 
| 502 | 
            +
                  sig {void}
         | 
| 378 503 | 
             
                  def ensure_clean!
         | 
| 379 504 | 
             
                    status_clean? || raise(LocalChangesPresent)
         | 
| 380 505 | 
             
                  end
         | 
| 381 506 |  | 
| 507 | 
            +
                  sig {returns(ObjectID)}
         | 
| 382 508 | 
             
                  def head
         | 
| 383 509 | 
             
                    rev_parse('HEAD')
         | 
| 384 510 | 
             
                  end
         | 
| 385 511 |  | 
| 386 | 
            -
                   | 
| 387 | 
            -
             | 
| 388 | 
            -
                     | 
| 512 | 
            +
                  sig {void}
         | 
| 513 | 
            +
                  def init
         | 
| 514 | 
            +
                    invoke('init', [])
         | 
| 515 | 
            +
                  end
         | 
| 516 | 
            +
             | 
| 517 | 
            +
                  sig {params(args: T::Array[String]).void}
         | 
| 518 | 
            +
                  def clone(args)
         | 
| 519 | 
            +
                    invoke('clone', args)
         | 
| 520 | 
            +
                  end
         | 
| 521 | 
            +
             | 
| 522 | 
            +
                  # Wrappers for Git commands that were called via `method_missing` before
         | 
| 523 | 
            +
                  # the move to static typing but for which the existing calls don't follow
         | 
| 524 | 
            +
                  # a clear enough pattern around which we could design a narrower API than
         | 
| 525 | 
            +
                  # forwarding an arbitrary argument list.  We may narrow the API in the
         | 
| 526 | 
            +
                  # future if it becomes clear what it should be.
         | 
| 527 | 
            +
             | 
| 528 | 
            +
                  sig {params(args: T::Array[String]).returns(String)}
         | 
| 529 | 
            +
                  def rev_list(args)
         | 
| 530 | 
            +
                    invoke('rev-list', args)
         | 
| 531 | 
            +
                  end
         | 
| 532 | 
            +
             | 
| 533 | 
            +
                  sig {params(args: T::Array[String]).void}
         | 
| 534 | 
            +
                  def update_ref(args)
         | 
| 535 | 
            +
                    invoke('update-ref', args)
         | 
| 536 | 
            +
                  end
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                  sig {params(args: T::Array[String]).void}
         | 
| 539 | 
            +
                  def push(args)
         | 
| 540 | 
            +
                    invoke('push', args)
         | 
| 389 541 | 
             
                  end
         | 
| 390 542 |  | 
| 391 | 
            -
                   | 
| 392 | 
            -
             | 
| 393 | 
            -
                    invoke( | 
| 543 | 
            +
                  sig {params(args: T::Array[String]).returns(String)}
         | 
| 544 | 
            +
                  def ls_remote(args)
         | 
| 545 | 
            +
                    invoke('ls-remote', args)
         | 
| 394 546 | 
             
                  end
         | 
| 395 547 |  | 
| 396 548 | 
             
                  private
         | 
| 397 549 |  | 
| 550 | 
            +
                  sig {params(name: String).returns(T::Array[String])}
         | 
| 398 551 | 
             
                  def command(name)
         | 
| 399 | 
            -
                     | 
| 552 | 
            +
                    [self.class.command, name]
         | 
| 400 553 | 
             
                  end
         | 
| 401 554 | 
             
                end
         | 
| 402 555 |  | 
| 403 556 | 
             
                class GitCache
         | 
| 557 | 
            +
                  extend T::Sig
         | 
| 404 558 | 
             
                  include Singleton
         | 
| 405 559 |  | 
| 560 | 
            +
                  sig {params(url: String).void}
         | 
| 406 561 | 
             
                  def fetch(url)
         | 
| 407 562 | 
             
                    dir = path(url)
         | 
| 408 563 |  | 
| 409 564 | 
             
                    # remove local cache if it was created with --no-checkout
         | 
| 410 | 
            -
                    if File. | 
| 565 | 
            +
                    if File.exist?("#{dir}/.git")
         | 
| 411 566 | 
             
                      FileUtils.rm_r(dir)
         | 
| 412 567 | 
             
                    end
         | 
| 413 568 |  | 
| 414 | 
            -
                    if File. | 
| 569 | 
            +
                    if File.exist?(dir)
         | 
| 415 570 | 
             
                      Dir.chdir(dir) do
         | 
| 416 571 | 
             
                        git.fetch
         | 
| 417 572 | 
             
                      end
         | 
| 418 573 | 
             
                    else
         | 
| 419 574 | 
             
                      FileUtils.mkdir_p(local_cache_dir)
         | 
| 420 | 
            -
                      git.clone('--mirror', url, dir)
         | 
| 575 | 
            +
                      git.clone(['--mirror', url, dir])
         | 
| 421 576 | 
             
                    end
         | 
| 422 577 | 
             
                  end
         | 
| 423 578 |  | 
| 579 | 
            +
                  sig {params(url: String).returns(String)}
         | 
| 424 580 | 
             
                  def path(url)
         | 
| 425 581 | 
             
                    File.join(local_cache_dir, url.gsub(/[\/:@]/, '_'))
         | 
| 426 582 | 
             
                  end
         | 
| 427 583 |  | 
| 428 584 | 
             
                  private
         | 
| 429 585 |  | 
| 586 | 
            +
                  sig {returns(String)}
         | 
| 430 587 | 
             
                  def local_cache_dir
         | 
| 431 588 | 
             
                    Braid.local_cache_dir
         | 
| 432 589 | 
             
                  end
         | 
| 433 590 |  | 
| 591 | 
            +
                  sig {returns(Git)}
         | 
| 434 592 | 
             
                  def git
         | 
| 435 593 | 
             
                    Git.instance
         | 
| 436 594 | 
             
                  end
         | 
| 437 595 | 
             
                end
         | 
| 438 596 |  | 
| 439 597 | 
             
                module VersionControl
         | 
| 598 | 
            +
                  extend T::Sig
         | 
| 599 | 
            +
                  sig {returns(Git)}
         | 
| 440 600 | 
             
                  def git
         | 
| 441 601 | 
             
                    Git.instance
         | 
| 442 602 | 
             
                  end
         | 
| 443 603 |  | 
| 604 | 
            +
                  sig {returns(GitCache)}
         | 
| 444 605 | 
             
                  def git_cache
         | 
| 445 606 | 
             
                    GitCache.instance
         | 
| 446 607 | 
             
                  end
         | 
| @@ -36,6 +36,10 @@ module Braid | |
| 36 36 | 
             
                def self.must(value)
         | 
| 37 37 | 
             
                  value
         | 
| 38 38 | 
             
                end
         | 
| 39 | 
            +
                def self.unsafe(value)
         | 
| 40 | 
            +
                  value
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
                def self.bind(value, type); end
         | 
| 39 43 |  | 
| 40 44 | 
             
                class FakeType
         | 
| 41 45 | 
             
                  include Singleton
         | 
| @@ -52,6 +56,12 @@ module Braid | |
| 52 56 | 
             
                def self.untyped
         | 
| 53 57 | 
             
                  FAKE_TYPE
         | 
| 54 58 | 
             
                end
         | 
| 59 | 
            +
                def self.noreturn
         | 
| 60 | 
            +
                  FAKE_TYPE
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
                def self.any(*types)
         | 
| 63 | 
            +
                  FAKE_TYPE
         | 
| 64 | 
            +
                end
         | 
| 55 65 | 
             
                Boolean = FAKE_TYPE
         | 
| 56 66 | 
             
                module Array
         | 
| 57 67 | 
             
                  def self.[](type)
         | 
    
        data/lib/braid/version.rb
    CHANGED
    
    
    
        data/lib/braid.rb
    CHANGED
    
    | @@ -12,14 +12,13 @@ module Braid | |
| 12 12 | 
             
              # See the background in the "Supported environments" section of README.md.
         | 
| 13 13 | 
             
              #
         | 
| 14 14 | 
             
              # The newest Git feature that Braid is currently known to rely on is
         | 
| 15 | 
            -
              # ` | 
| 16 | 
            -
              # spec/integration/push_spec.rb), which was added in Git 2.3.0 (in 2015).  It
         | 
| 15 | 
            +
              # `git ls-remote --symref`, which was added in Git 2.8.0 (in 2016).  It
         | 
| 17 16 | 
             
              # doesn't seem worth even a small amount of work to remove that dependency and
         | 
| 18 17 | 
             
              # support even older versions of Git.  So set that as the declared requirement
         | 
| 19 18 | 
             
              # for now.  In general, a reasonable approach might be to try to support the
         | 
| 20 19 | 
             
              # oldest version of Git in current "long-term support" versions of popular OS
         | 
| 21 20 | 
             
              # distributions.
         | 
| 22 | 
            -
              REQUIRED_GIT_VERSION = '2. | 
| 21 | 
            +
              REQUIRED_GIT_VERSION = '2.8.0'
         | 
| 23 22 |  | 
| 24 23 | 
             
              @verbose = T.let(false, T::Boolean)
         | 
| 25 24 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,17 +1,17 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: braid
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1.1. | 
| 4 | 
            +
              version: 1.1.9
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Cristi Balan
         | 
| 8 8 | 
             
            - Norbert Crombach
         | 
| 9 9 | 
             
            - Peter Donald
         | 
| 10 10 | 
             
            - Matt McCutchen
         | 
| 11 | 
            -
            autorequire:
         | 
| 11 | 
            +
            autorequire: 
         | 
| 12 12 | 
             
            bindir: exe
         | 
| 13 13 | 
             
            cert_chain: []
         | 
| 14 | 
            -
            date:  | 
| 14 | 
            +
            date: 2023-01-11 00:00:00.000000000 Z
         | 
| 15 15 | 
             
            dependencies:
         | 
| 16 16 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 17 17 | 
             
              name: main
         | 
| @@ -97,6 +97,20 @@ dependencies: | |
| 97 97 | 
             
                - - ">="
         | 
| 98 98 | 
             
                  - !ruby/object:Gem::Version
         | 
| 99 99 | 
             
                    version: '0'
         | 
| 100 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 101 | 
            +
              name: irb
         | 
| 102 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 103 | 
            +
                requirements:
         | 
| 104 | 
            +
                - - ">="
         | 
| 105 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 106 | 
            +
                    version: '0'
         | 
| 107 | 
            +
              type: :development
         | 
| 108 | 
            +
              prerelease: false
         | 
| 109 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 110 | 
            +
                requirements:
         | 
| 111 | 
            +
                - - ">="
         | 
| 112 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 113 | 
            +
                    version: '0'
         | 
| 100 114 | 
             
            description: A simple tool for tracking vendor branches in git.
         | 
| 101 115 | 
             
            email: evil@che.lu norbert.crombach@primetheory.org peter@realityforge.org matt@mattmccutchen.net
         | 
| 102 116 | 
             
            executables:
         | 
| @@ -128,7 +142,7 @@ files: | |
| 128 142 | 
             
            homepage: https://github.com/cristibalan/braid
         | 
| 129 143 | 
             
            licenses: []
         | 
| 130 144 | 
             
            metadata: {}
         | 
| 131 | 
            -
            post_install_message:
         | 
| 145 | 
            +
            post_install_message: 
         | 
| 132 146 | 
             
            rdoc_options:
         | 
| 133 147 | 
             
            - "--line-numbers"
         | 
| 134 148 | 
             
            - "--inline-source"
         | 
| @@ -148,8 +162,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 148 162 | 
             
                - !ruby/object:Gem::Version
         | 
| 149 163 | 
             
                  version: '0'
         | 
| 150 164 | 
             
            requirements: []
         | 
| 151 | 
            -
            rubygems_version: 3. | 
| 152 | 
            -
            signing_key:
         | 
| 165 | 
            +
            rubygems_version: 3.1.4
         | 
| 166 | 
            +
            signing_key: 
         | 
| 153 167 | 
             
            specification_version: 4
         | 
| 154 168 | 
             
            summary: A simple tool for tracking vendor branches in git.
         | 
| 155 169 | 
             
            test_files: []
         |