Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/but/skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ but <mutation> ... --status-after

- Commit: `but commit <branch> -m "<msg>" --changes <id>,<id> --status-after`
- Commit + create branch: `but commit <branch> -c -m "<msg>" --changes <id> --status-after`
- Amend: `but amend <file-id> <commit-id> --status-after`
- Amend: `but amend <file-id>[,<file-id>...] <commit-id> --status-after`
- Reorder commits: `but move <source-commit-id> <target-commit-id> --status-after` (**commit IDs**, not branch names)
- Stack branches: `but move <branch-name-or-id> <target-branch-name-or-id> --status-after` (**branch names or branch CLI IDs**)
- Tear off a branch: `but move <branch-name-or-id> zz --status-after` (`zz` = unassigned; branch name or branch CLI ID)
Expand All @@ -62,7 +62,7 @@ but <mutation> ... --status-after

1. `but status -fv` (or `but show <branch-id>`)
2. Locate file ID and target commit ID.
3. `but amend <file-id> <commit-id> --status-after`
3. `but amend <file-id>[,<file-id>...] <commit-id> --status-after`

### Reorder commits

Expand Down
4 changes: 2 additions & 2 deletions crates/but/skill/references/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,8 @@ but squash <branch> --status-after # Squash then show workspace status
Amend file into a specific commit. Use when you know exactly which commit the change belongs to.

```bash
but amend <file-id> <commit-id> # Amend file into specific commit
but amend <file-id> <commit-id> --status-after # Amend then show workspace status
but amend <file-id>[,<file-id>...] <commit-id> # Amend file(s) into specific commit
but amend <file-id>[,<file-id>...] <commit-id> --status-after # Amend then show workspace status
```

**When to use `amend` vs `absorb`:**
Expand Down
9 changes: 2 additions & 7 deletions crates/but/src/command/legacy/rub/amend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub(crate) fn uncommitted_to_commit_with_perm(
oid: ObjectId,
out: &mut OutputChannel,
perm: &mut RepoExclusive,
) -> anyhow::Result<()> {
) -> anyhow::Result<Option<ObjectId>> {
let first_hunk_assignment = hunk_assignments.first();
let stack_id = first_hunk_assignment.stack_id;

Expand All @@ -41,13 +41,8 @@ pub(crate) fn uncommitted_to_commit_with_perm(
})
.unwrap_or_default();
writeln!(out, "Amended {description} → {new_commit}")?;
} else if let Some(out) = out.for_json() {
out.write_value(serde_json::json!({
"ok": true,
"new_commit_id": outcome.new_commit.map(|c| c.to_string()),
}))?;
}
Ok(())
Ok(outcome.new_commit)
}

