Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions 01-git/merge-mamaev/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Merge tool for merge trees in this git-like kvs

## usage
Usage: ../../cmd/main.go <target_hash> <merged_hash> <comment>"

## test
To run test, execute command from tests readme from its directory
87 changes: 87 additions & 0 deletions 01-git/merge-mamaev/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"merge/internal/mergeTools"
"os"
"sort"
"strings"
"time"
)

func main() {
const RFC822 = "02 Jan 06 15:04 MST"

if len(os.Args) != 4 {
fmt.Println("Usage: ./merge-mamaev/cmd/main.go <target_hash> <merged_hash> <comment>")
os.Exit(1)
}

target := os.Args[1]
merged := os.Args[2]
comment := os.Args[3]
commit := mergeTools.MergeDirectories(target, merged, strings.Split(comment, "\n"))

sort.Strings(commit.Parent)

parentHash, err := CreateBlob(commit.Parent)
if err != nil {
log.Fatal("Cannot create file in this directory " + err.Error())
os.Exit(1)
}

newFileContent := append(commit.Content, fmt.Sprintf(".parent:\t%s", parentHash))
sort.Strings(newFileContent)

treeHash, err := CreateBlob(newFileContent)
loc, err := time.LoadLocation("Europe/Moscow")
if err != nil {
log.Fatal("Cannot load TimeZone " + err.Error())
os.Exit(1)
}
date := time.Now().In(loc)
//To prevent inconsistent state(tree with .commit file, but old hash in its name) need to create new tree refers to old
commitContent := []string{"Root: " + treeHash, "Date: " + date.Format(RFC822) + "\n", comment}

commitHash, err := CreateBlob(commitContent)
commitTreeContent := make([]string, 0)
for _, line := range newFileContent {
if strings.HasPrefix(line, ".parent") {
continue
}
commitTreeContent = append(commitTreeContent, line)

}

commitTreeContent = append(commitTreeContent, fmt.Sprintf(".parent:\t%s", treeHash), fmt.Sprintf(".commit:\t%s", commitHash))

sort.Strings(commitTreeContent)

result, err := CreateBlob(commitTreeContent)
if err != nil {
log.Fatal("cannot create merge tree " + err.Error())
os.Exit(1)
}

fmt.Println("Merge successful. New tree hash:", result)
}

func CreateBlob(content []string) (string, error) {
hasher := sha256.New()
fileText := strings.Join(content, "\n")
hasher.Write([]byte(fileText))
hash := hex.EncodeToString(hasher.Sum(nil))

file, err := os.Create(hash)
if err != nil {
return "", err
}

file.Write([]byte(fileText))
defer file.Close()

return hash, nil
}
3 changes: 3 additions & 0 deletions 01-git/merge-mamaev/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module merge

go 1.23.4
251 changes: 251 additions & 0 deletions 01-git/merge-mamaev/internal/mergeTools/merge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package mergeTools

import (
"errors"
"fmt"
"log"
"os"
"strings"
)

func getParent(hash string) string {
file, err := os.ReadFile(hash)
if err != nil {
return ""
}
lines := strings.Split(string(file), "\n")
for _, line := range lines {
if !strings.HasPrefix(line, ".parent") {
continue
}
hash := strings.Split(line, "\t")[1]
return hash
}

return ""
}

func getDepth(hash string) int {
count := 0
parent := getParent(hash)
for parent != "" {
parent = getParent(parent)
count += 1
}
return count
}

func lca(oldHash, newHash string) string {
lenOld := getDepth(oldHash)
lenNew := getDepth(newHash)
for lenOld != lenNew {
if lenOld > lenNew {
oldHash = getParent(oldHash)
lenOld -= 1
} else {
newHash = getParent(newHash)
lenNew -= 1
}
}
for oldHash != newHash {
oldHash = getParent(oldHash)
newHash = getParent(newHash)
}

return oldHash
}

func MergeDirectories(targetHash, mergingHash string, comment []string) Commit {
mutual := lca(targetHash, mergingHash)
data, err := os.ReadFile(mutual)
if err != nil {
log.Fatal("Cannot read file of tree " + mutual)
os.Exit(1)
}
initial := make(map[string]string)
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, ".parent") || strings.HasPrefix(line, ".commit") || len(line) == 0 {
continue
}
pair := getEntryAndHash(line)
initial[pair.Entry] = pair.Hash

}

