Class: SugarJar::Commands

Inherits:
Object
  • Object
show all
Defined in:
lib/sugarjar/commands.rb,
lib/sugarjar/commands/up.rb,
lib/sugarjar/commands/push.rb,
lib/sugarjar/commands/amend.rb,
lib/sugarjar/commands/bclean.rb,
lib/sugarjar/commands/branch.rb,
lib/sugarjar/commands/checks.rb,
lib/sugarjar/commands/feature.rb,
lib/sugarjar/commands/debuginfo.rb,
lib/sugarjar/commands/smartclone.rb,
lib/sugarjar/commands/pullsuggestions.rb,
lib/sugarjar/commands/smartpullrequest.rb

Overview

This is the workhorse of SugarJar. Short of #initialize, all other public methods are “commands”. Anything in private is internal implementation details.

Constant Summary collapse

MAIN_BRANCHES =
%w{master main}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Commands

Returns a new instance of Commands.


26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/sugarjar/commands.rb', line 26

def initialize(options)
  SugarJar::Log.debug("Commands.initialize options: #{options}")
  @ignore_dirty = options['ignore_dirty']
  @ignore_prerun_failure = options['ignore_prerun_failure']
  @repo_config = SugarJar::RepoConfig.config
  SugarJar::Log.debug("Repoconfig: #{@repo_config}")
  @color = options['color']
  @pr_autofill = options['pr_autofill']
  @pr_autostack = options['pr_autostack']
  @feature_prefix = options['feature_prefix']
  @checks = {}
  @main_branch = nil
  @main_remote_branches = {}
  @ghuser = @repo_config['github_user'] || options['github_user']
  @ghhost = @repo_config['github_host'] || options['github_host']

  die("No 'gh' found, please install 'gh'") unless gh_avail?

  # Tell the 'gh' cli where to talk to, if not github.com
  ENV['GH_HOST'] = @ghhost if @ghhost

  return if options['no_change']

  set_commit_template if @repo_config['commit_template']
end

Instance Method Details

#amendObject


5
6
7
8
9
# File 'lib/sugarjar/commands/amend.rb', line 5

def amend(*)
  assert_in_repo!
  # This cannot use shellout since we need a full terminal for the editor
  exit(system(SugarJar::Util.which('git'), 'commit', '--amend', *))
end

#bclean(name = nil) ⇒ Object


3
4
5
6
7
8
9
10
11
12
13
14
15
# File 'lib/sugarjar/commands/bclean.rb', line 3

def bclean(name = nil)
  assert_in_repo!
  name ||= current_branch
  name = fprefix(name)
  if clean_branch(name)
    SugarJar::Log.info("#{name}: #{color('reaped', :green)}")
  else
    die(
      "#{color("Cannot clean #{name}", :red)}! there are unmerged " +
      "commits; use 'git branch -D #{name}' to forcefully delete it.",
    )
  end
end

#bcleanallObject


17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/sugarjar/commands/bclean.rb', line 17

def bcleanall
  assert_in_repo!
  curr = current_branch
  all_local_branches.each do |branch|
    if MAIN_BRANCHES.include?(branch)
      SugarJar::Log.debug("Skipping #{branch}")
      next
    end

    if clean_branch(branch)
      SugarJar::Log.info("#{branch}: #{color('reaped', :green)}")
    else
      SugarJar::Log.info("#{branch}: skipped")
      SugarJar::Log.debug(
        "There are unmerged commits; use 'git branch -D #{branch}' to " +
        'forcefully delete it)',
      )
    end
  end

  # Return to the branch we were on, or main
  if all_local_branches.include?(curr)
    git('checkout', curr)
  else
    checkout_main_branch
  end
end

#binfoObject


24
25
26
27
28
29
30
# File 'lib/sugarjar/commands/branch.rb', line 24

def binfo
  assert_in_repo!
  SugarJar::Log.info(git(
    'log', '--graph', '--oneline', '--decorate', '--boundary',
    "#{tracked_branch}.."
  ).stdout.chomp)
end

#brObject


19
20
21
22
# File 'lib/sugarjar/commands/branch.rb', line 19

def br
  assert_in_repo!
  SugarJar::Log.info(git('branch', '-v').stdout.chomp)
end

#checkout(*args) ⇒ Object Also known as: co


3
4
5
6
7
8
9
10
11
12
13
14
15
16
# File 'lib/sugarjar/commands/branch.rb', line 3

