fixed http://groups.google.com/group/git-python/browse_thread/thread/62b972d2345c74c2...
[jelmer/gitpython.git] / lib / git / commit.py
1 import re
2 import time
3
4 from actor import Actor
5 from lazy import LazyMixin
6 import tree
7 import diff
8 import stats
9
10 class Commit(LazyMixin):
11     def __init__(self, repo, **kwargs):
12         """
13         Instantiate a new Commit
14
15         ``id``
16             is the id of the commit
17
18         ``parents``
19             is a list of commit ids (will be converted into Commit instances)
20
21         ``tree``
22             is the correspdonding tree id (will be converted into a Tree object)
23
24         ``author``
25             is the author string
26
27         ``authored_date``
28             is the authored DateTime
29
30         ``committer``
31             is the committer string
32
33         ``committed_date``
34             is the committed DateTime
35
36         ``message``
37             is the first line of the commit message
38
39         Returns
40             GitPython.Commit
41         """
42         LazyMixin.__init__(self)
43
44         self.repo = repo
45         self.id = None
46         self.tree = None
47         self.author = None
48         self.authored_date = None
49         self.committer = None
50         self.committed_date = None
51         self.message = None
52         self.parents = None
53
54         for k, v in kwargs.items():
55             setattr(self, k, v)
56
57         if self.id:
58             if 'parents' in kwargs:
59                 self.parents = map(lambda p: Commit(repo, **{'id': p}), kwargs['parents'])
60             if 'tree' in kwargs:
61                 self.tree = tree.Tree(repo, **{'id': kwargs['tree']})
62
63     def __bake__(self):
64         temp = Commit.find_all(self.repo, self.id, **{'max_count': 1})[0]
65         self.parents = temp.parents
66         self.tree = temp.tree
67         self.author = temp.author
68         self.authored_date = temp.authored_date
69         self.committer = temp.committer
70         self.committed_date = temp.committed_date
71         self.message = temp.message
72
73     @property
74     def id_abbrev(self):
75         return self.id[0:7]
76
77     @classmethod
78     def count(cls, repo, ref):
79         """
80         Count the number of commits reachable from this ref
81
82         ``repo``
83             is the Repo
84
85         ``ref``
86             is the ref from which to begin (SHA1 or name)
87
88         Returns
89             int
90         """
91         return len(repo.git.rev_list(ref).strip().splitlines())
92
93     @classmethod
94     def find_all(cls, repo, ref, **kwargs):
95         """
96         Find all commits matching the given criteria.
97         ``repo``
98             is the Repo
99
100         ``ref``
101             is the ref from which to begin (SHA1 or name)
102
103         ``options``
104             is a Hash of optional arguments to git where
105             ``max_count`` is the maximum number of commits to fetch
106             ``skip`` is the number of commits to skip
107
108         Returns
109             GitPython.Commit[]
110         """
111         options = {'pretty': 'raw'}
112         options.update(kwargs)
113
114         output = repo.git.rev_list(ref, **options)
115         return cls.list_from_string(repo, output)
116
117     @classmethod
118     def list_from_string(cls, repo, text):
119         """
120         Parse out commit information into a list of Commit objects
121
122         ``repo``
123             is the Repo
124
125         ``text``
126             is the text output from the git command (raw format)
127
128         Returns
129             GitPython.Commit[]
130         """
131         lines = [l for l in text.splitlines() if l.strip()]
132
133         commits = []
134
135         while lines:
136             id = lines.pop(0).split()[-1]
137             tree = lines.pop(0).split()[-1]
138
139             parents = []
140             while lines and re.search(r'^parent', lines[0]):
141                 parents.append(lines.pop(0).split()[-1])
142             author, authored_date = cls.actor(lines.pop(0))
143             committer, committed_date = cls.actor(lines.pop(0))
144
145             messages = []
146             while lines and re.search(r'^ {4}', lines[0]):
147                 messages.append(lines.pop(0).strip())
148
149             message = messages and messages[0] or ''
150
151             commits.append(Commit(repo, id=id, parents=parents, tree=tree, author=author, authored_date=authored_date, 
152                                   committer=committer, committed_date=committed_date, message=message))
153
154         return commits
155
156     @classmethod
157     def diff(cls, repo, a, b = None, paths = None):
158         """
159         Show diffs between two trees:
160
161         ``repo``
162             is the Repo
163
164         ``a``
165             is a named commit
166
167         ``b``
168             is an optional named commit.  Passing a list assumes you
169             wish to omit the second named commit and limit the diff to the
170             given paths.
171
172         ``paths``
173             is a list of paths to limit the diff.
174
175         Returns
176             GitPython.Diff[]
177         """
178         paths = paths or []
179
180         if isinstance(b, list):
181             paths = b
182             b = None
183
184         if paths:
185             paths.insert(0, "--")
186
187         if b:
188             paths.insert(0, b)
189         paths.insert(0, a)
190         text = repo.git.diff(*paths, **{'full_index': True})
191         return diff.Diff.list_from_string(repo, text)
192
193     @property
194     def diffs(self):
195         if not self.parents:
196             d = self.repo.git.show(self.id, **{'full_index': True, 'pretty': 'raw'})
197             if re.search(r'diff --git a', d):
198                 if not re.search(r'^diff --git a', d):
199                     p = re.compile(r'.+?(diff --git a)', re.MULTILINE | re.DOTALL)
200                     d = p.sub(r'diff --git a', d, 1)
201             else:
202                 d = ''
203             return diff.Diff.list_from_string(self.repo, d)
204         else:
205             return self.diff(self.repo, self.parents[0].id, self.id)
206
207     @property
208     def stats(self):
209         if not self.parents:
210             text = self.repo.git.diff(self.id, **{'numstat': True})
211             text2 = ""
212             for line in text.splitlines():
213                 (insertions, deletions, filename) = line.split("\t")
214                 text2 += "%s\t%s\t%s\n" % (deletions, insertions, filename)
215             text = text2
216         else:
217             text = self.repo.git.diff(self.parents[0].id, self.id, **{'numstat': True})
218         return stats.Stats.list_from_string(self.repo, text)
219
220     def __str__(self):
221         """ Convert commit to string which is SHA1 """
222         return self.id
223
224     def __repr__(self):
225         return '<GitPython.Commit "%s">' % self.id
226
227     @classmethod
228     def actor(cls, line):
229         """
230         Parse out the actor (author or committer) info
231
232         Returns
233             [str (actor name and email), time (acted at time)]
234         """
235         m = re.search(r'^.+? (.*) (\d+) .*$', line)
236         actor, epoch = m.groups()
237         return [Actor.from_string(actor), time.gmtime(int(epoch))]