fn amend_diff_specs(
Expand Down
28 changes: 26 additions & 2 deletions crates/but/src/command/legacy/rub/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,8 @@ pub(crate) fn handle_amend(
// Validate that commit is a commit
match commit {
CliId::Commit { commit_id, .. } => {
let mut commit_id = commit_id;
let mut amended_commit_ids = Vec::new();
// TODO(dp by st): This is a duplication of the UncommittedToCommitOperation which was previously
// called through `handle()` after validation. The problem is that it does its own locking.
// Since these are all mutations, it would have to be changed to take `perm` as well.
Expand All @@ -1455,18 +1457,40 @@ pub(crate) fn handle_amend(
OperationKind::AmendCommit,
guard.write_permission(),
);
amend::uncommitted_to_commit_with_perm(
if let Some(new_commit_id) = amend::uncommitted_to_commit_with_perm(
ctx,
uncommitted.hunk_assignments.as_ref(),
uncommitted.describe(),
commit_id,
out,
guard.write_permission(),
)?;
)? {
commit_id = new_commit_id;
amended_commit_ids.push(Some(new_commit_id));
} else {
amended_commit_ids.push(None);
}
}
_ => unreachable!("validated beforehand"),
}
}
if let Some(out) = out.for_json() {
let last_successful_commit_id = amended_commit_ids
.iter()
.rev()
.copied()
.flatten()
.next()
.map(|c| c.to_string());
out.write_value(serde_json::json!({
"ok": true,
"new_commit_id": last_successful_commit_id,
"new_commit_ids": amended_commit_ids
.into_iter()
.map(|commit_id| commit_id.map(|c| c.to_string()))
.collect::<Vec<_>>(),
}))?;
}
}
other => {
bail!(
Expand Down
121 changes: 121 additions & 0 deletions crates/but/tests/but/command/rub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1137,6 +1137,127 @@ Amended [..] → [..]
Ok(())
}

#[test]
fn amend_multiple_uncommitted_files_to_commit() -> anyhow::Result<()> {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks")?;
env.setup_metadata(&["A", "B"])?;

env.file("first-uncommitted.txt", "first\n");
env.file("second-uncommitted.txt", "second\n");

let before = status_json(&env)?;
let first_file = unassigned_cli_id_for_file(&before, "first-uncommitted.txt")
.expect("first uncommitted file should be present");
let second_file = unassigned_cli_id_for_file(&before, "second-uncommitted.txt")
.expect("second uncommitted file should be present");
let target_commit = branch_commit_ids(&before, "A")[0].clone();

env.but(format!("amend {first_file},{second_file} {target_commit}"))
.assert()
.success()
.stdout_eq(str![[r#"
Amended the only hunk in first-uncommitted.txt in the unassigned area → [..]
Amended the only hunk in second-uncommitted.txt in the unassigned area → [..]

"#]])
.stderr_eq(str![""]);

let after = status_json(&env)?;
assert!(
branch_commits_contain_file(&after, "A", "first-uncommitted.txt"),
"first file should be amended into the target commit"
);
assert!(
branch_commits_contain_file(&after, "A", "second-uncommitted.txt"),
"second file should be amended into the target commit"
);
assert!(
!unassigned_contains_file(&after, "first-uncommitted.txt"),
"first file should no longer be unassigned"
);
assert!(
!unassigned_contains_file(&after, "second-uncommitted.txt"),
"second file should no longer be unassigned"
);

Ok(())
}

#[test]
fn amend_multiple_uncommitted_files_to_commit_status_after_json() -> anyhow::Result<()> {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks")?;
env.setup_metadata(&["A", "B"])?;

env.file("first-uncommitted.txt", "first\n");
env.file("second-uncommitted.txt", "second\n");

let before = status_json(&env)?;
let first_file = unassigned_cli_id_for_file(&before, "first-uncommitted.txt")
.expect("first uncommitted file should be present");
let second_file = unassigned_cli_id_for_file(&before, "second-uncommitted.txt")
.expect("second uncommitted file should be present");
let target_commit = branch_commit_ids(&before, "A")[0].clone();

let output = env
.but(format!(
"--json --status-after amend {first_file},{second_file} {target_commit}"
))
.allow_json()
.output()?;
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stderr), "");

let value: serde_json::Value = serde_json::from_slice(&output.stdout)?;
let amended_commits = value["result"]["new_commit_ids"]
.as_array()
.expect("amend result should contain all rewritten commit ids");
assert_eq!(amended_commits.len(), 2);
assert!(value["result"]["new_commit_id"].as_str().is_some());
assert!(value["status"]["stacks"].as_array().is_some());

let after = status_json(&env)?;
assert!(
branch_commits_contain_file(&after, "A", "first-uncommitted.txt"),
"first file should be amended into the target commit"
);
assert!(
branch_commits_contain_file(&after, "A", "second-uncommitted.txt"),
"second file should be amended into the target commit"
);

Ok(())
}

#[test]
fn amend_duplicate_uncommitted_file_reports_last_successful_commit_json() -> anyhow::Result<()> {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks")?;
env.setup_metadata(&["A", "B"])?;

env.file("duplicate-uncommitted.txt", "content\n");

let before = status_json(&env)?;
let file_id = unassigned_cli_id_for_file(&before, "duplicate-uncommitted.txt")
.expect("uncommitted file should be present");
let target_commit = branch_commit_ids(&before, "A")[0].clone();

let output = env
.but(format!("--json amend {file_id},{file_id} {target_commit}"))
.allow_json()
.output()?;
assert!(output.status.success());
assert_eq!(String::from_utf8_lossy(&output.stderr), "");

let value: serde_json::Value = serde_json::from_slice(&output.stdout)?;
let amended_commits = value["new_commit_ids"]
.as_array()
.expect("amend result should contain all attempted commit ids");
assert_eq!(amended_commits.len(), 2);
assert_eq!(value["new_commit_id"], amended_commits[0]);
assert!(amended_commits[1].is_null());

Ok(())
}

#[test]
fn rub_matrix_uncommitted_to_stack_smoke() -> anyhow::Result<()> {
let env = Sandbox::init_scenario_with_target_and_default_settings("two-stacks")?;
Expand Down
Loading