Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Utility for copying dotfiles from HOME to some backup directory e.g. git/Dropbox/etc
backup: .dotsync_bck
- .bashrc
- .zshrc
exclude: []
- .snxrc
exclude: []


Backup your dotfiles in git repository.


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
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 =
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)
} 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) {
.collect{ case p: Path => p }
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 =>
println("Error during sync - skipping")
// status
def gitStatus(backupDir: os.Path): Unit = {
%.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
@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)
@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)
@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)
@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)
@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