chadtmiller.com/knotless
git that lives in your Atmosphere account
1
10
A merge commit's page shows what the merge brought in
crates/knotless-serve/src/web/repo.rs+4−2
| @@ -695,7 +695,9 @@ pub struct Change { | ||
files: Vec<FileDiffView>, | ||
} | ||
| ||
/// `dev.knotless.getCommit`. | ||
/// `dev.knotless.getCommit`. A merge's diff is against its first parent: | ||
/// what it brought into the branch it was made on, where git's own would | ||
/// show only what differs from both, nothing at all for a clean merge. | ||
pub async fn get_commit( | ||
State(app): State<Arc<App>>, | ||
Query(p): Query<RepoParams>, | ||
| @@ -707,7 +709,7 @@ pub async fn get_commit( | ||
let c = shown(&app, &repo, log).await.remove(0); | ||
let files = files( | ||
&repo.git, | ||
&["show", "--format=", "--patch"], | ||
&["show", "--format=", "--patch", "--diff-merges=first-parent"], | ||
&c.sha, | ||
&[], | ||
Rows::All, | ||
crates/knotless-serve/tests/web.rs+36−0
| @@ -882,6 +882,42 @@ fn browsing_a_repository() { | ||
} | ||
} | ||
| ||
/// A merge commit's page shows what it brought in, against its first | ||
/// parent, and not what its branch did meanwhile. | ||
#[test] | ||
fn a_merge_commit_shows_what_it_brought_in() { | ||
let site = serve(true); | ||
let s = person(account(), &site); | ||
let url = s.url("merged"); | ||
let work = s.work("w", 0); | ||
write(&work, "begun.txt", b"begun\n"); | ||
s.git(&work, &["add", "-A"]); | ||
s.git(&work, &["commit", "-q", "-m", "Begin"]); | ||
s.git(&work, &["checkout", "-q", "-b", "topic"]); | ||
write(&work, "brought.txt", b"brought\n"); | ||
s.git(&work, &["add", "-A"]); | ||
s.git(&work, &["commit", "-q", "-m", "Bring"]); | ||
s.git(&work, &["checkout", "-q", "main"]); | ||
write(&work, "meanwhile.txt", b"meanwhile\n"); | ||
s.git(&work, &["add", "-A"]); | ||
s.git(&work, &["commit", "-q", "-m", "Meanwhile"]); | ||
s.git( | ||
&work, | ||
&["merge", "-q", "--no-ff", "-m", "Merge topic", "topic"], | ||
); | ||
let merge = s.git(&work, &["rev-parse", "HEAD"]); | ||
s.git(&work, &["push", "-q", &url, "main"]); | ||
| ||
let uri = format!("at://{}/dev.knotless.repo/merged", s.account.did); | ||
let commit = xrpc("getCommit", &[("repo", &uri), ("sha", &merge)]); | ||
wait_until(|| site.status(&commit).0 == 200); | ||
let change = site.json(&commit); | ||
let files = change["files"].as_array().unwrap(); | ||
assert_eq!(files.len(), 1, "{change}"); | ||
assert_eq!(files[0]["new"], "brought.txt"); | ||
assert_eq!(files[0]["added"], 1); | ||
} | ||
| ||
/// Who last changed each line of a file, as the blame page shows it. | ||
#[test] | ||
fn blaming_a_file() { | ||
docs/reference/api.md+1−1
| @@ -571,7 +571,7 @@ name it has now. | ||
| `dev.knotless.getFile?repo=&ref=&path=` | a file, highlighted or rendered | | ||
| `dev.knotless.getBlame?repo=&ref=&path=` | who last changed each line of a text file, as `git blame` finds it: its text, highlighted; its lines in runs (`hunks`), each with its commit and, where the commit's parent had the file, `previous`, that parent and the file's path there; and each commit once. 400 for a binary or a file over 256 KiB | | ||
| `dev.knotless.listCommits?repo=&ref=&path=&limit=&cursor=&subjectOnly=` | history, 50 commits a page by default, with the newest checks of each; with `subjectOnly`, each message only to its first line | | ||
| `dev.knotless.getCommit?repo=&sha=` | a commit and its diff | | ||
| `dev.knotless.getCommit?repo=&sha=` | a commit and its diff, a merge's against its first parent | | ||
| `dev.knotless.listBranches?repo=&limit=&cursor=&commits=` | the branches, the default first, 100 a page by default; with `commits`, each with its tip and that commit's newest checks | | ||
| `dev.knotless.listTags?repo=&limit=&cursor=&commits=` | the tags that name a commit, the newest commit first, 100 a page by default; with `commits`, each with its commit and that commit's newest checks | | ||
| `dev.knotless.listItems?repo=&kind=&state=&q=&limit=&cursor=` | the open or closed pulls or issues that the filter `q` matches ([collaboration.md](../design/collaboration.md#filtering-pulls-and-issues)), `review-requested:@me` and `assignee:@me` the asker, each with `draft` for a draft pull, the `reviewers` asked, its base's required reviewers among them and named again in `requiredReviewers`, and the `assignees`, 50 a page by default, how many of each, and the repository's labels and the items' authors, for a filter to name | | ||