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.
- checksums.yaml +7 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bin/worktree +6 -0
- data/bin/worktree_tab_completion +9 -0
- data/lib/worktree.rb +37 -0
- data/lib/worktree/cli.rb +59 -0
- data/lib/worktree/command.rb +8 -0
- data/lib/worktree/command/add.rb +66 -0
- data/lib/worktree/command/cherry_pick.rb +68 -0
- data/lib/worktree/command/configure.rb +17 -0
- data/lib/worktree/command/open.rb +37 -0
- data/lib/worktree/command/remove.rb +64 -0
- data/lib/worktree/command/remove_stale.rb +38 -0
- data/lib/worktree/config.rb +23 -0
- data/lib/worktree/db_manager.rb +56 -0
- data/lib/worktree/error.rb +6 -0
- data/lib/worktree/feature/clone_dbs.rb +46 -0
- data/lib/worktree/feature/copy_files.rb +32 -0
- data/lib/worktree/feature/jira.rb +74 -0
- data/lib/worktree/feature/tmux.rb +67 -0
- data/lib/worktree/project.rb +29 -0
- data/lib/worktree/tab_completion.rb +8 -0
- data/lib/worktree/tab_completion/branch_completion.rb +54 -0
- data/lib/worktree/tab_completion/cli.rb +140 -0
- data/lib/worktree/tab_completion/project_dir_completion.rb +50 -0
- data/lib/worktree/tab_completion/remote_branch_completion.rb +24 -0
- data/lib/worktree/tab_completion/worktree_completion.rb +19 -0
- data/lib/worktree/version.rb +5 -0
- metadata +241 -0
checksums.yaml
ADDED
@@ -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
|
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/bin/worktree
ADDED
@@ -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?
|
data/lib/worktree.rb
ADDED
@@ -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
|
data/lib/worktree/cli.rb
ADDED
@@ -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,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
|