Class: ShopifyCLI::Git

Inherits:
Object
  • Object
show all
Defined in:
lib/shopify_cli/git.rb

Overview

ShopifyCLI::Git wraps git functionality to make it easier to integrate will git.

Class Method Summary collapse

Class Method Details

.available?(ctx) ⇒ Boolean

Check if the current working directory is a Git repository

Returns:

  • (Boolean)


16
17
18
19
20
21
# File 'lib/shopify_cli/git.rb', line 16

def available?(ctx)
  _output, status = ctx.capture2e("git", "status")
  status.success?
rescue Errno::ENOENT # git is not installed
  false
end

.branches(ctx) ⇒ Object

will fetch the repos list of branches.

#### Parameters

  • ‘ctx` - the current running context of your command, defaults to a new context.

#### Returns

  • ‘branches` - [String] an array of strings that are branch names

#### Example

branches = ShopifyCLI::Git.branches(@ctx)


175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/shopify_cli/git.rb', line 175

def branches(ctx)
  output, status = ctx.capture2e("git", "branch", "--list", "--format=%(refname:short)")
  ctx.abort(ctx.message("core.git.error.no_branches_found")) unless status.success?

  branches = if output == ""
    ["master"]
  else
    output.split("\n")
  end

  branches
end

.clone(repo_with_branch, dest, ctx: Context.new) ⇒ Object

calls git to clone a new repo into a supplied destination, it will also output progress of the cloning process into a new progress bar

#### Parameters

  • ‘repo_with_branch` - a git url for git to clone the repo from

  • ‘dest` - a filepath to where the repo should be cloned to

  • ‘ctx` - the current running context of your command, defaults to a new context.

#### Returns

  • ‘sha_string` - string of the sha of the most recent commit to the repo

#### Example

ShopifyCLI::Git.clone('[email protected]:shopify/test.git', 'test-app')


131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/shopify_cli/git.rb', line 131

def clone(repo_with_branch, dest, ctx: Context.new)
  if Dir.exist?(dest) && !Dir.empty?(dest)
    ctx.abort(ctx.message("core.git.error.directory_exists"))
  else
    msg = []
    # require at usage point to not slow down CLI startup
    # https://github.com/Shopify/shopify-cli/pull/698#discussion_r444342445
    require "open3"

    repo, branch = repo_with_branch.split("#")
    git_cmd = git_clone_command(repo, dest, branch)

    success_message = ctx.message("core.git.cloned", dest)

    CLI::UI::Frame.open(ctx.message("core.git.cloning", repo, dest), success_text: success_message) do
      CLI::UI::Progress.progress do |bar|
        success = Open3.popen3("git", *git_cmd, "--progress") do |_stdin, _stdout, stderr, thread|
          msg = clone_progress(stderr, bar: bar)

          thread.value
        end.success?

        ctx.abort((msg.join("\n"))) unless success
        bar.tick(set_percent: 1.0)
      end
    end
  end
end

.clone_progress(stderr, bar: nil) ⇒ Object

handles showing the progress of the git clone command. if block given, assumes passing percent to block, otherwise increments bar for progress bar

#### Parameters

  • ‘stderr` - Open3.popen3 output stream

  • ‘bar` - progress bar object to set percent



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
# File 'lib/shopify_cli/git.rb', line 288

def clone_progress(stderr, bar: nil)
  msg = []

  while (line = stderr.gets)
    msg << line.chomp
    next unless line.strip.start_with?("Receiving objects:")
    percent = (line.match(/Receiving objects:\s+(\d+)/)[1].to_f / 100).round(2)

    if block_given?
      yield percent
    elsif !bar.nil?
      bar.tick(set_percent: percent)
    end
  end

  msg
end

.exists?(ctx) ⇒ Boolean

Check if Git exists in the environment

Returns:

  • (Boolean)


8
9
10
11
12
13
# File 'lib/shopify_cli/git.rb', line 8

def exists?(ctx)
  _output, status = ctx.capture2e("git", "version")
  status.success?
rescue Errno::ENOENT # git is not installed
  false
end

.git_clone_command(repo, dest, branch) ⇒ Object

returns array with components of git clone command

#### Parameters

  • ‘repo` - repo url without branch name

  • ‘dest` - a filepath to where the repo should be cloned to

  • ‘branch` - branch name when cloning

#### Returns

  • array of strings

#### Example

["clone", "--single-branch", "--branch", "test-branch", "test-app"]


65
66
67
68
69
70
71
# File 'lib/shopify_cli/git.rb', line 65

def git_clone_command(repo, dest, branch)
  if branch
    ["clone", "--single-branch", "--branch", branch, repo, dest]
  else
    ["clone", "--single-branch", repo, dest]
  end
end

.init(ctx) ⇒ Object

will initialize a new repo in the current directory. This will output if it was successful or not.

#### Parameters

  • ‘ctx` - the current running context of your command, defaults to a new context.

#### Example

ShopifyCLI::Git.init(@ctx)


236
237
238
239
240
241
242
243
244
245
246
# File 'lib/shopify_cli/git.rb', line 236

