Moved that status function to the status command file
[stgit] / stgit / git.py
CommitLineData
41a6d859
CM
1"""Python GIT interface
2"""
3
4__copyright__ = """
5Copyright (C) 2005, Catalin Marinas <catalin.marinas@gmail.com>
6
7This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License version 2 as
9published by the Free Software Foundation.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16You should have received a copy of the GNU General Public License
17along with this program; if not, write to the Free Software
18Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19"""
20
f0de3f92 21import sys, os, re, gitmergeonefile
d5ae2173 22from shutil import copyfile
41a6d859 23
170f576b 24from stgit import basedir
41a6d859 25from stgit.utils import *
5e888f30 26from stgit.out import *
f0de3f92 27from stgit.run import *
b3bfa120 28from stgit.config import config
41a6d859
CM
29
30# git exception class
31class GitException(Exception):
32 pass
33
f0de3f92
KH
34# When a subprocess has a problem, we want the exception to be a
35# subclass of GitException.
36class GitRunException(GitException):
37 pass
38class GRun(Run):
39 exc = GitRunException
41a6d859 40
41a6d859 41
41a6d859
CM
42#
43# Classes
44#
9e3f506f
KH
45
46class Person:
47 """An author, committer, etc."""
48 def __init__(self, name = None, email = None, date = '',
49 desc = None):
5cd9e87f 50 self.name = self.email = self.date = None
9e3f506f
KH
51 if name or email or date:
52 assert not desc
53 self.name = name
54 self.email = email
55 self.date = date
56 elif desc:
57 assert not (name or email or date)
58 def parse_desc(s):
59 m = re.match(r'^(.+)<(.+)>(.*)$', s)
60 assert m
61 return [x.strip() or None for x in m.groups()]
62 self.name, self.email, self.date = parse_desc(desc)
63 def set_name(self, val):
64 if val:
65 self.name = val
66 def set_email(self, val):
67 if val:
68 self.email = val
69 def set_date(self, val):
70 if val:
71 self.date = val
72 def __str__(self):
73 if self.name and self.email:
74 return '%s <%s>' % (self.name, self.email)
75 else:
76 raise GitException, 'not enough identity data'
77
41a6d859
CM
78class Commit:
79 """Handle the commit objects
80 """
81 def __init__(self, id_hash):
82 self.__id_hash = id_hash
41a6d859 83
f0de3f92 84 lines = GRun('git-cat-file', 'commit', id_hash).output_lines()
26dba451
BL
85 for i in range(len(lines)):
86 line = lines[i]
f0de3f92
KH
87 if not line:
88 break # we've seen all the header fields
89 key, val = line.split(' ', 1)
90 if key == 'tree':
91 self.__tree = val
92 elif key == 'author':
93 self.__author = val
94 elif key == 'committer':
95 self.__committer = val
96 else:
97 pass # ignore other headers
98 self.__log = '\n'.join(lines[i+1:])
41a6d859
CM
99
100 def get_id_hash(self):
101 return self.__id_hash
102
103 def get_tree(self):
104 return self.__tree
105
106 def get_parent(self):
64354a2d
CM
107 parents = self.get_parents()
108 if parents:
109 return parents[0]
110 else:
111 return None
37a4d1bf
CM
112
113 def get_parents(self):
f0de3f92
KH
114 return GRun('git-rev-list', '--parents', '--max-count=1', self.__id_hash
115 ).output_one_line().split()[1:]
41a6d859
CM
116
117 def get_author(self):
118 return self.__author
119
120 def get_committer(self):
121 return self.__committer
122
37a4d1bf
CM
123 def get_log(self):
124 return self.__log
125
4d0ba818
KH
126 def __str__(self):
127 return self.get_id_hash()
128
8e29bcd2
CM
129# dictionary of Commit objects, used to avoid multiple calls to git
130__commits = dict()
41a6d859
CM
131
132#
133# Functions
134#
bae29ddd 135
8e29bcd2
CM
136def get_commit(id_hash):
137 """Commit objects factory. Save/look-up them in the __commits
138 dictionary
139 """
3237b6e4
CM
140 global __commits
141
8e29bcd2
CM
142 if id_hash in __commits:
143 return __commits[id_hash]
144 else:
145 commit = Commit(id_hash)
146 __commits[id_hash] = commit
147 return commit
148
41a6d859
CM
149def get_conflicts():
150 """Return the list of file conflicts
151 """
170f576b 152 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
153 if os.path.isfile(conflicts_file):
154 f = file(conflicts_file)
155 names = [line.strip() for line in f.readlines()]
156 f.close()
157 return names
158 else:
159 return None
160
2f830c0c
KH
161def exclude_files():
162 files = [os.path.join(basedir.get(), 'info', 'exclude')]
163 user_exclude = config.get('core.excludesfile')
164 if user_exclude:
165 files.append(user_exclude)
166 return files
167
d436b1da 168def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 169 noexclude = True, verbose = False, diff_flags = []):
70028538
DK
170 """Get the status of all changed files, or of a selected set of
171 files. Returns a list of pairs - (status, filename).
172
173 If 'files' is None, it will check all files, and optionally all
174 unknown files. If 'files' is a list, it will only check the files
175 in the list.
41a6d859 176 """
70028538
DK
177 assert files == None or not unknown
178
27ac2b7e
KH
179 if verbose:
180 out.start('Checking for changes in the working directory')
b6c37f44 181
f8fb5747 182 refresh_index()
41a6d859
CM
183
184 cache_files = []
185
186 # unknown files
187 if unknown:
6d0d7ee6
KH
188 cmd = ['git-ls-files', '-z', '--others', '--directory',
189 '--no-empty-directory']
14c88aa0
DK
190 if not noexclude:
191 cmd += ['--exclude=%s' % s for s in
192 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
193 cmd += ['--exclude-per-directory=.gitignore']
194 cmd += ['--exclude-from=%s' % fn
195 for fn in exclude_files()
196 if os.path.exists(fn)]
197
198 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 199 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
200
201 # conflicted files
202 conflicts = get_conflicts()
203 if not conflicts:
204 conflicts = []
70028538
DK
205 cache_files += [('C', filename) for filename in conflicts
206 if files == None or filename in files]
41a6d859
CM
207
208 # the rest
70028538
DK
209 args = diff_flags + [tree_id]
210 if files != None:
211 args += ['--'] + files
212 for line in GRun('git-diff-index', *args).output_lines():
26dba451 213 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
214 if fs[1] not in conflicts:
215 cache_files.append(fs)
41a6d859 216
27ac2b7e
KH
217 if verbose:
218 out.done()
b6c37f44 219
70028538 220 assert files == None or set(f for s,f in cache_files) <= set(files)
41a6d859
CM
221 return cache_files
222
06848fab 223def local_changes(verbose = True):
41a6d859
CM
224 """Return true if there are local changes in the tree
225 """
d436b1da 226 return len(tree_status(verbose = verbose)) != 0
41a6d859 227
262d31dc
KH
228def get_heads():
229 heads = []
216a1524 230 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
f0de3f92 231 for line in GRun('git-show-ref', '--heads').output_lines():
216a1524 232 m = hr.match(line)
262d31dc
KH
233 heads.append(m.group(1))
234 return heads
235
aa01a285
CM
236# HEAD value cached
237__head = None
238
41a6d859 239def get_head():
3097799d 240 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 241 """
aa01a285
CM
242 global __head
243
244 if not __head:
245 __head = rev_parse('HEAD')
246 return __head
41a6d859
CM
247
248def get_head_file():
249 """Returns the name of the file pointed to by the HEAD link
250 """