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

#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

#gbclean(name = nil, remote = nil) ⇒ Object Also known as: globalbranchclean



52
53
54
55
56
57
58
# File 'lib/sugarjar/commands/bclean.rb', line 52

def gbclean(name = nil, remote = nil)
  assert_in_repo!
  name ||= current_branch
  remote ||= 'origin'
  lbclean(name)
  rbclean(name, remote)
end

#gbcleanall(remote = nil) ⇒ Object Also known as: globalbranchcleanall



130
131
132
133
134
# File 'lib/sugarjar/commands/bclean.rb', line 130

def gbcleanall(remote = nil)
  assert_in_repo!
  bcleanall
  rcleanall(remote)
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



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

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



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

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

#lbclean(name = nil) ⇒ Object Also known as: localbranchclean, bclean



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

def lbclean(name = nil)
  assert_in_repo!
  name ||= current_branch
  name = fprefix(name)

  wt_branches = worktree_branches

  if wt_branches.include?(name)
    SugarJar::Log.warn("#{name}: #{color('skipped', :yellow)} (worktree)")
    return
  end

  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

#lbcleanallObject Also known as: localbranchcleanall, bcleanall



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/sugarjar/commands/bclean.rb', line 61

def lbcleanall
  assert_in_repo!
  curr = current_branch
  wt_branches = worktree_branches
  all_local_branches.each do |branch|
    if MAIN_BRANCHES.include?(branch)
      SugarJar::Log.debug("Skipping #{branch}")
      next
    end
    if wt_branches.include?(branch)
      SugarJar::Log.info(
        "#{branch}: #{color('skipped', :yellow)} (worktree)",
      )
      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

#lintObject



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

def lint
  assert_in_repo!

  # does not use dirty_check! as we want a custom message
  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
# File 'lib/sugarjar/commands/pullsuggestions.rb', line 5

def pullsuggestions
  assert_in_repo!
  dirty_check!

  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

#rbclean(name = nil, remote = nil) ⇒ Object Also known as: remotebranchclean



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

def rbclean(name = nil, remote = nil)
  assert_in_repo!
  name ||= current_branch
  name = fprefix(name)
  remote ||= 'origin'

  ref = "refs/remotes/#{remote}/#{name}"
  if git_nofail('show-ref', '--quiet', ref).error?
    SugarJar::Log.warn("Remote branch #{name} on #{remote} does not exist.")
    return
  end

  if clean_branch(ref, :remote)
    SugarJar::Log.info("#{ref}: #{color('reaped', :green)}")
  else
    die(
      "#{color("Cannot clean #{ref}", :red)}! there are unmerged " +
      "commits; use 'git push #{remote} -d #{name}' to forcefully delete " +
      ' it.',
    )
  end
end

#rbcleanall(remote = nil) ⇒ Object Also known as: remotebranchcleanall



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
# File 'lib/sugarjar/commands/bclean.rb', line 99

def rbcleanall(remote = nil)
  assert_in_repo!
  curr = current_branch
  remote ||= 'origin'
  all_remote_branches(remote).each do |branch|
    if (MAIN_BRANCHES + ['HEAD']).include?(branch)
      SugarJar::Log.debug("Skipping #{branch}")
      next
    end

    ref = "refs/remotes/#{remote}/#{branch}"
    if clean_branch(ref, :remote)
      SugarJar::Log.info("#{ref}: #{color('reaped', :green)}")
    else
      SugarJar::Log.info("#{ref}: 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

#run_check(type) ⇒ Object



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
138
139
# File 'lib/sugarjar/commands/checks.rb', line 74

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
67
# File 'lib/sugarjar/commands/smartpullrequest.rb', line 5

def smartpullrequest(*args)
  assert_in_repo!
  assert_common_main_branch!

  # does not use `dirty_check!` because we don't allow overriding here
  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

#syncObject



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/sugarjar/commands/up.rb', line 56

def sync
  assert_in_repo!
  dirty_check!

  src = "origin/#{current_branch}"
  fetch('origin')
  s = git_nofail('merge-base', '--is-ancestor', 'HEAD', src)
  if s.error?
    SugarJar::Log.debug(
      "Choosing rebase sync since this isn't a direct ancestor",
    )
    rebase(src)
  else
    SugarJar::Log.debug('Choosing reset sync since this is an ancestor')
    git('reset', '--hard', src)
  end
  SugarJar::Log.info("Synced to #{src}.")
end

#unitObject



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

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