def checkout(*args)
  assert_in_repo!
  # Pop the last arguement, which is _probably_ a branch name
  # and then add any featureprefix, and if _that_ is a branch
  # name, replace the last arguement with that
  name = args.last
  bname = fprefix(name)
  if all_local_branches.include?(bname)
    SugarJar::Log.debug("Featurepefixing #{name} -> #{bname}")
    args[-1] = bname
  end
  s = git('checkout', *args)
  SugarJar::Log.info(s.stderr + s.stdout.chomp)
end

#debuginfo(*args) ⇒ Object


5
6
7
8
9
10
11
12
13
14
# File 'lib/sugarjar/commands/debuginfo.rb', line 5

def debuginfo(*args)
  puts "sugarjar version #{SugarJar::VERSION}"
  puts ghcli('version').stdout
  puts git('version').stdout

  puts "Config: #{JSON.pretty_generate(args[0])}"
  return unless @repo_config

  puts "Repo config: #{JSON.pretty_generate(@repo_config)}"
end

#feature(name, base = nil) ⇒ Object Also known as: f


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/sugarjar/commands/feature.rb', line 3

def feature(name, base = nil)
  assert_in_repo!
  SugarJar::Log.debug("Feature: #{name}, #{base}")
  name = fprefix(name)
  die("#{name} already exists!") if all_local_branches.include?(name)
  if base
    fbase = fprefix(base)
    base = fbase if all_local_branches.include?(fbase)
  else
    base ||= most_main
  end
  # If our base is a local branch, don't try to parse it for a remote name
  unless all_local_branches.include?(base)
    base_pieces = base.split('/')
    git('fetch', base_pieces[0]) if base_pieces.length > 1
  end
  git('checkout', '-b', name, base)
  git('branch', '-u', base)
  SugarJar::Log.info(
    "Created feature branch #{color(name, :green)} based on " +
    color(base, :green),
  )
end

#forcepush(remote = nil, branch = nil) ⇒ Object Also known as: fpush


9
10
11
12
# File 'lib/sugarjar/commands/push.rb', line 9

def forcepush(remote = nil, branch = nil)
  assert_in_repo!
  _smartpush(remote, branch, true)
end

#get_checks(type) ⇒ Object

determine if we’re using the _list_cmd and if so run it to get the checks, or just use the directly-defined check, and cache it


54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/sugarjar/commands/checks.rb', line 54

def get_checks(type)
  return @checks[type] if @checks[type]

  ret = get_checks_from_command(type)
  if ret
    SugarJar::Log.debug("Found #{type}s: #{ret}")
    @checks[type] = ret
  # if it's explicitly false, we failed to run the command
  elsif ret == false
    @checks[type] = false
  # otherwise, we move on (basically: it's nil, there was no _list_cmd)
  else
    SugarJar::Log.debug("[#{type}]: using listed linters: #{ret}")
    @checks[type] = @repo_config[type] || []
  end
  @checks[type]
end

#get_checks_from_command(type) ⇒ Object


31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/sugarjar/commands/checks.rb', line 31

def get_checks_from_command(type)
  return nil unless @repo_config["#{type}_list_cmd"]

  cmd = @repo_config["#{type}_list_cmd"]
  short = cmd.split.first
  unless File.exist?(short)
    SugarJar::Log.error(
      "Configured #{type}_list_cmd #{short} does not exist!",
    )
    return false
  end
  s = Mixlib::ShellOut.new(cmd).run_command
  if s.error?
    SugarJar::Log.error(
      "#{type}_list_cmd (#{cmd}) failed: #{s.format_for_exception}",
    )
    return false
  end
  s.stdout.split("\n")
end

#lintObject


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/sugarjar/commands/checks.rb', line 5

def lint
  assert_in_repo!
  if dirty?
    if @ignore_dirty
      SugarJar::Log.warn(
        'Your repo is dirty, but --ignore-dirty was specified, so ' +
        'carrying on anyway. If the linter autocorrects, the displayed ' +
        'diff will be misleading',
      )
    else
      SugarJar::Log.error(
        'Your repo is dirty, but --ignore-dirty was not specified. ' +
        'Refusing to run lint. This is to ensure that if the linter ' +
        'autocorrects, we can show the correct diff.',
      )
      exit(1)
    end
  end
  exit(1) unless run_check('lint')
end

#pullsuggestionsObject Also known as: ps


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/sugarjar/commands/pullsuggestions.rb', line 5

def pullsuggestions
  assert_in_repo!

  if dirty?
    if @ignore_dirty
      SugarJar::Log.warn(
        'Your repo is dirty, but --ignore-dirty was specified, so ' +
        'carrying on anyway.',
      )
    else
      SugarJar::Log.error(
        'Your repo is dirty, so I am not going to push. Please commit ' +
        'or amend first.',
      )
      exit(1)
    end
  end

  src = "origin/#{current_branch}"
  fetch('origin')
  diff = git('diff', "..#{src}").stdout
  return unless diff && !diff.empty?

  puts "Will merge the following suggestions:\n\n#{diff}"

  loop do
    $stdout.print("\nAre you sure? [y/n] ")
    ans = $stdin.gets.strip
    case ans
    when /^[Yy]$/
      git = SugarJar::Util.which('git')
      system(git, 'merge', '--ff', "origin/#{current_branch}")
      break
    when /^[Nn]$/, /^[Qq](uit)?/
      puts 'Not merging at user request...'
      break
    else
      puts "Didn't understand '#{ans}'."
    end
  end
end

#qamendObject Also known as: amendq


11
12
13
14
# File 'lib/sugarjar/commands/amend.rb', line 11

def qamend(*)
  assert_in_repo!
  SugarJar::Log.info(git('commit', '--amend', '--no-edit', *).stdout)
end

#run_check(type) ⇒ Object


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/sugarjar/commands/checks.rb', line 72

def run_check(type)
  repo_root = SugarJar::Util.repo_root
  Dir.chdir repo_root do
    checks = get_checks(type)
    # if we failed to determine the checks, the the checks have effectively
    # failed
    return false unless checks

    checks.each do |check|
      SugarJar::Log.debug("Running #{type} #{check}")

      short = check.split.first
      if short.include?('/')
        short = File.join(repo_root, short) unless short.start_with?('/')
        unless File.exist?(short)
          SugarJar::Log.error("Configured #{type} #{short} does not exist!")
        end
      elsif !SugarJar::Util.which_nofail(short)
        SugarJar::Log.error("Configured #{type} #{short} does not exist!")
        return false
      end
      s = Mixlib::ShellOut.new(check).run_command

      # Linters auto-correct, lets handle that gracefully
      if type == 'lint' && dirty?
        SugarJar::Log.info(
          "[#{type}] #{short}: #{color('Corrected', :yellow)}",
        )
        SugarJar::Log.warn(
          "The linter modified the repo. Here's the diff:\n",
        )
        puts git('diff').stdout
        loop do
          $stdout.print(
            "\nWould you like to\n\t[q]uit and inspect\n\t[a]mend the " +
            "changes to the current commit and re-run\n  > ",
          )
          ans = $stdin.gets.strip
          case ans
          when /^q/
            SugarJar::Log.info('Exiting at user request.')
            exit(1)
          when /^a/
            qamend('-a')
            # break here, if we get out of this loop we 'redo', assuming
            # the user chose this option
            break
          end
        end
        redo
      end

      if s.error?
        SugarJar::Log.info(
          "[#{type}] #{short} #{color('failed', :red)}, output follows " +
          "(see debug for more)\n#{s.stdout}",
        )
        SugarJar::Log.debug(s.format_for_exception)
        return false
      end
      SugarJar::Log.info(
        "[#{type}] #{short}: #{color('OK', :green)}",
      )
    end
  end
end

#smartclone(repo, dir = nil) ⇒ Object Also known as: sclone


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/sugarjar/commands/smartclone.rb', line 3

def smartclone(repo, dir = nil, *)
  reponame = File.basename(repo, '.git')
  dir ||= reponame
  org = extract_org(repo)

  SugarJar::Log.info("Cloning #{reponame}...")

  # GH's 'fork' command (with the --clone arg) will fork, if necessary,
  # then clone, and then setup the remotes with the appropriate names. So
  # we just let it do all the work for us and return.
  #
  # Unless the repo is in our own org and cannot be forked, then it
  # will fail.
  if org == @ghuser
    git('clone', canonicalize_repo(repo), dir, *)
  else
    ghcli('repo', 'fork', '--clone', canonicalize_repo(repo), dir, *)
    # make the main branch track upstream
    Dir.chdir dir do
      git('branch', '-u', "upstream/#{main_branch}")
    end
  end

  SugarJar::Log.info('Remotes "origin" and "upstream" configured.')
end

#smartlogObject Also known as: sl

binfo for all branches


33
34
35
36
37
38
39
# File 'lib/sugarjar/commands/branch.rb', line 33

def smartlog
  assert_in_repo!
  SugarJar::Log.info(git(
    'log', '--graph', '--oneline', '--decorate', '--boundary',
    '--branches', "#{most_main}.."
  ).stdout.chomp)
end

#smartpullrequest(*args) ⇒ Object Also known as: spr, smartpr


5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/sugarjar/commands/smartpullrequest.rb', line 5

def smartpullrequest(*args)
  assert_in_repo!
  assert_common_main_branch!

  if dirty?
    SugarJar::Log.warn(
      'Your repo is dirty, so I am not going to create a pull request. ' +
      'You should commit or amend and push it to your remote first.',
    )
    exit(1)
  end

  user_specified_base = args.include?('-B') || args.include?('--base')

  curr = current_branch
  base = tracked_branch
  if @pr_autofill
    SugarJar::Log.info('Autofilling in PR from commit message')
    num_commits = git(
      'rev-list', '--count', curr, "^#{base}"
    ).stdout.strip.to_i
    if num_commits > 1
      args.unshift('--fill-first')
    else
      args.unshift('--fill')
    end
  end
  unless user_specified_base
    if subfeature?(base)
      if upstream_org != push_org
        SugarJar::Log.warn(
          'Unfortunately you cannot based one PR on another PR when' +
          " using fork-based PRs. We will base this on #{most_main}." +
          ' This just means the PR "Changes" tab will show changes for' +
          ' the full stack until those other PRs are merged and this PR' +
          ' PR is rebased.',
        )
      # nil is prompt, true is always, false is never
      elsif @pr_autostack.nil?
        $stdout.print(
          'It looks like this is a subfeature, would you like to base ' +
          "this PR on #{base}? [y/n] ",
        )
        ans = $stdin.gets.strip
        args.unshift('--base', base) if %w{Y y}.include?(ans)
      elsif @pr_autostack
        args.unshift('--base', base)
      end
    elsif base.include?('/') && base != most_main
      # If our base is a remote branch, then use that as the
      # base branch of the PR
      args.unshift('--base', base.split('/').last)
    end
  end

  # <org>:<branch> is the GH API syntax for:
  #   look for a branch of name <branch>, from a fork in owner <org>
  args.unshift('--head', "#{push_org}:#{curr}")
  SugarJar::Log.trace("Running: gh pr create #{args.join(' ')}")
  gh = SugarJar::Util.which('gh')
  system(gh, 'pr', 'create', *args)
end

#smartpush(remote = nil, branch = nil) ⇒ Object Also known as: spush


3
4
5
6
# File 'lib/sugarjar/commands/push.rb', line 3

def smartpush(remote = nil, branch = nil)
  assert_in_repo!
  _smartpush(remote, branch, false)
end

#subfeature(name) ⇒ Object Also known as: sf


28
29
30
31
32
# File 'lib/sugarjar/commands/feature.rb', line 28

def subfeature(name)
  assert_in_repo!
  SugarJar::Log.debug("Subfature: #{name}")
  feature(name, current_branch)
end

#unitObject


26
27
28
29
# File 'lib/sugarjar/commands/checks.rb', line 26

def unit
  assert_in_repo!
  exit(1) unless run_check('unit')
end

#up(branch = nil) ⇒ Object


3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/sugarjar/commands/up.rb', line 3

def up(branch = nil)
  assert_in_repo!
  branch ||= current_branch
  branch = fprefix(branch)
  # get a copy of our current branch, if rebase fails, we won't
  # be able to determine it without backing out
  curr = current_branch
  git('checkout', branch)
  result = rebase
  if result['so'].error?
    backout = ''
    if rebase_in_progress?
      backout = ' You can get out of this with a `git rebase --abort`.'
    end

    die(
      "#{color(curr, :red)}: Failed to rebase on " +
      "#{result['base']}. Leaving the repo as-is.#{backout} " +
      'Output from failed rebase is: ' +
      "\nSTDOUT:\n#{result['so'].stdout.lines.map { |x| "\t#{x}" }.join}" +
      "\nSTDERR:\n#{result['so'].stderr.lines.map { |x| "\t#{x}" }.join}",
    )
  else
    SugarJar::Log.info(
      "#{color(current_branch, :green)} rebased on #{result['base']}",
    )
    # go back to where we were if we rebased a different branch
    git('checkout', curr) if branch != curr
  end
end

#upallObject


34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/sugarjar/commands/up.rb', line 34

def upall
  assert_in_repo!
  all_local_branches.each do |branch|
    next if MAIN_BRANCHES.include?(branch)

    git('checkout', branch)
    result = rebase
    if result['so'].error?
      SugarJar::Log.error(
        "#{color(branch, :red)} failed rebase. Reverting attempt and " +
        'moving to next branch. Try `sj up` manually on that branch.',
      )
      git('rebase', '--abort') if rebase_in_progress?
    else
      SugarJar::Log.info(
        "#{color(branch, :green)} rebased on " +
        color(result['base'], :green).to_s,
      )
    end
  end
end