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