Skip to content

Instantly share code, notes, and snippets.

@MateuszKubuszok
Last active August 13, 2020 13:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MateuszKubuszok/0d7110bf4399b4adb18a1bc6b0f6697a to your computer and use it in GitHub Desktop.
Save MateuszKubuszok/0d7110bf4399b4adb18a1bc6b0f6697a to your computer and use it in GitHub Desktop.
Utility for copying dotfiles from HOME to some backup directory e.g. git/Dropbox/etc
backup: .dotsync_bck
global:
include:
- .bashrc
- .zshrc
exclude: []
local:
laptop2:
include:
- .snxrc
exclude: []

dotsync

Backup your dotfiles in git repository.

Usage

Initialize config and repo e.g. this way:

./dotsync status --dir .

Edit .dotsync.yml. Dotfiles shared between all you environments put into global, some-desktop-specific put into locals. Configure remote in git repository.

Copy .dotfiles into backup:

./dotsync backup --dir .

and commit and push them:

./dotsync push --dir . --msg "Initial commit"

In order to pull you can use

./dotsync pull --dir .

and then to restore

./dotsync restore --dir .

Local setups

Add them as arguments after backup/restore

./dotsync backup --dir . local1 local2
./dotsync restore --dir . local1 local2
#!/usr/bin/amm
import ammonite.ops._
// config format
import $ivy.`io.circe::circe-core:0.13.0`, io.circe._, io.circe.syntax._
import $ivy.`io.circe::circe-parser:0.13.0`, io.circe.parser._
import $ivy.`io.circe::circe-generic-extras:0.13.0`, io.circe.generic.extras._, io.circe.generic.extras.semiauto._
import $ivy.`io.circe::circe-yaml:0.12.0`, io.circe.yaml, io.circe.yaml.syntax._
implicit val codecConfiguration: Configuration =
Configuration.default.withKebabCaseConstructorNames
final case class Rules(
include: List[String] = Nil,
exclude: List[String] = Nil
) {
def ++(rules: Rules): Rules = Rules(include ++ rules.include, exclude ++ rules.exclude)
}
object Rules {
implicit val decoder: Decoder[Rules] = deriveConfiguredDecoder[Rules]
implicit val encoder: Encoder[Rules] = deriveConfiguredEncoder[Rules]
}
final case class DotSyncConfig(
backup: String = ".dotsync_bck",
global: Rules = Rules(),
local: Map[String, Rules] = Map.empty
)
object DotSyncConfig {
implicit val decoder: Decoder[DotSyncConfig] = deriveConfiguredDecoder[DotSyncConfig]
implicit val encoder: Encoder[DotSyncConfig] = deriveConfiguredEncoder[DotSyncConfig]
def readConfig(dir: os.Path): DotSyncConfig = {
val configLocation = dir/".dotsync.yml"
if (!(exists! configLocation)) {
println(s"$configLocation not found - generating empty config")
write(configLocation, DotSyncConfig().asJson.asYaml.spaces2)
DotSyncConfig()
} else {
println(s"$configLocation found, reading")
yaml.parser.parse(read! configLocation).flatMap(Decoder[DotSyncConfig].decodeJson).right.get
}
}
}
sealed trait OperationType
object OperationType {
case object Backup extends OperationType
case object Restore extends OperationType
case object Status extends OperationType
case object Push extends OperationType
case object Pull extends OperationType
}
// backup -- restore
def backupOrRestore(restore: Boolean,
dir: os.Path,
backupDir: os.Path,
global: Rules,
local: Map[String, Rules],
locals: List[String]): Unit = {
val (in, out) = if (restore) (backupDir, dir) else (dir, backupDir)
val Rules(includeRules, excludeRules) =
(global :: local.collect { case (tag, rule) if locals.contains(tag) => rule }.toList).reduce(_ ++ _)
def allFiles(file: os.Path): LazyList[os.Path] =
if (file.isDir) {
LazyList
.from(file.toIO.listFiles)
.map(FilePath(_))
.collect{ case p: Path => p }
.flatMap(allFiles)
}
else if (file.isFile) LazyList(file)
else LazyList.empty
val included = LazyList.from(includeRules).map(os.RelPath(_)).map(in / _).flatMap(allFiles(_))
val excluded = LazyList.from(excludeRules).map(os.RelPath(_)).map(in / _).flatMap(allFiles(_))
included.filterNot { i =>
excluded.exists(i.startsWith _)
}.foreach { inFile =>
try {
val outFile = out / inFile.relativeTo(in)
println(s"cp: $inFile -> $outFile")
val parent = outFile.toIO.getParentFile
if (!parent.exists) {
mkdir! FilePath(parent).asInstanceOf[os.Path]
}
cp.over(inFile, outFile)
} catch {
case e: Throwable =>
e.printStackTrace
println("Error during sync - skipping")
}
}
}
// status
def gitStatus(backupDir: os.Path): Unit = {
println(backupDir)
%.apply("git", "status")(backupDir)
}
// push
def gitPushChanges(backupDir: os.Path, msg: String): Unit = {
%%.apply("git", "add", ".", ".*")(backupDir)
%%.apply("git", "commit", "-m", msg)(backupDir)
try {
%%.apply("git", "push")(backupDir)
} catch {
case _ => println("git push failed, skipping")
}
}
// pull
def gitPullChanges(backupDir: os.Path): Unit = {
try {
%%.apply("git", "pull")(backupDir)
} catch {
case _ => println("git pull failed, skipping")
}
}
// dispatch
def dispatcher(operation: OperationType,
dir: os.Path,
locals: List[String] = Nil,
msg: String = ""): Unit = {
val DotSyncConfig(backupDirRel, global, local) = DotSyncConfig.readConfig(dir)
val backupDir = dir / backupDirRel
println(s"Backup dir resolved to $backupDir")
if (!backupDir.toIO.exists) {
println(s"$backupDir doesn't exists, creating")
mkdir! backupDir
}
if (!(backupDir / ".git").toIO.exists) {
println(s"Initialize git respository in $backupDir")
%%.apply("git", "init")(backupDir)
println(s"Don't forget to set up git remote in $backupDir to enable push and pull!")
}
operation match {
case OperationType.Backup => backupOrRestore(false, dir, backupDir, global, local, locals.toList)
case OperationType.Restore => backupOrRestore(true, dir, backupDir, global, local, locals.toList)
case OperationType.Status => gitStatus(backupDir)
case OperationType.Push => gitPushChanges(backupDir, msg)
case OperationType.Pull => gitPullChanges(backupDir)
}
}
// main
@main
@doc("copy files from dir to backup dir (included, not excluded)")
def backup(
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it")
dir: os.Path,
@doc("list of locals to process")
locals: String*
): Unit = dispatcher(OperationType.Backup, dir, locals = locals.toList)
@main
@doc("copy files from backup dir to dir (included, not excluded)")
def restore(
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it")
dir: os.Path,
@doc("list of locals to process")
locals: String*
): Unit = dispatcher(OperationType.Restore, dir, locals = locals.toList)
@main
@doc("check git status of backup dir")
def status(
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it")
dir: os.Path
): Unit = dispatcher(OperationType.Status, dir)
@main
@doc("commit all changes and push them to remote repository if possible")
def push(
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it")
dir: os.Path,
@doc("message for git commit")
msg: String
): Unit = dispatcher(OperationType.Push, dir, msg = msg)
@main
@doc("pull changes from remote respository")
def pull(
@doc("directory of .dotsync.yml, backup directory will be resolved in relation to it")
dir: os.Path
): Unit = dispatcher(OperationType.Pull, dir)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment