Skip to content

Commit 853a29b

Browse files
authored
Merge pull request #10437 from gitbutlerapp/kv-branch-55
Support rubbing of committed files
2 parents 5df41d2 + 74c9ffe commit 853a29b

File tree

4 files changed

+583
-136
lines changed

4 files changed

+583
-136
lines changed

crates/but/src/id/mod.rs

Lines changed: 130 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,37 +61,134 @@ impl CliId {
6161
}
6262
}
6363

64+
fn find_branches_by_name(ctx: &CommandContext, name: &str) -> anyhow::Result<Vec<Self>> {
65+
let stacks = crate::log::stacks(ctx)?;
66+
let mut matches = Vec::new();
67+
68+
for stack in stacks {
69+
for head in &stack.heads {
70+
let branch_name = head.name.to_string();
71+
// Exact match or partial match
72+
if branch_name == name || branch_name.contains(name) {
73+
matches.push(CliId::branch(&branch_name));
74+
}
75+
}
76+
}
77+
78+
Ok(matches)
79+
}
80+
81+
fn find_commits_by_sha(ctx: &CommandContext, sha_prefix: &str) -> anyhow::Result<Vec<Self>> {
82+
let mut matches = Vec::new();
83+
84+
// Only try SHA matching if the input looks like a hex string
85+
if sha_prefix.chars().all(|c| c.is_ascii_hexdigit()) && sha_prefix.len() >= 4 {
86+
let all_commits = crate::log::all_commits(ctx)?;
87+
for commit_id in all_commits {
88+
if let CliId::Commit { oid } = &commit_id {
89+
let sha_string = oid.to_string();
90+
if sha_string.starts_with(sha_prefix) {
91+
matches.push(commit_id);
92+
}
93+
}
94+
}
95+
}
96+
97+
Ok(matches)
98+
}
99+
64100
pub fn matches(&self, s: &str) -> bool {
65101
s == self.to_string()
66102
}
67103

