worktree 0.1.0

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