mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-25 10:51:26 +01:00
Git support (#143)
This commit is contained in:
90
src-tauri/yaak-git/src/branch.rs
Normal file
90
src-tauri/yaak-git/src/branch.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use crate::error::Error::GenericError;
|
||||
use crate::error::Result;
|
||||
use crate::merge::do_merge;
|
||||
use crate::repository::open_repo;
|
||||
use crate::util::{
|
||||
bytes_to_string, get_branch_by_name, get_current_branch, get_default_remote_for_push_in_repo,
|
||||
};
|
||||
use git2::build::CheckoutBuilder;
|
||||
use git2::{BranchType, Repository};
|
||||
use log::info;
|
||||
use std::path::Path;
|
||||
|
||||
pub(crate) fn branch_set_upstream_after_push(repo: &Repository, branch_name: &str) -> Result<()> {
|
||||
let mut branch = repo.find_branch(branch_name, BranchType::Local)?;
|
||||
|
||||
if branch.upstream().is_err() {
|
||||
let remote = get_default_remote_for_push_in_repo(repo)?;
|
||||
let upstream_name = format!("{remote}/{branch_name}");
|
||||
branch.set_upstream(Some(upstream_name.as_str()))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn git_checkout_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let branch = get_branch_by_name(&repo, branch)?;
|
||||
let branch_ref = branch.into_reference();
|
||||
let branch_tree = branch_ref.peel_to_tree()?;
|
||||
|
||||
let mut options = CheckoutBuilder::default();
|
||||
if force {
|
||||
options.force();
|
||||
}
|
||||
|
||||
repo.checkout_tree(branch_tree.as_object(), Some(&mut options))?;
|
||||
repo.set_head(branch_ref.name().unwrap())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn git_create_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let head = match repo.head() {
|
||||
Ok(h) => h,
|
||||
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
|
||||
let msg = "Cannot create branch when there are no commits";
|
||||
return Err(GenericError(msg.into()));
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let head = head.peel_to_commit()?;
|
||||
|
||||
repo.branch(name, &head, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn git_delete_branch(dir: &Path, name: &str) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let mut branch = get_branch_by_name(&repo, name)?;
|
||||
|
||||
if branch.is_head() {
|
||||
info!("Deleting head branch");
|
||||
let branches = repo.branches(Some(BranchType::Local))?;
|
||||
let other_branch = branches.into_iter().filter_map(|b| b.ok()).find(|b| !b.0.is_head());
|
||||
let other_branch = match other_branch {
|
||||
None => return Err(GenericError("Cannot delete only branch".into())),
|
||||
Some(b) => bytes_to_string(b.0.name_bytes()?)?,
|
||||
};
|
||||
|
||||
git_checkout_branch(dir, &other_branch, true)?;
|
||||
}
|
||||
|
||||
branch.delete()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn git_merge_branch(dir: &Path, name: &str, _force: bool) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let local_branch = get_current_branch(&repo)?.unwrap();
|
||||
|
||||
let commit_to_merge = get_branch_by_name(&repo, name)?.into_reference();
|
||||
let commit_to_merge = repo.reference_to_annotated_commit(&commit_to_merge)?;
|
||||
|
||||
do_merge(&repo, &local_branch, &commit_to_merge)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
76
src-tauri/yaak-git/src/callbacks.rs
Normal file
76
src-tauri/yaak-git/src/callbacks.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use git2::{Cred, RemoteCallbacks};
|
||||
use log::{debug, info};
|
||||
use crate::util::find_ssh_key;
|
||||
|
||||
pub(crate) fn default_callbacks<'s>() -> RemoteCallbacks<'s> {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
|
||||
let mut fail_next_call = false;
|
||||
let mut tried_agent = false;
|
||||
|
||||
callbacks.credentials(move |url, username_from_url, allowed_types| {
|
||||
if fail_next_call {
|
||||
info!("Failed to get credentials for push");
|
||||
return Err(git2::Error::from_str("Bad credentials."));
|
||||
}
|
||||
|
||||
debug!("getting credentials {url} {username_from_url:?} {allowed_types:?}");
|
||||
match (allowed_types.is_ssh_key(), username_from_url) {
|
||||
(true, Some(username)) => {
|
||||
if !tried_agent {
|
||||
tried_agent = true;
|
||||
return Cred::ssh_key_from_agent(username);
|
||||
}
|
||||
|
||||
fail_next_call = true; // This is our last try
|
||||
|
||||
// If the agent failed, try using the default SSH key
|
||||
if let Some(key) = find_ssh_key() {
|
||||
Cred::ssh_key(username, None, key.as_path(), None)
|
||||
} else {
|
||||
Err(git2::Error::from_str(
|
||||
"Bad credentials. Ensure your key was added using ssh-add",
|
||||
))
|
||||
}
|
||||
}
|
||||
(true, None) => Err(git2::Error::from_str("Couldn't get username from url")),
|
||||
_ => {
|
||||
todo!("Implement basic auth credential");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
callbacks.push_transfer_progress(|current, total, bytes| {
|
||||
debug!("progress: {}/{} ({} B)", current, total, bytes,);
|
||||
});
|
||||
|
||||
callbacks.transfer_progress(|p| {
|
||||
debug!("transfer: {}/{}", p.received_objects(), p.total_objects());
|
||||
true
|
||||
});
|
||||
|
||||
callbacks.pack_progress(|stage, current, total| {
|
||||
debug!("packing: {:?} - {}/{}", stage, current, total);
|
||||
});
|
||||
|
||||
callbacks.push_update_reference(|reference, msg| {
|
||||
debug!("push_update_reference: '{}' {:?}", reference, msg);
|
||||
Ok(())
|
||||
});
|
||||
|
||||
callbacks.update_tips(|name, a, b| {
|
||||
debug!("update tips: '{}' {} -> {}", name, a, b);
|
||||
if a != b {
|
||||
// let mut push_result = push_result.lock().unwrap();
|
||||
// *push_result = PushResult::Success
|
||||
}
|
||||
true
|
||||
});
|
||||
|
||||
callbacks.sideband_progress(|data| {
|
||||
debug!("sideband transfer: '{}'", String::from_utf8_lossy(data).trim());
|
||||
true
|
||||
});
|
||||
|
||||
callbacks
|
||||
}
|
||||
76
src-tauri/yaak-git/src/commands.rs
Normal file
76
src-tauri/yaak-git/src/commands.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use crate::branch::{git_checkout_branch, git_create_branch, git_delete_branch, git_merge_branch};
|
||||
use crate::error::Result;
|
||||
use crate::git::{
|
||||
git_add, git_commit, git_init, git_log, git_status, git_unstage, GitCommit, GitStatusSummary,
|
||||
};
|
||||
use crate::pull::{git_pull, PullResult};
|
||||
use crate::push::{git_push, PushResult};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::command;
|
||||
// NOTE: All of these commands are async to prevent blocking work from locking up the UI
|
||||
|
||||
#[command]
|
||||
pub async fn checkout(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
||||
git_checkout_branch(dir, branch, force)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
git_create_branch(dir, branch)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn delete_branch(dir: &Path, branch: &str) -> Result<()> {
|
||||
git_delete_branch(dir, branch)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn merge_branch(dir: &Path, branch: &str, force: bool) -> Result<()> {
|
||||
git_merge_branch(dir, branch, force)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn status(dir: &Path) -> Result<GitStatusSummary> {
|
||||
git_status(dir)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn log(dir: &Path) -> Result<Vec<GitCommit>> {
|
||||
git_log(dir)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn initialize(dir: &Path) -> Result<()> {
|
||||
git_init(dir)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn commit(dir: &Path, message: &str) -> Result<()> {
|
||||
git_commit(dir, message)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn push(dir: &Path) -> Result<PushResult> {
|
||||
git_push(dir)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn pull(dir: &Path) -> Result<PullResult> {
|
||||
git_pull(dir)
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn add(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
|
||||
for path in rela_paths {
|
||||
git_add(dir, &path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub async fn unstage(dir: &Path, rela_paths: Vec<PathBuf>) -> Result<()> {
|
||||
for path in rela_paths {
|
||||
git_unstage(dir, &path)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
55
src-tauri/yaak-git/src/error.rs
Normal file
55
src-tauri/yaak-git/src/error.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use serde::{Serialize, Serializer};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::string::FromUtf8Error;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Git repo not found {0}")]
|
||||
GitRepoNotFound(PathBuf),
|
||||
|
||||
#[error("Git error: {0}")]
|
||||
GitUnknown(#[from] git2::Error),
|
||||
|
||||
#[error("Yaml error: {0}")]
|
||||
YamlParseError(#[from] serde_yaml::Error),
|
||||
|
||||
#[error("Yaml error: {0}")]
|
||||
ModelError(#[from] yaak_models::error::Error),
|
||||
|
||||
#[error("Sync error: {0}")]
|
||||
SyncError(#[from] yaak_sync::error::Error),
|
||||
|
||||
#[error("I/o error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Yaml error: {0}")]
|
||||
JsonParseError(#[from] serde_json::Error),
|
||||
|
||||
#[error("Yaml error: {0}")]
|
||||
Utf8ConversionError(#[from] FromUtf8Error),
|
||||
|
||||
#[error("Git error: {0}")]
|
||||
GenericError(String),
|
||||
|
||||
#[error("No default remote found")]
|
||||
NoDefaultRemoteFound,
|
||||
|
||||
#[error("Merge failed due to conflicts")]
|
||||
MergeConflicts,
|
||||
|
||||
#[error("No active branch")]
|
||||
NoActiveBranch,
|
||||
}
|
||||
|
||||
impl Serialize for Error {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.to_string().as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
670
src-tauri/yaak-git/src/git.rs
Normal file
670
src-tauri/yaak-git/src/git.rs
Normal file
@@ -0,0 +1,670 @@
|
||||
use crate::error::Result;
|
||||
use crate::repository::open_repo;
|
||||
use crate::util::list_branch_names;
|
||||
use chrono::{DateTime, Utc};
|
||||
use git2::IndexAddOption;
|
||||
use log::{info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use ts_rs::TS;
|
||||
use yaak_sync::models::SyncModel;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub struct GitStatusSummary {
|
||||
pub path: String,
|
||||
pub head_ref: Option<String>,
|
||||
pub head_ref_shorthand: Option<String>,
|
||||
pub entries: Vec<GitStatusEntry>,
|
||||
pub origins: Vec<String>,
|
||||
pub branches: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub struct GitStatusEntry {
|
||||
pub rela_path: String,
|
||||
pub status: GitStatus,
|
||||
pub staged: bool,
|
||||
pub prev: Option<SyncModel>,
|
||||
pub next: Option<SyncModel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub enum GitStatus {
|
||||
Added,
|
||||
Conflict,
|
||||
Current,
|
||||
Modified,
|
||||
Removed,
|
||||
Renamed,
|
||||
TypeChange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub struct GitCommit {
|
||||
author: GitAuthor,
|
||||
when: DateTime<Utc>,
|
||||
message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub struct GitAuthor {
|
||||
name: Option<String>,
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
pub fn git_init(dir: &Path) -> Result<()> {
|
||||
git2::Repository::init(dir)?;
|
||||
let repo = open_repo(dir)?;
|
||||
// Default to main instead of master, to align with
|
||||
// the official Git and GitHub behavior
|
||||
repo.set_head("refs/heads/main")?;
|
||||
info!("Initialized {dir:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_add(dir: &Path, rela_path: &Path) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
let mut index = repo.index()?;
|
||||
|
||||
info!("Staging file {rela_path:?} to {dir:?}");
|
||||
index.add_all(&[rela_path], IndexAddOption::DEFAULT, None)?;
|
||||
index.write()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_unstage(dir: &Path, rela_path: &Path) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
|
||||
let head = match repo.head() {
|
||||
Ok(h) => h,
|
||||
Err(e) if e.code() == git2::ErrorCode::UnbornBranch => {
|
||||
info!("Unstaging file in empty branch {rela_path:?} to {dir:?}");
|
||||
// Repo has no commits, so "unstage" means remove from index
|
||||
let mut index = repo.index()?;
|
||||
index.remove_path(rela_path)?;
|
||||
index.write()?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
|
||||
// If repo has commits, update the index entry back to HEAD
|
||||
info!("Unstaging file {rela_path:?} to {dir:?}");
|
||||
let commit = head.peel_to_commit()?;
|
||||
repo.reset_default(Some(commit.as_object()), &[rela_path])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_commit(dir: &Path, message: &str) -> Result<()> {
|
||||
let repo = open_repo(dir)?;
|
||||
|
||||
// Clear the in-memory index, add the paths, and write the tree for committing
|
||||
let tree_oid = repo.index()?.write_tree()?;
|
||||
let tree = repo.find_tree(tree_oid)?;
|
||||
|
||||
// Make the signature
|
||||
let config = git2::Config::open_default()?.snapshot()?;
|
||||
let name = config.get_str("user.name").unwrap_or("Change Me");
|
||||
let email = config.get_str("user.email").unwrap_or("change_me@example.com");
|
||||
let sig = git2::Signature::now(name, email)?;
|
||||
|
||||
// Get the current HEAD commit (if it exists)
|
||||
let parent_commit = match repo.head() {
|
||||
Ok(head) => Some(head.peel_to_commit()?),
|
||||
Err(_) => None, // No parent if no HEAD exists (initial commit)
|
||||
};
|
||||
|
||||
let parents = parent_commit.as_ref().map(|p| vec![p]).unwrap_or_default();
|
||||
repo.commit(Some("HEAD"), &sig, &sig, message, &tree, parents.as_slice())?;
|
||||
|
||||
info!("Committed to {dir:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn git_log(dir: &Path) -> Result<Vec<GitCommit>> {
|
||||
let repo = open_repo(dir)?;
|
||||
|
||||
// Return empty if empty repo or no head (new repo)
|
||||
if repo.is_empty()? || repo.head().is_err() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push_head()?;
|
||||
revwalk.set_sorting(git2::Sort::TIME)?;
|
||||
|
||||
// Run git log
|
||||
macro_rules! filter_try {
|
||||
($e:expr) => {
|
||||
match $e {
|
||||
Ok(t) => t,
|
||||
Err(_) => return None,
|
||||
}
|
||||
};
|
||||
}
|
||||
let log: Vec<GitCommit> = revwalk
|
||||
.filter_map(|oid| {
|
||||
let oid = filter_try!(oid);
|
||||
let commit = filter_try!(repo.find_commit(oid));
|
||||
let author = commit.author();
|
||||
Some(GitCommit {
|
||||
author: GitAuthor {
|
||||
name: author.name().map(|s| s.to_string()),
|
||||
email: author.email().map(|s| s.to_string()),
|
||||
},
|
||||
when: convert_git_time_to_date(author.when()),
|
||||
message: commit.message().map(|m| m.to_string()),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(log)
|
||||
}
|
||||
|
||||
pub fn git_status(dir: &Path) -> Result<GitStatusSummary> {
|
||||
let repo = open_repo(dir)?;
|
||||
let (head_tree, head_ref, head_ref_shorthand) = match repo.head() {
|
||||
Ok(head) => {
|
||||
let tree = head.peel_to_tree().ok();
|
||||
let head_ref_shorthand = head.shorthand().map(|s| s.to_string());
|
||||
let head_ref = head.name().map(|s| s.to_string());
|
||||
|
||||
(tree, head_ref, head_ref_shorthand)
|
||||
}
|
||||
Err(_) => {
|
||||
// For "unborn" repos, reading from HEAD is the only way to get the branch name
|
||||
// See https://github.com/starship/starship/pull/1336
|
||||
let head_path = repo.path().join("HEAD");
|
||||
let head_ref = fs::read_to_string(&head_path)
|
||||
.ok()
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.next()
|
||||
.map(|s| s.trim_start_matches("ref:").trim().to_string());
|
||||
let head_ref_shorthand =
|
||||
head_ref.clone().map(|r| r.split('/').last().unwrap_or("unknown").to_string());
|
||||
(None, head_ref, head_ref_shorthand)
|
||||
}
|
||||
};
|
||||
|
||||
let mut opts = git2::StatusOptions::new();
|
||||
opts.include_ignored(false)
|
||||
.include_untracked(true) // Include untracked
|
||||
.recurse_untracked_dirs(true) // Show all untracked
|
||||
.include_unmodified(true); // Include unchanged
|
||||
|
||||
// TODO: Support renames
|
||||
|
||||
let mut entries: Vec<GitStatusEntry> = Vec::new();
|
||||
for entry in repo.statuses(Some(&mut opts))?.into_iter() {
|
||||
let rela_path = entry.path().unwrap().to_string();
|
||||
let status = entry.status();
|
||||
let index_status = match status {
|
||||
// Note: order matters here, since we're checking a bitmap!
|
||||
s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict,
|
||||
s if s.contains(git2::Status::INDEX_NEW) => GitStatus::Added,
|
||||
s if s.contains(git2::Status::INDEX_MODIFIED) => GitStatus::Modified,
|
||||
s if s.contains(git2::Status::INDEX_DELETED) => GitStatus::Removed,
|
||||
s if s.contains(git2::Status::INDEX_RENAMED) => GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::INDEX_TYPECHANGE) => GitStatus::TypeChange,
|
||||
s if s.contains(git2::Status::CURRENT) => GitStatus::Current,
|
||||
s => {
|
||||
warn!("Unknown index status {s:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let worktree_status = match status {
|
||||
// Note: order matters here, since we're checking a bitmap!
|
||||
s if s.contains(git2::Status::CONFLICTED) => GitStatus::Conflict,
|
||||
s if s.contains(git2::Status::WT_NEW) => GitStatus::Added,
|
||||
s if s.contains(git2::Status::WT_MODIFIED) => GitStatus::Modified,
|
||||
s if s.contains(git2::Status::WT_DELETED) => GitStatus::Removed,
|
||||
s if s.contains(git2::Status::WT_RENAMED) => GitStatus::Renamed,
|
||||
s if s.contains(git2::Status::WT_TYPECHANGE) => GitStatus::TypeChange,
|
||||
s if s.contains(git2::Status::CURRENT) => GitStatus::Current,
|
||||
s => {
|
||||
warn!("Unknown worktree status {s:?}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let status = if index_status == GitStatus::Current {
|
||||
worktree_status.clone()
|
||||
} else {
|
||||
index_status.clone()
|
||||
};
|
||||
|
||||
let staged = if index_status == GitStatus::Current && worktree_status == GitStatus::Current
|
||||
{
|
||||
// No change, so can't be added
|
||||
false
|
||||
} else if index_status != GitStatus::Current {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Get previous content from Git, if it's in there
|
||||
let prev = match head_tree.clone() {
|
||||
None => None,
|
||||
Some(t) => match t.get_path(&Path::new(&rela_path)) {
|
||||
Ok(entry) => {
|
||||
let obj = entry.to_object(&repo).unwrap();
|
||||
let content = obj.as_blob().unwrap().content();
|
||||
let name = Path::new(entry.name().unwrap_or_default());
|
||||
SyncModel::from_bytes(content.into(), name)?.map(|m| m.0)
|
||||
}
|
||||
Err(_) => None,
|
||||
},
|
||||
};
|
||||
|
||||
let next = {
|
||||
let full_path = repo.workdir().unwrap().join(rela_path.clone());
|
||||
SyncModel::from_file(full_path.as_path())?.map(|m| m.0)
|
||||
};
|
||||
|
||||
entries.push(GitStatusEntry {
|
||||
status,
|
||||
staged,
|
||||
rela_path,
|
||||
prev: prev.clone(),
|
||||
next: next.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
let origins = repo.remotes()?.into_iter().filter_map(|o| Some(o?.to_string())).collect();
|
||||
let branches = list_branch_names(&repo)?;
|
||||
|
||||
Ok(GitStatusSummary {
|
||||
entries,
|
||||
origins,
|
||||
path: dir.to_string_lossy().to_string(),
|
||||
head_ref,
|
||||
head_ref_shorthand,
|
||||
branches,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn convert_git_time_to_date(_git_time: git2::Time) -> DateTime<Utc> {
|
||||
DateTime::from_timestamp(0, 0).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
fn convert_git_time_to_date(git_time: git2::Time) -> DateTime<Utc> {
|
||||
let timestamp = git_time.seconds();
|
||||
DateTime::from_timestamp(timestamp, 0).unwrap()
|
||||
}
|
||||
|
||||
// // Write a test
|
||||
// #[cfg(test)]
|
||||
// mod test {
|
||||
// use crate::error::Error::GitRepoNotFound;
|
||||
// use crate::error::Result;
|
||||
// use crate::git::{
|
||||
// git_add, git_commit, git_init, git_log, git_status, git_unstage, open_repo, GitStatus,
|
||||
// GitStatusEntry,
|
||||
// };
|
||||
// use std::fs::{create_dir_all, remove_file, File};
|
||||
// use std::io::Write;
|
||||
// use std::path::{Path, PathBuf};
|
||||
// use tempdir::TempDir;
|
||||
//
|
||||
// fn new_dir() -> PathBuf {
|
||||
// let p = TempDir::new("yaak-git").unwrap().into_path();
|
||||
// p
|
||||
// }
|
||||
//
|
||||
// fn new_file(path: &Path, content: &str) {
|
||||
// let parent = path.parent().unwrap();
|
||||
// create_dir_all(parent).unwrap();
|
||||
// File::create(path).unwrap().write_all(content.as_bytes()).unwrap();
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// async fn test_status_no_repo() {
|
||||
// let dir = &new_dir();
|
||||
// let result = git_status(dir).await;
|
||||
// assert!(matches!(result, Err(GitRepoNotFound(_))));
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_open_repo() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
// open_repo(dir.as_path())?;
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_open_repo_from_subdir() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// let sub_dir = dir.join("a").join("b");
|
||||
// create_dir_all(sub_dir.as_path())?; // Create sub dir
|
||||
//
|
||||
// open_repo(sub_dir.as_path())?;
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// async fn test_status() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// assert_eq!(git_status(dir).await?.entries, Vec::new());
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo");
|
||||
// new_file(&dir.join("bar.txt"), "bar");
|
||||
// new_file(&dir.join("dir/baz.txt"), "baz");
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "dir/baz.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("baz".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("foo".to_string()),
|
||||
// },
|
||||
// ],
|
||||
// );
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// fn test_add() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo");
|
||||
// new_file(&dir.join("bar.txt"), "bar");
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
//
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: true,
|
||||
// prev: None,
|
||||
// next: Some("foo".to_string()),
|
||||
// },
|
||||
// ],
|
||||
// );
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo foo");
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: true,
|
||||
// prev: None,
|
||||
// next: Some("foo foo".to_string()),
|
||||
// },
|
||||
// ],
|
||||
// );
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// fn test_unstage() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo");
|
||||
// new_file(&dir.join("bar.txt"), "bar");
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: true,
|
||||
// prev: None,
|
||||
// next: Some("foo".to_string()),
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
//
|
||||
// git_unstage(dir, Path::new("foo.txt"))?;
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("foo".to_string()),
|
||||
// }
|
||||
// ]
|
||||
// );
|
||||
//
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// fn test_commit() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo");
|
||||
// new_file(&dir.join("bar.txt"), "bar");
|
||||
//
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("foo".to_string()),
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// git_commit(dir, "This is my message")?;
|
||||
//
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Current,
|
||||
// staged: false,
|
||||
// prev: Some("foo".to_string()),
|
||||
// next: Some("foo".to_string()),
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo foo");
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Modified,
|
||||
// staged: true,
|
||||
// prev: Some("foo".to_string()),
|
||||
// next: Some("foo foo".to_string()),
|
||||
// },
|
||||
// ]
|
||||
// );
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// async fn test_add_removed_file() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// let foo_path = &dir.join("foo.txt");
|
||||
// let bar_path = &dir.join("bar.txt");
|
||||
//
|
||||
// new_file(foo_path, "foo");
|
||||
// new_file(bar_path, "bar");
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// git_commit(dir, "Initial commit")?;
|
||||
//
|
||||
// remove_file(foo_path)?;
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Removed,
|
||||
// staged: false,
|
||||
// prev: Some("foo".to_string()),
|
||||
// next: None,
|
||||
// },
|
||||
// ],
|
||||
// );
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// assert_eq!(
|
||||
// git_status(dir).await?.entries,
|
||||
// vec![
|
||||
// GitStatusEntry {
|
||||
// rela_path: "bar.txt".to_string(),
|
||||
// status: GitStatus::Added,
|
||||
// staged: false,
|
||||
// prev: None,
|
||||
// next: Some("bar".to_string()),
|
||||
// },
|
||||
// GitStatusEntry {
|
||||
// rela_path: "foo.txt".to_string(),
|
||||
// status: GitStatus::Removed,
|
||||
// staged: true,
|
||||
// prev: Some("foo".to_string()),
|
||||
// next: None,
|
||||
// },
|
||||
// ],
|
||||
// );
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[tokio::test]
|
||||
// fn test_log_empty() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// let log = git_log(dir)?;
|
||||
// assert_eq!(log.len(), 0);
|
||||
// Ok(())
|
||||
// }
|
||||
//
|
||||
// #[test]
|
||||
// fn test_log() -> Result<()> {
|
||||
// let dir = &new_dir();
|
||||
// git_init(dir)?;
|
||||
//
|
||||
// new_file(&dir.join("foo.txt"), "foo");
|
||||
// new_file(&dir.join("bar.txt"), "bar");
|
||||
//
|
||||
// git_add(dir, Path::new("foo.txt"))?;
|
||||
// git_commit(dir, "This is my message")?;
|
||||
//
|
||||
// let log = git_log(dir)?;
|
||||
// assert_eq!(log.len(), 1);
|
||||
// assert_eq!(log.get(0).unwrap().message, Some("This is my message".to_string()));
|
||||
// Ok(())
|
||||
// }
|
||||
// }
|
||||
36
src-tauri/yaak-git/src/lib.rs
Normal file
36
src-tauri/yaak-git/src/lib.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use crate::commands::{add, branch, checkout, commit, delete_branch, initialize, log, merge_branch, pull, push, status, unstage};
|
||||
use tauri::{
|
||||
generate_handler,
|
||||
plugin::{Builder, TauriPlugin},
|
||||
Runtime,
|
||||
};
|
||||
|
||||
mod branch;
|
||||
mod callbacks;
|
||||
mod commands;
|
||||
mod error;
|
||||
mod git;
|
||||
mod merge;
|
||||
mod pull;
|
||||
mod push;
|
||||
mod repository;
|
||||
mod util;
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-git")
|
||||
.invoke_handler(generate_handler![
|
||||
add,
|
||||
branch,
|
||||
checkout,
|
||||
commit,
|
||||
delete_branch,
|
||||
initialize,
|
||||
log,
|
||||
merge_branch,
|
||||
pull,
|
||||
push,
|
||||
status,
|
||||
unstage
|
||||
])
|
||||
.build()
|
||||
}
|
||||
135
src-tauri/yaak-git/src/merge.rs
Normal file
135
src-tauri/yaak-git/src/merge.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use crate::error::Error::MergeConflicts;
|
||||
use crate::util::bytes_to_string;
|
||||
use git2::{AnnotatedCommit, Branch, IndexEntry, Reference, Repository};
|
||||
use log::{debug, info};
|
||||
|
||||
pub(crate) fn do_merge(
|
||||
repo: &Repository,
|
||||
local_branch: &Branch,
|
||||
commit_to_merge: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
debug!("Merging remote branches");
|
||||
let analysis = repo.merge_analysis(&[&commit_to_merge])?;
|
||||
|
||||
if analysis.0.is_fast_forward() {
|
||||
let refname = bytes_to_string(local_branch.get().name_bytes())?;
|
||||
match repo.find_reference(&refname) {
|
||||
Ok(mut r) => {
|
||||
merge_fast_forward(repo, &mut r, &commit_to_merge)?;
|
||||
}
|
||||
Err(_) => {
|
||||
// The branch doesn't exist, so set the reference to the commit directly. Usually
|
||||
// this is because you are pulling into an empty repository.
|
||||
repo.reference(
|
||||
&refname,
|
||||
commit_to_merge.id(),
|
||||
true,
|
||||
&format!("Setting {} to {}", refname, commit_to_merge.id()),
|
||||
)?;
|
||||
repo.set_head(&refname)?;
|
||||
repo.checkout_head(Some(
|
||||
git2::build::CheckoutBuilder::default()
|
||||
.allow_conflicts(true)
|
||||
.conflict_style_merge(true)
|
||||
.force(),
|
||||
))?;
|
||||
}
|
||||
};
|
||||
} else if analysis.0.is_normal() {
|
||||
let head_commit = repo.reference_to_annotated_commit(&repo.head()?)?;
|
||||
merge_normal(repo, &head_commit, commit_to_merge)?;
|
||||
} else {
|
||||
debug!("Skipping merge. Nothing to do")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn merge_fast_forward(
|
||||
repo: &Repository,
|
||||
local_reference: &mut Reference,
|
||||
remote_commit: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
info!("Performing fast forward");
|
||||
let name = match local_reference.name() {
|
||||
Some(s) => s.to_string(),
|
||||
None => String::from_utf8_lossy(local_reference.name_bytes()).to_string(),
|
||||
};
|
||||
let msg = format!("Fast-Forward: Setting {} to id: {}", name, remote_commit.id());
|
||||
local_reference.set_target(remote_commit.id(), &msg)?;
|
||||
repo.set_head(&name)?;
|
||||
repo.checkout_head(Some(
|
||||
git2::build::CheckoutBuilder::default()
|
||||
// For some reason, the force is required to make the working directory actually get
|
||||
// updated I suspect we should be adding some logic to handle dirty working directory
|
||||
// states, but this is just an example so maybe not.
|
||||
.force(),
|
||||
))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn merge_normal(
|
||||
repo: &Repository,
|
||||
local: &AnnotatedCommit,
|
||||
remote: &AnnotatedCommit,
|
||||
) -> crate::error::Result<()> {
|
||||
info!("Performing normal merge");
|
||||
let local_tree = repo.find_commit(local.id())?.tree()?;
|
||||
let remote_tree = repo.find_commit(remote.id())?.tree()?;
|
||||
let ancestor = repo.find_commit(repo.merge_base(local.id(), remote.id())?)?.tree()?;
|
||||
|
||||
let mut idx = repo.merge_trees(&ancestor, &local_tree, &remote_tree, None)?;
|
||||
|
||||
if idx.has_conflicts() {
|
||||
let conflicts = idx.conflicts()?;
|
||||
for conflict in conflicts {
|
||||
if let Ok(conflict) = conflict {
|
||||
print_conflict(&conflict);
|
||||
}
|
||||
}
|
||||
return Err(MergeConflicts);
|
||||
}
|
||||
|
||||
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
|
||||
// now create the merge commit
|
||||
let msg = format!("Merge: {} into {}", remote.id(), local.id());
|
||||
let sig = repo.signature()?;
|
||||
let local_commit = repo.find_commit(local.id())?;
|
||||
let remote_commit = repo.find_commit(remote.id())?;
|
||||
|
||||
// Do our merge commit and set current branch head to that commit.
|
||||
let _merge_commit = repo.commit(
|
||||
Some("HEAD"),
|
||||
&sig,
|
||||
&sig,
|
||||
&msg,
|
||||
&result_tree,
|
||||
&[&local_commit, &remote_commit],
|
||||
)?;
|
||||
|
||||
// Set working tree to match head.
|
||||
repo.checkout_head(None)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_conflict(conflict: &git2::IndexConflict) {
|
||||
let ancestor = conflict.ancestor.as_ref().map(path_from_index_entry);
|
||||
let ours = conflict.our.as_ref().map(path_from_index_entry);
|
||||
let theirs = conflict.their.as_ref().map(path_from_index_entry);
|
||||
|
||||
println!("Conflict detected:");
|
||||
if let Some(path) = ancestor {
|
||||
println!(" Common ancestor: {:?}", path);
|
||||
}
|
||||
if let Some(path) = ours {
|
||||
println!(" Ours: {:?}", path);
|
||||
}
|
||||
if let Some(path) = theirs {
|
||||
println!(" Theirs: {:?}", path);
|
||||
}
|
||||
}
|
||||
|
||||
fn path_from_index_entry(entry: &IndexEntry) -> String {
|
||||
String::from_utf8_lossy(entry.path.as_slice()).into_owned()
|
||||
}
|
||||
54
src-tauri/yaak-git/src/pull.rs
Normal file
54
src-tauri/yaak-git/src/pull.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
use crate::callbacks::default_callbacks;
|
||||
use crate::error::Error::NoActiveBranch;
|
||||
use crate::error::Result;
|
||||
use crate::merge::do_merge;
|
||||
use crate::repository::open_repo;
|
||||
use crate::util::{bytes_to_string, get_current_branch};
|
||||
use git2::{FetchOptions, ProxyOptions};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub(crate) struct PullResult {
|
||||
received_bytes: usize,
|
||||
received_objects: usize,
|
||||
}
|
||||
|
||||
pub(crate) fn git_pull(dir: &Path) -> Result<PullResult> {
|
||||
let repo = open_repo(dir)?;
|
||||
|
||||
let branch = get_current_branch(&repo)?.ok_or(NoActiveBranch)?;
|
||||
let branch_ref = branch.get();
|
||||
let branch_ref = bytes_to_string(branch_ref.name_bytes())?;
|
||||
|
||||
let remote_name = repo.branch_upstream_remote(&branch_ref)?;
|
||||
let remote_name = bytes_to_string(&remote_name)?;
|
||||
debug!("Pulling from {remote_name}");
|
||||
|
||||
let mut remote = repo.find_remote(&remote_name)?;
|
||||
|
||||
let mut options = FetchOptions::new();
|
||||
let callbacks = default_callbacks();
|
||||
options.remote_callbacks(callbacks);
|
||||
|
||||
let mut proxy = ProxyOptions::new();
|
||||
proxy.auto();
|
||||
options.proxy_options(proxy);
|
||||
|
||||
remote.fetch(&[&branch_ref], Some(&mut options), None)?;
|
||||
|
||||
let stats = remote.stats();
|
||||
|
||||
let fetch_head = repo.find_reference("FETCH_HEAD")?;
|
||||
let fetch_commit = repo.reference_to_annotated_commit(&fetch_head)?;
|
||||
do_merge(&repo, &branch, &fetch_commit)?;
|
||||
|
||||
Ok(PullResult {
|
||||
received_bytes: stats.received_bytes(),
|
||||
received_objects: stats.received_objects(),
|
||||
})
|
||||
}
|
||||
74
src-tauri/yaak-git/src/push.rs
Normal file
74
src-tauri/yaak-git/src/push.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
use crate::branch::branch_set_upstream_after_push;
|
||||
use crate::callbacks::default_callbacks;
|
||||
use crate::error::Result;
|
||||
use crate::repository::open_repo;
|
||||
use git2::{ProxyOptions, PushOptions};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub(crate) enum PushType {
|
||||
Branch,
|
||||
Tag,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(export, export_to = "gen_git.ts")]
|
||||
pub(crate) enum PushResult {
|
||||
Success,
|
||||
NothingToPush,
|
||||
}
|
||||
|
||||
pub(crate) fn git_push(dir: &Path) -> Result<PushResult> {
|
||||
let repo = open_repo(dir)?;
|
||||
let head = repo.head()?;
|
||||
let branch = head.shorthand().unwrap();
|
||||
let mut remote = repo.find_remote("origin")?;
|
||||
|
||||
let mut options = PushOptions::new();
|
||||
options.packbuilder_parallelism(0);
|
||||
|
||||
let push_result = Mutex::new(PushResult::NothingToPush);
|
||||
|
||||
let mut callbacks = default_callbacks();
|
||||
callbacks.push_transfer_progress(|_current, _total, _bytes| {
|
||||
let mut push_result = push_result.lock().unwrap();
|
||||
*push_result = PushResult::Success;
|
||||
});
|
||||
|
||||
options.remote_callbacks(default_callbacks());
|
||||
|
||||
let mut proxy = ProxyOptions::new();
|
||||
proxy.auto();
|
||||
options.proxy_options(proxy);
|
||||
|
||||
// Push the current branch
|
||||
let force = false;
|
||||
let delete = false;
|
||||
let branch_modifier = match (force, delete) {
|
||||
(true, true) => "+:",
|
||||
(false, true) => ":",
|
||||
(true, false) => "+",
|
||||
(false, false) => "",
|
||||
};
|
||||
|
||||
let ref_type = PushType::Branch;
|
||||
|
||||
let ref_type = match ref_type {
|
||||
PushType::Branch => "heads",
|
||||
PushType::Tag => "tags",
|
||||
};
|
||||
|
||||
let refspec = format!("{branch_modifier}refs/{ref_type}/{branch}");
|
||||
remote.push(&[refspec], Some(&mut options))?;
|
||||
|
||||
branch_set_upstream_after_push(&repo, branch)?;
|
||||
|
||||
let push_result = push_result.lock().unwrap();
|
||||
Ok(push_result.clone())
|
||||
}
|
||||
11
src-tauri/yaak-git/src/repository.rs
Normal file
11
src-tauri/yaak-git/src/repository.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use std::path::Path;
|
||||
use crate::error::Error::{GitRepoNotFound, GitUnknown};
|
||||
|
||||
pub(crate) fn open_repo(dir: &Path) -> crate::error::Result<git2::Repository> {
|
||||
match git2::Repository::discover(dir) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) if e.code() == git2::ErrorCode::NotFound => Err(GitRepoNotFound(dir.to_path_buf())),
|
||||
Err(e) => Err(GitUnknown(e)),
|
||||
}
|
||||
}
|
||||
|
||||
107
src-tauri/yaak-git/src/util.rs
Normal file
107
src-tauri/yaak-git/src/util.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use crate::error::Error::{GenericError, NoDefaultRemoteFound};
|
||||
use crate::error::Result;
|
||||
use git2::{Branch, BranchType, Repository};
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||
|
||||
pub(crate) fn find_ssh_key() -> Option<PathBuf> {
|
||||
let home_dir = env::var("HOME").ok()?;
|
||||
let key_paths = [
|
||||
format!("{}/.ssh/id_ed25519", home_dir),
|
||||
format!("{}/.ssh/id_rsa", home_dir),
|
||||
format!("{}/.ssh/id_ecdsa", home_dir),
|
||||
format!("{}/.ssh/id_dsa", home_dir),
|
||||
];
|
||||
|
||||
for key_path in key_paths.iter() {
|
||||
let path = Path::new(key_path);
|
||||
if path.exists() {
|
||||
return Some(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_current_branch(repo: &Repository) -> Result<Option<Branch>> {
|
||||
for b in repo.branches(None)? {
|
||||
let branch = b?.0;
|
||||
if branch.is_head() {
|
||||
return Ok(Some(branch));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub(crate) fn list_branch_names(repo: &Repository) -> Result<Vec<String>> {
|
||||
let mut branches = Vec::new();
|
||||
for branch in repo.branches(Some(BranchType::Local))? {
|
||||
let branch = branch?.0;
|
||||
let name = branch.name_bytes()?;
|
||||
let name = bytes_to_string(name)?;
|
||||
branches.push(name);
|
||||
}
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
pub(crate) fn get_branch_by_name<'s>(repo: &'s Repository, name: &str) -> Result<Branch<'s>> {
|
||||
Ok(repo.find_branch(name, BranchType::Local)?)
|
||||
}
|
||||
|
||||
pub(crate) fn bytes_to_string(bytes: &[u8]) -> Result<String> {
|
||||
Ok(String::from_utf8(bytes.to_vec())?)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_remote_for_push_in_repo(repo: &Repository) -> Result<String> {
|
||||
let config = repo.config()?;
|
||||
|
||||
let branch = get_current_branch(repo)?;
|
||||
|
||||
if let Some(branch) = branch {
|
||||
let remote_name = bytes_to_string(branch.name_bytes()?)?;
|
||||
|
||||
let entry_name = format!("branch.{}.pushRemote", &remote_name);
|
||||
|
||||
if let Ok(entry) = config.get_entry(&entry_name) {
|
||||
return bytes_to_string(entry.value_bytes());
|
||||
}
|
||||
|
||||
if let Ok(entry) = config.get_entry("remote.pushDefault") {
|
||||
return bytes_to_string(entry.value_bytes());
|
||||
}
|
||||
|
||||
let entry_name = format!("branch.{}.remote", &remote_name);
|
||||
|
||||
if let Ok(entry) = config.get_entry(&entry_name) {
|
||||
return bytes_to_string(entry.value_bytes());
|
||||
}
|
||||
}
|
||||
|
||||
get_default_remote_in_repo(repo)
|
||||
}
|
||||
|
||||
pub(crate) fn get_default_remote_in_repo(repo: &Repository) -> Result<String> {
|
||||
let remotes = repo.remotes()?;
|
||||
|
||||
// if `origin` exists return that
|
||||
let found_origin = remotes.iter().any(|r| r.is_some_and(|r| r == DEFAULT_REMOTE_NAME));
|
||||
if found_origin {
|
||||
return Ok(DEFAULT_REMOTE_NAME.into());
|
||||
}
|
||||
|
||||
// if only one remote exists pick that
|
||||
if remotes.len() == 1 {
|
||||
let first_remote = remotes
|
||||
.iter()
|
||||
.next()
|
||||
.flatten()
|
||||
.map(String::from)
|
||||
.ok_or_else(|| GenericError("no remote found".into()))?;
|
||||
|
||||
return Ok(first_remote);
|
||||
}
|
||||
|
||||
// inconclusive
|
||||
Err(NoDefaultRemoteFound)
|
||||
}
|
||||
Reference in New Issue
Block a user