104+
pub fn matches_prefix(&self, s: &str) -> bool {
105+
match self {
106+
CliId::Commit { oid } => {
107+
let oid_hash = hash(&oid.to_string());
108+
oid_hash.starts_with(s)
109+
}
110+
_ => self.to_string().starts_with(s),
111+
}
112+
}
113+
68114
pub fn from_str(ctx: &mut CommandContext, s: &str) -> anyhow::Result<Vec<Self>> {
69115
if s.len() < 2 {
70-
return Err(anyhow::anyhow!("Id needs to be 3 characters long: {}", s));
116+
return Err(anyhow::anyhow!(
117+
"Id needs to be at least 2 characters long: {}",
118+
s
119+
));
71120
}
72-
let s = &s[..2];
73-
let mut everything = Vec::new();
74-
crate::status::all_files(ctx)?
75-
.into_iter()
76-
.filter(|id| id.matches(s))
77-
.for_each(|id| everything.push(id));
78-
crate::status::all_branches(ctx)?
79-
.into_iter()
80-
.filter(|id| id.matches(s))
81-
.for_each(|id| everything.push(id));
82-
crate::log::all_commits(ctx)?
83-
.into_iter()
84-
.filter(|id| id.matches(s))
85-
.for_each(|id| everything.push(id));
86-
everything.push(CliId::unassigned());
87121

88122
let mut matches = Vec::new();
89-
for id in everything {
90-
if id.matches(s) {
91-
matches.push(id);
123+
124+
// First, try exact branch name match
125+
if let Ok(branch_matches) = Self::find_branches_by_name(ctx, s) {
126+
matches.extend(branch_matches);
127+
}
128+
129+
// Then try partial SHA matches (for commits)
130+
if let Ok(commit_matches) = Self::find_commits_by_sha(ctx, s) {
131+
matches.extend(commit_matches);
132+
}
133+
134+
// Then try CliId matching (both prefix and exact)
135+
if s.len() > 2 {
136+
// For longer strings, try prefix matching on CliIds
137+
let mut cli_matches = Vec::new();
138+
crate::status::all_files(ctx)?
139+
.into_iter()
140+
.filter(|id| id.matches_prefix(s))
141+
.for_each(|id| cli_matches.push(id));
142+
crate::status::all_committed_files(ctx)?
143+
.into_iter()
144+
.filter(|id| id.matches_prefix(s))
145+
.for_each(|id| cli_matches.push(id));
146+
crate::status::all_branches(ctx)?
147+
.into_iter()
148+
.filter(|id| id.matches_prefix(s))
149+
.for_each(|id| cli_matches.push(id));
150+
crate::log::all_commits(ctx)?
151+
.into_iter()
152+
.filter(|id| id.matches_prefix(s))
153+
.for_each(|id| cli_matches.push(id));
154+
if CliId::unassigned().matches_prefix(s) {
155+
cli_matches.push(CliId::unassigned());
156+
}
157+
matches.extend(cli_matches);
158+
} else {
159+
// For 2-character strings, try exact CliId matching
160+
let mut cli_matches = Vec::new();
161+
crate::status::all_files(ctx)?
162+
.into_iter()
163+
.filter(|id| id.matches(s))
164+
.for_each(|id| cli_matches.push(id));
165+
crate::status::all_committed_files(ctx)?
166+
.into_iter()
167+
.filter(|id| id.matches(s))
168+
.for_each(|id| cli_matches.push(id));
169+
crate::status::all_branches(ctx)?
170+
.into_iter()
171+
.filter(|id| id.matches(s))
172+
.for_each(|id| cli_matches.push(id));
173+
crate::log::all_commits(ctx)?
174+
.into_iter()
175+
.filter(|id| id.matches(s))
176+
.for_each(|id| cli_matches.push(id));
177+
if CliId::unassigned().matches(s) {
178+
cli_matches.push(CliId::unassigned());
92179
}
180+
matches.extend(cli_matches);
93181
}
94-
Ok(matches)
182+
183+
// Remove duplicates while preserving order
184+
let mut unique_matches = Vec::new();
185+
for m in matches {
186+
if !unique_matches.contains(&m) {
187+
unique_matches.push(m);
188+
}
189+
}
190+
191+
Ok(unique_matches)
95192
}
96193
}
97194

@@ -117,6 +214,8 @@ impl Display for CliId {
117214
write!(f, "00")
118215
}
119216
CliId::Commit { oid } => {
217+
// let oid_str = oid.to_string();
218+
// write!(f, "{}", hash(&oid_str))
120219
let oid = oid.to_string();
121220
write!(f, "{}", &oid[..2])
122221
}
@@ -129,12 +228,15 @@ pub(crate) fn hash(input: &str) -> String {
129228
for byte in input.bytes() {
130229
hash = hash.wrapping_mul(31).wrapping_add(byte as u64);
131230
}
132-
// Convert to base 36 (0-9, a-z)
133-
let chars = "0123456789abcdefghijklmnopqrstuvwxyz";
134-
let mut result = String::new();
135-
for _ in 0..2 {
136-
result.push(chars.chars().nth((hash % 36) as usize).unwrap());
137-
hash /= 36;
138-
}
139-
result
231+
232+
// First character: g-z (20 options)
233+
let first_chars = "ghijklmnopqrstuvwxyz";
234+
let first_char = first_chars.chars().nth((hash % 20) as usize).unwrap();
235+
hash /= 20;
236+
237+
// Second character: 0-9,a-z (36 options)
238+
let second_chars = "0123456789abcdefghijklmnopqrstuvwxyz";
239+
let second_char = second_chars.chars().nth((hash % 36) as usize).unwrap();
240+
241+
format!("{first_char}{second_char}")
140242
}

crates/but/src/rub/commits.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
use std::collections::HashSet;
2+
3+
use anyhow::{Context, Result};
4+
use bstr::ByteSlice;
5+
use but_core::diff::tree_changes;
6+
use but_hunk_assignment::HunkAssignmentRequest;
7+
use but_workspace::DiffSpec;
8+
use gitbutler_branch_actions::update_workspace_commit;
9+
use gitbutler_command_context::CommandContext;
10+
use gitbutler_stack::VirtualBranchesHandle;
11+
12+
use crate::rub::{assign::branch_name_to_stack_id, undo::stack_id_by_commit_id};
13+
14+
pub fn commited_file_to_another_commit(
15+
ctx: &mut CommandContext,
16+
path: &str,
17+
source_id: gix::ObjectId,
18+
target_id: gix::ObjectId,
19+
) -> Result<()> {
20+
let source_stack = stack_id_by_commit_id(ctx, &source_id)?;
21+
let target_stack = stack_id_by_commit_id(ctx, &target_id)?;
22+
23+
let repo = ctx.gix_repo()?;
24+
let source_commit = repo.find_commit(source_id)?;
25+
let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?;
26+
27+
let (tree_changes, _) = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?;
28+
let relevant_changes = tree_changes
29+
.into_iter()
30+
.filter(|tc| tc.path.to_str_lossy() == path)
31+
.map(Into::into)
32+
.collect::<Vec<DiffSpec>>();
33+
34+
but_workspace::move_changes_between_commits(
35+
ctx,
36+
source_stack,
37+
source_id,
38+
target_stack,
39+
target_id,
40+
relevant_changes,
41+
ctx.app_settings().context_lines,
42+
)?;
43+
44+
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
45+
update_workspace_commit(&vb_state, ctx)?;
46+
47+
println!("Moved files between commits!");
48+
49+
Ok(())
50+
}
51+
52+
pub fn uncommit_file(
53+
ctx: &mut CommandContext,
54+
path: &str,
55+
source_id: gix::ObjectId,
56+
target_branch: Option<&str>,
57+
) -> Result<()> {
58+
let source_stack = stack_id_by_commit_id(ctx, &source_id)?;
59+
60+
let repo = ctx.gix_repo()?;
61+
62+
let source_commit = repo.find_commit(source_id)?;
63+
let source_commit_parent_id = source_commit.parent_ids().next().context("First parent")?;
64+
65+
let (tree_changes, _) = tree_changes(&repo, Some(source_commit_parent_id.detach()), source_id)?;
66+
let relevant_changes = tree_changes
67+
.into_iter()
68+
.filter(|tc| tc.path.to_str_lossy() == path)
69+
.map(Into::into)
70+
.collect::<Vec<DiffSpec>>();
71+
72+
// If we want to assign the changes after uncommitting, we could try to do
73+
// something with the hunk headers, but this is not precise as the hunk
74+
// headers might have changed from what they were like when they were
75+
// committed.
76+
//
77+
// As such, we take all the old assignments, and all the new assignments from after the
78+
// uncommit, and find the ones that are not present in the old assignments.
79+
// We then convert those into assignment requests for the given stack.
80+
let before_assignments = but_hunk_assignment::assignments_with_fallback(
81+
ctx,
82+
false,
83+
None::<Vec<but_core::TreeChange>>,
84+
None,
85+
)?
86+
.0;
87+
88+
but_workspace::remove_changes_from_commit_in_stack(
89+
ctx,
90+
source_stack,
91+
source_id,
92+
relevant_changes,
93+
ctx.app_settings().context_lines,
94+
)?;
95+
96+
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
97+
update_workspace_commit(&vb_state, ctx)?;
98+
99+
let (after_assignments, _) = but_hunk_assignment::assignments_with_fallback(
100+
ctx,
101+
false,
102+
None::<Vec<but_core::TreeChange>>,
103+
None,
104+
)?;
105+
106+
let before_assignments = before_assignments
107+
.into_iter()
108+
.filter_map(|a| a.id)
109+
.collect::<HashSet<_>>();
110+
111+
if let Some(target_branch) = target_branch {
112+
let target_stack = branch_name_to_stack_id(ctx, Some(target_branch))?;
113+
let to_assign = after_assignments
114+
.into_iter()
115+
.filter(|a| a.id.is_some_and(|id| !before_assignments.contains(&id)))
116+
.map(|a| HunkAssignmentRequest {
117+
hunk_header: a.hunk_header,
118+
path_bytes: a.path_bytes,
119+
stack_id: target_stack,
120+
})
121+
.collect::<Vec<_>>();
122+
123+
but_hunk_assignment::assign(ctx, to_assign, None)?;
124+
}
125+
126+
println!("Uncommitted changes");
127+
128+
Ok(())
129+
}

0 commit comments

Comments
 (0)