2
0

notes.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  1. # notes.py -- Git notes handling
  2. # Copyright (C) 2024 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. """Git notes handling."""
  21. import stat
  22. from collections.abc import Iterator
  23. from typing import TYPE_CHECKING, Optional
  24. from .objects import Blob, Tree
  25. if TYPE_CHECKING:
  26. from .config import StackedConfig
  27. NOTES_REF_PREFIX = b"refs/notes/"
  28. DEFAULT_NOTES_REF = NOTES_REF_PREFIX + b"commits"
  29. def get_note_fanout_level(tree: Tree, object_store) -> int:
  30. """Determine the fanout level for a note tree.
  31. Git uses a fanout directory structure for performance with large numbers
  32. of notes. The fanout level determines how many levels of subdirectories
  33. are used.
  34. Args:
  35. tree: The notes tree to analyze
  36. object_store: Object store to retrieve subtrees
  37. Returns:
  38. Fanout level (0 for no fanout, 1 or 2 for fanout)
  39. """
  40. # Count the total number of notes in the tree recursively
  41. def count_notes(tree: Tree, level: int = 0) -> int:
  42. count = 0
  43. for name, mode, sha in tree.items():
  44. if stat.S_ISREG(mode):
  45. count += 1
  46. elif stat.S_ISDIR(mode) and level < 2: # Only recurse 2 levels deep
  47. try:
  48. subtree = object_store[sha]
  49. count += count_notes(subtree, level + 1)
  50. except KeyError:
  51. pass
  52. return count
  53. note_count = count_notes(tree)
  54. # Use fanout based on number of notes
  55. # Git typically starts using fanout around 256 notes
  56. if note_count < 256:
  57. return 0
  58. elif note_count < 65536: # 256^2
  59. return 1
  60. else:
  61. return 2
  62. def split_path_for_fanout(hexsha: bytes, fanout_level: int) -> tuple[bytes, ...]:
  63. """Split a hex SHA into path components based on fanout level.
  64. Args:
  65. hexsha: Hex SHA of the object
  66. fanout_level: Number of directory levels for fanout
  67. Returns:
  68. Tuple of path components
  69. """
  70. if fanout_level == 0:
  71. return (hexsha,)
  72. components = []
  73. for i in range(fanout_level):
  74. components.append(hexsha[i * 2 : (i + 1) * 2])
  75. components.append(hexsha[fanout_level * 2 :])
  76. return tuple(components)
  77. def get_note_path(object_sha: bytes, fanout_level: int = 0) -> bytes:
  78. """Get the path within the notes tree for a given object.
  79. Args:
  80. object_sha: Hex SHA of the object to get notes for
  81. fanout_level: Fanout level to use
  82. Returns:
  83. Path within the notes tree
  84. """
  85. components = split_path_for_fanout(object_sha, fanout_level)
  86. return b"/".join(components)
  87. class NotesTree:
  88. """Represents a Git notes tree."""
  89. def __init__(self, tree: Tree, object_store):
  90. """Initialize a notes tree.
  91. Args:
  92. tree: The tree object containing notes
  93. object_store: Object store to retrieve note contents from
  94. """
  95. self._tree = tree
  96. self._object_store = object_store
  97. self._fanout_level = self._detect_fanout_level()
  98. def _detect_fanout_level(self) -> int:
  99. """Detect the fanout level used in this notes tree.
  100. Returns:
  101. Detected fanout level
  102. """
  103. if not self._tree.items():
  104. return 0
  105. # Check for presence of both files and directories
  106. has_files = False
  107. has_dirs = False
  108. dir_names = []
  109. for name, mode, sha in self._tree.items():
  110. if stat.S_ISDIR(mode):
  111. has_dirs = True
  112. dir_names.append(name)
  113. elif stat.S_ISREG(mode):
  114. has_files = True
  115. # If we have files at the root level, check if they're full SHA names
  116. if has_files and not has_dirs:
  117. # Check if any file names are full 40-char hex strings
  118. for name, mode, sha in self._tree.items():
  119. if stat.S_ISREG(mode) and len(name) == 40:
  120. try:
  121. int(name, 16) # Verify it's a valid hex string
  122. return 0 # No fanout
  123. except ValueError:
  124. pass
  125. # Check if all directories are 2-character hex names
  126. if has_dirs and dir_names:
  127. all_two_char_hex = all(
  128. len(name) == 2 and all(c in b"0123456789abcdef" for c in name)
  129. for name in dir_names
  130. )
  131. if all_two_char_hex:
  132. # Check a sample directory to determine if it's level 1 or 2
  133. sample_dir_name = dir_names[0]
  134. try:
  135. sample_mode, sample_sha = self._tree[sample_dir_name]
  136. sample_tree = self._object_store[sample_sha]
  137. # Check if this subtree also has 2-char hex directories
  138. sub_has_dirs = False
  139. for sub_name, sub_mode, sub_sha in sample_tree.items():
  140. if stat.S_ISDIR(sub_mode) and len(sub_name) == 2:
  141. try:
  142. int(sub_name, 16)
  143. sub_has_dirs = True
  144. break
  145. except ValueError:
  146. pass
  147. return 2 if sub_has_dirs else 1
  148. except KeyError:
  149. return 1 # Assume level 1 if we can't inspect
  150. return 0
  151. def _reorganize_tree(self, new_fanout_level: int) -> None:
  152. """Reorganize the notes tree to use a different fanout level.
  153. Args:
  154. new_fanout_level: The desired fanout level
  155. """
  156. if new_fanout_level == self._fanout_level:
  157. return
  158. # Collect all existing notes
  159. notes = []
  160. for object_sha, note_sha in self.list_notes():
  161. note_obj = self._object_store[note_sha]
  162. if isinstance(note_obj, Blob):
  163. notes.append((object_sha, note_obj.data))
  164. # Create new empty tree
  165. new_tree = Tree()
  166. self._object_store.add_object(new_tree)
  167. self._tree = new_tree
  168. self._fanout_level = new_fanout_level
  169. # Re-add all notes with new fanout structure using set_note
  170. # Temporarily set fanout back to avoid recursion
  171. for object_sha, note_content in notes:
  172. # Use the internal tree update logic without checking fanout again
  173. note_blob = Blob.from_string(note_content)
  174. self._object_store.add_object(note_blob)
  175. path = get_note_path(object_sha, new_fanout_level)
  176. components = path.split(b"/")
  177. # Build new tree structure
  178. def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
  179. if len(components) == 1:
  180. # Leaf level - add the note blob
  181. new_tree = Tree()
  182. for name, mode, sha in tree.items():
  183. if name != components[0]:
  184. new_tree.add(name, mode, sha)
  185. new_tree.add(components[0], stat.S_IFREG | 0o644, blob_sha)
  186. return new_tree
  187. else:
  188. # Directory level
  189. new_tree = Tree()
  190. found = False
  191. for name, mode, sha in tree.items():
  192. if name == components[0]:
  193. # Update this subtree
  194. if stat.S_ISDIR(mode):
  195. subtree = self._object_store[sha]
  196. else:
  197. # If not a directory, we need to replace it
  198. subtree = Tree()
  199. new_subtree = update_tree(subtree, components[1:], blob_sha)
  200. self._object_store.add_object(new_subtree)
  201. new_tree.add(name, stat.S_IFDIR, new_subtree.id)
  202. found = True
  203. else:
  204. new_tree.add(name, mode, sha)
  205. if not found:
  206. # Create new subtree path
  207. subtree = Tree()
  208. new_subtree = update_tree(subtree, components[1:], blob_sha)
  209. self._object_store.add_object(new_subtree)
  210. new_tree.add(components[0], stat.S_IFDIR, new_subtree.id)
  211. return new_tree
  212. self._tree = update_tree(self._tree, components, note_blob.id)
  213. self._object_store.add_object(self._tree)
  214. def _update_tree_entry(
  215. self, tree: Tree, name: bytes, mode: int, sha: bytes
  216. ) -> Tree:
  217. """Update a tree entry and return the updated tree.
  218. Args:
  219. tree: The tree to update
  220. name: Name of the entry
  221. mode: File mode
  222. sha: SHA of the object
  223. Returns:
  224. The updated tree
  225. """
  226. new_tree = Tree()
  227. for existing_name, existing_mode, existing_sha in tree.items():
  228. if existing_name != name:
  229. new_tree.add(existing_name, existing_mode, existing_sha)
  230. new_tree.add(name, mode, sha)
  231. self._object_store.add_object(new_tree)
  232. # Update the tree reference
  233. if tree is self._tree:
  234. self._tree = new_tree
  235. return new_tree
  236. def _get_note_sha(self, object_sha: bytes) -> Optional[bytes]:
  237. """Get the SHA of the note blob for an object.
  238. Args:
  239. object_sha: SHA of the object to get notes for
  240. Returns:
  241. SHA of the note blob, or None if no note exists
  242. """
  243. path = get_note_path(object_sha, self._fanout_level)
  244. components = path.split(b"/")
  245. current_tree = self._tree
  246. for component in components[:-1]:
  247. try:
  248. mode, sha = current_tree[component]
  249. if not stat.S_ISDIR(mode): # Not a directory
  250. return None
  251. current_tree = self._object_store[sha]
  252. except KeyError:
  253. return None
  254. try:
  255. mode, sha = current_tree[components[-1]]
  256. if not stat.S_ISREG(mode): # Not a regular file
  257. return None
  258. return sha
  259. except KeyError:
  260. return None
  261. def get_note(self, object_sha: bytes) -> Optional[bytes]:
  262. """Get the note content for an object.
  263. Args:
  264. object_sha: SHA of the object to get notes for
  265. Returns:
  266. Note content as bytes, or None if no note exists
  267. """
  268. note_sha = self._get_note_sha(object_sha)
  269. if note_sha is None:
  270. return None
  271. try:
  272. note_obj = self._object_store[note_sha]
  273. if not isinstance(note_obj, Blob):
  274. return None
  275. return note_obj.data
  276. except KeyError:
  277. return None
  278. def set_note(self, object_sha: bytes, note_content: bytes) -> Tree:
  279. """Set or update a note for an object.
  280. Args:
  281. object_sha: SHA of the object to annotate
  282. note_content: Content of the note
  283. Returns:
  284. New tree object with the note added/updated
  285. """
  286. # Create note blob
  287. note_blob = Blob.from_string(note_content)
  288. self._object_store.add_object(note_blob)
  289. # Check if we need to reorganize the tree for better fanout
  290. desired_fanout = get_note_fanout_level(self._tree, self._object_store)
  291. if desired_fanout != self._fanout_level:
  292. self._reorganize_tree(desired_fanout)
  293. # Get path components
  294. path = get_note_path(object_sha, self._fanout_level)
  295. components = path.split(b"/")
  296. # Build new tree structure
  297. def update_tree(tree: Tree, components: list, blob_sha: bytes) -> Tree:
  298. if len(components) == 1:
  299. # Leaf level - add the note blob
  300. new_tree = Tree()
  301. for name, mode, sha in tree.items():
  302. if name != components[0]:
  303. new_tree.add(name, mode, sha)
  304. new_tree.add(components[0], stat.S_IFREG | 0o644, blob_sha)
  305. return new_tree
  306. else:
  307. # Directory level
  308. new_tree = Tree()
  309. found = False
  310. for name, mode, sha in tree.items():
  311. if name == components[0]:
  312. # Update this subtree
  313. if stat.S_ISDIR(mode):
  314. subtree = self._object_store[sha]
  315. else:
  316. # If not a directory, we need to replace it
  317. subtree = Tree()
  318. new_subtree = update_tree(subtree, components[1:], blob_sha)
  319. self._object_store.add_object(new_subtree)
  320. new_tree.add(name, stat.S_IFDIR, new_subtree.id)
  321. found = True
  322. else:
  323. new_tree.add(name, mode, sha)
  324. if not found:
  325. # Create new subtree path
  326. subtree = Tree()
  327. new_subtree = update_tree(subtree, components[1:], blob_sha)
  328. self._object_store.add_object(new_subtree)
  329. new_tree.add(components[0], stat.S_IFDIR, new_subtree.id)
  330. return new_tree
  331. new_tree = update_tree(self._tree, components, note_blob.id)
  332. self._object_store.add_object(new_tree)
  333. self._tree = new_tree
  334. self._fanout_level = self._detect_fanout_level()
  335. return new_tree
  336. def remove_note(self, object_sha: bytes) -> Optional[Tree]:
  337. """Remove a note for an object.
  338. Args:
  339. object_sha: SHA of the object to remove notes from
  340. Returns:
  341. New tree object with the note removed, or None if no note existed
  342. """
  343. if self._get_note_sha(object_sha) is None:
  344. return None
  345. # Get path components
  346. path = get_note_path(object_sha, self._fanout_level)
  347. components = path.split(b"/")
  348. # Build new tree structure without the note
  349. def remove_from_tree(tree: Tree, components: list) -> Optional[Tree]:
  350. if len(components) == 1:
  351. # Leaf level - remove the note
  352. new_tree = Tree()
  353. found = False
  354. for name, mode, sha in tree.items():
  355. if name != components[0]:
  356. new_tree.add(name, mode, sha)
  357. else:
  358. found = True
  359. if not found:
  360. return None
  361. # Return None if tree is now empty
  362. return new_tree if len(new_tree) > 0 else None
  363. else:
  364. # Directory level
  365. new_tree = Tree()
  366. modified = False
  367. for name, mode, sha in tree.items():
  368. if name == components[0] and stat.S_ISDIR(mode):
  369. # Update this subtree
  370. subtree = self._object_store[sha]
  371. new_subtree = remove_from_tree(subtree, components[1:])
  372. if new_subtree is not None:
  373. self._object_store.add_object(new_subtree)
  374. new_tree.add(name, stat.S_IFDIR, new_subtree.id)
  375. modified = True
  376. else:
  377. new_tree.add(name, mode, sha)
  378. if not modified:
  379. return None
  380. # Return None if tree is now empty
  381. return new_tree if len(new_tree) > 0 else None
  382. new_tree = remove_from_tree(self._tree, components)
  383. if new_tree is None:
  384. new_tree = Tree() # Empty tree
  385. self._object_store.add_object(new_tree)
  386. self._tree = new_tree
  387. self._fanout_level = self._detect_fanout_level()
  388. return new_tree
  389. def list_notes(self) -> Iterator[tuple[bytes, bytes]]:
  390. """List all notes in this tree.
  391. Yields:
  392. Tuples of (object_sha, note_sha)
  393. """
  394. def walk_tree(tree: Tree, prefix: bytes = b"") -> Iterator[tuple[bytes, bytes]]:
  395. for name, mode, sha in tree.items():
  396. if stat.S_ISDIR(mode): # Directory
  397. subtree = self._object_store[sha]
  398. yield from walk_tree(subtree, prefix + name)
  399. elif stat.S_ISREG(mode): # File
  400. # Reconstruct the full hex SHA from the path
  401. full_hex = prefix + name
  402. yield (full_hex, sha)
  403. yield from walk_tree(self._tree)
  404. def create_notes_tree(object_store) -> Tree:
  405. """Create an empty notes tree.
  406. Args:
  407. object_store: Object store to add the tree to
  408. Returns:
  409. Empty tree object
  410. """
  411. tree = Tree()
  412. object_store.add_object(tree)
  413. return tree
  414. class Notes:
  415. """High-level interface for Git notes operations."""
  416. def __init__(self, object_store, refs_container):
  417. """Initialize Notes.
  418. Args:
  419. object_store: Object store to read/write objects
  420. refs_container: Refs container to read/write refs
  421. """
  422. self._object_store = object_store
  423. self._refs = refs_container
  424. def get_notes_ref(
  425. self,
  426. notes_ref: Optional[bytes] = None,
  427. config: Optional["StackedConfig"] = None,
  428. ) -> bytes:
  429. """Get the notes reference to use.
  430. Args:
  431. notes_ref: The notes ref to use, or None to use the default
  432. config: Config to read notes.displayRef from
  433. Returns:
  434. The notes reference name
  435. """
  436. if notes_ref is None:
  437. if config is not None:
  438. notes_ref = config.get((b"notes",), b"displayRef")
  439. if notes_ref is None:
  440. notes_ref = DEFAULT_NOTES_REF
  441. return notes_ref
  442. def get_note(
  443. self,
  444. object_sha: bytes,
  445. notes_ref: Optional[bytes] = None,
  446. config: Optional["StackedConfig"] = None,
  447. ) -> Optional[bytes]:
  448. """Get the note for an object.
  449. Args:
  450. object_sha: SHA of the object to get notes for
  451. notes_ref: The notes ref to use, or None to use the default
  452. config: Config to read notes.displayRef from
  453. Returns:
  454. The note content as bytes, or None if no note exists
  455. """
  456. notes_ref = self.get_notes_ref(notes_ref, config)
  457. try:
  458. notes_commit_sha = self._refs[notes_ref]
  459. except KeyError:
  460. return None
  461. # Get the commit object
  462. notes_obj = self._object_store[notes_commit_sha]
  463. # If it's a commit, get the tree from it
  464. from .objects import Commit
  465. if isinstance(notes_obj, Commit):
  466. notes_tree = self._object_store[notes_obj.tree]
  467. else:
  468. # If it's directly a tree (shouldn't happen in normal usage)
  469. notes_tree = notes_obj
  470. if not isinstance(notes_tree, Tree):
  471. return None
  472. notes_tree_obj = NotesTree(notes_tree, self._object_store)
  473. return notes_tree_obj.get_note(object_sha)
  474. def set_note(
  475. self,
  476. object_sha: bytes,
  477. note_content: bytes,
  478. notes_ref: Optional[bytes] = None,
  479. author: Optional[bytes] = None,
  480. committer: Optional[bytes] = None,
  481. message: Optional[bytes] = None,
  482. config: Optional["StackedConfig"] = None,
  483. ) -> bytes:
  484. """Set or update a note for an object.
  485. Args:
  486. object_sha: SHA of the object to annotate
  487. note_content: Content of the note
  488. notes_ref: The notes ref to use, or None to use the default
  489. author: Author identity (defaults to committer)
  490. committer: Committer identity (defaults to config)
  491. message: Commit message for the notes update
  492. config: Config to read user identity and notes.displayRef from
  493. Returns:
  494. SHA of the new notes commit
  495. """
  496. import time
  497. from .objects import Commit
  498. from .repo import get_user_identity
  499. notes_ref = self.get_notes_ref(notes_ref, config)
  500. # Get current notes tree
  501. try:
  502. notes_commit_sha = self._refs[notes_ref]
  503. notes_obj = self._object_store[notes_commit_sha]
  504. # If it's a commit, get the tree from it
  505. if isinstance(notes_obj, Commit):
  506. notes_tree = self._object_store[notes_obj.tree]
  507. else:
  508. # If it's directly a tree (shouldn't happen in normal usage)
  509. notes_tree = notes_obj
  510. if not isinstance(notes_tree, Tree):
  511. notes_tree = create_notes_tree(self._object_store)
  512. except KeyError:
  513. notes_tree = create_notes_tree(self._object_store)
  514. # Update notes tree
  515. notes_tree_obj = NotesTree(notes_tree, self._object_store)
  516. new_tree = notes_tree_obj.set_note(object_sha, note_content)
  517. # Create commit
  518. if committer is None and config is not None:
  519. committer = get_user_identity(config, kind="COMMITTER")
  520. if committer is None:
  521. committer = b"Git User <user@example.com>"
  522. if author is None:
  523. author = committer
  524. if message is None:
  525. message = b"Notes added by 'git notes add'"
  526. commit = Commit()
  527. commit.tree = new_tree.id
  528. commit.author = author
  529. commit.committer = committer
  530. commit.commit_time = commit.author_time = int(time.time())
  531. commit.commit_timezone = commit.author_timezone = 0
  532. commit.encoding = b"UTF-8"
  533. commit.message = message
  534. # Set parent to previous notes commit if exists
  535. try:
  536. parent_sha = self._refs[notes_ref]
  537. parent = self._object_store[parent_sha]
  538. if isinstance(parent, Commit):
  539. commit.parents = [parent_sha]
  540. except KeyError:
  541. commit.parents = []
  542. self._object_store.add_object(commit)
  543. self._refs[notes_ref] = commit.id
  544. return commit.id
  545. def remove_note(
  546. self,
  547. object_sha: bytes,
  548. notes_ref: Optional[bytes] = None,
  549. author: Optional[bytes] = None,
  550. committer: Optional[bytes] = None,
  551. message: Optional[bytes] = None,
  552. config: Optional["StackedConfig"] = None,
  553. ) -> Optional[bytes]:
  554. """Remove a note for an object.
  555. Args:
  556. object_sha: SHA of the object to remove notes from
  557. notes_ref: The notes ref to use, or None to use the default
  558. author: Author identity (defaults to committer)
  559. committer: Committer identity (defaults to config)
  560. message: Commit message for the notes removal
  561. config: Config to read user identity and notes.displayRef from
  562. Returns:
  563. SHA of the new notes commit, or None if no note existed
  564. """
  565. import time
  566. from .objects import Commit
  567. from .repo import get_user_identity
  568. notes_ref = self.get_notes_ref(notes_ref, config)
  569. # Get current notes tree
  570. try:
  571. notes_commit_sha = self._refs[notes_ref]
  572. notes_obj = self._object_store[notes_commit_sha]
  573. # If it's a commit, get the tree from it
  574. if isinstance(notes_obj, Commit):
  575. notes_tree = self._object_store[notes_obj.tree]
  576. else:
  577. # If it's directly a tree (shouldn't happen in normal usage)
  578. notes_tree = notes_obj
  579. if not isinstance(notes_tree, Tree):
  580. return None
  581. except KeyError:
  582. return None
  583. # Remove from notes tree
  584. notes_tree_obj = NotesTree(notes_tree, self._object_store)
  585. new_tree = notes_tree_obj.remove_note(object_sha)
  586. if new_tree is None:
  587. return None
  588. # Create commit
  589. if committer is None and config is not None:
  590. committer = get_user_identity(config, kind="COMMITTER")
  591. if committer is None:
  592. committer = b"Git User <user@example.com>"
  593. if author is None:
  594. author = committer
  595. if message is None:
  596. message = b"Notes removed by 'git notes remove'"
  597. commit = Commit()
  598. commit.tree = new_tree.id
  599. commit.author = author
  600. commit.committer = committer
  601. commit.commit_time = commit.author_time = int(time.time())
  602. commit.commit_timezone = commit.author_timezone = 0
  603. commit.encoding = b"UTF-8"
  604. commit.message = message
  605. # Set parent to previous notes commit
  606. parent_sha = self._refs[notes_ref]
  607. parent = self._object_store[parent_sha]
  608. if isinstance(parent, Commit):
  609. commit.parents = [parent_sha]
  610. self._object_store.add_object(commit)
  611. self._refs[notes_ref] = commit.id
  612. return commit.id
  613. def list_notes(
  614. self,
  615. notes_ref: Optional[bytes] = None,
  616. config: Optional["StackedConfig"] = None,
  617. ) -> list[tuple[bytes, bytes]]:
  618. """List all notes in a notes ref.
  619. Args:
  620. notes_ref: The notes ref to use, or None to use the default
  621. config: Config to read notes.displayRef from
  622. Returns:
  623. List of tuples of (object_sha, note_content)
  624. """
  625. notes_ref = self.get_notes_ref(notes_ref, config)
  626. try:
  627. notes_commit_sha = self._refs[notes_ref]
  628. except KeyError:
  629. return []
  630. # Get the commit object
  631. from .objects import Commit
  632. notes_obj = self._object_store[notes_commit_sha]
  633. # If it's a commit, get the tree from it
  634. if isinstance(notes_obj, Commit):
  635. notes_tree = self._object_store[notes_obj.tree]
  636. else:
  637. # If it's directly a tree (shouldn't happen in normal usage)
  638. notes_tree = notes_obj
  639. if not isinstance(notes_tree, Tree):
  640. return []
  641. notes_tree_obj = NotesTree(notes_tree, self._object_store)
  642. result = []
  643. for object_sha, note_sha in notes_tree_obj.list_notes():
  644. note_obj = self._object_store[note_sha]
  645. if isinstance(note_obj, Blob):
  646. result.append((object_sha, note_obj.data))
  647. return result