Add the --graphical option to series
[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
3659ef88 21import sys, os, popen2, re, gitmergeonefile
41a6d859 22
170f576b 23from stgit import basedir
41a6d859
CM
24from stgit.utils import *
25
26# git exception class
27class GitException(Exception):
28 pass
29
30
41a6d859 31
41a6d859
CM
32#
33# Classes
34#
35class Commit:
36 """Handle the commit objects
37 """
38 def __init__(self, id_hash):
39 self.__id_hash = id_hash
41a6d859 40
26dba451
BL
41 lines = _output_lines('git-cat-file commit %s' % id_hash)
42 for i in range(len(lines)):
43 line = lines[i]
41a6d859
CM
44 if line == '\n':
45 break
46 field = line.strip().split(' ', 1)
47 if field[0] == 'tree':
48 self.__tree = field[1]
41a6d859
CM
49 if field[0] == 'author':
50 self.__author = field[1]
dad310d0 51 if field[0] == 'committer':
41a6d859 52 self.__committer = field[1]
0618ea9c 53 self.__log = ''.join(lines[i+1:])
41a6d859
CM
54
55 def get_id_hash(self):
56 return self.__id_hash
57
58 def get_tree(self):
59 return self.__tree
60
61 def get_parent(self):
2406f7d1 62 return self.get_parents()[0]
37a4d1bf
CM
63
64 def get_parents(self):
2406f7d1
CM
65 return _output_lines('git-rev-list --parents --max-count=1 %s'
66 % self.__id_hash)[0].split()[1:]
41a6d859
CM
67
68 def get_author(self):
69 return self.__author
70
71 def get_committer(self):
72 return self.__committer
73
37a4d1bf
CM
74 def get_log(self):
75 return self.__log
76
8e29bcd2
CM
77# dictionary of Commit objects, used to avoid multiple calls to git
78__commits = dict()
41a6d859
CM
79
80#
81# Functions
82#
bae29ddd 83
8e29bcd2
CM
84def get_commit(id_hash):
85 """Commit objects factory. Save/look-up them in the __commits
86 dictionary
87 """
3237b6e4
CM
88 global __commits
89
8e29bcd2
CM
90 if id_hash in __commits:
91 return __commits[id_hash]
92 else:
93 commit = Commit(id_hash)
94 __commits[id_hash] = commit
95 return commit
96
41a6d859
CM
97def get_conflicts():
98 """Return the list of file conflicts
99 """
170f576b 100 conflicts_file = os.path.join(basedir.get(), 'conflicts')
41a6d859
CM
101 if os.path.isfile(conflicts_file):
102 f = file(conflicts_file)
103 names = [line.strip() for line in f.readlines()]
104 f.close()
105 return names
106 else:
107 return None
108
0d2cd1e4 109def _input(cmd, file_desc):
741f2784 110 p = popen2.Popen3(cmd, True)
6fe6b1bd
CM
111 while True:
112 line = file_desc.readline()
113 if not line:
114 break
0d2cd1e4
CM
115 p.tochild.write(line)
116 p.tochild.close()
117 if p.wait():
2c5d2242
CM
118 raise GitException, '%s failed (%s)' % (str(cmd),
119 p.childerr.read().strip())
0d2cd1e4 120
d0bfda1a
CM
121def _input_str(cmd, string):
122 p = popen2.Popen3(cmd, True)
123 p.tochild.write(string)
124 p.tochild.close()
125 if p.wait():
2c5d2242
CM
126 raise GitException, '%s failed (%s)' % (str(cmd),
127 p.childerr.read().strip())
d0bfda1a 128
26dba451 129def _output(cmd):
741f2784 130 p=popen2.Popen3(cmd, True)
7cc615f3 131 output = p.fromchild.read()
26dba451 132 if p.wait():
2c5d2242
CM
133 raise GitException, '%s failed (%s)' % (str(cmd),
134 p.childerr.read().strip())
7cc615f3 135 return output
26dba451 136
d3cf7d86 137def _output_one_line(cmd, file_desc = None):
741f2784 138 p=popen2.Popen3(cmd, True)
d3cf7d86
PBG
139 if file_desc != None:
140 for line in file_desc:
141 p.tochild.write(line)
142 p.tochild.close()
7cc615f3 143 output = p.fromchild.readline().strip()
26dba451 144 if p.wait():
2c5d2242
CM
145 raise GitException, '%s failed (%s)' % (str(cmd),
146 p.childerr.read().strip())
7cc615f3 147 return output
41a6d859 148
26dba451 149def _output_lines(cmd):
741f2784 150 p=popen2.Popen3(cmd, True)
26dba451
BL
151 lines = p.fromchild.readlines()
152 if p.wait():
2c5d2242
CM
153 raise GitException, '%s failed (%s)' % (str(cmd),
154 p.childerr.read().strip())
26dba451
BL
155 return lines
156
157def __run(cmd, args=None):
158 """__run: runs cmd using spawnvp.
159
160 Runs cmd using spawnvp. The shell is avoided so it won't mess up
161 our arguments. If args is very large, the command is run multiple
162 times; args is split xargs style: cmd is passed on each
163 invocation. Unlike xargs, returns immediately if any non-zero
164 return code is received.
165 """
166
167 args_l=cmd.split()
168 if args is None:
169 args = []
170 for i in range(0, len(args)+1, 100):
171 r=os.spawnvp(os.P_WAIT, args_l[0], args_l + args[i:min(i+100, len(args))])
172 if r:
173 return r
174 return 0
175
9216b602 176def __tree_status(files = None, tree_id = 'HEAD', unknown = False,
be24d874 177 noexclude = True):
41a6d859
CM
178 """Returns a list of pairs - [status, filename]
179 """
f8fb5747 180 refresh_index()
41a6d859 181
9216b602
CL
182 if not files:
183 files = []
41a6d859
CM
184 cache_files = []
185
186 # unknown files
187 if unknown:
170f576b 188 exclude_file = os.path.join(basedir.get(), 'info', 'exclude')
be24d874
CM
189 base_exclude = ['--exclude=%s' % s for s in
190 ['*.[ao]', '*.pyc', '.*', '*~', '#*', 'TAGS', 'tags']]
191 base_exclude.append('--exclude-per-directory=.gitignore')
192
41a6d859 193 if os.path.exists(exclude_file):
3c6fbd2c 194 extra_exclude = ['--exclude-from=%s' % exclude_file]
be24d874
CM
195 else:
196 extra_exclude = []
4d4c0e3a
PBG
197 if noexclude:
198 extra_exclude = base_exclude = []
be24d874 199
2c02c3b7
PBG
200 lines = _output_lines(['git-ls-files', '--others', '--directory']
201 + base_exclude + extra_exclude)
26dba451 202 cache_files += [('?', line.strip()) for line in lines]
41a6d859
CM
203
204 # conflicted files
205 conflicts = get_conflicts()
206 if not conflicts:
207 conflicts = []
208 cache_files += [('C', filename) for filename in conflicts]
209
210 # the rest
a57bd720 211 for line in _output_lines(['git-diff-index', tree_id, '--'] + files):
26dba451 212 fs = tuple(line.rstrip().split(' ',4)[-1].split('\t',1))
41a6d859
CM
213 if fs[1] not in conflicts:
214 cache_files.append(fs)
41a6d859
CM
215
216 return cache_files
217
218def local_changes():
219 """Return true if there are local changes in the tree
220 """
221 return len(__tree_status()) != 0
222
aa01a285
CM
223# HEAD value cached
224__head = None
225
41a6d859 226def get_head():
3097799d 227 """Verifies the HEAD and returns the SHA1 id that represents it
41a6d859 228 """
aa01a285
CM
229 global __head
230
231 if not __head:
232 __head = rev_parse('HEAD')
233 return __head
41a6d859
CM
234
235def get_head_file():
236 """Returns the name of the file pointed to by the HEAD link
237 """