Cargo.lock 🔗
@@ -6162,6 +6162,7 @@ dependencies = [
"anyhow",
"askpass",
"buffer_diff",
+ "call",
"chrono",
"collections",
"command_palette_hooks",
Michael Sloan , Conrad Irwin , and Conrad created
Release Notes:
- Automatic population of `Co-authored-by` now uses `git config --global
user.email`
---------
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Conrad <conrad@zed.dev>
Cargo.lock | 1
crates/channel/src/channel_store_tests.rs | 3
crates/client/src/client.rs | 12
crates/client/src/user.rs | 6
crates/collab/migrations.sqlite/20221109000000_test_schema.sql | 4
crates/collab/migrations/20250612153105_add_collaborator_commit_email.sql | 4
crates/collab/src/db.rs | 4
crates/collab/src/db/queries/buffers.rs | 8
crates/collab/src/db/queries/channels.rs | 1
crates/collab/src/db/queries/projects.rs | 36
crates/collab/src/db/queries/rooms.rs | 4
crates/collab/src/db/tables/project_collaborator.rs | 2
crates/collab/src/db/tests/buffer_tests.rs | 4
crates/collab/src/rpc.rs | 32
crates/collab/src/tests/following_tests.rs | 2
crates/collab/src/tests/integration_tests.rs | 2
crates/collab_ui/src/chat_panel.rs | 3
crates/git/src/repository.rs | 46
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/commit_modal.rs | 1
crates/git_ui/src/git_panel.rs | 71
crates/project/src/project.rs | 4
crates/proto/proto/call.proto | 2
crates/proto/proto/core.proto | 4
24 files changed, 188 insertions(+), 69 deletions(-)
@@ -6162,6 +6162,7 @@ dependencies = [
"anyhow",
"askpass",
"buffer_diff",
+ "call",
"chrono",
"collections",
"command_palette_hooks",
@@ -269,7 +269,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
name: None,
- email: None,
}],
},
);
@@ -323,7 +322,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "maxbrunsfeld".into(),
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
name: None,
- email: None,
}],
},
);
@@ -368,7 +366,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
github_login: "as-cii".into(),
avatar_url: "http://avatar.com/as-cii".into(),
name: None,
- email: None,
}],
},
);
@@ -1887,8 +1887,16 @@ mod tests {
.set_entity(&entity3, &mut cx.to_async());
drop(subscription3);
- server.send(proto::JoinProject { project_id: 1 });
- server.send(proto::JoinProject { project_id: 2 });
+ server.send(proto::JoinProject {
+ project_id: 1,
+ committer_name: None,
+ committer_email: None,
+ });
+ server.send(proto::JoinProject {
+ project_id: 2,
+ committer_name: None,
+ committer_email: None,
+ });
done_rx1.recv().await.unwrap();
done_rx2.recv().await.unwrap();
}
@@ -49,7 +49,6 @@ pub struct User {
pub github_login: String,
pub avatar_uri: SharedUri,
pub name: Option<String>,
- pub email: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -58,6 +57,8 @@ pub struct Collaborator {
pub replica_id: ReplicaId,
pub user_id: UserId,
pub is_host: bool,
+ pub committer_name: Option<String>,
+ pub committer_email: Option<String>,
}
impl PartialOrd for User {
@@ -881,7 +882,6 @@ impl User {
github_login: message.github_login,
avatar_uri: message.avatar_url.into(),
name: message.name,
- email: message.email,
})
}
}
@@ -912,6 +912,8 @@ impl Collaborator {
replica_id: message.replica_id as ReplicaId,
user_id: message.user_id as UserId,
is_host: message.is_host,
+ committer_name: message.committer_name,
+ committer_email: message.committer_email,
})
}
}
@@ -185,7 +185,9 @@ CREATE TABLE "project_collaborators" (
"connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE,
"user_id" INTEGER NOT NULL,
"replica_id" INTEGER NOT NULL,
- "is_host" BOOLEAN NOT NULL
+ "is_host" BOOLEAN NOT NULL,
+ "committer_name" VARCHAR,
+ "committer_email" VARCHAR
);
CREATE INDEX "index_project_collaborators_on_project_id" ON "project_collaborators" ("project_id");
@@ -0,0 +1,4 @@
+alter table project_collaborators
+ add column committer_name varchar;
+alter table project_collaborators
+ add column committer_email varchar;
@@ -751,6 +751,8 @@ pub struct ProjectCollaborator {
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
+ pub committer_name: Option<String>,
+ pub committer_email: Option<String>,
}
impl ProjectCollaborator {
@@ -760,6 +762,8 @@ impl ProjectCollaborator {
replica_id: self.replica_id.0 as u32,
user_id: self.user_id.to_proto(),
is_host: self.is_host,
+ committer_name: self.committer_name.clone(),
+ committer_email: self.committer_email.clone(),
}
}
}
@@ -118,6 +118,8 @@ impl Database {
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
+ committer_name: None,
+ committer_email: None,
})
.collect(),
})
@@ -225,6 +227,8 @@ impl Database {
user_id: collaborator.user_id.to_proto(),
replica_id: collaborator.replica_id.0 as u32,
is_host: false,
+ committer_name: None,
+ committer_email: None,
})
.collect(),
},
@@ -261,6 +265,8 @@ impl Database {
replica_id: db_collaborator.replica_id.0 as u32,
user_id: db_collaborator.user_id.to_proto(),
is_host: false,
+ committer_name: None,
+ committer_email: None,
})
} else {
collaborator_ids_to_remove.push(db_collaborator.id);
@@ -390,6 +396,8 @@ impl Database {
replica_id: row.replica_id.0 as u32,
user_id: row.user_id.to_proto(),
is_host: false,
+ committer_name: None,
+ committer_email: None,
});
}
@@ -739,7 +739,6 @@ impl Database {
),
github_login: user.github_login,
name: user.name,
- email: user.email_address,
})
}
proto::ChannelMember {
@@ -98,7 +98,9 @@ impl Database {
user_id: ActiveValue::set(participant.user_id),
replica_id: ActiveValue::set(ReplicaId(replica_id)),
is_host: ActiveValue::set(true),
- ..Default::default()
+ id: ActiveValue::NotSet,
+ committer_name: ActiveValue::Set(None),
+ committer_email: ActiveValue::Set(None),
}
.insert(&*tx)
.await?;
@@ -784,13 +786,27 @@ impl Database {
project_id: ProjectId,
connection: ConnectionId,
user_id: UserId,
+ committer_name: Option<String>,
+ committer_email: Option<String>,
) -> Result<TransactionGuard<(Project, ReplicaId)>> {
- self.project_transaction(project_id, |tx| async move {
- let (project, role) = self
- .access_project(project_id, connection, Capability::ReadOnly, &tx)
- .await?;
- self.join_project_internal(project, user_id, connection, role, &tx)
+ self.project_transaction(project_id, move |tx| {
+ let committer_name = committer_name.clone();
+ let committer_email = committer_email.clone();
+ async move {
+ let (project, role) = self
+ .access_project(project_id, connection, Capability::ReadOnly, &tx)
+ .await?;
+ self.join_project_internal(
+ project,
+ user_id,
+ committer_name,
+ committer_email,
+ connection,
+ role,
+ &tx,
+ )
.await
+ }
})
.await
}
@@ -799,6 +815,8 @@ impl Database {
&self,
project: project::Model,
user_id: UserId,
+ committer_name: Option<String>,
+ committer_email: Option<String>,
connection: ConnectionId,
role: ChannelRole,
tx: &DatabaseTransaction,
@@ -822,7 +840,9 @@ impl Database {
user_id: ActiveValue::set(user_id),
replica_id: ActiveValue::set(replica_id),
is_host: ActiveValue::set(false),
- ..Default::default()
+ id: ActiveValue::NotSet,
+ committer_name: ActiveValue::set(committer_name),
+ committer_email: ActiveValue::set(committer_email),
}
.insert(tx)
.await?;
@@ -1026,6 +1046,8 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
+ committer_name: collaborator.committer_name,
+ committer_email: collaborator.committer_email,
})
.collect(),
worktrees,
@@ -553,6 +553,8 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
+ committer_name: collaborator.committer_name.clone(),
+ committer_email: collaborator.committer_email.clone(),
})
.collect(),
worktrees: reshared_project.worktrees.clone(),
@@ -857,6 +859,8 @@ impl Database {
user_id: collaborator.user_id,
replica_id: collaborator.replica_id,
is_host: collaborator.is_host,
+ committer_name: collaborator.committer_name,
+ committer_email: collaborator.committer_email,
})
.collect::<Vec<_>>();
@@ -13,6 +13,8 @@ pub struct Model {
pub user_id: UserId,
pub replica_id: ReplicaId,
pub is_host: bool,
+ pub committer_name: Option<String>,
+ pub committer_email: Option<String>,
}
impl Model {
@@ -126,12 +126,16 @@ async fn test_channel_buffers(db: &Arc<Database>) {
peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }),
replica_id: 0,
is_host: false,
+ committer_name: None,
+ committer_email: None,
},
rpc::proto::Collaborator {
user_id: b_id.to_proto(),
peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }),
replica_id: 1,
is_host: false,
+ committer_name: None,
+ committer_email: None,
}
]
);
@@ -14,7 +14,7 @@ use crate::{
db::{
self, BufferId, Capability, Channel, ChannelId, ChannelRole, ChannelsForUser,
CreatedChannelMessage, Database, InviteMemberResult, MembershipUpdated, MessageId,
- NotificationId, Project, ProjectId, RejoinedProject, RemoveChannelMemberResult, ReplicaId,
+ NotificationId, ProjectId, RejoinedProject, RemoveChannelMemberResult,
RespondToChannelInvite, RoomId, ServerId, UpdatedChannelMessage, User, UserId,
},
executor::Executor,
@@ -1890,28 +1890,16 @@ async fn join_project(
let db = session.db().await;
let (project, replica_id) = &mut *db
- .join_project(project_id, session.connection_id, session.user_id())
+ .join_project(
+ project_id,
+ session.connection_id,
+ session.user_id(),
+ request.committer_name.clone(),
+ request.committer_email.clone(),
+ )
.await?;
drop(db);
tracing::info!(%project_id, "join remote project");
- join_project_internal(response, session, project, replica_id)
-}
-
-trait JoinProjectInternalResponse {
- fn send(self, result: proto::JoinProjectResponse) -> Result<()>;
-}
-impl JoinProjectInternalResponse for Response<proto::JoinProject> {
- fn send(self, result: proto::JoinProjectResponse) -> Result<()> {
- Response::<proto::JoinProject>::send(self, result)
- }
-}
-
-fn join_project_internal(
- response: impl JoinProjectInternalResponse,
- session: Session,
- project: &mut Project,
- replica_id: &ReplicaId,
-) -> Result<()> {
let collaborators = project
.collaborators
.iter()
@@ -1939,6 +1927,8 @@ fn join_project_internal(
replica_id: replica_id.0 as u32,
user_id: guest_user_id.to_proto(),
is_host: false,
+ committer_name: request.committer_name.clone(),
+ committer_email: request.committer_email.clone(),
}),
};
@@ -2567,7 +2557,6 @@ async fn get_users(
id: user.id.to_proto(),
avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
github_login: user.github_login,
- email: user.email_address,
name: user.name,
})
.collect();
@@ -2601,7 +2590,6 @@ async fn fuzzy_search_users(
avatar_url: format!("https://github.com/{}.png?size=128", user.github_login),
github_login: user.github_login,
name: user.name,
- email: user.email_address,
})
.collect();
response.send(proto::UsersResponse { users })?;
@@ -1610,6 +1610,8 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut
.root(cx_a)
.unwrap();
+ executor.run_until_parked();
+
workspace_a_project_b.update(cx_a2, |workspace, cx| {
assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
assert!(workspace.is_being_followed(client_b.peer_id().unwrap()));
@@ -1876,7 +1876,6 @@ async fn test_active_call_events(
github_login: "user_a".to_string(),
avatar_uri: "avatar_a".into(),
name: None,
- email: None,
}),
project_id: project_a_id,
worktree_root_names: vec!["a".to_string()],
@@ -1896,7 +1895,6 @@ async fn test_active_call_events(
github_login: "user_b".to_string(),
avatar_uri: "avatar_b".into(),
name: None,
- email: None,
}),
project_id: project_b_id,
worktree_root_names: vec!["b".to_string()]
@@ -1218,7 +1218,6 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
- email: None,
}),
nonce: 5,
mentions: vec![(ranges[0].clone(), 101), (ranges[1].clone(), 102)],
@@ -1274,7 +1273,6 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
- email: None,
}),
nonce: 5,
mentions: Vec::new(),
@@ -1323,7 +1321,6 @@ mod tests {
avatar_uri: "avatar_fgh".into(),
id: 103,
name: None,
- email: None,
}),
nonce: 5,
mentions: Vec::new(),
@@ -26,8 +26,8 @@ use std::{
};
use sum_tree::MapSeekTarget;
use thiserror::Error;
-use util::ResultExt;
use util::command::{new_smol_command, new_std_command};
+use util::{ResultExt, paths};
use uuid::Uuid;
pub use askpass::{AskPassDelegate, AskPassResult, AskPassSession};
@@ -508,6 +508,50 @@ pub struct GitRepositoryCheckpoint {
pub commit_sha: Oid,
}
+#[derive(Debug)]
+pub struct GitCommitter {
+ pub name: Option<String>,
+ pub email: Option<String>,
+}
+
+pub async fn get_git_committer(cx: &AsyncApp) -> GitCommitter {
+ if cfg!(any(feature = "test-support", test)) {
+ return GitCommitter {
+ name: None,
+ email: None,
+ };
+ }
+
+ let git_binary_path =
+ if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") {
+ cx.update(|cx| {
+ cx.path_for_auxiliary_executable("git")
+ .context("could not find git binary path")
+ .log_err()
+ })
+ .ok()
+ .flatten()
+ } else {
+ None
+ };
+
+ let git = GitBinary::new(
+ git_binary_path.unwrap_or(PathBuf::from("git")),
+ paths::home_dir().clone(),
+ cx.background_executor().clone(),
+ );
+
+ cx.background_spawn(async move {
+ let name = git.run(["config", "--global", "user.name"]).await.log_err();
+ let email = git
+ .run(["config", "--global", "user.email"])
+ .await
+ .log_err();
+ GitCommitter { name, email }
+ })
+ .await
+}
+
impl GitRepository for RealGitRepository {
fn reload_index(&self) {
if let Ok(mut index) = self.repository.lock().index() {
@@ -21,6 +21,7 @@ agent_settings.workspace = true
anyhow.workspace = true
askpass.workspace = true
buffer_diff.workspace = true
+call.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -148,6 +148,7 @@ impl CommitModal {
}
}
git_panel.set_modal_open(true, cx);
+ git_panel.load_local_committer(cx);
});
let dock = workspace.dock_at_position(git_panel.position(window, cx));
@@ -20,8 +20,9 @@ use editor::{
use futures::StreamExt as _;
use git::blame::ParsedCommitMessage;
use git::repository::{
- Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, PushOptions,
- Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking, UpstreamTrackingStatus,
+ Branch, CommitDetails, CommitOptions, CommitSummary, DiffType, FetchOptions, GitCommitter,
+ PushOptions, Remote, RemoteCommandOutput, ResetMode, Upstream, UpstreamTracking,
+ UpstreamTrackingStatus, get_git_committer,
};
use git::status::StageStatus;
use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus};
@@ -358,6 +359,8 @@ pub struct GitPanel {
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
modal_open: bool,
show_placeholders: bool,
+ local_committer: Option<GitCommitter>,
+ local_committer_task: Option<Task<()>>,
_settings_subscription: Subscription,
}
@@ -520,6 +523,8 @@ impl GitPanel {
update_visible_entries_task: Task::ready(()),
width: None,
show_placeholders: false,
+ local_committer: None,
+ local_committer_task: None,
context_menu: None,
workspace: workspace.weak_handle(),
modal_open: false,
@@ -2250,6 +2255,19 @@ impl GitPanel {
}
}
+ pub fn load_local_committer(&mut self, cx: &Context<Self>) {
+ if self.local_committer_task.is_none() {
+ self.local_committer_task = Some(cx.spawn(async move |this, cx| {
+ let committer = get_git_committer(cx).await;
+ this.update(cx, |this, cx| {
+ this.local_committer = Some(committer);
+ cx.notify()
+ })
+ .ok();
+ }));
+ }
+ }
+
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
let mut new_co_authors = Vec::new();
let project = self.project.read(cx);
@@ -2272,34 +2290,38 @@ impl GitPanel {
let Some(participant) = room.remote_participant_for_peer_id(*peer_id) else {
continue;
};
- if participant.can_write() && participant.user.email.is_some() {
- let email = participant.user.email.clone().unwrap();
-
- new_co_authors.push((
- participant
- .user
- .name
- .clone()
- .unwrap_or_else(|| participant.user.github_login.clone()),
- email,
- ))
+ if !participant.can_write() {
+ continue;
+ }
+ if let Some(email) = &collaborator.committer_email {
+ let name = collaborator
+ .committer_name
+ .clone()
+ .or_else(|| participant.user.name.clone())
+ .unwrap_or_else(|| participant.user.github_login.clone());
+ new_co_authors.push((name.clone(), email.clone()))
}
}
if !project.is_local() && !project.is_read_only(cx) {
- if let Some(user) = room.local_participant_user(cx) {
- if let Some(email) = user.email.clone() {
- new_co_authors.push((
- user.name
- .clone()
- .unwrap_or_else(|| user.github_login.clone()),
- email.clone(),
- ))
- }
+ if let Some(local_committer) = self.local_committer(room, cx) {
+ new_co_authors.push(local_committer);
}
}
new_co_authors
}
+ fn local_committer(&self, room: &call::Room, cx: &App) -> Option<(String, String)> {
+ let user = room.local_participant_user(cx)?;
+ let committer = self.local_committer.as_ref()?;
+ let email = committer.email.clone()?;
+ let name = committer
+ .name
+ .clone()
+ .or_else(|| user.name.clone())
+ .unwrap_or_else(|| user.github_login.clone());
+ Some((name, email))
+ }
+
fn toggle_fill_co_authors(
&mut self,
_: &ToggleFillCoAuthors,
@@ -4244,8 +4266,9 @@ impl Render for GitPanel {
let has_write_access = self.has_write_access(cx);
let has_co_authors = room.map_or(false, |room| {
- room.read(cx)
- .remote_participants()
+ self.load_local_committer(cx);
+ let room = room.read(cx);
+ room.remote_participants()
.values()
.any(|remote_participant| remote_participant.can_write())
});
@@ -26,6 +26,7 @@ mod environment;
use buffer_diff::BufferDiff;
use context_server_store::ContextServerStore;
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
+use git::repository::get_git_committer;
use git_store::{Repository, RepositoryId};
pub mod search_history;
mod yarn;
@@ -1323,9 +1324,12 @@ impl Project {
),
EntitySubscription::DapStore(client.subscribe_to_entity::<DapStore>(remote_id)?),
];
+ let committer = get_git_committer(&cx).await;
let response = client
.request_envelope(proto::JoinProject {
project_id: remote_id,
+ committer_email: committer.email,
+ committer_name: committer.name,
})
.await?;
Self::from_join_project_response(
@@ -189,6 +189,8 @@ message UpdateProject {
message JoinProject {
uint64 project_id = 1;
+ optional string committer_email = 2;
+ optional string committer_name = 3;
}
message JoinProjectResponse {
@@ -7,10 +7,10 @@ message PeerId {
}
message User {
+ reserved 4;
uint64 id = 1;
string github_login = 2;
string avatar_url = 3;
- optional string email = 4;
optional string name = 5;
}
@@ -24,4 +24,6 @@ message Collaborator {
uint32 replica_id = 2;
uint64 user_id = 3;
bool is_host = 4;
+ optional string committer_name = 5;
+ optional string committer_email = 6;
}