Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

git: Add ability to view commit history #26226

Closed
Closed
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
74 changes: 74 additions & 0 deletions crates/git/src/commit_history.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use crate::repository::CommitDetails;
use anyhow::{anyhow, Result};
use std::{path::Path, process::Stdio};

pub struct CommitHistory {}

impl CommitHistory {
pub fn list(
git_binary: &Path,
working_directory: &Path,
skip: i32,
limit: i32,
) -> Result<Vec<CommitDetails>> {
const COMMIT_WRAPPER_START: &str = "<COMMIT_START>";
const COMMIT_WRAPPER_END: &str = "<COMMIT_END>";
const DATA_MARKER: &str = "<DATA_MARKER>";
// "--format=%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B",
let child = util::command::new_std_command(git_binary)
.current_dir(working_directory)
.arg("log")
.arg(format!(
"--format={}%H<DATA_MARKER>%aN<DATA_MARKER>%aE<DATA_MARKER>%ct<DATA_MARKER>%B{}%n",
COMMIT_WRAPPER_START, COMMIT_WRAPPER_END
))
.arg("-z")
.arg(format!("-n{}", limit))
.arg(format!("--skip={}", skip))
.args(["--topo-order", "--decorate=full"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| anyhow!("Failed to start git commit history process: {e}"))?;

let output = child
.wait_with_output()
.map_err(|e| anyhow!("Failed to read git commit history output: {e}"))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("git commit history process failed: {stderr}"));
}

let stdout = String::from_utf8_lossy(&output.stdout);
let commit_history = stdout
.split('\0')
.filter_map(|commit| {
let trimmed_commit = commit
.trim()
.replace(COMMIT_WRAPPER_START, "")
.replace(COMMIT_WRAPPER_END, "");
if trimmed_commit == "" || trimmed_commit == " " {
return None;
};
let records: Vec<String> = trimmed_commit
.split(DATA_MARKER)
.map(|s| s.trim().to_string())
.collect();
if records.len() >= 4 {
return Some(CommitDetails {
sha: records[0].to_string().into(),
committer_name: records[1].to_string().into(),
committer_email: records[2].to_string().into(),
commit_timestamp: records[3].to_string().parse::<i64>().unwrap_or(0),
message: records[4].to_string().into(),
});
} else {
return None;
}
})
.collect::<Vec<CommitDetails>>();
Ok(commit_history.into())
}
}
1 change: 1 addition & 0 deletions crates/git/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod blame;
pub mod commit;
pub mod commit_history;
mod hosting_provider;
mod remote;
pub mod repository;
Expand Down
21 changes: 20 additions & 1 deletion crates/git/src/repository.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::status::FileStatus;
use crate::GitHostingProviderRegistry;
use crate::{blame::Blame, status::GitStatus};
use crate::{blame::Blame, commit_history::CommitHistory, status::GitStatus};
use anyhow::{anyhow, Context, Result};
use askpass::{AskPassResult, AskPassSession};
use collections::{HashMap, HashSet};
Expand Down Expand Up @@ -162,6 +162,8 @@ pub trait GitRepository: Send + Sync {
/// Returns the list of git statuses, sorted by path
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;

fn commit_history(&self, skip: i32, limit: i32) -> Result<Vec<CommitDetails>>;

fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
fn create_branch(&self, _: &str) -> Result<()>;
Expand Down Expand Up @@ -471,6 +473,17 @@ impl GitRepository for RealGitRepository {
GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
}

fn commit_history(&self, skip: i32, limit: i32) -> Result<Vec<CommitDetails>> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();

CommitHistory::list(&self.git_binary_path, &working_directory, skip, limit)
}

fn branch_exits(&self, name: &str) -> Result<bool> {
let repo = self.repository.lock();
let branch = repo.find_branch(name, BranchType::Local);
Expand Down Expand Up @@ -993,6 +1006,12 @@ impl GitRepository for FakeGitRepository {
})
}

fn commit_history(&self, skip: i32, limit: i32) -> Result<Vec<CommitDetails>> {
let _ = limit;
let _ = skip;
Ok(Vec::new())
}

