porcelain.py 55 KB

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