├── .gitignore ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── exe └── git-status-all ├── git-status.png ├── git-status_all.gemspec ├── lib └── git │ ├── status_all.rb │ └── status_all │ ├── extensions.rb │ ├── git.rb │ └── version.rb └── status-all-test /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gemspec 3 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | git-status-all (1.1.4) 5 | colorize 6 | git (~> 1.3) 7 | optimist (~> 3.0) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | colorize (0.8.1) 13 | git (1.5.0) 14 | optimist (3.0.0) 15 | rake (10.5.0) 16 | 17 | PLATFORMS 18 | ruby 19 | x64-mingw32 20 | 21 | DEPENDENCIES 22 | bundler (~> 2.0) 23 | git-status-all! 24 | rake (~> 10.0) 25 | 26 | BUNDLED WITH 27 | 2.0.1 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Nathan Reed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git status-all 2 | 3 | ## Installation 4 | 5 | $ gem install git-status-all 6 | 7 | ## Usage 8 | 9 | Run the `status-all` subcommand inside a directory containing a number of repositories, and it will show the status for all of them. 10 | 11 | $ git status-all 12 | 13 | ![git-status terminal example](git-status.png) 14 | 15 | Often you want to fetch from all the remotes for each repository first to see if there are any upstream changes. Use the `--fetch` or `-f` option to do this. 16 | 17 | $ git status-all --fetch 18 | 19 | It is also possible to look in a particular path instead of the current directory 20 | 21 | $ git status-all ~/dev/ios 22 | 23 | ## License 24 | 25 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 26 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :build 3 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "git/status_all" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 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 9 | -------------------------------------------------------------------------------- /exe/git-status-all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'git/status_all' 3 | Git::StatusAll::App.new.main 4 | -------------------------------------------------------------------------------- /git-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reednj/git-status-all/f004db3be2eafca5c331d06a3b4dc7c937059046/git-status.png -------------------------------------------------------------------------------- /git-status_all.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'git/status_all/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "git-status-all" 8 | spec.version = Git::StatusAll::VERSION 9 | spec.authors = ["Nathan Reed"] 10 | spec.email = ["reednj@gmail.com"] 11 | 12 | spec.summary = %q{show the status of all the git repositories in a directory} 13 | spec.homepage = "https://github.com/reednj/git-status-all" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 17 | spec.bindir = "exe" 18 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 2.0" 22 | spec.add_development_dependency "rake", "~> 10.0" 23 | 24 | spec.required_ruby_version = '>=1.9.3' 25 | spec.add_dependency 'colorize' 26 | spec.add_dependency 'optimist', "~> 3.0" 27 | spec.add_dependency 'git', "~> 1.3" 28 | end 29 | -------------------------------------------------------------------------------- /lib/git/status_all.rb: -------------------------------------------------------------------------------- 1 | require 'git' 2 | require 'colorize' 3 | require 'optimist' 4 | 5 | require "git/status_all/version" 6 | require "git/status_all/extensions" 7 | require "git/status_all/git" 8 | 9 | module Git 10 | module StatusAll 11 | class App 12 | def main 13 | # we want to disable the text coloring if we are printing to a 14 | # file, or on a platform (like windows) that likely doesn't support 15 | # the colors 16 | String.disable_colorization = !$stdout.isatty 17 | 18 | opts = Optimist::options do 19 | version "git-status-all #{Git::StatusAll::VERSION} (c) 2016 @reednj (reednj@gmail.com)" 20 | banner "Usage: git-status-all [options] [path]" 21 | opt :fetch, "perform fetch for each repository before getting status", :default => false 22 | end 23 | 24 | repo_paths = [] 25 | 26 | begin 27 | dev_dir = ARGV.last || '.' 28 | repo_paths = Dir.entries(dev_dir). 29 | map {|p| { :name => p, :path => File.expand_path(p, dev_dir) } }. 30 | select { |p| Git.repo? p[:path] } 31 | rescue => e 32 | $stderr.puts "Could not read repositories in '#{dev_dir}': #{e}" 33 | end 34 | 35 | repo_paths.each do |p| 36 | name = p[:name] 37 | 38 | begin 39 | g = Git.open p[:path] 40 | 41 | if opts[:fetch] 42 | print "#{name}".right_align("[#{"fetching...".yellow}]") + "\r" 43 | 44 | if !g.remotes.empty? 45 | remote = g.remotes.select{|r| r.name.downcase == 'origin' }.first || g.remotes.first 46 | g.fetch remote 47 | end 48 | end 49 | 50 | s = file_status(g) 51 | r = remote_status(g) 52 | s = " #{s} ".black.on_yellow unless s.empty? 53 | n = s.empty? ? name : name.yellow 54 | puts "#{n}".pad_to_col(24).append(s).right_align("#{r} [#{g.branches.current.to_s.blue}]") 55 | rescue => e 56 | if e.to_s.include? "ambiguous argument 'HEAD'" 57 | err ='ERROR: NO HEAD' 58 | else 59 | err ='ERROR' 60 | end 61 | 62 | puts "#{name}".right_align("[#{err}]".red) 63 | puts e.to_s if err == 'ERROR' 64 | end 65 | end 66 | 67 | end 68 | 69 | def file_status(g) 70 | result = "" 71 | result += "A#{g.status.added.length}" if g.status.added.length > 0 72 | result += "D#{g.status.deleted.length}" if g.status.deleted.length > 0 73 | result += "M#{g.status.changed.length}" if g.status.changed.length > 0 74 | result += "U#{g.status.untracked.length}" if g.status.untracked.length > 0 75 | return result 76 | end 77 | 78 | def remote_status(g) 79 | if g.remotes.empty? 80 | return "no remotes".black.on_red 81 | end 82 | 83 | if g.remotes.select{|r| r.name.downcase == 'origin' }.empty? 84 | return "no origin".black.on_yellow 85 | end 86 | 87 | if !g.branches.current.up_to_date? 88 | b = g.branches.current 89 | 90 | s = '' 91 | s += "#{b.behind_count}\u2193" if b.behind_count > 0 92 | s += "#{b.ahead_count}\u2191" if b.ahead_count > 0 93 | 94 | return s.green 95 | end 96 | 97 | return '' 98 | end 99 | 100 | def term_width 101 | @term_width ||= `tput cols`.to_i 102 | end 103 | 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /lib/git/status_all/extensions.rb: -------------------------------------------------------------------------------- 1 | 2 | 3 | class String 4 | def append(s) 5 | self + s 6 | end 7 | 8 | def pad_to_col(n) 9 | pad_amount = n - self.uncolorize.length 10 | return " " + s if pad_amount < 0 11 | return self + (" " * pad_amount) 12 | end 13 | 14 | def right_align(s) 15 | pad_amount = self._term_width - self.uncolorize.length - s.uncolorize.length 16 | return " " + s if pad_amount < 0 17 | return self + (" " * pad_amount) + s 18 | end 19 | 20 | def _term_width 21 | @term_width ||= `tput cols`.to_i 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/git/status_all/git.rb: -------------------------------------------------------------------------------- 1 | module Git 2 | def self.repo? path 3 | begin 4 | self.open path 5 | return true 6 | rescue 7 | return false 8 | end 9 | end 10 | 11 | class Branches 12 | def current 13 | select{ |b| b.current? }.first 14 | end 15 | end 16 | 17 | class Branch 18 | def current? 19 | current 20 | end 21 | 22 | def up_to_date? 23 | ahead_count == 0 && behind_count == 0 24 | end 25 | 26 | def ahead 27 | origin = self.remotes(:origin) 28 | return [] if origin.nil? 29 | @base.log.between(origin.full, self.name) 30 | end 31 | 32 | def behind 33 | origin = self.remotes(:origin) 34 | return [] if origin.nil? 35 | @base.log.between(self.name, origin.full) 36 | end 37 | 38 | def ahead_count 39 | @ahead_count ||= ahead.count 40 | end 41 | 42 | def behind_count 43 | @behind_count ||= behind.count 44 | end 45 | 46 | def remotes(remote_name = nil) 47 | result = @base.branches.remote.select{|b| b.name == self.name } 48 | return result.select{|b| b.full.include? remote_name.to_s }.first unless remote_name.nil? 49 | return result 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/git/status_all/version.rb: -------------------------------------------------------------------------------- 1 | module Git 2 | module StatusAll 3 | VERSION = "1.1.4" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /status-all-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # exec ruby avoids the "not executable" error in bundler on x64-mingw32 3 | bundle exec ruby exe/git-status-all "$@" 4 | --------------------------------------------------------------------------------