Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions .github/workflows/post-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ on:
tags:
- "v*"
env:
BIN_NAME: git-stack
CRATE_NAME: git-stack
jobs:
create-release:
name: create-release
Expand Down Expand Up @@ -95,17 +95,19 @@ jobs:
shell: bash
run: |
outdir="./target/${{ env.TARGET_DIR }}/release"
staging="${{ env.BIN_NAME }}-${{ needs.create-release.outputs.release_version }}-${{ matrix.target }}"
staging="${{ env.CRATE_NAME }}-${{ needs.create-release.outputs.release_version }}-${{ matrix.target }}"
mkdir -p "$staging"/{complete,doc}
cp {README.md,LICENSE-*} "$staging/"
cp {CHANGELOG.md,docs/*} "$staging/doc/"
if [ "${{ matrix.os }}" = "windows-2019" ]; then
cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}.exe" "$staging/"
cp "target/${{ matrix.target }}/release/git-stack.exe" "$staging/"
cp "target/${{ matrix.target }}/release/git-branch-backup.exe" "$staging/"
cd "$staging"
7z a "../$staging.zip" .
echo "ASSET=$staging.zip" >> $GITHUB_ENV
else
cp "target/${{ matrix.target }}/release/${{ env.BIN_NAME }}" "$staging/"
cp "target/${{ matrix.target }}/release/git-stack" "$staging/"
cp "target/${{ matrix.target }}/release/git-branch-backup" "$staging/"
tar czf "$staging.tar.gz" -C "$staging" .
echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
fi
Expand Down
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ env_logger = { version = "0.9", default-features = false, features = ["termcolor
atty = "0.2"
itertools = "0.10"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.5"
ignore = "0.4"
bstr = "0.2"
maplit = "1"

[dev-dependencies]
git-fixture = { version = "0.1", path = "crates/git-fixture" }
Expand Down
91 changes: 91 additions & 0 deletions src/backup/backup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Backup {
pub branches: Vec<Branch>,
#[serde(default)]
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub metadata: std::collections::BTreeMap<String, serde_json::Value>,
}

impl Backup {
pub fn load(path: &std::path::Path) -> Result<Self, std::io::Error> {
let file = std::fs::File::open(path)?;
let reader = std::io::BufReader::new(file);
let b = serde_json::from_reader(reader)?;
Ok(b)
}

pub fn save(&self, path: &std::path::Path) -> Result<(), std::io::Error> {
let s = serde_json::to_string_pretty(self)?;
std::fs::write(path, &s)?;
Ok(())
}

pub fn from_repo(repo: &dyn crate::git::Repo) -> Result<Self, git2::Error> {
let mut branches: Vec<_> = repo
.local_branches()
.map(|b| {
let commit = repo.find_commit(b.id).unwrap();
Branch {
name: b.name,
id: b.id,
metadata: maplit::btreemap! {
"summary".to_owned() => serde_json::Value::String(
String::from_utf8_lossy(commit.summary.as_slice()).into_owned()
),
},
}
})
.collect();
branches.sort_unstable();
let metadata = Default::default();
Ok(Self { branches, metadata })
}

pub fn apply(&self, repo: &mut dyn crate::git::Repo) -> Result<(), git2::Error> {
for branch in self.branches.iter() {
log::debug!("Restoring {}", branch.name);
repo.branch(&branch.name, branch.id)?;
}
Ok(())
}
}

#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Branch {
pub name: String,
#[serde(serialize_with = "serialize_oid")]
#[serde(deserialize_with = "deserialize_oid")]
pub id: git2::Oid,
#[serde(default)]
#[serde(skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub metadata: std::collections::BTreeMap<String, serde_json::Value>,
}

fn serialize_oid<S>(id: &git2::Oid, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let id = id.to_string();
serializer.serialize_str(&id)
}

fn deserialize_oid<'de, D>(deserializer: D) -> Result<git2::Oid, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let s = String::deserialize(deserializer)?;
git2::Oid::from_str(&s).map_err(serde::de::Error::custom)
}

impl PartialOrd for Branch {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some((&self.name, self.id).cmp(&(&other.name, other.id)))
}
}

impl Ord for Branch {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(&self.name, self.id).cmp(&(&other.name, other.id))
}
}
6 changes: 6 additions & 0 deletions src/backup/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[allow(clippy::module_inception)]
mod backup;
mod stack;

pub use backup::*;
pub use stack::*;
130 changes: 130 additions & 0 deletions src/backup/stack.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
pub use super::Backup;

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Stack {
pub name: String,
root: std::path::PathBuf,
capacity: Option<usize>,
}

impl Stack {
pub const DEFAULT_STACK: &'static str = "recent";
const EXT: &'static str = "bak";

pub fn new(name: &str, repo: &crate::git::GitRepo) -> Self {
let root = stack_root(repo.raw().path(), name);
let name = name.to_owned();
Self {
name,
root,
capacity: None,
}
}

pub fn all(repo: &crate::git::GitRepo) -> impl Iterator<Item = Self> {
let root = stacks_root(repo.raw().path());
let mut stacks: Vec<_> = std::fs::read_dir(root)
.into_iter()
.flatten()
.filter_map(|e| {
let e = e.ok()?;
let e = e.file_type().ok()?.is_dir().then(|| e)?;
let p = e.path();
let stack_name = p.file_name()?.to_str()?.to_owned();
let stack_root = stack_root(repo.raw().path(), &stack_name);
Some(Self {
name: stack_name,
root: stack_root,
capacity: None,
})
})
.collect();
if !stacks.iter().any(|v| v.name == Self::DEFAULT_STACK) {
stacks.insert(0, Self::new(Self::DEFAULT_STACK, repo));
}
stacks.into_iter()
}

pub fn capacity(&mut self, capacity: Option<usize>) {
self.capacity = capacity;
}

pub fn iter(&self) -> impl DoubleEndedIterator<Item = std::path::PathBuf> {
let mut elements: Vec<(usize, std::path::PathBuf)> = std::fs::read_dir(&self.root)
.into_iter()
.flatten()
.filter_map(|e| {
let e = e.ok()?;
let e = e.file_type().ok()?.is_file().then(|| e)?;
let p = e.path();
let p = (p.extension()? == Self::EXT).then(|| p)?;
let index = p.file_stem()?.to_str()?.parse::<usize>().ok()?;
Some((index, p))
})
.collect();
elements.sort_unstable();
elements.into_iter().map(|(_, p)| p)
}

pub fn push(&mut self, backup: Backup) -> Result<std::path::PathBuf, std::io::Error> {
let elems: Vec<_> = self.iter().collect();
let last = elems.iter().last();
let next_index = match last {
Some(last) => {
let current_index = last
.file_stem()
.unwrap()
.to_str()
.unwrap()
.parse::<usize>()
.unwrap();
current_index + 1
}
None => 0,
};
std::fs::create_dir_all(&self.root)?;
let new_path = self.root.join(format!("{}.{}", next_index, Self::EXT));
backup.save(&new_path)?;
log::trace!("Backed up as {}", new_path.display());

if let Some(capacity) = self.capacity {
let len = elems.len();
if capacity < len {
let remove = len - capacity;
log::warn!("Too many backups, clearing {} oldest", remove);
for backup_path in &elems[0..remove] {
if let Err(err) = std::fs::remove_file(&backup_path) {
log::trace!("Failed to remove {}: {}", backup_path.display(), err);
} else {
log::trace!("Removed {}", backup_path.display());
}
}
}
}

Ok(new_path)
}

pub fn clear(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}

pub fn pop(&mut self) -> Option<std::path::PathBuf> {
let mut elems: Vec<_> = self.iter().collect();
let last = elems.pop()?;
std::fs::remove_file(&last).ok()?;
Some(last)
}

pub fn peek(&mut self) -> Option<std::path::PathBuf> {
self.iter().last()
}
}

fn stacks_root(repo: &std::path::Path) -> std::path::PathBuf {
repo.join("branch-backup")
}

fn stack_root(repo: &std::path::Path, stack: &str) -> std::path::PathBuf {
repo.join("branch-backup").join(stack)
}
86 changes: 86 additions & 0 deletions src/bin/git-branch-backup/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#[derive(structopt::StructOpt)]
#[structopt(
setting = structopt::clap::AppSettings::UnifiedHelpMessage,
setting = structopt::clap::AppSettings::DeriveDisplayOrder,
setting = structopt::clap::AppSettings::DontCollapseArgsInUsage
)]
pub struct Args {
#[structopt(subcommand)]
pub subcommand: Option<Subcommand>,

#[structopt(flatten)]
pub push: PushArgs,

#[structopt(flatten)]
pub color: git_stack::color::ColorArgs,

#[structopt(flatten)]
pub verbose: clap_verbosity_flag::Verbosity,
}

#[derive(structopt::StructOpt)]
pub enum Subcommand {
/// Backup all branches
Push(PushArgs),
/// List all backups
List(ListArgs),
/// Clear all backups
Clear(ClearArgs),
/// Delete the last backup
Drop(DropArgs),
/// Apply the last backup, deleting it
Pop(PopArgs),
/// Apply the last backup
Apply(ApplyArgs),
/// List all backup stacks
Stacks(StacksArgs),
}

#[derive(structopt::StructOpt)]
pub struct PushArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,

/// Annotate the backup with the given message
#[structopt(short, long)]
pub message: Option<String>,
}

#[derive(structopt::StructOpt)]
pub struct ListArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,
}

#[derive(structopt::StructOpt)]
pub struct ClearArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,
}

#[derive(structopt::StructOpt)]
pub struct DropArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,
}

#[derive(structopt::StructOpt)]
pub struct PopArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,
}

#[derive(structopt::StructOpt)]
pub struct ApplyArgs {
/// Specify which backup stack to use
#[structopt(default_value = git_stack::backup::Stack::DEFAULT_STACK)]
pub stack: String,
}

#[derive(structopt::StructOpt)]
pub struct StacksArgs {}
Loading