This commit is contained in:
Tom Preston-Werner 2009-02-28 15:14:10 -08:00
Родитель 85fea86e27
Коммит 6bb41e4c10
5 изменённых файлов: 873 добавлений и 222 удалений

Просмотреть файл

@ -1,3 +1,7 @@
==
* Minor Enhancements
* Convert readme to markdown
== 1.0.3 / 2009-02-13
* Minor Enhancements
* Added Grit::Commit#to_patch for plaintext formatted patches.

22
LICENSE Normal file
Просмотреть файл

@ -0,0 +1,22 @@
(The MIT License)
Copyright (c) 2007-2009 Tom Preston-Werner
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

210
README.md Normal file
Просмотреть файл

@ -0,0 +1,210 @@
Grit
====
Grit gives you object oriented read/write access to Git repositories via Ruby.
The main goals are stability and performance. To this end, some of the
interactions with Git repositories are done by shelling out to the system's
`git` command, and other interactions are done with pure Ruby
reimplementations of core Git functionality. This choice, however, is
transparent to end users, and you need not know which method is being used.
This software was developed to power GitHub, and should be considered
production ready. An extensive test suite is provided to verify its correctness.
Grit is maintained by Tom Preston-Werner, Scott Chacon, Chris Wanstrath, and
PJ Hyett.
This documentation is accurate as of Grit 1.0.2.
## Requirements #############################################################
* git (http://git-scm.com) tested with 1.6.0.2
## Install ##################################################################
Easiest install is via RubyGems:
$ gem install grit
or
$ gem sources -a http://gems.github.com/ (you only need to do this once)
$ gem install mojombo-grit
The gem from GitHub will generally be available sooner than the gem from
Rubyforge. Both sources will eventually contain the same releases.
## Source ###################################################################
Grit's Git repo is available on GitHub, which can be browsed at:
http://github.com/mojombo/grit
and cloned with:
git clone git://github.com/mojombo/grit.git
## Usage ####################################################################
Grit gives you object model access to your Git repositories. Once you have
created a `Repo` object, you can traverse it to find parent commits,
trees, blobs, etc.
### Initialize a Repo object
The first step is to create a `Grit::Repo` object to represent your repo. In
this documentation I include the `Grit` module to reduce typing.
require 'grit'
include Grit
repo = Repo.new("/Users/tom/dev/grit")
In the above example, the directory `/Users/tom/dev/grit` is my working
directory and contains the `.git` directory. You can also initialize Grit with
a bare repo.
repo = Repo.new("/var/git/grit.git")
### Getting a list of commits
From the `Repo` object, you can get a list of commits as an array of `Commit`
objects.
repo.commits
# => [#<Grit::Commit "e80bbd2ce67651aa18e57fb0b43618ad4baf7750">,
#<Grit::Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">,
#<Grit::Commit "038af8c329ef7c1bae4568b98bd5c58510465493">,
#<Grit::Commit "40d3057d09a7a4d61059bca9dca5ae698de58cbe">,
#<Grit::Commit "4ea50f4754937bf19461af58ce3b3d24c77311d9">]
Called without arguments, `Repo#commits` returns a list of up to ten commits
reachable by the **master** branch (starting at the latest commit). You can
ask for commits beginning at a different branch, commit, tag, etc.
repo.commits('mybranch')
repo.commits('40d3057d09a7a4d61059bca9dca5ae698de58cbe')
repo.commits('v0.1')
You can specify the maximum number of commits to return.
repo.commits('master', 100)
If you need paging, you can specify a number of commits to skip.
repo.commits('master', 10, 20)
The above will return commits 21-30 from the commit list.
### The Commit object
`Commit` objects contain information about that commit.
head = repo.commits.first
head.id
# => "e80bbd2ce67651aa18e57fb0b43618ad4baf7750"
head.parents
# => [#<Grit::Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">]
head.tree
# => #<Grit::Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
head.author
# => #<Grit::Actor "Tom Preston-Werner <tom@mojombo.com>">
head.authored_date
# => Wed Oct 24 22:02:31 -0700 2007
head.committer
# => #<Grit::Actor "Tom Preston-Werner <tom@mojombo.com>">
head.committed_date
# => Wed Oct 24 22:02:31 -0700 2007
head.message
# => "add Actor inspect"
You can traverse a commit's ancestry by chaining calls to `#parents`.
repo.commits.first.parents[0].parents[0].parents[0]
The above corresponds to **master^^^** or **master~3** in Git parlance.
### The Tree object
A tree records pointers to the contents of a directory. Let's say you want
the root tree of the latest commit on the **master** branch.
tree = repo.commits.first.tree
# => #<Grit::Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
tree.id
# => "3536eb9abac69c3e4db583ad38f3d30f8db4771f"
Once you have a tree, you can get the contents.
contents = tree.contents
# => [#<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">,
#<Grit::Blob "81d2c27608b352814cbe979a6acd678d30219678">,
#<Grit::Tree "c3d07b0083f01a6e1ac969a0f32b8d06f20c62e5">,
#<Grit::Tree "4d00fe177a8407dbbc64a24dbfc564762c0922d8">]
This tree contains two `Blob` objects and two `Tree` objects. The trees are
subdirectories and the blobs are files. Trees below the root have additional
attributes.
contents.last.name
# => "lib"
contents.last.mode
# => "040000"
There is a convenience method that allows you to get a named sub-object
from a tree.
tree / "lib"
# => #<Grit::Tree "e74893a3d8a25cbb1367cf241cc741bfd503c4b2">
You can also get a tree directly from the repo if you know its name.
repo.tree
# => #<Grit::Tree "master">
repo.tree("91169e1f5fa4de2eaea3f176461f5dc784796769")
# => #<Grit::Tree "91169e1f5fa4de2eaea3f176461f5dc784796769">
### The Blob object
A blob represents a file. Trees often contain blobs.
blob = tree.contents.first
# => #<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">
A blob has certain attributes.
blob.id
# => "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666"
blob.name
# => "README.txt"
blob.mode
# => "100644"
blob.size
# => 7726
You can get the data of a blob as a string.
blob.data
# => "Grit is a library to ..."
You can also get a blob directly from the repo if you know its name.
repo.blob("4ebc8aea50e0a67e000ba29a30809d0a7b9b2666")
# => #<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">

Просмотреть файл

@ -1,222 +0,0 @@
grit
by Tom Preston-Werner, Scott Chacon
http://github.com/mojombo/grit
== DESCRIPTION:
Grit is a Ruby library for extracting information from a git repository in an
object oriented manner.
== REQUIREMENTS:
* git (http://git-scm.com) tested with 1.6.0.2
== INSTALL:
Easiest install is via RubyGems:
$ gem install grit
or
$ gem sources -a http://gems.github.com/ (you only need to do this once)
$ gem install mojombo-grit
The gem from GitHub will generally be available sooner than the gem from
Rubyforge. Both sources will eventually contain the same releases.
== SOURCE:
Grit's git repo is available on GitHub, which can be browsed at:
http://github.com/mojombo/grit
and cloned from:
git://github.com/mojombo/grit.git
== USAGE:
Grit gives you object model access to your git repository. Once you have
created a repository object, you can traverse it to find parent commit(s),
trees, blobs, etc.
= Initialize a Repo object
The first step is to create a Grit::Repo object to represent your repo. I
include the Grit module so reduce typing.
require 'grit'
include Grit
repo = Repo.new("/Users/tom/dev/grit")
In the above example, the directory /Users/tom/dev/grit is my working
repo and contains the .git directory. You can also initialize Grit with a
bare repo.
repo = Repo.new("/var/git/grit.git")
= Getting a list of commits
From the Repo object, you can get a list of commits as an array of Commit
objects.
repo.commits
# => [#<Grit::Commit "e80bbd2ce67651aa18e57fb0b43618ad4baf7750">,
#<Grit::Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">,
#<Grit::Commit "038af8c329ef7c1bae4568b98bd5c58510465493">,
#<Grit::Commit "40d3057d09a7a4d61059bca9dca5ae698de58cbe">,
#<Grit::Commit "4ea50f4754937bf19461af58ce3b3d24c77311d9">]
Called without arguments, Repo#commits returns a list of up to ten commits
reachable by the master branch (starting at the latest commit). You can ask
for commits beginning at a different branch, commit, tag, etc.
repo.commits('mybranch')
repo.commits('40d3057d09a7a4d61059bca9dca5ae698de58cbe')
repo.commits('v0.1')
You can specify the maximum number of commits to return.
repo.commits('master', 100)
If you need paging, you can specify a number of commits to skip.
repo.commits('master', 10, 20)
The above will return commits 21-30 from the commit list.
= The Commit object
Commit objects contain information about that commit.
head = repo.commits.first
head.id
# => "e80bbd2ce67651aa18e57fb0b43618ad4baf7750"
head.parents
# => [#<Grit::Commit "91169e1f5fa4de2eaea3f176461f5dc784796769">]
head.tree
# => #<Grit::Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
head.author
# => #<Grit::Actor "Tom Preston-Werner <tom@mojombo.com>">
head.authored_date
# => Wed Oct 24 22:02:31 -0700 2007
head.committer
# => #<Grit::Actor "Tom Preston-Werner <tom@mojombo.com>">
head.committed_date
# => Wed Oct 24 22:02:31 -0700 2007
head.message
# => "add Actor inspect"
You can traverse a commit's ancestry by chaining calls to #parents.
repo.commits.first.parents[0].parents[0].parents[0]
The above corresponds to master^^^ or master~3 in git parlance.
= The Tree object
A tree records pointers to the contents of a directory. Let's say you want
the root tree of the latest commit on the master branch.
tree = repo.commits.first.tree
# => #<Grit::Tree "3536eb9abac69c3e4db583ad38f3d30f8db4771f">
tree.id
# => "3536eb9abac69c3e4db583ad38f3d30f8db4771f"
Once you have a tree, you can get the contents.
contents = tree.contents
# => [#<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">,
#<Grit::Blob "81d2c27608b352814cbe979a6acd678d30219678">,
#<Grit::Tree "c3d07b0083f01a6e1ac969a0f32b8d06f20c62e5">,
#<Grit::Tree "4d00fe177a8407dbbc64a24dbfc564762c0922d8">]
This tree contains two Blob objects and two Tree objects. The trees are
subdirectories and the blobs are files. Trees below the root have additional
attributes.
contents.last.name
# => "lib"
contents.last.mode
# => "040000"
There is a convenience method that allows you to get a named sub-object
from a tree.
tree/"lib"
# => #<Grit::Tree "e74893a3d8a25cbb1367cf241cc741bfd503c4b2">
You can also get a tree directly from the repo if you know its name.
repo.tree
# => #<Grit::Tree "master">
repo.tree("91169e1f5fa4de2eaea3f176461f5dc784796769")
# => #<Grit::Tree "91169e1f5fa4de2eaea3f176461f5dc784796769">
= The Blob object
A blob represents a file. Trees often contain blobs.
blob = tree.contents.first
# => #<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">
A blob has certain attributes.
blob.id
# => "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666"
blob.name
# => "README.txt"
blob.mode
# => "100644"
blob.size
# => 7726
You can get the data of a blob as a string.
blob.data
# => "Grit is a library to ..."
You can also get a blob directly from the repo if you know its name.
repo.blob("4ebc8aea50e0a67e000ba29a30809d0a7b9b2666")
# => #<Grit::Blob "4ebc8aea50e0a67e000ba29a30809d0a7b9b2666">
== LICENSE:
(The MIT License)
Copyright (c) 2007 Tom Preston-Werner
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

637
test/fixtures/show_cc поставляемый Normal file
Просмотреть файл

@ -0,0 +1,637 @@
commit f8dd1f0b48e1106a62b47cc2927609ca589dc39a
tree 4c23a5137714e62c52f22e99b3104122868400ab
parent a0710955e70cbceef8cf805645a447f1b370b966
parent b724247612be9f55df7914cb86b64703810d7b73
author administrator <gmalamid@thoughtworks.com> 1224499981 +0100
committer administrator <gmalamid@thoughtworks.com> 1224499981 +0100
imported William Morgans utilities too, which are very very nice
diff --cc git-publish-branch
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..b28b4fde621dff1c72bcb570183bbe923e4a24b2
new file mode 100755
--- /dev/null
+++ b/git-publish-branch
@@@ -1,0 -1,0 +1,70 @@@
++#!/usr/bin/env ruby
++
++## git-publish-branch: a simple script to ease the unnecessarily complex
++## task of "publishing" a branch, i.e., taking a local branch, creating a
++## reference to it on a remote repo, and setting up the local branch to
++## track the remote one, all in one go. you can even delete that remote
++## reference.
++##
++## Usage: git publish-branch [-d] <branch> [repository]
++##
++## '-d' signifies deletion. <branch> is the branch to publish, and
++## [repository] defaults to "origin". The remote branch name will be the
++## same as the local branch name. Don't make life unnecessarily complex
++## for yourself.
++##
++## Note that unpublishing a branch doesn't delete the local branch.
++## Safety first!
++##
++## git-publish-branch Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
++## This program is free software: you can redistribute it and/or modify
++## it under the terms of the GNU General Public License as published by
++## the Free Software Foundation, either version 3 of the License, or (at
++## your option) any later version.
++##
++## This program is distributed in the hope that it will be useful,
++## but WITHOUT ANY WARRANTY; without even the implied warranty of
++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++## GNU General Public License for more details.
++##
++## You can find the GNU General Public License at:
++## http://www.gnu.org/licenses/
++
++def exec cmd
++ puts cmd
++ system cmd or die unless $fake
++end
++
++def die s=nil
++ $stderr.puts s if s
++ exit(-1)
++end
++
++head = `git symbolic-ref HEAD`.chomp.gsub(/refs\/heads\//, "")
++delete = ARGV.delete "-d"
++$fake = ARGV.delete "-n"
++branch = (ARGV.shift || head).gsub(/refs\/heads\//, "")
++remote = ARGV.shift || "origin"
++local_ref = `git show-ref heads/#{branch}`
++remote_ref = `git show-ref remotes/#{remote}/#{branch}`
++remote_config = `git config branch.#{branch}.merge`
++
++if delete
++ ## we don't do any checking here because the remote branch might actually
++ ## exist, whether we actually know about it or not.
++ exec "git push #{remote} :refs/heads/#{branch}"
++
++ unless local_ref.empty?
++ exec "git config --unset branch.#{branch}.remote"
++ exec "git config --unset branch.#{branch}.merge"
++ end
++else
++ die "No local branch #{branch} exists!" if local_ref.empty?
++ die "A remote branch #{branch} on #{remote} already exists!" unless remote_ref.empty?
++ die "Local branch #{branch} is already a tracking branch!" unless remote_config.empty?
++
++ exec "git push #{remote} #{branch}:refs/heads/#{branch}"
++ exec "git config branch.#{branch}.remote #{remote}"
++ exec "git config branch.#{branch}.merge refs/heads/#{branch}"
++end
++
diff --cc git-rank-contributors
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..3b272206d185e2f9b8089e6aaa00fde6acc308ef
new file mode 100755
--- /dev/null
+++ b/git-rank-contributors
@@@ -1,0 -1,0 +1,60 @@@
++#!/usr/bin/env ruby
++
++## git-rank-contributors: a simple script to trace through the logs and
++## rank contributors by the total size of the diffs they're responsible for.
++## A change counts twice as much as a plain addition or deletion.
++##
++## Output may or may not be suitable for inclusion in a CREDITS file.
++## Probably not without some editing, because people often commit from more
++## than one address.
++##
++## git-rank-contributors Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
++## This program is free software: you can redistribute it and/or modify
++## it under the terms of the GNU General Public License as published by
++## the Free Software Foundation, either version 3 of the License, or (at
++## your option) any later version.
++##
++## This program is distributed in the hope that it will be useful,
++## but WITHOUT ANY WARRANTY; without even the implied warranty of
++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++## GNU General Public License for more details.
++##
++## You can find the GNU General Public License at:
++## http://www.gnu.org/licenses/
++
++class String
++ def obfuscate; gsub(/@/, " at the ").gsub(/\.(\w+)(>|$)/, ' dot \1s\2') end
++ def htmlize; gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
++end
++
++lines = {}
++verbose = ARGV.delete("-v")
++obfuscate = ARGV.delete("-o")
++htmlize = ARGV.delete("-h")
++
++author = nil
++state = :pre_author
++`git log -M -C -C -p --no-color`.each do |l|
++ case
++ when (state == :pre_author || state == :post_author) && l =~ /Author: (.*)$/
++ author = $1
++ state = :post_author
++ lines[author] ||= 0
++ when state == :post_author && l =~ /^\+\+\+/
++ state = :in_diff
++ when state == :in_diff && l =~ /^[\+\-]/
++ lines[author] += 1
++ when state == :in_diff && l =~ /^commit /
++ state = :pre_author
++ end
++end
++
++lines.sort_by { |a, c| -c }.each do |a, c|
++ a = a.obfuscate if obfuscate
++ a = a.htmlize if htmlize
++ if verbose
++ puts "#{a}: #{c} lines of diff"
++ else
++ puts a
++ end
++end
diff --cc git-show-merges
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..12907502a6c6885e7183a0e71d9cfeed77917824
new file mode 100755
--- /dev/null
+++ b/git-show-merges
@@@ -1,0 -1,0 +1,49 @@@
++#!/usr/bin/env ruby
++
++## git-show-merges: a simple script to show you which topic branches have
++## been merged into the current branch, and which haven't. (Or, specify
++## the set of merge branches you're interested in on the command line.)
++##
++## git-show-merges Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
++## This program is free software: you can redistribute it and/or modify
++## it under the terms of the GNU General Public License as published by
++## the Free Software Foundation, either version 3 of the License, or (at
++## your option) any later version.
++##
++## This program is distributed in the hope that it will be useful,
++## but WITHOUT ANY WARRANTY; without even the implied warranty of
++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++## GNU General Public License for more details.
++##
++## You can find the GNU General Public License at:
++## http://www.gnu.org/licenses/
++heads = if ARGV.empty?
++ [`git symbolic-ref HEAD`.chomp]
++else
++ ARGV
++end.map { |r| r.gsub(/refs\/heads\//, "") }
++
++branches = `git show-ref --heads`.
++ scan(/^\S+ refs\/heads\/(\S+)$/).
++ map { |a| a.first }
++
++unknown = heads - branches
++unless unknown.empty?
++ $stderr.puts "Unknown branch: #{unknown.first}"
++ exit(-1)
++end
++
++branches -= heads
++
++heads.each do |h|
++ merged = branches.select { |b| `git log #{h}..#{b}` == "" }
++ unmerged = branches - merged
++
++ puts "merged into #{h}:"
++ merged.each { |b| puts " #{b}" }
++ puts
++ puts "not merged into #{h}: "
++ unmerged.each { |b| puts " #{b}" }
++
++ puts
++end
diff --cc git-wt-add
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..3125ab5495e5f2ab55ed48af5303031fae02c2ac
new file mode 100755
--- /dev/null
+++ b/git-wt-add
@@@ -1,0 -1,0 +1,196 @@@
++#!/usr/bin/env ruby
++
++## git-wt-add: A darcs-style interactive staging script for git. As the
++## name implies, git-wt-add walks you through unstaged changes on a
++## hunk-by-hunk basis and allows you to pick the ones you'd like staged.
++##
++## git-wt-add Copyright 2007 William Morgan <wmorgan-git-wt-add@masanjin.net>.
++## This program is free software: you can redistribute it and/or modify
++## it under the terms of the GNU General Public License as published by
++## the Free Software Foundation, either version 3 of the License, or
++## (at your option) any later version.
++##
++## This program is distributed in the hope that it will be useful,
++## but WITHOUT ANY WARRANTY; without even the implied warranty of
++## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
++## GNU General Public License for more details.
++##
++## You can find the GNU General Public License at:
++## http://www.gnu.org/licenses/
++
++COLOR = /\e\[\d*m/
++
++class Hunk
++ attr_reader :file, :file_header, :diff
++ attr_accessor :disposition
++
++ def initialize file, file_header, diff
++ @file = file
++ @file_header = file_header
++ @diff = diff
++ @disposition = :unknown
++ end
++
++ def self.make_from diff
++ ret = []
++ state = :outside
++ file_header = hunk = file = nil
++
++ diff.each do |l| # a little state machine to parse git diff output
++ reprocess = false
++ begin
++ reprocess = false
++ case
++ when state == :outside && l =~ /^(#{COLOR})*diff --git a\/(.+) b\/(\2)/
++ file = $2
++ file_header = ""
++ when state == :outside && l =~ /^(#{COLOR})*index /
++ when state == :outside && l =~ /^(#{COLOR})*(---|\+\+\+) /
++ file_header += l + "\n"
++ when state == :outside && l =~ /^(#{COLOR})*@@ /
++ state = :in_hunk
++ hunk = l + "\n"
++ when state == :in_hunk && l =~ /^(#{COLOR})*(@@ |diff --git a)/
++ ret << Hunk.new(file, file_header, hunk)
++ state = :outside
++ reprocess = true
++ when state == :in_hunk
++ hunk += l + "\n"
++ else
++ raise "unparsable diff input: #{l.inspect}"
++ end
++ end while reprocess
++ end
++
++ ## add the final hunk
++ ret << Hunk.new(file, file_header, hunk) if hunk
++
++ ret
++ end
++end
++
++def help
++ puts <<EOS
++y: record this patch
++n: don't record it
++w: wait and decide later, defaulting to no
++
++s: don't record the rest of the changes to this file
++f: record the rest of the changes to this file
++
++d: record selected patches, skipping all the remaining patches
++a: record all the remaining patches
++q: cancel record
++
++j: skip to next patch
++k: back up to previous patch
++c: calculate number of patches
++h or ?: show this help
++
++<Space>: accept the current default (which is capitalized)
++EOS
++end
++
++def walk_through hunks
++ skip_files, record_files = {}, {}
++ skip_rest = record_rest = false
++
++ while hunks.any? { |h| h.disposition == :unknown }
++ pos = 0
++ until pos >= hunks.length
++ h = hunks[pos]
++ if h.disposition != :unknown
++ pos += 1
++ next
++ elsif skip_rest || skip_files[h.file]
++ h.disposition = :ignore
++ pos += 1
++ next
++ elsif record_rest || record_files[h.file]
++ h.disposition = :record
++ pos += 1
++ next
++ end
++
++ puts "Hunk from #{h.file}"
++ puts h.diff
++ print "Shall I stage this change? (#{pos + 1}/#{hunks.length}) [ynWsfqadk], or ? for help: "
++ c = $stdin.getc
++ puts
++ case c
++ when ?y: h.disposition = :record
++ when ?n: h.disposition = :ignore
++ when ?w, ?\ : h.disposition = :unknown
++ when ?s
++ h.disposition = :ignore
++ skip_files[h.file] = true
++ when ?f
++ h.disposition = :record
++ record_files[h.file] = true
++ when ?d: skip_rest = true
++ when ?a: record_rest = true
++ when ?q: exit
++ when ?k
++ if pos > 0
++ hunks[pos - 1].disposition = :unknown
++ pos -= 2 # double-bah
++ end
++ else
++ help
++ pos -= 1 # bah
++ end
++
++ pos += 1
++ puts
++ end
++ end
++end
++
++def make_patch hunks
++ patch = ""
++ did_header = {}
++ hunks.each do |h|
++ next unless h.disposition == :record
++ unless did_header[h.file]
++ patch += h.file_header
++ did_header[h.file] = true
++ end
++ patch += h.diff
++ end
++
++ patch.gsub COLOR, ""
++end
++
++### execution starts here ###
++
++diff = `git diff`.split(/\r?\n/)
++if diff.empty?
++ puts "No unstaged changes."
++ exit
++end
++hunks = Hunk.make_from diff
++
++## unix-centric!
++state = `stty -g`
++begin
++ `stty -icanon` # immediate keypress mode
++ walk_through hunks
++ensure
++ `stty #{state}`
++end
++
++patch = make_patch hunks
++if patch.empty?
++ puts "No changes selected for staging."
++else
++ IO.popen("git apply --cached", "w") { |f| f.puts patch }
++ puts <<EOS
++Staged patch of #{patch.split("\n").size} lines.
++
++Possible next commands:
++ git diff --cached: see staged changes
++ git commit: commit staged changes
++ git reset: unstage changes
++EOS
++end
++
diff --cc git-wtf
index 0000000000000000000000000000000000000000,0000000000000000000000000000000000000000..ea4b27f6812cf9daf84b047bff4443cc554f92bf
new file mode 100755
--- /dev/null
+++ b/git-wtf
@@@ -1,0 -1,0 +1,223 @@@
++#!/usr/bin/env ruby
++
++## git-wtf: display the state of your repository in a readable and easy-to-scan
++## format.
++##
++## git-wtf tries to ease the task of having many git branches. It's also useful
++## for getting a summary of how tracking branches relate to a remote server.
++##
++## git-wtf shows you:
++## - How your branch relates to the remote repo, if it's a tracking branch.
++## - How your branch relates to non-feature ("version") branches, if it's a
++## feature branch.
++## - How your branch relates to the feature branches, if it's a version branch.
++##
++## For each of these relationships, git-wtf displays the commits pending on
++## either side, if any. It displays checkboxes along the side for easy scanning
++## of merged/non-merged branches.
++##
++## If you're working against a remote repo, git-wtf is best used between a 'git
++## fetch' and a 'git merge' (or 'git pull' if you don't mind the redundant
++## network access).
++##
++## Usage: git wtf [branch+] [-l|--long] [-a|--all] [--dump-config]
++##
++## If [branch] is not specified, git-wtf will use the current branch. With
++## --long, you'll see author info and date for each commit. With --all, you'll
++## see all commits, not just the first 5. With --dump-config, git-wtf will
++## print out its current configuration in YAML format and exit.
++##
++## git-wtf uses some heuristics to determine which branches are version
++## branches, and which are feature branches. (Specifically, it assumes the
++## version branches are named "master", "next" and "edge".) If it guesses
++## incorrectly, you will have to create a .git-wtfrc file.
++##
++## git-wtf looks for a .git-wtfrc file starting in the current directory, and
++## recursively up to the root. The config file is a YAML file that specifies
++## the version branches, any branches to ignore, and the max number of commits
++## to display when --all isn't used. To start building a configuration file,
++## run "git-wtf --dump-config > .git-wtfrc" and edit it.
++##
++## IMPORTANT NOTE: all local branches referenced in .git-wtfrc must be prefixed
++## with heads/, e.g. "heads/master". Remote branches must be of the form
++## remotes/<remote>/<branch>.
++##
++## git-wtf Copyright 2008 William Morgan <wmorgan-git-wt-add@masanjin.net>.
++## This program is free software: you can redistribute it and/or modify it
++## under the terms of the GNU General Public License as published by the Free
++## Software Foundation, either version 3 of the License, or (at your option)
++## any later version.
++##
++## This program is distributed in the hope that it will be useful, but WITHOUT
++## ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
++## FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
++## more details.
++##
++## You can find the GNU General Public License at: http://www.gnu.org/licenses/
++
++
++require 'yaml'
++CONFIG_FN = ".git-wtfrc"
++
++class Numeric; def pluralize s; "#{to_s} #{s}" + (self != 1 ? "s" : "") end end
++
++$long = ARGV.delete("--long") || ARGV.delete("-l")
++$all = ARGV.delete("--all") || ARGV.delete("-a")
++$dump_config = ARGV.delete("--dump-config")
++
++## find config file
++$config = { "versions" => %w(heads/master heads/next heads/edge), "ignore" => [], "max_commits" => 5 }.merge begin
++ p = File.expand_path "."
++ fn = while true
++ fn = File.join p, CONFIG_FN
++ break fn if File.exist? fn
++ pp = File.expand_path File.join(p, "..")
++ break if p == pp
++ p = pp
++ end
++
++ (fn && YAML::load_file(fn)) || {} # YAML turns empty files into false
++end
++
++if $dump_config
++ puts $config.to_yaml
++ exit(0)
++end
++
++## the set of commits in 'to' that aren't in 'from'.
++## if empty, 'to' has been merged into 'from'.
++def commits_between from, to
++ if $long
++ `git log --pretty=format:"- %s [%h] (%ae; %ar)" #{from}..#{to}`
++ else
++ `git log --pretty=format:"- %s [%h]" #{from}..#{to}`
++ end.split(/[\r\n]+/)
++end
++
++def show_commits commits, prefix=" "
++ if commits.empty?
++ puts "#{prefix} none"
++ else
++ max = $all ? commits.size : $config["max_commits"]
++ max -= 1 if max == commits.size - 1 # never show "and 1 more"
++ commits[0 ... max].each { |c| puts "#{prefix}#{c}" }
++ puts "#{prefix}... and #{commits.size - max} more." if commits.size > max
++ end
++end
++
++def ahead_behind_string ahead, behind
++ [ahead.empty? ? nil : "#{ahead.size.pluralize 'commit'} ahead",
++ behind.empty? ? nil : "#{behind.size.pluralize 'commit'} behind"].
++ compact.join("; ")
++end
++
++def show b, all_branches
++ puts "Local branch: #{b[:local_branch]}"
++ both = false
++
++ if b[:remote_branch]
++ pushc = commits_between b[:remote_branch], b[:local_branch]
++ pullc = commits_between b[:local_branch], b[:remote_branch]
++
++ both = !pushc.empty? && !pullc.empty?
++ if pushc.empty?
++ puts "[x] in sync with remote"
++ else
++ action = both ? "push after rebase / merge" : "push"
++ puts "[ ] NOT in sync with remote (needs #{action})"
++ show_commits pushc
++ end
++
++ puts "\nRemote branch: #{b[:remote_branch]} (#{b[:remote_url]})"
++
++ if pullc.empty?
++ puts "[x] in sync with local"
++ else
++ action = pushc.empty? ? "merge" : "rebase / merge"
++ puts "[ ] NOT in sync with local (needs #{action})"
++ show_commits pullc
++
++ both = !pushc.empty? && !pullc.empty?
++ end
++ end
++
++ vbs, fbs = all_branches.partition { |name, br| $config["versions"].include? br[:local_branch] }
++ if $config["versions"].include? b[:local_branch]
++ puts "\nFeature branches:" unless fbs.empty?
++ fbs.each do |name, br|
++ remote_ahead = b[:remote_branch] ? commits_between(b[:remote_branch], br[:local_branch]) : []
++ local_ahead = commits_between b[:local_branch], br[:local_branch]
++ if local_ahead.empty? && remote_ahead.empty?
++ puts "[x] #{br[:name]} is merged in"
++ elsif local_ahead.empty? && b[:remote_branch]
++ puts "(x) #{br[:name]} merged in (only locally)"
++ else
++ behind = commits_between br[:local_branch], b[:local_branch]
++ puts "[ ] #{br[:name]} is NOT merged in (#{ahead_behind_string local_ahead, behind})"
++ show_commits local_ahead
++ end
++ end
++ else
++ puts "\nVersion branches:" unless vbs.empty? # unlikely
++ vbs.each do |v, br|
++ ahead = commits_between v, b[:local_branch]
++ if ahead.empty?
++ puts "[x] merged into #{v}"
++ else
++ #behind = commits_between b[:local_branch], v
++ puts "[ ] NOT merged into #{v} (#{ahead.size.pluralize 'commit'} ahead)"
++ show_commits ahead
++ end
++ end
++ end
++
++ puts "\nWARNING: local and remote branches have diverged. A merge will occur unless you rebase." if both
++end
++
++branches = `git show-ref`.inject({}) do |hash, l|
++ sha1, ref = l.chomp.split " refs/"
++ next hash if $config["ignore"].member? ref
++ next hash unless ref =~ /^heads\/(.+)/
++ name = $1
++ hash[name] = { :name => name, :local_branch => ref }
++ hash
++end
++
++remotes = `git config --get-regexp ^remote\.\*\.url`.inject({}) do |hash, l|
++ l =~ /^remote\.(.+?)\.url (.+)$/ or next hash
++ hash[$1] ||= $2
++ hash
++end
++
++`git config --get-regexp ^branch\.`.each do |l|
++ case l
++ when /branch\.(.*?)\.remote (.+)/
++ branches[$1] ||= {}
++ branches[$1][:remote] = $2
++ branches[$1][:remote_url] = remotes[$2]
++ when /branch\.(.*?)\.merge ((refs\/)?heads\/)?(.+)/
++ branches[$1] ||= {}
++ branches[$1][:remote_mergepoint] = $4
++ end
++end
++
++branches.each { |k, v| v[:remote_branch] = "#{v[:remote]}/#{v[:remote_mergepoint]}" if v[:remote] && v[:remote_mergepoint] }
++
++show_dirty = ARGV.empty?
++targets = if ARGV.empty?
++ [`git symbolic-ref HEAD`.chomp.sub(/^refs\/heads\//, "")]
++else
++ ARGV
++end.map { |t| branches[t] or abort "Error: can't find branch #{t.inspect}." }
++
++targets.each { |t| show t, branches }
++
++modified = show_dirty && `git ls-files -m` != ""
++uncommitted = show_dirty && `git diff-index --cached HEAD` != ""
++
++puts if modified || uncommitted
++puts "NOTE: working directory contains modified files" if modified
++puts "NOTE: staging area contains staged but uncommitted files" if uncommitted
++
++# the end!
++