gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / repo.py @ 959
History | View | Annotate | Download (38.4 KB)
1 |
# repo.py -- For dealing with git repositories.
|
---|---|
2 |
# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
|
3 |
# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
|
4 |
#
|
5 |
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
|
6 |
# General Public License as public by the Free Software Foundation; version 2.0
|
7 |
# or (at your option) any later version. You can redistribute it and/or
|
8 |
# modify it under the terms of either of these two licenses.
|
9 |
#
|
10 |
# Unless required by applicable law or agreed to in writing, software
|
11 |
# distributed under the License is distributed on an "AS IS" BASIS,
|
12 |
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
13 |
# See the License for the specific language governing permissions and
|
14 |
# limitations under the License.
|
15 |
#
|
16 |
# You should have received a copy of the licenses; if not, see
|
17 |
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
|
18 |
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
|
19 |
# License, Version 2.0.
|
20 |
#
|
21 |
|
22 |
|
23 |
"""Repository access.
|
24 |
|
25 |
This module contains the base class for git repositories
|
26 |
(BaseRepo) and an implementation which uses a repository on
|
27 |
local disk (Repo).
|
28 |
|
29 |
"""
|
30 |
|
31 |
from io import BytesIO |
32 |
import errno |
33 |
import os |
34 |
import sys |
35 |
import stat |
36 |
|
37 |
from dulwich.errors import ( |
38 |
NoIndexPresent, |
39 |
NotBlobError, |
40 |
NotCommitError, |
41 |
NotGitRepository, |
42 |
NotTreeError, |
43 |
NotTagError, |
44 |
CommitError, |
45 |
RefFormatError, |
46 |
HookError, |
47 |
) |
48 |
from dulwich.file import ( |
49 |
GitFile, |
50 |
) |
51 |
from dulwich.object_store import ( |
52 |
DiskObjectStore, |
53 |
MemoryObjectStore, |
54 |
ObjectStoreGraphWalker, |
55 |
) |
56 |
from dulwich.objects import ( |
57 |
check_hexsha, |
58 |
Blob, |
59 |
Commit, |
60 |
ShaFile, |
61 |
Tag, |
62 |
Tree, |
63 |
) |
64 |
|
65 |
from dulwich.hooks import ( |
66 |
PreCommitShellHook, |
67 |
PostCommitShellHook, |
68 |
CommitMsgShellHook, |
69 |
) |
70 |
|
71 |
from dulwich.refs import ( |
72 |
check_ref_format, |
73 |
RefsContainer, |
74 |
DictRefsContainer, |
75 |
InfoRefsContainer, |
76 |
DiskRefsContainer, |
77 |
read_packed_refs, |
78 |
read_packed_refs_with_peeled, |
79 |
write_packed_refs, |
80 |
SYMREF, |
81 |
) |
82 |
|
83 |
|
84 |
import warnings |
85 |
|
86 |
|
87 |
CONTROLDIR = '.git'
|
88 |
OBJECTDIR = 'objects'
|
89 |
REFSDIR = 'refs'
|
90 |
REFSDIR_TAGS = 'tags'
|
91 |
REFSDIR_HEADS = 'heads'
|
92 |
INDEX_FILENAME = "index"
|
93 |
COMMONDIR = 'commondir'
|
94 |
GITDIR = 'gitdir'
|
95 |
WORKTREES = 'worktrees'
|
96 |
|
97 |
BASE_DIRECTORIES = [ |
98 |
["branches"],
|
99 |
[REFSDIR], |
100 |
[REFSDIR, REFSDIR_TAGS], |
101 |
[REFSDIR, REFSDIR_HEADS], |
102 |
["hooks"],
|
103 |
["info"]
|
104 |
] |
105 |
|
106 |
DEFAULT_REF = b'refs/heads/master'
|
107 |
|
108 |
|
109 |
def parse_graftpoints(graftpoints): |
110 |
"""Convert a list of graftpoints into a dict
|
111 |
|
112 |
:param graftpoints: Iterator of graftpoint lines
|
113 |
|
114 |
Each line is formatted as:
|
115 |
<commit sha1> <parent sha1> [<parent sha1>]*
|
116 |
|
117 |
Resulting dictionary is:
|
118 |
<commit sha1>: [<parent sha1>*]
|
119 |
|
120 |
https://git.wiki.kernel.org/index.php/GraftPoint
|
121 |
"""
|
122 |
grafts = {} |
123 |
for l in graftpoints: |
124 |
raw_graft = l.split(None, 1) |
125 |
|
126 |
commit = raw_graft[0]
|
127 |
if len(raw_graft) == 2: |
128 |
parents = raw_graft[1].split()
|
129 |
else:
|
130 |
parents = [] |
131 |
|
132 |
for sha in [commit] + parents: |
133 |
check_hexsha(sha, 'Invalid graftpoint')
|
134 |
|
135 |
grafts[commit] = parents |
136 |
return grafts
|
137 |
|
138 |
|
139 |
def serialize_graftpoints(graftpoints): |
140 |
"""Convert a dictionary of grafts into string
|
141 |
|
142 |
The graft dictionary is:
|
143 |
<commit sha1>: [<parent sha1>*]
|
144 |
|
145 |
Each line is formatted as:
|
146 |
<commit sha1> <parent sha1> [<parent sha1>]*
|
147 |
|
148 |
https://git.wiki.kernel.org/index.php/GraftPoint
|
149 |
|
150 |
"""
|
151 |
graft_lines = [] |
152 |
for commit, parents in graftpoints.items(): |
153 |
if parents:
|
154 |
graft_lines.append(commit + b' ' + b' '.join(parents)) |
155 |
else:
|
156 |
graft_lines.append(commit) |
157 |
return b'\n'.join(graft_lines) |
158 |
|
159 |
|
160 |
class BaseRepo(object): |
161 |
"""Base class for a git repository.
|
162 |
|
163 |
:ivar object_store: Dictionary-like object for accessing
|
164 |
the objects
|
165 |
:ivar refs: Dictionary-like object with the refs in this
|
166 |
repository
|
167 |
"""
|
168 |
|
169 |
def __init__(self, object_store, refs): |
170 |
"""Open a repository.
|
171 |
|
172 |
This shouldn't be called directly, but rather through one of the
|
173 |
base classes, such as MemoryRepo or Repo.
|
174 |
|
175 |
:param object_store: Object store to use
|
176 |
:param refs: Refs container to use
|
177 |
"""
|
178 |
self.object_store = object_store
|
179 |
self.refs = refs
|
180 |
|
181 |
self._graftpoints = {}
|
182 |
self.hooks = {}
|
183 |
|
184 |
def _determine_file_mode(self): |
185 |
"""Probe the file-system to determine whether permissions can be trusted.
|
186 |
|
187 |
:return: True if permissions can be trusted, False otherwise.
|
188 |
"""
|
189 |
raise NotImplementedError(self._determine_file_mode) |
190 |
|
191 |
def _init_files(self, bare): |
192 |
"""Initialize a default set of named files."""
|
193 |
from dulwich.config import ConfigFile |
194 |
self._put_named_file('description', b"Unnamed repository") |
195 |
f = BytesIO() |
196 |
cf = ConfigFile() |
197 |
cf.set(b"core", b"repositoryformatversion", b"0") |
198 |
if self._determine_file_mode(): |
199 |
cf.set(b"core", b"filemode", True) |
200 |
else:
|
201 |
cf.set(b"core", b"filemode", False) |
202 |
|
203 |
cf.set(b"core", b"bare", bare) |
204 |
cf.set(b"core", b"logallrefupdates", True) |
205 |
cf.write_to_file(f) |
206 |
self._put_named_file('config', f.getvalue()) |
207 |
self._put_named_file(os.path.join('info', 'exclude'), b'') |
208 |
|
209 |
def get_named_file(self, path): |
210 |
"""Get a file from the control dir with a specific name.
|
211 |
|
212 |
Although the filename should be interpreted as a filename relative to
|
213 |
the control dir in a disk-based Repo, the object returned need not be
|
214 |
pointing to a file in that location.
|
215 |
|
216 |
:param path: The path to the file, relative to the control dir.
|
217 |
:return: An open file object, or None if the file does not exist.
|
218 |
"""
|
219 |
raise NotImplementedError(self.get_named_file) |
220 |
|
221 |
def _put_named_file(self, path, contents): |
222 |
"""Write a file to the control dir with the given name and contents.
|
223 |
|
224 |
:param path: The path to the file, relative to the control dir.
|
225 |
:param contents: A string to write to the file.
|
226 |
"""
|
227 |
raise NotImplementedError(self._put_named_file) |
228 |
|
229 |
def open_index(self): |
230 |
"""Open the index for this repository.
|
231 |
|
232 |
:raise NoIndexPresent: If no index is present
|
233 |
:return: The matching `Index`
|
234 |
"""
|
235 |
raise NotImplementedError(self.open_index) |
236 |
|
237 |
def fetch(self, target, determine_wants=None, progress=None): |
238 |
"""Fetch objects into another repository.
|
239 |
|
240 |
:param target: The target repository
|
241 |
:param determine_wants: Optional function to determine what refs to
|
242 |
fetch.
|
243 |
:param progress: Optional progress function
|
244 |
:return: The local refs
|
245 |
"""
|
246 |
if determine_wants is None: |
247 |
determine_wants = target.object_store.determine_wants_all |
248 |
target.object_store.add_objects( |
249 |
self.fetch_objects(determine_wants, target.get_graph_walker(),
|
250 |
progress)) |
251 |
return self.get_refs() |
252 |
|
253 |
def fetch_objects(self, determine_wants, graph_walker, progress, |
254 |
get_tagged=None):
|
255 |
"""Fetch the missing objects required for a set of revisions.
|
256 |
|
257 |
:param determine_wants: Function that takes a dictionary with heads
|
258 |
and returns the list of heads to fetch.
|
259 |
:param graph_walker: Object that can iterate over the list of revisions
|
260 |
to fetch and has an "ack" method that will be called to acknowledge
|
261 |
that a revision is present.
|
262 |
:param progress: Simple progress function that will be called with
|
263 |
updated progress strings.
|
264 |
:param get_tagged: Function that returns a dict of pointed-to sha -> tag
|
265 |
sha for including tags.
|
266 |
:return: iterator over objects, with __len__ implemented
|
267 |
"""
|
268 |
wants = determine_wants(self.get_refs())
|
269 |
if not isinstance(wants, list): |
270 |
raise TypeError("determine_wants() did not return a list") |
271 |
|
272 |
shallows = getattr(graph_walker, 'shallow', frozenset()) |
273 |
unshallows = getattr(graph_walker, 'unshallow', frozenset()) |
274 |
|
275 |
if wants == []:
|
276 |
# TODO(dborowitz): find a way to short-circuit that doesn't change
|
277 |
# this interface.
|
278 |
|
279 |
if shallows or unshallows: |
280 |
# Do not send a pack in shallow short-circuit path
|
281 |
return None |
282 |
|
283 |
return []
|
284 |
|
285 |
# If the graph walker is set up with an implementation that can
|
286 |
# ACK/NAK to the wire, it will write data to the client through
|
287 |
# this call as a side-effect.
|
288 |
haves = self.object_store.find_common_revisions(graph_walker)
|
289 |
|
290 |
# Deal with shallow requests separately because the haves do
|
291 |
# not reflect what objects are missing
|
292 |
if shallows or unshallows: |
293 |
haves = [] # TODO: filter the haves commits from iter_shas.
|
294 |
# the specific commits aren't missing.
|
295 |
|
296 |
def get_parents(commit): |
297 |
if commit.id in shallows: |
298 |
return []
|
299 |
return self.get_parents(commit.id, commit) |
300 |
|
301 |
return self.object_store.iter_shas( |
302 |
self.object_store.find_missing_objects(
|
303 |
haves, wants, progress, |
304 |
get_tagged, |
305 |
get_parents=get_parents)) |
306 |
|
307 |
def get_graph_walker(self, heads=None): |
308 |
"""Retrieve a graph walker.
|
309 |
|
310 |
A graph walker is used by a remote repository (or proxy)
|
311 |
to find out which objects are present in this repository.
|
312 |
|
313 |
:param heads: Repository heads to use (optional)
|
314 |
:return: A graph walker object
|
315 |
"""
|
316 |
if heads is None: |
317 |
heads = self.refs.as_dict(b'refs/heads').values() |
318 |
return ObjectStoreGraphWalker(heads, self.get_parents) |
319 |
|
320 |
def get_refs(self): |
321 |
"""Get dictionary with all refs.
|
322 |
|
323 |
:return: A ``dict`` mapping ref names to SHA1s
|
324 |
"""
|
325 |
return self.refs.as_dict() |
326 |
|
327 |
def head(self): |
328 |
"""Return the SHA1 pointed at by HEAD."""
|
329 |
return self.refs[b'HEAD'] |
330 |
|
331 |
def _get_object(self, sha, cls): |
332 |
assert len(sha) in (20, 40) |
333 |
ret = self.get_object(sha)
|
334 |
if not isinstance(ret, cls): |
335 |
if cls is Commit: |
336 |
raise NotCommitError(ret)
|
337 |
elif cls is Blob: |
338 |
raise NotBlobError(ret)
|
339 |
elif cls is Tree: |
340 |
raise NotTreeError(ret)
|
341 |
elif cls is Tag: |
342 |
raise NotTagError(ret)
|
343 |
else:
|
344 |
raise Exception("Type invalid: %r != %r" % ( |
345 |
ret.type_name, cls.type_name)) |
346 |
return ret
|
347 |
|
348 |
def get_object(self, sha): |
349 |
"""Retrieve the object with the specified SHA.
|
350 |
|
351 |
:param sha: SHA to retrieve
|
352 |
:return: A ShaFile object
|
353 |
:raise KeyError: when the object can not be found
|
354 |
"""
|
355 |
return self.object_store[sha] |
356 |
|
357 |
def get_parents(self, sha, commit=None): |
358 |
"""Retrieve the parents of a specific commit.
|
359 |
|
360 |
If the specific commit is a graftpoint, the graft parents
|
361 |
will be returned instead.
|
362 |
|
363 |
:param sha: SHA of the commit for which to retrieve the parents
|
364 |
:param commit: Optional commit matching the sha
|
365 |
:return: List of parents
|
366 |
"""
|
367 |
|
368 |
try:
|
369 |
return self._graftpoints[sha] |
370 |
except KeyError: |
371 |
if commit is None: |
372 |
commit = self[sha]
|
373 |
return commit.parents
|
374 |
|
375 |
def get_config(self): |
376 |
"""Retrieve the config object.
|
377 |
|
378 |
:return: `ConfigFile` object for the ``.git/config`` file.
|
379 |
"""
|
380 |
raise NotImplementedError(self.get_config) |
381 |
|
382 |
def get_description(self): |
383 |
"""Retrieve the description for this repository.
|
384 |
|
385 |
:return: String with the description of the repository
|
386 |
as set by the user.
|
387 |
"""
|
388 |
raise NotImplementedError(self.get_description) |
389 |
|
390 |
def set_description(self, description): |
391 |
"""Set the description for this repository.
|
392 |
|
393 |
:param description: Text to set as description for this repository.
|
394 |
"""
|
395 |
raise NotImplementedError(self.set_description) |
396 |
|
397 |
def get_config_stack(self): |
398 |
"""Return a config stack for this repository.
|
399 |
|
400 |
This stack accesses the configuration for both this repository
|
401 |
itself (.git/config) and the global configuration, which usually
|
402 |
lives in ~/.gitconfig.
|
403 |
|
404 |
:return: `Config` instance for this repository
|
405 |
"""
|
406 |
from dulwich.config import StackedConfig |
407 |
backends = [self.get_config()] + StackedConfig.default_backends()
|
408 |
return StackedConfig(backends, writable=backends[0]) |
409 |
|
410 |
def get_peeled(self, ref): |
411 |
"""Get the peeled value of a ref.
|
412 |
|
413 |
:param ref: The refname to peel.
|
414 |
:return: The fully-peeled SHA1 of a tag object, after peeling all
|
415 |
intermediate tags; if the original ref does not point to a tag, this
|
416 |
will equal the original SHA1.
|
417 |
"""
|
418 |
cached = self.refs.get_peeled(ref)
|
419 |
if cached is not None: |
420 |
return cached
|
421 |
return self.object_store.peel_sha(self.refs[ref]).id |
422 |
|
423 |
def get_walker(self, include=None, *args, **kwargs): |
424 |
"""Obtain a walker for this repository.
|
425 |
|
426 |
:param include: Iterable of SHAs of commits to include along with their
|
427 |
ancestors. Defaults to [HEAD]
|
428 |
:param exclude: Iterable of SHAs of commits to exclude along with their
|
429 |
ancestors, overriding includes.
|
430 |
:param order: ORDER_* constant specifying the order of results. Anything
|
431 |
other than ORDER_DATE may result in O(n) memory usage.
|
432 |
:param reverse: If True, reverse the order of output, requiring O(n)
|
433 |
memory.
|
434 |
:param max_entries: The maximum number of entries to yield, or None for
|
435 |
no limit.
|
436 |
:param paths: Iterable of file or subtree paths to show entries for.
|
437 |
:param rename_detector: diff.RenameDetector object for detecting
|
438 |
renames.
|
439 |
:param follow: If True, follow path across renames/copies. Forces a
|
440 |
default rename_detector.
|
441 |
:param since: Timestamp to list commits after.
|
442 |
:param until: Timestamp to list commits before.
|
443 |
:param queue_cls: A class to use for a queue of commits, supporting the
|
444 |
iterator protocol. The constructor takes a single argument, the
|
445 |
Walker.
|
446 |
:return: A `Walker` object
|
447 |
"""
|
448 |
from dulwich.walk import Walker |
449 |
if include is None: |
450 |
include = [self.head()]
|
451 |
if isinstance(include, str): |
452 |
include = [include] |
453 |
|
454 |
kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit) |
455 |
|
456 |
return Walker(self.object_store, include, *args, **kwargs) |
457 |
|
458 |
def __getitem__(self, name): |
459 |
"""Retrieve a Git object by SHA1 or ref.
|
460 |
|
461 |
:param name: A Git object SHA1 or a ref name
|
462 |
:return: A `ShaFile` object, such as a Commit or Blob
|
463 |
:raise KeyError: when the specified ref or object does not exist
|
464 |
"""
|
465 |
if not isinstance(name, bytes): |
466 |
raise TypeError("'name' must be bytestring, not %.80s" % |
467 |
type(name).__name__)
|
468 |
if len(name) in (20, 40): |
469 |
try:
|
470 |
return self.object_store[name] |
471 |
except (KeyError, ValueError): |
472 |
pass
|
473 |
try:
|
474 |
return self.object_store[self.refs[name]] |
475 |
except RefFormatError:
|
476 |
raise KeyError(name) |
477 |
|
478 |
def __contains__(self, name): |
479 |
"""Check if a specific Git object or ref is present.
|
480 |
|
481 |
:param name: Git object SHA1 or ref name
|
482 |
"""
|
483 |
if len(name) in (20, 40): |
484 |
return name in self.object_store or name in self.refs |
485 |
else:
|
486 |
return name in self.refs |
487 |
|
488 |
def __setitem__(self, name, value): |
489 |
"""Set a ref.
|
490 |
|
491 |
:param name: ref name
|
492 |
:param value: Ref value - either a ShaFile object, or a hex sha
|
493 |
"""
|
494 |
if name.startswith(b"refs/") or name == b'HEAD': |
495 |
if isinstance(value, ShaFile): |
496 |
self.refs[name] = value.id
|
497 |
elif isinstance(value, bytes): |
498 |
self.refs[name] = value
|
499 |
else:
|
500 |
raise TypeError(value) |
501 |
else:
|
502 |
raise ValueError(name) |
503 |
|
504 |
def __delitem__(self, name): |
505 |
"""Remove a ref.
|
506 |
|
507 |
:param name: Name of the ref to remove
|
508 |
"""
|
509 |
if name.startswith(b"refs/") or name == b"HEAD": |
510 |
del self.refs[name] |
511 |
else:
|
512 |
raise ValueError(name) |
513 |
|
514 |
def _get_user_identity(self): |
515 |
"""Determine the identity to use for new commits.
|
516 |
"""
|
517 |
config = self.get_config_stack()
|
518 |
return (config.get((b"user", ), b"name") + b" <" + |
519 |
config.get((b"user", ), b"email") + b">") |
520 |
|
521 |
def _add_graftpoints(self, updated_graftpoints): |
522 |
"""Add or modify graftpoints
|
523 |
|
524 |
:param updated_graftpoints: Dict of commit shas to list of parent shas
|
525 |
"""
|
526 |
|
527 |
# Simple validation
|
528 |
for commit, parents in updated_graftpoints.items(): |
529 |
for sha in [commit] + parents: |
530 |
check_hexsha(sha, 'Invalid graftpoint')
|
531 |
|
532 |
self._graftpoints.update(updated_graftpoints)
|
533 |
|
534 |
def _remove_graftpoints(self, to_remove=[]): |
535 |
"""Remove graftpoints
|
536 |
|
537 |
:param to_remove: List of commit shas
|
538 |
"""
|
539 |
for sha in to_remove: |
540 |
del self._graftpoints[sha] |
541 |
|
542 |
def do_commit(self, message=None, committer=None, |
543 |
author=None, commit_timestamp=None, |
544 |
commit_timezone=None, author_timestamp=None, |
545 |
author_timezone=None, tree=None, encoding=None, |
546 |
ref=b'HEAD', merge_heads=None): |
547 |
"""Create a new commit.
|
548 |
|
549 |
:param message: Commit message
|
550 |
:param committer: Committer fullname
|
551 |
:param author: Author fullname (defaults to committer)
|
552 |
:param commit_timestamp: Commit timestamp (defaults to now)
|
553 |
:param commit_timezone: Commit timestamp timezone (defaults to GMT)
|
554 |
:param author_timestamp: Author timestamp (defaults to commit timestamp)
|
555 |
:param author_timezone: Author timestamp timezone
|
556 |
(defaults to commit timestamp timezone)
|
557 |
:param tree: SHA1 of the tree root to use (if not specified the
|
558 |
current index will be committed).
|
559 |
:param encoding: Encoding
|
560 |
:param ref: Optional ref to commit to (defaults to current branch)
|
561 |
:param merge_heads: Merge heads (defaults to .git/MERGE_HEADS)
|
562 |
:return: New commit SHA1
|
563 |
"""
|
564 |
import time |
565 |
c = Commit() |
566 |
if tree is None: |
567 |
index = self.open_index()
|
568 |
c.tree = index.commit(self.object_store)
|
569 |
else:
|
570 |
if len(tree) != 40: |
571 |
raise ValueError("tree must be a 40-byte hex sha string") |
572 |
c.tree = tree |
573 |
|
574 |
try:
|
575 |
self.hooks['pre-commit'].execute() |
576 |
except HookError as e: |
577 |
raise CommitError(e)
|
578 |
except KeyError: # no hook defined, silent fallthrough |
579 |
pass
|
580 |
|
581 |
if merge_heads is None: |
582 |
# FIXME: Read merge heads from .git/MERGE_HEADS
|
583 |
merge_heads = [] |
584 |
if committer is None: |
585 |
# FIXME: Support GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL environment
|
586 |
# variables
|
587 |
committer = self._get_user_identity()
|
588 |
c.committer = committer |
589 |
if commit_timestamp is None: |
590 |
# FIXME: Support GIT_COMMITTER_DATE environment variable
|
591 |
commit_timestamp = time.time() |
592 |
c.commit_time = int(commit_timestamp)
|
593 |
if commit_timezone is None: |
594 |
# FIXME: Use current user timezone rather than UTC
|
595 |
commit_timezone = 0
|
596 |
c.commit_timezone = commit_timezone |
597 |
if author is None: |
598 |
# FIXME: Support GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL environment
|
599 |
# variables
|
600 |
author = committer |
601 |
c.author = author |
602 |
if author_timestamp is None: |
603 |
# FIXME: Support GIT_AUTHOR_DATE environment variable
|
604 |
author_timestamp = commit_timestamp |
605 |
c.author_time = int(author_timestamp)
|
606 |
if author_timezone is None: |
607 |
author_timezone = commit_timezone |
608 |
c.author_timezone = author_timezone |
609 |
if encoding is not None: |
610 |
c.encoding = encoding |
611 |
if message is None: |
612 |
# FIXME: Try to read commit message from .git/MERGE_MSG
|
613 |
raise ValueError("No commit message specified") |
614 |
|
615 |
try:
|
616 |
c.message = self.hooks['commit-msg'].execute(message) |
617 |
if c.message is None: |
618 |
c.message = message |
619 |
except HookError as e: |
620 |
raise CommitError(e)
|
621 |
except KeyError: # no hook defined, message not modified |
622 |
c.message = message |
623 |
|
624 |
if ref is None: |
625 |
# Create a dangling commit
|
626 |
c.parents = merge_heads |
627 |
self.object_store.add_object(c)
|
628 |
else:
|
629 |
try:
|
630 |
old_head = self.refs[ref]
|
631 |
c.parents = [old_head] + merge_heads |
632 |
self.object_store.add_object(c)
|
633 |
ok = self.refs.set_if_equals(ref, old_head, c.id)
|
634 |
except KeyError: |
635 |
c.parents = merge_heads |
636 |
self.object_store.add_object(c)
|
637 |
ok = self.refs.add_if_new(ref, c.id)
|
638 |
if not ok: |
639 |
# Fail if the atomic compare-and-swap failed, leaving the commit and
|
640 |
# all its objects as garbage.
|
641 |
raise CommitError("%s changed during commit" % (ref,)) |
642 |
|
643 |
try:
|
644 |
self.hooks['post-commit'].execute() |
645 |
except HookError as e: # silent failure |
646 |
warnings.warn("post-commit hook failed: %s" % e, UserWarning) |
647 |
except KeyError: # no hook defined, silent fallthrough |
648 |
pass
|
649 |
|
650 |
return c.id
|
651 |
|
652 |
|
653 |
|
654 |
def read_gitfile(f): |
655 |
"""Read a ``.git`` file.
|
656 |
|
657 |
The first line of the file should start with "gitdir: "
|
658 |
|
659 |
:param f: File-like object to read from
|
660 |
:return: A path
|
661 |
"""
|
662 |
cs = f.read() |
663 |
if not cs.startswith("gitdir: "): |
664 |
raise ValueError("Expected file to start with 'gitdir: '") |
665 |
return cs[len("gitdir: "):].rstrip("\n") |
666 |
|
667 |
|
668 |
class Repo(BaseRepo): |
669 |
"""A git repository backed by local disk.
|
670 |
|
671 |
To open an existing repository, call the contructor with
|
672 |
the path of the repository.
|
673 |
|
674 |
To create a new repository, use the Repo.init class method.
|
675 |
"""
|
676 |
|
677 |
def __init__(self, root): |
678 |
hidden_path = os.path.join(root, CONTROLDIR) |
679 |
if os.path.isdir(os.path.join(hidden_path, OBJECTDIR)):
|
680 |
self.bare = False |
681 |
self._controldir = hidden_path
|
682 |
elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and |
683 |
os.path.isdir(os.path.join(root, REFSDIR))): |
684 |
self.bare = True |
685 |
self._controldir = root
|
686 |
elif os.path.isfile(hidden_path):
|
687 |
self.bare = False |
688 |
with open(hidden_path, 'r') as f: |
689 |
path = read_gitfile(f) |
690 |
self.bare = False |
691 |
self._controldir = os.path.join(root, path)
|
692 |
else:
|
693 |
raise NotGitRepository(
|
694 |
"No git repository was found at %(path)s" % dict(path=root) |
695 |
) |
696 |
commondir = self.get_named_file(COMMONDIR)
|
697 |
if commondir is not None: |
698 |
with commondir:
|
699 |
self._commondir = os.path.join(
|
700 |
self.controldir(),
|
701 |
commondir.read().rstrip(b"\r\n").decode(sys.getfilesystemencoding()))
|
702 |
else:
|
703 |
self._commondir = self._controldir |
704 |
self.path = root
|
705 |
object_store = DiskObjectStore( |
706 |
os.path.join(self.commondir(), OBJECTDIR))
|
707 |
refs = DiskRefsContainer(self.commondir(), self._controldir) |
708 |
BaseRepo.__init__(self, object_store, refs)
|
709 |
|
710 |
self._graftpoints = {}
|
711 |
graft_file = self.get_named_file(os.path.join("info", "grafts"), |
712 |
basedir=self.commondir())
|
713 |
if graft_file:
|
714 |
with graft_file:
|
715 |
self._graftpoints.update(parse_graftpoints(graft_file))
|
716 |
graft_file = self.get_named_file("shallow", |
717 |
basedir=self.commondir())
|
718 |
if graft_file:
|
719 |
with graft_file:
|
720 |
self._graftpoints.update(parse_graftpoints(graft_file))
|
721 |
|
722 |
self.hooks['pre-commit'] = PreCommitShellHook(self.controldir()) |
723 |
self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir()) |
724 |
self.hooks['post-commit'] = PostCommitShellHook(self.controldir()) |
725 |
|
726 |
@classmethod
|
727 |
def discover(cls, start='.'): |
728 |
"""Iterate parent directories to discover a repository
|
729 |
|
730 |
Return a Repo object for the first parent directory that looks like a
|
731 |
Git repository.
|
732 |
|
733 |
:param start: The directory to start discovery from (defaults to '.')
|
734 |
"""
|
735 |
remaining = True
|
736 |
path = os.path.abspath(start) |
737 |
while remaining:
|
738 |
try:
|
739 |
return cls(path)
|
740 |
except NotGitRepository:
|
741 |
path, remaining = os.path.split(path) |
742 |
raise NotGitRepository(
|
743 |
"No git repository was found at %(path)s" % dict(path=start) |
744 |
) |
745 |
|
746 |
def controldir(self): |
747 |
"""Return the path of the control directory."""
|
748 |
return self._controldir |
749 |
|
750 |
def commondir(self): |
751 |
"""Return the path of the common directory.
|
752 |
|
753 |
For a main working tree, it is identical to `controldir()`.
|
754 |
|
755 |
For a linked working tree, it is the control directory of the
|
756 |
main working tree."""
|
757 |
|
758 |
return self._commondir |
759 |
|
760 |
def _determine_file_mode(self): |
761 |
"""Probe the file-system to determine whether permissions can be trusted.
|
762 |
|
763 |
:return: True if permissions can be trusted, False otherwise.
|
764 |
"""
|
765 |
fname = os.path.join(self.path, '.probe-permissions') |
766 |
with open(fname, 'w') as f: |
767 |
f.write('')
|
768 |
|
769 |
st1 = os.lstat(fname) |
770 |
os.chmod(fname, st1.st_mode ^ stat.S_IXUSR) |
771 |
st2 = os.lstat(fname) |
772 |
|
773 |
os.unlink(fname) |
774 |
|
775 |
mode_differs = st1.st_mode != st2.st_mode |
776 |
st2_has_exec = (st2.st_mode & stat.S_IXUSR) != 0
|
777 |
|
778 |
return mode_differs and st2_has_exec |
779 |
|
780 |
def _put_named_file(self, path, contents): |
781 |
"""Write a file to the control dir with the given name and contents.
|
782 |
|
783 |
:param path: The path to the file, relative to the control dir.
|
784 |
:param contents: A string to write to the file.
|
785 |
"""
|
786 |
path = path.lstrip(os.path.sep) |
787 |
with GitFile(os.path.join(self.controldir(), path), 'wb') as f: |
788 |
f.write(contents) |
789 |
|
790 |
def get_named_file(self, path, basedir=None): |
791 |
"""Get a file from the control dir with a specific name.
|
792 |
|
793 |
Although the filename should be interpreted as a filename relative to
|
794 |
the control dir in a disk-based Repo, the object returned need not be
|
795 |
pointing to a file in that location.
|
796 |
|
797 |
:param path: The path to the file, relative to the control dir.
|
798 |
:param basedir: Optional argument that specifies an alternative to the control dir.
|
799 |
:return: An open file object, or None if the file does not exist.
|
800 |
"""
|
801 |
# TODO(dborowitz): sanitize filenames, since this is used directly by
|
802 |
# the dumb web serving code.
|
803 |
if basedir is None: |
804 |
basedir = self.controldir()
|
805 |
path = path.lstrip(os.path.sep) |
806 |
try:
|
807 |
return open(os.path.join(basedir, path), 'rb') |
808 |
except (IOError, OSError) as e: |
809 |
if e.errno == errno.ENOENT:
|
810 |
return None |
811 |
raise
|
812 |
|
813 |
def index_path(self): |
814 |
"""Return path to the index file."""
|
815 |
return os.path.join(self.controldir(), INDEX_FILENAME) |
816 |
|
817 |
def open_index(self): |
818 |
"""Open the index for this repository.
|
819 |
|
820 |
:raise NoIndexPresent: If no index is present
|
821 |
:return: The matching `Index`
|
822 |
"""
|
823 |
from dulwich.index import Index |
824 |
if not self.has_index(): |
825 |
raise NoIndexPresent()
|
826 |
return Index(self.index_path()) |
827 |
|
828 |
def has_index(self): |
829 |
"""Check if an index is present."""
|
830 |
# Bare repos must never have index files; non-bare repos may have a
|
831 |
# missing index file, which is treated as empty.
|
832 |
return not self.bare |
833 |
|
834 |
def stage(self, fs_paths): |
835 |
"""Stage a set of paths.
|
836 |
|
837 |
:param fs_paths: List of paths, relative to the repository path
|
838 |
"""
|
839 |
|
840 |
root_path_bytes = self.path.encode(sys.getfilesystemencoding())
|
841 |
|
842 |
if not isinstance(fs_paths, list): |
843 |
fs_paths = [fs_paths] |
844 |
from dulwich.index import ( |
845 |
blob_from_path_and_stat, |
846 |
index_entry_from_stat, |
847 |
_fs_to_tree_path, |
848 |
) |
849 |
index = self.open_index()
|
850 |
for fs_path in fs_paths: |
851 |
if not isinstance(fs_path, bytes): |
852 |
fs_path = fs_path.encode(sys.getfilesystemencoding()) |
853 |
tree_path = _fs_to_tree_path(fs_path) |
854 |
full_path = os.path.join(root_path_bytes, fs_path) |
855 |
try:
|
856 |
st = os.lstat(full_path) |
857 |
except OSError: |
858 |
# File no longer exists
|
859 |
try:
|
860 |
del index[tree_path]
|
861 |
except KeyError: |
862 |
pass # already removed |
863 |
else:
|
864 |
blob = blob_from_path_and_stat(full_path, st) |
865 |
self.object_store.add_object(blob)
|
866 |
index[tree_path] = index_entry_from_stat(st, blob.id, 0)
|
867 |
index.write() |
868 |
|
869 |
def clone(self, target_path, mkdir=True, bare=False, |
870 |
origin=b"origin"):
|
871 |
"""Clone this repository.
|
872 |
|
873 |
:param target_path: Target path
|
874 |
:param mkdir: Create the target directory
|
875 |
:param bare: Whether to create a bare repository
|
876 |
:param origin: Base name for refs in target repository
|
877 |
cloned from this repository
|
878 |
:return: Created repository as `Repo`
|
879 |
"""
|
880 |
if not bare: |
881 |
target = self.init(target_path, mkdir=mkdir)
|
882 |
else:
|
883 |
target = self.init_bare(target_path)
|
884 |
self.fetch(target)
|
885 |
target.refs.import_refs( |
886 |
b'refs/remotes/' + origin, self.refs.as_dict(b'refs/heads')) |
887 |
target.refs.import_refs( |
888 |
b'refs/tags', self.refs.as_dict(b'refs/tags')) |
889 |
try:
|
890 |
target.refs.add_if_new(DEFAULT_REF, self.refs[DEFAULT_REF])
|
891 |
except KeyError: |
892 |
pass
|
893 |
target_config = target.get_config() |
894 |
encoded_path = self.path
|
895 |
if not isinstance(encoded_path, bytes): |
896 |
encoded_path = encoded_path.encode(sys.getfilesystemencoding()) |
897 |
target_config.set((b'remote', b'origin'), b'url', encoded_path) |
898 |
target_config.set((b'remote', b'origin'), b'fetch', |
899 |
b'+refs/heads/*:refs/remotes/origin/*')
|
900 |
target_config.write_to_path() |
901 |
|
902 |
# Update target head
|
903 |
head_chain, head_sha = self.refs.follow(b'HEAD') |
904 |
if head_chain and head_sha is not None: |
905 |
target.refs.set_symbolic_ref(b'HEAD', head_chain[-1]) |
906 |
target[b'HEAD'] = head_sha
|
907 |
|
908 |
if not bare: |
909 |
# Checkout HEAD to target dir
|
910 |
target.reset_index() |
911 |
|
912 |
return target
|
913 |
|
914 |
def reset_index(self, tree=None): |
915 |
"""Reset the index back to a specific tree.
|
916 |
|
917 |
:param tree: Tree SHA to reset to, None for current HEAD tree.
|
918 |
"""
|
919 |
from dulwich.index import ( |
920 |
build_index_from_tree, |
921 |
validate_path_element_default, |
922 |
validate_path_element_ntfs, |
923 |
) |
924 |
if tree is None: |
925 |
tree = self[b'HEAD'].tree |
926 |
config = self.get_config()
|
927 |
honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt") |
928 |
if config.get_boolean('core', 'core.protectNTFS', os.name == "nt"): |
929 |
validate_path_element = validate_path_element_ntfs |
930 |
else:
|
931 |
validate_path_element = validate_path_element_default |
932 |
return build_index_from_tree(self.path, self.index_path(), |
933 |
self.object_store, tree, honor_filemode=honor_filemode,
|
934 |
validate_path_element=validate_path_element) |
935 |
|
936 |
def get_config(self): |
937 |
"""Retrieve the config object.
|
938 |
|
939 |
:return: `ConfigFile` object for the ``.git/config`` file.
|
940 |
"""
|
941 |
from dulwich.config import ConfigFile |
942 |
path = os.path.join(self._controldir, 'config') |
943 |
try:
|
944 |
return ConfigFile.from_path(path)
|
945 |
except (IOError, OSError) as e: |
946 |
if e.errno != errno.ENOENT:
|
947 |
raise
|
948 |
ret = ConfigFile() |
949 |
ret.path = path |
950 |
return ret
|
951 |
|
952 |
def get_description(self): |
953 |
"""Retrieve the description of this repository.
|
954 |
|
955 |
:return: A string describing the repository or None.
|
956 |
"""
|
957 |
path = os.path.join(self._controldir, 'description') |
958 |
try:
|
959 |
with GitFile(path, 'rb') as f: |
960 |
return f.read()
|
961 |
except (IOError, OSError) as e: |
962 |
if e.errno != errno.ENOENT:
|
963 |
raise
|
964 |
return None |
965 |
|
966 |
def __repr__(self): |
967 |
return "<Repo at %r>" % self.path |
968 |
|
969 |
def set_description(self, description): |
970 |
"""Set the description for this repository.
|
971 |
|
972 |
:param description: Text to set as description for this repository.
|
973 |
"""
|
974 |
|
975 |
self._put_named_file('description', description) |
976 |
|
977 |
@classmethod
|
978 |
def _init_maybe_bare(cls, path, bare): |
979 |
for d in BASE_DIRECTORIES: |
980 |
os.mkdir(os.path.join(path, *d)) |
981 |
DiskObjectStore.init(os.path.join(path, OBJECTDIR)) |
982 |
ret = cls(path) |
983 |
ret.refs.set_symbolic_ref(b'HEAD', DEFAULT_REF)
|
984 |
ret._init_files(bare) |
985 |
return ret
|
986 |
|
987 |
@classmethod
|
988 |
def init(cls, path, mkdir=False): |
989 |
"""Create a new repository.
|
990 |
|
991 |
:param path: Path in which to create the repository
|
992 |
:param mkdir: Whether to create the directory
|
993 |
:return: `Repo` instance
|
994 |
"""
|
995 |
if mkdir:
|
996 |
os.mkdir(path) |
997 |
controldir = os.path.join(path, CONTROLDIR) |
998 |
os.mkdir(controldir) |
999 |
cls._init_maybe_bare(controldir, False)
|
1000 |
return cls(path)
|
1001 |
|
1002 |
@classmethod
|
1003 |
def _init_new_working_directory(cls, path, main_repo, identifier=None, mkdir=False): |
1004 |
"""Create a new working directory linked to a repository.
|
1005 |
|
1006 |
:param path: Path in which to create the working tree.
|
1007 |
:param main_repo: Main repository to reference
|
1008 |
:param identifier: Worktree identifier
|
1009 |
:param mkdir: Whether to create the directory
|
1010 |
:return: `Repo` instance
|
1011 |
"""
|
1012 |
if mkdir:
|
1013 |
os.mkdir(path) |
1014 |
if identifier is None: |
1015 |
identifier = os.path.basename(path) |
1016 |
main_worktreesdir = os.path.join(main_repo.controldir(), WORKTREES) |
1017 |
worktree_controldir = os.path.join(main_worktreesdir, identifier) |
1018 |
gitdirfile = os.path.join(path, CONTROLDIR) |
1019 |
with open(gitdirfile, 'wb') as f: |
1020 |
f.write(b'gitdir: ' +
|
1021 |
worktree_controldir.encode(sys.getfilesystemencoding()) + |
1022 |
b'\n')
|
1023 |
try:
|
1024 |
os.mkdir(main_worktreesdir) |
1025 |
except OSError as e: |
1026 |
if e.errno != errno.EEXIST:
|
1027 |
raise
|
1028 |
try:
|
1029 |
os.mkdir(worktree_controldir) |
1030 |
except OSError as e: |
1031 |
if e.errno != errno.EEXIST:
|
1032 |
raise
|
1033 |
with open(os.path.join(worktree_controldir, GITDIR), 'wb') as f: |
1034 |
f.write(gitdirfile.encode(sys.getfilesystemencoding()) + b'\n')
|
1035 |
with open(os.path.join(worktree_controldir, COMMONDIR), 'wb') as f: |
1036 |
f.write(b'../..\n')
|
1037 |
with open(os.path.join(worktree_controldir, 'HEAD'), 'wb') as f: |
1038 |
f.write(main_repo.head() + b'\n')
|
1039 |
r = cls(path) |
1040 |
r.reset_index() |
1041 |
return r
|
1042 |
|
1043 |
@classmethod
|
1044 |
def init_bare(cls, path): |
1045 |
"""Create a new bare repository.
|
1046 |
|
1047 |
``path`` should already exist and be an emty directory.
|
1048 |
|
1049 |
:param path: Path to create bare repository in
|
1050 |
:return: a `Repo` instance
|
1051 |
"""
|
1052 |
return cls._init_maybe_bare(path, True) |
1053 |
|
1054 |
create = init_bare |
1055 |
|
1056 |
def close(self): |
1057 |
"""Close any files opened by this repository."""
|
1058 |
self.object_store.close()
|
1059 |
|
1060 |
def __enter__(self): |
1061 |
return self |
1062 |
|
1063 |
def __exit__(self, exc_type, exc_val, exc_tb): |
1064 |
self.close()
|
1065 |
|
1066 |
|
1067 |
class MemoryRepo(BaseRepo): |
1068 |
"""Repo that stores refs, objects, and named files in memory.
|
1069 |
|
1070 |
MemoryRepos are always bare: they have no working tree and no index, since
|
1071 |
those have a stronger dependency on the filesystem.
|
1072 |
"""
|
1073 |
|
1074 |
def __init__(self): |
1075 |
from dulwich.config import ConfigFile |
1076 |
BaseRepo.__init__(self, MemoryObjectStore(), DictRefsContainer({}))
|
1077 |
self._named_files = {}
|
1078 |
self.bare = True |
1079 |
self._config = ConfigFile()
|
1080 |
|
1081 |
def _determine_file_mode(self): |
1082 |
"""Probe the file-system to determine whether permissions can be trusted.
|
1083 |
|
1084 |
:return: True if permissions can be trusted, False otherwise.
|
1085 |
"""
|
1086 |
return sys.platform != 'win32' |
1087 |
|
1088 |
def _put_named_file(self, path, contents): |
1089 |
"""Write a file to the control dir with the given name and contents.
|
1090 |
|
1091 |
:param path: The path to the file, relative to the control dir.
|
1092 |
:param contents: A string to write to the file.
|
1093 |
"""
|
1094 |
self._named_files[path] = contents
|
1095 |
|
1096 |
def get_named_file(self, path): |
1097 |
"""Get a file from the control dir with a specific name.
|
1098 |
|
1099 |
Although the filename should be interpreted as a filename relative to
|
1100 |
the control dir in a disk-baked Repo, the object returned need not be
|
1101 |
pointing to a file in that location.
|
1102 |
|
1103 |
:param path: The path to the file, relative to the control dir.
|
1104 |
:return: An open file object, or None if the file does not exist.
|
1105 |
"""
|
1106 |
contents = self._named_files.get(path, None) |
1107 |
if contents is None: |
1108 |
return None |
1109 |
return BytesIO(contents)
|
1110 |
|
1111 |
def open_index(self): |
1112 |
"""Fail to open index for this repo, since it is bare.
|
1113 |
|
1114 |
:raise NoIndexPresent: Raised when no index is present
|
1115 |
"""
|
1116 |
raise NoIndexPresent()
|
1117 |
|
1118 |
def get_config(self): |
1119 |
"""Retrieve the config object.
|
1120 |
|
1121 |
:return: `ConfigFile` object.
|
1122 |
"""
|
1123 |
return self._config |
1124 |
|
1125 |
def get_description(self): |
1126 |
"""Retrieve the repository description.
|
1127 |
|
1128 |
This defaults to None, for no description.
|
1129 |
"""
|
1130 |
return None |
1131 |
|
1132 |
@classmethod
|
1133 |
def init_bare(cls, objects, refs): |
1134 |
"""Create a new bare repository in memory.
|
1135 |
|
1136 |
:param objects: Objects for the new repository,
|
1137 |
as iterable
|
1138 |
:param refs: Refs as dictionary, mapping names
|
1139 |
to object SHA1s
|
1140 |
"""
|
1141 |
ret = cls() |
1142 |
for obj in objects: |
1143 |
ret.object_store.add_object(obj) |
1144 |
for refname, sha in refs.items(): |
1145 |
ret.refs[refname] = sha |
1146 |
ret._init_files(bare=True)
|
1147 |
return ret
|