github.com/sqlitebrowser/dio@v0.0.0-20240125125356-b587368e5c6b/cmd/branchRevert.go (about) 1 package cmd 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "time" 10 11 "github.com/spf13/cobra" 12 ) 13 14 var ( 15 branchRevertBranch, branchRevertCommit, branchRevertTag string 16 branchRevertForce *bool 17 ) 18 19 // Reverts a database to a prior commit in its history 20 var branchRevertCmd = &cobra.Command{ 21 Use: "revert [database name] --branch xxx --commit yyy", 22 Short: "Resets a database branch back to a previous commit", 23 RunE: func(cmd *cobra.Command, args []string) error { 24 return branchRevert(args) 25 }, 26 } 27 28 func init() { 29 branchCmd.AddCommand(branchRevertCmd) 30 branchRevertCmd.Flags().StringVar(&branchRevertBranch, "branch", "", 31 "Branch to operate on") 32 branchRevertCmd.Flags().StringVar(&branchRevertCommit, "commit", "", 33 "Commit ID for the to revert to") 34 branchRevertForce = branchRevertCmd.Flags().BoolP("force", "f", false, 35 "Overwrite unsaved changes to the database?") 36 branchRevertCmd.Flags().StringVar(&branchRevertTag, "tag", "", "Name of tag to revert to") 37 } 38 39 func branchRevert(args []string) error { 40 // Ensure a database file was given 41 var db string 42 var err error 43 var meta metaData 44 if len(args) == 0 { 45 db, err = getDefaultDatabase() 46 if err != nil { 47 return err 48 } 49 if db == "" { 50 // No database name was given on the command line, and we don't have a default database selected 51 return errors.New("No database file specified") 52 } 53 } else { 54 db = args[0] 55 } 56 if len(args) > 1 { 57 return errors.New("Only one database can be changed at a time (for now)") 58 } 59 60 // Ensure the required info was given 61 if branchRevertCommit == "" && branchRevertTag == "" { 62 return errors.New("Either a commit ID or tag must be given.") 63 } 64 65 // Ensure we were given only a commit ID OR a tag 66 if branchRevertCommit != "" && branchRevertTag != "" { 67 return errors.New("Either a commit ID or tag must be given. Not both!") 68 } 69 70 // Load the metadata 71 meta, err = loadMetadata(db) 72 if err != nil { 73 return err 74 } 75 76 // Unless --force is specified, check whether the file has changed since the last commit, and let the user know 77 if *branchRevertForce == false { 78 changed, err := dbChanged(db, meta) 79 if err != nil { 80 return err 81 } 82 if changed { 83 _, err = fmt.Fprintf(fOut, "%s has been changed since the last commit. Use --force if you "+ 84 "really want to overwrite it\n", db) 85 return err 86 } 87 } 88 89 // If a tag was given, make sure it exists 90 if branchRevertTag != "" { 91 tagData, ok := meta.Tags[branchRevertTag] 92 if !ok { 93 return errors.New("That tag doesn't exist") 94 } 95 96 // Use the commit associated with the tag 97 branchRevertCommit = tagData.Commit 98 } 99 100 // If no branch name was passed, use the active branch 101 if branchRevertBranch == "" { 102 branchRevertBranch = meta.ActiveBranch 103 } 104 105 // Make sure the branch exists 106 matchFound := false 107 head, ok := meta.Branches[branchRevertBranch] 108 if ok == false { 109 return errors.New("That branch doesn't exist") 110 } 111 if head.Commit == branchRevertCommit { 112 matchFound = true 113 } 114 delList := map[string]struct{}{} 115 if !matchFound { 116 delList[head.Commit] = struct{}{} // Start creating a list of the branch commits to be deleted 117 } 118 119 // Build a list of commits in the branch 120 commitList := []string{head.Commit} 121 c, ok := meta.Commits[head.Commit] 122 if ok == false { 123 return errors.New("Something has gone wrong. Head commit for the branch isn't in the commit list") 124 } 125 for c.Parent != "" { 126 c = meta.Commits[c.Parent] 127 if c.ID == branchRevertCommit { 128 matchFound = true 129 } 130 if !matchFound { 131 delList[c.ID] = struct{}{} // Only commits prior to matchFound should be deleted 132 } 133 commitList = append(commitList, c.ID) 134 } 135 136 // Make sure the requested commit exists on the selected branch 137 if !matchFound { 138 return errors.New("The given commit or tag doesn't seem to exist on the selected branch") 139 } 140 141 // Make sure the correct database from the target branch is in local cache 142 var shaSum string 143 var lastMod time.Time 144 if branchRevertCommit != "" { 145 shaSum = meta.Commits[branchRevertCommit].Tree.Entries[0].Sha256 146 lastMod = meta.Commits[branchRevertCommit].Tree.Entries[0].LastModified 147 148 // Fetch the database from DBHub.io if it's not in the local cache 149 err = checkDBCache(db, shaSum) 150 if err != nil { 151 return err 152 } 153 } else { 154 return errors.New("Haven't been able to determine branch name. This shouldn't happen") 155 } 156 157 // Check if deleting the commits would leave isolated tags or releases. If so, abort and warn the user 158 type isolCheck struct { 159 safe bool 160 commit string 161 } 162 var isolatedTags []string 163 var isolatedReleases []string 164 commitTags := map[string]isolCheck{} 165 commitReleases := map[string]isolCheck{} 166 for delCommit := range delList { 167 // Ensure that deleting this commit won't result in any isolated/unreachable tags 168 for tName, tEntry := range meta.Tags { 169 // Scan through the database tag list, checking if any of the tags is for the commit we're deleting 170 if tEntry.Commit == delCommit { 171 commitTags[tName] = isolCheck{safe: false, commit: delCommit} 172 } 173 } 174 175 // Ensure that deleting this commit won't result in any isolated/unreachable releases 176 for rName, rEntry := range meta.Releases { 177 // Scan through the database release list, checking if any of the releases is for the commit we're 178 // deleting 179 if rEntry.Commit == delCommit { 180 commitReleases[rName] = isolCheck{safe: false, commit: delCommit} 181 } 182 } 183 } 184 185 if len(commitTags) > 0 { 186 // If a commit we're deleting has a tag on it, we need to check whether the commit is on other branches too 187 // * If it is, we're ok to proceed as the tag can still be reached from the other branch(es) 188 // * If it isn't, we need to abort this deletion (and tell the user), as the tag would become unreachable 189 for bName, bEntry := range meta.Branches { 190 if bName == branchRevertBranch { 191 // We only run this comparison from "other branches", not the branch whose history we're changing 192 continue 193 } 194 c, ok = meta.Commits[bEntry.Commit] 195 if !ok { 196 return fmt.Errorf("Broken commit history encountered when checking for isolated tags "+ 197 "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db) 198 } 199 for tName, tEntry := range commitTags { 200 if c.ID == tEntry.commit { 201 // The commit is also on another branch, so we're ok to delete the commit 202 tmp := commitTags[tName] 203 tmp.safe = true 204 commitTags[tName] = tmp 205 } 206 } 207 for c.Parent != "" { 208 c, ok = meta.Commits[c.Parent] 209 if !ok { 210 return fmt.Errorf("Broken commit history encountered when checking for isolated tags "+ 211 "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db) 212 } 213 for tName, tEntry := range commitTags { 214 if c.ID == tEntry.commit { 215 // The commit is also on another branch, so we're ok to delete the commit 216 tmp := commitTags[tName] 217 tmp.safe = true 218 commitTags[tName] = tmp 219 } 220 } 221 } 222 } 223 224 // Create a list of would-be-isolated tags 225 for tName, tEntry := range commitTags { 226 if tEntry.safe == false { 227 isolatedTags = append(isolatedTags, tName) 228 } 229 } 230 } 231 232 if len(commitReleases) > 0 { 233 // If a commit we're deleting has a release on it, we need to check whether the commit is on other branches too 234 // * If it is, we're ok to proceed as the release can still be reached from the other branch(es) 235 // * If it isn't, we need to abort this deletion (and tell the user), as the release would become unreachable 236 for bName, bEntry := range meta.Branches { 237 if bName == branchRevertBranch { 238 // We only run this comparison from "other branches", not the branch whose history we're changing 239 continue 240 } 241 c, ok = meta.Commits[bEntry.Commit] 242 if !ok { 243 return fmt.Errorf("Broken commit history encountered when checking for isolated releases "+ 244 "while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db) 245 } 246 for rName, rEntry := range commitReleases { 247 if c.ID == rEntry.commit { 248 // The commit is also on another branch, so we're ok to delete the commit 249 tmp := commitReleases[rName] 250 tmp.safe = true 251 commitReleases[rName] = tmp 252 } 253 } 254 for c.Parent != "" { 255 c, ok = meta.Commits[c.Parent] 256 if !ok { 257 return fmt.Errorf("Broken commit history encountered when checking for isolated "+ 258 "releases while reverting in branch '%s' of database '%s'\n", branchRevertBranch, db) 259 } 260 for rName, rEntry := range commitReleases { 261 if c.ID == rEntry.commit { 262 // The commit is also on another branch, so we're ok to delete the commit 263 tmp := commitReleases[rName] 264 tmp.safe = true 265 commitReleases[rName] = tmp 266 } 267 } 268 } 269 } 270 271 // Create a list of would-be-isolated releases 272 for rName, rEntry := range commitReleases { 273 if rEntry.safe == false { 274 isolatedReleases = append(isolatedReleases, rName) 275 } 276 } 277 } 278 279 // If any tags or releases would be isolated, abort 280 if len(isolatedTags) > 0 || len(isolatedReleases) > 0 { 281 e := fmt.Sprint("You need to remove the following tags and releases before reverting to this " + 282 "commit:\n\n") 283 for _, j := range isolatedTags { 284 e = fmt.Sprintf("%s * tag '%s'\n", e, j) 285 } 286 for _, j := range isolatedReleases { 287 e = fmt.Sprintf("%s * release '%s'\n", e, j) 288 } 289 return errors.New(e) 290 } 291 292 // Count the number of commits in the updated branch 293 var commitCount int 294 listLen := len(commitList) - 1 295 for i := 0; i <= listLen; i++ { 296 commitCount++ 297 if commitList[listLen-i] == branchRevertCommit { 298 break 299 } 300 } 301 302 // Revert the branch 303 // TODO: Remove the no-longer-referenced commits (if any) caused by this revert 304 // * One alternative would be to leave them, and only clean up with with some kind of garbage collection 305 // operation. Even a "dio gc" to manually trigger it 306 newHead := branchEntry{ 307 Commit: branchRevertCommit, 308 CommitCount: commitCount, 309 Description: head.Description, 310 } 311 meta.Branches[branchRevertBranch] = newHead 312 313 // Copy the file from local cache to the working directory 314 var b []byte 315 b, err = ioutil.ReadFile(filepath.Join(".dio", db, "db", shaSum)) 316 if err != nil { 317 return err 318 } 319 err = ioutil.WriteFile(db, b, 0644) 320 if err != nil { 321 return err 322 } 323 err = os.Chtimes(db, time.Now(), lastMod) 324 if err != nil { 325 return err 326 } 327 328 // Save the updated metadata back to disk 329 err = saveMetadata(db, meta) 330 if err != nil { 331 return err 332 } 333 334 _, err = fmt.Fprintln(fOut, "Branch reverted") 335 return err 336 }