def init(ctx)
  output, status = ctx.capture2e("git", "status")

  unless status.success?
    ctx.abort(ctx.message("core.git.error.repo_not_initiated"))
  end

  if output.include?("No commits yet")
    ctx.abort(ctx.message("core.git.error.no_commits_made"))
  end
end

.merge_file(current_file, base_file, other_file, opts = [], ctx: Context.new) ⇒ Object

Run git three-way file merge (it doesn’t require an initialized git repository)

#### Parameters

  • ‘current_file - string path of the current file

  • ‘base_file` - string path of the base file

  • ‘other_file` - string path of the other file

  • ‘opts` - list of “git merge-file” options. Valid values:

    - "-q"       - do not warn about conflicts
    - "--diff3"  - show conflicts
    - "--ours"   - resolve conflicts favoring lines from `current_file`
    - "--theirs" - resolve conflicts favoring lines from `other_file`
    - "--union"  - resolve conflicts favoring lines from both files
    - "-p"       - send results to standard output instead of
                 overwriting the `current_file`
    
  • ‘ctx` - the current running context of your command, defaults to a new context

#### Returns

  • standard output from git

#### Example

output = ShopifyCLI::Git.merge_file(current_file, base_file, other_file, opts, ctx: ctx)


214
215
216
217
218
219
220
221
222
# File 'lib/shopify_cli/git.rb', line 214

def merge_file(current_file, base_file, other_file, opts = [], ctx: Context.new)
  output, status = ctx.capture2e("git", "merge-file", current_file, base_file, other_file, *opts)

  unless status.success?
    ctx.abort(ctx.message("core.git.error.merge_failed"))
  end

  output
end

.raw_clone(repo_with_branch, dest, ctx: Context.new) ⇒ Object

calls git to clone a new repo into a supplied destination, it will also call a supplied block with the percentage of clone completion

#### Parameters

  • ‘repo_with_branch` - a git url for git to clone the repo from

  • ‘dest` - a filepath to where the repo should be cloned to

  • ‘ctx` - the current running context of your command, defaults to a new context.

#### Returns

  • ‘sha_string` - string of the sha of the most recent commit to the repo

#### Example

ShopifyCLI::Git.raw_clone('[email protected]:shopify/test.git', 'test-app')


91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/shopify_cli/git.rb', line 91

def raw_clone(repo_with_branch, dest, ctx: Context.new)
  if Dir.exist?(dest) && !Dir.empty?(dest)
    ctx.abort(ctx.message("core.git.error.directory_exists"))
  else
    msg = []
    # require at usage point to not slow down CLI startup
    # https://github.com/Shopify/shopify-cli/pull/698#discussion_r444342445
    require "open3"

    repo, branch = repo_with_branch.split("#")
    git_cmd = git_clone_command(repo, dest, branch)

    success = Open3.popen3("git", *git_cmd, "--progress") do |_stdin, _stdout, stderr, thread|
      msg = clone_progress(stderr, bar: nil)

      thread.value
    end.success?

    ctx.abort((msg.join("\n"))) unless success
  end
end

.sha(dir: Dir.pwd, ctx: Context.new) ⇒ Object

will return the current sha of the cli repo

#### Parameters

  • ‘dir` - the directory of the git repo. This defaults to the cli repo

  • ‘ctx` - the current running context of your command

#### Returns

  • ‘sha_string` - string of the sha of the most recent commit to the repo

#### Example

ShopifyCLI::Git.sha

Some environments don’t have git in PATH and this prevents the execution from raising an error app.bugsnag.com/shopify/shopify-cli/errors/615dd36365ce57000889d4c5



42
43
44
45
46
# File 'lib/shopify_cli/git.rb', line 42

def sha(dir: Dir.pwd, ctx: Context.new)
  if available?(ctx)
    rev_parse("HEAD", dir: dir, ctx: ctx)
  end
end

.sparse_checkout(repo, set, branch, ctx) ⇒ Object



248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/shopify_cli/git.rb', line 248

def sparse_checkout(repo, set, branch, ctx)
  _, status = ctx.capture2e("git init")
  unless status.success?
    ctx.abort(ctx.message("core.git.error.repo_not_initiated"))
  end

  _, status = ctx.capture2e("git remote add -f origin #{repo}")
  unless status.success?
    ctx.abort(ctx.message("core.git.error.remote_not_added"))
  end

  _, status = ctx.capture2e("git config core.sparsecheckout true")
  unless status.success?
    ctx.abort(ctx.message("core.git.error.sparse_checkout_not_enabled"))
  end

  _, status = ctx.capture2e("git sparse-checkout set #{set}")
  unless status.success?
    ctx.abort(ctx.message("core.git.error.sparse_checkout_not_set"))
  end

  resp, status = ctx.capture2e("git pull origin #{branch}")
  unless status.success?
    if resp.include?("fatal: couldn't find remote ref")
      ctx.abort(ctx.message("core.git.error.pull_failed_bad_branch", branch))
    end
    ctx.abort(ctx.message("core.git.error.pull_failed"))
  end
end