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
:
359 def diff_tree(self
, t1
, t2
, diff_opts
):
360 assert isinstance(t1
, Tree
)
361 assert isinstance(t2
, Tree
)
362 return self
.run(['git', 'diff-tree', '-p'] + list(diff_opts
)
363 + [t1
.sha1
, t2
.sha1
]).raw_output()
365 class MergeException(exception
.StgException
):
368 class Index(RunWithEnv
):
369 def __init__(self
, repository
, filename
):
370 self
.__repository
= repository
371 if os
.path
.isdir(filename
):
372 # Create a temp index in the given directory.
373 self
.__filename
= os
.path
.join(
374 filename
, 'index.temp-%d-%x' %
(os
.getpid(), id(self
)))
377 self
.__filename
= filename
378 env
= property(lambda self
: utils
.add_dict(
379 self
.__repository
.env
, { 'GIT_INDEX_FILE': self
.__filename
}))
380 def read_tree(self
, tree
):
381 self
.run(['git', 'read-tree', tree
.sha1
]).no_output()
382 def write_tree(self
):
384 return self
.__repository
.get_tree(
385 self
.run(['git', 'write-tree']).discard_stderr(
387 except run
.RunException
:
388 raise MergeException('Conflicting merge')
391 self
.run(['git', 'update-index', '--refresh']).discard_output()
392 except run
.RunException
:
396 def merge(self
, base
, ours
, theirs
):
397 """In-index merge, no worktree involved."""
398 self
.run(['git', 'read-tree', '-m', '-i', '--aggressive',
399 base
.sha1
, ours
.sha1
, theirs
.sha1
]).no_output()
400 def apply(self
, patch_text
):
401 """In-index patch application, no worktree involved."""
403 self
.run(['git', 'apply', '--cached']
404 ).raw_input(patch_text
).no_output()
405 except run
.RunException
:
406 raise MergeException('Patch does not apply cleanly')
408 if os
.path
.isfile(self
.__filename
):
409 os
.remove(self
.__filename
)
411 """The set of conflicting paths."""
413 for line
in self
.run(['git', 'ls-files', '-z', '--unmerged']
414 ).raw_output().split('\0')[:-1]:
415 stat
, path
= line
.split('\t', 1)
419 class Worktree(object):
420 def __init__(self
, directory
):
421 self
.__directory
= directory
422 env
= property(lambda self
: { 'GIT_WORK_TREE': self
.__directory
})
423 directory
= property(lambda self
: self
.__directory
)
425 class CheckoutException(exception
.StgException
):
428 class IndexAndWorktree(RunWithEnv
):
429 def __init__(self
, index
, worktree
):
431 self
.__worktree
= worktree
432 index
= property(lambda self
: self
.__index
)
433 env
= property(lambda self
: utils
.add_dict(self
.__index
.env
,
434 self
.__worktree
.env
))
435 def checkout(self
, old_tree
, new_tree
):
436 # TODO: Optionally do a 3-way instead of doing nothing when we
437 # have a problem. Or maybe we should stash changes in a patch?
438 assert isinstance(old_tree
, Tree
)
439 assert isinstance(new_tree
, Tree
)
441 self
.run(['git', 'read-tree', '-u', '-m',
442 '--exclude-per-directory=.gitignore',
443 old_tree
.sha1
, new_tree
.sha1
]
444 ).cwd(self
.__worktree
.directory
).discard_output()
445 except run
.RunException
:
446 raise CheckoutException('Index/workdir dirty')
447 def merge(self
, base
, ours
, theirs
):
448 assert isinstance(base
, Tree
)
449 assert isinstance(ours
, Tree
)
450 assert isinstance(theirs
, Tree
)
452 self
.run(['git', 'merge-recursive', base
.sha1
, '--', ours
.sha1
,
454 env
= { 'GITHEAD_%s' % base
.sha1
: 'ancestor',
455 'GITHEAD_%s' % ours
.sha1
: 'current',
456 'GITHEAD_%s' % theirs
.sha1
: 'patched'}
457 ).cwd(self
.__worktree
.directory
).discard_output()
458 except run
.RunException
, e
:
459 raise MergeException('Index/worktree dirty')
460 def changed_files(self
):
461 return self
.run(['git', 'diff-files', '--name-only']).output_lines()
462 def update_index(self
, files
):
463 self
.run(['git', 'update-index', '--remove', '-z', '--stdin']
464 ).input_nulterm(files
).discard_output()