diff --git a/git-backup.go b/git-backup.go
index 5ea68fb14f7247d7ddb62afec9fd56760c85e3a0..75d9889af84fb58b78e91817d39c8ccc22d1b424 100644
--- a/git-backup.go
+++ b/git-backup.go
@@ -185,7 +185,7 @@ func blob_to_file(g *git.Repository, blob_sha1 Sha1, mode uint32, path string) {
 // another, so that on backup restore we only have to recreate original tag
 // object and tagged object is kept there in repo thanks to it being reachable
 // through created commit.
-func obj_represent_as_commit(g *git.Repository, sha1 Sha1, obj_type git.ObjectType) Sha1 {
+func obj_represent_as_commit(ctx context.Context, g *git.Repository, sha1 Sha1, obj_type git.ObjectType) Sha1 {
 	switch obj_type {
 	case git.ObjectTag, git.ObjectTree, git.ObjectBlob:
 		// ok
@@ -223,7 +223,7 @@ func obj_represent_as_commit(g *git.Repository, sha1 Sha1, obj_type git.ObjectTy
 	//  v                 .tree   -> ø
 	// Commit             .parent -> Commit
 	if tagged_type == git.ObjectCommit {
-		return zcommit_tree(mktree_empty(), []Sha1{tagged_sha1}, obj_encoded)
+		return zcommit_tree(mktree_empty(ctx), []Sha1{tagged_sha1}, obj_encoded)
 	}
 
 	// Tag        ~>     Commit*
@@ -239,7 +239,7 @@ func obj_represent_as_commit(g *git.Repository, sha1 Sha1, obj_type git.ObjectTy
 	//  v                 .tree   -> Tree* "tagged" -> Blob
 	// Blob               .parent -> ø
 	if tagged_type == git.ObjectBlob {
-		tree_for_blob := xgitSha1("mktree", RunWith{stdin: fmt.Sprintf("100644 blob %s\ttagged\n", tagged_sha1)})
+		tree_for_blob := xgitSha1(ctx, "mktree", RunWith{stdin: fmt.Sprintf("100644 blob %s\ttagged\n", tagged_sha1)})
 		return zcommit_tree(tree_for_blob, []Sha1{}, obj_encoded)
 	}
 
@@ -248,8 +248,8 @@ func obj_represent_as_commit(g *git.Repository, sha1 Sha1, obj_type git.ObjectTy
 	//  v                 .tree   -> ø
 	// Tag₁               .parent -> Commit₁*
 	if tagged_type == git.ObjectTag {
-		commit1 := obj_represent_as_commit(g, tagged_sha1, tagged_type)
-		return zcommit_tree(mktree_empty(), []Sha1{commit1}, obj_encoded)
+		commit1 := obj_represent_as_commit(ctx, g, tagged_sha1, tagged_type)
+		return zcommit_tree(mktree_empty(ctx), []Sha1{commit1}, obj_encoded)
 	}
 
 	exc.Raisef("%s (%q): unknown tagged type", sha1, tagged_type)
@@ -335,7 +335,7 @@ type PullSpec struct {
 	dir, prefix string
 }
 
-func cmd_pull(gb *git.Repository, argv []string) {
+func cmd_pull(ctx context.Context, gb *git.Repository, argv []string) {
 	flags := flag.FlagSet{Usage: cmd_pull_usage}
 	flags.Init("", flag.ExitOnError)
 	flags.Parse(argv)
@@ -358,7 +358,7 @@ func cmd_pull(gb *git.Repository, argv []string) {
 		pullspecv = append(pullspecv, PullSpec{dir, prefix})
 	}
 
-	cmd_pull_(gb, pullspecv)
+	cmd_pull_(ctx, gb, pullspecv)
 }
 
 // Ref is info about a reference pointing to sha1.
@@ -367,7 +367,7 @@ type Ref struct {
 	sha1 Sha1
 }
 
-func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
+func cmd_pull_(ctx context.Context, gb *git.Repository, pullspecv []PullSpec) {
 	// while pulling, we'll keep refs from all pulled repositories under temp
 	// unique work refs namespace.
 	backup_time := time.Now().Format("20060102-1504")               // %Y%m%d-%H%M
@@ -375,18 +375,18 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 
 	// prevent another `git-backup pull` from running simultaneously
 	backup_lock := "refs/backup.locked"
-	xgit("update-ref", backup_lock, mktree_empty(), Sha1{})
-	defer xgit("update-ref", "-d", backup_lock)
+	xgit(ctx, "update-ref", backup_lock, mktree_empty(ctx), Sha1{})
+	defer xgit(context.Background(), "update-ref", "-d", backup_lock)
 
 	// make sure there is root commit
 	var HEAD Sha1
 	var err error
-	gerr, __, _ := ggit("rev-parse", "--verify", "HEAD")
+	gerr, __, _ := ggit(ctx, "rev-parse", "--verify", "HEAD")
 	if gerr != nil {
 		infof("# creating root commit")
 		// NOTE `git commit` does not work in bare repo - do commit by hand
-		HEAD = xcommit_tree(gb, mktree_empty(), []Sha1{}, "Initialize git-backup repository")
-		xgit("update-ref", "-m", "git-backup pull init", "HEAD", HEAD)
+		HEAD = xcommit_tree(gb, mktree_empty(ctx), []Sha1{}, "Initialize git-backup repository")
+		xgit(ctx, "update-ref", "-m", "git-backup pull init", "HEAD", HEAD)
 	} else {
 		HEAD, err = Sha1Parse(__)
 		exc.Raiseif(err)
@@ -408,7 +408,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	// Of those there are ~ 1.9·10⁶ commit objects, i.e. ~10% of total.
 	// Since 1 sha1 is 2·10¹ bytes, the space needed for keeping sha1 of all
 	// commits is ~ 4·10⁷B = ~40MB. It is thus ok to keep this index in RAM for now.
-	for _, __ := range xstrings.SplitLines(xgit("rev-list", HEAD), "\n") {
+	for _, __ := range xstrings.SplitLines(xgit(ctx, "rev-list", HEAD), "\n") {
 		sha1, err := Sha1Parse(__)
 		exc.Raiseif(err)
 		alreadyHave.Add(sha1)
@@ -425,7 +425,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	htree, err := hcommit.Tree()
 	exc.Raiseif(err)
 	if htree.EntryByName("backup.refs") != nil {
-		repotab, err := loadBackupRefs(fmt.Sprintf("%s:backup.refs", HEAD))
+		repotab, err := loadBackupRefs(ctx, fmt.Sprintf("%s:backup.refs", HEAD))
 		exc.Raiseif(err)
 
 		for _, repo := range repotab {
@@ -449,7 +449,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 
 		// make sure index is empty for prefix (so that we start from clean
 		// prefix namespace and this way won't leave stale removed things)
-		xgit("rm", "--cached", "-r", "--ignore-unmatch", "--", prefix)
+		xgit(ctx, "rm", "--cached", "-r", "--ignore-unmatch", "--", prefix)
 
 		here := my.FuncName()
 		err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) (errout error) {
@@ -463,6 +463,10 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 				return err
 			}
 
+			if ctx.Err() != nil {
+				return ctx.Err()
+			}
+
 			// propagate exceptions properly via filepath.Walk as errors with calling context
 			// (filepath is not our code)
 			defer exc.Catch(func(e *exc.Error) {
@@ -492,7 +496,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 
 			// git repo - let's pull all refs from it to our backup refs namespace
 			infof("# git  %s\t<- %s", prefix, path)
-			refv, _, err := fetch(path, alreadyHave)
+			refv, _, err := fetch(ctx, path, alreadyHave)
 			exc.Raiseif(err)
 
 			// TODO don't store to git references all references from fetched repository:
@@ -535,7 +539,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	}
 
 	// add to index files we converted to blobs
-	xgit("update-index", "--add", "--index-info", RunWith{stdin: strings.Join(blobbedv, "\n")})
+	xgit(ctx, "update-index", "--add", "--index-info", RunWith{stdin: strings.Join(blobbedv, "\n")})
 
 	// all refs from all found git repositories populated.
 	// now prepare manifest with ref -> sha1 and do a synthetic commit merging all that sha1
@@ -553,7 +557,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	//
 	// NOTE `git for-each-ref` sorts output by ref
 	//      -> backup_refs is sorted and stable between runs
-	backup_refs_dump := xgit("for-each-ref", backup_refs_work)
+	backup_refs_dump := xgit(ctx, "for-each-ref", backup_refs_work)
 	backup_refs_list := []Ref{}       // parsed dump
 	backup_refsv := []string{}        // backup.refs content
 	backup_refs_parents := Sha1Set{}  // sha1 for commit parents, obtained from refs
@@ -579,7 +583,7 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 				if !ok {
 					exc.Raisef("%s: invalid git type in entry %q", backup_refs_work, __)
 				}
-				sha1_ = obj_represent_as_commit(gb, sha1, obj_type)
+				sha1_ = obj_represent_as_commit(ctx, gb, sha1, obj_type)
 				noncommit_seen[sha1] = sha1_
 			}
 
@@ -598,17 +602,17 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	sort.Sort(BySha1(backup_refs_parentv)) // so parents order is stable in between runs
 
 	// backup_refs -> blob
-	backup_refs_sha1 := xgitSha1("hash-object", "-w", "--stdin", RunWith{stdin: backup_refs})
+	backup_refs_sha1 := xgitSha1(ctx, "hash-object", "-w", "--stdin", RunWith{stdin: backup_refs})
 
 	// add backup_refs blob to index
-	xgit("update-index", "--add", "--cacheinfo", fmt.Sprintf("100644,%s,backup.refs", backup_refs_sha1))
+	xgit(ctx, "update-index", "--add", "--cacheinfo", fmt.Sprintf("100644,%s,backup.refs", backup_refs_sha1))
 
 	// index is ready - prepare tree and commit
-	backup_tree_sha1 := xgitSha1("write-tree")
+	backup_tree_sha1 := xgitSha1(ctx, "write-tree")
 	commit_sha1 := xcommit_tree(gb, backup_tree_sha1, append([]Sha1{HEAD}, backup_refs_parentv...),
 		"Git-backup "+backup_time)
 
-	xgit("update-ref", "-m", "git-backup pull", "HEAD", commit_sha1, HEAD)
+	xgit(ctx, "update-ref", "-m", "git-backup pull", "HEAD", commit_sha1, HEAD)
 
 	// remove no-longer needed backup refs & verify they don't stay
 	backup_refs_delete := ""
@@ -616,8 +620,8 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 		backup_refs_delete += fmt.Sprintf("delete %s %s\n", ref.name, ref.sha1)
 	}
 
-	xgit("update-ref", "--stdin", RunWith{stdin: backup_refs_delete})
-	__ = xgit("for-each-ref", backup_refs_work)
+	xgit(ctx, "update-ref", "--stdin", RunWith{stdin: backup_refs_delete})
+	__ = xgit(ctx, "for-each-ref", backup_refs_work)
 	if __ != "" {
 		exc.Raisef("Backup refs under %s not deleted properly", backup_refs_work)
 	}
@@ -637,21 +641,21 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 	//       accumulate, the longer pull starts to be, so it becomes O(n^2).
 	//
 	//       -> what to do is described nearby fetch/mkref call.
-	gitdir := xgit("rev-parse", "--git-dir")
+	gitdir := xgit(ctx, "rev-parse", "--git-dir")
 	err = os.RemoveAll(gitdir + "/" + backup_refs_work)
 	exc.Raiseif(err) // NOTE err is nil if path does not exist
 
 	// if we have working copy - update it
-	bare := xgit("rev-parse", "--is-bare-repository")
+	bare := xgit(ctx, "rev-parse", "--is-bare-repository")
 	if bare != "true" {
 		// `git checkout-index -af`  -- does not delete deleted files
 		// `git read-tree -v -u --reset HEAD~ HEAD`  -- needs index matching
 		// original worktree to properly work, but we already have updated index
 		//
 		// so we get changes we committed as diff and apply to worktree
-		diff := xgit("diff", "--binary", HEAD, "HEAD", RunWith{raw: true})
+		diff := xgit(ctx, "diff", "--binary", HEAD, "HEAD", RunWith{raw: true})
 		if diff != "" {
-			diffstat := xgit("apply", "--stat", "--apply", "--binary", "--whitespace=nowarn",
+			diffstat := xgit(ctx, "apply", "--stat", "--apply", "--binary", "--whitespace=nowarn",
 				RunWith{stdin: diff, raw: true})
 			infof("%s", diffstat)
 		}
@@ -680,11 +684,11 @@ func cmd_pull_(gb *git.Repository, pullspecv []PullSpec) {
 //
 // Note: fetch does not create any local references - the references returned
 // only describe state of references in fetched source repository.
-func fetch(repo string, alreadyHave Sha1Set) (refv, fetchedv []Ref, err error) {
+func fetch(ctx context.Context, repo string, alreadyHave Sha1Set) (refv, fetchedv []Ref, err error) {
 	defer xerr.Contextf(&err, "fetch %s", repo)
 
 	// first check which references are advertised
-	refv, err = lsremote(repo)
+	refv, err = lsremote(ctx, repo)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -730,7 +734,7 @@ func fetch(repo string, alreadyHave Sha1Set) (refv, fetchedv []Ref, err error) {
 	}
 	arg(RunWith{stderr: gitprogress()})
 
-	gerr, _, _ := ggit(argv...)
+	gerr, _, _ := ggit(ctx, argv...)
 	if gerr != nil {
 		return nil, nil, gerr
 	}
@@ -754,7 +758,7 @@ func fetch(repo string, alreadyHave Sha1Set) (refv, fetchedv []Ref, err error) {
 	}
 	arg(RunWith{stderr: gitprogress()})
 
-	gerr, _, _ = ggit(argv...)
+	gerr, _, _ = ggit(ctx, argv...)
 	if gerr != nil {
 		return nil, nil, fmt.Errorf("remote did not send all neccessary objects")
 	}
@@ -764,7 +768,7 @@ func fetch(repo string, alreadyHave Sha1Set) (refv, fetchedv []Ref, err error) {
 }
 
 // lsremote lists all references advertised by repo.
-func lsremote(repo string) (refv []Ref, err error) {
+func lsremote(ctx context.Context, repo string) (refv []Ref, err error) {
 	defer xerr.Contextf(&err, "lsremote %s", repo)
 
 	// NOTE --refs instructs to omit peeled refs like
@@ -777,7 +781,7 @@ func lsremote(repo string) (refv []Ref, err error) {
 	//   https://public-inbox.org/git/20180610143231.7131-1-kirr@nexedi.com/
 	//
 	// we don't need to pull them anyway.
-	gerr, stdout, _ := ggit("ls-remote", "--refs", repo)
+	gerr, stdout, _ := ggit(ctx, "ls-remote", "--refs", repo)
 	if gerr != nil {
 		return nil, gerr
 	}
@@ -821,7 +825,7 @@ type RestoreSpec struct {
 	prefix, dir string
 }
 
-func cmd_restore(gb *git.Repository, argv []string) {
+func cmd_restore(ctx context.Context, gb *git.Repository, argv []string) {
 	flags := flag.FlagSet{Usage: cmd_restore_usage}
 	flags.Init("", flag.ExitOnError)
 	flags.Parse(argv)
@@ -846,7 +850,7 @@ func cmd_restore(gb *git.Repository, argv []string) {
 		restorespecv = append(restorespecv, RestoreSpec{prefix, dir})
 	}
 
-	cmd_restore_(gb, HEAD, restorespecv)
+	cmd_restore_(ctx, gb, HEAD, restorespecv)
 }
 
 // kirr/wendelin.core.git/heads/master -> kirr/wendelin.core.git, heads/master
@@ -943,11 +947,11 @@ type PackExtractReq struct {
 	prefix string
 }
 
-func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec) {
-	HEAD := xgitSha1("rev-parse", "--verify", HEAD_)
+func cmd_restore_(ctx context.Context, gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec) {
+	HEAD := xgitSha1(ctx, "rev-parse", "--verify", HEAD_)
 
 	// read backup refs index
-	repotab, err := loadBackupRefs(fmt.Sprintf("%s:backup.refs", HEAD))
+	repotab, err := loadBackupRefs(ctx, fmt.Sprintf("%s:backup.refs", HEAD))
 	exc.Raiseif(err)
 
 	// flattened & sorted repotab
@@ -962,7 +966,7 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 	repotab = nil
 
 	packxq := make(chan PackExtractReq, 2*njobs) // requests to extract packs
-	wg := xsync.NewWorkGroup(context.Background())
+	wg := xsync.NewWorkGroup(ctx)
 
 	// main worker: walk over specified prefixes restoring files and
 	// scheduling pack extraction requests from *.git -> packxq
@@ -982,7 +986,7 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 			exc.Raiseif(err)
 
 			// files
-			lstree := xgit("ls-tree", "--full-tree", "-r", "-z", "--", HEAD, prefix, RunWith{raw: true})
+			lstree := xgit(ctx, "ls-tree", "--full-tree", "-r", "-z", "--", HEAD, prefix, RunWith{raw: true})
 			repos_seen := StrSet{} // dirs of *.git seen while restoring files
 			for _, __ := range xstrings.SplitLines(lstree, "\x00") {
 				mode, type_, sha1, filename, err := parse_lstree_entry(__)
@@ -994,6 +998,8 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 					exc.Raisef("%s: invalid/unexpected ls-tree entry %q", HEAD, __)
 				}
 
+				exc.Raiseif(ctx.Err())
+
 				filename = reprefix(prefix, dir, filename)
 				infof("# file %s\t-> %s", prefix, filename)
 				blob_to_file(gb, sha1, mode, filename)
@@ -1082,10 +1088,10 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 					}
 					pack_argv = append(pack_argv, p.repopath+"/objects/pack/pack")
 
-					xgit2(pack_argv, RunWith{stdin: p.refs.Sha1HeadsStr(), stderr: gitprogress()})
+					xgit2(ctx, pack_argv, RunWith{stdin: p.refs.Sha1HeadsStr(), stderr: gitprogress()})
 
 					// verify that extracted repo refs match backup.refs index after extraction
-					x_ref_list := xgit("--git-dir="+p.repopath,
+					x_ref_list := xgit(ctx, "--git-dir="+p.repopath,
 						"for-each-ref", "--format=%(objectname) %(refname)")
 					repo_refs := p.refs.Values()
 					sort.Sort(ByRefname(repo_refs))
@@ -1107,7 +1113,7 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 					//
 					// Compared to fsck we do not re-compute sha1 sum of objects which
 					// is significantly faster.
-					gerr, _, _ := ggit("--git-dir="+p.repopath,
+					gerr, _, _ := ggit(ctx, "--git-dir="+p.repopath,
 						"rev-list", "--objects", "--stdin", "--quiet", RunWith{stdin: p.refs.Sha1HeadsStr()})
 					if gerr != nil {
 						fmt.Fprintln(os.Stderr, "E: Problem while checking connectivity of extracted repo:")
@@ -1135,10 +1141,10 @@ func cmd_restore_(gb *git.Repository, HEAD_ string, restorespecv []RestoreSpec)
 // loadBackupRefs loads 'backup.ref' content from a git object.
 //
 // an example of object is e.g. "HEAD:backup.ref".
-func loadBackupRefs(object string) (repotab map[string]*BackupRepo, err error) {
+func loadBackupRefs(ctx context.Context, object string) (repotab map[string]*BackupRepo, err error) {
 	defer xerr.Contextf(&err, "load backup.refs %q", object)
 
-	gerr, backup_refs, _ := ggit("cat-file", "blob", object)
+	gerr, backup_refs, _ := ggit(ctx, "cat-file", "blob", object)
 	if gerr != nil {
 		return nil, gerr
 	}
@@ -1177,7 +1183,7 @@ func loadBackupRefs(object string) (repotab map[string]*BackupRepo, err error) {
 	return repotab, nil
 }
 
-var commands = map[string]func(*git.Repository, []string){
+var commands = map[string]func(context.Context, *git.Repository, []string){
 	"pull":    cmd_pull,
 	"restore": cmd_restore,
 }
@@ -1238,5 +1244,5 @@ func main() {
 	gb, err := git.OpenRepository(".")
 	exc.Raiseif(err)
 
-	cmd(gb, argv[1:])
+	cmd(context.Background(), gb, argv[1:])
 }
diff --git a/git-backup_test.go b/git-backup_test.go
index 89a35e7a7fdac7a3b20e94431b30b3d6989dfa1d..1f538a5f5ef74a754ea0b4f76c4e80febc9e3dd8 100644
--- a/git-backup_test.go
+++ b/git-backup_test.go
@@ -20,6 +20,7 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -70,12 +71,14 @@ func xgittype(s string) git.ObjectType {
 
 // xnoref asserts that git reference ref does not exists.
 func xnoref(ref string) {
-	xgit("update-ref", "--stdin", RunWith{stdin: fmt.Sprintf("verify refs/%s %s\n", ref, Sha1{})})
+	xgit(context.Background(), "update-ref", "--stdin", RunWith{stdin: fmt.Sprintf("verify refs/%s %s\n", ref, Sha1{})})
 }
 
 
 // verify end-to-end pull-restore
 func TestPullRestore(t *testing.T) {
+	ctx := context.Background()
+
 	// if something raises -> don't let testing panic - report it as proper error with context.
 	here := my.FuncName()
 	defer exc.Catch(func(e *exc.Error) {
@@ -114,7 +117,7 @@ func TestPullRestore(t *testing.T) {
 	}
 
 	// init backup repository
-	xgit("init", "--bare", "backup.git")
+	xgit(ctx, "init", "--bare", "backup.git")
 	xchdir(t, "backup.git")
 	gb, err := git.OpenRepository(".")
 	if err != nil {
@@ -123,10 +126,10 @@ func TestPullRestore(t *testing.T) {
 
 	// pull from testdata
 	my0 := mydir + "/testdata/0"
-	cmd_pull(gb, []string{my0 + ":b0"}) // only empty repo in testdata/0
+	cmd_pull(ctx, gb, []string{my0 + ":b0"}) // only empty repo in testdata/0
 
 	my1 := mydir + "/testdata/1"
-	cmd_pull(gb, []string{my1 + ":b1"})
+	cmd_pull(ctx, gb, []string{my1 + ":b1"})
 
 	// verify tag/tree/blob encoding is 1) consistent and 2) always the same.
 	// we need it be always the same so different git-backup versions can
@@ -158,8 +161,8 @@ func TestPullRestore(t *testing.T) {
 		}
 
 		// encoding original object should give sha1_
-		obj_type := xgit("cat-file", "-t", nc.sha1)
-		sha1_ := obj_represent_as_commit(gb, nc.sha1, xgittype(obj_type))
+		obj_type := xgit(ctx, "cat-file", "-t", nc.sha1)
+		sha1_ := obj_represent_as_commit(ctx, gb, nc.sha1, xgittype(obj_type))
 		if sha1_ != nc.sha1_ {
 			t.Fatalf("encode %s -> %s ;  want %s", sha1, sha1_, nc.sha1_)
 		}
@@ -181,10 +184,10 @@ func TestPullRestore(t *testing.T) {
 		}
 
 		// prune all non-reachable objects (e.g. tags just pulled - they were encoded as commits)
-		xgit("prune")
+		xgit(ctx, "prune")
 
 		// verify backup repo is all ok
-		xgit("fsck")
+		xgit(ctx, "fsck")
 
 		// verify that just pulled tag objects are now gone after pruning -
 		// - they become not directly git-present. The only possibility to
@@ -193,7 +196,7 @@ func TestPullRestore(t *testing.T) {
 			if !nc.istag {
 				continue
 			}
-			gerr, _, _ := ggit("cat-file", "-p", nc.sha1)
+			gerr, _, _ := ggit(ctx, "cat-file", "-p", nc.sha1)
 			if gerr == nil {
 				t.Fatalf("tag %s still present in backup.git after git-prune", nc.sha1)
 			}
@@ -210,14 +213,14 @@ func TestPullRestore(t *testing.T) {
 	afterPull()
 
 	// pull again - it should be noop
-	h1 := xgitSha1("rev-parse", "HEAD")
-	cmd_pull(gb, []string{my1 + ":b1"})
+	h1 := xgitSha1(ctx, "rev-parse", "HEAD")
+	cmd_pull(ctx, gb, []string{my1 + ":b1"})
 	afterPull()
-	h2 := xgitSha1("rev-parse", "HEAD")
+	h2 := xgitSha1(ctx, "rev-parse", "HEAD")
 	if h1 == h2 {
 		t.Fatal("pull: second run did not ajusted HEAD")
 	}
-	δ12 := xgit("diff", h1, h2)
+	δ12 := xgit(ctx, "diff", h1, h2)
 	if δ12 != "" {
 		t.Fatalf("pull: second run was not noop: δ:\n%s", δ12)
 	}
@@ -225,10 +228,10 @@ func TestPullRestore(t *testing.T) {
 
 	// restore backup
 	work1 := workdir + "/1"
-	cmd_restore(gb, []string{"HEAD", "b1:" + work1})
+	cmd_restore(ctx, gb, []string{"HEAD", "b1:" + work1})
 
 	// verify files restored to the same as original
-	gerr, diff, _ := ggit("diff", "--no-index", "--raw", "--exit-code", my1, work1)
+	gerr, diff, _ := ggit(ctx, "diff", "--no-index", "--raw", "--exit-code", my1, work1)
 	// 0 - no diff, 1 - has diff, 2 - problem
 	if gerr != nil && gerr.Sys().(syscall.WaitStatus).ExitStatus() > 1 {
 		t.Fatal(gerr)
@@ -267,12 +270,12 @@ func TestPullRestore(t *testing.T) {
 
 		for _, repo := range R {
 			// fsck just in case
-			xgit("--git-dir="+repo.path, "fsck")
+			xgit(ctx, "--git-dir="+repo.path, "fsck")
 			// NOTE for-each-ref sorts output by refname
-			repo.reflist = xgit("--git-dir="+repo.path, "for-each-ref")
+			repo.reflist = xgit(ctx, "--git-dir="+repo.path, "for-each-ref")
 			// NOTE rev-list emits objects in reverse chronological order,
 			//      starting from refs roots which are also ordered by refname
-			repo.revlist = xgit("--git-dir="+repo.path, "rev-list", "--all", "--objects")
+			repo.revlist = xgit(ctx, "--git-dir="+repo.path, "rev-list", "--all", "--objects")
 		}
 
 		if R[0].reflist != R[1].reflist {
@@ -301,7 +304,7 @@ func TestPullRestore(t *testing.T) {
 			xnoref("backup.locked")
 		})
 
-		cmd_pull(gb, []string{my2 + ":b2"})
+		cmd_pull(ctx, gb, []string{my2 + ":b2"})
 		t.Fatal("pull corrupt.git: did not complain")
 	}()
 
@@ -341,7 +344,7 @@ func TestPullRestore(t *testing.T) {
 		err = os.Setenv("HOME", my3+"/incomplete-send-pack.git/"+kind)
 		exc.Raiseif(err)
 
-		cmd_pull(gb, []string{my3 + ":b3"})
+		cmd_pull(ctx, gb, []string{my3 + ":b3"})
 		t.Fatalf("pull incomplete-send-pack.git/%s: did not complain", kind)
 	}
 
@@ -358,7 +361,7 @@ func TestPullRestore(t *testing.T) {
 
 	// pulling incomplete-send-pack.git without pack-objects hook must succeed:
 	// without $HOME tweaks full and complete pack is sent.
-	cmd_pull(gb, []string{my3 + ":b3"})
+	cmd_pull(ctx, gb, []string{my3 + ":b3"})
 }
 
 func TestRepoRefSplit(t *testing.T) {
diff --git a/git.go b/git.go
index 2d064233c75fa35f53746a9c5b9ace41117f91b1..8be6751fd61244041db27e38b750fbcf1b8b1a33 100644
--- a/git.go
+++ b/git.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2016  Nexedi SA and Contributors.
+// Copyright (C) 2015-2020  Nexedi SA and Contributors.
 //                          Kirill Smelkov <kirr@nexedi.com>
 //
 // This program is free software: you can Use, Study, Modify and Redistribute
@@ -22,6 +22,7 @@ package main
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"os"
 	"os/exec"
@@ -48,18 +49,20 @@ type RunWith struct {
 }
 
 // run `git *argv` -> error, stdout, stderr
-func _git(argv []string, ctx RunWith) (err error, stdout, stderr string) {
+func _git(ctx context.Context, argv []string, rctx RunWith) (err error, stdout, stderr string) {
 	debugf("git %s", strings.Join(argv, " "))
 
-	cmd := exec.Command("git", argv...)
+	// XXX exec.CommandContext does `kill -9` on ctx cancel
+	// XXX -> rework to `kill -TERM` so that spawned process can finish cleanly?
+	cmd := exec.CommandContext(ctx, "git", argv...)
 	stdoutBuf := bytes.Buffer{}
 	stderrBuf := bytes.Buffer{}
 
-	if ctx.stdin != "" {
-		cmd.Stdin = strings.NewReader(ctx.stdin)
+	if rctx.stdin != "" {
+		cmd.Stdin = strings.NewReader(rctx.stdin)
 	}
 
-	switch ctx.stdout {
+	switch rctx.stdout {
 	case PIPE:
 		cmd.Stdout = &stdoutBuf
 	case DontRedirect:
@@ -68,7 +71,7 @@ func _git(argv []string, ctx RunWith) (err error, stdout, stderr string) {
 		panic("git: stdout redirect mode invalid")
 	}
 
-	switch ctx.stderr {
+	switch rctx.stderr {
 	case PIPE:
 		cmd.Stderr = &stderrBuf
 	case DontRedirect:
@@ -77,9 +80,9 @@ func _git(argv []string, ctx RunWith) (err error, stdout, stderr string) {
 		panic("git: stderr redirect mode invalid")
 	}
 
-	if ctx.env != nil {
+	if rctx.env != nil {
 		env := []string{}
-		for k, v := range ctx.env {
+		for k, v := range rctx.env {
 			env = append(env, k+"="+v)
 		}
 		cmd.Env = env
@@ -89,7 +92,7 @@ func _git(argv []string, ctx RunWith) (err error, stdout, stderr string) {
 	stdout = mem.String(stdoutBuf.Bytes())
 	stderr = mem.String(stderrBuf.Bytes())
 
-	if !ctx.raw {
+	if !rctx.raw {
 		// prettify stdout (e.g. so that 'sha1\n' becomes 'sha1' and can be used directly
 		stdout = strings.TrimSpace(stdout)
 		stderr = strings.TrimSpace(stderr)
@@ -138,9 +141,9 @@ func (e *GitErrContext) Error() string {
 	return msg
 }
 
-// argv -> []string, ctx    (for passing argv + RunWith handy - see ggit() for details)
-func _gitargv(argv ...interface{}) (argvs []string, ctx RunWith) {
-	ctx_seen := false
+// ctx, argv -> ctx, []string, rctx  (for passing argv + RunWith handy - see ggit() for details)
+func _gitargv(ctx context.Context, argv ...interface{}) (_ context.Context, argvs []string, rctx RunWith) {
+	rctx_seen := false
 
 	for _, arg := range argv {
 		switch arg := arg.(type) {
@@ -149,47 +152,47 @@ func _gitargv(argv ...interface{}) (argvs []string, ctx RunWith) {
 		default:
 			argvs = append(argvs, fmt.Sprint(arg))
 		case RunWith:
-			if ctx_seen {
+			if rctx_seen {
 				panic("git: multiple RunWith contexts")
 			}
-			ctx, ctx_seen = arg, true
+			rctx, rctx_seen = arg, true
 		}
 	}
 
-	return argvs, ctx
+	return ctx, argvs, rctx
 }
 
 // run `git *argv` -> err, stdout, stderr
 // - arguments are automatically converted to strings
-// - RunWith argument is passed as ctx
+// - RunWith argument is passed as rctx
 // - error is returned only when git command could run and exits with error status
 // - on other errors - exception is raised
 //
 // NOTE err is concrete *GitError, not error
-func ggit(argv ...interface{}) (err *GitError, stdout, stderr string) {
-	return ggit2(_gitargv(argv...))
+func ggit(ctx context.Context, argv ...interface{}) (err *GitError, stdout, stderr string) {
+	return ggit2(_gitargv(ctx, argv...))
 }
 
-func ggit2(argv []string, ctx RunWith) (err *GitError, stdout, stderr string) {
-	e, stdout, stderr := _git(argv, ctx)
+func ggit2(ctx context.Context, argv []string, rctx RunWith) (err *GitError, stdout, stderr string) {
+	e, stdout, stderr := _git(ctx, argv, rctx)
 	eexec, _ := e.(*exec.ExitError)
 	if e != nil && eexec == nil {
 		exc.Raisef("git %s : %s", strings.Join(argv, " "), e)
 	}
 	if eexec != nil {
-		err = &GitError{GitErrContext{argv, ctx.stdin, stdout, stderr}, eexec}
+		err = &GitError{GitErrContext{argv, rctx.stdin, stdout, stderr}, eexec}
 	}
 	return err, stdout, stderr
 }
 
 // run `git *argv` -> stdout
 // on error - raise exception
-func xgit(argv ...interface{}) string {
-	return xgit2(_gitargv(argv...))
+func xgit(ctx context.Context, argv ...interface{}) string {
+	return xgit2(_gitargv(ctx, argv...))
 }
 
-func xgit2(argv []string, ctx RunWith) string {
-	gerr, stdout, _ := ggit2(argv, ctx)
+func xgit2(ctx context.Context, argv []string, rctx RunWith) string {
+	gerr, stdout, _ := ggit2(ctx, argv, rctx)
 	if gerr != nil {
 		exc.Raise(gerr)
 	}
@@ -197,8 +200,8 @@ func xgit2(argv []string, ctx RunWith) string {
 }
 
 // like xgit(), but automatically parse stdout to Sha1
-func xgitSha1(argv ...interface{}) Sha1 {
-	return xgit2Sha1(_gitargv(argv...))
+func xgitSha1(ctx context.Context, argv ...interface{}) Sha1 {
+	return xgit2Sha1(_gitargv(ctx, argv...))
 }
 
 // error when git output is not valid sha1
@@ -212,14 +215,14 @@ func (e *GitSha1Error) Error() string {
 	return msg
 }
 
-func xgit2Sha1(argv []string, ctx RunWith) Sha1 {
-	gerr, stdout, stderr := ggit2(argv, ctx)
+func xgit2Sha1(ctx context.Context, argv []string, rctx RunWith) Sha1 {
+	gerr, stdout, stderr := ggit2(ctx, argv, rctx)
 	if gerr != nil {
 		exc.Raise(gerr)
 	}
 	sha1, err := Sha1Parse(stdout)
 	if err != nil {
-		exc.Raise(&GitSha1Error{GitErrContext{argv, ctx.stdin, stdout, stderr}})
+		exc.Raise(&GitSha1Error{GitErrContext{argv, rctx.stdin, stdout, stderr}})
 	}
 	return sha1
 }
diff --git a/gitobjects.go b/gitobjects.go
index ac6d947d5939d1824811c124dbc1e9bf942a2961..2dc9189a623de49e04a131058a7e9930792d16ee 100644
--- a/gitobjects.go
+++ b/gitobjects.go
@@ -1,4 +1,4 @@
-// Copyright (C) 2015-2016  Nexedi SA and Contributors.
+// Copyright (C) 2015-2020  Nexedi SA and Contributors.
 //                          Kirill Smelkov <kirr@nexedi.com>
 //
 // This program is free software: you can Use, Study, Modify and Redistribute
@@ -21,6 +21,7 @@ package main
 // Git-backup | Git object: Blob Tree Commit Tag
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -163,9 +164,9 @@ func (e *InvalidLstreeEntry) Error() string {
 
 // create empty git tree -> tree sha1
 var tree_empty Sha1
-func mktree_empty() Sha1 {
+func mktree_empty(ctx context.Context) Sha1 {
 	if tree_empty.IsNull() {
-		tree_empty = xgitSha1("mktree", RunWith{stdin: ""})
+		tree_empty = xgitSha1(ctx, "mktree", RunWith{stdin: ""})
 	}
 	return tree_empty
 }