a := getUpcomingChanges(targetHash, initial)
b := getUpcomingChanges(mergingHash, initial)

mutualChanges, err := makeMutualChanges(&a, &b)
if err != nil {
log.Fatal(err)
os.Exit(1)
}
content := formCommit(&mutualChanges, initial)

commit := Commit{Parent: []string{targetHash, mergingHash}, Comment: comment, Content: content}
return commit
}

func makeMutualChanges(target, merged *Changes) (Changes, error) {
deletedHashMap := make(map[EntryAndHashPair]int)
for _, pair := range merged.Deleted {
deletedHashMap[pair] = 1
}
for _, pair := range target.Deleted {
deletedHashMap[pair] = 1
}
deleted := make([]EntryAndHashPair, 0, len(deletedHashMap))
for eahp := range deletedHashMap {
deleted = append(deleted, eahp)
}

changedConflicts := make([][]EntryAndHashPair, 0)
changedHashMap := make(map[string]string)
for _, pair := range merged.Changed {
changedHashMap[pair.Entry] = pair.Hash
}
for _, pair := range target.Changed {
if existingHash, ok := changedHashMap[pair.Entry]; ok {
if existingHash == pair.Hash {
//If both trees have equal changes, IMHO there is no conflict
continue
}
changedConflicts = append(changedConflicts, []EntryAndHashPair{
{Entry: pair.Entry, Hash: existingHash}, pair})
} else {
changedHashMap[pair.Entry] = pair.Hash
}
}
changed := make([]EntryAndHashPair, 0, len(changedHashMap))
for eahp := range changedHashMap {
changed = append(changed, EntryAndHashPair{eahp, changedHashMap[eahp]})
}

addedConflicts := make([][]EntryAndHashPair, 0)
addedHashMap := make(map[string]string)
for _, pair := range merged.Added {
addedHashMap[pair.Entry] = pair.Hash
}
for _, pair := range target.Added {
if existingHash, ok := addedHashMap[pair.Entry]; ok {
if existingHash == pair.Hash {
//If both trees have equal changes, IMHO there is no conflict
continue
}
addedConflicts = append(addedConflicts, []EntryAndHashPair{
{Entry: pair.Entry, Hash: existingHash}, pair})
} else {
addedHashMap[pair.Entry] = pair.Hash
}
}
added := make([]EntryAndHashPair, 0, len(addedHashMap))
for eahp := range addedHashMap {
added = append(added, EntryAndHashPair{eahp, addedHashMap[eahp]})
}

if len(addedConflicts)+len(changedConflicts) > 0 {
fmt.Println(buildConflictMessage(&changedConflicts, &addedConflicts))
return Changes{}, errors.New("Fatal error in merge, aborting...")
}

return Changes{Deleted: deleted, Added: added, Changed: changed}, nil
}

func buildConflictMessage(changed, added *[][]EntryAndHashPair) string {
message := "Failed to merge: there was conflicts in following files\n\n"
if len(*changed) != 0 {
message += "files/directories concurrently modified:\n\n"
for _, item := range *changed {
message += fmt.Sprintf("\t%s -- merging [%s...] -> target [%s...]\n", item[0].Entry, item[0].Hash[0:8], item[1].Hash[0:8])
}
}
if len(*added) != 0 {
message += "files/directories concurrently added:\n\n"
for _, item := range *added {
message += fmt.Sprintf("\t%s -- merging [%s...] -> target [%s...]\n", item[0].Entry, item[0].Hash[0:8], item[1].Hash[0:8])
}
}
return message
}

func formCommit(changes *Changes, initial map[string]string) []string {

for _, pair := range changes.Deleted {
delete(initial, pair.Entry)
}

for _, pair := range changes.Changed {
initial[pair.Entry] = pair.Hash
}

for _, pair := range changes.Added {
initial[pair.Entry] = pair.Hash
}

final := make([]string, 0)
for key := range initial {
final = append(final, fmt.Sprintf("%s\t%s", key, initial[key]))
}
return final
}

