Compare commits

...

4 Commits

Author SHA1 Message Date
Mikayla Maki
0d705bdab5 Notes.... 2025-02-23 18:20:35 -08:00
Mikayla Maki
a94b13ddf4 WIP: Implement push 2025-02-23 18:20:32 -08:00
Mikayla Maki
a3b08aa8da Make uncommit prompt if replacing the commit message
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-02-20 09:54:53 -08:00
Mikayla Maki
0c4224a3e0 Add the push button to the UI
Co-authored-by: Michael Sloan <mgsloan@gmail.com>
2025-02-20 09:44:17 -08:00
9 changed files with 529 additions and 137 deletions

View File

@@ -392,6 +392,8 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::OpenContext>)
.add_request_handler(forward_mutating_project_request::<proto::CreateContext>)
.add_request_handler(forward_mutating_project_request::<proto::SynchronizeContexts>)
.add_request_handler(forward_mutating_project_request::<proto::Push>)
.add_request_handler(forward_mutating_project_request::<proto::ForcePush>)
.add_request_handler(forward_mutating_project_request::<proto::Stage>)
.add_request_handler(forward_mutating_project_request::<proto::Unstage>)
.add_request_handler(forward_mutating_project_request::<proto::Commit>)

View File

@@ -43,6 +43,7 @@ actions!(
RestoreTrackedFiles,
TrashUntrackedFiles,
Uncommit,
Push,
Commit,
]
);

View File

