porcelain.py 68 KB

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