func getUpcomingChanges(hash string, previous map[string]string) Changes {
data, err := os.ReadFile(hash)
if err != nil {
os.Exit(1)
}
deleted := make([]EntryAndHashPair, 0)
visited := make(map[string]int)

changed := make([]EntryAndHashPair, 0)

added := make([]EntryAndHashPair, 0)
for _, line := range strings.Split(string(data), "\n") {
if strings.HasPrefix(line, ".parent") || strings.HasPrefix(line, ".commit") || len(line) == 0 {
continue
}
pair := getEntryAndHash(line)
if previousHash, ok := previous[pair.Entry]; ok {
visited[pair.Entry] = 1
if pair.Hash == previousHash {
continue
} else {
changed = append(changed, pair)
}
} else {
added = append(added, pair)
}
}

for pKey := range previous {
if _, ok := visited[pKey]; ok {
continue
} else {
deleted = append(deleted, EntryAndHashPair{Entry: pKey, Hash: previous[pKey]})
}
}

return Changes{Deleted: deleted, Changed: changed, Added: added}
}

func getEntryAndHash(line string) EntryAndHashPair {
parts := strings.Split(line, "\t")
return EntryAndHashPair{Entry: parts[0], Hash: parts[1]}
}

type EntryAndHashPair struct {
Entry string
Hash string
}

type Changes struct {
Deleted []EntryAndHashPair
Changed []EntryAndHashPair
Added []EntryAndHashPair
}

type Commit struct {
Parent []string
Comment []string
Content []string
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
init
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.parent: 63d648b2c27ee40ece8d540a8f686d2939289b5b5306503272732d60ca0b58e9
a: 1d71bf03f7d96d1b94d9aad04e2852e6dfc00b4669a7a976bfe138d8062f60b3
init: 06111c6797d5e5932b792d0c3afdf13bc912ba44af806058bbca4770d8915305
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
another content
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.parent: 01ba4719c80b6fe911b091a7c05124b64eeece964e09c058ef8f9805daca546b
init: 06111c6797d5e5932b792d0c3afdf13bc912ba44af806058bbca4770d8915305
5 changes: 5 additions & 0 deletions 01-git/merge-mamaev/tests/added_conflicts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# test
should trigger conflict error

# Command line
go run ../../cmd/main.go 08d44d60bd3c2f808a126faaedee05528dae6616bd9552b576a9c0b6ca79e54d cdd8158eceb414fd083017a95001c7579dcd56bd0bec24353c808d9cbf74d23e conflict
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.parent: 63d648b2c27ee40ece8d540a8f686d2939289b5b5306503272732d60ca0b58e9
a: 1c87b6727f523662df714f06a94ea27fa4d9050c38f4f7712bd4663ffbfdfa01
init: 06111c6797d5e5932b792d0c3afdf13bc912ba44af806058bbca4770d8915305
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
init
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
some content
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
another content
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.parent: f543ebc6593b491e4e8e0635a210b8b7f5428e99b9db82d5ad4d0f9617ff692e
a: 1c87b6727f523662df714f06a94ea27fa4d9050c38f4f7712bd4663ffbfdfa01
init: 06111c6797d5e5932b792d0c3afdf13bc912ba44af806058bbca4770d8915305
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.parent: f543ebc6593b491e4e8e0635a210b8b7f5428e99b9db82d5ad4d0f9617ff692e
a: 1d71bf03f7d96d1b94d9aad04e2852e6dfc00b4669a7a976bfe138d8062f60b3
init: 06111c6797d5e5932b792d0c3afdf13bc912ba44af806058bbca4770d8915305
4 changes: 4 additions & 0 deletions 01-git/merge-mamaev/tests/changed_conflicts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# test
should trigger conflict error (files concurrently modified)
# Command line
go run ../../cmd/main.go 1de076ccfb9852469cdcf1ee37d3d64c31895738a68a51b22ee665e8c4c08a31 84c0cb0fab5288dd86df9d45eaad9f19998afa3d235e4ae37d871e9a04e4881e conflict
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
initial content
Loading