github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/checkoutindex.go (about) 1 package git 2 3 import ( 4 "bufio" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strconv" 11 "strings" 12 ) 13 14 // CheckoutIndexOptions represents the options that may be passed to 15 // "git checkout-index" 16 type CheckoutIndexOptions struct { 17 UpdateStat bool 18 19 Quiet bool 20 Force bool 21 All bool 22 23 NoCreate bool 24 25 Prefix string 26 27 // Stage not implemented 28 Stage string // <number>|all 29 30 Temp bool 31 32 // Stdin implies checkout-index with the --stdin parameter. 33 // nil implies it wasn't passed. 34 Stdin io.Reader // nil implies no --stdin param passed 35 NullTerminate bool 36 } 37 38 // Performs CheckoutIndex on the Stdin io.Reader from opts, with the git index 39 // passed as a parameter. 40 func CheckoutIndexFromReaderUncommited(c *Client, idx *Index, opts CheckoutIndexOptions) error { 41 if opts.Stdin == nil { 42 return fmt.Errorf("Invalid Reader for opts.Stdin") 43 } 44 reader := bufio.NewReader(opts.Stdin) 45 46 var delim byte = '\n' 47 if opts.NullTerminate { 48 delim = 0 49 } 50 51 var f File 52 for s, err := reader.ReadString(delim); err == nil; s, err = reader.ReadString(delim) { 53 f = File(strings.TrimSuffix(s, string(delim))) 54 55 e := CheckoutIndexUncommited(c, idx, opts, []File{f}) 56 if e != nil { 57 fmt.Fprintln(os.Stderr, e) 58 } 59 } 60 return nil 61 } 62 63 // Performs a CheckoutIndex on the files read from opts.Stdin 64 func CheckoutIndexFromReader(c *Client, opts CheckoutIndexOptions) error { 65 idx, err := c.GitDir.ReadIndex() 66 if err != nil { 67 return err 68 } 69 return CheckoutIndexFromReaderUncommited(c, idx, opts) 70 } 71 72 // Handles checking out a file when --temp is specified on the command line. 73 func checkoutTemp(c *Client, entry *IndexEntry, opts CheckoutIndexOptions) (string, error) { 74 // I don't know where ".merged_file" comes from 75 // for checkout-index, but it's what the real 76 // git client seems to use for a prefix.. 77 tmpfile, err := ioutil.TempFile(".", ".merge_file_") 78 if err != nil { 79 return "", err 80 } 81 defer tmpfile.Close() 82 83 obj, err := c.GetObject(entry.Sha1) 84 if err != nil { 85 return "", err 86 } 87 _, err = tmpfile.Write(obj.GetContent()) 88 if err != nil { 89 return "", err 90 } 91 92 os.Chmod(tmpfile.Name(), os.FileMode(entry.Mode)) 93 return tmpfile.Name(), nil 94 } 95 96 // Checks out a given index entry. 97 func checkoutFile(c *Client, entry *IndexEntry, opts CheckoutIndexOptions) error { 98 f, err := entry.PathName.FilePath(c) 99 if err != nil { 100 return err 101 } 102 f = File(opts.Prefix) + f 103 if f.Exists() && !opts.Force { 104 if !opts.Quiet { 105 return fmt.Errorf("%v already exists, no checkout", entry.PathName.String()) 106 } 107 return nil 108 } 109 110 obj, err := c.GetObject(entry.Sha1) 111 if err != nil { 112 return err 113 } 114 if !opts.NoCreate { 115 fmode := os.FileMode(entry.Mode) 116 if f.Exists() && f.IsDir() { 117 if err := os.RemoveAll(f.String()); err != nil { 118 return err 119 } 120 } 121 122 p := filepath.Dir(f.String()) 123 if f := File(p); !f.Exists() { 124 if err := os.MkdirAll(p, 0777); err != nil { 125 return err 126 } 127 } else if !f.IsDir() { 128 // FIXME: This shouldn't be required, this 129 // should be handled by being returned by 130 // ls-files -k before we get to this point. 131 if err := os.Remove(f.String()); err != nil { 132 return err 133 } 134 if err := os.MkdirAll(p, 0777); err != nil { 135 return err 136 } 137 } 138 err := ioutil.WriteFile(f.String(), obj.GetContent(), fmode) 139 if err != nil { 140 return err 141 } 142 os.Chmod(f.String(), os.FileMode(entry.Mode)) 143 } 144 145 // Update the stat information, but only if it's the same 146 // file name. We only change the mtime, and ctime because the only 147 // other thing we track is the file size, and that couldn't 148 // have changed. 149 // Don't change the stat info if there's a prefix, because 150 // if we checkout out into a prefix, it means we haven't 151 // touched the index. 152 if opts.Prefix == "" && opts.UpdateStat { 153 if err := entry.RefreshStat(c); err != nil { 154 return err 155 } 156 } 157 return nil 158 } 159 160 // Same as "git checkout-index", except the Index is passed as a parameter (and 161 // may not have been written to disk yet). You likely want CheckoutIndex instead. 162 // 163 // (This is primarily for read-tree to be able to update the filesystem with the 164 // -u parameter.) 165 func CheckoutIndexUncommited(c *Client, idx *Index, opts CheckoutIndexOptions, files []File) error { 166 if opts.All { 167 files = make([]File, 0, len(idx.Objects)) 168 for _, entry := range idx.Objects { 169 f, err := entry.PathName.FilePath(c) 170 if err != nil { 171 return err 172 } 173 files = append(files, f) 174 } 175 } 176 177 killed, err := LsFiles(c, LsFilesOptions{Killed: true}, files) 178 if err != nil { 179 return err 180 } 181 if len(killed) > 0 { 182 if !opts.Force { 183 msg := "" 184 for i, path := range killed { 185 if i > 0 { 186 msg += "\n" 187 188 } 189 f, err := path.PathName.FilePath(c) 190 if err != nil { 191 return err 192 } 193 msg += fmt.Sprintf("fatal: cannot create directory at '%v': File exists", f) 194 } 195 return fmt.Errorf("%v", msg) 196 } else { 197 for _, file := range killed { 198 f, err := file.PathName.FilePath(c) 199 if err != nil { 200 return err 201 } 202 203 if err := os.RemoveAll(f.String()); err != nil { 204 return err 205 } 206 } 207 } 208 } 209 210 var stageMap map[IndexStageEntry]*IndexEntry 211 if opts.Stage == "all" { 212 // This is only used if stage==all, but we don't want to reallocate 213 // it every iteration of the loop, so we just define it as a var 214 // and let it stay nil unless stage=="all" 215 stageMap = idx.GetStageMap() 216 } 217 var delim byte = '\n' 218 if opts.NullTerminate { 219 delim = 0 220 } 221 222 for _, file := range files { 223 fname := File(file) 224 indexpath, err := fname.IndexPath(c) 225 if err != nil { 226 if !opts.Quiet { 227 fmt.Fprintf(os.Stderr, "%v\n", err) 228 } 229 continue 230 } 231 232 if opts.Stage == "all" { 233 if _, ok := stageMap[IndexStageEntry{indexpath, Stage0}]; ok { 234 if !opts.Quiet { 235 // This error doesn't make any sense to me, 236 // but it's what the official git client says when 237 // you try and checkout-index --stage=all a file that isn't 238 // in conflict. 239 fmt.Fprintf(os.Stderr, "git checkout-index: %v does not exist at stage 0\n", indexpath) 240 } 241 continue 242 } 243 if stg1, s1ok := stageMap[IndexStageEntry{indexpath, Stage1}]; s1ok { 244 name, err := checkoutTemp(c, stg1, opts) 245 if err != nil { 246 fmt.Fprintln(os.Stderr, err) 247 } 248 fmt.Print(name, " ") 249 } else { 250 fmt.Print(". ") 251 } 252 if stg2, s2ok := stageMap[IndexStageEntry{indexpath, Stage2}]; s2ok { 253 name, err := checkoutTemp(c, stg2, opts) 254 if err != nil { 255 fmt.Fprintln(os.Stderr, err) 256 } 257 fmt.Print(name, " ") 258 } else { 259 fmt.Print(". ") 260 } 261 if stg3, s3ok := stageMap[IndexStageEntry{indexpath, Stage3}]; s3ok { 262 name, err := checkoutTemp(c, stg3, opts) 263 if err != nil { 264 fmt.Fprintln(os.Stderr, err) 265 } 266 fmt.Printf("%s\t%s%c", name, stg3.PathName, delim) 267 } else { 268 fmt.Printf(".\t%s%c", stg3.PathName, delim) 269 } 270 continue 271 } 272 273 for _, entry := range idx.Objects { 274 if entry.PathName != indexpath { 275 continue 276 } 277 278 if !opts.Temp && !opts.Force && opts.Prefix == "" && entry.PathName.IsClean(c, entry.Sha1) { 279 // don't bother checkout out the file 280 // if it's already clean. This makes us less 281 // likely to avoid GetObject have an error 282 // trying to read from a packfile (which isn't 283 // supported yet.) 284 // We don't check this if there's a prefix, since it's not checking out 285 // into the same location as the index. 286 // FIXME: This should use stat information, not hash 287 // the whole file. 288 continue 289 } 290 291 switch opts.Stage { 292 case "": 293 if entry.Stage() == Stage0 { 294 var name string 295 if opts.Temp { 296 name, err = checkoutTemp(c, entry, opts) 297 if name != "" { 298 fmt.Printf("%v\t%v%c", name, entry.PathName, delim) 299 } 300 } else { 301 err = checkoutFile(c, entry, opts) 302 } 303 } else { 304 return fmt.Errorf("Index has unmerged entries. Aborting.") 305 } 306 case "1", "2", "3": 307 stg, _ := strconv.Atoi(opts.Stage) 308 if entry.Stage() == Stage(stg) { 309 var name string 310 311 if opts.Temp { 312 name, err = checkoutTemp(c, entry, opts) 313 if name != "" { 314 fmt.Printf("%v\t%v%c", name, entry.PathName, delim) 315 } 316 317 } else { 318 err = checkoutFile(c, entry, opts) 319 } 320 } 321 default: 322 return fmt.Errorf("Invalid stage: %v", opts.Stage) 323 } 324 if err != nil { 325 fmt.Fprintln(os.Stderr, err) 326 } 327 328 } 329 } 330 331 if opts.UpdateStat { 332 f, err := c.GitDir.Create(File("index")) 333 if err != nil { 334 return err 335 } 336 defer f.Close() 337 return idx.WriteIndex(f) 338 } 339 return nil 340 } 341 342 // CheckoutIndex implements the "git checkout-index" subcommand of git. 343 func CheckoutIndex(c *Client, opts CheckoutIndexOptions, files []File) error { 344 if len(files) != 0 && opts.All { 345 return fmt.Errorf("Can not mix --all and named files") 346 } 347 348 idx, err := c.GitDir.ReadIndex() 349 if err != nil { 350 return err 351 } 352 if opts.Stdin == nil { 353 return CheckoutIndexUncommited(c, idx, opts, files) 354 } else { 355 if len(files) != 0 { 356 return fmt.Errorf("Can not mix --stdin and paths on command line") 357 } 358 return CheckoutIndexFromReaderUncommited(c, idx, opts) 359 } 360 }