worktree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|