`git history` कमांड
(lalitm.com)- Git 2.54 और 2.55 में पेश किया गया प्रयोगात्मक
git historyकमांड, मौजूदा Git workflow को बनाए रखते हुए पुराने commits में बदलाव और उनसे निकली branches के पुनर्गठन को स्वचालित करता है fixupstaged बदलावों को किसी पुराने commit में मिलाता है, फिर उस commit से निकली सभी local branches को नए commit hash के अनुसार अपने-आप rebase करता हैrewordपुराने commit message को बदलता है, औरsplitcommit के बदलावों को hunk स्तर पर बांटकर दो commits में विभाजित करता है, फिर बाद के commits और branches को दोबारा व्यवस्थित करता है- ये तीनों subcommands atomic processing के साथ काम करते हैं, जो conflict की संभावना वाले कामों को मना करके working tree को बीच में टूटे हुए हाल में जाने से बचाते हैं, लेकिन merge commits वाली history में काम नहीं करते
- हालांकि यह
jjकी first-class conflict handling, operation log-आधारित undo, या working copy की commit modeling जैसी सुविधाएं नहीं देता, फिर भी बिना अलग install के Git core distribution में कईjj-style history editing features इस्तेमाल किए जा सकते हैं
मौजूदा Git history editing असुविधाजनक क्यों है
- कई बदलावों को parallel में संभालने पर branches और commits के बीच लगातार आना-जाना पड़ता है, और
rebase -iके दौरान working tree अधूरी स्थिति में फंस सकती है - Git के विकल्प
jjको अक्सर इस समस्या के समाधान के रूप में बताया जाता है, लेकिन डेढ़ साल तक हर 3 महीने में कुछ दिनों के लिए आजमाने पर भी अंततः वापस मौजूदा Git workflow पर लौटना पड़ा git historyGit core distribution में शामिल एक experimental command है, इसलिए बिना अलग tool install किए इसे आजमाया जा सकता है- subcommands तीन हैं:
fixup,reword,split
fixup: पुराने commit और उससे निकली branches को साथ में ठीक करना
git history fixupपुराने commit की गलती को ठीक करता है और उस commit से निकली branches को अपने-आप rebase करता है- सामान्य सुधार प्रक्रिया की तरह
git addसे बदलाव staged करने के बादgit history fixup <commit>चलाने पर staged बदलाव target commit में मिल जाते हैं - यह
git commit --fixupऔर autosquash rebase के संयोजन जैसा है, लेकिन फर्क यह है कि यह target commit को शामिल करने वाली दूसरी branches को भी update करता है - जहां
git rebase --update-refsकेवल मौजूदा rebase range के अंदर वाले refs को move करता है, वहींgit historytarget commit से निकली सभी local branches को ढूंढकर दोबारा लिखता है- जरूरत हो तो update का दायरा केवल current branch तक सीमित किया जा सकता है
- merge commits वाली history में यह काम नहीं करता, इसलिए कुछ Git usage patterns में इसे लागू करना मुश्किल है
Bमें सुधार मिलाने पर नए hash वालाB*बनता है, और उसके ऊपर केCऔरDभी क्रमशःC*,D*के रूप में फिर से बनाए जाते हैंfeat-1औरfeat-2के branch tips भी नए commits के साथ आगे बढ़ जाते हैं
atomic processing और conflict सीमाएं
- तीनों subcommands पूरी तरह atomically process होते हैं, इसलिए working tree आधी-टूटी स्थिति में नहीं छूटती
- atomicity इस तरह सुनिश्चित की जाती है कि जिन operations से conflict हो सकता है, उन्हें शुरू से ही मना कर दिया जाता है
- इस वजह से यह
jjकी तुलना में कार्यात्मक रूप से सीमित है, क्योंकिjjconflicts को first-class object की तरह संभालता है और rebase के दौरान conflict state को बनाए रखकर बाद में resolve करने देता है git historyकी सीमाएं जानबूझकर लगाई गई हैं ताकि history rewriting, state preservation operation में न बदल जाए- अगर भविष्य में Git first-class conflicts को support करे, तो ये सीमाएं हटाई जा सकती हैं
reword: working tree छुए बिना commit message बदलना
git history rewordपुराने commit message को बदलता है और उसके ऊपर के commits व branches को अपने-आप फिर से व्यवस्थित करता हैgit history reword <commit>चलाने पर editor में पुराना message खुलता है; उसे बदलकर save करने पर बाद का commit stack नए hash के हिसाब से दोबारा बन जाता है- iterative development के दौरान design बदल जाने पर पुराने commit message को मौजूदा structure के मुताबिक ठीक करने में यह उपयोगी है
- tree content की जगह सिर्फ commit message बदलने को छोड़ दें तो यह
fixupकी तरह ही काम करता है rewordindex या working tree को छुए बिना सिर्फ commit graph बदलता है- checkout न की गई branch के commits भी मौजूदा काम में बाधा डाले बिना बदले जा सकते हैं
Bका message बदलने पर hash बदल जाता है, इसलिए बाद वालाC,C*के रूप में फिर बनता है औरfeat-1भी नए commit के साथ आगे बढ़ता है
split: एक commit को दो हिस्सों में बांटना
git history splitकिसी commit के बदलावों को interactive तरीके से चुनकर दो commits में बांटता हैgit history split <commit>चलाने पर target commit के बदलावों को hunk स्तर पर चुनने का prompt खुलता है- चुने गए और बनाए रखे गए hunks पहला commit बनाते हैं
- बाकी hunks दूसरे commit में जाते हैं
- यह
git add -pजैसा है, लेकिनgit rebaseके साथ जटिल प्रक्रिया जोड़ने की जरूरत नहीं पड़ती - अगर असंबंधित दो बदलावों वाले
Bको बांटा जाए, तोB1औरB2बनते हैं, और बाद वालाCइन दोनों commits के ऊपरC*के रूप में फिर से बनाया जाता है - तीनों commands में इसका उपयोग सबसे ज्यादा विशेष परिस्थितियों में होता है, लेकिन जब commit को सचमुच बांटना हो तब यह बहुत उपयोगी है
jj से फीचर अंतर
git history,jjके पूरे working model का विकल्प नहीं है- इसके उलट,
git historyमौजूदा Git के भीतर पुराने commits में सुधार, message बदलना, commit split करना और derived branches को update करना सपोर्ट करता है, जिससेjjके कुछ फायदे मौजूदा workflow में लाए जा सकते हैं - भविष्य में इन सीमाओं को ढीला करने की संभावना खुली है, इसलिए आने वाली Git releases में इसमें और सुधार की गुंजाइश है
1 टिप्पणियां
Hacker News की राय
Git असुविधाजनक लगता था, लेकिन मुफ्त में उपलब्ध Pro Git पुस्तक के पहले तीन अध्याय पढ़ने के बाद स्थिति बेहतर हुई। यह Git के अंदरूनी कामकाज के बारे में एक शानदार mental model देता है, और अच्छा हो या बुरा, Git का user interface उसकी internal structure को सीधे दर्शाता है, इसलिए इसे समझने के बाद सब कुछ समझ में आने लगा
git checkoutbranch बदलते समय उन बदलावों को नहीं खोता जो अभी Git में नहीं गए हैं, लेकिन किसी फ़ाइल का दूसरा version restore करते समय बदलावों को वापस लेने के लिए डिज़ाइन किया गया है—यह internal structure को सीधे कैसे दर्शाता है?इससे अलग, भले ही आपको अंदरूनी तौर पर चाहिए क्या और कौन-सा command चाहिए यह पता हो, फिर भी सही command-line syntax ढूँढना मुश्किल बना देने वाली CLI की असंगतता भी Git को इस्तेमाल करना कठिन बनाती है
git commit -am “Changes”काफ़ी नहीं हैgit rebase --abortमौजूद है, और rebase से पहले tag बनाकर बाद मेंgit reset --hard $set_tagसे वापस भी जाया जा सकता है। पिछली state गायब नहीं होती, इसलिए interactive rebase से डरने की कोई वजह नहीं हैtempयाbefore-rebaseनाम की branches अक्सर बना लेता हूँ। वे tag की तरह काम करती हैं, लेकिन branch को आगे-पीछे करना ज़्यादा आसान होता है, इसलिए बड़ेrebase -iसे पहले मैं आम तौर परgit branch before-rebase/some-featureचलाता हूँवास्तव में
git reset before-rebaseकी ज़रूरत मुझे बहुत कम पड़ी है, लेकिन rebase के बाद की branch सेgit log -p before-rebaseकी तुलना करके conflict resolution के दौरान हुए अनचाहे बदलाव जाँचने का काम मैंने कई बार किया हैgit reflogइस्तेमाल करते हैं?jj undoसे आसान कुछ नहीं हो सकताgit rebaseटाइप करने के बाद लगभग आधी बार मैं तुरंत--abortटाइप करता हूँमैं खुद को coder या programmer नहीं मानता, लेकिन Git सीखने से मुझे लगता है कि मेरी सोच में व्यवस्था की सुपरपावर आ गई। मैं इसका उपयोग वेबसाइट, design projects, electronics, composition, personal knowledge store, remote management scripts, config management, code snippets जैसी अनगिनत चीज़ों में करता हूँ, और भले यह परफ़ेक्ट न हो, फिर भी साथ काम करने वाले हर व्यक्ति को इसे सीखने की सलाह देता हूँ
David Allen की 『Getting Things Done』 में कहा गया है कि विचारों को इकट्ठा और व्यवस्थित करने के लिए एक विश्वसनीय system चाहिए। मैं tasks, projects और lists के लिए OmniFocus का उपयोग करता हूँ, लेकिन कंप्यूटर पर बनाए गए software और कुछ data को व्यवस्थित और सुरक्षित रखने के लिए मैं Git पर भरोसा करता हूँ
मुझे सिद्धांत रूप में ये commands बहुत पसंद हैं और
jjके equivalent commands भी मैं हमेशा इस्तेमाल करता हूँ, लेकिनgit historyसे बदले गए commits पर signature करने का तरीका मुझे नहीं मिला। Manual और Git source code देखने पर भी, अगर मौजूदा GPG-signed commit कोgit history reword HEAD~से बदला जाए तो signatures सब गायब हो जाते हैंCommit signatures बनाए रखना चाहता था, इसलिए आखिरकार मुझे पारंपरिक
git rebase -iपर लौटना पड़ाgit historyको experimental feature कहा गया है, इसलिए इसे bug के रूप में report करना उचित होगानए कमांड के बिना भी मैं 10 साल से ज़्यादा समय से local Git commit history को बिना किसी बड़ी मुश्किल के काफी हद तक बदलता रहा हूँ। मुझे पूरा यक़ीन है कि Git और rebase के मुश्किल लगने की बड़ी वजह user interface की समस्या है, क्योंकि सही command-line options और उनके व्यवहार को खुद समझना पड़ता है
Windows में TortoiseGit का rebase dialog शक्तिशाली होने के साथ इस्तेमाल में भी आसान था। Commit log में base commit पर right-click करके
"Rebase onto this"चुनना होता है, और https://tortoisegit.org/docs/tortoisegit/tgit-dug-rebase.htm... की तरह commits का क्रम बदलना, split, squash, edit करना साफ़ तौर पर दिखता है, और बीच में cancel करने पर यह मूल स्थिति में लौट आता हैहालांकि यह shell extension शैली पर आधारित है, इसलिए clicks कुछ ज़्यादा लगते हैं; tabs वाला standalone app होता तो बेहतर होता, लेकिन यह शायद TortoiseSVN से आई design principle है। मैंने Git Extensions भी थोड़ी देर इस्तेमाल किया, लेकिन या तो अक्सर इस्तेमाल होने वाले features नहीं मिले, या मैं पहले से TortoiseGit का इतना अभ्यस्त था कि उस पर टिक नहीं पाया। Log screen https://tortoisegit.org/docs/tortoisegit/tgit-dug-showlog.ht... में देखी जा सकती है
My Computerजैसी special objects जोड़ सकता था। मेरी समझ में यह XP पर अपने शिखर पर था, और Vista से Microsoft ने इसे ऐसे बदलना शुरू किया कि इसे extend करना कठिन हो गयालेकिन VC++6 का कोई menu सामान्य list की जगह folder जैसे shell object बनाकर file-open dialog में क्यों दिखाता था, यह आज भी समझ नहीं आता। वह project में components जोड़ने वाला wizard था, और dialog खुले रहने के दौरान मैं उस नकली folder को
My Computerमें ढूँढा करता थाfixupपहले सेgit commit --fixupया उससे भी पहलेgit rebase -iसे संभव था, औरrewordभीgit rebase -iसे किया जा सकता थालगता है कि हर commit पर कौन-सा action करना है यह तय करने के लिए file edit करने की असुविधा पर काफी feedback जमा हुआ, इसलिए मौजूदा functionality को wrap करने वाला higher-level command जोड़ा गया
Command-line tools में अगला command चलाते ही पिछले output तक पहुँचना मुश्किल हो जाता है।
git logमें देखा गया commit अगर cherry-pick करना हो, तो उसका hash terminal में बचा न हो तो सिर्फ़ याददाश्त से टाइप नहीं किया जा सकता; लेकिन stateful interface कई interactions को parallel बनाए रखता है और पुरानी जानकारी को संभालकर रखता हैGit में याद रखने लायक state information बहुत ज़्यादा होती है, जैसे current branch, पिछली branch, modified files, unpushed commits वगैरह। अगर shell plugin की branch display और autocomplete, कई tabs/window splits, tabs के बीच copy, autocomplete search जैसे आसपास के tools की आदत न हो, तो Git का इस्तेमाल बहुत अप्रिय हो सकता है
मेरे हिसाब से
jjकी असली कमजोरी सिर्फ़ Git integration है। यह file sets, revision sets और templates के लिए domain-specific language को औपचारिक रूप देता है और settings तथा CLI में उसे लगातार एक समान तरीके से इस्तेमाल करता है, साथ ही internal structure को सीधे उजागर किए बिना शक्तिशाली concepts को interface के रूप में देता हैलेकिन यह इतना flexible है कि Git interoperability काफी हद तक conventions और discipline पर निर्भर करती है, और
jjकी लगभग हर state को Git में व्यक्त किया जा सकता है, इसलिए remote repository को बिगाड़ देना आसान है। इसे ठीक करने के लिए Git की गहरी समझ चाहिएअगर नया standalone development project हो, तो
jjआज़माने लायक है। अनुकूल नज़रिए से देखें तो यह Subversion की तुलना में शुरुआती Git जैसा महसूस होता है; और अगर पहलेjjके अपने तरीके की आदत पड़ जाए, तो बाद की कठिन Git integration के लिए भी शायद कुछ बेहतर तैयारी हो जाती है। संबंधित संदर्भ https://news.ycombinator.com/item?id=48903054 में हैjjके लिए Git के अलावा दूसरे backends भी हैं, लेकिन इस समय production environment के लिए उपयुक्त बताया गया backend सिर्फ़ Git backend है। Git integration के बिनाjjअभी वास्तविकता से ज़्यादा एक concept जैसा हैमुझे
jjऔर उसका version control mental model पसंद है, इसलिए मैं इसे इस्तेमाल करता रहूँगा, लेकिन कभी-कभी काम पूरा करने के लिए Git तक नीचे उतरना पड़ता है, इसलिए यह किसी नए configuration management system से ज़्यादा Git के ऊपर एक सुविधाजनक wrapper जैसा लगता है। अगर Git,jjworkflow को support करने वाले commands जोड़ दे, तोjjको अलग से install करने की वजह ढूँढना मुश्किल होगाgit history fixupका सभी descendant branches को अपने-आप rebase कर देने वाला तरीका मेरे workflow से मेल नहीं खाता। मैं feature branch के हर version कोmyfeature.1,myfeature.2की तरह सुरक्षित रखता हूँ, ताकि नए version में व्यवहार बदलने पर पुराना version ढूँढ सकूँ;git reflogइसका विकल्प नहीं हो सकताउदाहरण के लिए, अगर समस्या
myfeature.58औरmyfeature.59के बीच आई हो, और हर feature में 15 commits हों, तोgit range-diff myfeature.58~15..myfeature.58 myfeature.59~15..myfeature.59से, base commit अलग होने पर भी, दोनों versions का commit-दर-commit अंतर देखा जा सकता है। इसलिए मैं किसी खास commit को शामिल करने वाली सभी branches के automatic rebase नहीं चाहता, और पुराने versions पर tags इस्तेमाल करने का तरीका भी मुझे खास जँचा नहींआधुनिक Git, अक्सर इस्तेमाल होने वाले
git rebase --interactiveके तीन कामों को कम friction वाले अलग commands के रूप में लागू करता है, और लगता है कि ये केवल तब काम करते हैं जब conflicts न होंgit historyवाकई कितना उपयोगी है, इस पर मुझे संदेह है। मैं interactive rebase में commits को दृश्य रूप से खिसकाना पसंद करता हूँ, और मैं गलती से ऐसे orphaned commit hash कोgit historyका target नहीं बनाना चाहूँगा जो current local branch से गायब हो चुका होमुझे समझ नहीं आता कि Git history को पूरी तरह चमकाने-सँवारने में इतना ज़ोर क्यों लगाया जाता है। वैसे भी कोई अलग-अलग commits दोबारा पढ़ता नहीं, इसलिए merge से पहले सबको squash कर दो
"oops"और"fix"जैसे commits से भरी हो तो मैं उसे तुरंत reject कर देता हूँcommits काम की कहानी बताते हैं, इसलिए reviewer को intent समझने में अतिरिक्त मेहनत न करनी पड़े, इसके लिए पढ़ने लायक commit history देना चाहिए
अगर codebase और features बहुत छोटे हों तो सब कुछ squash करना ठीक हो सकता है, लेकिन मैं ऐसा तरीका पसंद करता हूँ जिसमें वास्तविक बदलावों का flow दिखे और commits को चुनिंदा तौर पर cherry-pick या revert किया जा सके
मेरी समझ यह थी कि अगर अभी
Dcheckout किया हुआ है औरBकोB'में बदला जाए, तोgit rebase --update-refsrebase range के बाहर वालेEको वैसे ही छोड़ देगा और सिर्फ़B' → C' → D'बनाएगा, जबकि पुरानाB → Eबना रहेगादूसरी ओर, क्या
git history fixupEको भीE'में update करता है? अगर हाँ, तो क्या दशकों से आदत बनेgit rebaseके साथ भी सभी descendant branches को update करने का कोई तरीका है?Eको जस-का-तस नहीं छोड़ा जा सकता। मैंने खुद--update-refsसीधे इस्तेमाल नहीं किया है, लेकिन docs के अनुसार देखें तोE'तक बनने वाला तीसरा graph ही result होना चाहिए, यानी जो behavior आप चाहते हैं वहgit rebaseमें पहले से संभव है