github.com/sqlitebrowser/dio@v0.0.0-20240125125356-b587368e5c6b/cmd/commit.go (about)

     1  package cmd
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/hex"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/cobra"
    15  	"github.com/spf13/viper"
    16  )
    17  
    18  var (
    19  	commitCmdAuthEmail, commitCmdAuthName, commitCmdBranch, commitCmdCommit string
    20  	commitCmdLicence, commitCmdMsg, commitCmdTimestamp                      string
    21  )
    22  
    23  // Create a commit for the database on the currently active branch
    24  var (
    25  	commitCmd = &cobra.Command{
    26  		Use:   "commit [database file]",
    27  		Short: "Creates a new commit for the database",
    28  		RunE: func(cmd *cobra.Command, args []string) error {
    29  			return commit(args)
    30  		},
    31  	}
    32  )
    33  
    34  func init() {
    35  	RootCmd.AddCommand(commitCmd)
    36  	commitCmd.Flags().StringVar(&commitCmdBranch, "branch", "",
    37  		"The branch this commit will be appended to")
    38  	commitCmd.Flags().StringVar(&commitCmdCommit, "commit", "",
    39  		"ID of the previous commit, for appending this new database to")
    40  	commitCmd.Flags().StringVar(&commitCmdAuthEmail, "email", "",
    41  		"Email address of the commit author")
    42  	commitCmd.Flags().StringVar(&commitCmdLicence, "licence", "",
    43  		"The licence (ID) for the database, as per 'dio licence list'")
    44  	commitCmd.Flags().StringVar(&commitCmdMsg, "message", "",
    45  		"Description / commit message")
    46  	commitCmd.Flags().StringVar(&commitCmdAuthName, "name", "", "Name of the commit author")
    47  	commitCmd.Flags().StringVar(&commitCmdTimestamp, "timestamp", "", "Timestamp for the commit")
    48  }
    49  
    50  func commit(args []string) error {
    51  	// Ensure a database file was given
    52  	var db string
    53  	var err error
    54  	var meta metaData
    55  	if len(args) == 0 {
    56  		db, err = getDefaultDatabase()
    57  		if err != nil {
    58  			return err
    59  		}
    60  		if db == "" {
    61  			// No database name was given on the command line, and we don't have a default database selected
    62  			return errors.New("No database file specified")
    63  		}
    64  	} else {
    65  		db = args[0]
    66  	}
    67  	// TODO: Allow giving multiple database files on the command line.  Hopefully just needs turning this
    68  	// TODO  into a for loop
    69  	if len(args) > 1 {
    70  		return errors.New("Only one database can be uploaded at a time (for now)")
    71  	}
    72  
    73  	// Ensure the database file exists
    74  	fi, err := os.Stat(db)
    75  	if err != nil {
    76  		return err
    77  	}
    78  
    79  	// Grab author name & email from the dio config file, but allow command line flags to override them
    80  	var authorName, authorEmail, committerName, committerEmail string
    81  	if z, ok := viper.Get("user.name").(string); ok {
    82  		authorName = z
    83  		committerName = z
    84  	}
    85  	if z, ok := viper.Get("user.email").(string); ok {
    86  		authorEmail = z
    87  		committerEmail = z
    88  	}
    89  	if commitCmdAuthName != "" {
    90  		authorName = commitCmdAuthName
    91  	}
    92  	if commitCmdAuthEmail != "" {
    93  		authorEmail = commitCmdAuthEmail
    94  	}
    95  
    96  	// Author name and email are required
    97  	if authorName == "" || authorEmail == "" || committerName == "" || committerEmail == "" {
    98  		return errors.New("Author and committer name and email addresses are required!")
    99  	}
   100  
   101  	// If a timestamp was provided, make sure it parses ok
   102  	commitTime := time.Now()
   103  	if commitCmdTimestamp != "" {
   104  		commitTime, err = time.Parse(time.RFC3339, commitCmdTimestamp)
   105  		if err != nil {
   106  			return err
   107  		}
   108  	}
   109  
   110  	// If the database metadata doesn't exist locally, check if it does exist on the server.
   111  	var newDB, localPresent bool
   112  	if _, err = os.Stat(filepath.Join(".dio", db, "db")); os.IsNotExist(err) {
   113  		// At the moment, since there's no better way to check for the existence of a remote database, we just
   114  		// grab the list of the users databases and check against that
   115  		dbList, errInner := getDatabases(cloud, certUser)
   116  		if errInner != nil {
   117  			return errInner
   118  		}
   119  		for _, j := range dbList {
   120  			if db == j.Name {
   121  				// This database already exists on DBHub.io.  We need local metadata in order to proceed, but don't
   122  				// yet have it.  Safest option, at least for now, is to tell the user and abort
   123  				return errors.New("Aborting: the database exists on the remote server, but has no " +
   124  					"local metadata cache.  Please retrieve the remote metadata, then run the commit command again")
   125  			}
   126  		}
   127  
   128  		// This is a new database, so we generate new metadata
   129  		newDB = true
   130  		meta = newMetaStruct(commitCmdBranch)
   131  	} else {
   132  		// We have local metaData
   133  		localPresent = true
   134  	}
   135  
   136  	// Load the metadata
   137  	if !newDB {
   138  		meta, err = loadMetadata(db)
   139  		if err != nil {
   140  			return err
   141  		}
   142  	}
   143  
   144  	// If no branch name was passed, use the active branch
   145  	if commitCmdBranch == "" {
   146  		commitCmdBranch = meta.ActiveBranch
   147  	}
   148  
   149  	// Check if the database is unchanged from the previous commit, and if so we abort the commit
   150  	if localPresent {
   151  		changed, err := dbChanged(db, meta)
   152  		if err != nil {
   153  			return err
   154  		}
   155  		if !changed && commitCmdLicence == "" {
   156  			return fmt.Errorf("Database is unchanged from last commit.  No need to commit anything.")
   157  		}
   158  	}
   159  
   160  	// Get the current head commit for the selected branch, as that will be the parent commit for this new one
   161  	head, ok := meta.Branches[commitCmdBranch]
   162  	if !ok {
   163  		return errors.New(fmt.Sprintf("That branch ('%s') doesn't exist", commitCmdBranch))
   164  	}
   165  	var existingLicSHA string
   166  	if newDB {
   167  		if commitCmdLicence == "" {
   168  			// If this is a new database, and no licence was given on the command line, then default to
   169  			// 'Not specified'
   170  			commitCmdLicence = "Not specified"
   171  		}
   172  	} else {
   173  		if localPresent {
   174  			// We can only use commit data if local metadata is present
   175  			headCommit, ok := meta.Commits[head.Commit]
   176  			if !ok {
   177  				return errors.New("Aborting: info for the head commit isn't found in the local commit cache")
   178  			}
   179  			existingLicSHA = headCommit.Tree.Entries[0].LicenceSHA
   180  		}
   181  	}
   182  
   183  	// Retrieve the list of known licences
   184  	licList, err := getLicences()
   185  	if err != nil {
   186  		return err
   187  	}
   188  
   189  	// Determine the SHA256 of the requested licence
   190  	var licID, licSHA string
   191  	if commitCmdLicence != "" {
   192  		// Scan the licence list for a matching licence name
   193  		matchFound := false
   194  		lwrLic := strings.ToLower(commitCmdLicence)
   195  		for i, j := range licList {
   196  			if strings.ToLower(i) == lwrLic {
   197  				licID = i
   198  				licSHA = j.Sha256
   199  				matchFound = true
   200  				break
   201  			}
   202  		}
   203  		if !matchFound {
   204  			return errors.New("Aborting: could not determine the name of the existing database licence")
   205  		}
   206  	} else {
   207  		// If no licence was given, use the licence from the previous commit
   208  		licSHA = existingLicSHA
   209  	}
   210  
   211  	// Generate an appropriate commit message if none was provided
   212  	if commitCmdMsg == "" {
   213  		if !newDB && existingLicSHA != licSHA {
   214  			// * The licence has changed, so we create a reasonable commit message indicating this *
   215  
   216  			// Work out the human friendly short licence name for the current database
   217  			matchFound := false
   218  			var existingLicID string
   219  			for i, j := range licList {
   220  				if existingLicSHA == j.Sha256 {
   221  					existingLicID = i
   222  					matchFound = true
   223  					break
   224  				}
   225  			}
   226  			if !matchFound {
   227  				return errors.New("Aborting: could not locate the requested database licence")
   228  			}
   229  			commitCmdMsg = fmt.Sprintf("Database licence changed from '%s' to '%s'.", existingLicID, licID)
   230  		}
   231  
   232  		// If it's a new database and there's still no commit message, generate a reasonable one
   233  		if newDB && commitCmdMsg == "" {
   234  			commitCmdMsg = "New database created"
   235  		}
   236  	}
   237  
   238  	// * Collect info for the new commit *
   239  
   240  	// Get file size and last modified time for the database
   241  	fileSize := fi.Size()
   242  	lastModified := fi.ModTime()
   243  
   244  	// Verify we've read the file from disk ok
   245  	b, err := ioutil.ReadFile(db)
   246  	if err != nil {
   247  		return err
   248  	}
   249  	if int64(len(b)) != fileSize {
   250  		return errors.New(numFormat.Sprintf("Aborting: # of bytes read (%d) when generating commit don't "+
   251  			"match database file size (%d)", len(b), fileSize))
   252  	}
   253  
   254  	// Generate sha256
   255  	s := sha256.Sum256(b)
   256  	shaSum := hex.EncodeToString(s[:])
   257  
   258  	// * Generate the new commit *
   259  
   260  	// Create a new dbTree entry for the database file
   261  	var e dbTreeEntry
   262  	e.EntryType = DATABASE
   263  	e.LastModified = lastModified.UTC()
   264  	e.LicenceSHA = licSHA
   265  	e.Name = db
   266  	e.Sha256 = shaSum
   267  	e.Size = fileSize
   268  
   269  	// Create a new dbTree structure for the new database entry
   270  	var t dbTree
   271  	t.Entries = append(t.Entries, e)
   272  	t.ID = createDBTreeID(t.Entries)
   273  
   274  	// Create a new commit for the new tree
   275  	newCom := commitEntry{
   276  		AuthorName:     authorName,
   277  		AuthorEmail:    authorEmail,
   278  		CommitterName:  committerName,
   279  		CommitterEmail: committerEmail,
   280  		Message:        commitCmdMsg,
   281  		Parent:         head.Commit,
   282  		Timestamp:      commitTime.UTC(),
   283  		Tree:           t,
   284  	}
   285  
   286  	// Calculate the new commit ID, which incorporates the updated tree ID (and thus the new licence sha256)
   287  	newCom.ID = createCommitID(newCom)
   288  
   289  	// Add the new commit info to the database commit list
   290  	meta.Commits[newCom.ID] = newCom
   291  
   292  	// Update the branch head info to point at the new commit
   293  	meta.Branches[commitCmdBranch] = branchEntry{
   294  		Commit:      newCom.ID,
   295  		CommitCount: head.CommitCount + 1,
   296  		Description: head.Description,
   297  	}
   298  
   299  	// If the database file isn't already in the local cache, then copy it there
   300  	if _, err = os.Stat(filepath.Join(".dio", db, "db", shaSum)); os.IsNotExist(err) {
   301  		if _, err = os.Stat(filepath.Join(".dio", db)); os.IsNotExist(err) {
   302  			err = os.MkdirAll(filepath.Join(".dio", db, "db"), 0770)
   303  			if err != nil {
   304  				return err
   305  			}
   306  		}
   307  		err = ioutil.WriteFile(filepath.Join(".dio", db, "db", shaSum), b, 0644)
   308  		if err != nil {
   309  			return err
   310  		}
   311  	}
   312  
   313  	// Save the updated metadata back to disk
   314  	err = saveMetadata(db, meta)
   315  	if err != nil {
   316  		return err
   317  	}
   318  
   319  	// Display results to the user
   320  	_, err = fmt.Fprintf(fOut, "Commit created on '%s'\n", db)
   321  	if err != nil {
   322  		return err
   323  	}
   324  	_, err = fmt.Fprintf(fOut, "  * Commit ID: %s\n", newCom.ID)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	_, err = fmt.Fprintf(fOut, "    Branch: %s\n", commitCmdBranch)
   329  	if err != nil {
   330  		return err
   331  	}
   332  	if licID != "" {
   333  		_, err = fmt.Fprintf(fOut, "    Licence: %s\n", licID)
   334  		if err != nil {
   335  			return err
   336  		}
   337  	}
   338  	_, err = numFormat.Fprintf(fOut, "    Size: %d bytes\n", e.Size)
   339  	if err != nil {
   340  		return err
   341  	}
   342  	if commitCmdMsg != "" {
   343  		_, err = fmt.Fprintf(fOut, "    Commit message: %s\n\n", commitCmdMsg)
   344  		if err != nil {
   345  			return err
   346  		}
   347  	}
   348  	return nil
   349  }
   350  
   351  // Creates a new metadata structure in memory
   352  func newMetaStruct(branch string) (meta metaData) {
   353  	b := branchEntry{
   354  		Commit:      "",
   355  		CommitCount: 0,
   356  		Description: "",
   357  	}
   358  	var initialBranch string
   359  	if branch == "" {
   360  		initialBranch = "main"
   361  	} else {
   362  		initialBranch = branch
   363  	}
   364  	meta = metaData{
   365  		ActiveBranch: initialBranch,
   366  		Branches:     map[string]branchEntry{initialBranch: b},
   367  		Commits:      map[string]commitEntry{},
   368  		DefBranch:    initialBranch,
   369  		Releases:     map[string]releaseEntry{},
   370  		Tags:         map[string]tagEntry{},
   371  	}
   372  	return
   373  }