fn branches(&self) -> Result<Vec<Branch>> {
let state = self.state.lock();
let current_branch = &state.current_branch_name;
Expand Down
75 changes: 75 additions & 0 deletions crates/project/src/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ impl GitStore {
client.add_entity_request_handler(Self::handle_askpass);
client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
client.add_entity_request_handler(Self::handle_git_diff);
client.add_entity_request_handler(Self::handle_commit_history);
}

pub fn active_repository(&self) -> Option<Entity<Repository>> {
Expand Down Expand Up @@ -663,6 +664,37 @@ impl GitStore {
Ok(proto::Ack {})
}

async fn handle_commit_history(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitGetCommitHistory>,
mut cx: AsyncApp,
) -> Result<proto::GitCommitHistoryResponse> {
let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
let repository_handle =
Self::repository_for_request(&this, worktree_id, work_directory_id, &mut cx)?;

let commits = repository_handle
.update(&mut cx, |repository_handle, _| {
repository_handle.get_commit_history(envelope.payload.skip, envelope.payload.limit)
})?
.await??;

Ok(proto::GitCommitHistoryResponse {
commits: commits
.clone()
.into_iter()
.map(|commit| proto::GitCommitDetails {
committer_name: commit.committer_name.into(),
commit_timestamp: commit.commit_timestamp.into(),
committer_email: commit.committer_email.into(),
sha: commit.sha.into(),
message: commit.message.into(),
})
.collect::<Vec<_>>(),
})
}

async fn handle_show(
this: Entity<Self>,
envelope: TypedEnvelope<proto::GitShow>,
Expand Down Expand Up @@ -1662,6 +1694,49 @@ impl Repository {
})
}

pub fn get_commit_history(
&self,
skip: i32,
limit: i32,
) -> oneshot::Receiver<Result<Vec<CommitDetails>>> {
self.send_job(move |repo| async move {
match repo {
GitRepo::Local(git_repository) => git_repository.commit_history(skip, limit),
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
let response = client
.request(proto::GitGetCommitHistory {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
skip: skip,
limit: limit,
})
.await?;

let commits = response
.commits
.clone()
.into_iter()
.map(|commit| CommitDetails {
committer_name: commit.committer_name.into(),
commit_timestamp: commit.commit_timestamp.into(),
committer_email: commit.committer_email.into(),
sha: commit.sha.into(),
message: commit.message.into(),
})
.collect();

Ok(commits)
}
}
})
}

pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
self.send_job(|repo| async move {
match repo {
Expand Down
16 changes: 15 additions & 1 deletion crates/proto/proto/zed.proto
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,9 @@ message Envelope {
AskPassResponse ask_pass_response = 318;

GitDiff git_diff = 319;
GitDiffResponse git_diff_response = 320; // current max
GitDiffResponse git_diff_response = 320;
GitGetCommitHistory git_get_commit_history = 321;
GitCommitHistoryResponse git_commit_history_response = 322; // current max
}

reserved 87 to 88;
Expand Down Expand Up @@ -2709,6 +2711,10 @@ message GitBranchesResponse {
repeated Branch branches = 1;
}

message GitCommitHistoryResponse {
repeated GitCommitDetails commits = 1;
}

message UpdateGitBranch {
uint64 project_id = 1;
string branch_name = 2;
Expand Down Expand Up @@ -2937,3 +2943,11 @@ message GitDiff {
message GitDiffResponse {
string diff = 1;
}

message GitGetCommitHistory {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
int32 skip = 4;
int32 limit = 5;
}
4 changes: 4 additions & 0 deletions crates/proto/src/proto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,8 @@ messages!(
(CheckForPushedCommitsResponse, Background),
(GitDiff, Background),
(GitDiffResponse, Background),
(GitGetCommitHistory, Background),
(GitCommitHistoryResponse, Background),
);

request_messages!(
Expand Down Expand Up @@ -607,6 +609,7 @@ request_messages!(
(GitChangeBranch, Ack),
(CheckForPushedCommits, CheckForPushedCommitsResponse),
(GitDiff, GitDiffResponse),
(GitGetCommitHistory, GitCommitHistoryResponse)
);

entity_messages!(
Expand Down Expand Up @@ -713,6 +716,7 @@ entity_messages!(
GitCreateBranch,
CheckForPushedCommits,
GitDiff,
GitGetCommitHistory
);

entity_messages!(
Expand Down