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 }