porcelain.py 70 KB

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