grit 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grit might be problematic. Click here for more details.

@@ -0,0 +1,169 @@
1
+ module Grit
2
+
3
+ class Commit
4
+ include Lazy
5
+
6
+ attr_reader :id
7
+ lazy_reader :parents
8
+ lazy_reader :tree
9
+ lazy_reader :author
10
+ lazy_reader :authored_date
11
+ lazy_reader :committer
12
+ lazy_reader :committed_date
13
+ lazy_reader :message
14
+
15
+ # Instantiate a new Commit
16
+ # +id+ is the id of the commit
17
+ # +parents+ is an array of commit ids (will be converted into Commit instances)
18
+ # +tree+ is the correspdonding tree id (will be converted into a Tree object)
19
+ # +author+ is the author string
20
+ # +authored_date+ is the authored Time
21
+ # +committer+ is the committer string
22
+ # +committed_date+ is the committed Time
23
+ # +message+ is the first line of the commit message
24
+ #
25
+ # Returns Grit::Commit (baked)
26
+ def initialize(repo, id, parents, tree, author, authored_date, committer, committed_date, message)
27
+ @repo = repo
28
+ @id = id
29
+ @parents = parents.map { |p| Commit.create(repo, :id => p) }
30
+ @tree = Tree.create(repo, :id => tree)
31
+ @author = author
32
+ @authored_date = authored_date
33
+ @committer = committer
34
+ @committed_date = committed_date
35
+ @message = message
36
+
37
+ __baked__
38
+ end
39
+
40
+ def id_abbrev
41
+ @id_abbrev ||= @repo.git.rev_parse({:short => true}, self.id).chomp
42
+ end
43
+
44
+ # Create an unbaked Commit containing just the specified attributes
45
+ # +repo+ is the Repo
46
+ # +atts+ is a Hash of instance variable data
47
+ #
48
+ # Returns Grit::Commit (unbaked)
49
+ def self.create(repo, atts)
50
+ self.allocate.create_initialize(repo, atts)
51
+ end
52
+
53
+ # Initializer for Commit.create
54
+ # +repo+ is the Repo
55
+ # +atts+ is a Hash of instance variable data
56
+ #
57
+ # Returns Grit::Commit (unbaked)
58
+ def create_initialize(repo, atts)
59
+ @repo = nil
60
+ @id = nil
61
+ @parents = nil
62
+ @tree = nil
63
+ @author = nil
64
+ @authored_date = nil
65
+ @committer = nil
66
+ @committed_date = nil
67
+ @message = nil
68
+ @__baked__ = nil
69
+
70
+ @repo = repo
71
+ atts.each do |k, v|
72
+ instance_variable_set("@#{k}".to_sym, v)
73
+ end
74
+ self
75
+ end
76
+
77
+ # Use the id of this instance to populate all of the other fields
78
+ # when any of them are called.
79
+ #
80
+ # Returns nil
81
+ def __bake__
82
+ temp = self.class.find_all(@repo, @id, {:max_count => 1}).first
83
+ @parents = temp.parents
84
+ @tree = temp.tree
85
+ @author = temp.author
86
+ @authored_date = temp.authored_date
87
+ @committer = temp.committer
88
+ @committed_date = temp.committed_date
89
+ @message = temp.message
90
+ nil
91
+ end
92
+
93
+ # Find all commits matching the given criteria.
94
+ # +repo+ is the Repo
95
+ # +ref+ is the ref from which to begin (SHA1 or name)
96
+ # +options+ is a Hash of optional arguments to git
97
+ # :max_count is the maximum number of commits to fetch
98
+ # :skip is the number of commits to skip
99
+ #
100
+ # Returns Grit::Commit[] (baked)
101
+ def self.find_all(repo, ref, options = {})
102
+ allowed_options = [:max_count, :skip]
103
+
104
+ default_options = {:pretty => "raw"}
105
+ actual_options = default_options.merge(options)
106
+
107
+ output = repo.git.rev_list(actual_options, ref)
108
+
109
+ self.list_from_string(repo, output)
110
+ end
111
+
112
+ # Parse out commit information into an array of baked Commit objects
113
+ # +repo+ is the Repo
114
+ # +text+ is the text output from the git command (raw format)
115
+ #
116
+ # Returns Grit::Commit[] (baked)
117
+ def self.list_from_string(repo, text)
118
+ # remove empty lines
119
+ lines = text.split("\n").select { |l| !l.strip.empty? }
120
+
121
+ commits = []
122
+
123
+ while !lines.empty?
124
+ id = lines.shift.split.last
125
+ tree = lines.shift.split.last
126
+
127
+ parents = []
128
+ parents << lines.shift.split.last while lines.first =~ /^parent/
129
+
130
+ author, authored_date = self.actor(lines.shift)
131
+ committer, committed_date = self.actor(lines.shift)
132
+
133
+ messages = []
134
+ messages << lines.shift.strip while lines.first =~ /^ {4}/
135
+ message = messages.first || ''
136
+
137
+ commits << Commit.new(repo, id, parents, tree, author, authored_date, committer, committed_date, message)
138
+ end
139
+
140
+ commits
141
+ end
142
+
143
+ def self.diff(repo, id)
144
+ text = repo.git.diff({:full_index => true}, id)
145
+ Diff.list_from_string(repo, text)
146
+ end
147
+
148
+ # Convert this Commit to a String which is just the SHA1 id
149
+ def to_s
150
+ @id
151
+ end
152
+
153
+ # Pretty object inspection
154
+ def inspect
155
+ %Q{#<Grit::Commit "#{@id}">}
156
+ end
157
+
158
+ # private
159
+
160
+ # Parse out the actor (author or committer) info
161
+ #
162
+ # Returns [String (actor name and email), Time (acted at time)]
163
+ def self.actor(line)
164
+ m, actor, epoch = *line.match(/^.+? (.*) (\d+) .*$/)
165
+ [Actor.from_string(actor), Time.at(epoch.to_i)]
166
+ end
167
+ end # Commit
168
+
169
+ end # Grit
data/lib/grit/diff.rb ADDED
@@ -0,0 +1,57 @@
1
+ module Grit
2
+
3
+ class Diff
4
+ attr_reader :a_path, :b_path
5
+ attr_reader :a_commit, :b_commit
6
+ attr_reader :mode
7
+ attr_reader :new_file, :deleted_file
8
+ attr_reader :diff
9
+
10
+ def initialize(repo, a_path, b_path, a_commit, b_commit, mode, new_file, deleted_file, diff)
11
+ @repo = repo
12
+ @a_path = a_path
13
+ @b_path = b_path
14
+ @a_commit = a_commit =~ /^0{40}$/ ? nil : Commit.create(repo, :id => a_commit)
15
+ @b_commit = b_commit =~ /^0{40}$/ ? nil : Commit.create(repo, :id => b_commit)
16
+ @mode = mode
17
+ @new_file = new_file
18
+ @deleted_file = deleted_file
19
+ @diff = diff
20
+ end
21
+
22
+ def self.list_from_string(repo, text)
23
+ lines = text.split("\n")
24
+
25
+ diffs = []
26
+
27
+ while !lines.empty?
28
+ m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(\S+) b/(\S+)$})
29
+
30
+ new_file = false
31
+ deleted_file = false
32
+
33
+ if lines.first =~ /^new file/
34
+ m, mode = lines.shift.match(/^new file mode (.+)$/)
35
+ new_file = true
36
+ elsif lines.first =~ /^deleted file/
37
+ m, mode = lines.shift.match(/^deleted file mode (.+)$/)
38
+ deleted_file = true
39
+ end
40
+
41
+ m, a_commit, b_commit, mode = *lines.shift.match(%r{^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$})
42
+ mode.strip! if mode
43
+
44
+ diff_lines = []
45
+ while lines.first && lines.first !~ /^diff/
46
+ diff_lines << lines.shift
47
+ end
48
+ diff = diff_lines.join("\n")
49
+
50
+ diffs << Diff.new(repo, a_path, b_path, a_commit, b_commit, mode, new_file, deleted_file, diff)
51
+ end
52
+
53
+ diffs
54
+ end
55
+ end # Diff
56
+
57
+ end # Grit
@@ -0,0 +1,7 @@
1
+ module Grit
2
+ class InvalidGitRepositoryError < StandardError
3
+ end
4
+
5
+ class NoSuchPathError < StandardError
6
+ end
7
+ end
data/lib/grit/git.rb ADDED
@@ -0,0 +1,64 @@
1
+ module Grit
2
+
3
+ class Git
4
+ class << self
5
+ attr_accessor :git_binary
6
+ end
7
+
8
+ self.git_binary = "/usr/bin/env git"
9
+
10
+ attr_accessor :git_dir
11
+
12
+ def initialize(git_dir)
13
+ self.git_dir = git_dir
14
+ end
15
+
16
+ # Run the given git command with the specified arguments and return
17
+ # the result as a String
18
+ # +cmd+ is the command
19
+ # +options+ is a hash of Ruby style options
20
+ # +args+ is the list of arguments (to be joined by spaces)
21
+ #
22
+ # Examples
23
+ # git.rev_list({:max_count => 10, :header => true}, "master")
24
+ #
25
+ # Returns String
26
+ def method_missing(cmd, options = {}, *args)
27
+ opt_args = transform_options(options)
28
+
29
+ call = "#{Git.git_binary} --git-dir='#{self.git_dir}' #{cmd.to_s.gsub(/_/, '-')} #{(opt_args + args).join(' ')}"
30
+ puts call if Grit.debug
31
+ response = `#{call}`
32
+ puts response if Grit.debug
33
+ response
34
+ end
35
+
36
+ # Transform Ruby style options into git command line options
37
+ # +options+ is a hash of Ruby style options
38
+ #
39
+ # Returns String[]
40
+ # e.g. ["--max-count=10", "--header"]
41
+ def transform_options(options)
42
+ args = []
43
+ options.keys.each do |opt|
44
+ if opt.to_s.size == 1
45
+ if options[opt] == true
46
+ args << "-#{opt}"
47
+ else
48
+ val = options.delete(opt)
49
+ args << "-#{opt.to_s} #{val}"
50
+ end
51
+ else
52
+ if options[opt] == true
53
+ args << "--#{opt.to_s.gsub(/_/, '-')}"
54
+ else
55
+ val = options.delete(opt)
56
+ args << "--#{opt.to_s.gsub(/_/, '-')}=#{val}"
57
+ end
58
+ end
59
+ end
60
+ args
61
+ end
62
+ end # Git
63
+
64
+ end # Grit
data/lib/grit/head.rb ADDED
@@ -0,0 +1,79 @@
1
+ module Grit
2
+
3
+ # A Head is a named reference to a Commit. Every Head instance contains a name
4
+ # and a Commit object.
5
+ #
6
+ # r = Grit::Repo.new("/path/to/repo")
7
+ # h = r.heads.first
8
+ # h.name # => "master"
9
+ # h.commit # => #<Grit::Commit "1c09f116cbc2cb4100fb6935bb162daa4723f455">
10
+ # h.commit.id # => "1c09f116cbc2cb4100fb6935bb162daa4723f455"
11
+ class Head
12
+ attr_reader :name
13
+ attr_reader :commit
14
+
15
+ # Instantiate a new Head
16
+ # +name+ is the name of the head
17
+ # +commit+ is the Commit that the head points to
18
+ #
19
+ # Returns Grit::Head (baked)
20
+ def initialize(name, commit)
21
+ @name = name
22
+ @commit = commit
23
+ end
24
+
25
+ # Find all Heads
26
+ # +repo+ is the Repo
27
+ # +options+ is a Hash of options
28
+ #
29
+ # Returns Grit::Head[] (baked)
30
+ def self.find_all(repo, options = {})
31
+ default_options = {:sort => "committerdate",
32
+ :format => "'%(refname)%00%(objectname)'"}
33
+
34
+ actual_options = default_options.merge(options)
35
+
36
+ output = repo.git.for_each_ref(actual_options, "refs/heads")
37
+
38
+ Head.list_from_string(repo, output)
39
+ end
40
+
41
+ # Parse out head information into an array of baked head objects
42
+ # +repo+ is the Repo
43
+ # +text+ is the text output from the git command
44
+ #
45
+ # Returns Grit::Head[] (baked)
46
+ def self.list_from_string(repo, text)
47
+ heads = []
48
+
49
+ text.split("\n").each do |line|
50
+ heads << self.from_string(repo, line)
51
+ end
52
+
53
+ heads
54
+ end
55
+
56
+ # Create a new Head instance from the given string.
57
+ # +repo+ is the Repo
58
+ # +line+ is the formatted head information
59
+ #
60
+ # Format
61
+ # name: [a-zA-Z_/]+
62
+ # <null byte>
63
+ # id: [0-9A-Fa-f]{40}
64
+ #
65
+ # Returns Grit::Head (baked)
66
+ def self.from_string(repo, line)
67
+ full_name, id = line.split("\0")
68
+ name = full_name.split("/").last
69
+ commit = Commit.create(repo, :id => id)
70
+ self.new(name, commit)
71
+ end
72
+
73
+ # Pretty object inspection
74
+ def inspect
75
+ %Q{#<Grit::Head "#{@name}">}
76
+ end
77
+ end # Head
78
+
79
+ end # Grit
data/lib/grit/lazy.rb ADDED
@@ -0,0 +1,53 @@
1
+ # Allows attributes to be declared as lazy, meaning that they won't be
2
+ # computed until they are asked for. Just mix this module in:
3
+ #
4
+ # class Foo
5
+ # include Lazy
6
+ # ...
7
+ # end
8
+ #
9
+ # To specify a lazy reader:
10
+ #
11
+ # lazy_reader :att
12
+ #
13
+ # Then, define a method called __bake__ that computes all your lazy
14
+ # attributes:
15
+ #
16
+ # def __bake__
17
+ # @att = ...
18
+ # end
19
+ #
20
+ # If you happen to have already done all the hard work, you can mark an instance
21
+ # as already baked by calling:
22
+ #
23
+ # __baked__
24
+ #
25
+ # That's it! (Tom Preston-Werner: rubyisawesome.com)
26
+ module Lazy
27
+ module ClassMethods
28
+ def lazy_reader(*args)
29
+ args.each do |arg|
30
+ define_method(arg) do
31
+ val = instance_variable_get("@#{arg}")
32
+ return val if val
33
+ self.__prebake__
34
+ instance_variable_get("@#{arg}")
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ def __prebake__
41
+ return if @__baked__
42
+ self.__bake__
43
+ @__baked__ = true
44
+ end
45
+
46
+ def __baked__
47
+ @__baked__ = true
48
+ end
49
+
50
+ def self.included(base)
51
+ base.extend(ClassMethods)
52
+ end
53
+ end
data/lib/grit/repo.rb ADDED
@@ -0,0 +1,185 @@
1
+ module Grit
2
+
3
+ class Repo
4
+ # The path of the git repo as a String
5
+ attr_accessor :path
6
+ attr_reader :bare
7
+
8
+ # The git command line interface object
9
+ attr_accessor :git
10
+
11
+ # Create a new Repo instance
12
+ # +path+ is the path to either the root git directory or the bare git repo
13
+ #
14
+ # Examples
15
+ # g = Repo.new("/Users/tom/dev/grit")
16
+ # g = Repo.new("/Users/tom/public/grit.git")
17
+ #
18
+ # Returns Grit::Repo
19
+ def initialize(path)
20
+ epath = File.expand_path(path)
21
+
22
+ if File.exist?(File.join(epath, '.git'))
23
+ self.path = File.join(epath, '.git')
24
+ @bare = false
25
+ elsif File.exist?(epath) && epath =~ /\.git$/
26
+ self.path = epath
27
+ @bare = true
28
+ elsif File.exist?(epath)
29
+ raise InvalidGitRepositoryError.new(epath)
30
+ else
31
+ raise NoSuchPathError.new(epath)
32
+ end
33
+
34
+ self.git = Git.new(self.path)
35
+ end
36
+
37
+ # The project's description. Taken verbatim from GIT_REPO/description
38
+ #
39
+ # Returns String
40
+ def description
41
+ File.open(File.join(self.path, 'description')).read.chomp
42
+ end
43
+
44
+ # An array of Head objects representing the branch heads in
45
+ # this repo
46
+ #
47
+ # Returns Grit::Head[] (baked)
48
+ def heads
49
+ Head.find_all(self)
50
+ end
51
+
52
+ alias_method :branches, :heads
53
+
54
+ # An array of Commit objects representing the history of a given ref/commit
55
+ # +start+ is the branch/commit name (default 'master')
56
+ # +max_count+ is the maximum number of commits to return (default 10)
57
+ # +skip+ is the number of commits to skip (default 0)
58
+ #
59
+ # Returns Grit::Commit[] (baked)
60
+ def commits(start = 'master', max_count = 10, skip = 0)
61
+ options = {:max_count => max_count,
62
+ :skip => skip}
63
+
64
+ Commit.find_all(self, start, options)
65
+ end
66
+
67
+ # The Commit object for the specified id
68
+ # +id+ is the SHA1 identifier of the commit
69
+ #
70
+ # Returns Grit::Commit (baked)
71
+ def commit(id)
72
+ options = {:max_count => 1}
73
+
74
+ Commit.find_all(self, id, options).first
75
+ end
76
+
77
+ # The Tree object for the given treeish reference
78
+ # +treeish+ is the reference (default 'master')
79
+ # +paths+ is an optional Array of directory paths to restrict the tree (deafult [])
80
+ #
81
+ # Examples
82
+ # Repo.tree('master', ['lib/'])
83
+ #
84
+ # Returns Grit::Tree (baked)
85
+ def tree(treeish = 'master', paths = [])
86
+ Tree.construct(self, treeish, paths)
87
+ end
88
+
89
+ # The Blob object for the given id
90
+ # +id+ is the SHA1 id of the blob
91
+ #
92
+ # Returns Grit::Blob (unbaked)
93
+ def blob(id)
94
+ Blob.create(self, :id => id)
95
+ end
96
+
97
+ # The commit log for a treeish
98
+ #
99
+ # Returns Grit::Commit[]
100
+ def log(commit = 'master', path = nil, options = {})
101
+ default_options = {:pretty => "raw"}
102
+ actual_options = default_options.merge(options)
103
+ arg = path ? "#{commit} -- #{path}" : commit
104
+ commits = self.git.log(actual_options, arg)
105
+ Commit.list_from_string(self, commits)
106
+ end
107
+
108
+ # The diff from commit +a+ to commit +b+, optionally restricted to the given file(s)
109
+ # +a+ is the base commit
110
+ # +b+ is the other commit
111
+ # +paths+ is an optional list of file paths on which to restrict the diff
112
+ def diff(a, b, *paths)
113
+ self.git.diff({}, a, b, '--', *paths)
114
+ end
115
+
116
+ # The commit diff for the given commit
117
+ # +commit+ is the commit name/id
118
+ #
119
+ # Returns Grit::Diff[]
120
+ def commit_diff(commit)
121
+ Commit.diff(self, commit)
122
+ end
123
+
124
+ # Initialize a bare git repository at the given path
125
+ # +path+ is the full path to the repo (traditionally ends with /<name>.git)
126
+ #
127
+ # Examples
128
+ # Grit::Repo.init_bare('/var/git/myrepo.git')
129
+ #
130
+ # Returns Grit::Repo (the newly created repo)
131
+ def self.init_bare(path)
132
+ git = Git.new(path)
133
+ git.init
134
+ self.new(path)
135
+ end
136
+
137
+ # Archive the given treeish
138
+ # +treeish+ is the treeish name/id (default 'master')
139
+ # +prefix+ is the optional prefix
140
+ #
141
+ # Examples
142
+ # repo.archive_tar
143
+ # # => <String containing tar archive>
144
+ #
145
+ # repo.archive_tar('a87ff14')
146
+ # # => <String containing tar archive for commit a87ff14>
147
+ #
148
+ # repo.archive_tar('master', 'myproject/')
149
+ # # => <String containing tar archive and prefixed with 'myproject/'>
150
+ #
151
+ # Returns String (containing tar archive)
152
+ def archive_tar(treeish = 'master', prefix = nil)
153
+ options = {}
154
+ options[:prefix] = prefix if prefix
155
+ self.git.archive(options, treeish)
156
+ end
157
+
158
+ # Archive and gzip the given treeish
159
+ # +treeish+ is the treeish name/id (default 'master')
160
+ # +prefix+ is the optional prefix
161
+ #
162
+ # Examples
163
+ # repo.archive_tar_gz
164
+ # # => <String containing tar.gz archive>
165
+ #
166
+ # repo.archive_tar_gz('a87ff14')
167
+ # # => <String containing tar.gz archive for commit a87ff14>
168
+ #
169
+ # repo.archive_tar_gz('master', 'myproject/')
170
+ # # => <String containing tar.gz archive and prefixed with 'myproject/'>
171
+ #
172
+ # Returns String (containing tar.gz archive)
173
+ def archive_tar_gz(treeish = 'master', prefix = nil)
174
+ options = {}
175
+ options[:prefix] = prefix if prefix
176
+ self.git.archive(options, treeish, "| gzip")
177
+ end
178
+
179
+ # Pretty object inspection
180
+ def inspect
181
+ %Q{#<Grit::Repo "#{@path}">}
182
+ end
183
+ end # Repo
184
+
185
+ end # Grit