History

refactor (#450)

Sebastian Thiel committed 4 years ago

Commit 71c6a201 parent 71f15fc4 files · +334 −336Browse files

Unified · Split

4 files
  1. git-repository/src/object/tree/diff.rs +0 −336
  2. git-repository/src/object/tree/diff/change.rs +99 −0
  3. git-repository/src/object/tree/diff/for_each.rs +155 −0
  4. git-repository/src/object/tree/diff/mod.rs +80 −0
git-repository/src/object/tree/diff.rs+0−336
@@ -1,336 +0,0 @@
1−use crate::{bstr::BStr, Repository, Tree};
2−
3−/// Returned by the `for_each` function to control flow.
4−#[derive(Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
5−pub enum Action {
6− /// Continue the traversal of changes.
7− Continue,
8− /// Stop the traversal of changes and stop calling this function.
9− Cancel,
10−}
11−
12−impl Default for Action {
13− fn default() -> Self {
14− Action::Continue
15− }
16−}
17−
18−/// Represents any possible change in order to turn one tree into another.
19−#[derive(Debug, Clone, Copy)]
20−pub struct Change<'a, 'old, 'new> {
21− /// The location of the file or directory described by `event`, if tracking was enabled.
22− ///
23− /// Otherwise this value is always an empty path.
24− pub location: &'a BStr,
25− /// The diff event itself to provide information about what would need to change.
26− pub event: change::Event<'old, 'new>,
27−}
28−
29−///
30−pub mod change {
31− use git_object::tree::EntryMode;
32−
33− use crate::{bstr::ByteSlice, Id, Repository};
34−
35− /// An event emitted when finding differences between two trees.
36− #[derive(Debug, Clone, Copy)]
37− pub enum Event<'old, 'new> {
38− /// An entry was added, like the addition of a file or directory.
39− Addition {
40− /// The mode of the added entry.
41− entry_mode: git_object::tree::EntryMode,
42− /// The object id of the added entry.
43− id: Id<'new>,
44− },
45− /// An entry was deleted, like the deletion of a file or directory.
46− Deletion {
47− /// The mode of the deleted entry.
48− entry_mode: git_object::tree::EntryMode,
49− /// The object id of the deleted entry.
50− id: Id<'old>,
51− },
52− /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning
53− /// a file into a symbolic link adjusts its mode.
54− Modification {
55− /// The mode of the entry before the modification.
56− previous_entry_mode: git_object::tree::EntryMode,
57− /// The object id of the entry before the modification.
58− previous_id: Id<'old>,
59−
60− /// The mode of the entry after the modification.
61− entry_mode: git_object::tree::EntryMode,
62− /// The object id after the modification.
63− id: Id<'new>,
64− },
65− }
66−
67− /// A platform to keep temporary information to perform line diffs.
68− pub struct DiffPlatform<'old, 'new> {
69− old: crate::Object<'old>,
70− new: crate::Object<'new>,
71− algo: git_diff::text::Algorithm,
72− }
73−
74− impl<'old, 'new> Event<'old, 'new> {
75− fn repo(&self) -> &Repository {
76− match self {
77− Event::Addition { id, .. } => id.repo,
78− Event::Deletion { id, .. } => id.repo,
79− Event::Modification { id, .. } => id.repo,
80− }
81− }
82− }
83−
84− impl<'old, 'new> Event<'old, 'new> {
85− /// Produce a platform for performing a line-diff, or `None` if this is not a [`Modification`][Event::Modification]
86− /// or one of the entries to compare is not a blob.
87− pub fn diff(&self) -> Option<Result<DiffPlatform<'old, 'new>, crate::object::find::existing::Error>> {
88− // let algo = self.repo().config.diff_algorithm()?;
89− match self {
90− Event::Modification {
91− previous_entry_mode: EntryMode::BlobExecutable | EntryMode::Blob,
92− previous_id,
93− entry_mode: EntryMode::BlobExecutable | EntryMode::Blob,
94− id,
95− } => match previous_id.object().and_then(|old| id.object().map(|new| (old, new))) {
96− Ok((old, new)) => Some(Ok(DiffPlatform {
97− old,
98− new,
99− algo: git_diff::text::Algorithm::Myers,
100− })),
101− Err(err) => Some(Err(err)),
102− },
103− _ => None,
104− }
105− }
106− }
107−
108− impl<'old, 'new> DiffPlatform<'old, 'new> {
109− /// Perform a diff on lines between the old and the new version of a blob.
110− /// The algorithm is determined by the `diff.algorithm` configuration.
111− /// Note that the [`Sink`][git_diff::text::imara::Sink] implementation is
112− /// what makes the diff usable and relies heavily on what the caller requires, as created by `make_sink`.
113− pub fn lines<FnS, S>(&self, new_sink: FnS) -> S::Out
114− where
115− FnS: for<'a> FnOnce(&git_diff::text::imara::intern::InternedInput<&'a [u8]>) -> S,
116− S: git_diff::text::imara::Sink,
117− {
118− git_diff::text::with(
119− self.old.data.as_bstr(),
120− self.new.data.as_bstr(),
121− git_diff::text::Algorithm::Myers, // TODO: use diff.algorithm
122− // TODO: make use of `core.eol` and/or filters to do line-counting correctly. It's probably
123− // OK to just know how these objects are saved to know what constitutes a line.
124− git_diff::text::imara::intern::InternedInput::new,
125− new_sink,
126− )
127− .1
128− }
129− }
130−}
131−
132−/// Diffing
133−impl<'repo> Tree<'repo> {
134− /// Return a platform to see the changes needed to create other trees, for instance.
135− ///
136− /// # Performance
137− ///
138− /// It's highly recommended to set an object cache to avoid extracting the same object multiple times.
139− pub fn changes<'a>(&'a self) -> Platform<'a, 'repo> {
140− Platform {
141− state: Default::default(),
142− lhs: self,
143− tracking: None,
144− }
145− }
146−}
147−
148−/// The diffing platform returned by [`Tree::changes()`].
149−#[derive(Clone)]
150−pub struct Platform<'a, 'repo> {
151− state: git_diff::tree::State,
152− lhs: &'a Tree<'repo>,
153− tracking: Option<Tracking>,
154−}
155−
156−#[derive(Clone, Copy)]
157−enum Tracking {
158− FileName,
159− Path,
160−}
161−
162−/// Configuration
163−impl<'a, 'repo> Platform<'a, 'repo> {
164− /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item.
165− pub fn track_filename(&mut self) -> &mut Self {
166− self.tracking = Some(Tracking::FileName);
167− self
168− }
169−
170− /// Keep track of the entire path of a change, relative to the repository.
171− ///
172− /// This makes the [`location`][Change::location] field usable.
173− pub fn track_path(&mut self) -> &mut Self {
174− self.tracking = Some(Tracking::Path);
175− self
176− }
177−}
178−
179−///
180−pub mod for_each {
181− use super::{change, Action, Change, Platform, Tracking};
182− use crate::bstr::{BStr, BString, ByteSlice, ByteVec};
183− use crate::ext::ObjectIdExt;
184− use crate::{Repository, Tree};
185− use git_object::TreeRefIter;
186− use git_odb::FindExt;
187− use std::collections::VecDeque;
188−
189− /// The error return by methods on the [diff platform][Platform].
190− #[derive(Debug, thiserror::Error)]
191− #[allow(missing_docs)]
192− pub enum Error {
193− #[error(transparent)]
194− Diff(#[from] git_diff::tree::changes::Error),
195− #[error("The user-provided callback failed")]
196− ForEach(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
197− }
198−
199− /// Add the item to compare to.
200− impl<'a, 'old> Platform<'a, 'old> {
201− /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`.
202− pub fn for_each_to_obtain_tree<'new, E>(
203− &mut self,
204− other: &Tree<'new>,
205− for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result<Action, E>,
206− ) -> Result<(), Error>
207− where
208− E: std::error::Error + Sync + Send + 'static,
209− {
210− let repo = self.lhs.repo;
211− let mut delegate = Delegate {
212− repo: self.lhs.repo,
213− other_repo: other.repo,
214− tracking: self.tracking,
215− location: BString::default(),
216− path_deque: Default::default(),
217− visit: for_each,
218− err: None,
219− };
220− git_diff::tree::Changes::from(TreeRefIter::from_bytes(&self.lhs.data)).needed_to_obtain(
221− TreeRefIter::from_bytes(&other.data),
222− &mut self.state,
223− |oid, buf| repo.objects.find_tree_iter(oid, buf),
224− &mut delegate,
225− )?;
226− match delegate.err {
227− Some(err) => Err(Error::ForEach(Box::new(err))),
228− None => Ok(()),
229− }
230− }
231− }
232−
233− struct Delegate<'old, 'new, VisitFn, E> {
234− repo: &'old Repository,
235− other_repo: &'new Repository,
236− tracking: Option<Tracking>,
237− location: BString,
238− path_deque: VecDeque<BString>,
239− visit: VisitFn,
240− err: Option<E>,
241− }
242−
243− impl<A, B> Delegate<'_, '_, A, B> {
244− fn pop_element(&mut self) {
245− if let Some(pos) = self.location.rfind_byte(b'/') {
246− self.location.resize(pos, 0);
247− } else {
248− self.location.clear();
249− }
250− }
251−
252− fn push_element(&mut self, name: &BStr) {
253− if !self.location.is_empty() {
254− self.location.push(b'/');
255− }
256− self.location.push_str(name);
257− }
258− }
259−
260− impl<'old, 'new, VisitFn, E> git_diff::tree::Visit for Delegate<'old, 'new, VisitFn, E>
261− where
262− VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result<Action, E>,
263− E: std::error::Error + Sync + Send + 'static,
264− {
265− fn pop_front_tracked_path_and_set_current(&mut self) {
266− if let Some(Tracking::Path) = self.tracking {
267− self.location = self
268− .path_deque
269− .pop_front()
270− .expect("every call is matched with push_tracked_path_component");
271− }
272− }
273−
274− fn push_back_tracked_path_component(&mut self, component: &BStr) {
275− if let Some(Tracking::Path) = self.tracking {
276− self.push_element(component);
277− self.path_deque.push_back(self.location.clone());
278− }
279− }
280−
281− fn push_path_component(&mut self, component: &BStr) {
282− match self.tracking {
283− Some(Tracking::FileName) => {
284− self.location.clear();
285− self.location.push_str(component);
286− }
287− Some(Tracking::Path) => {
288− self.push_element(component);
289− }
290− None => {}
291− }
292− }
293−
294− fn pop_path_component(&mut self) {
295− if let Some(Tracking::Path) = self.tracking {
296− self.pop_element();
297− }
298− }
299−
300− fn visit(&mut self, change: git_diff::tree::visit::Change) -> git_diff::tree::visit::Action {
301− use git_diff::tree::visit::Change::*;
302− let event = match change {
303− Addition { entry_mode, oid } => change::Event::Addition {
304− entry_mode,
305− id: oid.attach(self.other_repo),
306− },
307− Deletion { entry_mode, oid } => change::Event::Deletion {
308− entry_mode,
309− id: oid.attach(self.repo),
310− },
311− Modification {
312− previous_entry_mode,
313− previous_oid,
314− entry_mode,
315− oid,
316− } => change::Event::Modification {
317− previous_entry_mode,
318− entry_mode,
319− previous_id: previous_oid.attach(self.repo),
320− id: oid.attach(self.other_repo),
321− },
322− };
323− match (self.visit)(Change {
324− event,
325− location: self.location.as_ref(),
326− }) {
327− Ok(Action::Cancel) => git_diff::tree::visit::Action::Cancel,
328− Ok(Action::Continue) => git_diff::tree::visit::Action::Continue,
329− Err(err) => {
330− self.err = Some(err);
331− git_diff::tree::visit::Action::Cancel
332− }
333− }
334− }
335− }
336−}
git-repository/src/object/tree/diff/change.rs+99−0
@@ -0,0 +1,99 @@
1+use git_object::tree::EntryMode;
2+
3+use crate::{bstr::ByteSlice, Id, Repository};
4+
5+/// An event emitted when finding differences between two trees.
6+#[derive(Debug, Clone, Copy)]
7+pub enum Event<'old, 'new> {
8+ /// An entry was added, like the addition of a file or directory.
9+ Addition {
10+ /// The mode of the added entry.
11+ entry_mode: git_object::tree::EntryMode,
12+ /// The object id of the added entry.
13+ id: Id<'new>,
14+ },
15+ /// An entry was deleted, like the deletion of a file or directory.
16+ Deletion {
17+ /// The mode of the deleted entry.
18+ entry_mode: git_object::tree::EntryMode,
19+ /// The object id of the deleted entry.
20+ id: Id<'old>,
21+ },
22+ /// An entry was modified, e.g. changing the contents of a file adjusts its object id and turning
23+ /// a file into a symbolic link adjusts its mode.
24+ Modification {
25+ /// The mode of the entry before the modification.
26+ previous_entry_mode: git_object::tree::EntryMode,
27+ /// The object id of the entry before the modification.
28+ previous_id: Id<'old>,
29+
30+ /// The mode of the entry after the modification.
31+ entry_mode: git_object::tree::EntryMode,
32+ /// The object id after the modification.
33+ id: Id<'new>,
34+ },
35+}
36+
37+/// A platform to keep temporary information to perform line diffs.
38+pub struct DiffPlatform<'old, 'new> {
39+ old: crate::Object<'old>,
40+ new: crate::Object<'new>,
41+ algo: git_diff::text::Algorithm,
42+}
43+
44+impl<'old, 'new> Event<'old, 'new> {
45+ fn repo(&self) -> &Repository {
46+ match self {
47+ Event::Addition { id, .. } => id.repo,
48+ Event::Deletion { id, .. } => id.repo,
49+ Event::Modification { id, .. } => id.repo,
50+ }
51+ }
52+}
53+
54+impl<'old, 'new> Event<'old, 'new> {
55+ /// Produce a platform for performing a line-diff, or `None` if this is not a [`Modification`][Event::Modification]
56+ /// or one of the entries to compare is not a blob.
57+ pub fn diff(&self) -> Option<Result<DiffPlatform<'old, 'new>, crate::object::find::existing::Error>> {
58+ // let algo = self.repo().config.diff_algorithm()?;
59+ match self {
60+ Event::Modification {
61+ previous_entry_mode: EntryMode::BlobExecutable | EntryMode::Blob,
62+ previous_id,
63+ entry_mode: EntryMode::BlobExecutable | EntryMode::Blob,
64+ id,
65+ } => match previous_id.object().and_then(|old| id.object().map(|new| (old, new))) {
66+ Ok((old, new)) => Some(Ok(DiffPlatform {
67+ old,
68+ new,
69+ algo: git_diff::text::Algorithm::Myers,
70+ })),
71+ Err(err) => Some(Err(err)),
72+ },
73+ _ => None,
74+ }
75+ }
76+}
77+
78+impl<'old, 'new> DiffPlatform<'old, 'new> {
79+ /// Perform a diff on lines between the old and the new version of a blob.
80+ /// The algorithm is determined by the `diff.algorithm` configuration.
81+ /// Note that the [`Sink`][git_diff::text::imara::Sink] implementation is
82+ /// what makes the diff usable and relies heavily on what the caller requires, as created by `make_sink`.
83+ pub fn lines<FnS, S>(&self, new_sink: FnS) -> S::Out
84+ where
85+ FnS: for<'a> FnOnce(&git_diff::text::imara::intern::InternedInput<&'a [u8]>) -> S,
86+ S: git_diff::text::imara::Sink,
87+ {
88+ git_diff::text::with(
89+ self.old.data.as_bstr(),
90+ self.new.data.as_bstr(),
91+ git_diff::text::Algorithm::Myers, // TODO: use diff.algorithm
92+ // TODO: make use of `core.eol` and/or filters to do line-counting correctly. It's probably
93+ // OK to just know how these objects are saved to know what constitutes a line.
94+ git_diff::text::imara::intern::InternedInput::new,
95+ new_sink,
96+ )
97+ .1
98+ }
99+}
git-repository/src/object/tree/diff/for_each.rs+155−0
@@ -0,0 +1,155 @@
1+use super::{change, Action, Change, Platform, Tracking};
2+use crate::bstr::{BStr, BString, ByteSlice, ByteVec};
3+use crate::ext::ObjectIdExt;
4+use crate::{Repository, Tree};
5+use git_object::TreeRefIter;
6+use git_odb::FindExt;
7+use std::collections::VecDeque;
8+
9+/// The error return by methods on the [diff platform][Platform].
10+#[derive(Debug, thiserror::Error)]
11+#[allow(missing_docs)]
12+pub enum Error {
13+ #[error(transparent)]
14+ Diff(#[from] git_diff::tree::changes::Error),
15+ #[error("The user-provided callback failed")]
16+ ForEach(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
17+}
18+
19+/// Add the item to compare to.
20+impl<'a, 'old> Platform<'a, 'old> {
21+ /// Call `for_each` repeatedly with all changes that are needed to convert the source of the diff to the tree to `other`.
22+ pub fn for_each_to_obtain_tree<'new, E>(
23+ &mut self,
24+ other: &Tree<'new>,
25+ for_each: impl FnMut(Change<'_, 'old, 'new>) -> Result<Action, E>,
26+ ) -> Result<(), Error>
27+ where
28+ E: std::error::Error + Sync + Send + 'static,
29+ {
30+ let repo = self.lhs.repo;
31+ let mut delegate = Delegate {
32+ repo: self.lhs.repo,
33+ other_repo: other.repo,
34+ tracking: self.tracking,
35+ location: BString::default(),
36+ path_deque: Default::default(),
37+ visit: for_each,
38+ err: None,
39+ };
40+ git_diff::tree::Changes::from(TreeRefIter::from_bytes(&self.lhs.data)).needed_to_obtain(
41+ TreeRefIter::from_bytes(&other.data),
42+ &mut self.state,
43+ |oid, buf| repo.objects.find_tree_iter(oid, buf),
44+ &mut delegate,
45+ )?;
46+ match delegate.err {
47+ Some(err) => Err(Error::ForEach(Box::new(err))),
48+ None => Ok(()),
49+ }
50+ }
51+}
52+
53+struct Delegate<'old, 'new, VisitFn, E> {
54+ repo: &'old Repository,
55+ other_repo: &'new Repository,
56+ tracking: Option<Tracking>,
57+ location: BString,
58+ path_deque: VecDeque<BString>,
59+ visit: VisitFn,
60+ err: Option<E>,
61+}
62+
63+impl<A, B> Delegate<'_, '_, A, B> {
64+ fn pop_element(&mut self) {
65+ if let Some(pos) = self.location.rfind_byte(b'/') {
66+ self.location.resize(pos, 0);
67+ } else {
68+ self.location.clear();
69+ }
70+ }
71+
72+ fn push_element(&mut self, name: &BStr) {
73+ if !self.location.is_empty() {
74+ self.location.push(b'/');
75+ }
76+ self.location.push_str(name);
77+ }
78+}
79+
80+impl<'old, 'new, VisitFn, E> git_diff::tree::Visit for Delegate<'old, 'new, VisitFn, E>
81+where
82+ VisitFn: for<'delegate> FnMut(Change<'delegate, 'old, 'new>) -> Result<Action, E>,
83+ E: std::error::Error + Sync + Send + 'static,
84+{
85+ fn pop_front_tracked_path_and_set_current(&mut self) {
86+ if let Some(Tracking::Path) = self.tracking {
87+ self.location = self
88+ .path_deque
89+ .pop_front()
90+ .expect("every call is matched with push_tracked_path_component");
91+ }
92+ }
93+
94+ fn push_back_tracked_path_component(&mut self, component: &BStr) {
95+ if let Some(Tracking::Path) = self.tracking {
96+ self.push_element(component);
97+ self.path_deque.push_back(self.location.clone());
98+ }
99+ }
100+
101+ fn push_path_component(&mut self, component: &BStr) {
102+ match self.tracking {
103+ Some(Tracking::FileName) => {
104+ self.location.clear();
105+ self.location.push_str(component);
106+ }
107+ Some(Tracking::Path) => {
108+ self.push_element(component);
109+ }
110+ None => {}
111+ }
112+ }
113+
114+ fn pop_path_component(&mut self) {
115+ if let Some(Tracking::Path) = self.tracking {
116+ self.pop_element();
117+ }
118+ }
119+
120+ fn visit(&mut self, change: git_diff::tree::visit::Change) -> git_diff::tree::visit::Action {
121+ use git_diff::tree::visit::Change::*;
122+ let event = match change {
123+ Addition { entry_mode, oid } => change::Event::Addition {
124+ entry_mode,
125+ id: oid.attach(self.other_repo),
126+ },
127+ Deletion { entry_mode, oid } => change::Event::Deletion {
128+ entry_mode,
129+ id: oid.attach(self.repo),
130+ },
131+ Modification {
132+ previous_entry_mode,
133+ previous_oid,
134+ entry_mode,
135+ oid,
136+ } => change::Event::Modification {
137+ previous_entry_mode,
138+ entry_mode,
139+ previous_id: previous_oid.attach(self.repo),
140+ id: oid.attach(self.other_repo),
141+ },
142+ };
143+ match (self.visit)(Change {
144+ event,
145+ location: self.location.as_ref(),
146+ }) {
147+ Ok(Action::Cancel) => git_diff::tree::visit::Action::Cancel,
148+ Ok(Action::Continue) => git_diff::tree::visit::Action::Continue,
149+ Err(err) => {
150+ self.err = Some(err);
151+ git_diff::tree::visit::Action::Cancel
152+ }
153+ }
154+ }
155+}
git-repository/src/object/tree/diff/mod.rs+80−0
@@ -0,0 +1,80 @@
1+use crate::{bstr::BStr, Tree};
2+
3+/// Returned by the `for_each` function to control flow.
4+#[derive(Clone, Copy, PartialOrd, PartialEq, Ord, Eq, Hash)]
5+pub enum Action {
6+ /// Continue the traversal of changes.
7+ Continue,
8+ /// Stop the traversal of changes and stop calling this function.
9+ Cancel,
10+}
11+
12+impl Default for Action {
13+ fn default() -> Self {
14+ Action::Continue
15+ }
16+}
17+
18+/// Represents any possible change in order to turn one tree into another.
19+#[derive(Debug, Clone, Copy)]
20+pub struct Change<'a, 'old, 'new> {
21+ /// The location of the file or directory described by `event`, if tracking was enabled.
22+ ///
23+ /// Otherwise this value is always an empty path.
24+ pub location: &'a BStr,
25+ /// The diff event itself to provide information about what would need to change.
26+ pub event: change::Event<'old, 'new>,
27+}
28+
29+///
30+pub mod change;
31+
32+/// Diffing
33+impl<'repo> Tree<'repo> {
34+ /// Return a platform to see the changes needed to create other trees, for instance.
35+ ///
36+ /// # Performance
37+ ///
38+ /// It's highly recommended to set an object cache to avoid extracting the same object multiple times.
39+ pub fn changes<'a>(&'a self) -> Platform<'a, 'repo> {
40+ Platform {
41+ state: Default::default(),
42+ lhs: self,
43+ tracking: None,
44+ }
45+ }
46+}
47+
48+/// The diffing platform returned by [`Tree::changes()`].
49+#[derive(Clone)]
50+pub struct Platform<'a, 'repo> {
51+ state: git_diff::tree::State,
52+ lhs: &'a Tree<'repo>,
53+ tracking: Option<Tracking>,
54+}
55+
56+#[derive(Clone, Copy)]
57+enum Tracking {
58+ FileName,
59+ Path,
60+}
61+
62+/// Configuration
63+impl<'a, 'repo> Platform<'a, 'repo> {
64+ /// Keep track of file-names, which makes the [`location`][Change::location] field usable with the filename of the changed item.
65+ pub fn track_filename(&mut self) -> &mut Self {
66+ self.tracking = Some(Tracking::FileName);
67+ self
68+ }
69+
70+ /// Keep track of the entire path of a change, relative to the repository.
71+ ///
72+ /// This makes the [`location`][Change::location] field usable.
73+ pub fn track_path(&mut self) -> &mut Self {
74+ self.tracking = Some(Tracking::Path);
75+ self
76+ }
77+}
78+
79+///
80+pub mod for_each;

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t