@@ -20,6 +20,131 @@ use sum_tree::MapSeekTarget;
use util::command::new_std_command;
use util::ResultExt;
// GIT MERGE-BASE:
// git merge-base main HEAD -> Most recent commit from main
// git merge-base @{u} HEAD -> Most recent commit from upstream
//
// We can detect what branch is 'main' via `git symbolic-ref refs/remotes/origin/HEAD` (producing: refs/remotes/origin/main)
// -> This then requires finding which branch has this set as the upstream.
// -> If all that works, we can reliably determine a branch for checking uncommit.
// If the above process doesn't work, we can fall back on checking if there's a master or main OR init.defaultBranch at all.
// -> If there is one, we use that.
// If both of the above fail, we simply don't do that check.
//
// To determine what remote to use for 'main detection' above, we just check every configured remote
// and then when doing the "most recent commit from main" check, we use all of the branches that we found
// as 'mains', and show that in the prompt
//
// What if git-merge-base is slow?
// Let's build a git log debug view, so that we can collect timing information on all git commands
// This will probably be useful for debugging monorepos in various ways.
//
// Treat this like LSP commands, only shown and stored if you have a git log debug view open
//
// GIT FETCH
// Need a sync button for running git fetch. VSCode uses `git fetch --all`,,,, so it can help you manage all the git things
// (and a tree view)
// but we should probably just use `git fetch`, as we don't need a tree view
//
// GIT PUSH:
// To figure out which origin to set, just get a list of all origins and ask in a picker prompt, then use '--set-upstream'
// OR if there is just one origin, use that
// git push --set-upstream origin git-push
// (just like VSCode)
//
// GIT PULL:
// VSCode just uses:
// git pull --tags origin fix-pop-over-2
// ^ I wonder why they do this.... probably just so they can always have the latest tags
//
// This can cause a confusing scenario when you know a remote branch with the same name exists
// (and you can even see it in the history view in vscode), but somehow you haven't setup the upstream properly yet.
// Perhaps we enable git pull, but prompt you in this case that we're going to do this thing. IDK.
// ^ above is probably too niche to care about
//
// Merge / Rebase issue:
// I'd like there to be contextual buttons for this as well. Maybe "pull" if you're behind, and "rebase" if you're ahead and behind
// - Check a zed project setting for "default pull behavior" (default: rebase), then check for 'git config pull.rebase'
// If either of those set to merge, change the button to 'merge'
// ALTERNATIVELY: just always delegate to git pull, and if it fails prompt the user to select "merge or rebase"
// I kind of like making people think about always doing a rebase though. Feels nicely opinionated.
// I think telling the user what git is going to do is difficult, branches can be individually set to manage this at the git level,
// and git can be locally or globally configured on what to do in this scenario. Adding another layer of Zed querying or settings seems 😮‍💨
// I think the alternative plan is more straightforward.
// ALTERNATIVELY again: If get allows a general "what will pull do on this branch" query that resolves all these settings, maybe we show the merge / rebase buttons
// if configured then fallback to prompting
//
// GIT CONFLICTS:
// Create a simple 'git conflicts' tree sitter language that we add, that supports all files
// It adds a special 'git conflicts' query that we can use to run editor APIs off of
// This query is only accessible from native zed languages (e.g. extensions can't override this)
// This allows us to:
// - Highlight conflicts in the editor and provide UI for resolving them
// - Open conflicts in one big excerpt in the project diff
// TODO: Can the syntax map handle this situation? Or does it need a single root language? Or can we
// carve out a one-off for this situation?
// Why use tree-sitter at all? Fast, incremental parsing that we already know how to use efficiently.
//
// LOG VIEW:
// Basically, a way of looking at the output of 'git log'. It's what opens up if you click on the most recent commit.
// Should be pretty easy and nice. Model it after github desktop.
// - Each item should allow you to open a read-only multibuffer diff of the commit.
// - Let's not worry about large commits for now. Same issue as with our project search
// - Maybe a button "checkout this commit detached"
// If adding this button, make sure that creating a branch correctly handles the detached state.
// Git will probably do it for us. But you never know.
//
// STAGING VIEW:
// essentially the same as `git: Diff` except:
// 0. `git: Staging`
// 1. No integration with the existing git panel
// - MAYBE: You can have a setting which controls which of these diffs the panel tracks
// 2. Staging and unstaging are seperated one after the other
// 3. Items re-sort (basically a copy of the sublime merge experience)
//
// PANEL:
// - Sinced everything is very stable with these checkboxes, we can restore the tree view in the combined diff panel.
// - Maybe we have a switcher here for flat vs. tree view
// - This does not apply to the staging mode for this panel.
//
// KEYBINDINGS / PANEL UX:
// - Need to add more contextual keystrokes for all the various commands.
// - TODO: Figure out how to make it feel good to manuever the panel via the keyboard...
// - The diff view is pretty reliable for keyboard navigation. I like what conrad has built
// - TODO: Make sure everything he's been adding gets merged even if it's broken when doing this big upgrade I have planned.
// Issues:
// - Very little context as to what's been changed and how. If I'm looking at the panel I need those line numbers
// to contextualize where I want to double check
// - Git performance might be a blocker for line numbers, again let's add that debug view and see how it goes.
// - How to get down to the commit panel in a way that feels good?
// - How to show who the collaborators are automatically? - switch to avatars instead of this weird blue button
// - Once in the commit panel, I need a prompt to finish the commit,
// - and another to close this all out and get back to editing.
// - Essentially, there's a top to bottom flow implied by the panel, but no consistent keyboard driven way to follow (or escape) the flow.
//
// ^ I think if we can get all this, we'll have a nice and featureful feeling git experience in Zed. Right now it feels too narrow
// to me.
//
// SPLIT VIEW:
// - Out of scope.
// HISTORY TREE VIEW:
// - Out of scope
// STASH
// - Out of scope
//
// Goals for this weekend:
// - Fetch (remote syncing)
// - Push
// - Pull (settings integration?)
// - keybindings
// Stretch goals:
// - git conflicts
// - cli log debug view
// - line numbers
// - merge-base
// - git log view
// - Staging diff and staging panel
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Branch {
pub is_head: bool,
@@ -28,7 +153,39 @@ pub struct Branch {
pub most_recent_commit: Option<CommitSummary>,
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum PushAction {
Republish,
Publish,
ForcePush { ahead: u32, behind: u32 },
Push { ahead: u32 },
}
impl Branch {
pub fn push_action(&self) -> Option<PushAction> {
if let Some(upstream) = &self.upstream {
match upstream.tracking {
UpstreamTracking::Gone => Some(PushAction::Republish),
UpstreamTracking::Tracked(tracking) => {
if tracking.behind > 0 {
Some(PushAction::ForcePush {
ahead: tracking.ahead,
behind: tracking.behind,
})
} else if tracking.ahead > 0 {
Some(PushAction::Push {
ahead: tracking.ahead,
})
} else {
None
}
}
}
} else {
Some(PushAction::Publish)
}
}
pub fn priority_key(&self) -> (bool, Option<i64>) {
(
self.is_head,
@@ -42,11 +199,28 @@ impl Branch {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Upstream {
pub ref_name: SharedString,
pub tracking: Option<UpstreamTracking>,
pub tracking: UpstreamTracking,
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct UpstreamTracking {
pub enum UpstreamTracking {
/// Remote ref not present in local repository.
Gone,
/// Remote ref present in local repository (fetched from remote).
Tracked(UpstreamTrackingStatus),
}
impl UpstreamTracking {
pub fn status(&self) -> Option<UpstreamTrackingStatus> {
match self {
UpstreamTracking::Gone => None,
UpstreamTracking::Tracked(status) => Some(*status),
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub struct UpstreamTrackingStatus {
pub ahead: u32,
pub behind: u32,
}
@@ -68,6 +242,10 @@ pub struct CommitDetails {
pub committer_name: SharedString,
}
pub struct Remote {
pub name: SharedString,
}
pub enum ResetMode {
// reset the branch pointer, leave index and worktree unchanged
// (this will make it look like things that were committed are now
@@ -139,6 +317,16 @@ pub trait GitRepository: Send + Sync {
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
// With set_upstream, we pass this configured remote to push
// Otherwise we just use `git push origin branch`
fn push(&self, upstream: Remote, options: Option<PushOptions>) -> Result<()>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PushOptions {
SetUpstream,
Force,
}
impl std::fmt::Debug for dyn GitRepository {
@@ -165,6 +353,14 @@ impl RealGitRepository {
hosting_provider_registry,
}
}
fn working_directory(&self) -> Result<PathBuf> {
self.repository
.lock()
.workdir()
.context("failed to read git work directory")
.map(Path::to_path_buf)
}
}
// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
@@ -209,12 +405,7 @@ impl GitRepository for RealGitRepository {
}
fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
let mode_flag = match mode {
ResetMode::Mixed => "--mixed",
@@ -238,12 +429,7 @@ impl GitRepository for RealGitRepository {
if paths.is_empty() {
return Ok(());
}
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -296,12 +482,7 @@ impl GitRepository for RealGitRepository {
}
fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if let Some(content) = content {
let mut child = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
@@ -485,12 +666,7 @@ impl GitRepository for RealGitRepository {
}
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -509,12 +685,7 @@ impl GitRepository for RealGitRepository {
}
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
if !paths.is_empty() {
let output = new_std_command(&self.git_binary_path)
@@ -533,12 +704,7 @@ impl GitRepository for RealGitRepository {
}
fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
let working_directory = self
.repository
.lock()
.workdir()
.context("failed to read git work directory")?
.to_path_buf();
let working_directory = self.working_directory()?;
let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
if let Some(author) = author.as_deref() {
@@ -559,6 +725,30 @@ impl GitRepository for RealGitRepository {
}
Ok(())
}
fn push(&self, remote: Remote, options: Option<PushOptions>) -> Result<()> {
let working_directory = self.working_directory()?;
let output = new_std_command(&self.git_binary_path)
.current_dir(&working_directory)
.args(["push", "--quiet"])
.args(options.map(|option| match option {
PushOptions::SetUpstream => "--set-upstream",
PushOptions::Force => "--force-with-lease",
}))
.arg(remote.name.as_ref())
.output()?;
if !output.status.success() {
return Err(anyhow!(
"Failed to push:\n{}",
String::from_utf8_lossy(&output.stderr)
));
}
// TODO: Get remote response out of this and show it to the user
Ok(())
}
}
#[derive(Debug, Clone)]
@@ -743,6 +933,10 @@ impl GitRepository for FakeGitRepository {
fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
unimplemented!()
}
fn push(&self, _remote: Remote, _options: Option<PushOptions>) -> Result<()> {
unimplemented!()
}
}
fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
@@ -911,9 +1105,9 @@ fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
Ok(branches)
}
fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>> {
fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
if upstream_track == "" {
return Ok(Some(UpstreamTracking {
return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0,
}));
@@ -929,7 +1123,7 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
let mut behind: u32 = 0;
for component in upstream_track.split(", ") {
if component == "gone" {
return Ok(None);
return Ok(UpstreamTracking::Gone);
}
if let Some(ahead_num) = component.strip_prefix("ahead ") {
ahead = ahead_num.parse::<u32>()?;
@@ -938,7 +1132,10 @@ fn parse_upstream_track(upstream_track: &str) -> Result<Option<UpstreamTracking>
behind = behind_num.parse::<u32>()?;
}
}
Ok(Some(UpstreamTracking { ahead, behind }))
Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead,
behind,
}))
}
#[test]
@@ -953,7 +1150,7 @@ fn test_branches_parsing() {
name: "zed-patches".into(),
upstream: Some(Upstream {
ref_name: "refs/remotes/origin/zed-patches".into(),
tracking: Some(UpstreamTracking {
tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: 0,
behind: 0
})

View File

@@ -11,7 +11,7 @@ use editor::{
scroll::ScrollbarAutoHide, Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer,
ShowScrollbar,
};
use git::repository::{CommitDetails, ResetMode};
use git::repository::{Branch, CommitDetails, PushAction, ResetMode};
use git::{repository::RepoPath, status::FileStatus, Commit, ToggleStaged};
use git::{RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll};
use gpui::*;
@@ -1101,23 +1101,44 @@ impl GitPanel {
let Some(repo) = self.active_repository.clone() else {
return;
};
// TODO: Use git merge-base to find the upstream and main branch split
let confirmation = Task::ready(true);
// let confirmation = if self.commit_editor.read(cx).is_empty(cx) {
// Task::ready(true)
// } else {
// let prompt = window.prompt(
// PromptLevel::Warning,
// "Uncomitting will replace the current commit message with the previous commit's message",
// None,
// &["Ok", "Cancel"],
// cx,
// );
// cx.spawn(|_, _| async move { prompt.await.is_ok_and(|i| i == 0) })
// };
let prior_head = self.load_commit_details("HEAD", cx);
let task = cx.spawn(|_, mut cx| async move {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
.await??;
Ok(prior_head)
});
let task = cx.spawn_in(window, |this, mut cx| async move {
let result = task.await;
let result = maybe!(async {
if !confirmation.await {
Ok(None)
} else {
let prior_head = prior_head.await?;
repo.update(&mut cx, |repo, _| repo.reset("HEAD^", ResetMode::Soft))?
.await??;
Ok(Some(prior_head))
}
})
.await;
this.update_in(&mut cx, |this, window, cx| {
this.pending_commit.take();
match result {
Ok(prior_commit) => {
Ok(None) => {}
Ok(Some(prior_commit)) => {
this.commit_editor.update(cx, |editor, cx| {
editor.set_text(prior_commit.message, window, cx)
});
@@ -1131,6 +1152,31 @@ impl GitPanel {
self.pending_commit = Some(task);
}
// TODO: support more remotes other than `origin`
fn push(
&mut self,
push_action: Option<PushAction>,
_window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(repo) = self.active_repository.clone() else {
return;
};
let Some(push_action) = push_action else {
return;
};
let repo = repo.read(cx);
let push = match push_action {
PushAction::Republish => repo.push_upstream(),
PushAction::Publish => repo.push_upstream(),
PushAction::ForcePush { .. } => repo.force_push(),
PushAction::Push { .. } => repo.push(),
};
cx.spawn(move |_, _| push).detach();
}
fn potential_co_authors(&self, cx: &App) -> Vec<(String, String)> {
let mut new_co_authors = Vec::new();
let project = self.project.read(cx);
@@ -1599,22 +1645,20 @@ impl GitPanel {
&& !self.has_unstaged_conflicts()
&& self.has_write_access(cx);
// let can_commit_all =
// !self.commit_pending && self.can_commit_all && !editor.read(cx).is_empty(cx);
let panel_editor_style = panel_editor_style(true, window, cx);
let editor_focus_handle = editor.read(cx).focus_handle(cx).clone();
let focus_handle_1 = self.focus_handle(cx).clone();
let tooltip = if self.has_staged_changes() {
"Commit staged changes"
"git commit"
} else {
"Commit changes to tracked files"
"git commit --all"
};
let title = if self.has_staged_changes() {
"Commit"
} else {
"Commit All"
"Commit Tracked"
};
let commit_button = panel_filled_button(title)
@@ -1725,20 +1769,10 @@ impl GitPanel {
let branch = active_repository.read(cx).branch()?;
let commit = branch.most_recent_commit.as_ref()?.clone();
if branch.upstream.as_ref().is_some_and(|upstream| {
if let Some(tracking) = &upstream.tracking {
tracking.ahead == 0
} else {
true
}
}) {
return None;
}
let tooltip = if self.has_staged_changes() {
"git reset HEAD^ --soft"
} else {
"git reset HEAD^"
};
// Previous commit -> Always show this if present
// Uncommit -> Puts the last commit message back in the box, prompt for confirmation if text in box,
// always shown
// Push -> Always show, but disabled if identical
let this = cx.entity();
Some(
@@ -1779,9 +1813,17 @@ impl GitPanel {
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.tooltip(Tooltip::for_action_title(tooltip, &git::Uncommit))
.tooltip(Tooltip::for_action_title(
if self.has_staged_changes() {
"git reset HEAD^ --soft"
} else {
"git reset HEAD^"
},
&git::Uncommit,
))
.on_click(cx.listener(|this, _, window, cx| this.uncommit(window, cx))),
),
)
.child(self.render_push_button(branch, cx)),
)
}
@@ -2197,6 +2239,45 @@ impl GitPanel {
.into_any_element()
}
fn render_push_button(&self, branch: &Branch, cx: &Context<Self>) -> AnyElement {
// TODO: think about the GitAction based interface
// so that we can enforce a near 1:1 mapping between UI buttons and CLI commands
let action = branch.push_action();
let mut disabled = false;
let button: SharedString = match action {
Some(PushAction::Republish) => "Republish".into(),
Some(PushAction::Publish) => "Publish".into(),
Some(PushAction::ForcePush { ahead, behind }) => {
format!("Force Push (-{} -> +{})", ahead, behind).into()
}
Some(PushAction::Push { ahead }) => format!("Push (+{})", ahead).into(),
None => {
disabled = true;
"Push".into()
}
};
let tooltip: SharedString = match action {
Some(PushAction::Republish) => "git push --set-upstream".into(),
Some(PushAction::Publish) => "git push --set-upstream".into(),
Some(PushAction::ForcePush { .. }) => "git push --force-with-lease".into(),
Some(PushAction::Push { .. }) => "git push".into(),
None => "Upstream matches local branch".into(),
};
panel_filled_button(button)
.icon(IconName::ArrowUp)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.disabled(disabled)
.tooltip(Tooltip::for_action_title(tooltip, &git::Push))
.on_click(cx.listener(move |this, _, window, cx| this.push(action, window, cx)))
.into_any_element()
}
fn has_write_access(&self, cx: &App) -> bool {
!self.project.read(cx).is_read_only(cx)
}

View File

@@ -74,6 +74,8 @@ pub enum Message {
Stage(GitRepo, Vec<RepoPath>),
Unstage(GitRepo, Vec<RepoPath>),
SetIndexText(GitRepo, RepoPath, Option<String>),
Push(GitRepo, bool),
ForcePush(GitRepo),
}
pub enum GitEvent {
@@ -107,6 +109,8 @@ impl GitStore {
}
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_push);
client.add_entity_request_handler(Self::handle_force_push);
client.add_entity_request_handler(Self::handle_stage);
client.add_entity_request_handler(Self::handle_unstage);
client.add_entity_request_handler(Self::handle_commit);
@@ -242,8 +246,10 @@ impl GitStore {
mpsc::unbounded::<(Message, oneshot::Sender<Result<()>>)>();
cx.spawn(|_, cx| async move {
while let Some((msg, respond)) = update_receiver.next().await {
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
respond.send(result).ok();
if !respond.is_canceled() {
let result = cx.background_spawn(Self::process_git_msg(msg)).await;
respond.send(result).ok();
}
}
})
.detach();
@@ -252,6 +258,49 @@ impl GitStore {
async fn process_git_msg(msg: Message) -> Result<()> {
match msg {
Message::ForcePush(repo) => {
match repo {
GitRepo::Local(git_repository) => git_repository.force_push()?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::ForcePush {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
})
.await
.context("sending force push request")?;
}
}
Ok(())
}
Message::Push(repo, upstream) => {
match repo {
GitRepo::Local(git_repository) => git_repository.push(upstream)?,
GitRepo::Remote {
project_id,
client,
worktree_id,
work_directory_id,
} => {
client
.request(proto::Push {
project_id: project_id.0,
worktree_id: worktree_id.to_proto(),
work_directory_id: work_directory_id.to_proto(),
set_upstream: upstream,
})
.await
.context("sending push request")?;
}
}
Ok(())
}
Message::Stage(repo, paths) => {
match repo {
GitRepo::Local(repo) => repo.stage_paths(&paths)?,
@@ -413,6 +462,46 @@ impl GitStore {
}
}
async fn handle_force_push(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ForcePush>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
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)?;
repository_handle
.update(&mut cx, |repository_handle, _cx| {
repository_handle.force_push()
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_push(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Push>,
mut cx: AsyncApp,
) -> Result<proto::Ack> {
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)?;
repository_handle
.update(&mut cx, |repository_handle, _cx| {
if envelope.payload.set_upstream {
repository_handle.push_upstream()
} else {
repository_handle.push()
}
})?
.await??;
Ok(proto::Ack {})
}
async fn handle_stage(
this: Entity<Self>,
envelope: TypedEnvelope<proto::Stage>,
@@ -802,35 +891,19 @@ impl Repository {
commit: &str,
paths: Vec<RepoPath>,
) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
self.update_sender
.unbounded_send((
Message::CheckoutFiles {
repo: self.git_repo.clone(),
commit,
paths,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::CheckoutFiles {
repo: self.git_repo.clone(),
commit: commit.to_string().into(),
paths,
})
}
pub fn reset(&self, commit: &str, reset_mode: ResetMode) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
let commit = commit.to_string().into();
self.update_sender
.unbounded_send((
Message::Reset {
repo: self.git_repo.clone(),
commit,
reset_mode,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::Reset {
repo: self.git_repo.clone(),
commit: commit.to_string().into(),
reset_mode,
})
}
pub fn show(&self, commit: &str, cx: &Context<Self>) -> Task<Result<CommitDetails>> {
@@ -987,18 +1060,23 @@ impl Repository {
message: SharedString,
name_and_email: Option<(SharedString, SharedString)>,
) -> oneshot::Receiver<Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
Message::Commit {
git_repo: self.git_repo.clone(),
message,
name_and_email,
},
result_tx,
))
.ok();
result_rx
self.send_message(Message::Commit {
git_repo: self.git_repo.clone(),
message,
name_and_email,
})
}
pub fn push(&self) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::Push(self.git_repo.clone(), false))
}
pub fn push_upstream(&self) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::Push(self.git_repo.clone(), true))
}
pub fn force_push(&self) -> oneshot::Receiver<Result<()>> {
self.send_message(Message::ForcePush(self.git_repo.clone()))
}
pub fn set_index_text(
@@ -1006,13 +1084,16 @@ impl Repository {
path: &RepoPath,
content: Option<String>,
) -> oneshot::Receiver<anyhow::Result<()>> {
self.send_message(Message::SetIndexText(
self.git_repo.clone(),
path.clone(),
content,
))
}
fn send_message(&self, message: Message) -> oneshot::Receiver<anyhow::Result<()>> {
let (result_tx, result_rx) = futures::channel::oneshot::channel();
self.update_sender
.unbounded_send((
Message::SetIndexText(self.git_repo.clone(), path.clone(), content),
result_tx,
))
.ok();
self.update_sender.unbounded_send((message, result_tx)).ok();
result_rx
}
}

View File

@@ -946,12 +946,17 @@ impl WorktreeStore {
upstream: proto_branch.upstream.map(|upstream| {
git::repository::Upstream {
ref_name: upstream.ref_name.into(),
tracking: upstream.tracking.map(|tracking| {
git::repository::UpstreamTracking {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
}
}),
tracking: upstream
.tracking
.map(|tracking| {
git::repository::UpstreamTracking::Tracked(
git::repository::UpstreamTrackingStatus {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
},
)
})
.unwrap_or(git::repository::UpstreamTracking::Gone),
}
}),
most_recent_commit: proto_branch.most_recent_commit.map(|commit| {

View File

@@ -312,6 +312,8 @@ message Envelope {
Stage stage = 293;
Unstage unstage = 294;
Commit commit = 295;
Push push = 304;
ForcePush force_push = 305; // current max
OpenCommitMessageBuffer open_commit_message_buffer = 296;
OpenUncommittedDiff open_uncommitted_diff = 297;
@@ -321,7 +323,7 @@ message Envelope {
GitCommitDetails git_commit_details = 302;
SetIndexText set_index_text = 299;
GitCheckoutFiles git_checkout_files = 303; // current max
GitCheckoutFiles git_checkout_files = 303;
}
reserved 87 to 88;
@@ -2772,3 +2774,16 @@ message OpenCommitMessageBuffer {
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
}
message Push {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
bool set_upstream = 4;
}
message ForcePush {
uint64 project_id = 1;
uint64 worktree_id = 2;
uint64 work_directory_id = 3;
}

View File

@@ -445,6 +445,8 @@ messages!(
(GitShow, Background),
(GitCommitDetails, Background),
(SetIndexText, Background),
(Push, Background),
(ForcePush, Background),
);
request_messages!(
@@ -582,6 +584,8 @@ request_messages!(
(GitReset, Ack),
(GitCheckoutFiles, Ack),
(SetIndexText, Ack),
(Push, Ack),
(ForcePush, Ack),
);
entity_messages!(
@@ -678,6 +682,8 @@ entity_messages!(
GitReset,
GitCheckoutFiles,
SetIndexText,
Push,
ForcePush,
);
entity_messages!(

View File

@@ -20,7 +20,7 @@ use futures::{
};
use fuzzy::CharBag;
use git::{
repository::{Branch, GitRepository, RepoPath},
repository::{Branch, GitRepository, RepoPath, UpstreamTrackingStatus},
status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
},
@@ -329,7 +329,7 @@ pub fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch {
ref_name: upstream.ref_name.to_string(),
tracking: upstream
.tracking
.as_ref()
.status()
.map(|upstream| proto::UpstreamTracking {
ahead: upstream.ahead as u64,
behind: upstream.behind as u64,
@@ -355,12 +355,16 @@ pub fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch {
.as_ref()
.map(|upstream| git::repository::Upstream {
ref_name: upstream.ref_name.to_string().into(),
tracking: upstream.tracking.as_ref().map(|tracking| {
git::repository::UpstreamTracking {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
}
}),
tracking: upstream
.tracking
.as_ref()
.map(|tracking| {
git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus {
ahead: tracking.ahead as u32,
behind: tracking.behind as u32,
})
})
.unwrap_or(git::repository::UpstreamTracking::Gone),
}),
most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| {
git::repository::CommitSummary {