repo.py 48 KB

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