porcelain.py 49 KB

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