porcelain.py 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120
  1. # porcelain.py -- Porcelain-like layer on top of Dulwich
  2. # Copyright (C) 2013 Jelmer Vernooij <jelmer@jelmer.uk>
  3. #
  4. # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
  5. # General Public License as public by the Free Software Foundation; version 2.0
  6. # or (at your option) any later version. You can redistribute it and/or
  7. # modify it under the terms of either of these two licenses.
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. #
  15. # You should have received a copy of the licenses; if not, see
  16. # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
  17. # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
  18. # License, Version 2.0.
  19. #
  20. """Simple wrapper that provides porcelain-like functions on top of Dulwich.
  21. Currently implemented:
  22. * archive
  23. * add
  24. * branch{_create,_delete,_list}
  25. * check-ignore
  26. * checkout
  27. * clone
  28. * commit
  29. * commit-tree
  30. * daemon
  31. * describe
  32. * diff-tree
  33. * fetch
  34. * init
  35. * ls-files
  36. * ls-remote
  37. * ls-tree
  38. * pull
  39. * push
  40. * rm
  41. * remote{_add}
  42. * receive-pack
  43. * reset
  44. * submodule_add
  45. * submodule_init
  46. * submodule_list
  47. * rev-list
  48. * tag{_create,_delete,_list}
  49. * upload-pack
  50. * update-server-info
  51. * status
  52. * symbolic-ref
  53. These functions are meant to behave similarly to the git subcommands.
  54. Differences in behaviour are considered bugs.
  55. Note: one of the consequences of this is that paths tend to be
  56. interpreted relative to the current working directory rather than relative
  57. to the repository root.
  58. Functions should generally accept both unicode strings and bytestrings
  59. """
  60. from collections import namedtuple
  61. from contextlib import (
  62. closing,
  63. contextmanager,
  64. )
  65. from io import BytesIO, RawIOBase
  66. import datetime
  67. import os
  68. from pathlib import Path
  69. import posixpath
  70. import stat
  71. import sys
  72. import time
  73. from typing import (
  74. Optional,
  75. Tuple,
  76. Union,
  77. )
  78. from dulwich.archive import (
  79. tar_stream,
  80. )
  81. from dulwich.client import (
  82. get_transport_and_path,
  83. )
  84. from dulwich.config import (
  85. Config,
  86. ConfigFile,
  87. StackedConfig,
  88. read_submodules,
  89. )
  90. from dulwich.diff_tree import (
  91. CHANGE_ADD,
  92. CHANGE_DELETE,
  93. CHANGE_MODIFY,
  94. CHANGE_RENAME,
  95. CHANGE_COPY,
  96. RENAME_CHANGE_TYPES,
  97. )
  98. from dulwich.errors import (
  99. SendPackError,
  100. )
  101. from dulwich.graph import (
  102. can_fast_forward,
  103. )
  104. from dulwich.ignore import IgnoreFilterManager
  105. from dulwich.index import (
  106. blob_from_path_and_stat,
  107. get_unstaged_changes,
  108. build_file_from_blob,
  109. _fs_to_tree_path,
  110. )
  111. from dulwich.object_store import (
  112. tree_lookup_path,
  113. )
  114. from dulwich.objects import (
  115. Commit,
  116. Tag,
  117. format_timezone,
  118. parse_timezone,
  119. pretty_format_tree_entry,
  120. )
  121. from dulwich.objectspec import (
  122. parse_commit,
  123. parse_object,
  124. parse_ref,
  125. parse_reftuples,
  126. parse_tree,
  127. )
  128. from dulwich.pack import (
  129. write_pack_index,
  130. write_pack_objects,
  131. )
  132. from dulwich.patch import write_tree_diff
  133. from dulwich.protocol import (
  134. Protocol,
  135. ZERO_SHA,
  136. )
  137. from dulwich.refs import (
  138. LOCAL_BRANCH_PREFIX,
  139. LOCAL_TAG_PREFIX,
  140. _import_remote_refs,
  141. )
  142. from dulwich.repo import BaseRepo, Repo
  143. from dulwich.server import (
  144. FileSystemBackend,
  145. TCPGitServer,
  146. ReceivePackHandler,
  147. UploadPackHandler,
  148. update_server_info as server_update_server_info,
  149. )
  150. # Module level tuple definition for status output
  151. GitStatus = namedtuple("GitStatus", "staged unstaged untracked")
  152. class NoneStream(RawIOBase):
  153. """Fallback if stdout or stderr are unavailable, does nothing."""
  154. def read(self, size=-1):
  155. return None
  156. def readall(self):
  157. return None
  158. def readinto(self, b):
  159. return None
  160. def write(self, b):
  161. return None
  162. default_bytes_out_stream = getattr(sys.stdout, "buffer", None) or NoneStream()
  163. default_bytes_err_stream = getattr(sys.stderr, "buffer", None) or NoneStream()
  164. DEFAULT_ENCODING = "utf-8"
  165. class Error(Exception):
  166. """Porcelain-based error. """
  167. def __init__(self, msg):
  168. super(Error, self).__init__(msg)
  169. class RemoteExists(Error):
  170. """Raised when the remote already exists."""
  171. class TimezoneFormatError(Error):
  172. """Raised when the timezone cannot be determined from a given string."""
  173. def parse_timezone_format(tz_str):
  174. """Parse given string and attempt to return a timezone offset.
  175. Different formats are considered in the following order:
  176. - Git internal format: <unix timestamp> <timezone offset>
  177. - RFC 2822: e.g. Mon, 20 Nov 1995 19:12:08 -0500
  178. - ISO 8601: e.g. 1995-11-20T19:12:08-0500
  179. Args:
  180. tz_str: datetime string
  181. Returns: Timezone offset as integer
  182. Raises:
  183. TimezoneFormatError: if timezone information cannot be extracted
  184. """
  185. import re
  186. # Git internal format
  187. internal_format_pattern = re.compile("^[0-9]+ [+-][0-9]{,4}$")
  188. if re.match(internal_format_pattern, tz_str):
  189. try:
  190. tz_internal = parse_timezone(tz_str.split(" ")[1].encode(DEFAULT_ENCODING))
  191. return tz_internal[0]
  192. except ValueError:
  193. pass
  194. # RFC 2822
  195. import email.utils
  196. rfc_2822 = email.utils.parsedate_tz(tz_str)
  197. if rfc_2822:
  198. return rfc_2822[9]
  199. # ISO 8601
  200. # Supported offsets:
  201. # sHHMM, sHH:MM, sHH
  202. iso_8601_pattern = re.compile("[0-9] ?([+-])([0-9]{2})(?::(?=[0-9]{2}))?([0-9]{2})?$")
  203. match = re.search(iso_8601_pattern, tz_str)
  204. total_secs = 0
  205. if match:
  206. sign, hours, minutes = match.groups()
  207. total_secs += int(hours) * 3600
  208. if minutes:
  209. total_secs += int(minutes) * 60
  210. total_secs = -total_secs if sign == "-" else total_secs
  211. return total_secs
  212. # YYYY.MM.DD, MM/DD/YYYY, DD.MM.YYYY contain no timezone information
  213. raise TimezoneFormatError(tz_str)
  214. def get_user_timezones():
  215. """Retrieve local timezone as described in
  216. https://raw.githubusercontent.com/git/git/v2.3.0/Documentation/date-formats.txt
  217. Returns: A tuple containing author timezone, committer timezone
  218. """
  219. local_timezone = time.localtime().tm_gmtoff
  220. if os.environ.get("GIT_AUTHOR_DATE"):
  221. author_timezone = parse_timezone_format(os.environ["GIT_AUTHOR_DATE"])
  222. else:
  223. author_timezone = local_timezone
  224. if os.environ.get("GIT_COMMITTER_DATE"):
  225. commit_timezone = parse_timezone_format(os.environ["GIT_COMMITTER_DATE"])
  226. else:
  227. commit_timezone = local_timezone
  228. return author_timezone, commit_timezone
  229. def open_repo(path_or_repo):
  230. """Open an argument that can be a repository or a path for a repository."""
  231. if isinstance(path_or_repo, BaseRepo):
  232. return path_or_repo
  233. return Repo(path_or_repo)
  234. @contextmanager
  235. def _noop_context_manager(obj):
  236. """Context manager that has the same api as closing but does nothing."""
  237. yield obj
  238. def open_repo_closing(path_or_repo):
  239. """Open an argument that can be a repository or a path for a repository.
  240. returns a context manager that will close the repo on exit if the argument
  241. is a path, else does nothing if the argument is a repo.
  242. """
  243. if isinstance(path_or_repo, BaseRepo):
  244. return _noop_context_manager(path_or_repo)
  245. return closing(Repo(path_or_repo))
  246. def path_to_tree_path(repopath, path, tree_encoding=DEFAULT_ENCODING):
  247. """Convert a path to a path usable in an index, e.g. bytes and relative to
  248. the repository root.
  249. Args:
  250. repopath: Repository path, absolute or relative to the cwd
  251. path: A path, absolute or relative to the cwd
  252. Returns: A path formatted for use in e.g. an index
  253. """
  254. # Resolve might returns a relative path on Windows
  255. # https://bugs.python.org/issue38671
  256. if sys.platform == "win32":
  257. path = os.path.abspath(path)
  258. path = Path(path)
  259. resolved_path = path.resolve()
  260. # Resolve and abspath seems to behave differently regarding symlinks,
  261. # as we are doing abspath on the file path, we need to do the same on
  262. # the repo path or they might not match
  263. if sys.platform == "win32":
  264. repopath = os.path.abspath(repopath)
  265. repopath = Path(repopath).resolve()
  266. try:
  267. relpath = resolved_path.relative_to(repopath)
  268. except ValueError:
  269. # If path is a symlink that points to a file outside the repo, we
  270. # want the relpath for the link itself, not the resolved target
  271. if path.is_symlink():
  272. parent = path.parent.resolve()
  273. relpath = (parent / path.name).relative_to(repopath)
  274. else:
  275. raise
  276. if sys.platform == "win32":
  277. return str(relpath).replace(os.path.sep, "/").encode(tree_encoding)
  278. else:
  279. return bytes(relpath)
  280. class DivergedBranches(Error):
  281. """Branches have diverged and fast-forward is not possible."""
  282. def __init__(self, current_sha, new_sha):
  283. self.current_sha = current_sha
  284. self.new_sha = new_sha
  285. def check_diverged(repo, current_sha, new_sha):
  286. """Check if updating to a sha can be done with fast forwarding.
  287. Args:
  288. repo: Repository object
  289. current_sha: Current head sha
  290. new_sha: New head sha
  291. """
  292. try:
  293. can = can_fast_forward(repo, current_sha, new_sha)
  294. except KeyError:
  295. can = False
  296. if not can:
  297. raise DivergedBranches(current_sha, new_sha)
  298. def archive(
  299. repo,
  300. committish=None,
  301. outstream=default_bytes_out_stream,
  302. errstream=default_bytes_err_stream,
  303. ):
  304. """Create an archive.
  305. Args:
  306. repo: Path of repository for which to generate an archive.
  307. committish: Commit SHA1 or ref to use
  308. outstream: Output stream (defaults to stdout)
  309. errstream: Error stream (defaults to stderr)
  310. """
  311. if committish is None:
  312. committish = "HEAD"
  313. with open_repo_closing(repo) as repo_obj:
  314. c = parse_commit(repo_obj, committish)
  315. for chunk in tar_stream(
  316. repo_obj.object_store, repo_obj.object_store[c.tree], c.commit_time
  317. ):
  318. outstream.write(chunk)
  319. def update_server_info(repo="."):
  320. """Update server info files for a repository.
  321. Args:
  322. repo: path to the repository
  323. """
  324. with open_repo_closing(repo) as r:
  325. server_update_server_info(r)
  326. def symbolic_ref(repo, ref_name, force=False):
  327. """Set git symbolic ref into HEAD.
  328. Args:
  329. repo: path to the repository
  330. ref_name: short name of the new ref
  331. force: force settings without checking if it exists in refs/heads
  332. """
  333. with open_repo_closing(repo) as repo_obj:
  334. ref_path = _make_branch_ref(ref_name)
  335. if not force and ref_path not in repo_obj.refs.keys():
  336. raise Error("fatal: ref `%s` is not a ref" % ref_name)
  337. repo_obj.refs.set_symbolic_ref(b"HEAD", ref_path)
  338. def pack_refs(repo, all=False):
  339. with open_repo_closing(repo) as repo_obj:
  340. refs = repo_obj.refs
  341. packed_refs = {
  342. ref: refs[ref]
  343. for ref in refs
  344. if (all or ref.startswith(LOCAL_TAG_PREFIX)) and ref != b"HEAD"
  345. }
  346. refs.add_packed_refs(packed_refs)
  347. def commit(
  348. repo=".",
  349. message=None,
  350. author=None,
  351. author_timezone=None,
  352. committer=None,
  353. commit_timezone=None,
  354. encoding=None,
  355. no_verify=False,
  356. signoff=False,
  357. ):
  358. """Create a new commit.
  359. Args:
  360. repo: Path to repository
  361. message: Optional commit message
  362. author: Optional author name and email
  363. author_timezone: Author timestamp timezone
  364. committer: Optional committer name and email
  365. commit_timezone: Commit timestamp timezone
  366. no_verify: Skip pre-commit and commit-msg hooks
  367. signoff: GPG Sign the commit (bool, defaults to False,
  368. pass True to use default GPG key,
  369. pass a str containing Key ID to use a specific GPG key)
  370. Returns: SHA1 of the new commit
  371. """
  372. # FIXME: Support --all argument
  373. if getattr(message, "encode", None):
  374. message = message.encode(encoding or DEFAULT_ENCODING)
  375. if getattr(author, "encode", None):
  376. author = author.encode(encoding or DEFAULT_ENCODING)
  377. if getattr(committer, "encode", None):
  378. committer = committer.encode(encoding or DEFAULT_ENCODING)
  379. local_timezone = get_user_timezones()
  380. if author_timezone is None:
  381. author_timezone = local_timezone[0]
  382. if commit_timezone is None:
  383. commit_timezone = local_timezone[1]
  384. with open_repo_closing(repo) as r:
  385. return r.do_commit(
  386. message=message,
  387. author=author,
  388. author_timezone=author_timezone,
  389. committer=committer,
  390. commit_timezone=commit_timezone,
  391. encoding=encoding,
  392. no_verify=no_verify,
  393. sign=signoff if isinstance(signoff, (str, bool)) else None,
  394. )
  395. def commit_tree(repo, tree, message=None, author=None, committer=None):
  396. """Create a new commit object.
  397. Args:
  398. repo: Path to repository
  399. tree: An existing tree object
  400. author: Optional author name and email
  401. committer: Optional committer name and email
  402. """
  403. with open_repo_closing(repo) as r:
  404. return r.do_commit(
  405. message=message, tree=tree, committer=committer, author=author
  406. )
  407. def init(path=".", bare=False):
  408. """Create a new git repository.
  409. Args:
  410. path: Path to repository.
  411. bare: Whether to create a bare repository.
  412. Returns: A Repo instance
  413. """
  414. if not os.path.exists(path):
  415. os.mkdir(path)
  416. if bare:
  417. return Repo.init_bare(path)
  418. else:
  419. return Repo.init(path)
  420. def clone(
  421. source,
  422. target=None,
  423. bare=False,
  424. checkout=None,
  425. errstream=default_bytes_err_stream,
  426. outstream=None,
  427. origin: Optional[str] = "origin",
  428. depth: Optional[int] = None,
  429. branch: Optional[Union[str, bytes]] = None,
  430. config: Optional[Config] = None,
  431. **kwargs
  432. ):
  433. """Clone a local or remote git repository.
  434. Args:
  435. source: Path or URL for source repository
  436. target: Path to target repository (optional)
  437. bare: Whether or not to create a bare repository
  438. checkout: Whether or not to check-out HEAD after cloning
  439. errstream: Optional stream to write progress to
  440. outstream: Optional stream to write progress to (deprecated)
  441. origin: Name of remote from the repository used to clone
  442. depth: Depth to fetch at
  443. branch: Optional branch or tag to be used as HEAD in the new repository
  444. instead of the cloned repository's HEAD.
  445. config: Configuration to use
  446. Returns: The new repository
  447. """
  448. if outstream is not None:
  449. import warnings
  450. warnings.warn(
  451. "outstream= has been deprecated in favour of errstream=.",
  452. DeprecationWarning,
  453. stacklevel=3,
  454. )
  455. # TODO(jelmer): Capture logging output and stream to errstream
  456. if config is None:
  457. config = StackedConfig.default()
  458. if checkout is None:
  459. checkout = not bare
  460. if checkout and bare:
  461. raise Error("checkout and bare are incompatible")
  462. if target is None:
  463. target = source.split("/")[-1]
  464. if isinstance(branch, str):
  465. branch = branch.encode(DEFAULT_ENCODING)
  466. mkdir = not os.path.exists(target)
  467. (client, path) = get_transport_and_path(
  468. source, config=config, **kwargs)
  469. return client.clone(
  470. path,
  471. target,
  472. mkdir=mkdir,
  473. bare=bare,
  474. origin=origin,
  475. checkout=checkout,
  476. branch=branch,
  477. progress=errstream.write,
  478. depth=depth,
  479. )
  480. def add(repo=".", paths=None):
  481. """Add files to the staging area.
  482. Args:
  483. repo: Repository for the files
  484. paths: Paths to add. No value passed stages all modified files.
  485. Returns: Tuple with set of added files and ignored files
  486. If the repository contains ignored directories, the returned set will
  487. contain the path to an ignored directory (with trailing slash). Individual
  488. files within ignored directories will not be returned.
  489. """
  490. ignored = set()
  491. with open_repo_closing(repo) as r:
  492. repo_path = Path(r.path).resolve()
  493. ignore_manager = IgnoreFilterManager.from_repo(r)
  494. if not paths:
  495. paths = list(
  496. get_untracked_paths(
  497. str(Path(os.getcwd()).resolve()),
  498. str(repo_path),
  499. r.open_index(),
  500. )
  501. )
  502. relpaths = []
  503. if not isinstance(paths, list):
  504. paths = [paths]
  505. for p in paths:
  506. path = Path(p)
  507. relpath = str(path.resolve().relative_to(repo_path))
  508. # FIXME: Support patterns
  509. if path.is_dir():
  510. relpath = os.path.join(relpath, "")
  511. if ignore_manager.is_ignored(relpath):
  512. ignored.add(relpath)
  513. continue
  514. relpaths.append(relpath)
  515. r.stage(relpaths)
  516. return (relpaths, ignored)
  517. def _is_subdir(subdir, parentdir):
  518. """Check whether subdir is parentdir or a subdir of parentdir
  519. If parentdir or subdir is a relative path, it will be disamgibuated
  520. relative to the pwd.
  521. """
  522. parentdir_abs = os.path.realpath(parentdir) + os.path.sep
  523. subdir_abs = os.path.realpath(subdir) + os.path.sep
  524. return subdir_abs.startswith(parentdir_abs)
  525. # TODO: option to remove ignored files also, in line with `git clean -fdx`
  526. def clean(repo=".", target_dir=None):
  527. """Remove any untracked files from the target directory recursively
  528. Equivalent to running ``git clean -fd`` in target_dir.
  529. Args:
  530. repo: Repository where the files may be tracked
  531. target_dir: Directory to clean - current directory if None
  532. """
  533. if target_dir is None:
  534. target_dir = os.getcwd()
  535. with open_repo_closing(repo) as r:
  536. if not _is_subdir(target_dir, r.path):
  537. raise Error("target_dir must be in the repo's working dir")
  538. config = r.get_config_stack()
  539. require_force = config.get_boolean( # noqa: F841
  540. (b"clean",), b"requireForce", True
  541. )
  542. # TODO(jelmer): if require_force is set, then make sure that -f, -i or
  543. # -n is specified.
  544. index = r.open_index()
  545. ignore_manager = IgnoreFilterManager.from_repo(r)
  546. paths_in_wd = _walk_working_dir_paths(target_dir, r.path)
  547. # Reverse file visit order, so that files and subdirectories are
  548. # removed before containing directory
  549. for ap, is_dir in reversed(list(paths_in_wd)):
  550. if is_dir:
  551. # All subdirectories and files have been removed if untracked,
  552. # so dir contains no tracked files iff it is empty.
  553. is_empty = len(os.listdir(ap)) == 0
  554. if is_empty:
  555. os.rmdir(ap)
  556. else:
  557. ip = path_to_tree_path(r.path, ap)
  558. is_tracked = ip in index
  559. rp = os.path.relpath(ap, r.path)
  560. is_ignored = ignore_manager.is_ignored(rp)
  561. if not is_tracked and not is_ignored:
  562. os.remove(ap)
  563. def remove(repo=".", paths=None, cached=False):
  564. """Remove files from the staging area.
  565. Args:
  566. repo: Repository for the files
  567. paths: Paths to remove
  568. """
  569. with open_repo_closing(repo) as r:
  570. index = r.open_index()
  571. for p in paths:
  572. full_path = os.fsencode(os.path.abspath(p))
  573. tree_path = path_to_tree_path(r.path, p)
  574. try:
  575. index_sha = index[tree_path].sha
  576. except KeyError as exc:
  577. raise Error("%s did not match any files" % p) from exc
  578. if not cached:
  579. try:
  580. st = os.lstat(full_path)
  581. except OSError:
  582. pass
  583. else:
  584. try:
  585. blob = blob_from_path_and_stat(full_path, st)
  586. except IOError:
  587. pass
  588. else:
  589. try:
  590. committed_sha = tree_lookup_path(
  591. r.__getitem__, r[r.head()].tree, tree_path
  592. )[1]
  593. except KeyError:
  594. committed_sha = None
  595. if blob.id != index_sha and index_sha != committed_sha:
  596. raise Error(
  597. "file has staged content differing "
  598. "from both the file and head: %s" % p
  599. )
  600. if index_sha != committed_sha:
  601. raise Error("file has staged changes: %s" % p)
  602. os.remove(full_path)
  603. del index[tree_path]
  604. index.write()
  605. rm = remove
  606. def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING):
  607. if commit.encoding:
  608. encoding = commit.encoding.decode("ascii")
  609. else:
  610. encoding = default_encoding
  611. return contents.decode(encoding, "replace")
  612. def commit_encode(commit, contents, default_encoding=DEFAULT_ENCODING):
  613. if commit.encoding:
  614. encoding = commit.encoding.decode("ascii")
  615. else:
  616. encoding = default_encoding
  617. return contents.encode(encoding)
  618. def print_commit(commit, decode, outstream=sys.stdout):
  619. """Write a human-readable commit log entry.
  620. Args:
  621. commit: A `Commit` object
  622. outstream: A stream file to write to
  623. """
  624. outstream.write("-" * 50 + "\n")
  625. outstream.write("commit: " + commit.id.decode("ascii") + "\n")
  626. if len(commit.parents) > 1:
  627. outstream.write(
  628. "merge: "
  629. + "...".join([c.decode("ascii") for c in commit.parents[1:]])
  630. + "\n"
  631. )
  632. outstream.write("Author: " + decode(commit.author) + "\n")
  633. if commit.author != commit.committer:
  634. outstream.write("Committer: " + decode(commit.committer) + "\n")
  635. time_tuple = time.gmtime(commit.author_time + commit.author_timezone)
  636. time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
  637. timezone_str = format_timezone(commit.author_timezone).decode("ascii")
  638. outstream.write("Date: " + time_str + " " + timezone_str + "\n")
  639. outstream.write("\n")
  640. outstream.write(decode(commit.message) + "\n")
  641. outstream.write("\n")
  642. def print_tag(tag, decode, outstream=sys.stdout):
  643. """Write a human-readable tag.
  644. Args:
  645. tag: A `Tag` object
  646. decode: Function for decoding bytes to unicode string
  647. outstream: A stream to write to
  648. """
  649. outstream.write("Tagger: " + decode(tag.tagger) + "\n")
  650. time_tuple = time.gmtime(tag.tag_time + tag.tag_timezone)
  651. time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
  652. timezone_str = format_timezone(tag.tag_timezone).decode("ascii")
  653. outstream.write("Date: " + time_str + " " + timezone_str + "\n")
  654. outstream.write("\n")
  655. outstream.write(decode(tag.message))
  656. outstream.write("\n")
  657. def show_blob(repo, blob, decode, outstream=sys.stdout):
  658. """Write a blob to a stream.
  659. Args:
  660. repo: A `Repo` object
  661. blob: A `Blob` object
  662. decode: Function for decoding bytes to unicode string
  663. outstream: A stream file to write to
  664. """
  665. outstream.write(decode(blob.data))
  666. def show_commit(repo, commit, decode, outstream=sys.stdout):
  667. """Show a commit to a stream.
  668. Args:
  669. repo: A `Repo` object
  670. commit: A `Commit` object
  671. decode: Function for decoding bytes to unicode string
  672. outstream: Stream to write to
  673. """
  674. print_commit(commit, decode=decode, outstream=outstream)
  675. if commit.parents:
  676. parent_commit = repo[commit.parents[0]]
  677. base_tree = parent_commit.tree
  678. else:
  679. base_tree = None
  680. diffstream = BytesIO()
  681. write_tree_diff(diffstream, repo.object_store, base_tree, commit.tree)
  682. diffstream.seek(0)
  683. outstream.write(commit_decode(commit, diffstream.getvalue()))
  684. def show_tree(repo, tree, decode, outstream=sys.stdout):
  685. """Print a tree to a stream.
  686. Args:
  687. repo: A `Repo` object
  688. tree: A `Tree` object
  689. decode: Function for decoding bytes to unicode string
  690. outstream: Stream to write to
  691. """
  692. for n in tree:
  693. outstream.write(decode(n) + "\n")
  694. def show_tag(repo, tag, decode, outstream=sys.stdout):
  695. """Print a tag to a stream.
  696. Args:
  697. repo: A `Repo` object
  698. tag: A `Tag` object
  699. decode: Function for decoding bytes to unicode string
  700. outstream: Stream to write to
  701. """
  702. print_tag(tag, decode, outstream)
  703. show_object(repo, repo[tag.object[1]], decode, outstream)
  704. def show_object(repo, obj, decode, outstream):
  705. return {
  706. b"tree": show_tree,
  707. b"blob": show_blob,
  708. b"commit": show_commit,
  709. b"tag": show_tag,
  710. }[obj.type_name](repo, obj, decode, outstream)
  711. def print_name_status(changes):
  712. """Print a simple status summary, listing changed files."""
  713. for change in changes:
  714. if not change:
  715. continue
  716. if isinstance(change, list):
  717. change = change[0]
  718. if change.type == CHANGE_ADD:
  719. path1 = change.new.path
  720. path2 = ""
  721. kind = "A"
  722. elif change.type == CHANGE_DELETE:
  723. path1 = change.old.path
  724. path2 = ""
  725. kind = "D"
  726. elif change.type == CHANGE_MODIFY:
  727. path1 = change.new.path
  728. path2 = ""
  729. kind = "M"
  730. elif change.type in RENAME_CHANGE_TYPES:
  731. path1 = change.old.path
  732. path2 = change.new.path
  733. if change.type == CHANGE_RENAME:
  734. kind = "R"
  735. elif change.type == CHANGE_COPY:
  736. kind = "C"
  737. yield "%-8s%-20s%-20s" % (kind, path1, path2)
  738. def log(
  739. repo=".",
  740. paths=None,
  741. outstream=sys.stdout,
  742. max_entries=None,
  743. reverse=False,
  744. name_status=False,
  745. ):
  746. """Write commit logs.
  747. Args:
  748. repo: Path to repository
  749. paths: Optional set of specific paths to print entries for
  750. outstream: Stream to write log output to
  751. reverse: Reverse order in which entries are printed
  752. name_status: Print name status
  753. max_entries: Optional maximum number of entries to display
  754. """
  755. with open_repo_closing(repo) as r:
  756. walker = r.get_walker(max_entries=max_entries, paths=paths, reverse=reverse)
  757. for entry in walker:
  758. def decode(x):
  759. return commit_decode(entry.commit, x)
  760. print_commit(entry.commit, decode, outstream)
  761. if name_status:
  762. outstream.writelines(
  763. [line + "\n" for line in print_name_status(entry.changes())]
  764. )
  765. # TODO(jelmer): better default for encoding?
  766. def show(
  767. repo=".",
  768. objects=None,
  769. outstream=sys.stdout,
  770. default_encoding=DEFAULT_ENCODING,
  771. ):
  772. """Print the changes in a commit.
  773. Args:
  774. repo: Path to repository
  775. objects: Objects to show (defaults to [HEAD])
  776. outstream: Stream to write to
  777. default_encoding: Default encoding to use if none is set in the
  778. commit
  779. """
  780. if objects is None:
  781. objects = ["HEAD"]
  782. if not isinstance(objects, list):
  783. objects = [objects]
  784. with open_repo_closing(repo) as r:
  785. for objectish in objects:
  786. o = parse_object(r, objectish)
  787. if isinstance(o, Commit):
  788. def decode(x):
  789. return commit_decode(o, x, default_encoding)
  790. else:
  791. def decode(x):
  792. return x.decode(default_encoding)
  793. show_object(r, o, decode, outstream)
  794. def diff_tree(repo, old_tree, new_tree, outstream=default_bytes_out_stream):
  795. """Compares the content and mode of blobs found via two tree objects.
  796. Args:
  797. repo: Path to repository
  798. old_tree: Id of old tree
  799. new_tree: Id of new tree
  800. outstream: Stream to write to
  801. """
  802. with open_repo_closing(repo) as r:
  803. write_tree_diff(outstream, r.object_store, old_tree, new_tree)
  804. def rev_list(repo, commits, outstream=sys.stdout):
  805. """Lists commit objects in reverse chronological order.
  806. Args:
  807. repo: Path to repository
  808. commits: Commits over which to iterate
  809. outstream: Stream to write to
  810. """
  811. with open_repo_closing(repo) as r:
  812. for entry in r.get_walker(include=[r[c].id for c in commits]):
  813. outstream.write(entry.commit.id + b"\n")
  814. def _canonical_part(url: str) -> str:
  815. name = url.rsplit('/', 1)[-1]
  816. if name.endswith('.git'):
  817. name = name[:-4]
  818. return name
  819. def submodule_add(repo, url, path=None, name=None):
  820. """Add a new submodule.
  821. Args:
  822. repo: Path to repository
  823. url: URL of repository to add as submodule
  824. path: Path where submodule should live
  825. """
  826. with open_repo_closing(repo) as r:
  827. if path is None:
  828. path = os.path.relpath(_canonical_part(url), r.path)
  829. if name is None:
  830. name = path
  831. # TODO(jelmer): Move this logic to dulwich.submodule
  832. gitmodules_path = os.path.join(r.path, ".gitmodules")
  833. try:
  834. config = ConfigFile.from_path(gitmodules_path)
  835. except FileNotFoundError:
  836. config = ConfigFile()
  837. config.path = gitmodules_path
  838. config.set(("submodule", name), "url", url)
  839. config.set(("submodule", name), "path", path)
  840. config.write_to_path()
  841. def submodule_init(repo):
  842. """Initialize submodules.
  843. Args:
  844. repo: Path to repository
  845. """
  846. with open_repo_closing(repo) as r:
  847. config = r.get_config()
  848. gitmodules_path = os.path.join(r.path, '.gitmodules')
  849. for path, url, name in read_submodules(gitmodules_path):
  850. config.set((b'submodule', name), b'active', True)
  851. config.set((b'submodule', name), b'url', url)
  852. config.write_to_path()
  853. def submodule_list(repo):
  854. """List submodules.
  855. Args:
  856. repo: Path to repository
  857. """
  858. from .submodule import iter_cached_submodules
  859. with open_repo_closing(repo) as r:
  860. for path, sha in iter_cached_submodules(r.object_store, r[r.head()].tree):
  861. yield path.decode(DEFAULT_ENCODING), sha.decode(DEFAULT_ENCODING)
  862. def tag_create(
  863. repo,
  864. tag,
  865. author=None,
  866. message=None,
  867. annotated=False,
  868. objectish="HEAD",
  869. tag_time=None,
  870. tag_timezone=None,
  871. sign=False,
  872. encoding=DEFAULT_ENCODING
  873. ):
  874. """Creates a tag in git via dulwich calls:
  875. Args:
  876. repo: Path to repository
  877. tag: tag string
  878. author: tag author (optional, if annotated is set)
  879. message: tag message (optional)
  880. annotated: whether to create an annotated tag
  881. objectish: object the tag should point at, defaults to HEAD
  882. tag_time: Optional time for annotated tag
  883. tag_timezone: Optional timezone for annotated tag
  884. sign: GPG Sign the tag (bool, defaults to False,
  885. pass True to use default GPG key,
  886. pass a str containing Key ID to use a specific GPG key)
  887. """
  888. with open_repo_closing(repo) as r:
  889. object = parse_object(r, objectish)
  890. if annotated:
  891. # Create the tag object
  892. tag_obj = Tag()
  893. if author is None:
  894. # TODO(jelmer): Don't use repo private method.
  895. author = r._get_user_identity(r.get_config_stack())
  896. tag_obj.tagger = author
  897. tag_obj.message = message + "\n".encode(encoding)
  898. tag_obj.name = tag
  899. tag_obj.object = (type(object), object.id)
  900. if tag_time is None:
  901. tag_time = int(time.time())
  902. tag_obj.tag_time = tag_time
  903. if tag_timezone is None:
  904. tag_timezone = get_user_timezones()[1]
  905. elif isinstance(tag_timezone, str):
  906. tag_timezone = parse_timezone(tag_timezone)
  907. tag_obj.tag_timezone = tag_timezone
  908. if sign:
  909. tag_obj.sign(sign if isinstance(sign, str) else None)
  910. r.object_store.add_object(tag_obj)
  911. tag_id = tag_obj.id
  912. else:
  913. tag_id = object.id
  914. r.refs[_make_tag_ref(tag)] = tag_id
  915. def tag_list(repo, outstream=sys.stdout):
  916. """List all tags.
  917. Args:
  918. repo: Path to repository
  919. outstream: Stream to write tags to
  920. """
  921. with open_repo_closing(repo) as r:
  922. tags = sorted(r.refs.as_dict(b"refs/tags"))
  923. return tags
  924. def tag_delete(repo, name):
  925. """Remove a tag.
  926. Args:
  927. repo: Path to repository
  928. name: Name of tag to remove
  929. """
  930. with open_repo_closing(repo) as r:
  931. if isinstance(name, bytes):
  932. names = [name]
  933. elif isinstance(name, list):
  934. names = name
  935. else:
  936. raise Error("Unexpected tag name type %r" % name)
  937. for name in names:
  938. del r.refs[_make_tag_ref(name)]
  939. def reset(repo, mode, treeish="HEAD"):
  940. """Reset current HEAD to the specified state.
  941. Args:
  942. repo: Path to repository
  943. mode: Mode ("hard", "soft", "mixed")
  944. treeish: Treeish to reset to
  945. """
  946. if mode != "hard":
  947. raise Error("hard is the only mode currently supported")
  948. with open_repo_closing(repo) as r:
  949. tree = parse_tree(r, treeish)
  950. r.reset_index(tree.id)
  951. def get_remote_repo(
  952. repo: Repo, remote_location: Optional[Union[str, bytes]] = None
  953. ) -> Tuple[Optional[str], str]:
  954. config = repo.get_config()
  955. if remote_location is None:
  956. remote_location = get_branch_remote(repo)
  957. if isinstance(remote_location, str):
  958. encoded_location = remote_location.encode()
  959. else:
  960. encoded_location = remote_location
  961. section = (b"remote", encoded_location)
  962. remote_name: Optional[str] = None
  963. if config.has_section(section):
  964. remote_name = encoded_location.decode()
  965. encoded_location = config.get(section, "url")
  966. else:
  967. remote_name = None
  968. return (remote_name, encoded_location.decode())
  969. def push(
  970. repo,
  971. remote_location=None,
  972. refspecs=None,
  973. outstream=default_bytes_out_stream,
  974. errstream=default_bytes_err_stream,
  975. force=False,
  976. **kwargs
  977. ):
  978. """Remote push with dulwich via dulwich.client
  979. Args:
  980. repo: Path to repository
  981. remote_location: Location of the remote
  982. refspecs: Refs to push to remote
  983. outstream: A stream file to write output
  984. errstream: A stream file to write errors
  985. force: Force overwriting refs
  986. """
  987. # Open the repo
  988. with open_repo_closing(repo) as r:
  989. if refspecs is None:
  990. refspecs = [active_branch(r)]
  991. (remote_name, remote_location) = get_remote_repo(r, remote_location)
  992. # Get the client and path
  993. client, path = get_transport_and_path(
  994. remote_location, config=r.get_config_stack(), **kwargs
  995. )
  996. selected_refs = []
  997. remote_changed_refs = {}
  998. def update_refs(refs):
  999. selected_refs.extend(parse_reftuples(r.refs, refs, refspecs, force=force))
  1000. new_refs = {}
  1001. # TODO: Handle selected_refs == {None: None}
  1002. for (lh, rh, force_ref) in selected_refs:
  1003. if lh is None:
  1004. new_refs[rh] = ZERO_SHA
  1005. remote_changed_refs[rh] = None
  1006. else:
  1007. try:
  1008. localsha = r.refs[lh]
  1009. except KeyError as exc:
  1010. raise Error(
  1011. "No valid ref %s in local repository" % lh
  1012. ) from exc
  1013. if not force_ref and rh in refs:
  1014. check_diverged(r, refs[rh], localsha)
  1015. new_refs[rh] = localsha
  1016. remote_changed_refs[rh] = localsha
  1017. return new_refs
  1018. err_encoding = getattr(errstream, "encoding", None) or DEFAULT_ENCODING
  1019. remote_location = client.get_url(path)
  1020. try:
  1021. result = client.send_pack(
  1022. path,
  1023. update_refs,
  1024. generate_pack_data=r.generate_pack_data,
  1025. progress=errstream.write,
  1026. )
  1027. except SendPackError as exc:
  1028. raise Error(
  1029. "Push to " + remote_location + " failed -> " + exc.args[0].decode(),
  1030. ) from exc
  1031. else:
  1032. errstream.write(
  1033. b"Push to " + remote_location.encode(err_encoding) + b" successful.\n"
  1034. )
  1035. for ref, error in (result.ref_status or {}).items():
  1036. if error is not None:
  1037. errstream.write(
  1038. b"Push of ref %s failed: %s\n" % (ref, error.encode(err_encoding))
  1039. )
  1040. else:
  1041. errstream.write(b"Ref %s updated\n" % ref)
  1042. if remote_name is not None:
  1043. _import_remote_refs(r.refs, remote_name, remote_changed_refs)
  1044. def pull(
  1045. repo,
  1046. remote_location=None,
  1047. refspecs=None,
  1048. outstream=default_bytes_out_stream,
  1049. errstream=default_bytes_err_stream,
  1050. fast_forward=True,
  1051. force=False,
  1052. **kwargs
  1053. ):
  1054. """Pull from remote via dulwich.client
  1055. Args:
  1056. repo: Path to repository
  1057. remote_location: Location of the remote
  1058. refspecs: refspecs to fetch
  1059. outstream: A stream file to write to output
  1060. errstream: A stream file to write to errors
  1061. """
  1062. # Open the repo
  1063. with open_repo_closing(repo) as r:
  1064. (remote_name, remote_location) = get_remote_repo(r, remote_location)
  1065. if refspecs is None:
  1066. refspecs = [b"HEAD"]
  1067. selected_refs = []
  1068. def determine_wants(remote_refs, **kwargs):
  1069. selected_refs.extend(
  1070. parse_reftuples(remote_refs, r.refs, refspecs, force=force)
  1071. )
  1072. return [
  1073. remote_refs[lh]
  1074. for (lh, rh, force_ref) in selected_refs
  1075. if remote_refs[lh] not in r.object_store
  1076. ]
  1077. client, path = get_transport_and_path(
  1078. remote_location, config=r.get_config_stack(), **kwargs
  1079. )
  1080. fetch_result = client.fetch(
  1081. path, r, progress=errstream.write, determine_wants=determine_wants
  1082. )
  1083. for (lh, rh, force_ref) in selected_refs:
  1084. if not force_ref and rh in r.refs:
  1085. try:
  1086. check_diverged(r, r.refs.follow(rh)[1], fetch_result.refs[lh])
  1087. except DivergedBranches as exc:
  1088. if fast_forward:
  1089. raise
  1090. else:
  1091. raise NotImplementedError(
  1092. "merge is not yet supported") from exc
  1093. r.refs[rh] = fetch_result.refs[lh]
  1094. if selected_refs:
  1095. r[b"HEAD"] = fetch_result.refs[selected_refs[0][1]]
  1096. # Perform 'git checkout .' - syncs staged changes
  1097. tree = r[b"HEAD"].tree
  1098. r.reset_index(tree=tree)
  1099. if remote_name is not None:
  1100. _import_remote_refs(r.refs, remote_name, fetch_result.refs)
  1101. def status(repo=".", ignored=False, untracked_files="all"):
  1102. """Returns staged, unstaged, and untracked changes relative to the HEAD.
  1103. Args:
  1104. repo: Path to repository or repository object
  1105. ignored: Whether to include ignored files in untracked
  1106. untracked_files: How to handle untracked files, defaults to "all":
  1107. "no": do not return untracked files
  1108. "all": include all files in untracked directories
  1109. Using untracked_files="no" can be faster than "all" when the worktreee
  1110. contains many untracked files/directories.
  1111. Note: untracked_files="normal" (git's default) is not implemented.
  1112. Returns: GitStatus tuple,
  1113. staged - dict with lists of staged paths (diff index/HEAD)
  1114. unstaged - list of unstaged paths (diff index/working-tree)
  1115. untracked - list of untracked, un-ignored & non-.git paths
  1116. """
  1117. with open_repo_closing(repo) as r:
  1118. # 1. Get status of staged
  1119. tracked_changes = get_tree_changes(r)
  1120. # 2. Get status of unstaged
  1121. index = r.open_index()
  1122. normalizer = r.get_blob_normalizer()
  1123. filter_callback = normalizer.checkin_normalize
  1124. unstaged_changes = list(get_unstaged_changes(index, r.path, filter_callback))
  1125. untracked_paths = get_untracked_paths(
  1126. r.path,
  1127. r.path,
  1128. index,
  1129. exclude_ignored=not ignored,
  1130. untracked_files=untracked_files,
  1131. )
  1132. if sys.platform == "win32":
  1133. untracked_changes = [
  1134. path.replace(os.path.sep, "/") for path in untracked_paths
  1135. ]
  1136. else:
  1137. untracked_changes = list(untracked_paths)
  1138. return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
  1139. def _walk_working_dir_paths(frompath, basepath, prune_dirnames=None):
  1140. """Get path, is_dir for files in working dir from frompath
  1141. Args:
  1142. frompath: Path to begin walk
  1143. basepath: Path to compare to
  1144. prune_dirnames: Optional callback to prune dirnames during os.walk
  1145. dirnames will be set to result of prune_dirnames(dirpath, dirnames)
  1146. """
  1147. for dirpath, dirnames, filenames in os.walk(frompath):
  1148. # Skip .git and below.
  1149. if ".git" in dirnames:
  1150. dirnames.remove(".git")
  1151. if dirpath != basepath:
  1152. continue
  1153. if ".git" in filenames:
  1154. filenames.remove(".git")
  1155. if dirpath != basepath:
  1156. continue
  1157. if dirpath != frompath:
  1158. yield dirpath, True
  1159. for filename in filenames:
  1160. filepath = os.path.join(dirpath, filename)
  1161. yield filepath, False
  1162. if prune_dirnames:
  1163. dirnames[:] = prune_dirnames(dirpath, dirnames)
  1164. def get_untracked_paths(
  1165. frompath, basepath, index, exclude_ignored=False, untracked_files="all"
  1166. ):
  1167. """Get untracked paths.
  1168. Args:
  1169. frompath: Path to walk
  1170. basepath: Path to compare to
  1171. index: Index to check against
  1172. exclude_ignored: Whether to exclude ignored paths
  1173. untracked_files: How to handle untracked files:
  1174. - "no": return an empty list
  1175. - "all": return all files in untracked directories
  1176. - "normal": Not implemented
  1177. Note: ignored directories will never be walked for performance reasons.
  1178. If exclude_ignored is False, only the path to an ignored directory will
  1179. be yielded, no files inside the directory will be returned
  1180. """
  1181. if untracked_files == "normal":
  1182. raise NotImplementedError("normal is not yet supported")
  1183. if untracked_files not in ("no", "all"):
  1184. raise ValueError("untracked_files must be one of (no, all)")
  1185. if untracked_files == "no":
  1186. return
  1187. with open_repo_closing(basepath) as r:
  1188. ignore_manager = IgnoreFilterManager.from_repo(r)
  1189. ignored_dirs = []
  1190. def prune_dirnames(dirpath, dirnames):
  1191. for i in range(len(dirnames) - 1, -1, -1):
  1192. path = os.path.join(dirpath, dirnames[i])
  1193. ip = os.path.join(os.path.relpath(path, basepath), "")
  1194. if ignore_manager.is_ignored(ip):
  1195. if not exclude_ignored:
  1196. ignored_dirs.append(
  1197. os.path.join(os.path.relpath(path, frompath), "")
  1198. )
  1199. del dirnames[i]
  1200. return dirnames
  1201. for ap, is_dir in _walk_working_dir_paths(
  1202. frompath, basepath, prune_dirnames=prune_dirnames
  1203. ):
  1204. if not is_dir:
  1205. ip = path_to_tree_path(basepath, ap)
  1206. if ip not in index:
  1207. if not exclude_ignored or not ignore_manager.is_ignored(
  1208. os.path.relpath(ap, basepath)
  1209. ):
  1210. yield os.path.relpath(ap, frompath)
  1211. yield from ignored_dirs
  1212. def get_tree_changes(repo):
  1213. """Return add/delete/modify changes to tree by comparing index to HEAD.
  1214. Args:
  1215. repo: repo path or object
  1216. Returns: dict with lists for each type of change
  1217. """
  1218. with open_repo_closing(repo) as r:
  1219. index = r.open_index()
  1220. # Compares the Index to the HEAD & determines changes
  1221. # Iterate through the changes and report add/delete/modify
  1222. # TODO: call out to dulwich.diff_tree somehow.
  1223. tracked_changes = {
  1224. "add": [],
  1225. "delete": [],
  1226. "modify": [],
  1227. }
  1228. try:
  1229. tree_id = r[b"HEAD"].tree
  1230. except KeyError:
  1231. tree_id = None
  1232. for change in index.changes_from_tree(r.object_store, tree_id):
  1233. if not change[0][0]:
  1234. tracked_changes["add"].append(change[0][1])
  1235. elif not change[0][1]:
  1236. tracked_changes["delete"].append(change[0][0])
  1237. elif change[0][0] == change[0][1]:
  1238. tracked_changes["modify"].append(change[0][0])
  1239. else:
  1240. raise NotImplementedError("git mv ops not yet supported")
  1241. return tracked_changes
  1242. def daemon(path=".", address=None, port=None):
  1243. """Run a daemon serving Git requests over TCP/IP.
  1244. Args:
  1245. path: Path to the directory to serve.
  1246. address: Optional address to listen on (defaults to ::)
  1247. port: Optional port to listen on (defaults to TCP_GIT_PORT)
  1248. """
  1249. # TODO(jelmer): Support git-daemon-export-ok and --export-all.
  1250. backend = FileSystemBackend(path)
  1251. server = TCPGitServer(backend, address, port)
  1252. server.serve_forever()
  1253. def web_daemon(path=".", address=None, port=None):
  1254. """Run a daemon serving Git requests over HTTP.
  1255. Args:
  1256. path: Path to the directory to serve
  1257. address: Optional address to listen on (defaults to ::)
  1258. port: Optional port to listen on (defaults to 80)
  1259. """
  1260. from dulwich.web import (
  1261. make_wsgi_chain,
  1262. make_server,
  1263. WSGIRequestHandlerLogger,
  1264. WSGIServerLogger,
  1265. )
  1266. backend = FileSystemBackend(path)
  1267. app = make_wsgi_chain(backend)
  1268. server = make_server(
  1269. address,
  1270. port,
  1271. app,
  1272. handler_class=WSGIRequestHandlerLogger,
  1273. server_class=WSGIServerLogger,
  1274. )
  1275. server.serve_forever()
  1276. def upload_pack(path=".", inf=None, outf=None):
  1277. """Upload a pack file after negotiating its contents using smart protocol.
  1278. Args:
  1279. path: Path to the repository
  1280. inf: Input stream to communicate with client
  1281. outf: Output stream to communicate with client
  1282. """
  1283. if outf is None:
  1284. outf = getattr(sys.stdout, "buffer", sys.stdout)
  1285. if inf is None:
  1286. inf = getattr(sys.stdin, "buffer", sys.stdin)
  1287. path = os.path.expanduser(path)
  1288. backend = FileSystemBackend(path)
  1289. def send_fn(data):
  1290. outf.write(data)
  1291. outf.flush()
  1292. proto = Protocol(inf.read, send_fn)
  1293. handler = UploadPackHandler(backend, [path], proto)
  1294. # FIXME: Catch exceptions and write a single-line summary to outf.
  1295. handler.handle()
  1296. return 0
  1297. def receive_pack(path=".", inf=None, outf=None):
  1298. """Receive a pack file after negotiating its contents using smart protocol.
  1299. Args:
  1300. path: Path to the repository
  1301. inf: Input stream to communicate with client
  1302. outf: Output stream to communicate with client
  1303. """
  1304. if outf is None:
  1305. outf = getattr(sys.stdout, "buffer", sys.stdout)
  1306. if inf is None:
  1307. inf = getattr(sys.stdin, "buffer", sys.stdin)
  1308. path = os.path.expanduser(path)
  1309. backend = FileSystemBackend(path)
  1310. def send_fn(data):
  1311. outf.write(data)
  1312. outf.flush()
  1313. proto = Protocol(inf.read, send_fn)
  1314. handler = ReceivePackHandler(backend, [path], proto)
  1315. # FIXME: Catch exceptions and write a single-line summary to outf.
  1316. handler.handle()
  1317. return 0
  1318. def _make_branch_ref(name):
  1319. if getattr(name, "encode", None):
  1320. name = name.encode(DEFAULT_ENCODING)
  1321. return LOCAL_BRANCH_PREFIX + name
  1322. def _make_tag_ref(name):
  1323. if getattr(name, "encode", None):
  1324. name = name.encode(DEFAULT_ENCODING)
  1325. return LOCAL_TAG_PREFIX + name
  1326. def branch_delete(repo, name):
  1327. """Delete a branch.
  1328. Args:
  1329. repo: Path to the repository
  1330. name: Name of the branch
  1331. """
  1332. with open_repo_closing(repo) as r:
  1333. if isinstance(name, list):
  1334. names = name
  1335. else:
  1336. names = [name]
  1337. for name in names:
  1338. del r.refs[_make_branch_ref(name)]
  1339. def branch_create(repo, name, objectish=None, force=False):
  1340. """Create a branch.
  1341. Args:
  1342. repo: Path to the repository
  1343. name: Name of the new branch
  1344. objectish: Target object to point new branch at (defaults to HEAD)
  1345. force: Force creation of branch, even if it already exists
  1346. """
  1347. with open_repo_closing(repo) as r:
  1348. if objectish is None:
  1349. objectish = "HEAD"
  1350. object = parse_object(r, objectish)
  1351. refname = _make_branch_ref(name)
  1352. ref_message = b"branch: Created from " + objectish.encode(DEFAULT_ENCODING)
  1353. if force:
  1354. r.refs.set_if_equals(refname, None, object.id, message=ref_message)
  1355. else:
  1356. if not r.refs.add_if_new(refname, object.id, message=ref_message):
  1357. raise Error("Branch with name %s already exists." % name)
  1358. def branch_list(repo):
  1359. """List all branches.
  1360. Args:
  1361. repo: Path to the repository
  1362. """
  1363. with open_repo_closing(repo) as r:
  1364. return r.refs.keys(base=LOCAL_BRANCH_PREFIX)
  1365. def active_branch(repo):
  1366. """Return the active branch in the repository, if any.
  1367. Args:
  1368. repo: Repository to open
  1369. Returns:
  1370. branch name
  1371. Raises:
  1372. KeyError: if the repository does not have a working tree
  1373. IndexError: if HEAD is floating
  1374. """
  1375. with open_repo_closing(repo) as r:
  1376. active_ref = r.refs.follow(b"HEAD")[0][1]
  1377. if not active_ref.startswith(LOCAL_BRANCH_PREFIX):
  1378. raise ValueError(active_ref)
  1379. return active_ref[len(LOCAL_BRANCH_PREFIX) :]
  1380. def get_branch_remote(repo):
  1381. """Return the active branch's remote name, if any.
  1382. Args:
  1383. repo: Repository to open
  1384. Returns:
  1385. remote name
  1386. Raises:
  1387. KeyError: if the repository does not have a working tree
  1388. """
  1389. with open_repo_closing(repo) as r:
  1390. branch_name = active_branch(r.path)
  1391. config = r.get_config()
  1392. try:
  1393. remote_name = config.get((b"branch", branch_name), b"remote")
  1394. except KeyError:
  1395. remote_name = b"origin"
  1396. return remote_name
  1397. def fetch(
  1398. repo,
  1399. remote_location=None,
  1400. outstream=sys.stdout,
  1401. errstream=default_bytes_err_stream,
  1402. message=None,
  1403. depth=None,
  1404. prune=False,
  1405. prune_tags=False,
  1406. force=False,
  1407. **kwargs
  1408. ):
  1409. """Fetch objects from a remote server.
  1410. Args:
  1411. repo: Path to the repository
  1412. remote_location: String identifying a remote server
  1413. outstream: Output stream (defaults to stdout)
  1414. errstream: Error stream (defaults to stderr)
  1415. message: Reflog message (defaults to b"fetch: from <remote_name>")
  1416. depth: Depth to fetch at
  1417. prune: Prune remote removed refs
  1418. prune_tags: Prune reomte removed tags
  1419. Returns:
  1420. Dictionary with refs on the remote
  1421. """
  1422. with open_repo_closing(repo) as r:
  1423. (remote_name, remote_location) = get_remote_repo(r, remote_location)
  1424. if message is None:
  1425. message = b"fetch: from " + remote_location.encode(DEFAULT_ENCODING)
  1426. client, path = get_transport_and_path(
  1427. remote_location, config=r.get_config_stack(), **kwargs
  1428. )
  1429. fetch_result = client.fetch(path, r, progress=errstream.write, depth=depth)
  1430. if remote_name is not None:
  1431. _import_remote_refs(
  1432. r.refs,
  1433. remote_name,
  1434. fetch_result.refs,
  1435. message,
  1436. prune=prune,
  1437. prune_tags=prune_tags,
  1438. )
  1439. return fetch_result
  1440. def ls_remote(remote, config: Optional[Config] = None, **kwargs):
  1441. """List the refs in a remote.
  1442. Args:
  1443. remote: Remote repository location
  1444. config: Configuration to use
  1445. Returns:
  1446. Dictionary with remote refs
  1447. """
  1448. if config is None:
  1449. config = StackedConfig.default()
  1450. client, host_path = get_transport_and_path(remote, config=config, **kwargs)
  1451. return client.get_refs(host_path)
  1452. def repack(repo):
  1453. """Repack loose files in a repository.
  1454. Currently this only packs loose objects.
  1455. Args:
  1456. repo: Path to the repository
  1457. """
  1458. with open_repo_closing(repo) as r:
  1459. r.object_store.pack_loose_objects()
  1460. def find_pack_for_reuse(repo):
  1461. reuse_pack = None
  1462. max_pack_len = 0
  1463. # The pack file which contains the largest number of objects
  1464. # will be most suitable for object reuse.
  1465. for p in repo.object_store.packs:
  1466. if len(p) > max_pack_len:
  1467. reuse_pack = p
  1468. max_pack_len = len(reuse_pack)
  1469. return reuse_pack
  1470. def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None, deltify=None, reuse_deltas=True):
  1471. """Pack objects into a file.
  1472. Args:
  1473. repo: Path to the repository
  1474. object_ids: List of object ids to write
  1475. packf: File-like object to write to
  1476. idxf: File-like object to write to (can be None)
  1477. delta_window_size: Sliding window size for searching for deltas;
  1478. Set to None for default window size.
  1479. deltify: Whether to deltify objects
  1480. reuse_deltas: Allow reuse of existing deltas while deltifying
  1481. """
  1482. with open_repo_closing(repo) as r:
  1483. reuse_pack = None
  1484. if deltify and reuse_deltas:
  1485. reuse_pack = find_pack_for_reuse(r)
  1486. entries, data_sum = write_pack_objects(
  1487. packf.write,
  1488. r.object_store.iter_shas((oid, None) for oid in object_ids),
  1489. deltify=deltify,
  1490. delta_window_size=delta_window_size,
  1491. reuse_pack=reuse_pack
  1492. )
  1493. if idxf is not None:
  1494. entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
  1495. write_pack_index(idxf, entries, data_sum)
  1496. def ls_tree(
  1497. repo,
  1498. treeish=b"HEAD",
  1499. outstream=sys.stdout,
  1500. recursive=False,
  1501. name_only=False,
  1502. ):
  1503. """List contents of a tree.
  1504. Args:
  1505. repo: Path to the repository
  1506. treeish: Tree id to list
  1507. outstream: Output stream (defaults to stdout)
  1508. recursive: Whether to recursively list files
  1509. name_only: Only print item name
  1510. """
  1511. def list_tree(store, treeid, base):
  1512. for (name, mode, sha) in store[treeid].iteritems():
  1513. if base:
  1514. name = posixpath.join(base, name)
  1515. if name_only:
  1516. outstream.write(name + b"\n")
  1517. else:
  1518. outstream.write(pretty_format_tree_entry(name, mode, sha))
  1519. if stat.S_ISDIR(mode) and recursive:
  1520. list_tree(store, sha, name)
  1521. with open_repo_closing(repo) as r:
  1522. tree = parse_tree(r, treeish)
  1523. list_tree(r.object_store, tree.id, "")
  1524. def remote_add(repo: Repo, name: Union[bytes, str], url: Union[bytes, str]):
  1525. """Add a remote.
  1526. Args:
  1527. repo: Path to the repository
  1528. name: Remote name
  1529. url: Remote URL
  1530. """
  1531. if not isinstance(name, bytes):
  1532. name = name.encode(DEFAULT_ENCODING)
  1533. if not isinstance(url, bytes):
  1534. url = url.encode(DEFAULT_ENCODING)
  1535. with open_repo_closing(repo) as r:
  1536. c = r.get_config()
  1537. section = (b"remote", name)
  1538. if c.has_section(section):
  1539. raise RemoteExists(section)
  1540. c.set(section, b"url", url)
  1541. c.write_to_path()
  1542. def remote_remove(repo: Repo, name: Union[bytes, str]):
  1543. """Remove a remote
  1544. Args:
  1545. repo: Path to the repository
  1546. name: Remote name
  1547. """
  1548. if not isinstance(name, bytes):
  1549. name = name.encode(DEFAULT_ENCODING)
  1550. with open_repo_closing(repo) as r:
  1551. c = r.get_config()
  1552. section = (b"remote", name)
  1553. del c[section]
  1554. c.write_to_path()
  1555. def check_ignore(repo, paths, no_index=False):
  1556. """Debug gitignore files.
  1557. Args:
  1558. repo: Path to the repository
  1559. paths: List of paths to check for
  1560. no_index: Don't check index
  1561. Returns: List of ignored files
  1562. """
  1563. with open_repo_closing(repo) as r:
  1564. index = r.open_index()
  1565. ignore_manager = IgnoreFilterManager.from_repo(r)
  1566. for path in paths:
  1567. if not no_index and path_to_tree_path(r.path, path) in index:
  1568. continue
  1569. if os.path.isabs(path):
  1570. path = os.path.relpath(path, r.path)
  1571. if ignore_manager.is_ignored(path):
  1572. yield path
  1573. def update_head(repo, target, detached=False, new_branch=None):
  1574. """Update HEAD to point at a new branch/commit.
  1575. Note that this does not actually update the working tree.
  1576. Args:
  1577. repo: Path to the repository
  1578. detached: Create a detached head
  1579. target: Branch or committish to switch to
  1580. new_branch: New branch to create
  1581. """
  1582. with open_repo_closing(repo) as r:
  1583. if new_branch is not None:
  1584. to_set = _make_branch_ref(new_branch)
  1585. else:
  1586. to_set = b"HEAD"
  1587. if detached:
  1588. # TODO(jelmer): Provide some way so that the actual ref gets
  1589. # updated rather than what it points to, so the delete isn't
  1590. # necessary.
  1591. del r.refs[to_set]
  1592. r.refs[to_set] = parse_commit(r, target).id
  1593. else:
  1594. r.refs.set_symbolic_ref(to_set, parse_ref(r, target))
  1595. if new_branch is not None:
  1596. r.refs.set_symbolic_ref(b"HEAD", to_set)
  1597. def reset_file(repo, file_path: str, target: bytes = b'HEAD',
  1598. symlink_fn=None):
  1599. """Reset the file to specific commit or branch.
  1600. Args:
  1601. repo: dulwich Repo object
  1602. file_path: file to reset, relative to the repository path
  1603. target: branch or commit or b'HEAD' to reset
  1604. """
  1605. tree = parse_tree(repo, treeish=target)
  1606. tree_path = _fs_to_tree_path(file_path)
  1607. file_entry = tree.lookup_path(repo.object_store.__getitem__, tree_path)
  1608. full_path = os.path.join(os.fsencode(repo.path), tree_path)
  1609. blob = repo.object_store[file_entry[1]]
  1610. mode = file_entry[0]
  1611. build_file_from_blob(blob, mode, full_path, symlink_fn=symlink_fn)
  1612. def check_mailmap(repo, contact):
  1613. """Check canonical name and email of contact.
  1614. Args:
  1615. repo: Path to the repository
  1616. contact: Contact name and/or email
  1617. Returns: Canonical contact data
  1618. """
  1619. with open_repo_closing(repo) as r:
  1620. from dulwich.mailmap import Mailmap
  1621. try:
  1622. mailmap = Mailmap.from_path(os.path.join(r.path, ".mailmap"))
  1623. except FileNotFoundError:
  1624. mailmap = Mailmap()
  1625. return mailmap.lookup(contact)
  1626. def fsck(repo):
  1627. """Check a repository.
  1628. Args:
  1629. repo: A path to the repository
  1630. Returns: Iterator over errors/warnings
  1631. """
  1632. with open_repo_closing(repo) as r:
  1633. # TODO(jelmer): check pack files
  1634. # TODO(jelmer): check graph
  1635. # TODO(jelmer): check refs
  1636. for sha in r.object_store:
  1637. o = r.object_store[sha]
  1638. try:
  1639. o.check()
  1640. except Exception as e:
  1641. yield (sha, e)
  1642. def stash_list(repo):
  1643. """List all stashes in a repository."""
  1644. with open_repo_closing(repo) as r:
  1645. from dulwich.stash import Stash
  1646. stash = Stash.from_repo(r)
  1647. return enumerate(list(stash.stashes()))
  1648. def stash_push(repo):
  1649. """Push a new stash onto the stack."""
  1650. with open_repo_closing(repo) as r:
  1651. from dulwich.stash import Stash
  1652. stash = Stash.from_repo(r)
  1653. stash.push()
  1654. def stash_pop(repo, index):
  1655. """Pop a stash from the stack."""
  1656. with open_repo_closing(repo) as r:
  1657. from dulwich.stash import Stash
  1658. stash = Stash.from_repo(r)
  1659. stash.pop(index)
  1660. def stash_drop(repo, index):
  1661. """Drop a stash from the stack."""
  1662. with open_repo_closing(repo) as r:
  1663. from dulwich.stash import Stash
  1664. stash = Stash.from_repo(r)
  1665. stash.drop(index)
  1666. def ls_files(repo):
  1667. """List all files in an index."""
  1668. with open_repo_closing(repo) as r:
  1669. return sorted(r.open_index())
  1670. def find_unique_abbrev(object_store, object_id):
  1671. """For now, just return 7 characters."""
  1672. # TODO(jelmer): Add some logic here to return a number of characters that
  1673. # scales relative with the size of the repository
  1674. return object_id.decode("ascii")[:7]
  1675. def describe(repo, abbrev=7):
  1676. """Describe the repository version.
  1677. Args:
  1678. repo: git repository
  1679. abbrev: number of characters of commit to take, default is 7
  1680. Returns: a string description of the current git revision
  1681. Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
  1682. """
  1683. # Get the repository
  1684. with open_repo_closing(repo) as r:
  1685. # Get a list of all tags
  1686. refs = r.get_refs()
  1687. tags = {}
  1688. for key, value in refs.items():
  1689. key = key.decode()
  1690. obj = r.get_object(value)
  1691. if u"tags" not in key:
  1692. continue
  1693. _, tag = key.rsplit(u"/", 1)
  1694. try:
  1695. commit = obj.object
  1696. except AttributeError:
  1697. continue
  1698. else:
  1699. commit = r.get_object(commit[1])
  1700. tags[tag] = [
  1701. datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
  1702. commit.id.decode("ascii"),
  1703. ]
  1704. sorted_tags = sorted(tags.items(), key=lambda tag: tag[1][0], reverse=True)
  1705. # If there are no tags, return the current commit
  1706. if len(sorted_tags) == 0:
  1707. return "g{}".format(find_unique_abbrev(r.object_store, r[r.head()].id))
  1708. # We're now 0 commits from the top
  1709. commit_count = 0
  1710. # Get the latest commit
  1711. latest_commit = r[r.head()]
  1712. # Walk through all commits
  1713. walker = r.get_walker()
  1714. for entry in walker:
  1715. # Check if tag
  1716. commit_id = entry.commit.id.decode("ascii")
  1717. for tag in sorted_tags:
  1718. tag_name = tag[0]
  1719. tag_commit = tag[1][1]
  1720. if commit_id == tag_commit:
  1721. if commit_count == 0:
  1722. return tag_name
  1723. else:
  1724. return "{}-{}-g{}".format(
  1725. tag_name,
  1726. commit_count,
  1727. latest_commit.id.decode("ascii")[:abbrev],
  1728. )
  1729. commit_count += 1
  1730. # Return plain commit if no parent tag can be found
  1731. return "g{}".format(latest_commit.id.decode("ascii")[:abbrev])
  1732. def get_object_by_path(repo, path, committish=None):
  1733. """Get an object by path.
  1734. Args:
  1735. repo: A path to the repository
  1736. path: Path to look up
  1737. committish: Commit to look up path in
  1738. Returns: A `ShaFile` object
  1739. """
  1740. if committish is None:
  1741. committish = "HEAD"
  1742. # Get the repository
  1743. with open_repo_closing(repo) as r:
  1744. commit = parse_commit(r, committish)
  1745. base_tree = commit.tree
  1746. if not isinstance(path, bytes):
  1747. path = commit_encode(commit, path)
  1748. (mode, sha) = tree_lookup_path(r.object_store.__getitem__, base_tree, path)
  1749. return r[sha]
  1750. def write_tree(repo):
  1751. """Write a tree object from the index.
  1752. Args:
  1753. repo: Repository for which to write tree
  1754. Returns: tree id for the tree that was written
  1755. """
  1756. with open_repo_closing(repo) as r:
  1757. return r.open_index().commit(r.object_store)