git.__remotes_from_dir() should only return lists
[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 = []):
14c88aa0 170 """Returns a list of pairs - (status, filename)
41a6d859 171 """
27ac2b7e
KH
172 if verbose:
173 out.start('Checking for changes in the working directory')
b6c37f44 174
f8fb5747 175 refresh_index()
41a6d859 176
9216b602
CL
177 if not files:
178 files = []
41a6d859
CM
179 cache_files = []
180
181 # unknown files
182 if unknown:
6d0d7ee6
KH
183 cmd = ['git-ls-files', '-z', '--others', '--directory',
184 '--no-empty-directory']
14c88aa0
DK
185 if not noexclude:
186 cmd += ['--exclude=%s' % s for s in
187 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
188 cmd += ['--exclude-per-directory=.gitignore']
189 cmd += ['--exclude-from=%s' % fn
190 for fn in exclude_files()
191 if os.path.exists(fn)]
192
193 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 194 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
195
196 # conflicted files
197 conflicts = get_conflicts()
198 if not conflicts:
199 conflicts = []
200 cache_files += [('C', filename) for filename in conflicts]
201
202 # the rest
f0de3f92
KH
203 for line in GRun('git-diff-index', *(diff_flags + [tree_id, '--'] + files)
204 ).output_lines():
26dba451 205 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
206 if fs[1] not in conflicts:
207 cache_files.append(fs)
41a6d859 208
27ac2b7e
KH
209 if verbose:
210 out.done()
b6c37f44 211
41a6d859
CM
212 return cache_files
213
06848fab 214def local_changes(verbose = True):
41a6d859
CM
215 """Return true if there are local changes in the tree
216 """
d436b1da 217 return len(tree_status(verbose = verbose)) != 0
41a6d859 218
262d31dc
KH
219def get_heads():
220 heads = []
216a1524 221 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
f0de3f92 222 for line in GRun('git-show-ref', '--heads').output_lines():
216a1524 223 m = hr.match(line)
262d31dc
KH
224 heads.append(m.group(1))
225 return heads
226
aa01a285
CM
227# HEAD value cached
228__head = None
229
41a6d859 230def get_head():
3097799d 231 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 232 """
aa01a285
CM
233 global __head
234
235 if not __head:
236 __head = rev_parse('HEAD')
237 return __head
41a6d859
CM
238
239def get_head_file():
240 """Returns the name of the file pointed to by the HEAD link
241 """