porcelain.py 48 KB

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