worktree 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ac156da458c44bc340de5b6f895d428958f98867abb3a88a2ec763f77a831ccd
4
+ data.tar.gz: a763ab93c450a14b5864cc74f6f8b2ee813359e5ee68e21adfa2e1a40661154c
5
+ SHA512:
6
+ metadata.gz: b9a84849c822d8ba4506bf4df3332fa600f7d21faa6c03785f073a8f7809d69a289055fa309eecf2b9c5fa60b8ab6952cfd3445bf73cba7b02073e603ce3f3d9
7
+ data.tar.gz: fb68dd78ac65464f8eea7a1021c5618f0a33aa58af4dd378d8c14e831f6285d3cf0f681103d2ba69e333c45aa747a7f24b9dc559a1356b2f3f2625eb3eb5e4c6
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'worktree'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'worktree/cli'
5
+
6
+ Worktree::CLI.start(ARGV)
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'worktree/tab_completion/cli'
5
+
6
+ command_line = ENV.fetch('COMP_LINE')
7
+ compl = Worktree::TabCompletion::CLI.new
8
+ compl_matches = Array(compl.find_matches_for(command_line))
9
+ puts compl_matches unless compl_matches.empty?
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'tty-command'
5
+ # require 'active_support/all'
6
+ require 'active_support/core_ext'
7
+ require 'git'
8
+ require 'zeitwerk'
9
+
10
+ loader = Zeitwerk::Loader.for_gem
11
+ loader.setup
12
+
13
+ module Worktree
14
+ JIRA_ISSUE_ID_REGEX_TEMPLATE = ENV.fetch('JIRA_ISSUE_ID_REGEX') { '^\w\-\d+' }
15
+ JIRA_ISSUE_ID_REGEX = Regexp.new(JIRA_ISSUE_ID_REGEX_TEMPLATE)
16
+
17
+ def logger
18
+ return @logger if defined?(@logger)
19
+
20
+ @logger = Logger.new(STDOUT)
21
+ @logger.level = Logger::INFO
22
+ @logger
23
+ end
24
+
25
+ def run_command(cmd, options = {})
26
+ command = TTY::Command.new(output: Worktree.logger)
27
+ command.run cmd, options
28
+ rescue TTY::Command::ExitError => e
29
+ raise Error, e.message
30
+ end
31
+
32
+ def git_for(p_dir)
33
+ Git.open("#{p_dir}/master", log: Worktree.logger)
34
+ end
35
+
36
+ module_function :logger, :run_command, :git_for
37
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'worktree'
4
+ require 'thor'
5
+
6
+ module Worktree
7
+ class CLI < Thor
8
+ def self.exit_on_failure?
9
+ true
10
+ end
11
+
12
+ desc 'new BRANCH', 'Create a new branch'
13
+ option :from, default: Worktree::Command::Add::DEFAULT_BRANCH_REMOTE
14
+ option :project_dir
15
+ def new(branch)
16
+ Worktree::Command::Add.new(branch,
17
+ from: options[:from],
18
+ project_dir: options[:project_dir]).do!
19
+ end
20
+
21
+ desc 'open BRANCH', 'Open existing worktree'
22
+ option :project_dir
23
+ def open(branch)
24
+ Worktree::Command::Open.new(branch,
25
+ project_dir: options[:project_dir]).do!
26
+ end
27
+
28
+ desc 'remove BRANCH', 'Remove branches'
29
+ option :project_dir
30
+ def remove(*branches)
31
+ branches.each do |b|
32
+ Worktree::Command::Remove.new(b,
33
+ project_dir: options[:project_dir]).do!
34
+ end
35
+ end
36
+
37
+ desc 'remove-stale', 'Remove all stale branches'
38
+ option :project_dir
39
+ def remove_stale
40
+ Worktree::Command::RemoveStale.new(project_dir: options[:project_dir]).do!
41
+ rescue TTY::Reader::InputInterrupt
42
+ Worktree.logger.info { "You've interrupted removing of stale branches!" }
43
+ end
44
+
45
+ desc 'cherry_pick COMMIT', 'Create a new cherry pick'
46
+ option :to, required: true
47
+ option :project_dir, default: Dir.pwd
48
+ def cherry_pick(commit)
49
+ Worktree::Command::CherryPick.new(commit,
50
+ to: options[:to],
51
+ project_dir: options[:project_dir]).do!
52
+ end
53
+
54
+ desc 'configure', 'Configure worktree'
55
+ def configure
56
+ Worktree::Command::Configure.new.do!
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/all'
4
+
5
+ module Worktree
6
+ module Command
7
+ end
8
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+
5
+ module Worktree
6
+ module Command
7
+ class Add
8
+ DEFAULT_BRANCH_REMOTE = 'upstream/master'
9
+
10
+ def initialize(branch, from:, project_dir:)
11
+ @branch = branch
12
+ @branch_remote = from
13
+ @project_dir = project_dir || Project.resolve(branch).root
14
+ @worktree = "#{@project_dir}/#{@branch}"
15
+ end
16
+
17
+ def do!
18
+ raise "Worktree #{@worktree} already exists!" if Dir.exist?(@worktree)
19
+ raise 'No master repo found!' unless Dir.exist?("#{@project_dir}/master/.git")
20
+
21
+ # fetch all
22
+ # TODO: silence log while fetching remotes
23
+ git.remotes.each { |remote| git.fetch(remote, prune: true) }
24
+
25
+ # update master
26
+ git.pull('upstream', 'master')
27
+
28
+ Worktree.run_command "git worktree add -b #{@branch} ../#{@branch} #{@branch_remote}", chdir: "#{@project_dir}/master"
29
+
30
+ copy_files
31
+ clone_dbs
32
+ tmux
33
+ end
34
+
35
+ private
36
+
37
+ def copy_files
38
+ Feature::CopyFiles.new(
39
+ project_dir: @project_dir,
40
+ branch: @branch
41
+ ).run!
42
+ end
43
+
44
+ def clone_dbs
45
+ if File.exist?("#{@project_dir}/master/config/database.yml")
46
+ Feature::CloneDbs.new(
47
+ project_dir: @project_dir,
48
+ branch: @branch
49
+ ).run! unless TTY::Prompt.new.no?('Clone development database?')
50
+ end
51
+ end
52
+
53
+ def tmux
54
+ tmux_session_name = @branch.tr('.', '-')
55
+ Feature::Tmux.new(
56
+ project_dir: @project_dir,
57
+ branch: @branch
58
+ ).run!(tmux_session_name)
59
+ end
60
+
61
+ def git
62
+ @git ||= Worktree.git_for(@project_dir)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+ require 'tty-prompt'
5
+
6
+ module Worktree
7
+ module Command
8
+ class CherryPick
9
+ def initialize(commit, to:, project_dir:)
10
+ @commit = commit[0..7] # short commit
11
+ @branch_remote = to
12
+ @branch = "cherry-pick-#{@commit}-to-#{@branch_remote.tr('/', '-')}"
13
+ @project_dir = project_dir.chomp('/')
14
+ end
15
+
16
+ def do!
17
+ raise "Folder #{@branch} already exists!" if Dir.exist?("#{@project_dir}/#{@branch}")
18
+ raise 'No master repo found!' unless Dir.exist?("#{@project_dir}/master/.git")
19
+
20
+ # fetch all
21
+ git.remotes.each(&:fetch)
22
+
23
+ Worktree.run_command "git worktree add -b #{@branch} ../#{@branch} #{@branch_remote}", chdir: "#{@project_dir}/master"
24
+
25
+ begin
26
+ Worktree.run_command "git cherry-pick #{@commit} -m 1", chdir: "#{@project_dir}/#{@branch}"
27
+ rescue Worktree::Error => e
28
+ # bypass conflicts while cherry-picking
29
+ Worktree.logger.warn { e.message }
30
+ end
31
+
32
+ copy_files
33
+ clone_dbs
34
+ tmux
35
+ end
36
+
37
+ private
38
+
39
+ def copy_files
40
+ Feature::CopyFiles.new(
41
+ project_dir: @project_dir,
42
+ branch: @branch
43
+ ).run!
44
+ end
45
+
46
+ def clone_dbs
47
+ if File.exist?("#{@project_dir}/master/config/database.yml")
48
+ Feature::CloneDbs.new(
49
+ project_dir: @project_dir,
50
+ branch: @branch
51
+ ).run! unless TTY::Prompt.new.no?('Clone development database?')
52
+ end
53
+ end
54
+
55
+ def tmux
56
+ tmux_session_name = @branch.tr('.', '-')
57
+ Feature::Tmux.new(
58
+ project_dir: @project_dir,
59
+ branch: @branch
60
+ ).run!(tmux_session_name)
61
+ end
62
+
63
+ def git
64
+ @git ||= Worktree.git_for(@project_dir)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Worktree
4
+ module Command
5
+ class Configure # :nodoc:
6
+ def do!
7
+ system("#{editor} #{Worktree::Config.config_file}")
8
+ end
9
+
10
+ private
11
+
12
+ def editor
13
+ ENV.fetch('EDITOR') { 'vim' }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tty-prompt'
4
+
5
+ module Worktree
6
+ module Command
7
+ class Open
8
+ def initialize(branch, project_dir:)
9
+ @branch = branch
10
+ @project_dir = project_dir || Project.resolve(branch).root
11
+ @worktree = "#{@project_dir}/#{@branch}"
12
+ end
13
+
14
+ def do!
15
+ raise "Worktree #{@worktree} not found exists!" unless Dir.exist?(@worktree)
16
+ raise 'No master repo found!' unless Dir.exist?("#{@project_dir}/master/.git")
17
+
18
+ tmux
19
+ end
20
+
21
+ private
22
+
23
+ def tmux
24
+ project_dir_name = File.expand_path(@project_dir).chomp('/').split('/').last
25
+ tmux_session_name = if @branch == 'master'
26
+ "#{project_dir_name}-#{@branch}"
27
+ else
28
+ @branch
29
+ end
30
+ Feature::Tmux.new(
31
+ project_dir: @project_dir,
32
+ branch: @branch
33
+ ).run!(tmux_session_name)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Worktree
4
+ module Command
5
+ class Remove
6
+ def initialize(branch, project_dir:, update_refs: true)
7
+ @branch = branch
8
+ @project_dir = project_dir || Project.resolve(branch).root
9
+ @worktree = "#{@project_dir}/#{@branch}"
10
+ @update_refs = update_refs
11
+ end
12
+
13
+ def do!
14
+ return unless Dir.exist?(@worktree)
15
+ return unless TTY::Prompt.new.yes?("Do you want to remove #{@worktree}?")
16
+
17
+ # update refs
18
+ git.remotes.each { |remote| git.fetch(remote, prune: true) } if @update_refs
19
+
20
+ unless git.branch('master').contains?(@branch)
21
+ unless TTY::Prompt.new.yes?("The branch #{@branch} was not merged to master. Would you like to remove it anyway?")
22
+ Worktree.logger.warn { "You've skipped removing the worktree #{@worktree}" }
23
+ return
24
+ end
25
+ end
26
+
27
+ drop_db! if File.exist?("#{@worktree}/config/database.yml")
28
+
29
+ # remove stale worktree
30
+ Worktree.run_command "git worktree remove #{@worktree} --force", chdir: "#{@project_dir}/master"
31
+
32
+ # if remote branch exists then remove it also
33
+ if Git.ls_remote(git.dir)['remotes'].keys.include?("origin/#{@branch}")
34
+ if TTY::Prompt.new.yes?("Do you want to remove remote branch origin/#{@branch}?")
35
+ git.push('origin', @branch, delete: true)
36
+ end
37
+ end
38
+
39
+ # remove local branch
40
+ git.branch(@branch).delete
41
+ end
42
+
43
+ private
44
+
45
+ def drop_db!
46
+ db_manager_master = db_manager_for('master')
47
+ db_manager = db_manager_for(@branch)
48
+ return if db_manager.template == db_manager_master.template
49
+
50
+ if TTY::Prompt.new.yes?("Do you want to drop database #{db_manager.template}?")
51
+ db_manager.dropdb!
52
+ end
53
+ end
54
+
55
+ def db_manager_for(branch)
56
+ DbManager.new("#{@project_dir}/#{branch}/config/database.yml")
57
+ end
58
+
59
+ def git
60
+ @git ||= Worktree.git_for(@project_dir)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Worktree
4
+ module Command
5
+ class RemoveStale
6
+ def initialize(project_dir:)
7
+ @project_dir = project_dir || Dir.pwd
8
+ end
9
+
10
+ def do!
11
+ # update refs
12
+ git.remotes.each { |remote| git.fetch(remote, prune: true) }
13
+ git.pull('upstream', 'master')
14
+
15
+ branches = Dir.entries(@project_dir).
16
+ reject { |d| d == '.' || d == '..' || d == 'master' }.
17
+ select { |f| File.directory?(f) }
18
+
19
+ stale_branches = branches.select do |branch|
20
+ git.branch('master').contains?(branch)
21
+ end
22
+
23
+ Worktree.logger.info { "You have #{stale_branches.size} stale branches!" }
24
+
25
+ stale_branches.each_with_index do |stale_branch, index|
26
+ Worktree.logger.info { "#{index + 1} of #{stale_branches.size}" }
27
+ Remove.new(stale_branch, project_dir: @project_dir, update_refs: false).do!
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def git
34
+ @git ||= Worktree.git_for(@project_dir)
35
+ end
36
+ end
37
+ end
38
+ end