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
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module Config
|
7
|
+
def config_file
|
8
|
+
xdg_config_home = ENV.fetch('XDG_CONFIG_HOME') { "#{ENV['HOME']}/.config" }
|
9
|
+
_config_file = "#{xdg_config_home}/worktree/worktree.yml"
|
10
|
+
unless File.exist?(_config_file)
|
11
|
+
raise Worktree::Error, "config file #{_config_file} not found!"
|
12
|
+
end
|
13
|
+
|
14
|
+
_config_file
|
15
|
+
end
|
16
|
+
|
17
|
+
def config
|
18
|
+
YAML.load_file(Worktree::Config.config_file)
|
19
|
+
end
|
20
|
+
|
21
|
+
module_function :config_file, :config
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
class DbManager
|
7
|
+
attr_reader :spec
|
8
|
+
|
9
|
+
def initialize(config_file, environment = 'development')
|
10
|
+
@spec = YAML.load_file(config_file)
|
11
|
+
@environment = environment
|
12
|
+
end
|
13
|
+
|
14
|
+
def environment_spec
|
15
|
+
@spec.fetch(@environment, {})
|
16
|
+
end
|
17
|
+
|
18
|
+
def multi?
|
19
|
+
environment_spec.key? 'primary'
|
20
|
+
end
|
21
|
+
|
22
|
+
def db_port
|
23
|
+
if multi?
|
24
|
+
environment_spec.dig('primary', 'port')
|
25
|
+
else
|
26
|
+
environment_spec['port']
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def template
|
31
|
+
if multi?
|
32
|
+
environment_spec.dig('primary', 'database')
|
33
|
+
else
|
34
|
+
environment_spec['database']
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def createdb!(db_name)
|
39
|
+
cmd = if db_port
|
40
|
+
"createdb -h localhost -p #{db_port} -T #{template} #{db_name}"
|
41
|
+
else
|
42
|
+
"createdb -h localhost -T #{template} #{db_name}"
|
43
|
+
end
|
44
|
+
Worktree.run_command cmd
|
45
|
+
end
|
46
|
+
|
47
|
+
def dropdb!
|
48
|
+
cmd = if db_port
|
49
|
+
"dropdb -h localhost -p #{db_port} #{template}"
|
50
|
+
else
|
51
|
+
"dropdb -h localhost #{template}"
|
52
|
+
end
|
53
|
+
Worktree.run_command cmd
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module Feature
|
7
|
+
class CloneDbs
|
8
|
+
|
9
|
+
def initialize(project_dir:, branch:)
|
10
|
+
@project_dir = project_dir
|
11
|
+
@branch = branch
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
@db_manager = DbManager.new("#{@project_dir}/master/config/database.yml")
|
16
|
+
@db_manager.createdb!(db_name)
|
17
|
+
|
18
|
+
write!
|
19
|
+
rescue StandardError => e
|
20
|
+
# bypass error
|
21
|
+
Worktree.logger.error { e.message }
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def write!
|
27
|
+
new_spec = @db_manager.spec.dup
|
28
|
+
if @db_manager.multi?
|
29
|
+
new_spec['development']['primary']['database'] = db_name
|
30
|
+
else
|
31
|
+
new_spec['development']['database'] = db_name
|
32
|
+
end
|
33
|
+
# write changed database config back
|
34
|
+
File.write("#{@project_dir}/#{@branch}/config/database.yml", new_spec.to_yaml)
|
35
|
+
end
|
36
|
+
|
37
|
+
def db_name
|
38
|
+
# db name cannot be > 63 bytes
|
39
|
+
db_suffix = '_development'
|
40
|
+
max_db_prefix_length = 62 - db_suffix.length
|
41
|
+
db_prefix = @branch[0..max_db_prefix_length]
|
42
|
+
"#{db_prefix}#{db_suffix}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module Feature
|
7
|
+
class CopyFiles
|
8
|
+
|
9
|
+
def initialize(project_dir:, branch:)
|
10
|
+
@project_dir = project_dir
|
11
|
+
@branch = branch
|
12
|
+
end
|
13
|
+
|
14
|
+
def run!
|
15
|
+
project_key = Worktree::Project.project_key_for(@branch)
|
16
|
+
paths = Worktree::Config.config.dig('projects', project_key, 'copy_files') || []
|
17
|
+
paths.each { |path| copy_file(path) }
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def copy_file(file)
|
23
|
+
master_path = "#{@project_dir}/master/#{file}"
|
24
|
+
if File.exist?(master_path)
|
25
|
+
FileUtils.cp_r master_path, "#{@project_dir}/#{@branch}/#{file}"
|
26
|
+
else
|
27
|
+
print "The path #{master_path} was not found!"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jira-ruby'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module Feature
|
7
|
+
class Jira
|
8
|
+
def initialize(project_dir:, branch:)
|
9
|
+
@project_dir = project_dir
|
10
|
+
@branch = branch
|
11
|
+
end
|
12
|
+
|
13
|
+
def run!
|
14
|
+
if jira_issue?
|
15
|
+
prompt = "Jira issue #{jira_issue_id} status: #{jira_issue.status.name}. Would you like to change it?"
|
16
|
+
jira_process! unless TTY::Prompt.new.no?(prompt)
|
17
|
+
end
|
18
|
+
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def jira_client
|
25
|
+
@jira_client ||= JIRA::Client.new(jira_client_options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def jira_client_options
|
29
|
+
{
|
30
|
+
username: ENV['JIRA_USERNAME'],
|
31
|
+
password: ENV['JIRA_PASSWORD'],
|
32
|
+
site: ENV['JIRA_SITE'],
|
33
|
+
context_path: '',
|
34
|
+
auth_type: :basic
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
def jira_issue?
|
39
|
+
return false unless jira_issue_id
|
40
|
+
|
41
|
+
jira_issue_id =~ Worktree::JIRA_ISSUE_ID_REGEX
|
42
|
+
end
|
43
|
+
|
44
|
+
def jira_process!
|
45
|
+
transition = choose_transition
|
46
|
+
apply_transition!(transition) if transition != -1
|
47
|
+
rescue StandardError => e
|
48
|
+
Worktree.logger.error { e.message }
|
49
|
+
end
|
50
|
+
|
51
|
+
def jira_issue_id
|
52
|
+
(@branch.match(/^\w+\-\d+/) || [])[0]
|
53
|
+
end
|
54
|
+
|
55
|
+
def jira_issue
|
56
|
+
@jira_issue ||= jira_client.Issue.find(jira_issue_id)
|
57
|
+
end
|
58
|
+
|
59
|
+
def apply_transition!(transition)
|
60
|
+
jira_issue.transitions.build.save!(transition: { id: transition.id })
|
61
|
+
end
|
62
|
+
|
63
|
+
def choose_transition
|
64
|
+
TTY::Prompt.new.select('Choose a transition?', cycle: true) do |menu|
|
65
|
+
menu.enum '.'
|
66
|
+
menu.choice 'Skip it', -1
|
67
|
+
jira_issue.transitions.all.each do |s|
|
68
|
+
menu.choice s.name, s
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Worktree
|
4
|
+
module Feature
|
5
|
+
class Tmux
|
6
|
+
|
7
|
+
class VimEditor
|
8
|
+
attr_reader :window_name
|
9
|
+
|
10
|
+
def initialize(cwd:)
|
11
|
+
@cwd = cwd
|
12
|
+
@window_name = 'vim'
|
13
|
+
end
|
14
|
+
|
15
|
+
# open Gemfile if present
|
16
|
+
def cmd
|
17
|
+
if File.exist?("#{@cwd}/Gemfile")
|
18
|
+
'vim Gemfile'
|
19
|
+
else
|
20
|
+
'vim'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(project_dir:, branch:)
|
26
|
+
@project_dir = project_dir
|
27
|
+
@branch = branch
|
28
|
+
@working_directory = "#{@project_dir}/#{@branch}".chomp('/')
|
29
|
+
end
|
30
|
+
|
31
|
+
def run!(session_name)
|
32
|
+
if session_exist?(session_name)
|
33
|
+
Worktree.logger.info { "TMUX session #{session_name} already exist" }
|
34
|
+
# TODO: ask for attach to it
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
Worktree.run_command "tmux new-session -t #{session_name} -d", chdir: @working_directory
|
39
|
+
Worktree.run_command "tmux new-window -d -t #{session_name} -n #{editor.window_name}", chdir: @working_directory
|
40
|
+
Worktree.run_command "tmux send-keys -t #{session_name}:2 \"#{editor.cmd}\" C-m"
|
41
|
+
Worktree.run_command "tmux select-window -t #{session_name}:2" # select vim window
|
42
|
+
if inside_tmux?
|
43
|
+
Kernel.system "tmux switch -t #{session_name}"
|
44
|
+
else
|
45
|
+
Kernel.system "tmux attach-session -t #{session_name}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def editor
|
52
|
+
return @editor if defined?(@editor)
|
53
|
+
|
54
|
+
@editor = VimEditor.new(cwd: @working_directory)
|
55
|
+
end
|
56
|
+
|
57
|
+
def inside_tmux?
|
58
|
+
Worktree.run_command('echo $TMUX').out.strip.present?
|
59
|
+
end
|
60
|
+
|
61
|
+
def session_exist?(name)
|
62
|
+
cmd = "tmux list-sessions -F '#S' | awk '/'#{name}/' {print $1}'"
|
63
|
+
Worktree.run_command(cmd).out.strip.present?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Worktree
|
4
|
+
class Project
|
5
|
+
def self.project_key_for(branch)
|
6
|
+
project_keys = Worktree::Config.config['projects'].keys
|
7
|
+
return nil if project_keys.empty?
|
8
|
+
|
9
|
+
re = Regexp.new("^(#{project_keys.join('|')})\-")
|
10
|
+
(branch.match(re) || [])[1]
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.resolve(branch)
|
14
|
+
new(project_key_for(branch))
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(key)
|
18
|
+
@key = key
|
19
|
+
end
|
20
|
+
|
21
|
+
def root
|
22
|
+
if @key
|
23
|
+
Worktree::Config.config.dig('projects', @key, 'root').chomp('/')
|
24
|
+
else
|
25
|
+
Dir.pwd
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jira-ruby'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module TabCompletion
|
7
|
+
class BranchCompletion
|
8
|
+
def initialize(compl)
|
9
|
+
@compl = compl
|
10
|
+
end
|
11
|
+
|
12
|
+
def list
|
13
|
+
issue_id = find_jira_issue_by(@compl)
|
14
|
+
if issue_id
|
15
|
+
jira_issue = jira_client.Issue.find(issue_id)
|
16
|
+
["#{issue_id}-#{clean_jira_summary(jira_issue)}"]
|
17
|
+
else
|
18
|
+
[]
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def find_jira_issue_by(comp_line)
|
25
|
+
(comp_line.match(Worktree::JIRA_ISSUE_ID_REGEX) || [])[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
def jira_client
|
29
|
+
@jira_client ||= JIRA::Client.new(
|
30
|
+
username: ENV['JIRA_USERNAME'],
|
31
|
+
password: ENV['JIRA_PASSWORD'],
|
32
|
+
site: ENV['JIRA_SITE'],
|
33
|
+
context_path: '',
|
34
|
+
auth_type: :basic
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def clean_jira_summary(jira_issue)
|
39
|
+
raw_summary = jira_issue.summary
|
40
|
+
raw_summary = raw_summary.strip
|
41
|
+
|
42
|
+
# translate raw summary to branch name
|
43
|
+
summary = raw_summary.split(' ').map(&:underscore).join('-')
|
44
|
+
summary.gsub!('&&', 'and')
|
45
|
+
summary.gsub!(/\(|\)/, '') # remove brackets
|
46
|
+
summary.gsub!(/"|'|”|“|«|»/, '') # remove quotes
|
47
|
+
summary.gsub!(/\.$/, '') # remove end period
|
48
|
+
summary.gsub!(%r{/}, '-') # change back slash to minus
|
49
|
+
summary.gsub!(/:|;/, '')
|
50
|
+
summary
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'worktree/tab_completion'
|
4
|
+
|
5
|
+
module Worktree
|
6
|
+
module TabCompletion
|
7
|
+
class CLI # :nodoc:
|
8
|
+
COMMAND_NEW = 'new'
|
9
|
+
COMMAND_CHERRY_PICK = 'cherry_pick'
|
10
|
+
COMMAND_REMOVE = 'remove'
|
11
|
+
COMMAND_CONFIGURE = 'configure'
|
12
|
+
COMMAND_OPEN = 'open'
|
13
|
+
|
14
|
+
def find_matches_for(command_line)
|
15
|
+
command_parameters = command_line.split(' ', -1)[1..-1]
|
16
|
+
|
17
|
+
# 'worktree '
|
18
|
+
# 'worktree n'
|
19
|
+
if command_parameters.empty? || command_parameters.size == 1
|
20
|
+
return [
|
21
|
+
COMMAND_NEW,
|
22
|
+
COMMAND_CHERRY_PICK,
|
23
|
+
COMMAND_REMOVE,
|
24
|
+
COMMAND_CONFIGURE,
|
25
|
+
COMMAND_OPEN
|
26
|
+
]
|
27
|
+
end
|
28
|
+
|
29
|
+
# 'worktree new '
|
30
|
+
# 'worktree new DAPI'
|
31
|
+
if command_parameters.size == 2
|
32
|
+
command, compl = command_parameters
|
33
|
+
|
34
|
+
return BranchCompletion.new(compl).list if command == COMMAND_NEW
|
35
|
+
return WorktreeCompletion.new(compl).list if command == COMMAND_OPEN
|
36
|
+
return WorktreeCompletion.new(compl).list if command == COMMAND_REMOVE
|
37
|
+
# TODO: cherry_pick/remove command
|
38
|
+
end
|
39
|
+
|
40
|
+
# 'worktree new BRANCHNAME '
|
41
|
+
# 'worktree new BRANCHNAME --project-dir='
|
42
|
+
# 'worktree new BRANCHNAME --project-dir=../'
|
43
|
+
# 'worktree new BRANCHNAME --project-dir=/tmp/path'
|
44
|
+
# 'worktree new BRANCHNAME --project-dir=~/path'
|
45
|
+
# 'worktree new BRANCHNAME --from='
|
46
|
+
# 'worktree new BRANCHNAME --from=upstream/'
|
47
|
+
if command_parameters.size == 3
|
48
|
+
command, branch, compl = command_parameters
|
49
|
+
|
50
|
+
if command == COMMAND_NEW
|
51
|
+
if compl.starts_with?('--project-dir=')
|
52
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
53
|
+
map { |dir| "--project-dir=#{dir}" }
|
54
|
+
elsif command_parameters[2].starts_with?('--from=')
|
55
|
+
project_dir = Project.resolve(branch).root
|
56
|
+
return RemoteBranchCompletion.new(compl[7..-1], project_dir: project_dir).list.
|
57
|
+
map { |b| "--from=#{b}" }
|
58
|
+
else
|
59
|
+
return ['--from=', '--project-dir=']
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
if command == COMMAND_CHERRY_PICK
|
64
|
+
if compl.starts_with?('--project-dir=')
|
65
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
66
|
+
map { |dir| "--project-dir=#{dir}" }
|
67
|
+
elsif command_parameters[2].starts_with?('--to=')
|
68
|
+
return RemoteBranchCompletion.new(compl[5..-1]).list.
|
69
|
+
map { |dir| "--to=#{dir}" }
|
70
|
+
else
|
71
|
+
return ['--to=', '--project-dir=']
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if command == COMMAND_REMOVE
|
76
|
+
if compl.starts_with?('--project-dir=')
|
77
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
78
|
+
map { |dir| "--project-dir=#{dir}" }
|
79
|
+
else
|
80
|
+
return ['--project-dir=']
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
if command == COMMAND_OPEN
|
85
|
+
if compl.starts_with?('--project-dir=')
|
86
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
87
|
+
map { |dir| "--project-dir=#{dir}" }
|
88
|
+
else
|
89
|
+
return ['--project-dir=']
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
if command_parameters.size == 4
|
95
|
+
command, _branch_name, project_dir_or_from, compl = command_parameters
|
96
|
+
|
97
|
+
if command == COMMAND_NEW
|
98
|
+
if project_dir_or_from.starts_with?('--project-dir=')
|
99
|
+
if compl.starts_with?('--from=')
|
100
|
+
project_dir = project_dir_or_from[14..-1]
|
101
|
+
return RemoteBranchCompletion.new(compl[7..-1], project_dir: project_dir).list.
|
102
|
+
map { |dir| "--from=#{dir}" }
|
103
|
+
else
|
104
|
+
return ['--from=']
|
105
|
+
end
|
106
|
+
elsif project_dir_or_from.starts_with?('--from=')
|
107
|
+
if compl.starts_with?('--project-dir=')
|
108
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
109
|
+
map { |dir| "--project-dir=#{dir}" }
|
110
|
+
else
|
111
|
+
return ['--project-dir=']
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if command == COMMAND_CHERRY_PICK
|
117
|
+
if project_dir_or_from.starts_with?('--project-dir=')
|
118
|
+
if compl.starts_with?('--to=')
|
119
|
+
project_dir = project_dir_or_from[14..-1]
|
120
|
+
return RemoteBranchCompletion.new(compl[5..-1], project_dir: project_dir).list.
|
121
|
+
map { |dir| "--to=#{dir}" }
|
122
|
+
else
|
123
|
+
return ['--to=']
|
124
|
+
end
|
125
|
+
elsif project_dir_or_from.starts_with?('--to=')
|
126
|
+
if compl.starts_with?('--project-dir=')
|
127
|
+
return ProjectDirCompletion.new(compl[14..-1]).list.
|
128
|
+
map { |dir| "--project-dir=#{dir}" }
|
129
|
+
else
|
130
|
+
return ['--project-dir=']
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
[]
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|