2 from stgit
import exception
, run
, utils
4 class RepositoryException(exception
.StgException
):
7 class DetachedHeadException(RepositoryException
):
9 RepositoryException
.__init__(self
, 'Not on any branch')
15 class NoValue(object):
18 def make_defaults(defaults
):
22 elif defaults
!= NoValue
:
23 return getattr(defaults
, attr
)
30 def __init__(self
, name
= NoValue
, email
= NoValue
,
31 date
= NoValue
, defaults
= NoValue
):
32 d
= make_defaults(defaults
)
33 self
.__name
= d(name
, 'name')
34 self
.__email
= d(email
, 'email')
35 self
.__date
= d(date
, 'date')
36 name
= property(lambda self
: self
.__name
)
37 email
= property(lambda self
: self
.__email
)
38 date
= property(lambda self
: self
.__date
)
39 def set_name(self
, name
):
40 return type(self
)(name
= name
, defaults
= self
)
41 def set_email(self
, email
):
42 return type(self
)(email
= email
, defaults
= self
)
43 def set_date(self
, date
):
44 return type(self
)(date
= date
, defaults
= self
)
46 return '%s <%s> %s' %
(self
.name
, self
.email
, self
.date
)
49 m
= re
.match(r
'^([^<]*)<([^>]*)>\s+(\d+\s+[+-]\d{4})$', s
)
51 name
= m
.group(1).strip()
54 return cls(name
, email
, date
)
58 def __init__(self
, sha1
):
60 sha1
= property(lambda self
: self
.__sha1
)
62 return 'Tree<%s>' % self
.sha1
64 class Commitdata(Repr
):
66 def __init__(self
, tree
= NoValue
, parents
= NoValue
, author
= NoValue
,
67 committer
= NoValue
, message
= NoValue
, defaults
= NoValue
):
68 d
= make_defaults(defaults
)
69 self
.__tree
= d(tree
, 'tree')
70 self
.__parents
= d(parents
, 'parents')
71 self
.__author
= d(author
, 'author')
72 self
.__committer
= d(committer
, 'committer')
73 self
.__message
= d(message
, 'message')
74 tree
= property(lambda self
: self
.__tree
)
75 parents
= property(lambda self
: self
.__parents
)
78 assert len(self
.__parents
) == 1
79 return self
.__parents
[0]
80 author
= property(lambda self
: self
.__author
)
81 committer
= property(lambda self
: self
.__committer
)
82 message
= property(lambda self
: self
.__message
)
83 def set_tree(self
, tree
):
84 return type(self
)(tree
= tree
, defaults
= self
)
85 def set_parents(self
, parents
):
86 return type(self
)(parents
= parents
, defaults
= self
)
87 def add_parent(self
, parent
):
88 return type(self
)(parents
= list(self
.parents
or []) + [parent
],
90 def set_parent(self
, parent
):
91 return self
.set_parents([parent
])
92 def set_author(self
, author
):
93 return type(self
)(author
= author
, defaults
= self
)
94 def set_committer(self
, committer
):
95 return type(self
)(committer
= committer
, defaults
= self
)
96 def set_message(self
, message
):
97 return type(self
)(message
= message
, defaults
= self
)
98 def is_nochange(self
):
99 return len(self
.parents
) == 1 and self
.tree
== self
.parent
.data
.tree
101 if self
.tree
== None:
104 tree
= self
.tree
.sha1
105 if self
.parents
== None:
108 parents
= [p
.sha1
for p
in self
.parents
]
109 return ('Commitdata<tree: %s, parents: %s, author: %s,'
110 ' committer: %s, message: "%s">'
111 ) %
(tree
, parents
, self
.author
, self
.committer
, self
.message
)
113 def parse(cls
, repository
, s
):
115 lines
= list(s
.splitlines(True))
116 for i
in xrange(len(lines
)):
117 line
= lines
[i
].strip()
119 return cd
.set_message(''.join(lines
[i
+1:]))
120 key
, value
= line
.split(None, 1)
122 cd
= cd
.set_tree(repository
.get_tree(value
))
123 elif key
== 'parent':
124 cd
= cd
.add_parent(repository
.get_commit(value
))
125 elif key
== 'author':
126 cd
= cd
.set_author(Person
.parse(value
))
127 elif key
== 'committer':
128 cd
= cd
.set_committer(Person
.parse(value
))
135 def __init__(self
, repository
, sha1
):
137 self
.__repository
= repository
139 sha1
= property(lambda self
: self
.__sha1
)
142 if self
.__data
== None:
143 self
.__data
= Commitdata
.parse(
145 self
.__repository
.cat_object(self
.sha1
))
148 return 'Commit<sha1: %s, data: %s>' %
(self
.sha1
, self
.__data
)
151 def __init__(self
, repository
):
152 self
.__repository
= repository
154 def __cache_refs(self
):
156 for line
in self
.__repository
.run(['git', 'show-ref']).output_lines():
157 m
= re
.match(r
'^([0-9a-f]{40})\s+(\S+)$', line
)
158 sha1
, ref
= m
.groups()
159 self
.__refs
[ref
] = sha1
161 """Throws KeyError if ref doesn't exist."""
162 if self
.__refs
== None:
164 return self
.__repository
.get_commit(self
.__refs
[ref
])
165 def exists(self
, ref
):
172 def set(self
, ref
, commit
, msg
):
173 if self
.__refs
== None:
175 old_sha1
= self
.__refs
.get(ref
, '0'*40)
176 new_sha1
= commit
.sha1
177 if old_sha1
!= new_sha1
:
178 self
.__repository
.run(['git', 'update-ref', '-m', msg
,
179 ref
, new_sha1
, old_sha1
]).no_output()
180 self
.__refs
[ref
] = new_sha1
181 def delete(self
, ref
):
182 if self
.__refs
== None:
184 self
.__repository
.run(['git', 'update-ref',
185 '-d', ref
, self
.__refs
[ref
]]).no_output()
188 class ObjectCache(object):
189 """Cache for Python objects, for making sure that we create only one
190 Python object per git object."""
191 def __init__(self
, create
):
193 self
.__create
= create
194 def __getitem__(self
, name
):
195 if not name
in self
.__objects
:
196 self
.__objects
[name
] = self
.__create(name
)
197 return self
.__objects
[name
]
198 def __contains__(self
, name
):
199 return name
in self
.__objects
200 def __setitem__(self
, name
, val
):
201 assert not name
in self
.__objects
202 self
.__objects
[name
] = val
204 class RunWithEnv(object):
205 def run(self
, args
, env
= {}):
206 return run
.Run(*args
).env(utils
.add_dict(self
.env
, env
))
208 class Repository(RunWithEnv
):
209 def __init__(self
, directory
):
210 self
.__git_dir
= directory
211 self
.__refs
= Refs(self
)
212 self
.__trees
= ObjectCache(lambda sha1
: Tree(sha1
))
213 self
.__commits
= ObjectCache(lambda sha1
: Commit(self
, sha1
))
214 env
= property(lambda self
: { 'GIT_DIR': self
.__git_dir
})
217 """Return the default repository."""
219 return cls(run
.Run('git', 'rev-parse', '--git-dir'
221 except run
.RunException
:
222 raise RepositoryException('Cannot find git repository')
223 def default_index(self
):
224 return Index(self
, (os
.environ
.get('GIT_INDEX_FILE', None)
225 or os
.path
.join(self
.__git_dir
, 'index')))
226 def temp_index(self
):
227 return Index(self
, self
.__git_dir
)
228 def default_worktree(self
):
229 path
= os
.environ
.get('GIT_WORK_TREE', None)
231 o
= run
.Run('git', 'rev-parse', '--show-cdup').output_lines()
235 return Worktree(path
)
236 def default_iw(self
):
237 return IndexAndWorktree(self
.default_index(), self
.default_worktree())
238 directory
= property(lambda self
: self
.__git_dir
)
239 refs
= property(lambda self
: self
.__refs
)
240 def cat_object(self
, sha1
):
241 return self
.run(['git', 'cat-file', '-p', sha1
]).raw_output()
242 def rev_parse(self
, rev
):
244 return self
.get_commit(self
.run(
245 ['git', 'rev-parse', '%s^{commit}' % rev
]
247 except run
.RunException
:
248 raise RepositoryException('%s: No such revision' % rev
)
249 def get_tree(self
, sha1
):
250 return self
.__trees
[sha1
]
251 def get_commit(self
, sha1
):
252 return self
.__commits
[sha1
]
253 def commit(self
, commitdata
):
254 c
= ['git', 'commit-tree', commitdata
.tree
.sha1
]
255 for p
in commitdata
.parents
:
259 for p
, v1
in ((commitdata
.author
, 'AUTHOR'),
260 (commitdata
.committer
, 'COMMITTER')):
262 for attr
, v2
in (('name', 'NAME'), ('email', 'EMAIL'),
264 if getattr(p
, attr
) != None:
265 env
['GIT_%s_%s' %
(v1
, v2
)] = getattr(p
, attr
)
266 sha1
= self
.run(c
, env
= env
).raw_input(commitdata
.message
268 return self
.get_commit(sha1
)
272 return self
.run(['git', 'symbolic-ref', '-q', 'HEAD']
274 except run
.RunException
:
275 raise DetachedHeadException()
276 def set_head(self
, ref
, msg
):
277 self
.run(['git', 'symbolic-ref', '-m', msg
, 'HEAD', ref
]).no_output()
278 def simple_merge(self
, base
, ours
, theirs
):
279 """Given three trees, tries to do an in-index merge in a temporary
280 index with a temporary index. Returns the result tree, or None if
281 the merge failed (due to conflicts)."""
282 assert isinstance(base
, Tree
)
283 assert isinstance(ours
, Tree
)
284 assert isinstance(theirs
, Tree
)
286 # Take care of the really trivial cases.
294 index
= self
.temp_index()
296 index
.merge(base
, ours
, theirs
)
298 return index
.write_tree()
299 except MergeException
:
304 class MergeException(exception
.StgException
):
307 class Index(RunWithEnv
):
308 def __init__(self
, repository
, filename
):
309 self
.__repository
= repository
310 if os
.path
.isdir(filename
):
311 # Create a temp index in the given directory.
312 self
.__filename
= os
.path
.join(
313 filename
, 'index.temp-%d-%x' %
(os
.getpid(), id(self
)))
316 self
.__filename
= filename
317 env
= property(lambda self
: utils
.add_dict(
318 self
.__repository
.env
, { 'GIT_INDEX_FILE': self
.__filename
}))
319 def read_tree(self
, tree
):
320 self
.run(['git', 'read-tree', tree
.sha1
]).no_output()
321 def write_tree(self
):
323 return self
.__repository
.get_tree(
324 self
.run(['git', 'write-tree']).discard_stderr(
326 except run
.RunException
:
327 raise MergeException('Conflicting merge')
330 self
.run(['git', 'update-index', '--refresh']).discard_output()
331 except run
.RunException
:
335 def merge(self
, base
, ours
, theirs
):
336 """In-index merge, no worktree involved."""
337 self
.run(['git', 'read-tree', '-m', '-i', '--aggressive',
338 base
.sha1
, ours
.sha1
, theirs
.sha1
]).no_output()
340 if os
.path
.isfile(self
.__filename
):
341 os
.remove(self
.__filename
)
343 class Worktree(object):
344 def __init__(self
, directory
):
345 self
.__directory
= directory
346 env
= property(lambda self
: { 'GIT_WORK_TREE': self
.__directory
})
348 class CheckoutException(exception
.StgException
):
351 class IndexAndWorktree(RunWithEnv
):
352 def __init__(self
, index
, worktree
):
354 self
.__worktree
= worktree
355 index
= property(lambda self
: self
.__index
)
356 env
= property(lambda self
: utils
.add_dict(self
.__index
.env
,
357 self
.__worktree
.env
))
358 def checkout(self
, old_tree
, new_tree
):
359 # TODO: Optionally do a 3-way instead of doing nothing when we
360 # have a problem. Or maybe we should stash changes in a patch?
361 assert isinstance(old_tree
, Tree
)
362 assert isinstance(new_tree
, Tree
)
364 self
.run(['git', 'read-tree', '-u', '-m',
365 '--exclude-per-directory=.gitignore',
366 old_tree
.sha1
, new_tree
.sha1
]
368 except run
.RunException
:
369 raise CheckoutException('Index/workdir dirty')
370 def merge(self
, base
, ours
, theirs
):
371 assert isinstance(base
, Tree
)
372 assert isinstance(ours
, Tree
)
373 assert isinstance(theirs
, Tree
)
375 self
.run(['git', 'merge-recursive', base
.sha1
, '--', ours
.sha1
,
377 env
= { 'GITHEAD_%s' % base
.sha1
: 'ancestor',
378 'GITHEAD_%s' % ours
.sha1
: 'current',
379 'GITHEAD_%s' % theirs
.sha1
: 'patched'}
381 except run
.RunException
, e
:
382 raise MergeException('Index/worktree dirty')
383 def changed_files(self
):
384 return self
.run(['git', 'diff-files', '--name-only']).output_lines()
385 def update_index(self
, files
):
386 self
.run(['git', 'update-index', '--remove', '-z', '--stdin']
387 ).input_nulterm(files
).discard_output()