├── .autotest ├── .gitignore ├── LICENSE ├── README.markdown ├── Rakefile ├── bin └── git-pair ├── config └── cucumber.yml ├── features ├── adding_an_author.feature ├── removing_an_author.feature ├── step_definitions │ └── config_steps.rb ├── support │ └── env.rb └── switching_authors.feature ├── git-pair.gemspec └── lib ├── git-pair.rb └── git-pair └── VERSION /.autotest: -------------------------------------------------------------------------------- 1 | Autotest.add_hook :initialize do |autotest| 2 | [/\.git/, /pkg\//, /\.gemspec$/, /\.log$/].each do |regexp| 3 | autotest.add_exception regexp 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Chris Kampmeier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # git-pair 2 | 3 | A git porcelain for changing `user.name` and `user.email` so you can commit as 4 | more than one author. 5 | 6 | ## Usage 7 | 8 | Install the gem: 9 | 10 | gem install git-pair 11 | 12 | And here's how to use it! (Note: this reflects the current development 13 | version. Run `git pair` with no arguments to see the instructions for your 14 | version.) 15 | 16 | $ git pair 17 | 18 | Configuration: 19 | git pair [options] 20 | -a, --add NAME Add an author. Format: "Author Name " 21 | -r, --remove NAME Remove an author. Use the full name. 22 | 23 | Switching authors: 24 | git pair AA [BB] Where AA and BB are any abbreviation of an 25 | author's name. You can specify one or more authors. 26 | 27 | Once you've added authors, running `git pair` with no options will also print 28 | out their names, the current pair, and some other information. 29 | 30 | ## Known issues 31 | 32 | * I just shoved everything into a gem. Refactor into separate files. 33 | * Test coverage is low -- I'm working on a cucumber suite. 34 | 35 | ## Feature hit list 36 | 37 | * It'd be better if you could specify an email address for each author instead 38 | of just automatically using the authors' initials. Especially if you have two 39 | authors with the same initials. And also because when there's just one author, 40 | it should use that person's email instead of an interpolation like 41 | `devs+ck@example.com`. Started! Now accepts author names w/ emails, but 42 | doesn't yet prompt for/generate an email based on the pairs' addresses. 43 | * Needs `git pair --reset` to restore the original `user.name` and `user.email`. 44 | For now, just `git config --edit` and remove the `[user]` section to go back 45 | to your global config. 46 | 47 | ## License 48 | 49 | Copyright (c) 2009 Chris Kampmeier. See `LICENSE` for details. 50 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rake' 3 | 4 | begin 5 | require 'jeweler' 6 | Jeweler::Tasks.new do |gem| 7 | gem.name = "git-pair" 8 | gem.version = File.read("lib/git-pair/VERSION").strip 9 | gem.summary = "Configure git to commit as more than one author" 10 | gem.description = "A git porcelain for pair programming. Changes " + 11 | "git-config's user.name and user.email settings so you " + 12 | "can commit as more than one author." 13 | gem.email = "chris@kampers.net" 14 | gem.homepage = "http://github.com/chrisk/git-pair" 15 | gem.authors = ["Chris Kampmeier"] 16 | gem.add_development_dependency "cucumber", ">= 0" 17 | end 18 | Jeweler::GemcutterTasks.new 19 | rescue LoadError 20 | puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" 21 | end 22 | 23 | begin 24 | require 'cucumber/rake/task' 25 | Cucumber::Rake::Task.new(:features) 26 | 27 | task :features => :check_dependencies 28 | rescue LoadError 29 | task :features do 30 | abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber" 31 | end 32 | end 33 | 34 | task :default => :features 35 | 36 | # Don't print commands when shelling out (for example, running Cucumber) 37 | RakeFileUtils.verbose(false) 38 | 39 | -------------------------------------------------------------------------------- /bin/git-pair: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib")) 3 | 4 | require 'optparse' 5 | require 'git-pair' 6 | 7 | begin 8 | banner = "\n#{GitPair::C_REVERSE} Configuration: #{GitPair::C_RESET}" 9 | parser = OptionParser.new do |opts| 10 | opts.banner = banner 11 | opts.separator " git pair [options]" 12 | opts.on("-a", "--add NAME", 'Add an author. Format: "Author Name "') { |name| GitPair::Commands.add name } 13 | opts.on("-r", "--remove NAME", "Remove an author. Use the full name.") { |name| GitPair::Commands.remove name } 14 | 15 | opts.separator " " 16 | opts.separator ["#{GitPair::C_REVERSE} Switching authors: #{GitPair::C_RESET}", 17 | " git pair AA [BB] Where AA and BB are any abbreviation of an", 18 | " "*37 + "author's name. You can specify one or more authors."] 19 | 20 | opts.separator " " 21 | opts.separator ["#{GitPair::C_REVERSE} Current config: #{GitPair::C_RESET}", 22 | *(GitPair::Helpers.display_string_for_config.split("\n") + [" "] + 23 | GitPair::Helpers.display_string_for_current_info.split("\n"))] 24 | end 25 | 26 | unused_options = parser.parse!.dup 27 | ARGV.clear 28 | 29 | if GitPair::Commands.config_change_made? 30 | puts GitPair::Helpers.display_string_for_config 31 | elsif unused_options.any? 32 | GitPair::Commands.switch(unused_options) 33 | puts GitPair::Helpers.display_string_for_current_info 34 | else 35 | puts parser.help 36 | end 37 | 38 | rescue OptionParser::MissingArgument 39 | GitPair::Helpers.abort "missing required argument", parser.help 40 | rescue OptionParser::InvalidOption, OptionParser::InvalidArgument => e 41 | GitPair::Helpers.abort e.message.sub(':', ''), parser.help 42 | rescue GitPair::NoMatchingAuthorsError => e 43 | GitPair::Helpers.abort e.message, "\n" + GitPair::Helpers.display_string_for_config 44 | rescue GitPair::MissingConfigurationError => e 45 | GitPair::Helpers.abort e.message, parser.help 46 | end 47 | -------------------------------------------------------------------------------- /config/cucumber.yml: -------------------------------------------------------------------------------- 1 | default: --format progress --color features 2 | autotest: --format pretty 3 | autotest-all: --profile default 4 | -------------------------------------------------------------------------------- /features/adding_an_author.feature: -------------------------------------------------------------------------------- 1 | Feature: Adding an author 2 | In order to commit as a pair 3 | A user should be able to 4 | add a name and email to the list of authors 5 | 6 | Scenario: adding a name and email 7 | When I add the author "Linus Torvalds " 8 | Then `git pair` should display "Linus Torvalds" in its author list 9 | 10 | Scenario: adding the same name and email twice 11 | When I add the author "Linus Torvalds " 12 | And I add the author "Linus Torvalds " 13 | Then `git pair` should display "Linus Torvalds" in its author list only once 14 | And the gitconfig should include "Linus Torvalds" in its author list only once 15 | 16 | Scenario: adding the same name twice with different emails 17 | When I add the author "Linus Torvalds " 18 | And I add the author "Linus Torvalds " 19 | Then `git pair` should display "Linus Torvalds" in its author list only once 20 | And the gitconfig should include "Linus Torvalds" in its author list only once 21 | And the gitconfig should include "linus@example.com" as the email of "Linus Torvalds" 22 | -------------------------------------------------------------------------------- /features/removing_an_author.feature: -------------------------------------------------------------------------------- 1 | Feature: Adding an author 2 | In order remove old pairing partners 3 | A user should be able to 4 | remove a name from the list of authors 5 | 6 | Scenario: removing a name 7 | When I add the author "Linus Torvalds " 8 | And I add the author "Junio C Hamano " 9 | And I remove the name "Junio C Hamano" 10 | Then `git pair` should display the following author list: 11 | | name | 12 | | Linus Torvalds | 13 | 14 | Scenario: removing all names 15 | When I add the author "Linus Torvalds " 16 | And I add the author "Junio C Hamano " 17 | And I remove the name "Linus Torvalds" 18 | And I remove the name "Junio C Hamano" 19 | Then `git pair` should display an empty author list 20 | -------------------------------------------------------------------------------- /features/step_definitions/config_steps.rb: -------------------------------------------------------------------------------- 1 | Given /^I have added the author "([^\"]*)"$/ do |name_and_email| 2 | When %(I add the author "#{name_and_email}") 3 | end 4 | 5 | When /^I add the author "([^\"]*)"$/ do |name_and_email| 6 | git_pair %(--add "#{name_and_email}") 7 | end 8 | 9 | When /^I remove the name "([^\"]*)"$/ do |name| 10 | git_pair %(--remove "#{name}") 11 | end 12 | 13 | When /^I (?:try to )?switch to the pair "([^\"]*)"$/ do |abbreviations| 14 | @output = git_pair abbreviations 15 | end 16 | 17 | Then /^`git pair` should display "([^\"]*)" in its author list$/ do |name| 18 | output = git_pair 19 | authors = authors_list_from_output(output) 20 | assert authors.include?(name) 21 | end 22 | 23 | Then /^`git pair` should display "([^\"]*)" in its author list only once$/ do |name| 24 | output = git_pair 25 | authors = authors_list_from_output(output) 26 | assert_equal 1, authors.select { |author| author == name}.size 27 | end 28 | 29 | Then /^`git pair` should display "([^\"]*)" for the current author$/ do |names| 30 | output = git_pair 31 | assert_equal names, current_author_from_output(output) 32 | end 33 | 34 | Then /^`git pair` should display "([^\"]*)" for the current email$/ do |email| 35 | output = git_pair 36 | assert_equal email, current_email_from_output(output) 37 | end 38 | 39 | Then /^the gitconfig should include "([^\"]*)" in its author list only once$/ do |name| 40 | output = git_config 41 | authors = output.split("\n").map { |line| line =~ /^git-pair\.authors=(.*) <[^>]+>$/; $1 }.compact 42 | assert_equal 1, authors.select { |author| author == name}.size 43 | end 44 | 45 | Then /^the gitconfig should include "([^\"]*)" as the email of "([^\"]*)"$/ do |email, name| 46 | output = git_config 47 | authors = output.split("\n").map { |line| line =~ /^git-pair\.authors=.* <([^>]+)>$/; $1 }.compact 48 | assert_equal 1, authors.select { |author| author == email}.size 49 | end 50 | 51 | Then /^`git pair` should display the following author list:$/ do |table| 52 | output = git_pair 53 | names = authors_list_from_output(output).map { |name| {"name" => name} } 54 | table.diff! names 55 | end 56 | 57 | Then /^`git pair` should display an empty author list$/ do 58 | output = git_pair 59 | assert authors_list_from_output(output).empty? 60 | end 61 | 62 | Then /^the last command's output should include "([^\"]*)"$/ do |output| 63 | assert @output.include?(output) 64 | end 65 | 66 | def authors_list_from_output(output) 67 | output =~ /Author list: (.*?)\n\s?\n/im 68 | $1.strip.split("\n").map { |name| name.strip } 69 | end 70 | 71 | def current_author_from_output(output) 72 | output =~ /Current author: (.*?)\n/im 73 | $1.strip 74 | end 75 | 76 | def current_email_from_output(output) 77 | output =~ /Current email: (.*?)\n/im 78 | $1.strip 79 | end 80 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'tmpdir' 2 | require 'test/unit/assertions' 3 | World(Test::Unit::Assertions) 4 | 5 | 6 | module RepositoryHelper 7 | # TODO: use 1.8.7's Dir.mktmpdir? 8 | TEST_REPO_PATH = File.join(Dir::tmpdir, "git-pair-test-repo") 9 | TEST_REPO_DOT_GIT_PATH = "#{TEST_REPO_PATH}/.git" 10 | 11 | PROJECT_PATH = File.join(File.dirname(__FILE__), "../..") 12 | GIT_PAIR = "#{PROJECT_PATH}/bin/git-pair" 13 | CONFIG_BACKUP_PATH = "#{PROJECT_PATH}/tmp" 14 | 15 | def git_pair(options = "") 16 | output = `GIT_DIR=#{TEST_REPO_DOT_GIT_PATH} && #{GIT_PAIR} #{options} 2>&1` 17 | output.gsub(/\e\[\d\d?m/, '') # strip any ANSI colors 18 | end 19 | 20 | def git_config 21 | `GIT_DIR=#{TEST_REPO_DOT_GIT_PATH} && git config --list 2>&1` 22 | end 23 | 24 | def backup_gitconfigs 25 | FileUtils.mkdir_p CONFIG_BACKUP_PATH 26 | FileUtils.cp File.expand_path("~/.gitconfig"), "#{CONFIG_BACKUP_PATH}/.gitconfig.backup" 27 | FileUtils.cp "#{PROJECT_PATH}/.git/config", "#{CONFIG_BACKUP_PATH}/config.backup" 28 | end 29 | 30 | def restore_gitconfigs 31 | FileUtils.cp "#{CONFIG_BACKUP_PATH}/config.backup", "#{PROJECT_PATH}/.git/config" 32 | FileUtils.cp "#{CONFIG_BACKUP_PATH}/.gitconfig.backup", File.expand_path("~/.gitconfig") 33 | FileUtils.rm_rf CONFIG_BACKUP_PATH 34 | end 35 | end 36 | 37 | World(RepositoryHelper) 38 | 39 | 40 | Before do 41 | backup_gitconfigs 42 | FileUtils.mkdir_p RepositoryHelper::TEST_REPO_PATH 43 | `GIT_DIR=#{RepositoryHelper::TEST_REPO_DOT_GIT_PATH} && git init` 44 | end 45 | 46 | After do 47 | FileUtils.rm_rf RepositoryHelper::TEST_REPO_PATH 48 | restore_gitconfigs 49 | end 50 | -------------------------------------------------------------------------------- /features/switching_authors.feature: -------------------------------------------------------------------------------- 1 | Feature: Switching authors 2 | In order to indicate which authors are committing 3 | A user should be able to 4 | change the currently active pair 5 | 6 | Scenario: No authors have been added 7 | When I try to switch to the pair "AA BB" 8 | Then the last command's output should include "Please add some authors first" 9 | 10 | Scenario: Two authors with similar emails 11 | Given I have added the author "Linus Torvalds " 12 | And I have added the author "Junio C Hamano " 13 | When I switch to the pair "LT JH" 14 | Then `git pair` should display "Junio C Hamano + Linus Torvalds" for the current author 15 | And `git pair` should display "tbd+junio+linus@example.org" for the current email 16 | -------------------------------------------------------------------------------- /git-pair.gemspec: -------------------------------------------------------------------------------- 1 | # Generated by jeweler 2 | # DO NOT EDIT THIS FILE DIRECTLY 3 | # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command 4 | # -*- encoding: utf-8 -*- 5 | 6 | Gem::Specification.new do |s| 7 | s.name = %q{git-pair} 8 | s.version = "0.1.0" 9 | 10 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 11 | s.authors = ["Chris Kampmeier"] 12 | s.date = %q{2009-12-07} 13 | s.default_executable = %q{git-pair} 14 | s.description = %q{A git porcelain for pair programming. Changes git-config's user.name and user.email settings so you can commit as more than one author.} 15 | s.email = %q{chris@kampers.net} 16 | s.executables = ["git-pair"] 17 | s.extra_rdoc_files = [ 18 | "LICENSE", 19 | "README.markdown" 20 | ] 21 | s.files = [ 22 | ".gitignore", 23 | "LICENSE", 24 | "README.markdown", 25 | "Rakefile", 26 | "bin/git-pair", 27 | "features/git-pair.feature", 28 | "features/step_definitions/git-pair_steps.rb", 29 | "features/support/env.rb", 30 | "git-pair.gemspec", 31 | "lib/git-pair.rb", 32 | "lib/git-pair/VERSION" 33 | ] 34 | s.homepage = %q{http://github.com/chrisk/git-pair} 35 | s.rdoc_options = ["--charset=UTF-8"] 36 | s.require_paths = ["lib"] 37 | s.rubygems_version = %q{1.3.5} 38 | s.summary = %q{Configure git to commit as more than one author} 39 | 40 | if s.respond_to? :specification_version then 41 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 42 | s.specification_version = 3 43 | 44 | if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then 45 | s.add_development_dependency(%q, [">= 0"]) 46 | else 47 | s.add_dependency(%q, [">= 0"]) 48 | end 49 | else 50 | s.add_dependency(%q, [">= 0"]) 51 | end 52 | end 53 | 54 | -------------------------------------------------------------------------------- /lib/git-pair.rb: -------------------------------------------------------------------------------- 1 | C_BOLD, C_REVERSE, C_RED, C_RESET = "\e[1m", "\e[7m", "\e[91m", "\e[0m" 2 | 3 | module GitPair 4 | 5 | VERSION = File.read(File.join(File.dirname(__FILE__), "git-pair", "VERSION")).strip 6 | 7 | C_BOLD, C_REVERSE, C_RED, C_RESET = "\e[1m", "\e[7m", "\e[91m", "\e[0m" 8 | 9 | 10 | class NoMatchingAuthorsError < ArgumentError; end 11 | class MissingConfigurationError < RuntimeError; end 12 | 13 | 14 | module Commands 15 | def add(author_string) 16 | @config_changed = true 17 | authors = Helpers.author_strings_with_new(author_string) 18 | remove_all 19 | authors.each do |name_and_email| 20 | `git config --add git-pair.authors "#{name_and_email}"` 21 | end 22 | end 23 | 24 | def remove(name) 25 | @config_changed = true 26 | `git config --unset-all git-pair.authors "^#{name} <"` 27 | end 28 | 29 | def remove_all 30 | @config_changed = true 31 | `git config --unset-all git-pair.authors` 32 | end 33 | 34 | def config_change_made? 35 | @config_changed 36 | end 37 | 38 | def switch(abbreviations) 39 | raise MissingConfigurationError, "Please add some authors first" if Helpers.author_names.empty? 40 | 41 | names = abbreviations.map { |abbrev| 42 | name = Helpers.author_name_from_abbreviation(abbrev) 43 | raise NoMatchingAuthorsError, "no authors matched #{abbrev}" if name.nil? 44 | name 45 | } 46 | 47 | sorted_names = names.uniq.sort_by { |name| name.split.last } 48 | `git config user.name "#{sorted_names.join(' + ')}"` 49 | 50 | puts "Switching to #{sorted_names.join(' + ')}." 51 | puts "Please enter a unique email address for this pair." 52 | puts "Something like ____ is suggested." 53 | print "Email: " 54 | email = gets 55 | `git config user.email "#{email}"` 56 | end 57 | 58 | extend self 59 | end 60 | 61 | 62 | module Helpers 63 | def display_string_for_config 64 | "#{C_BOLD} Author list: #{C_RESET}" + author_names.join("\n ") 65 | end 66 | 67 | def display_string_for_current_info 68 | "#{C_BOLD} Current author: #{C_RESET}" + current_author + "\n" + 69 | "#{C_BOLD} Current email: #{C_RESET}" + current_email + "\n " 70 | end 71 | 72 | def author_strings 73 | `git config --get-all git-pair.authors`.split("\n") 74 | end 75 | 76 | def author_strings_with_new(author_string) 77 | strings = author_strings.push(author_string) 78 | 79 | strings.reject! { |str| 80 | !strings.one? { |s| parse_author_string(s).first == parse_author_string(str).first } 81 | } 82 | strings.push(author_string) if !strings.include?(author_string) 83 | strings.sort_by { |str| parse_author_string(str).first } 84 | end 85 | 86 | def author_names 87 | author_strings.map { |line| parse_author_string(line).first }.sort_by { |name| name.split.last } 88 | end 89 | 90 | def email(*initials_list) 91 | initials_string = initials_list.map { |initials| "+#{initials}" }.join 92 | 'dev@example.com'.sub("@", "#{initials_string}@") 93 | end 94 | 95 | def current_author 96 | `git config --get user.name`.strip 97 | end 98 | 99 | def current_email 100 | `git config --get user.email`.strip 101 | end 102 | 103 | def author_name_from_abbreviation(abbrev) 104 | # initials 105 | author_names.each do |name| 106 | return name if abbrev.downcase == name.split.map { |word| word[0].chr }.join.downcase 107 | end 108 | 109 | # start of a name 110 | author_names.each do |name| 111 | return name if name.gsub(" ", "") =~ /^#{abbrev}/i 112 | end 113 | 114 | # includes the letters in order 115 | author_names.detect do |name| 116 | name =~ /#{abbrev.split("").join(".*")}/i 117 | end 118 | end 119 | 120 | def parse_author_string(author_string) 121 | author_string =~ /^(.+)\s+<([^>]+)>$/ 122 | [$1, $2] 123 | end 124 | 125 | def abort(error_message, extra = "") 126 | super "#{C_RED}#{C_REVERSE} Error: #{error_message} #{C_RESET}\n" + extra 127 | end 128 | 129 | extend self 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/git-pair/VERSION: -------------------------------------------------------------------------------- 1 | 0.1.0 2 | --------------------------------------------------------------------------------