Allow "stg add" when we have no stack
[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
fba895f5 29from sets import Set
41a6d859
CM
30
31# git exception class
32class GitException(Exception):
33 pass
34
f0de3f92
KH
35# When a subprocess has a problem, we want the exception to be a
36# subclass of GitException.
37class GitRunException(GitException):
38 pass
39class GRun(Run):
40 exc = GitRunException
41a6d859 41
41a6d859 42
41a6d859
CM
43#
44# Classes
45#
9e3f506f
KH
46
47class Person:
48 """An author, committer, etc."""
49 def __init__(self, name = None, email = None, date = '',
50 desc = None):
5cd9e87f 51 self.name = self.email = self.date = None
9e3f506f
KH
52 if name or email or date:
53 assert not desc
54 self.name = name
55 self.email = email
56 self.date = date
57 elif desc:
58 assert not (name or email or date)
59 def parse_desc(s):
60 m = re.match(r'^(.+)<(.+)>(.*)$', s)
61 assert m
62 return [x.strip() or None for x in m.groups()]
63 self.name, self.email, self.date = parse_desc(desc)
64 def set_name(self, val):
65 if val:
66 self.name = val
67 def set_email(self, val):
68 if val:
69 self.email = val
70 def set_date(self, val):
71 if val:
72 self.date = val
73 def __str__(self):
74 if self.name and self.email:
75 return '%s <%s>' % (self.name, self.email)
76 else:
77 raise GitException, 'not enough identity data'
78
41a6d859
CM
79class Commit:
80 """Handle the commit objects
81 """
82 def __init__(self, id_hash):
83 self.__id_hash = id_hash
41a6d859 84
f0de3f92 85 lines = GRun('git-cat-file', 'commit', id_hash).output_lines()
26dba451
BL
86 for i in range(len(lines)):
87 line = lines[i]
f0de3f92
KH
88 if not line:
89 break # we've seen all the header fields
90 key, val = line.split(' ', 1)
91 if key == 'tree':
92 self.__tree = val
93 elif key == 'author':
94 self.__author = val
95 elif key == 'committer':
96 self.__committer = val
97 else:
98 pass # ignore other headers
99 self.__log = '\n'.join(lines[i+1:])
41a6d859
CM
100
101 def get_id_hash(self):
102 return self.__id_hash
103
104 def get_tree(self):
105 return self.__tree
106
107 def get_parent(self):
64354a2d
CM
108 parents = self.get_parents()
109 if parents:
110 return parents[0]
111 else:
112 return None
37a4d1bf
CM
113
114 def get_parents(self):
f0de3f92
KH
115 return GRun('git-rev-list', '--parents', '--max-count=1', self.__id_hash
116 ).output_one_line().split()[1:]
41a6d859
CM
117
118 def get_author(self):
119 return self.__author
120
121 def get_committer(self):
122 return self.__committer
123
37a4d1bf
CM
124 def get_log(self):
125 return self.__log
126
4d0ba818
KH
127 def __str__(self):
128 return self.get_id_hash()
129
8e29bcd2
CM
130# dictionary of Commit objects, used to avoid multiple calls to git
131__commits = dict()
41a6d859
CM
132
133#
134# Functions
135#
bae29ddd 136
8e29bcd2
CM
137def get_commit(id_hash):
138 """Commit objects factory. Save/look-up them in the __commits
139 dictionary
140 """
3237b6e4
CM
141 global __commits
142
8e29bcd2
CM
143 if id_hash in __commits:
144 return __commits[id_hash]
145 else:
146 commit = Commit(id_hash)
147 __commits[id_hash] = commit
148 return commit
149
41a6d859
CM
150def get_conflicts():
151 """Return the list of file conflicts
152 """
170f576b 153 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
154 if os.path.isfile(conflicts_file):
155 f = file(conflicts_file)
156 names = [line.strip() for line in f.readlines()]
157 f.close()
158 return names
159 else:
160 return None
161
2f830c0c
KH
162def exclude_files():
163 files = [os.path.join(basedir.get(), 'info', 'exclude')]
164 user_exclude = config.get('core.excludesfile')
165 if user_exclude:
166 files.append(user_exclude)
167 return files
168
d436b1da 169def tree_status(files = None, tree_id = 'HEAD', unknown = False,
2ace36ab 170 noexclude = True, verbose = False, diff_flags = []):
14c88aa0 171 """Returns a list of pairs - (status, filename)
41a6d859 172 """
27ac2b7e
KH
173 if verbose:
174 out.start('Checking for changes in the working directory')
b6c37f44 175
f8fb5747 176 refresh_index()
41a6d859 177
9216b602
CL
178 if not files:
179 files = []
41a6d859
CM
180 cache_files = []
181
182 # unknown files
183 if unknown:
6d0d7ee6
KH
184 cmd = ['git-ls-files', '-z', '--others', '--directory',
185 '--no-empty-directory']
14c88aa0
DK
186 if not noexclude:
187 cmd += ['--exclude=%s' % s for s in
188 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
189 cmd += ['--exclude-per-directory=.gitignore']
190 cmd += ['--exclude-from=%s' % fn
191 for fn in exclude_files()
192 if os.path.exists(fn)]
193
194 lines = GRun(*cmd).raw_output().split('\0')
6d0d7ee6 195 cache_files += [('?', line) for line in lines if line]
41a6d859
CM
196
197 # conflicted files
198 conflicts = get_conflicts()
199 if not conflicts:
200 conflicts = []
201 cache_files += [('C', filename) for filename in conflicts]
202
203 # the rest
f0de3f92
KH
204 for line in GRun('git-diff-index', *(diff_flags + [tree_id, '--'] + files)
205 ).output_lines():
26dba451 206 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
207 if fs[1] not in conflicts:
208 cache_files.append(fs)
41a6d859 209
27ac2b7e
KH
210 if verbose:
211 out.done()
b6c37f44 212
41a6d859
CM
213 return cache_files
214
06848fab 215def local_changes(verbose = True):
41a6d859
CM
216 """Return true if there are local changes in the tree
217 """
d436b1da 218 return len(tree_status(verbose = verbose)) != 0
41a6d859 219
262d31dc
KH
220def get_heads():
221 heads = []
216a1524 222 hr = re.compile(r'^[0-9a-f]{40} refs/heads/(.+)$')
f0de3f92 223 for line in GRun('git-show-ref', '--heads').output_lines():
216a1524 224 m = hr.match(line)
262d31dc
KH
225 heads.append(m.group(1))
226 return heads
227
aa01a285
CM
228# HEAD value cached
229__head = None
230
41a6d859 231def get_head():
3097799d 232 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 233 """
aa01a285
CM
234 global __head
235
236 if not __head:
237 __head = rev_parse('HEAD')
238 return __head
41a6d859
CM
239
240def get_head_file():
241 """Returns the name of the file pointed to by the HEAD link
242 """