github.com/knoebber/dotfile@v1.0.6/local/local.go (about)

     1  // Package local tracks files by writing to JSON files in the dotfile directory.
     2  //
     3  // For every new file that is tracked a new .json file is created.
     4  // For each commit on a tracked file, a new file is created with the same name as the hash.
     5  //
     6  // Example: ~/.emacs.d/init.el is added with alias "emacs".
     7  // Supposing Storage.dir is ~/.config/dotfile, then the following files are created:
     8  //
     9  // ~/.config/dotfile/emacs.json
    10  // ~/.config/dotfile/emacs/8f94c7720a648af9cf9dab33e7f297d28b8bf7cd
    11  //
    12  // The emacs.json file would look something like this:
    13  //
    14  //	{
    15  //	  "path": "~/.emacs.d/init.el",
    16  //	  "revision": "8f94c7720a648af9cf9dab33e7f297d28b8bf7cd"
    17  //	  "commits": [{
    18  //	    "hash": "8f94c7720a648af9cf9dab33e7f297d28b8bf7cd",
    19  //	    "timestamp": 1558896290,
    20  //	    "message": "Initial commit"
    21  //	  }]
    22  //	}
    23  package local
    24  
    25  import (
    26  	"fmt"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/knoebber/dotfile/dotfile"
    32  	"github.com/pkg/errors"
    33  )
    34  
    35  const jsonIndent = "  "
    36  
    37  // Creates a path that is reusable between machines.
    38  // Returns an error when path does not exist.
    39  func convertPath(path string) (string, error) {
    40  	var err error
    41  
    42  	home, err := os.UserHomeDir()
    43  	if err != nil {
    44  		return "", err
    45  	}
    46  
    47  	if !exists(path) {
    48  		return "", fmt.Errorf("%q not found", path)
    49  	}
    50  
    51  	//  the full path.
    52  	if !filepath.IsAbs(path) {
    53  		path, err = filepath.Abs(path)
    54  		if err != nil {
    55  			return "", err
    56  		}
    57  	}
    58  
    59  	// If the path is not in $HOME then use as is.
    60  	if !strings.Contains(path, home) {
    61  		return path, nil
    62  	}
    63  
    64  	return strings.Replace(path, home, "~", 1), nil
    65  }
    66  
    67  // Returns whether the file or directory exists.
    68  func exists(path string) bool {
    69  	_, err := os.Stat(path)
    70  	if os.IsNotExist(err) {
    71  		return false
    72  	}
    73  
    74  	return true
    75  }
    76  
    77  // Creates a directory if it does not exist.
    78  func createDir(dir string) error {
    79  	if exists(dir) {
    80  		return nil
    81  	}
    82  
    83  	return os.Mkdir(dir, 0755)
    84  }
    85  
    86  func writeCommit(contents []byte, storageDir string, alias, hash string) error {
    87  	// The directory for the files commits.
    88  	commitDir := filepath.Join(storageDir, alias)
    89  
    90  	// Example: ~/.local/share/dotfile/bash_profile
    91  	if err := createDir(commitDir); err != nil {
    92  		return errors.Wrap(err, "creating directory for revision")
    93  	}
    94  
    95  	// Example: ~/.local/share/dotfile/bash_profile/8f94c7720a648af9cf9dab33e7f297d28b8bf7cd
    96  	commitPath := filepath.Join(commitDir, hash)
    97  
    98  	if err := os.WriteFile(commitPath, contents, 0644); err != nil {
    99  		return errors.Wrap(err, "writing revision")
   100  	}
   101  
   102  	return nil
   103  }
   104  
   105  // DefaultStorageDir returns the default location for storing dotfile information.
   106  // Creates the location when it does not exist.
   107  func DefaultStorageDir() (storageDir string, err error) {
   108  	home, err := os.UserHomeDir()
   109  	if err != nil {
   110  		return "", err
   111  	}
   112  
   113  	localSharePath := filepath.Join(home, ".local/share/")
   114  	if exists(localSharePath) {
   115  		// Priority one : ~/.local/share/dotfile
   116  		storageDir = filepath.Join(localSharePath, "dotfile/")
   117  	} else {
   118  		// Priority two: ~/.dotfile/
   119  		storageDir = filepath.Join(home, ".dotfile/")
   120  	}
   121  
   122  	if err = createDir(storageDir); err != nil {
   123  		return
   124  	}
   125  
   126  	return
   127  }
   128  
   129  // InitializeFile sets up a new file to be tracked.
   130  // When alias is empty its generated from path.
   131  // Returns a Storage that is loaded with the new file.
   132  func InitializeFile(storageDir, path, alias string) (*Storage, error) {
   133  	var err error
   134  
   135  	alias, err = dotfile.Alias(alias, path)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	s := &Storage{Dir: storageDir, Alias: alias}
   141  	if s.hasSavedData() {
   142  		return nil, fmt.Errorf("%q is already tracked", alias)
   143  	}
   144  
   145  	s.FileData = new(dotfile.TrackingData)
   146  	s.FileData.Path, err = convertPath(path)
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	if err := dotfile.Init(s, s.FileData.Path, s.Alias); err != nil {
   152  		return nil, err
   153  	}
   154  
   155  	return s, nil
   156  }
   157  
   158  func listAliases(storageDir string) ([]string, error) {
   159  	files, err := filepath.Glob(filepath.Join(storageDir, "*.json"))
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	aliases := make([]string, len(files))
   165  	for i, filename := range files {
   166  		aliases[i] = strings.TrimSuffix(filepath.Base(filename), ".json")
   167  	}
   168  
   169  	return aliases, nil
   170  }
   171  
   172  // ListAliases returns a funtion that lists all aliases in the storage directory.
   173  func ListAliases(storageDir string) func() []string {
   174  	return func() []string {
   175  		aliases, err := listAliases(storageDir)
   176  		if err != nil {
   177  			return nil
   178  		}
   179  
   180  		return aliases
   181  	}
   182  }
   183  
   184  // List returns a slice of aliases for all locally tracked files.
   185  // When the file has uncommitted changes an asterisks is added to the end.
   186  func List(storageDir string, path bool) ([]string, error) {
   187  	aliases, err := listAliases(storageDir)
   188  	if err != nil {
   189  		return nil, err
   190  	}
   191  
   192  	result := make([]string, len(aliases))
   193  
   194  	s := &Storage{Dir: storageDir}
   195  	s.FileData = new(dotfile.TrackingData)
   196  
   197  	for i, alias := range aliases {
   198  		s.Alias = alias
   199  
   200  		if err := s.SetTrackingData(); err != nil {
   201  			return nil, err
   202  		}
   203  
   204  		fullPath, err := s.Path()
   205  		if err != nil {
   206  			return nil, err
   207  		}
   208  
   209  		if !exists(fullPath) {
   210  			alias += " - removed"
   211  		} else {
   212  			clean, err := dotfile.IsClean(s, s.FileData.Revision)
   213  			if err != nil {
   214  				return nil, err
   215  			}
   216  
   217  			if !clean {
   218  				alias += "*"
   219  			}
   220  		}
   221  
   222  		result[i] = alias
   223  		if path {
   224  			result[i] += " " + s.FileData.Path
   225  		}
   226  	}
   227  
   228  	return result, nil
   229  }
   230  
   231  func createDirectories(path string) error {
   232  	if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
   233  		return errors.Wrapf(err, "creating %q", filepath.Dir(path))
   234  	}
   235  
   236  	return nil
   237  }