আজকে একটা প্রজেক্টে কাজ করতে গিয়ে খুব স্পেসিফিক একটা সমস্যায় পড়লাম। দ্রুপাল কোরের একটা আপডেটের জন্য প্রজেক্টের কনফিগ ফাইলগুলোতে git diff
করে দেখি প্রায় ৪৫০র কাছাকাছি আপডেট হয়েছে! কিছু ইনভেস্টিগেশন করে জানা গেলো অধিকাংশ চেঞ্জই লাইন-পজিশন এর। মানে, ২ নাম্বার লাইন ৪ এ আসছে, ৩ নাম্বার ১ নাম্বারে গেছে, এমন। অনেকটা নিচের diff টার মত।
diff --git a/A.txt b/A.txt
--- a/A.txt
+++ b/A.txt
@@ -1,5 +1,5 @@
Hello World!
-I am excited about programming.
-
Thanks!
+
+I am excited about programming.
এখন আমাকে প্রত্যেকটা ফাইলে গিয়ে গিয়ে দেখতে হবে কোনটার পজিশন আর কোনটায় আসলেই চেঞ্জ হয়েছে। খুবই বোরিং একটা কাজ। তাই অলস হিসাবে আমার সুনাম ধরে রাখতে ভাবলাম, go দিয়ে একটা স্ক্রিপ্ট লিখে কাজটা অটোমেট করা যায় কি? কিছু ফান্ডামেন্টালও ক্লিয়ার হবে, আর সময়ও বাঁচবে(নাইস জোক, ডক ঘাটতে ঘাটতে সারাদিন গেছে !) তাই শুরু করে দিলাম!
কিভাবে করা যেতে পারে ?
প্রথমে ল্যাঙ্গুয়েজ স্পেসিফিক কিছু না ভেবে একটা টাস্ক লিস্ট বানালাম। তারপরে গিয়ে নাহয় লিস্টের একেকটা আইটেম go তে কিভাবে ইমপ্লিমেন্ট করা যায় সেটা জানা লাগবে।
একটা নির্দিষ্ট ফোল্ডারে গিয়ে
git diff
এই কমান্ডটা চালানো লাগবে। তাহলে আমরা কনসোলে বাstdout
এ যে আউটপুট পাবো সেটার উপরে আমাদের বাকি কাজ।stdout
এর ভ্যালুকে স্ট্রিং এ কনভার্ট করে সেটার উপরেregex
চালিয়ে আমাদের খুঁজে বের করতে হবে প্রত্যেকটা ফাইলের জন্য কি কি চেঞ্জ আসছে।চেঞ্জগুলো যদি শুধু পজিশনাল না হয়, তাহলে ওই ভ্যালুকে একটা
JSON
ফাইলে রাইট করতে হবে, যেখানে প্রত্যেকটা এন্ট্রি হবে এমন,
[
"filename": {
"+": [added changes],
"-": [deleted changes]
}
]
কিভাবে করার চেষ্টা করেছি
উপরের স্টেপ গুলো একে একে ইমপ্লিমেন্ট করা যাক।
git diff
এর আউটপুট স্ট্রিং এ কনভার্ট করে স্টোর করা
আমার প্রথম কাজ হল, যেখান থেকেই স্ক্রিপ্টটা রান করা হোক না কেন, সেটাতে একটা আর্গুমেন্ট পাঠানোর সুযোগ থাকবে। মানে আমি যদি লিখি, go run diff-to-json.go ~/Documents
, তাহলে প্রথমে প্রোগ্রামটা Documents
ফোল্ডারে যাবে এবং সেখানে গিয়ে git diff
রান করবে। go তে খুব সহজেই কমান্ডের আর্গুমেন্ট ক্যাচ করা যায় os.Args
দিয়ে।
// determine folder location from cmd arguments
loc := "."
if len(os.Args) > 1 && os.Args[1] != "" {
loc = os.Args[1]
}
আচ্ছা, আমি লোকেশন পেয়ে গেলাম, কোথায় গিয়ে আমাকে git diff
চালাতে হবে। এবার কাজ হল git diff
এর আউটপুটকে স্ট্রিং এ কনভার্ট করে স্টোর করা।
// generate diff as []byte
diff, err := generateDiff(loc)
if err != nil {
panic("Couldn't generate diff. Check the folder path.")
}
// create slice of lines from diff string
lines := strings.Split(string(diff), "\n")
if len(lines) == 0 {
panic("No diff found!")
}
তাহলে, lines
স্লাইসের মধ্যে আমাদের সব diff
লাইন বাই লাইন চলে আসলো। generateDiff
ফাংশনের মধ্যে কি হচ্ছে? শুধু দুইটা কমান্ড চালাচ্ছি os/exec
প্যাকেজ এর ফাংশন দিয়ে।
// generate git diff for loc folder
func generateDiff(loc string) ([]byte, error) {
args := []string{"cd", loc, "&&", "git", "diff"}
cmd := exec.Command("/bin/bash", "-c", strings.Join(args, " "))
return cmd.CombinedOutput()
}
সিনট্যাক্সের বর্ণনায় আর গেলাম না, ডকে খুব সুন্দর করে বলা আছে এমনিতেও।
regex
দিয়ে আমাদের প্রয়োজনীয় লাইনগুলো খুঁজে বের করা
আমাদের কাছে যেহেতু এখন lines
স্লাইসে প্রত্যেকটা লাইন আলাদা করে সাজানো আছে, আমরা সহজেই লুপ চালিয়ে আমাদের দরকারি লাইনগুলো আলাদা করে ফেলতে পারি। এখানে আমি কমেন্টে অনেকটাই বলে দেবার চেষ্টা করেছি কি করা হচ্ছে। কনফিউশান থাকলে জানাতে পারেন।
allDiffs := make(map[string]map[string][]string)
selectedDiffs := make(map[string]map[string][]string)
currentfilepath := ""
for _, line := range lines {
// select only if changed line or filepath line
// - negate: true
// + negate: false
// --- a/config/drupal/default/addtoany.settings.yml
// +++ a/config/drupal/default/addtoany.settings.yml
matched, err := regexp.MatchString("^[+-]([+-][+-])?[a-zA-Z _].*", line)
if err != nil {
panic(err)
}
if matched {
// select only filepath line
// --- a/config/drupal/default/addtoany.settings.yml
// +++ a/config/drupal/default/addtoany.settings.yml
if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") {
// extract filepath from full path
// config/drupal/default/addtoany.settings.yml
fullpath := regexp.MustCompile(` [ab]/`).Split(line, -1)
filepath := fullpath[len(fullpath)-1]
// init entry against the filepath
if _, ok := allDiffs[filepath]; !ok {
allDiffs[filepath] = make(map[string][]string)
currentfilepath = filepath
}
} else {
// if changed line, add according to add/delete sign
if string(line[0]) == "+" {
allDiffs[currentfilepath]["+"] = append(allDiffs[currentfilepath]["+"], line[1:])
}
if string(line[0]) == "-" {
allDiffs[currentfilepath]["-"] = append(allDiffs[currentfilepath]["-"], line[1:])
}
}
}
}
// after building map from diff, filter the map to get
// diff which are not positional changes. Meaning, if a
// statement was on line 10, and now on line 15, the diff
// for the file will be excluded.
for key, val := range allDiffs {
added := val["+"]
deleted := val["-"]
for _, va := range added {
found := false
for _, vd := range deleted {
if va == vd {
found = true
break
}
}
if found == false {
if _, ok := selectedDiffs[key]; !ok {
selectedDiffs[key] = map[string][]string{
"+": val["+"],
"-": val["-"],
}
}
}
}
}
// write selected diff lines to JSON
writeToJSON(loc, selectedDiffs)
এখানে আমি নেস্টেড লুপ চালিয়ে প্রথমে ম্যাপ বানিয়েছি স্লাইস থেকে। তার পরের নেস্টেড লুপে সেই ম্যাপ ফিল্টার করে বের করেছি কোনোগুলো চেঞ্জ শুধু পজিশনাল না। আরো এফিসিয়েন্টলি এটা করা যায়, কিন্তু অপ্টিমাইজ করার সময় কই ?!!
শেষমেষ, এখন আমাদের কাছে একটা ম্যাপ selectedDiffs
আছে যার প্রতেকটা কি আর ভ্যালু একদম আমাদের JSON
এর স্ট্রাকচারের মত! সেটাকে .json
ফাইলে কনভার্ট করতে আমরা কল করেছি writeToJSON
ফাংশনটা।
ফাইলে JSON
রাইট করা
go এর একটা জিনিস আমার খুব ভালো লেগেছে, তা হল এর সিমপ্লিসিটি। লাইব্রেরী ফাংশনগুলো সিম্পল আর এক্সপ্রেসিভ, ফলে খুব সহজেই অনেক ভারী কাজ করা যায়!
// writes selected diffs to a json file in loc folder
func writeToJSON(loc string, selectedDiffs map[string]map[string][]string) {
// create or overwrite previous file
file, err := os.OpenFile(loc+"/result.json", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
panic(err)
}
// close file when writeToJSON ends
defer file.Close()
// convert map to JSON
jsonDiff, err := json.Marshal(selectedDiffs)
if err != nil {
panic(err)
}
// write to file
if _, err := file.WriteString(string(jsonDiff)); err != nil {
panic(err)
}
}
os
আর json
প্যাকেজ দিয়ে একটা ফাইল ওপেন করে তাতে কিছু রাইট করা হচ্ছে । Marshal
ব্যাপারটা আমি নিজেও ক্লিয়ারলি বুঝিনি ঠিক, কিন্তু কাজ চালানোর মত নলেজ ডকেই আছে!
ফলাফল
সব ঠিকঠাক চললে আর্গুমেন্টে যে ফোল্ডার দেয়া হয়েছে সেখানে results.json
নামে একটা ফাইল তৈরি হবে, শুরুতে দেয়া স্ট্রাকচারের মত। পুরো কোড পাওয়া যাবে এই ফাইলে।
শেষকথা
স্ক্রিপটিং এর জন্য আমি এতদিন ব্যাশ আর পাইথনই ব্যবহার করতাম । কিন্তু go হয়তো আমাকে পুরোপুরি কনভার্ট করে ফেলবে। এর পাওয়ারফুল লাইব্রেরী আর সিম্পল এর মধ্যে গর্জিয়াস এপিআই ব্যবহার করে অখুশি হওয়া কঠিন! আজকে এটুকুই। কোনো মতামত থাকলে জানাবেন অবশ্যই। এদ্দুর পড়ার জন্য ধন্যবাদ।