Skip to content

Commit 071a62e

Browse files
authored
Merge pull request #62 from twpayne/import
Add import command
2 parents aa0ac03 + e26f3ec commit 071a62e

File tree

4 files changed

+276
-0
lines changed

4 files changed

+276
-0
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,24 @@ The `source` command accepts the usual `-n` and `-v` flags, so you can see
311311
exactly what it will run without executing it.
312312

313313

314+
## Importing archives
315+
316+
It is occasionally useful to import entire archives of configuration into your
317+
home directory. The `import` command does this. For example, to import the
318+
latest version
319+
[`github.com/robbyrussell/oh-my-zsh`](https://github.com/robbyrussell/oh-my-zsh)
320+
to your `~/.oh-my-zsh` directory, run:
321+
322+
$ curl -s -L -o oh-my-zsh-master.tar.gz https://github.com/robbyrussell/oh-my-zsh/archive/master.tar.gz
323+
$ chezmoi import --strip-components 1 --destination ~/.oh-my-zsh oh-my-zsh-master.tar.gz
324+
325+
Note that this only updates the source state. You will need to run
326+
327+
$ chezmoi apply
328+
329+
to update your home directory.
330+
331+
314332
## Under the hood
315333

316334
`chezmoi` stores the desired state of files, symbolic links, and directories in

cmd/cmd.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type Config struct {
4040
add addCommandConfig
4141
dump dumpCommandConfig
4242
edit editCommandConfig
43+
importC importCommandConfig
4344
keyring keyringCommandConfig
4445
}
4546

cmd/import.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cmd
2+
3+
// FIXME add zip support
4+
// FIXME add --apply flag
5+
// FIXME add --diff flag
6+
// FIXME add --prompt flag
7+
8+
import (
9+
"archive/tar"
10+
"compress/bzip2"
11+
"compress/gzip"
12+
"fmt"
13+
"io"
14+
"os"
15+
"path/filepath"
16+
"strings"
17+
18+
"github.com/spf13/cobra"
19+
vfs "github.com/twpayne/go-vfs"
20+
)
21+
22+
var importCommand = &cobra.Command{
23+
Use: "import",
24+
Args: cobra.MaximumNArgs(1),
25+
Short: "Import an archive",
26+
RunE: makeRunE(config.runImportCommand),
27+
}
28+
29+
type importCommandConfig struct {
30+
destination string
31+
removeDestination bool
32+
stripComponents int
33+
}
34+
35+
func init() {
36+
rootCommand.AddCommand(importCommand)
37+
38+
persistentFlags := importCommand.PersistentFlags()
39+
persistentFlags.StringVarP(&config.importC.destination, "destination", "d", "", "destination prefix")
40+
persistentFlags.IntVar(&config.importC.stripComponents, "strip-components", 0, "strip components")
41+
persistentFlags.BoolVarP(&config.importC.removeDestination, "remove-destination", "r", false, "remove destination before import")
42+
}
43+
44+
func (c *Config) runImportCommand(fs vfs.FS, cmd *cobra.Command, args []string) error {
45+
targetState, err := c.getTargetState(fs)
46+
if err != nil {
47+
return err
48+
}
49+
var r io.Reader
50+
if len(args) == 0 {
51+
r = os.Stdin
52+
} else {
53+
arg := args[0]
54+
f, err := fs.Open(arg)
55+
if err != nil {
56+
return err
57+
}
58+
defer f.Close()
59+
switch {
60+
case strings.HasSuffix(arg, ".tar.gz") || strings.HasSuffix(arg, ".tgz"):
61+
r, err = gzip.NewReader(f)
62+
if err != nil {
63+
return err
64+
}
65+
case strings.HasSuffix(arg, ".tar.bz2"):
66+
r = bzip2.NewReader(f)
67+
case strings.HasSuffix(arg, ".tar"):
68+
r = f
69+
default:
70+
return fmt.Errorf("%s: unknown format", arg)
71+
}
72+
}
73+
actuator := c.getDefaultActuator(fs)
74+
if c.importC.removeDestination {
75+
entry, err := targetState.Get(c.importC.destination)
76+
switch {
77+
case err == nil:
78+
if err := actuator.RemoveAll(filepath.Join(c.SourceDir, entry.SourceName())); err != nil {
79+
return err
80+
}
81+
case os.IsNotExist(err):
82+
default:
83+
return err
84+
}
85+
}
86+
return targetState.AddArchive(tar.NewReader(r), c.importC.destination, c.importC.stripComponents, actuator)
87+
}

lib/chezmoi/chezmoi.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"archive/tar"
55
"bytes"
66
"fmt"
7+
"io"
8+
"io/ioutil"
79
"os"
810
"os/user"
911
"path/filepath"
@@ -657,6 +659,27 @@ func (ts *TargetState) Get(target string) (Entry, error) {
657659
return ts.findEntry(targetName)
658660
}
659661

662+
func (ts *TargetState) AddArchive(r *tar.Reader, destinationDir string, stripComponents int, actuator Actuator) error {
663+
for {
664+
header, err := r.Next()
665+
if err == io.EOF {
666+
break
667+
} else if err != nil {
668+
return err
669+
}
670+
switch header.Typeflag {
671+
case tar.TypeDir, tar.TypeReg, tar.TypeSymlink:
672+
if err := ts.addArchiveHeader(r, header, destinationDir, stripComponents, actuator); err != nil {
673+
return err
674+
}
675+
case tar.TypeXGlobalHeader:
676+
default:
677+
return fmt.Errorf("%s: unspported typeflag '%c'", header.Name, header.Typeflag)
678+
}
679+
}
680+
return nil
681+
}
682+
660683
// Populate walks fs from ts.SourceDir to populate ts.
661684
func (ts *TargetState) Populate(fs vfs.FS) error {
662685
return vfs.Walk(fs, ts.SourceDir, func(path string, info os.FileInfo, _ error) error {
@@ -741,6 +764,153 @@ func (ts *TargetState) Populate(fs vfs.FS) error {
741764
})
742765
}
743766

767+
func (ts *TargetState) addArchiveHeader(r *tar.Reader, header *tar.Header, destinationDir string, stripComponents int, actuator Actuator) error {
768+
targetPath := header.Name
769+
if stripComponents > 0 {
770+
targetPath = filepath.Join(strings.Split(targetPath, string(os.PathSeparator))[stripComponents:]...)
771+
}
772+
if destinationDir != "" {
773+
targetPath = filepath.Join(destinationDir, targetPath)
774+
} else {
775+
targetPath = filepath.Join(ts.TargetDir, targetPath)
776+
}
777+
targetName, err := filepath.Rel(ts.TargetDir, targetPath)
778+
if err != nil {
779+
return err
780+
}
781+
parentDirSourceName := ""
782+
entries := ts.Entries
783+
if parentDirName := filepath.Dir(targetName); parentDirName != "." {
784+
parentEntry, err := ts.findEntry(parentDirName)
785+
if err != nil {
786+
return err
787+
}
788+
parentDir, ok := parentEntry.(*Dir)
789+
if !ok {
790+
return fmt.Errorf("%s: parent is not a directory", targetName)
791+
}
792+
parentDirSourceName = parentDir.sourceName
793+
entries = parentDir.Entries
794+
}
795+
name := filepath.Base(targetName)
796+
switch header.Typeflag {
797+
case tar.TypeReg:
798+
var existingFile *File
799+
var existingContents []byte
800+
if entry, ok := entries[name]; ok {
801+
existingFile, ok = entry.(*File)
802+
if !ok {
803+
return fmt.Errorf("%s: already added and not a regular file", targetName)
804+
}
805+
existingContents, err = existingFile.Contents()
806+
if err != nil {
807+
return err
808+
}
809+
}
810+
perm := os.FileMode(header.Mode) & os.ModePerm
811+
empty := header.Size == 0
812+
sourceName := ParsedSourceFileName{
813+
FileName: name,
814+
Mode: perm,
815+
Empty: empty,
816+
}.SourceFileName()
817+
if parentDirSourceName != "" {
818+
sourceName = filepath.Join(parentDirSourceName, sourceName)
819+
}
820+
contents, err := ioutil.ReadAll(r)
821+
if err != nil {
822+
return err
823+
}
824+
file := &File{
825+
sourceName: sourceName,
826+
targetName: targetName,
827+
Empty: empty,
828+
Perm: perm,
829+
Template: false,
830+
contents: contents,
831+
}
832+
if existingFile != nil {
833+
if bytes.Equal(existingFile.contents, file.contents) {
834+
if existingFile.sourceName == file.sourceName {
835+
return nil
836+
}
837+
return actuator.Rename(filepath.Join(ts.SourceDir, existingFile.sourceName), filepath.Join(ts.SourceDir, file.sourceName))
838+
}
839+
if err := actuator.RemoveAll(filepath.Join(ts.SourceDir, existingFile.sourceName)); err != nil {
840+
return err
841+
}
842+
}
843+
entries[name] = file
844+
return actuator.WriteFile(filepath.Join(ts.SourceDir, sourceName), contents, 0666&^ts.Umask, existingContents)
845+
case tar.TypeDir:
846+
var existingDir *Dir
847+
if entry, ok := entries[name]; ok {
848+
existingDir, ok = entry.(*Dir)
849+
if !ok {
850+
return fmt.Errorf("%s: already added and not a directory", targetName)
851+
}
852+
}
853+
perm := os.FileMode(header.Mode) & os.ModePerm
854+
sourceName := ParsedSourceDirName{
855+
DirName: name,
856+
Perm: perm,
857+
}.SourceDirName()
858+
if parentDirSourceName != "" {
859+
sourceName = filepath.Join(parentDirSourceName, sourceName)
860+
}
861+
dir := newDir(sourceName, targetName, perm)
862+
if existingDir != nil {
863+
if existingDir.sourceName == dir.sourceName {
864+
return nil
865+
}
866+
return actuator.Rename(filepath.Join(ts.SourceDir, existingDir.sourceName), filepath.Join(ts.SourceDir, dir.sourceName))
867+
}
868+
// FIXME Add a .keep file if the directory is empty
869+
entries[name] = dir
870+
return actuator.Mkdir(filepath.Join(ts.SourceDir, sourceName), 0777&^ts.Umask)
871+
case tar.TypeSymlink:
872+
var existingSymlink *Symlink
873+
var existingLinkName string
874+
if entry, ok := entries[name]; ok {
875+
existingSymlink, ok = entry.(*Symlink)
876+
if !ok {
877+
return fmt.Errorf("%s: already added and not a symlink", targetName)
878+
}
879+
existingLinkName, err = existingSymlink.LinkName()
880+
if err != nil {
881+
return err
882+
}
883+
}
884+
sourceName := ParsedSourceFileName{
885+
FileName: name,
886+
Mode: os.ModeSymlink,
887+
}.SourceFileName()
888+
if parentDirSourceName != "" {
889+
sourceName = filepath.Join(parentDirSourceName, sourceName)
890+
}
891+
symlink := &Symlink{
892+
sourceName: sourceName,
893+
targetName: targetName,
894+
linkName: header.Linkname,
895+
}
896+
if existingSymlink != nil {
897+
if existingSymlink.linkName == symlink.linkName {
898+
if existingSymlink.sourceName == symlink.sourceName {
899+
return nil
900+
}
901+
return actuator.Rename(filepath.Join(ts.SourceDir, existingSymlink.sourceName), filepath.Join(ts.SourceDir, symlink.sourceName))
902+
}
903+
if err := actuator.RemoveAll(filepath.Join(ts.SourceDir, existingSymlink.sourceName)); err != nil {
904+
return err
905+
}
906+
}
907+
entries[name] = symlink
908+
return actuator.WriteFile(filepath.Join(ts.SourceDir, symlink.sourceName), []byte(symlink.linkName), 0666&^ts.Umask, []byte(existingLinkName))
909+
default:
910+
return fmt.Errorf("%s: unspported typeflag '%c'", header.Name, header.Typeflag)
911+
}
912+
}
913+
744914
func (ts *TargetState) executeTemplate(fs vfs.FS, path string) ([]byte, error) {
745915
data, err := fs.ReadFile(path)
746916
if err != nil {

0 commit comments

Comments
 (0)