├── .gitignore ├── .project_id ├── .rspec ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── git-amend ├── git-bump ├── git-cleanup ├── git-delete-remote-branch ├── git-files-changed ├── git-graph ├── git-merge-into-branch ├── git-minor ├── git-open ├── git-polish ├── git-pull-request ├── git-push-branch ├── git-switch ├── git-sync ├── git-sync-fork ├── git-typo ├── git-typos └── git-undo ├── git-utils.gemspec ├── lib ├── git-utils.rb └── git-utils │ ├── command.rb │ ├── delete_remote_branch.rb │ ├── merge_branch.rb │ ├── open.rb │ ├── options.rb │ ├── pull_request.rb │ ├── push_branch.rb │ ├── switch.rb │ ├── sync.rb │ ├── sync_fork.rb │ └── version.rb └── spec ├── .DS_Store ├── commands ├── .DS_Store ├── command_spec.rb ├── delete_remote_branch_spec.rb ├── merge_branch_spec.rb ├── open_spec.rb ├── pull_request_spec.rb ├── push_branch_spec.rb ├── switch_spec.rb ├── sync_fork_spec.rb └── sync_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | .rvmrc 19 | .DS_Store 20 | .api_token 21 | .project_id 22 | -------------------------------------------------------------------------------- /.project_id: -------------------------------------------------------------------------------- 1 | 745955 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rspec', '~> 2.13.0' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | git-utils (2.4.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | diff-lcs (1.2.4) 10 | rspec (2.13.0) 11 | rspec-core (~> 2.13.0) 12 | rspec-expectations (~> 2.13.0) 13 | rspec-mocks (~> 2.13.0) 14 | rspec-core (2.13.1) 15 | rspec-expectations (2.13.0) 16 | diff-lcs (>= 1.1.3, < 2.0) 17 | rspec-mocks (2.13.1) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | git-utils! 24 | rspec (~> 2.13.0) 25 | 26 | BUNDLED WITH 27 | 2.1.4 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Michael Hartl 2 | Copyright (c) 2013 Aleksandar Simic 3 | 4 | MIT License 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git utilities 2 | 3 | This repo contains some Git utility scripts. The highlights are `git open`, `git pull-request`, `git push-branch`, and `git undo`, which you’ll never understand how you did without. 4 | 5 | `git-utils` used to be pure Bash scripts, but they are now available as a Ruby gem: 6 | 7 | gem install git-utils 8 | 9 | See below for more details on the commands defined by `git-utils`. To learn more about how to use Git itself, see the tutorial book and online course [*Learn Enough Git to Be Dangerous*](https://www.learnenough.com/git). 10 | 11 | ## Installation 12 | 13 | gem install git-utils 14 | 15 | ## Commands 16 | 17 | * `git amend`: alias for `git commit --amend` 18 | * `git bump`: makes a commit with the message `"Bump version number"` 19 | * `git cleanup`: deletes every branch already merged into current branch (apart from `master`, `main`, `staging`, `development`, and any branches listed in `~/.git-cleanup-preserved`). Pass the `-r` option to delete remote merged branches. 20 | * `git files-changed`: alias for `git log --name-only`, showing only the commit message and which files changed (no diffs). 21 | * `git merge-into-branch [branch]`: merges current branch into given branch (defaults to repo's default branch) 22 | * `git minor`: makes a commit with the message `"Make minor changes"` 23 | * `git open`: opens the remote page for the repo (macOS & Linux) 24 | * `git polish`: makes a commit with the message `"Polish"` 25 | * `git pull-request`: pushes the branch and opens the remote page for issuing a new a pull request (macOS-only) 26 | * `git push-branch`: pushes the current branch up to origin 27 | * `git delete-remote-branch `: deletes the remote branch if it is safe to do so 28 | * `git switch `: switches to the first branch matching the given pattern 29 | * `git sync [branch]`: syncs the given branch with the remote branch (defaults to repo's default branch) 30 | * `git sync-fork`: syncs the default branch of a fork with the original upstream default (assumes upstream configuration as in “[Configuring a remote for a fork](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/configuring-a-remote-for-a-fork)”) 31 | * `git typo`: makes a commit with the message `"Fix typo"` 32 | * `git typos`: makes a commit with the message `"Fix typos"` 33 | * `git undo`: undoes the last commit 34 | * `git graph`: displays full repository history in graphical format; alias for `git log --graph --oneline --decorate --all --full-history --author-date-order --no-notes` 35 | 36 | ## Aliases 37 | 38 | Here are some suggested aliases: 39 | 40 | git config --global alias.mib merge-into-branch 41 | git config --global alias.pr pull-request 42 | git config --global alias.pb push-branch 43 | 44 | ## Further details 45 | 46 | Some of these commands deserve further explanation. 47 | 48 | ### git merge-into-branch 49 | 50 | `git merge-into-branch [target]` merges the current branch into the target branch (defaults to repo's default branch). On a branch called `add-markdown-support` in a repo with default branch `main`, `git merge-into-branch` is equivalent to the following: 51 | 52 | $ git checkout main 53 | $ git merge --no-ff --log add-markdown-support 54 | 55 | Note that this effectively changes the default merge behavior from fast-forward to no-fast-forward, which makes it possible to use `git log` to see which of the commit objects together have implemented a feature on a particular branch. As noted in [A successful Git branching model](http://nvie.com/posts/a-successful-git-branching-model/): 56 | 57 | > The `--no-ff` flag causes the merge to always create a new commit object, even if the merge could be performed with a fast-forward. This avoids losing information about the historical existence of a feature branch and groups together all commits that together added the feature… Yes, it will create a few more (empty) commit objects, but the gain is much bigger than that cost. 58 | 59 | In addition, the `--log` option puts the commit messages from the individual commits in the merge message, which is especially useful for viewing the full diff represented by the commit. 60 | 61 | These options can be overriden (and thus restored to their defaults) by passing the options `-ff` or `--no-log`. `git merge-into-branch` accepts any options valid for `git merge`. 62 | 63 | ### git push-branch 64 | 65 | `git push-branch` creates a remote branch at `origin` with the name of the current branch: 66 | 67 | $ git push-branch 68 | * [new branch] add-markdown-support -> add-markdown-support 69 | 70 | `git push-branch` accepts any options valid for `git push`. 71 | 72 | 73 | ### git sync 74 | 75 | `git sync [branch]` syncs the given local branch with the remote branch (defaults to repo's default branch). On a branch called `add-markdown-support` in a repo with default branch `master`, `git sync` is equivalent to the following: 76 | 77 | $ git checkout master 78 | $ git pull 79 | $ git checkout add-markdown-support 80 | 81 | The main purpose of `git sync` is to prepare the current branch for merging with the default branch: 82 | 83 | $ git sync 84 | $ git merge master # or `main`, etc. 85 | 86 | (This is essentially equivalent to 87 | 88 | $ git fetch 89 | $ git merge origin/master 90 | 91 | but I don’t like having `master` and `origin/master` be different since that means you have to remember to run `git pull` on `master` some time down the line.) 92 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /bin/git-amend: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Opens the last commit message for editing. 4 | system 'git commit --amend' 5 | -------------------------------------------------------------------------------- /bin/git-bump: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Bumps version number. 4 | system 'git commit -am "Bump version number"' 5 | -------------------------------------------------------------------------------- /bin/git-cleanup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'optparse' 3 | 4 | # Deletes (almost) every branch already merged into current branch. 5 | # Exceptions are `master`, `main`, `staging`, and `development`, 6 | # and the current branch, which are preserved. 7 | # We also support custom configuration via the `~/.git-cleanup-preserved` file. 8 | 9 | options = {} 10 | OptionParser.new do |opts| 11 | opts.banner = "Usage: git cleanup [options]" 12 | 13 | opts.on("-r", "Clean up remote branches") do 14 | options[:remote] = true 15 | end 16 | end.parse! 17 | 18 | preserved = "master|main|staging|development" 19 | preserved_file = File.join(Dir.home, '.git-cleanup-preserved') 20 | if File.exist?(preserved_file) 21 | additional_preserved = File.read(preserved_file).strip.split("\n") 22 | unless additional_preserved.empty? 23 | preserved += '|' + additional_preserved.join('|') 24 | end 25 | end 26 | cmd = %(git branch --merged | grep -v "\*" | egrep -v "(#{preserved}|HEAD)" | ) 27 | if options[:remote] 28 | cmd += "sed -e 's/origin\\//:/' | xargs git push origin" 29 | cmd.sub!('git branch', 'git branch -r') 30 | else 31 | cmd += 'xargs -n 1 git branch -d' 32 | end 33 | system cmd 34 | -------------------------------------------------------------------------------- /bin/git-delete-remote-branch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/delete_remote_branch' 4 | 5 | # Deletes the remote branch on origin if it is safe to do so. 6 | exit Command.run!(DeleteRemoteBranch, ARGV.dup) 7 | -------------------------------------------------------------------------------- /bin/git-files-changed: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # See which files changed. 4 | system 'git log --name-only' 5 | -------------------------------------------------------------------------------- /bin/git-graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Displays full repository history in graphical format 4 | system 'git log --graph --oneline --decorate --all --full-history --author-date-order --no-notes' 5 | -------------------------------------------------------------------------------- /bin/git-merge-into-branch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/merge_branch' 4 | 5 | # Merges the current branch into the given branch (defaults to master). 6 | # E.g., 'git merge-into-branch foobar' merges the current branch into foobar. 7 | # 'git merge-into-branch', merges the current branch into master. 8 | # git merge-into-branch uses the --no-ff --log options to ensure that the 9 | # merge creates a new commit object and that the individual commits appear 10 | # in the log file. 11 | exit Command.run!(MergeBranch, ARGV.dup) 12 | -------------------------------------------------------------------------------- /bin/git-minor: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system 'git commit -am "Make minor changes"' 4 | -------------------------------------------------------------------------------- /bin/git-open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/open' 4 | 5 | # Opens the remote page for the repo (OS X only). 6 | exit Command.run!(Open, ARGV.dup) 7 | -------------------------------------------------------------------------------- /bin/git-polish: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Makes a commit with the message 'Polish' 4 | system 'git commit -am "Polish"' 5 | -------------------------------------------------------------------------------- /bin/git-pull-request: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/pull_request' 4 | 5 | # Opens the remote page for issuing a new pull request. 6 | exit Command.run!(PullRequest, ARGV.dup) 7 | -------------------------------------------------------------------------------- /bin/git-push-branch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/push_branch' 4 | 5 | # Pushes the current branch to origin. 6 | exit Command.run!(PushBranch, ARGV.dup) 7 | 8 | -------------------------------------------------------------------------------- /bin/git-switch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/switch' 4 | 5 | # Switches to the first branch matching the given pattern. 6 | # E.g., 'git switch foobar' switches to 'the-foobar-branch'. 7 | exit Command.run!(Switch, ARGV.dup) 8 | -------------------------------------------------------------------------------- /bin/git-sync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/sync' 4 | 5 | # Syncs the local master branch with remote. 6 | exit Command.run!(Sync, ARGV.dup) 7 | -------------------------------------------------------------------------------- /bin/git-sync-fork: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 3 | require 'git-utils/sync_fork' 4 | 5 | # Syncs the local master branch with remote. 6 | exit Command.run!(SyncFork, ARGV.dup) 7 | -------------------------------------------------------------------------------- /bin/git-typo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system 'git commit -am "Fix typo"' 4 | -------------------------------------------------------------------------------- /bin/git-typos: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | system 'git commit -am "Fix typos"' 4 | -------------------------------------------------------------------------------- /bin/git-undo: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # Undoes the last commit and places the changes back in the staging area. 4 | system 'git reset --soft HEAD^' 5 | -------------------------------------------------------------------------------- /git-utils.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'git-utils/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "git-utils" 8 | gem.version = Git::Utils::VERSION 9 | gem.authors = ["Michael Hartl"] 10 | gem.email = ["michael@michaelhartl.com"] 11 | gem.description = %q{Add some Git utilities} 12 | gem.summary = %q{See the README for full documentation} 13 | gem.homepage = "https://github.com/mhartl/git-utils" 14 | gem.license = "MIT" 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ["lib"] 20 | end 21 | -------------------------------------------------------------------------------- /lib/git-utils.rb: -------------------------------------------------------------------------------- 1 | require "git-utils/version" 2 | require "git-utils/options" 3 | require "git-utils/command" 4 | require "git-utils/merge_branch" 5 | require "git-utils/open" 6 | require "git-utils/delete_remote_branch" 7 | require "git-utils/push_branch" 8 | require "git-utils/switch" 9 | require "git-utils/sync" 10 | require "git-utils/sync_fork" 11 | require "git-utils/pull_request" 12 | -------------------------------------------------------------------------------- /lib/git-utils/command.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | require 'ostruct' 3 | require 'git-utils/options' 4 | 5 | class Command 6 | attr_accessor :args, :cmd, :options, :known_options, :unknown_options 7 | 8 | def initialize(args = []) 9 | self.args = args 10 | self.options = OpenStruct.new 11 | parse 12 | end 13 | 14 | def parse 15 | self.known_options = Options::known_options(parser, args) 16 | self.unknown_options = Options::unknown_options(parser, args) 17 | parser.parse!(known_options) 18 | end 19 | 20 | def parser 21 | OptionParser.new 22 | end 23 | 24 | # Returns the current Git branch. 25 | def current_branch 26 | @current_branch ||= `git rev-parse --abbrev-ref HEAD`.strip 27 | end 28 | 29 | # Returns the default branch for the current repository. 30 | # Command retrieved from 31 | # https://stackoverflow.com/questions/28666357/git-how-to-get-default-branch 32 | def default_branch 33 | branch_name = `git symbolic-ref --short refs/remotes/origin/HEAD \ 34 | | sed 's@^origin/@@'`.strip 35 | if branch_name.empty? 36 | $stderr.puts "Repository configuration error" 37 | $stderr.puts "Missing reference to refs/remotes/origin/HEAD" 38 | $stderr.puts "Run" 39 | $stderr.puts 40 | $stderr.puts " git remote set-head origin " 41 | $stderr.puts 42 | $stderr.puts "where is the default branch name" 43 | $stderr.puts "(typically `main`, `master`, or `trunk`)" 44 | $stderr.puts "and then rerun the command" 45 | exit(1) 46 | end 47 | @default_branch ||= branch_name 48 | end 49 | 50 | # Returns the URL for the remote origin. 51 | def origin_url 52 | @origin_url ||= `git config --get remote.origin.url`.strip 53 | end 54 | 55 | # Returns the name of the repository service. 56 | # It's currently GitHub, Bitbucket, or Stash. 57 | # We return blank for an unknown service; the command will still 58 | # often work in that case. 59 | def service 60 | if origin_url =~ /github/i 61 | 'github' 62 | elsif origin_url =~ /bitbucket/i 63 | 'bitbucket' 64 | elsif origin_url =~ /stash/i 65 | 'stash' 66 | else 67 | '' 68 | end 69 | end 70 | 71 | # Returns the protocol of the origin URL (defaults to ssh). 72 | def protocol 73 | if origin_url =~ /https?:\/\// 74 | 'http' 75 | else 76 | 'ssh' 77 | end 78 | end 79 | 80 | # Runs a command. 81 | # If the argument array contains '--debug', returns the command that would 82 | # have been run. 83 | def self.run!(command_class, args) 84 | debug = args.delete('--debug') 85 | command = command_class.new(args) 86 | if debug 87 | puts command.cmd 88 | return 1 89 | else 90 | command.run! 91 | return 0 92 | end 93 | end 94 | 95 | def run! 96 | system cmd 97 | end 98 | 99 | private 100 | 101 | # Returns an argument string based on given arguments. 102 | # The main trick is to add in quotes for option 103 | # arguments when necessary. 104 | # For example, ['-a', '-m', 'foo bar'] becomes 105 | # '-a -m "foo bar"' 106 | def argument_string(args) 107 | args.inject([]) do |opts, opt| 108 | opts << (opt =~ /^-/ ? opt : opt.inspect) 109 | end.join(' ') 110 | end 111 | 112 | def finish? 113 | options.finish 114 | end 115 | 116 | def deliver? 117 | options.deliver 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /lib/git-utils/delete_remote_branch.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class DeleteRemoteBranch < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git delete-remote-branch " 8 | opts.on("-o", "--override", "override unsafe delete") do |opt| 9 | self.options.override = opt 10 | end 11 | opts.on_tail("-h", "--help", "this usage guide") do 12 | puts opts.to_s; exit 0 13 | end 14 | end 15 | end 16 | 17 | def delete_safely? 18 | command = "git log ..origin/#{target_branch} 2> /dev/null" 19 | system(command) && `#{command}`.strip.empty? 20 | end 21 | 22 | # Returns a command appropriate for executing at the command line. 23 | def cmd 24 | if delete_safely? || options.override 25 | c = ["git push origin :#{target_branch}"] 26 | c << argument_string(unknown_options) unless unknown_options.empty? 27 | c.join(" ") 28 | else 29 | $stderr.puts "Target branch contains unmerged commits." 30 | $stderr.puts "Please cherry-pick the commits or merge the branch again." 31 | $stderr.puts "Use -o or --override to override." 32 | end 33 | end 34 | 35 | private 36 | 37 | # Returns the name of the branch to be deleted. 38 | def target_branch 39 | self.known_options.first 40 | end 41 | end -------------------------------------------------------------------------------- /lib/git-utils/merge_branch.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class MergeBranch < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git merge-into-branch [branch] [options]" 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns a command appropriate for executing at the command line. 15 | # For example: 16 | # git checkout master 17 | # git merge --no-ff --log 18 | def cmd 19 | lines = ["git checkout #{target_branch}"] 20 | c = ["git merge --no-ff --log"] 21 | c << argument_string(unknown_options) unless unknown_options.empty? 22 | c << current_branch 23 | lines << c.join(' ') 24 | lines.join("\n") 25 | end 26 | 27 | private 28 | 29 | # Returns the name of the branch to be merged into. 30 | # If there is anything left in the known options after parsing, 31 | # that's the merge branch. Otherwise, it's the default branch. 32 | def target_branch 33 | self.known_options.first || default_branch 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/git-utils/open.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class Open < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git open" 8 | opts.on("-p", "--print", "print URL instead of opening") do |opt| 9 | self.options.print = opt 10 | end 11 | opts.on_tail("-h", "--help", "this usage guide") do 12 | puts opts.to_s; exit 0 13 | end 14 | end 15 | end 16 | 17 | # Returns the URL for the repository page. 18 | def page_url 19 | if service == 'stash' && protocol == 'ssh' 20 | pattern = /(.*)@([^:]*):?([^\/]*)\/([^\/]*)\/(.*)\.git/ 21 | replacement = 'https://\2/projects/\4/repos/\5/browse?at=' + 22 | current_branch 23 | elsif service == 'stash' && protocol == 'http' 24 | pattern = /(.*)@([^:\/]*)(:?[^\/]*)\/(.*)scm\/([^\/]*)\/(.*)\.git/ 25 | replacement = 'https://\2\3/\4projects/\5/repos/\6/browse?at=' + 26 | current_branch 27 | elsif protocol == 'ssh' 28 | pattern = /(.*)@(.*):(.*)\.git/ 29 | replacement = 'https://\2/\3' 30 | elsif protocol == 'http' 31 | pattern = /https?\:\/\/(([^@]*)@)?(.*)\.git/ 32 | replacement = 'https://\3' 33 | end 34 | origin_url.sub(pattern, replacement) 35 | end 36 | 37 | # Returns a command appropriate for executing at the command line. 38 | def cmd 39 | if options[:print] 40 | puts page_url 41 | "" 42 | else 43 | "#{open} #{page_url}" 44 | end 45 | end 46 | 47 | private 48 | 49 | # Returns the system-dependent `open` command. 50 | def open 51 | if os_x? 52 | 'open' 53 | elsif linux? 54 | 'xdg-open' 55 | else 56 | raise "Platform #{RUBY_PLATFORM} not supported" 57 | end 58 | end 59 | 60 | # Returns true if platform is OS X. 61 | def os_x? 62 | RUBY_PLATFORM.match(/darwin/) 63 | end 64 | 65 | # Returns true if platform is Linux. 66 | def linux? 67 | RUBY_PLATFORM.match(/linux/) 68 | end 69 | end -------------------------------------------------------------------------------- /lib/git-utils/options.rb: -------------------------------------------------------------------------------- 1 | require 'optparse' 2 | 3 | module Options 4 | 5 | # Returns a list of options unknown to a particular options parser 6 | # For example, if '-a' is a known option but '-b' and '-c' are not, 7 | # unknown_options(parser, ['-a', '-b', '-c']) returns ['-b', '-c']. 8 | # It also preserves arguments, so 9 | # unknown_options(parser, ['-a', '-b', '-c', 'foo bar']) returns 10 | # ['-b', '-c', 'foo bar']. 11 | def self.unknown_options(parser, args) 12 | unknown = [] 13 | recursive_parse = Proc.new do |arg_list| 14 | begin 15 | # Hack to handle an unknown '-ff' argument 16 | # The issue here is that OptParse interprets '-ff' as a '-f' option 17 | # applied twice. This is sort-of a feature, as it allows, e.g., '-am' 18 | # to set both the '-a' and '-m' options, but it interacts badly 19 | # with '-ff' (as used by 'git merge') when '-f' is one of the options. 20 | unknown << arg_list.delete('-ff') if arg_list.include?('-ff') 21 | parser.parse!(arg_list) 22 | rescue OptionParser::InvalidOption => e 23 | unknown.concat(e.args) 24 | while !arg_list.empty? && arg_list.first[0] != "-" 25 | unknown << arg_list.shift 26 | end 27 | recursive_parse.call(arg_list) 28 | end 29 | end 30 | recursive_parse.call(args.dup) 31 | unknown 32 | end 33 | 34 | # Returns a list of options with unknown options removed 35 | def self.known_options(parser, args) 36 | unknown = unknown_options(parser, args) 37 | args.reject { |arg| unknown.include?(arg) } 38 | end 39 | end -------------------------------------------------------------------------------- /lib/git-utils/pull_request.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class PullRequest < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git pull-request" 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns the URL for a new pull request. 15 | def new_pr_url 16 | if service == 'stash' && protocol == 'ssh' 17 | pattern = /(.*)@([^:]*):?([^\/]*)\/([^\/]*)\/(.*)\.git/ 18 | replacement = 'https://\2/projects/\4/repos/\5/pull-requests?create&sourceBranch=' + 19 | current_branch 20 | elsif service == 'stash' && protocol == 'http' 21 | pattern = /(.*)@([^:\/]*)(:?[^\/]*)\/(.*)scm\/([^\/]*)\/(.*)\.git/ 22 | replacement = 'https://\2\3/\4projects/\5/repos/\6/pull-requests?create&sourceBranch=' + 23 | current_branch 24 | elsif service == 'github' && protocol == 'ssh' 25 | pattern = /(.*)@(.*):(.*)\.git/ 26 | replacement = 'https://\2/\3/pull/new/' + current_branch 27 | elsif service == 'github' && protocol == 'http' 28 | pattern = /https?\:\/\/(([^@]*)@)?(.*)\.git/ 29 | replacement = 'https://\3/pull/new/' + current_branch 30 | elsif service == 'bitbucket' && protocol == 'ssh' 31 | pattern = /(.*)@(.*):(.*)\.git/ 32 | replacement = 'https://\2/\3/pull-request/new/' 33 | elsif service == 'bitbucket' && protocol == 'http' 34 | pattern = /https?\:\/\/(([^@]*)@)?(.*)\.git/ 35 | replacement = 'https://\3/pull-request/new/' 36 | end 37 | origin_url.sub(pattern, replacement) 38 | end 39 | 40 | # Returns a command appropriate for executing at the command line. 41 | def cmd 42 | push = ["git push-branch"] 43 | push += argument_string(unknown_options) unless unknown_options.empty? 44 | push = push.join(" ") 45 | c = [push, "open #{new_pr_url}"] 46 | c.join("\n") 47 | end 48 | end -------------------------------------------------------------------------------- /lib/git-utils/push_branch.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class PushBranch < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git push-branch" 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns a command appropriate for executing at the command line. 15 | def cmd 16 | c = ["git push --set-upstream origin #{current_branch}"] 17 | c << argument_string(unknown_options) unless unknown_options.empty? 18 | c.join(" ") 19 | end 20 | end -------------------------------------------------------------------------------- /lib/git-utils/switch.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class Switch < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git switch " 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns the branch to switch to. 15 | # When multiple branches match, switch to the first one. 16 | def other_branch 17 | @other_branch ||= `git branch | grep #{pattern}`.split.first 18 | end 19 | 20 | # Returns a command appropriate for executing at the command line. 21 | def cmd 22 | "git checkout #{other_branch}" 23 | end 24 | 25 | private 26 | 27 | # Returns the pattern of the branch to switch to. 28 | def pattern 29 | self.known_options.first 30 | end 31 | end -------------------------------------------------------------------------------- /lib/git-utils/sync.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class Sync < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git sync [branch]" 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns a command appropriate for executing at the command line. 15 | def cmd 16 | branch = self.known_options.first || default_branch 17 | c = ["git checkout #{branch}"] 18 | c << "git pull" 19 | c << "git checkout #{current_branch}" 20 | c.join("\n") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/git-utils/sync_fork.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils/command' 2 | 3 | class SyncFork < Command 4 | 5 | def parser 6 | OptionParser.new do |opts| 7 | opts.banner = "Usage: git sync-fork [default]" 8 | opts.on_tail("-h", "--help", "this usage guide") do 9 | puts opts.to_s; exit 0 10 | end 11 | end 12 | end 13 | 14 | # Returns a command appropriate for executing at the command line. 15 | def cmd 16 | c = ["git checkout #{default_branch}"] 17 | c << "git fetch upstream" 18 | c << "git merge upstream/#{default_branch}" 19 | c.join("\n") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/git-utils/version.rb: -------------------------------------------------------------------------------- 1 | module Git 2 | module Utils 3 | VERSION = "2.4.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhartl/git-utils/e43216dba9c181713dd2c85e68d4e5a59296cc9d/spec/.DS_Store -------------------------------------------------------------------------------- /spec/commands/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mhartl/git-utils/e43216dba9c181713dd2c85e68d4e5a59296cc9d/spec/commands/.DS_Store -------------------------------------------------------------------------------- /spec/commands/command_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Command do 4 | let(:command) { Command.new } 5 | subject { command } 6 | 7 | it { should respond_to(:cmd) } 8 | it { should respond_to(:args) } 9 | it { should respond_to(:options) } 10 | it { should respond_to(:parse) } 11 | end -------------------------------------------------------------------------------- /spec/commands/delete_remote_branch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe DeleteRemoteBranch do 4 | 5 | let(:command) { DeleteRemoteBranch.new(['remote_branch']) } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | command.stub(:delete_safely?).and_return(true) 9 | end 10 | subject { command } 11 | 12 | its(:cmd) { should match /git push origin :remote_branch/ } 13 | 14 | describe "command-line command" do 15 | subject { `bin/git-delete-remote-branch foobar -o --debug` } 16 | it { should match /git push origin/ } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/commands/merge_branch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MergeBranch do 4 | 5 | let(:command) { MergeBranch.new } 6 | before { command.stub(:current_branch).and_return('tau-manifesto') } 7 | subject { command } 8 | 9 | its(:cmd) { should match /git merge/ } 10 | 11 | shared_examples "merge-into-branch with known options" do 12 | subject { command } 13 | it "should not raise an error" do 14 | expect { command.parse }.not_to raise_error(OptionParser::InvalidOption) 15 | end 16 | end 17 | 18 | describe "with no options" do 19 | its(:cmd) { should match /git checkout master/ } 20 | end 21 | 22 | describe "default branch" do 23 | let(:command) { MergeBranch.new } 24 | 25 | describe "for current real repo" do 26 | subject { command.default_branch } 27 | it { should match 'master' } 28 | end 29 | 30 | describe "for repo with different default" do 31 | before { command.stub(:default_branch).and_return('main') } 32 | subject { command.default_branch } 33 | it { should match 'main' } 34 | end 35 | end 36 | 37 | describe "with a custom development branch" do 38 | let(:command) { MergeBranch.new(['development']) } 39 | its(:cmd) { should match /git checkout development/ } 40 | end 41 | 42 | describe "with some unknown options" do 43 | let(:command) { MergeBranch.new(['dev', '-o', '-a', '-z', '--foo']) } 44 | it_should_behave_like "merge-into-branch with known options" 45 | its(:cmd) { should match /-a -z --foo/ } 46 | end 47 | 48 | describe "command-line command" do 49 | subject { `bin/git-merge-into-branch --debug development` } 50 | it { should match /git checkout development/ } 51 | it { should match /git merge --no-ff --log/ } 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/commands/open_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Open do 4 | 5 | let(:command) { Open.new } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | end 9 | subject { command } 10 | 11 | its(:cmd) { should match /open #{command.page_url}/ } 12 | 13 | it "should have the right page URLs" do 14 | urls = %w[ 15 | https://mwatson@bitbucket.org/atlassian/amps.git https://bitbucket.org/atlassian/amps 16 | git@bitbucket.org:atlassian/amps.git https://bitbucket.org/atlassian/amps 17 | git@github.com:mhartl/git-utils.git https://github.com/mhartl/git-utils 18 | https://github.com/mhartl/git-utils.git https://github.com/mhartl/git-utils 19 | ssh://git@stash.atlassian.com:7999/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/browse?at=test-br 20 | https://mwatson@stash.atlassian.com:7990/scm/stash/stash.git https://stash.atlassian.com:7990/projects/stash/repos/stash/browse?at=test-br 21 | ssh://git@stash.atlassian.com/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/browse?at=test-br 22 | https://mwatson@stash.atlassian.com/scm/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/browse?at=test-br 23 | https://mwatson@stash.atlassian.com/stash/scm/stash/stash.git https://stash.atlassian.com/stash/projects/stash/repos/stash/browse?at=test-br 24 | https://mwatson@stash.atlassian.com:7990/stash/scm/stash/stash.git https://stash.atlassian.com:7990/stash/projects/stash/repos/stash/browse?at=test-br 25 | https://example.com/repos/foobar.git https://example.com/repos/foobar 26 | ] 27 | urls.each_slice(2) do |origin_url, page_url| 28 | command.stub(:origin_url).and_return(origin_url) 29 | expect(command.page_url).to include page_url 30 | end 31 | end 32 | 33 | describe "command-line command" do 34 | subject { `bin/git-open --debug` } 35 | it { should match /open/ } 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/commands/pull_request_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PullRequest do 4 | 5 | let(:command) { PullRequest.new } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | end 9 | subject { command } 10 | 11 | its(:cmd) { should match /open #{command.new_pr_url}/ } 12 | 13 | it "should have the right pull request URLs" do 14 | urls = %w[ 15 | https://mwatson@bitbucket.org/atlassian/amps.git https://bitbucket.org/atlassian/amps/pull-request/new 16 | git@bitbucket.org:atlassian/amps.git https://bitbucket.org/atlassian/amps/pull-request/new 17 | git@github.com:mhartl/git-utils.git https://github.com/mhartl/git-utils/pull/new/test-br 18 | https://github.com/mhartl/git-utils.git https://github.com/mhartl/git-utils/pull/new/test-br 19 | ssh://git@stash.atlassian.com:7999/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 20 | https://mwatson@stash.atlassian.com:7990/scm/stash/stash.git https://stash.atlassian.com:7990/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 21 | ssh://git@stash.atlassian.com/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 22 | https://mwatson@stash.atlassian.com/scm/stash/stash.git https://stash.atlassian.com/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 23 | https://mwatson@stash.atlassian.com/stash/scm/stash/stash.git https://stash.atlassian.com/stash/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 24 | https://mwatson@stash.atlassian.com:7990/stash/scm/stash/stash.git https://stash.atlassian.com:7990/stash/projects/stash/repos/stash/pull-requests?create&sourceBranch=test-br 25 | ] 26 | 27 | urls.each_slice(2) do |origin_url, new_pr_url| 28 | command.stub(:origin_url).and_return(origin_url) 29 | expect(command.new_pr_url).to include new_pr_url 30 | end 31 | end 32 | 33 | 34 | describe "command-line command" do 35 | subject { `bin/git-pull-request --debug` } 36 | it { should match /git push-branch/ } 37 | it { should match /open/ } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/commands/push_branch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe PushBranch do 4 | 5 | let(:command) { PushBranch.new(['remote_branch']) } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | end 9 | subject { command } 10 | 11 | its(:cmd) do 12 | should match /git push --set-upstream origin #{command.current_branch}/ 13 | end 14 | 15 | describe "command-line command" do 16 | subject { `bin/git-push-branch --debug` } 17 | it { should match /git push --set-upstream origin/ } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/commands/switch_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Switch do 4 | 5 | let(:command) { Switch.new(['other-branch']) } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | command.stub(:other_branch).and_return('other-branch') 9 | end 10 | subject { command } 11 | 12 | its(:cmd) { should match /git checkout #{command.other_branch}/ } 13 | 14 | describe "command-line command" do 15 | subject { `bin/git-switch example --debug` } 16 | it { should match /git checkout/ } 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/commands/sync_fork_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SyncFork do 4 | 5 | let(:command) { SyncFork.new } 6 | subject { command } 7 | 8 | its(:cmd) { should match /git checkout master/ } 9 | its(:cmd) { should match /git fetch upstream/ } 10 | its(:cmd) { should match /git merge upstream\/master/ } 11 | 12 | 13 | describe "command-line command" do 14 | subject { `bin/git-sync-fork --debug` } 15 | it { should match /git fetch upstream/ } 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/commands/sync_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Sync do 4 | 5 | let(:command) { Sync.new } 6 | before do 7 | command.stub(:current_branch).and_return('test-br') 8 | end 9 | subject { command } 10 | 11 | its(:cmd) { should match /git checkout master/ } 12 | its(:cmd) { should match /git pull/ } 13 | its(:cmd) { should match /git checkout #{command.current_branch}/ } 14 | 15 | describe "description" do 16 | let(:alternate_branch) { 'alternate' } 17 | before { command.stub(:known_options).and_return([alternate_branch]) } 18 | its(:cmd) { should match /git checkout #{alternate_branch}/ } 19 | end 20 | 21 | describe "command-line command" do 22 | subject { `bin/git-sync --debug` } 23 | it { should match /git checkout master/ } 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'git-utils' 2 | 3 | RSpec.configure do |config| 4 | config.treat_symbols_as_metadata_keys_with_true_values = true 5 | config.run_all_when_everything_filtered = true 6 | config.filter_run :focus 7 | 8 | # Disallow the old-style 'object.should' syntax. 9 | config.expect_with :rspec do |c| 10 | c.syntax = :expect 11 | end 12 | end 13 | --------------------------------------------------------------------------------