2 from stgit import exception, run, utils
3 from stgit.config import config
5 class RepositoryException(exception.StgException):
8 class DetachedHeadException(RepositoryException):
10 RepositoryException.__init__(self, 'Not on any branch')
16 class NoValue(object):
19 def make_defaults(defaults):
23 elif defaults != NoValue:
24 return getattr(defaults, attr)
31 def __init__(self, name = NoValue, email = NoValue,
32 date = NoValue, defaults = NoValue):
33 d = make_defaults(defaults)
34 self.__name = d(name, 'name')
35 self.__email = d(email, 'email')
36 self.__date = d(date, 'date')
37 name = property(lambda self: self.__name)
38 email = property(lambda self: self.__email)
39 date = property(lambda self: self.__date)
40 def set_name(self, name):
41 return type(self)(name = name, defaults = self)
42 def set_email(self, email):
43 return type(self)(email = email, defaults = self)
44 def set_date(self, date):
45 return type(self)(date = date, defaults = self)
47 return '%s <%s> %s' % (self.name, self.email, self.date)
50 m = re.match(r'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s)
52 name = m.group(1).strip()
55 return cls(name, email, date)
58 if not hasattr(cls, '__user'):
59 cls.__user = cls(name = config.get('user.name'),
60 email = config.get('user.email'))
64 if not hasattr(cls, '__author'):
66 name = os.environ.get('GIT_AUTHOR_NAME', NoValue),
67 email = os.environ.get('GIT_AUTHOR_EMAIL', NoValue),
68 date = os.environ.get('GIT_AUTHOR_DATE', NoValue),
69 defaults = cls.user())
73 if not hasattr(cls, '__committer'):
74 cls.__committer = cls(
75 name = os.environ.get('GIT_COMMITTER_NAME', NoValue),
76 email = os.environ.get('GIT_COMMITTER_EMAIL', NoValue),
77 date = os.environ.get('GIT_COMMITTER_DATE', NoValue),
78 defaults = cls.user())
79 return cls.__committer
83 def __init__(self, sha1):
85 sha1 = property(lambda self: self.__sha1)
87 return 'Tree<%s>' % self.sha1
89 class Commitdata(Repr):
91 def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,
92 committer = NoValue, message = NoValue, defaults = NoValue):
93 d = make_defaults(defaults)
94 self.__tree = d(tree, 'tree')
95 self.__parents = d(parents, 'parents')
96 self.__author = d(author, 'author')
97 self.__committer = d(committer, 'committer')
98 self.__message = d(message, 'message')
99 tree = property(lambda self: self.__tree)
100 parents = property(lambda self: self.__parents)
103 assert len(self.__parents) == 1
104 return self.__parents[0]
105 author = property(lambda self: self.__author)
106 committer = property(lambda self: self.__committer)
107 message = property(lambda self: self.__message)
108 def set_tree(self, tree):
109 return type(self)(tree = tree, defaults = self)
110 def set_parents(self, parents):
111 return type(self)(parents = parents, defaults = self)
112 def add_parent(self, parent):
113 return type(self)(parents = list(self.parents or []) + [parent],
115 def set_parent(self, parent):
116 return self.set_parents([parent])
117 def set_author(self, author):
118 return type(self)(author = author, defaults = self)
119 def set_committer(self, committer):
120 return type(self)(committer = committer, defaults = self)
121 def set_message(self, message):
122 return type(self)(message = message, defaults = self)
123 def is_nochange(self):
124 return len(self.parents) == 1 and self.tree == self.parent.data.tree
126 if self.tree == None:
129 tree = self.tree.sha1
130 if self.parents == None:
133 parents = [p.sha1 for p in self.parents]
134 return ('Commitdata<tree: %s, parents: %s, author: %s,'
135 ' committer: %s, message: "%s">'
136 ) % (tree, parents, self.author, self.committer, self.message)
138 def parse(cls, repository, s):
140 lines = list(s.splitlines(True))
141 for i in xrange(len(lines)):
142 line = lines[i].strip()
144 return cd.set_message(''.join(lines[i+1:]))
145 key, value = line.split(None, 1)
147 cd = cd.set_tree(repository.get_tree(value))
148 elif key == 'parent':
149 cd = cd.add_parent(repository.get_commit(value))
150 elif key == 'author':
151 cd = cd.set_author(Person.parse(value))
152 elif key == 'committer':
153 cd = cd.set_committer(Person.parse(value))
160 def __init__(self, repository, sha1):
162 self.__repository = repository
164 sha1 = property(lambda self: self.__sha1)
167 if self.__data == None:
168 self.__data = Commitdata.parse(
170 self.__repository.cat_object(self.sha1))
173 return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)
176 def __init__(self, repository):
177 self.__repository = repository
179 def __cache_refs(self):
181 for line in self.__repository.run(['git', 'show-ref']).output_lines():
182 m = re.match(r'^([0-9a-f]{40})\s+(\S+)$', line)
183 sha1, ref = m.groups()
184 self.__refs[ref] = sha1
186 """Throws KeyError if ref doesn't exist."""
187 if self.__refs == None:
189 return self.__repository.get_commit(self.__refs[ref])
190 def exists(self, ref):
197 def set(self, ref, commit, msg):
198 if self.__refs == None:
200 old_sha1 = self.__refs.get(ref, '0'*40)
201 new_sha1 = commit.sha1
202 if old_sha1 != new_sha1:
203 self.__repository.run(['git', 'update-ref', '-m', msg,
204 ref, new_sha1, old_sha1]).no_output()
205 self.__refs[ref] = new_sha1
206 def delete(self, ref):
207 if self.__refs == None:
209 self.__repository.run(['git', 'update-ref',
210 '-d', ref, self.__refs[ref]]).no_output()
213 class ObjectCache(object):
214 """Cache for Python objects, for making sure that we create only one
215 Python object per git object."""
216 def __init__(self, create):
218 self.__create = create
219 def __getitem__(self, name):
220 if not name in self.__objects:
221 self.__objects[name] = self.__create(name)
222 return self.__objects[name]
223 def __contains__(self, name):
224 return name in self.__objects
225 def __setitem__(self, name, val):
226 assert not name in self.__objects
227 self.__objects[name] = val
229 class RunWithEnv(object):
230 def run(self, args, env = {}):
231 return run.Run(*args).env(utils.add_dict(self.env, env))
233 class Repository(RunWithEnv):
234 def __init__(self, directory):
235 self.__git_dir = directory
236 self.__refs = Refs(self)
237 self.__trees = ObjectCache(lambda sha1: Tree(sha1))
238 self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))
239 self.__default_index = None
240 self.__default_worktree = None
241 self.__default_iw = None
242 env = property(lambda self: { 'GIT_DIR': self.__git_dir })
245 """Return the default repository."""
247 return cls(run.Run('git', 'rev-parse', '--git-dir'
249 except run.RunException:
250 raise RepositoryException('Cannot find git repository')
252 def default_index(self):
253 if self.__default_index == None:
254 self.__default_index = Index(
255 self, (os.environ.get('GIT_INDEX_FILE', None)
256 or os.path.join(self.__git_dir, 'index')))
257 return self.__default_index
258 def temp_index(self):
259 return Index(self, self.__git_dir)
261 def default_worktree(self):
262 if self.__default_worktree == None:
263 path = os.environ.get('GIT_WORK_TREE', None)
265 o = run.Run('git', 'rev-parse', '--show-cdup').output_lines()
269 self.__default_worktree = Worktree(path)
270 return self.__default_worktree
272 def default_iw(self):
273 if self.__default_iw == None:
274 self.__default_iw = IndexAndWorktree(self.default_index,
275 self.default_worktree)
276 return self.__default_iw
277 directory = property(lambda self: self.__git_dir)
278 refs = property(lambda self: self.__refs)
279 def cat_object(self, sha1):
280 return self.run(['git', 'cat-file', '-p', sha1]).raw_output()
281 def rev_parse(self, rev):
283 return self.get_commit(self.run(
284 ['git', 'rev-parse', '%s^{commit}' % rev]
286 except run.RunException:
287 raise RepositoryException('%s: No such revision' % rev)
288 def get_tree(self, sha1):
289 return self.__trees[sha1]
290 def get_commit(self, sha1):
291 return self.__commits[sha1]
292 def commit(self, commitdata):
293 c = ['git', 'commit-tree', commitdata.tree.sha1]
294 for p in commitdata.parents:
298 for p, v1 in ((commitdata.author, 'AUTHOR'),
299 (commitdata.committer, 'COMMITTER')):
301 for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),
303 if getattr(p, attr) != None:
304 env['GIT_%s_%s' % (v1, v2)] = getattr(p, attr)
305 sha1 = self.run(c, env = env).raw_input(commitdata.message
307 return self.get_commit(sha1)
311 return self.run(['git', 'symbolic-ref', '-q', 'HEAD']
313 except run.RunException:
314 raise DetachedHeadException()
315 def set_head(self, ref, msg):
316 self.run(['git', 'symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()
317 def simple_merge(self, base, ours, theirs):
318 """Given three trees, tries to do an in-index merge in a temporary
319 index with a temporary index. Returns the result tree, or None if
320 the merge failed (due to conflicts)."""
321 assert isinstance(base, Tree)
322 assert isinstance(ours, Tree)
323 assert isinstance(theirs, Tree)
325 # Take care of the really trivial cases.
333 index = self.temp_index()
335 index.merge(base, ours, theirs)
337 return index.write_tree()
338 except MergeException:
342 def apply(self, tree, patch_text):
343 """Given a tree and a patch, will either return the new tree that
344 results when the patch is applied, or None if the patch
345 couldn't be applied."""
346 assert isinstance(tree, Tree)
349 index = self.temp_index()
351 index.read_tree(tree)
353 index.apply(patch_text)
354 return index.write_tree()
355 except MergeException:
360 class MergeException(exception.StgException):
363 class Index(RunWithEnv):
364 def __init__(self, repository, filename):
365 self.__repository = repository
366 if os.path.isdir(filename):
367 # Create a temp index in the given directory.
368 self.__filename = os.path.join(
369 filename, 'index.temp-%d-%x' % (os.getpid(), id(self)))
372 self.__filename = filename
373 env = property(lambda self: utils.add_dict(
374 self.__repository.env, { 'GIT_INDEX_FILE': self.__filename }))
375 def read_tree(self, tree):
376 self.run(['git', 'read-tree', tree.sha1]).no_output()
377 def write_tree(self):
379 return self.__repository.get_tree(
380 self.run(['git', 'write-tree']).discard_stderr(
382 except run.RunException:
383 raise MergeException('Conflicting merge')
386 self.run(['git', 'update-index', '--refresh']).discard_output()
387 except run.RunException:
391 def merge(self, base, ours, theirs):
392 """In-index merge, no worktree involved."""
393 self.run(['git', 'read-tree', '-m', '-i', '--aggressive',
394 base.sha1, ours.sha1, theirs.sha1]).no_output()
395 def apply(self, patch_text):
396 """In-index patch application, no worktree involved."""
398 self.run(['git', 'apply', '--cached']
399 ).raw_input(patch_text).no_output()
400 except run.RunException:
401 raise MergeException('Patch does not apply cleanly')
403 if os.path.isfile(self.__filename):
404 os.remove(self.__filename)
406 """The set of conflicting paths."""
408 for line in self.run(['git', 'ls-files', '-z', '--unmerged']
409 ).raw_output().split('\0')[:-1]:
410 stat, path = line.split('\t', 1)
414 class Worktree(object):
415 def __init__(self, directory):
416 self.__directory = directory
417 env = property(lambda self: { 'GIT_WORK_TREE': self.__directory })
418 directory = property(lambda self: self.__directory)
420 class CheckoutException(exception.StgException):
423 class IndexAndWorktree(RunWithEnv):
424 def __init__(self, index, worktree):
426 self.__worktree = worktree
427 index = property(lambda self: self.__index)
428 env = property(lambda self: utils.add_dict(self.__index.env,
429 self.__worktree.env))
430 def checkout(self, old_tree, new_tree):
431 # TODO: Optionally do a 3-way instead of doing nothing when we
432 # have a problem. Or maybe we should stash changes in a patch?
433 assert isinstance(old_tree, Tree)
434 assert isinstance(new_tree, Tree)
436 self.run(['git', 'read-tree', '-u', '-m',
437 '--exclude-per-directory=.gitignore',
438 old_tree.sha1, new_tree.sha1]
439 ).cwd(self.__worktree.directory).discard_output()
440 except run.RunException:
441 raise CheckoutException('Index/workdir dirty')
442 def merge(self, base, ours, theirs):
443 assert isinstance(base, Tree)
444 assert isinstance(ours, Tree)
445 assert isinstance(theirs, Tree)
447 self.run(['git', 'merge-recursive', base.sha1, '--', ours.sha1,
449 env = { 'GITHEAD_%s' % base.sha1: 'ancestor',
450 'GITHEAD_%s' % ours.sha1: 'current',
451 'GITHEAD_%s' % theirs.sha1: 'patched'}
452 ).cwd(self.__worktree.directory).discard_output()
453 except run.RunException, e:
454 raise MergeException('Index/worktree dirty')
455 def changed_files(self):
456 return self.run(['git', 'diff-files', '--name-only']).output_lines()
457 def update_index(self, files):
458 self.run(['git', 'update-index', '--remove', '-z', '--stdin']
459 ).input_nulterm(files).discard_output()