worktree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Worktree
4
+ class Error < RuntimeError
5
+ end
6
+ 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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'worktree'
4
+
5
+ module Worktree
6
+ module TabCompletion # :nodoc:
7
+ end
8
+ 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